diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 437dbfd48..c80b956ac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: tests/fixtures|docs/examples repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.15.0 + rev: v2.18.1 hooks: - id: pyupgrade args: [--py37-plus] @@ -11,7 +11,7 @@ repos: hooks: - id: reorder-python-imports - repo: https://github.com/ambv/black - rev: 21.5b0 + rev: 21.5b1 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 @@ -26,7 +26,7 @@ repos: args: ["--suppress-none-returning"] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.0.1 hooks: - id: trailing-whitespace - id: end-of-file-fixer diff --git a/docs/json.rst b/docs/json.rst index 50058f8b8..3185c83e9 100644 --- a/docs/json.rst +++ b/docs/json.rst @@ -2,8 +2,22 @@ JSON Binding ============ -Binding JSON lacks a bit in features and for edge cases with wildcards and derived -types doing roundtrip conversions is not always possible. +All binding modules rely on a :class:`~xsdata.formats.dataclass.context.XmlContext` +instance to cache marshalling information. + +It's recommended to either reuse the same parser/serializer instance or reuse the +context instance. + +.. code-block:: + + from xsdata.formats.dataclass.context import XmlContext + from xsdata.formats.dataclass.parsers import JsonParser + from xsdata.formats.dataclass.serializers import JsonSerializer + + context = XmlContext() + parser = JsonParser(context=context) + serializer = JsonSerializer(context=context) + .. testsetup:: * @@ -91,6 +105,28 @@ From json path BookForm(author='Hightower, Kim', title='The First Book', genre='Fiction', price=44.95, pub_date=XmlDate(2000, 10, 1), review='An amazing story of nothing.', id='bk001', lang='en') +Ignore unknown properties +------------------------- + +.. doctest:: + + >>> from tests.fixtures.books import * # Import all classes + >>> from xsdata.formats.dataclass.parsers.config import ParserConfig + ... + >>> config = ParserConfig( + ... fail_on_unknown_properties=False, + ... ) + >>> json_string = """{ + ... "author": "Hightower, Kim", + ... "unknown_property": "I will fail" + ... }""" + >>> parser = JsonParser(config=config) + >>> parser.from_string(json_string, BookForm) + BookForm(author='Hightower, Kim', title=None, genre=None, price=None, pub_date=None, review=None, id=None, lang='en') + +API :ref:`Reference `. + + Unknown json target type ------------------------ @@ -113,6 +149,12 @@ all the imported modules to find a matching dataclass. >>> parser.from_string(json_string) BookForm(author='Hightower, Kim', title='The First Book', genre='Fiction', price=44.95, pub_date=XmlDate(2000, 10, 1), review='An amazing story of nothing.', id='bk001', lang='en') +.. warning:: + + The class locator searches for a dataclass that includes all the input object + properties. This process doesn't work for documents with unknown properties even + if the configuration option is disabled! + List of Objects --------------- diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py new file mode 100644 index 000000000..0272367a2 --- /dev/null +++ b/tests/fixtures/models.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass, field +from typing import List, Dict, Union, Optional +from xml.etree.ElementTree import QName + + +@dataclass +class TypeA: + x: int + + +@dataclass +class TypeB: + x: int + y: str + + +@dataclass +class TypeC: + x: int + y: str + z: float + + +@dataclass +class TypeD: + x: int + y: str + z: Optional[bool] + + +@dataclass +class ExtendedType: + a: Optional[TypeA] = field(default=None) + any: Optional[object] = field(default=None) + wildcard: Optional[object] = field(default=None, metadata={"type": "Wildcard"}) + + +@dataclass +class ChoiceType: + choice: List[object] = field(metadata={ + "type": "Elements", + "choices": ( + {"name": "a", "type": TypeA}, + {"name": "b", "type": TypeB}, + {"name": "int", "type": int}, + {"name": "int2", "type": int}, + {"name": "float", "type": float}, + {"name": "qname", "type": QName}, + {"name": "tokens", "type": List[int], "tokens": True}, + ) + }) + + +@dataclass +class UnionType: + element: Union[TypeA, TypeB, TypeC, TypeD] + + +@dataclass +class AttrsType: + attrs: Dict[str, str] = field(metadata={"type": "Attributes"}) diff --git a/tests/formats/dataclass/models/test_builders.py b/tests/formats/dataclass/models/test_builders.py index f622a2fdb..f523d91bf 100644 --- a/tests/formats/dataclass/models/test_builders.py +++ b/tests/formats/dataclass/models/test_builders.py @@ -92,7 +92,7 @@ def test_build_with_no_dataclass_raises_exception(self, *args): with self.assertRaises(XmlContextError) as cm: XmlMetaBuilder.build(int, None, return_input, return_input) - self.assertEqual(f"Object {int} is not a dataclass.", str(cm.exception)) + self.assertEqual(f"Type '{int}' is not a dataclass.", str(cm.exception)) def test_build_vars(self): result = XmlMetaBuilder.build_vars(BookForm, None, text.pascal_case, str.upper) diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py index 2045854df..1ee2d1edd 100644 --- a/tests/formats/dataclass/parsers/test_json.py +++ b/tests/formats/dataclass/parsers/test_json.py @@ -1,22 +1,26 @@ import json -from dataclasses import make_dataclass from typing import List from typing import Optional -from unittest.case import TestCase from xml.etree.ElementTree import QName from tests import fixtures_dir from tests.fixtures.books import BookForm from tests.fixtures.books import Books +from tests.fixtures.models import AttrsType +from tests.fixtures.models import ChoiceType +from tests.fixtures.models import ExtendedType +from tests.fixtures.models import TypeA +from tests.fixtures.models import TypeB +from tests.fixtures.models import TypeC +from tests.fixtures.models import TypeD +from tests.fixtures.models import UnionType from xsdata.exceptions import ParserError -from xsdata.formats.dataclass.models.elements import XmlType from xsdata.formats.dataclass.models.generics import AnyElement from xsdata.formats.dataclass.models.generics import DerivedElement from xsdata.formats.dataclass.parsers.json import JsonParser from xsdata.formats.dataclass.serializers import JsonSerializer from xsdata.models.datatype import XmlDate from xsdata.utils.testing import FactoryTestCase -from xsdata.utils.testing import XmlVarFactory class JsonParserTests(FactoryTestCase): @@ -99,19 +103,22 @@ def test_parse_with_unknown_class(self): self.assertIsInstance(book_list[1], BookForm) def test_verify_type(self): - invalid_cases = [ ( '{"not": 1, "found": 1}', None, - "No class found matching the document keys(['not', 'found'])", + "Unable to locate model with properties(['not', 'found'])", ), ("{}", None, "Document is empty, can not detect type"), ("[]", BookForm, "Document is array, expected object"), ("{}", List[BookForm], "Document is object, expected array"), - ("{}", Optional[BookForm], "Origin typing.Union is not supported"), - ("[]", List[int], "List argument must be a dataclass"), - ("[]", List, "List argument must be a dataclass"), + ( + "{}", + Optional[ChoiceType], + f"Invalid clazz argument: {Optional[ChoiceType]}", + ), + ("[]", List[int], f"Invalid clazz argument: {List[int]}"), + ("[]", List, f"Invalid clazz argument: {List}"), ] for json_string, clazz, exc_msg in invalid_cases: @@ -121,219 +128,231 @@ def test_verify_type(self): self.assertEqual(exc_msg, str(cm.exception)) - def test_parse_with_non_iterable_value(self): + def test_bind_dataclass_with_unknown_property(self): + data = {"unknown": True} with self.assertRaises(ParserError) as cm: - self.parser.from_string('{"book": 1}') + self.parser.bind_dataclass(data, Books) - self.assertEqual("Key `book` value is not iterable", str(cm.exception)) + self.assertEqual("Unknown property Books.unknown", str(cm.exception)) - def test_bind_value_with_attributes_var(self): - var = XmlVarFactory.create(xml_type=XmlType.ATTRIBUTES, name="a") - value = {"a": 1} - actual = self.parser.bind_value(var, value) - self.assertEqual(value, actual) - self.assertIsNot(value, actual) + self.parser.config.fail_on_unknown_properties = False + self.assertEqual(Books(), self.parser.bind_dataclass(data, Books)) - def test_bind_dataclass_union(self): - a = make_dataclass("a", [("x", int), ("y", str)]) - b = make_dataclass("b", [("x", int), ("y", str), ("z", float)]) - c = make_dataclass("c", [("x", int), ("y", str), ("z", str)]) - d = make_dataclass("d", [("x", int)]) - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - name="union", - qname="union", - types=(a, b, c, int), - ) + def test_bind_dataclass_with_required_fields(self): + obj = self.parser.bind_dataclass({"x": 1, "y": "a", "z": None}, TypeD) - data = {"x": 1, "y": "foo", "z": "foo"} - actual = self.parser.bind_value(var, data) + self.assertEqual(1, obj.x) + self.assertEqual("a", obj.y) + self.assertIsNone(obj.z) - self.assertIsInstance(actual, c) + with self.assertRaises(ParserError): + self.parser.bind_dataclass({"x": 1, "y": "a"}, TypeD) - def test_bind_type_union(self): - a = make_dataclass("a", [("x", int), ("y", str)]) - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - name="union", - qname="union", - types=(a, int, float), - ) + def test_bind_derived_dataclass(self): + data = { + "qname": "{urn:books}BookForm", + "type": None, + "value": { + "author": "Nagata, Suanne", + "title": "Becoming Somebody", + }, + } - data = "1.1" - self.assertEqual(1.1, self.parser.bind_value(var, data)) - - def test_bind_choice_simple(self): - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENTS, - qname="compound", - name="compound", - elements={ - "int": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, qname="int", name="int", types=(int,) - ), - "tokens": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - qname="tokens", - name="tokens", - types=(int,), - tokens=True, - ), - "float": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - qname="float", - name="float", - types=(float,), - ), - "qname": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - qname="qname", - name="qname", - types=(QName,), - ), + actual = self.parser.bind_dataclass(data, BookForm) + expected = DerivedElement( + qname="{urn:books}BookForm", + value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"), + type=None, + ) + self.assertEqual(expected, actual) + + def test_bind_derived_dataclass_with_xsi_type(self): + data = { + "qname": "foobar", + "type": "{urn:books}BookForm", + "value": { + "author": "Nagata, Suanne", + "title": "Becoming Somebody", }, + } + + actual = self.parser.bind_dataclass(data, DerivedElement) + expected = DerivedElement( + qname="foobar", + value=BookForm(author="Nagata, Suanne", title="Becoming Somebody"), + type="{urn:books}BookForm", ) - self.assertEqual(1.0, self.parser.bind_choice("1.0", var)) - self.assertEqual(1, self.parser.bind_choice(1, var)) - self.assertEqual([1], self.parser.bind_choice(["1"], var)) + self.assertEqual(expected, actual) - actual = self.parser.bind_choice("a", var) - self.assertEqual(QName("a"), actual) - self.assertIsInstance(actual, QName) + with self.assertRaises(ParserError) as cm: + data["type"] = "notExists" + self.parser.bind_dataclass(data, DerivedElement) - actual = self.parser.bind_choice("{a}b", var) - self.assertIsInstance(actual, QName) - self.assertEqual(QName("{a}b"), actual) + self.assertEqual( + "Unable to locate derived model with" " properties(['author', 'title'])", + str(cm.exception), + ) - actual = self.parser.bind_choice("!NotQName", var) - self.assertIsInstance(actual, str) - self.assertEqual("!NotQName", actual) + with self.assertRaises(ParserError) as cm: + data["type"] = None + self.parser.bind_dataclass(data, DerivedElement) - def test_bind_choice_dataclass(self): - a = make_dataclass("a", [("x", int)]) - b = make_dataclass("b", [("x", int), ("y", str)]) - - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENTS, - qname="compound", - name="compound", - elements={ - "a": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - qname="a", - name="a", - types=(a,), - ), - "b": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, - qname="b", - name="b", - types=(b,), - ), - "c": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, qname="c", name="c", types=(int,) - ), - }, - ) + def test_bind_dataclass_union(self): + data = {"element": {"x": 1, "y": "foo", "z": "1.0"}} + actual = self.parser.bind_dataclass(data, UnionType) - self.assertEqual(a(1), self.parser.bind_choice({"x": 1}, var)) - self.assertEqual(b(1, "2"), self.parser.bind_choice({"x": 1, "y": "2"}, var)) + self.assertIsInstance(actual.element, TypeC) with self.assertRaises(ParserError) as cm: - self.parser.bind_choice({"x": 1, "y": "2", "z": 3}, var) + self.parser.bind_dataclass({"element": {"a": 1}}, UnionType) self.assertEqual( - "XmlElements undefined choice: `compound` for `{'x': 1, 'y': '2', 'z': 3}`", + "Failed to bind object with properties(['a']) " + "to any of the ['TypeA', 'TypeB', 'TypeC', 'TypeD']", str(cm.exception), ) - def test_bind_choice_generic_with_derived(self): - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENTS, - qname="compound", - name="compound", - elements={ - "a": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, name="a", types=(int,) - ), - "b": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, name="b", types=(float,) - ), - }, - ) - data = {"qname": "a", "value": 1, "substituted": True} + def test_bind_attributes(self): + data = {"attrs": {"a": 1, "b": 2}} - self.assertEqual( - DerivedElement(qname="a", value=1, substituted=True), - self.parser.bind_choice(data, var), - ) + actual = self.parser.bind_dataclass(data, AttrsType) + self.assertEqual(data["attrs"], actual.attrs) + self.assertIsNot(data["attrs"], actual.attrs) - def test_bind_choice_generic_with_wildcard(self): - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENTS, - qname="compound", - name="compound", - elements={ - "a": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, name="a", types=(int,) - ), - "b": XmlVarFactory.create( - xml_type=XmlType.ELEMENT, name="b", types=(float,) - ), - }, + def test_bind_simple_type_with_wildcard_var(self): + data = {"any": 1, "wildcard": 2} + actual = self.parser.bind_dataclass(data, ExtendedType) + self.assertEqual(1, actual.any) + self.assertEqual(2, actual.wildcard) + + def test_bind_simple_type_with_elements_var(self): + data = {"choice": ["1.0", 1, ["1"], "a", "{a}b"]} + + actual = self.parser.bind_dataclass(data, ChoiceType) + + self.assertEqual(1.0, actual.choice[0]) + self.assertEqual(1, actual.choice[1]) + self.assertEqual([1], actual.choice[2]) + self.assertEqual(QName("a"), actual.choice[3]) + self.assertIsInstance(actual.choice[3], QName) + self.assertEqual(QName("{a}b"), actual.choice[4]) + self.assertIsInstance(actual.choice[4], QName) + + data = {"choice": ["!NotAQname"]} + with self.assertRaises(ParserError) as cm: + self.parser.bind_dataclass(data, ChoiceType) + + self.assertEqual( + "Failed to bind '!NotAQname' to ChoiceType.choice field", + str(cm.exception), ) + def test_bind_any_element(self): + data = { + "wildcard": { + "qname": "a", + "text": 1, + "tail": None, + "children": [], + "attributes": {}, + } + } self.assertEqual( - AnyElement(qname="a", text="1"), - self.parser.bind_choice({"qname": "a", "text": 1}, var), + ExtendedType(wildcard=AnyElement(qname="a", text="1")), + self.parser.bind_dataclass(data, ExtendedType), ) - def test_bind_choice_generic_with_unknown_qname(self): - var = XmlVarFactory.create( - xml_type=XmlType.ELEMENTS, qname="compound", name="compound" + def test_bind_choice_dataclass(self): + data = {"choice": [{"x": 1}, {"x": 1, "y": "a"}]} + expected = ChoiceType(choice=[TypeA(x=1), TypeB(x=1, y="a")]) + self.assertEqual(expected, self.parser.bind_dataclass(data, ChoiceType)) + + def test_bind_derived_value_with_simple_type(self): + data = {"choice": [{"qname": "int2", "value": 1, "type": None}]} + + actual = self.parser.bind_dataclass(data, ChoiceType) + expected = ChoiceType(choice=[DerivedElement(qname="int2", value=1)]) + self.assertEqual(expected, actual) + + def test_bind_derived_value_with_choice_var(self): + data = { + "choice": [ + { + "qname": "b", + "type": None, + "value": { + "x": "1", + "y": "a", + }, + } + ] + } + expected = ChoiceType( + choice=[ + DerivedElement( + qname="b", + value=TypeB(x=1, y="a"), + ) + ] ) + self.assertEqual(expected, self.parser.bind_dataclass(data, ChoiceType)) with self.assertRaises(ParserError) as cm: - self.parser.bind_choice({"qname": "foo", "text": 1}, var) + data["choice"][0]["qname"] = "nope" + self.parser.bind_dataclass(data, ChoiceType) self.assertEqual( - "XmlElements undefined choice: `compound` for qname `foo`", + "Unable to locate compound element ChoiceType.choice[nope]", str(cm.exception), ) - def test_bind_wildcard_with_any_element(self): - var = XmlVarFactory.create( - xml_type=XmlType.WILDCARD, - name="any_element", - qname="any_element", - types=(object,), + def test_bind_wildcard_dataclass(self): + + data = {"a": None, "wildcard": {"x": 1}} + expected = ExtendedType(wildcard=TypeA(x=1)) + self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType)) + + def test_bind_wildcard_with_derived_dataclass(self): + data = { + "wildcard": { + "qname": "b", + "type": "TypeB", + "value": { + "x": "1", + "y": "a", + }, + } + } + expected = ExtendedType( + wildcard=DerivedElement(qname="b", value=TypeB(x=1, y="a"), type="TypeB") ) + self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType)) + + def test_bind_any_type_with_derived_dataclass(self): + data = { + "any": { + "qname": "any", + "type": None, + "value": {"x": "1"}, + } + } + expected = ExtendedType(any=DerivedElement(qname="any", value=TypeA(x=1))) + self.assertEqual(expected, self.parser.bind_dataclass(data, ExtendedType)) - self.assertEqual( - AnyElement(qname="a", text="1"), - self.parser.bind_value(var, {"qname": "a", "text": 1}), - ) + with self.assertRaises(ParserError) as cm: + data["any"]["type"] = "notexists" + self.parser.bind_dataclass(data, ExtendedType) - def test_bind_wildcard_with_derived_element(self): - var = XmlVarFactory.create( - any_type=True, - name="a", - qname="a", - types=(object,), - ) - actual = DerivedElement(qname="a", value=Books(book=[]), substituted=True) - data = {"qname": "a", "value": {"book": []}, "substituted": True} + self.assertEqual("Unable to locate xsi:type `notexists`", str(cm.exception)) - self.assertEqual(actual, self.parser.bind_value(var, data)) + def test_find_var(self): + meta = self.parser.context.build(TypeB) + xml_vars = meta.get_all_vars() - def test_bind_wildcard_with_no_matching_value(self): - var = XmlVarFactory.create( - any_type=True, - name="a", - qname="a", - types=(object,), - ) + self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "x")) + self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "x", True)) - data = {"test_bind_wildcard_with_no_matching_value": False} - self.assertEqual(data, self.parser.bind_value(var, data)) - self.assertEqual(1, self.parser.bind_value(var, 1)) + meta = self.parser.context.build(ExtendedType) + xml_vars = meta.get_all_vars() + self.assertIsNone(self.parser.find_var(xml_vars, "a", True)) + self.assertEqual(xml_vars[0], self.parser.find_var(xml_vars, "a")) diff --git a/tests/formats/dataclass/parsers/test_nodes.py b/tests/formats/dataclass/parsers/test_nodes.py index 977367192..7ef5c55fb 100644 --- a/tests/formats/dataclass/parsers/test_nodes.py +++ b/tests/formats/dataclass/parsers/test_nodes.py @@ -791,6 +791,7 @@ def test_start_with_any_type_root(self): attrs=attrs, ns_map=ns_map, derived=True, + xsi_type="{urn:books}books", ) parser.start(None, queue, objects, "doc", attrs, ns_map) self.assertEqual(1, len(queue)) @@ -816,6 +817,7 @@ def test_start_with_derived_class(self): attrs=attrs, ns_map={}, derived=True, + xsi_type="b", ) self.assertEqual(1, len(queue)) diff --git a/tests/formats/dataclass/serializers/test_xml.py b/tests/formats/dataclass/serializers/test_xml.py index 7491a3234..cd8675d91 100644 --- a/tests/formats/dataclass/serializers/test_xml.py +++ b/tests/formats/dataclass/serializers/test_xml.py @@ -103,7 +103,7 @@ def test_write_dataclass_can_overwrite_params(self): def test_write_dataclass_with_no_dataclass(self): with self.assertRaises(XmlContextError) as cm: next(self.serializer.write_dataclass(1)) - self.assertEqual("Object is not a dataclass.", str(cm.exception)) + self.assertEqual("Type '' is not a dataclass.", str(cm.exception)) def test_write_mixed_content(self): var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a", mixed=True) @@ -238,7 +238,9 @@ def test_write_any_type_with_derived_element_primitive(self): def test_write_any_type_with_derived_element_dataclass(self): var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a") - value = DerivedElement(qname="a", value=BookForm(title="def"), substituted=True) + value = DerivedElement( + qname="a", value=BookForm(title="def"), type="{urn:books}BookForm" + ) expected = [ (XmlWriterEvent.START, "a"), (XmlWriterEvent.ATTR, "lang", "en"), diff --git a/tests/formats/dataclass/test_context.py b/tests/formats/dataclass/test_context.py index e323d4477..4dda83a2b 100644 --- a/tests/formats/dataclass/test_context.py +++ b/tests/formats/dataclass/test_context.py @@ -71,9 +71,6 @@ def test_find_type_by_fields(self): field_names = {f.name for f in fields(BookForm)} self.assertEqual(BookForm, self.ctx.find_type_by_fields(field_names)) - field_names.pop() # Test matching less fields - self.assertEqual(BookForm, self.ctx.find_type_by_fields(field_names)) - field_names.update({"please", "dont", "exist"}) # Test matching with more self.assertIsNone(self.ctx.find_type_by_fields(field_names)) diff --git a/tests/formats/dataclass/test_elements.py b/tests/formats/dataclass/test_elements.py index 8c4005d7c..a78597ad8 100644 --- a/tests/formats/dataclass/test_elements.py +++ b/tests/formats/dataclass/test_elements.py @@ -4,7 +4,16 @@ from typing import Iterator from unittest import mock from unittest.case import TestCase - +from xml.etree.ElementTree import QName + +from tests.fixtures.models import ChoiceType +from tests.fixtures.models import TypeA +from tests.fixtures.models import TypeB +from tests.fixtures.models import TypeC +from tests.fixtures.models import TypeD +from tests.fixtures.models import UnionType +from xsdata.formats.dataclass.context import XmlContext +from xsdata.formats.dataclass.models.builders import XmlMetaBuilder from xsdata.formats.dataclass.models.elements import XmlType from xsdata.formats.dataclass.models.elements import XmlVar from xsdata.utils.testing import XmlMetaFactory @@ -17,6 +26,9 @@ class Fixture: class XmlValTests(TestCase): + def setUp(self) -> None: + self.context = XmlContext() + def test_property_local_name(self): var = XmlVarFactory.create(name="a", qname="{B}A") self.assertEqual("A", var.local_name) @@ -40,6 +52,11 @@ def test_property_is_clazz_union(self): var = XmlVarFactory.create(name="foo", types=(Fixture, int)) self.assertTrue(var.is_clazz_union) + def test_property_element_types(self): + meta = self.context.build(ChoiceType) + var = meta.choices[0] + self.assertEqual({TypeA, TypeB, int, float, QName}, var.element_types) + def test_find_choice(self): var = XmlVarFactory.create( xml_type=XmlType.ELEMENTS, @@ -105,6 +122,7 @@ def test_find_value_choice(self): ) self.assertIsNone(var.find_value_choice("foo")) + self.assertIsNone(var.find_value_choice(["1.1", "1.2"])) self.assertEqual(elements[0], var.find_value_choice(1)) self.assertEqual(elements[1], var.find_value_choice([1, 2])) self.assertEqual(elements[2], var.find_value_choice(d())) @@ -182,6 +200,7 @@ def test_repr(self): class XmlMetaTests(TestCase): def setUp(self) -> None: a = make_dataclass("a", []) + self.context = XmlContext() self.meta = XmlMetaFactory.create( clazz=a, qname="a", @@ -192,6 +211,10 @@ def setUp(self) -> None: any_attributes=[], ) + def test_property_element_types(self): + meta = self.context.build(UnionType) + self.assertEqual({TypeA, TypeB, TypeC, TypeD}, meta.element_types) + def test_find_attribute(self): a = XmlVarFactory.create(xml_type=XmlType.ATTRIBUTE, name="a") b = XmlVarFactory.create(xml_type=XmlType.ATTRIBUTE, name="b") diff --git a/tests/formats/dataclass/test_typing.py b/tests/formats/dataclass/test_typing.py index 8755fc5cf..ffa1b6c66 100644 --- a/tests/formats/dataclass/test_typing.py +++ b/tests/formats/dataclass/test_typing.py @@ -34,12 +34,10 @@ def test_get_origin_list(self): get_origin(List) def test_get_origin_dict(self): + self.assertEqual(Dict, get_origin(Dict)) self.assertEqual(Dict, get_origin(Dict[int, str])) self.assertEqual(Dict, get_origin(Dict[Union[int], Union[str]])) - with self.assertRaises(TypeError): - get_origin(Dict) - def test_get_origin_union(self): self.assertIsNone(get_origin(Union[int])) self.assertEqual(Union, get_origin(Optional[int])) @@ -92,12 +90,14 @@ def test_evaluate_simple(self): self.assertEqual((int,), evaluate(int)) def test_evaluate_dict(self): + self.assertEqual((dict, str, str), evaluate(Dict)) self.assertEqual((dict, str, int), evaluate(Dict[str, int])) unsupported_cases = [ Dict[Any, Any], Dict[Union[str, int], int], Dict[int, Union[str, int]], + Dict[TypeVar("A", bound=int), str], ] for case in unsupported_cases: diff --git a/tests/formats/test_converter.py b/tests/formats/test_converter.py index 276c7daeb..768c92873 100644 --- a/tests/formats/test_converter.py +++ b/tests/formats/test_converter.py @@ -114,17 +114,16 @@ def setUp(self): self.converter = converter.type_converter(bool) def test_deserialize(self): - with self.assertRaises(ConverterError): - self.converter.deserialize("True") + for invalid in ("True", "False", 1, 0): + with self.assertRaises(ConverterError): + self.converter.deserialize(invalid) self.assertTrue(self.converter.deserialize("true")) self.assertTrue(self.converter.deserialize("1")) self.assertFalse(self.converter.deserialize("false")) self.assertFalse(self.converter.deserialize("0")) self.assertTrue(self.converter.deserialize(True)) - self.assertTrue(self.converter.deserialize(1)) self.assertFalse(self.converter.deserialize(False)) - self.assertFalse(self.converter.deserialize(0)) def test_serialize(self): self.assertEqual("true", self.converter.serialize(True)) diff --git a/xsdata/formats/converter.py b/xsdata/formats/converter.py index 38d580620..3bd7baf03 100644 --- a/xsdata/formats/converter.py +++ b/xsdata/formats/converter.py @@ -99,7 +99,7 @@ def serialize(self, value: Any, **kwargs: Any) -> Any: return None if isinstance(value, list): - return " ".join([self.serialize(val, **kwargs) for val in value]) + return " ".join(self.serialize(val, **kwargs) for val in value) instance = self.value_converter(value) return instance.serialize(value, **kwargs) @@ -232,7 +232,10 @@ def deserialize(self, value: Any, **kwargs: Any) -> bool: raise ConverterError(f"Invalid bool literal '{value}'") - return True if value else False + if value is True or value is False: + return value + + raise ConverterError(f"Invalid bool literal '{value}'") def serialize(self, value: bool, **kwargs: Any) -> str: return "true" if value else "false" @@ -362,7 +365,7 @@ def serialize( return f"{prefix}:{tag}" if prefix else tag @staticmethod - def resolve(value: str, ns_map: Optional[Dict]) -> Tuple: + def resolve(value: str, ns_map: Optional[Dict] = None) -> Tuple: value = value.strip() if not value: diff --git a/xsdata/formats/dataclass/context.py b/xsdata/formats/dataclass/context.py index d48b745f3..a1ce27d88 100644 --- a/xsdata/formats/dataclass/context.py +++ b/xsdata/formats/dataclass/context.py @@ -2,7 +2,6 @@ from collections import defaultdict from dataclasses import dataclass from dataclasses import field -from dataclasses import fields from dataclasses import is_dataclass from typing import Any from typing import Callable @@ -12,6 +11,7 @@ from typing import Set from typing import Type +from xsdata.exceptions import XmlContextError from xsdata.formats.bindings import T from xsdata.formats.dataclass.models.builders import XmlMetaBuilder from xsdata.formats.dataclass.models.elements import XmlMeta @@ -105,7 +105,7 @@ def find_type(self, qname: str) -> Optional[Type[T]]: types: List[Type] = self.find_types(qname) return types[-1] if types else None - def find_type_by_fields(self, field_names: Set) -> Optional[Type[T]]: + def find_type_by_fields(self, field_names: Set[str]) -> Optional[Type[T]]: """ Find a dataclass from all the imported modules that matches the given list of field names. @@ -116,7 +116,7 @@ def find_type_by_fields(self, field_names: Set) -> Optional[Type[T]]: self.build_xsi_cache() for types in self.xsi_cache.values(): for clazz in types: - if not field_names.difference({attr.name for attr in fields(clazz)}): + if self.local_names_match(field_names, clazz): return clazz return None @@ -157,6 +157,14 @@ def build(self, clazz: Type, parent_ns: Optional[str] = None) -> XmlMeta: ) return self.cache[clazz] + def local_names_match(self, names: Set[str], clazz: Type) -> bool: + try: + meta = self.build(clazz) + local_names = {var.local_name for var in meta.get_all_vars()} + return not names.difference(local_names) + except XmlContextError: + return False + @classmethod def is_derived(cls, obj: Any, clazz: Type) -> bool: """ diff --git a/xsdata/formats/dataclass/filters.py b/xsdata/formats/dataclass/filters.py index f3e8e93bb..5c0c3d8db 100644 --- a/xsdata/formats/dataclass/filters.py +++ b/xsdata/formats/dataclass/filters.py @@ -305,16 +305,14 @@ def format_string(self, data: str, indent: int, key: str = "", pad: int = 0) -> next_indent = indent + 4 value = "\n".join( - [ - f'{" " * next_indent}"{line}"' - for line in textwrap.wrap( - value, - width=self.max_line_length - next_indent - 2, # plus quotes - drop_whitespace=False, - replace_whitespace=False, - break_long_words=True, - ) - ] + f'{" " * next_indent}"{line}"' + for line in textwrap.wrap( + value, + width=self.max_line_length - next_indent - 2, # plus quotes + drop_whitespace=False, + replace_whitespace=False, + break_long_words=True, + ) ) return f"(\n{value}\n{' ' * indent})" diff --git a/xsdata/formats/dataclass/models/builders.py b/xsdata/formats/dataclass/models/builders.py index abe8c982f..5ca4acdbe 100644 --- a/xsdata/formats/dataclass/models/builders.py +++ b/xsdata/formats/dataclass/models/builders.py @@ -44,7 +44,7 @@ def build( """Build the binding metadata for a dataclass and its fields.""" if not is_dataclass(clazz): - raise XmlContextError(f"Object {clazz} is not a dataclass.") + raise XmlContextError(f"Type '{clazz}' is not a dataclass.") # Fetch the dataclass meta settings and make sure we don't inherit # the parent class meta. diff --git a/xsdata/formats/dataclass/models/elements.py b/xsdata/formats/dataclass/models/elements.py index c4acf9f26..82afc66fe 100644 --- a/xsdata/formats/dataclass/models/elements.py +++ b/xsdata/formats/dataclass/models/elements.py @@ -7,8 +7,10 @@ from typing import Mapping from typing import Optional from typing import Sequence +from typing import Set from typing import Type +from xsdata.formats.converter import converter from xsdata.models.enums import NamespaceType from xsdata.utils import collections from xsdata.utils.constants import EMPTY_SEQUENCE @@ -171,6 +173,10 @@ def __init__( else: self.is_text = True + @property + def element_types(self) -> Set[Type]: + return {tp for element in self.elements.values() for tp in element.types} + def get_xml_type(self) -> str: if self.is_wildcard: return XmlType.WILDCARD @@ -207,10 +213,10 @@ def find_value_choice(self, value: Any) -> Optional["XmlVar"]: tokens = False check_subclass = is_dataclass(value) - return self.find_type_choice(tp, tokens, check_subclass) + return self.find_type_choice(value, tp, tokens, check_subclass) def find_type_choice( - self, tp: Type, tokens: bool, check_subclass: bool + self, value: Any, tp: Type, tokens: bool, check_subclass: bool ) -> Optional["XmlVar"]: """Match and return a choice field that matches the given type.""" @@ -222,18 +228,24 @@ def find_type_choice( if tp is NoneType: if element.nillable: return element - elif self.match_type(tp, element.types, check_subclass): + elif self.match_type(value, tp, element.types, check_subclass): return element return None @classmethod - def match_type(cls, tp: Type, types: Sequence[Type], check_subclass: bool) -> bool: + def match_type( + cls, value: Any, tp: Type, types: Sequence[Type], check_subclass: bool + ) -> bool: + for candidate in types: if tp == candidate or (check_subclass and issubclass(tp, candidate)): return True - return False + if isinstance(value, list): + return all(converter.test(val, types) for val in value) + + return converter.test(value, types) def match_namespace(self, qname: str) -> bool: """Match the given qname to the wildcard allowed namespaces.""" @@ -416,6 +428,15 @@ def find_children(self, qname: str) -> Iterator[XmlVar]: if chd: yield chd + @property + def element_types(self) -> Set[Type]: + return { + tp + for elements in self.elements.values() + for element in elements + for tp in element.types + } + def __repr__(self) -> str: params = ( f"clazz={self.clazz}, " diff --git a/xsdata/formats/dataclass/models/generics.py b/xsdata/formats/dataclass/models/generics.py index 2f3461b4f..b4c60d89a 100644 --- a/xsdata/formats/dataclass/models/generics.py +++ b/xsdata/formats/dataclass/models/generics.py @@ -43,9 +43,9 @@ class DerivedElement(Generic[T]): :param qname: The element's qualified name :param value: The wrapped value - :param substituted: Specify whether the value is a type substitution + :param type: The real xsi:type """ qname: str value: T - substituted: bool = False + type: Optional[str] = None diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py index 006b94ec1..f6bd20fc0 100644 --- a/xsdata/formats/dataclass/parsers/json.py +++ b/xsdata/formats/dataclass/parsers/json.py @@ -7,6 +7,7 @@ from typing import Any from typing import Callable from typing import Dict +from typing import Iterable from typing import List from typing import Optional from typing import Sequence @@ -18,10 +19,14 @@ from xsdata.formats.bindings import AbstractParser from xsdata.formats.bindings import T from xsdata.formats.dataclass.context import XmlContext +from xsdata.formats.dataclass.models.elements import XmlMeta from xsdata.formats.dataclass.models.elements import XmlVar from xsdata.formats.dataclass.models.generics import AnyElement from xsdata.formats.dataclass.models.generics import DerivedElement +from xsdata.formats.dataclass.parsers.config import ParserConfig from xsdata.formats.dataclass.parsers.utils import ParserUtils +from xsdata.formats.dataclass.typing import get_args +from xsdata.formats.dataclass.typing import get_origin from xsdata.utils.constants import EMPTY_MAP ANY_KEYS = {f.name for f in fields(AnyElement)} @@ -33,10 +38,12 @@ class JsonParser(AbstractParser): """ Json parser for dataclasses. + :param config: Parser configuration :param context: Model context provider :param load_factory: Replace the default json.load call with another implementation """ + config: ParserConfig = field(default_factory=ParserConfig) context: XmlContext = field(default_factory=XmlContext) load_factory: Callable = field(default=json.load) @@ -63,10 +70,21 @@ def verify_type(self, clazz: Optional[Type[T]], data: Union[Dict, List]) -> Type if clazz is None: return self.detect_type(data) - origin = getattr(clazz, "__origin__", None) - list_type = origin in (list, List) or clazz is List - if origin is not None and not list_type: - raise ParserError(f"Origin {origin} is not supported") + try: + origin = get_origin(clazz) + list_type = False + if origin is List: + list_type = True + args = get_args(clazz) + + if len(args) != 1 or not is_dataclass(args[0]): + raise TypeError() + + clazz = args[0] + elif origin is not None: + raise TypeError() + except TypeError: + raise ParserError(f"Invalid clazz argument: {clazz}") if list_type != isinstance(data, list): if list_type: @@ -74,187 +92,211 @@ def verify_type(self, clazz: Optional[Type[T]], data: Union[Dict, List]) -> Type else: raise ParserError("Document is array, expected object") - if list_type: - args = getattr(clazz, "__args__", ()) - if args is None or len(args) != 1 or not is_dataclass(args[0]): - raise ParserError("List argument must be a dataclass") - - clazz = args[0] - return clazz # type: ignore def detect_type(self, data: Union[Dict, List]) -> Type[T]: if not data: raise ParserError("Document is empty, can not detect type") - keys = list(data[0].keys() if isinstance(data, list) else data.keys()) + keys = data[0].keys() if isinstance(data, list) else data.keys() clazz: Optional[Type[T]] = self.context.find_type_by_fields(set(keys)) - if clazz is None: - raise ParserError(f"No class found matching the document keys({keys})") - - return clazz - - def bind_value(self, var: XmlVar, value: Any) -> Any: - """Bind value according to the class var.""" - - if var.is_attributes: - return dict(value) - - if var.is_clazz_union: - if isinstance(value, dict): - return self.bind_dataclass_union(value, var) - - return self.bind_type_union(value, var) - - if var.clazz: - return self.bind_dataclass(value, var.clazz) + if clazz: + return clazz - if var.is_elements: - return self.bind_choice(value, var) - - if var.is_wildcard or var.any_type: - return self.bind_wildcard(value) - - return self.parse_value(value, var.types, var.default, var.tokens, var.format) + raise ParserError(f"Unable to locate model with properties({list(keys)})") def bind_dataclass(self, data: Dict, clazz: Type[T]) -> T: """Recursively build the given model from the input dict data.""" - params = {} - for var in self.context.build(clazz).get_all_vars(): - value = data.get(var.local_name) - if value is None or not var.init: - continue + if set(data.keys()) == DERIVED_KEYS: + return self.bind_derived_dataclass(data, clazz) - if var.list_element: - if not isinstance(value, list): - raise ParserError(f"Key `{var.name}` value is not iterable") + meta = self.context.build(clazz) + xml_vars = meta.get_all_vars() - params[var.name] = [self.bind_value(var, val) for val in value] - else: - params[var.name] = self.bind_value(var, value) + params = {} + for key, value in data.items(): + is_list = isinstance(value, list) + var = self.find_var(xml_vars, key, is_list) - return clazz(**params) # type: ignore + if var is None and self.config.fail_on_unknown_properties: + raise ParserError(f"Unknown property {clazz.__qualname__}.{key}") - def maybe_bind_dataclass(self, data: Dict, clazz: Type[T]) -> Optional[T]: + if var and var.init: + params[var.name] = self.bind_value(meta, var, value) + + try: + return clazz(**params) # type: ignore + except TypeError as e: + raise ParserError(e) + + def bind_derived_dataclass(self, data: Dict, clazz: Type[T]) -> Any: + qname = data["qname"] + xsi_type = data["type"] + params = data["value"] + + if clazz is DerivedElement: + real_clazz: Optional[Type[T]] = None + if xsi_type: + real_clazz = self.context.find_type(xsi_type) + + if real_clazz is None: + raise ParserError( + f"Unable to locate derived model " + f"with properties({list(params.keys())})" + ) + + value = self.bind_dataclass(params, real_clazz) + else: + value = self.bind_dataclass(params, clazz) + + return DerivedElement(qname=qname, type=xsi_type, value=value) + + def bind_best_dataclass(self, data: Dict, classes: Iterable[Type[T]]) -> T: + """Attempt to bind the given data to one possible models, if more than + one is successful return the object with the highest score.""" + obj = None + keys = set(data.keys()) + max_score = -1.0 + for clazz in classes: + if is_dataclass(clazz) and self.context.local_names_match(keys, clazz): + candidate = self.bind_optional_dataclass(data, clazz) + score = ParserUtils.score_object(candidate) + if score > max_score: + max_score = score + obj = candidate + + if obj: + return obj + + raise ParserError( + f"Failed to bind object with properties({list(data.keys())}) " + f"to any of the {[cls.__qualname__ for cls in classes]}" + ) + + def bind_optional_dataclass(self, data: Dict, clazz: Type[T]) -> Optional[T]: """Recursively build the given model from the input dict data but fail on any converter warnings.""" try: with warnings.catch_warnings(): warnings.filterwarnings("error", category=ConverterWarning) return self.bind_dataclass(data, clazz) - except ConverterWarning: + except Exception: return None - def bind_dataclass_union(self, value: Dict, var: XmlVar) -> Any: - """Bind data to all possible models and return the best candidate.""" - obj = None - max_score = -1.0 - for clazz in var.types: - if not is_dataclass(clazz): - continue - - candidate = self.maybe_bind_dataclass(value, clazz) - score = ParserUtils.score_object(candidate) - if score > max_score: - max_score = score - obj = candidate - - return obj - - def bind_type_union(self, value: Any, var: XmlVar) -> Any: - types = [tp for tp in var.types if not is_dataclass(tp)] - return self.parse_value(value, types, var.default, var.tokens, var.format) - - def bind_wildcard(self, value: Any) -> Any: - """Bind data to a wildcard model.""" - if isinstance(value, Dict): - keys = set(value.keys()) - - if not (keys - ANY_KEYS): - return self.bind_dataclass(value, AnyElement) - - if not (keys - DERIVED_KEYS): - return self.bind_dataclass(value, DerivedElement) + def bind_value( + self, meta: XmlMeta, var: XmlVar, value: Any, recursive: bool = False + ) -> Any: + """Main entry point for binding values.""" - clazz: Optional[Type] = self.context.find_type_by_fields(keys) - if clazz: - return self.bind_dataclass(value, clazz) + # xs:anyAttributes get it out of the way, it's the mapping exception! + if var.is_attributes: + return dict(value) - return value + # Repeating element, recursively bind the values + if not recursive and var.list_element and isinstance(value, list): + return [self.bind_value(meta, var, val, True) for val in value] - def bind_choice(self, value: Any, var: XmlVar) -> Any: - """Bind data to one of the choice models.""" + # If not dict this is an text or tokens value. if not isinstance(value, dict): - return self.bind_choice_simple(value, var) + return self.bind_text(meta, var, value) - if "qname" in value: - return self.bind_choice_generic(value, var) + keys = value.keys() + if keys == ANY_KEYS: + # Bind data to AnyElement dataclass + return self.bind_dataclass(value, AnyElement) - return self.bind_choice_dataclass(value, var) + if keys == DERIVED_KEYS: + # Bind data to AnyElement dataclass + return self.bind_derived_value(meta, var, value) - def bind_choice_simple(self, value: Any, var: XmlVar) -> Any: - """Bind data to one of the simple choice types and return the first - that succeeds.""" - choice = var.find_value_choice(value) - if choice: - return self.bind_value(choice, value) + # Bind data to a user defined dataclass + return self.bind_complex_type(meta, var, value) - # Sometimes exact type match doesn't work, eg Decimals, try all of them - is_list = isinstance(value, list) - for choice in var.elements.values(): - if choice.clazz or choice.tokens != is_list: - continue + def bind_text(self, meta: XmlMeta, var: XmlVar, value: Any) -> Any: + """Bind text/tokens value entrypoint.""" - with warnings.catch_warnings(record=True) as w: - result = self.bind_value(choice, value) - if not w: - return result + if var.elements: + # Compound field we need to match the value to one of the choice elements + choice = var.find_value_choice(value) + if choice: + return self.bind_text(meta, choice, value) - return value - - def bind_choice_generic(self, value: Dict, var: XmlVar) -> Any: - """Bind data to a either a derived or a user derived model.""" - qname = value["qname"] - choice = var.find_choice(qname) - - if not choice: raise ParserError( - f"XmlElements undefined choice: `{var.name}` for qname `{qname}`" + f"Failed to bind '{value}' " + f"to {meta.clazz.__qualname__}.{var.name} field" ) - if "value" in value: - obj = self.bind_value(choice, value["value"]) - substituted = value.get("substituted", False) - - return DerivedElement(qname=qname, value=obj, substituted=substituted) + if var.any_type or var.is_wildcard: + # field can support any object return the value as it is + return value - return self.bind_dataclass(value, AnyElement) + # Convert value according to the field types + return ParserUtils.parse_value( + value, var.types, var.default, EMPTY_MAP, var.tokens, var.format + ) - def bind_choice_dataclass(self, value: Dict, var: XmlVar) -> Any: - """Bind data to the best matching choice model.""" - keys = set(value.keys()) - for choice in var.elements.values(): - if choice.clazz: - meta = self.context.build(choice.clazz) - attrs = {var.local_name for var in meta.get_all_vars()} - if attrs == keys: - return self.bind_value(choice, value) + def bind_complex_type(self, meta: XmlMeta, var: XmlVar, data: Dict) -> Any: + """Bind data to a user defined dataclass.""" - raise ParserError(f"XmlElements undefined choice: `{var.name}` for `{value}`") + if var.is_clazz_union: + # Union of dataclasses + return self.bind_best_dataclass(data, var.types) + elif var.elements: + # Compound field with multiple choices + return self.bind_best_dataclass(data, var.element_types) + elif var.any_type or var.is_wildcard: + # xs:anyType element, check all meta classes + return self.bind_best_dataclass(data, meta.element_types) + else: + return self.bind_dataclass(data, var.clazz) + + def bind_derived_value( + self, meta: XmlMeta, var: XmlVar, data: Dict + ) -> DerivedElement: + """Bind derived element entry point.""" + + qname = data["qname"] + xsi_type = data["type"] + params = data["value"] + + if var.elements: + choice = var.find_choice(qname) + if choice is None: + raise ParserError( + f"Unable to locate compound element" + f" {meta.clazz.__qualname__}.{var.name}[{qname}]" + ) + return self.bind_derived_value(meta, choice, data) + + if not isinstance(params, dict): + value = self.bind_text(meta, var, params) + elif var.clazz: + value = self.bind_complex_type(meta, var, params) + elif xsi_type: + clazz: Optional[Type] = self.context.find_type(xsi_type) + + if clazz is None: + raise ParserError(f"Unable to locate xsi:type `{xsi_type}`") + + value = self.bind_dataclass(params, clazz) + else: + value = self.bind_best_dataclass(params, meta.element_types) + + return DerivedElement(qname=qname, value=value, type=xsi_type) @classmethod - def parse_value( - cls, - value: Any, - types: Sequence[Type], - default: Any, - tokens: bool, - fmt: Optional[str], - ) -> Any: - """Convert any value to one of the given var types.""" - return ParserUtils.parse_value(value, types, default, EMPTY_MAP, tokens, fmt) + def find_var( + cls, xml_vars: Sequence[XmlVar], local_name: str, is_list: bool = False + ) -> Optional[XmlVar]: + for var in xml_vars: + if var.local_name == local_name: + var_is_list = var.list_element or var.tokens + if is_list == var_is_list or var.clazz is None: + return var + + return None @dataclass diff --git a/xsdata/formats/dataclass/parsers/nodes.py b/xsdata/formats/dataclass/parsers/nodes.py index 570a354a0..8e335ce6f 100644 --- a/xsdata/formats/dataclass/parsers/nodes.py +++ b/xsdata/formats/dataclass/parsers/nodes.py @@ -56,7 +56,7 @@ class ElementNode(XmlNode): position: int mixed: bool = False derived: bool = False - substituted: bool = False + xsi_type: Optional[str] = None assigned: Set = field(default_factory=set) def bind(self, qname: str, text: NoneStr, tail: NoneStr, objects: List) -> bool: @@ -78,7 +78,7 @@ def bind(self, qname: str, text: NoneStr, tail: NoneStr, objects: List) -> bool: obj = self.meta.clazz(**params) if self.derived: - obj = DerivedElement(qname=qname, value=obj, substituted=self.substituted) + obj = DerivedElement(qname=qname, value=obj, type=self.xsi_type) objects.append((qname, obj)) @@ -180,7 +180,7 @@ def build_element_node( context=self.context, position=position, derived=derived, - substituted=xsi_type is not None, + xsi_type=xsi_type, mixed=self.meta.mixed_content, ) @@ -486,6 +486,7 @@ def start( ns_map=ns_map, context=self.context, derived=derived, + xsi_type=xsi_type if derived else None, ) queue.append(child) diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py index 1c6967d00..15888d710 100644 --- a/xsdata/formats/dataclass/serializers/xml.py +++ b/xsdata/formats/dataclass/serializers/xml.py @@ -195,7 +195,7 @@ def write_derived_element( ) -> Generator: if is_dataclass(value.value): xsi_type = None - if value.substituted: + if value.type: meta = self.context.build(value.value.__class__) xsi_type = QName(meta.source_qname) diff --git a/xsdata/formats/dataclass/typing.py b/xsdata/formats/dataclass/typing.py index c5a674d1b..d98fbe791 100644 --- a/xsdata/formats/dataclass/typing.py +++ b/xsdata/formats/dataclass/typing.py @@ -13,7 +13,11 @@ def get_origin(tp: Any) -> Any: - if tp in (List, Dict, Union): + + if tp is Dict: + return Dict + + if tp in (List, Union): raise TypeError() if isinstance(tp, TypeVar): @@ -69,10 +73,24 @@ def _evaluate(tp: Any) -> Iterator[Type]: def _evaluate_mapping(tp: Any) -> Iterator[Type]: yield dict args = get_args(tp) + + if not args: + yield str + yield str + for arg in args: - if get_origin(arg): + origin = get_origin(arg) + if origin is TypeVar: + try: + next(_evaluate_typevar(arg)) + except TypeError: + yield str + else: + raise TypeError() + elif origin is not None: raise TypeError() - yield arg + else: + yield arg def _evaluate_list(tp: Any) -> Iterator[Type]: diff --git a/xsdata/models/xsd.py b/xsdata/models/xsd.py index b4a3a1d26..fcaf4c6cb 100644 --- a/xsdata/models/xsd.py +++ b/xsdata/models/xsd.py @@ -863,7 +863,7 @@ def get_restrictions(self) -> Dict[str, Anything]: if self.patterns: restrictions["pattern"] = "|".join( - [pattern.value for pattern in self.patterns] + pattern.value for pattern in self.patterns ) return restrictions