From 2325be65e7465b5fd98ff893481b135828629f8a Mon Sep 17 00:00:00 2001 From: Christodoulos Tsoulloftas Date: Sat, 6 Aug 2022 20:23:47 +0300 Subject: [PATCH] Avoid using DerivedElement with known elements for mixed wildcards --- docs/examples.rst | 11 ++- .../working-with-wildcards.rst} | 69 ++++++++++++++++++- tests/fixtures/models.py | 9 ++- .../dataclass/parsers/nodes/test_element.py | 24 +++++-- .../dataclass/parsers/nodes/element.py | 13 ++-- xsdata/formats/dataclass/serializers/xml.py | 10 ++- 6 files changed, 119 insertions(+), 17 deletions(-) rename docs/{faq/how-to-work-with-wildcard-fields.rst => examples/working-with-wildcards.rst} (65%) diff --git a/docs/examples.rst b/docs/examples.rst index 86c4102f5..c03d3b0bc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -11,12 +11,21 @@ Code Generation examples/docstrings examples/xml-modeling examples/json-modeling - examples/pycode-serializer examples/dtd-modeling examples/compound-fields examples/dataclasses-features +Data Binding +============ + +.. toctree:: + :maxdepth: 1 + + examples/pycode-serializer + examples/working-with-wildcards + + Advance Topics ============== diff --git a/docs/faq/how-to-work-with-wildcard-fields.rst b/docs/examples/working-with-wildcards.rst similarity index 65% rename from docs/faq/how-to-work-with-wildcard-fields.rst rename to docs/examples/working-with-wildcards.rst index a7963af67..ac3c7584a 100644 --- a/docs/faq/how-to-work-with-wildcard-fields.rst +++ b/docs/examples/working-with-wildcards.rst @@ -1,5 +1,7 @@ -How to work with wildcard fields? -================================== +====================== +Working with wildcards +====================== + One of the xml schema traits is to support any extensions with wildcards. @@ -39,6 +41,9 @@ The generator will roughly create this class for you. ... ) +Generics +======== + xsdata comes with two generic models that are used during parsing and you can also use to generate any custom xml element. @@ -87,3 +92,63 @@ to generate any custom xml element. + + +Mixed content +============= + +For mixed content with known choices you can skip wrapping your instances with a +generic model. During data binding xsdata will try first to match one of the qualified +choices. + +.. doctest:: + + >>> @dataclass + ... class Beta: + ... class Meta: + ... name = "beta" + ... + >>> @dataclass + ... class Alpha: + ... class Meta: + ... name = "alpha" + ... + >>> @dataclass + ... class Doc: + ... class Meta: + ... name = "doc" + ... + ... content: List[object] = field( + ... default_factory=list, + ... metadata={ + ... "type": "Wildcard", + ... "namespace": "##any", + ... "mixed": True, + ... "choices": ( + ... { + ... "name": "a", + ... "type": Alpha, + ... "namespace": "", + ... }, + ... { + ... "name": "b", + ... "type": Beta, + ... "namespace": "", + ... }, + ... ), + ... } + ... ) + ... + >>> obj = Doc( + ... content=[ + ... Alpha(), + ... Beta(), + ... ] + ... ) + ... + >>> print(serializer.render(obj)) + + + + + diff --git a/tests/fixtures/models.py b/tests/fixtures/models.py index 675fa9766..0c44a9c8d 100644 --- a/tests/fixtures/models.py +++ b/tests/fixtures/models.py @@ -166,7 +166,14 @@ class Meta: content: List[object] = field( default_factory=list, - metadata=dict(type="Wildcard", namespace="##any", mixed=True), + metadata=dict( + type="Wildcard", + namespace="##any", + mixed=True, + choices=( + dict(name="span", type=Span), + ), + ) ) diff --git a/tests/formats/dataclass/parsers/nodes/test_element.py b/tests/formats/dataclass/parsers/nodes/test_element.py index f71e0d8e1..23c670b8b 100644 --- a/tests/formats/dataclass/parsers/nodes/test_element.py +++ b/tests/formats/dataclass/parsers/nodes/test_element.py @@ -266,26 +266,38 @@ def test_bind_wild_list_var(self): self.assertEqual(expected, params) def test_prepare_generic_value(self): - actual = self.node.prepare_generic_value(None, 1) + var = XmlVarFactory.create( + index=2, + xml_type=XmlType.WILDCARD, + qname="a", + types=(object,), + elements={"known": XmlVarFactory.create()}, + ) + + actual = self.node.prepare_generic_value(None, 1, var) self.assertEqual(1, actual) - actual = self.node.prepare_generic_value("a", 1) + actual = self.node.prepare_generic_value("a", 1, var) expected = AnyElement(qname="a", text="1") self.assertEqual(expected, actual) - actual = self.node.prepare_generic_value("a", "foo") + actual = self.node.prepare_generic_value("a", "foo", var) expected = AnyElement(qname="a", text="foo") self.assertEqual(expected, actual) fixture = make_dataclass("Fixture", [("content", str)]) - actual = self.node.prepare_generic_value("a", fixture("foo")) + actual = self.node.prepare_generic_value("a", fixture("foo"), var) expected = DerivedElement(qname="a", value=fixture("foo"), type="Fixture") self.assertEqual(expected, actual) - actual = self.node.prepare_generic_value("a", expected) + fixture = make_dataclass("Fixture", [("content", str)]) + actual = self.node.prepare_generic_value("known", fixture("foo"), var) + self.assertEqual(fixture("foo"), actual) + + actual = self.node.prepare_generic_value("a", expected, var) self.assertIs(expected, actual) - actual = self.node.prepare_generic_value("Fixture", fixture("foo")) + actual = self.node.prepare_generic_value("Fixture", fixture("foo"), var) expected = DerivedElement(qname="Fixture", value=fixture("foo")) self.assertEqual(expected, actual) diff --git a/xsdata/formats/dataclass/parsers/nodes/element.py b/xsdata/formats/dataclass/parsers/nodes/element.py index 6f8915882..68c9b1348 100644 --- a/xsdata/formats/dataclass/parsers/nodes/element.py +++ b/xsdata/formats/dataclass/parsers/nodes/element.py @@ -214,7 +214,7 @@ def bind_wild_var(self, params: Dict, var: XmlVar, qname: str, value: Any) -> bo generic instance add the current value as a child object. """ - value = self.prepare_generic_value(qname, value) + value = self.prepare_generic_value(qname, value, var) if var.list_element: items = params.get(var.name) @@ -240,11 +240,14 @@ def bind_mixed_objects(self, params: Dict, var: XmlVar, objects: List): pos = self.position params[var.name] = [ - self.prepare_generic_value(qname, value) for qname, value in objects[pos:] + self.prepare_generic_value(qname, value, var) + for qname, value in objects[pos:] ] del objects[pos:] - def prepare_generic_value(self, qname: Optional[str], value: Any) -> Any: + def prepare_generic_value( + self, qname: Optional[str], value: Any, var: XmlVar + ) -> Any: """Prepare parsed value before binding to a wildcard field.""" if qname: @@ -253,7 +256,9 @@ def prepare_generic_value(self, qname: Optional[str], value: Any) -> Any: if not self.context.class_type.is_model(value): value = any_factory(qname=qname, text=converter.serialize(value)) - elif not isinstance(value, (any_factory, derived_factory)): + elif not isinstance( + value, (any_factory, derived_factory) + ) and not var.find_choice(qname): meta = self.context.fetch(type(value)) xsi_type = namespaces.real_xsi_type(qname, meta.target_qname) value = derived_factory(qname=qname, value=value, type=xsi_type) diff --git a/xsdata/formats/dataclass/serializers/xml.py b/xsdata/formats/dataclass/serializers/xml.py index 28fbaa697..0ae7eb0df 100644 --- a/xsdata/formats/dataclass/serializers/xml.py +++ b/xsdata/formats/dataclass/serializers/xml.py @@ -118,11 +118,15 @@ def write_dataclass( yield XmlWriterEvent.END, qname def write_xsi_type(self, value: Any, var: XmlVar, namespace: NoneStr) -> Generator: - """Produce an events stream from a dataclass for the given var with - with xsi abstract type check for non wildcards.""" + """Produce an events stream from a dataclass for the given var with xsi + abstract type check for non wildcards.""" if var.is_wildcard: - yield from self.write_dataclass(value, namespace) + choice = var.find_value_choice(value, True) + if choice: + yield from self.write_value(value, choice, namespace) + else: + yield from self.write_dataclass(value, namespace) else: xsi_type = self.xsi_type(var, value, namespace) yield from self.write_dataclass(