Skip to content

Commit

Permalink
Support strict content process for wildcards
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed Jul 25, 2023
1 parent 76769a6 commit 62fb9e9
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 54 deletions.
76 changes: 42 additions & 34 deletions tests/formats/dataclass/parsers/nodes/test_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,15 +235,16 @@ def test_bind_objects(self, mock_warning):

def test_bind_wild_var(self):
self.node.meta = self.context.build(ExtendedType)
x = make_dataclass("x", [("value", int)])

params = {}
objects = [("x", 1), ("x", 2), ("z", 3.0)]
objects = [("x", x(1)), ("y", 2), ("z", 3.0)]
self.node.bind_objects(params, objects)
expected = {
"wildcard": AnyElement(
children=[
AnyElement(qname="x", text="1"),
AnyElement(qname="x", text="2"),
x(1),
AnyElement(qname="y", text="2"),
AnyElement(qname="z", text="3.0"),
]
)
Expand Down Expand Up @@ -287,20 +288,8 @@ def test_prepare_generic_value(self):

fixture = make_dataclass("Fixture", [("content", str)])
actual = self.node.prepare_generic_value("a", fixture("foo"), var)
expected = DerivedElement(qname="a", value=fixture("foo"), type="Fixture")
self.assertEqual(expected, actual)

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"), var)
expected = DerivedElement(qname="Fixture", value=fixture("foo"))
self.assertEqual(expected, actual)

def test_child(self):
var = XmlVarFactory.create(xml_type=XmlType.ELEMENT, qname="a", types=(TypeC,))
attrs = {"a": "b"}
Expand All @@ -316,10 +305,10 @@ def test_child(self):

def test_child_with_unique_element(self):
single = XmlVarFactory.create(
index=1, xml_type=XmlType.ELEMENT, qname="a", types=(TypeC,)
index=1, xml_type=XmlType.ELEMENT, qname="cc", types=(TypeC,)
)
wildcard = XmlVarFactory.create(
index=2, xml_type=XmlType.WILDCARD, qname="a", types=(object,)
index=2, xml_type=XmlType.WILDCARD, qname="cc", types=(object,)
)
self.meta.elements[single.qname] = [single]
self.meta.wildcards.append(wildcard)
Expand All @@ -328,11 +317,11 @@ def test_child_with_unique_element(self):
ns_map = {"ns0": "xsdata"}
position = 1

actual = self.node.child("a", attrs, ns_map, position)
actual = self.node.child("cc", attrs, ns_map, position)
self.assertIsInstance(actual, ElementNode)
self.assertIn(single.index, self.node.assigned)

actual = self.node.child("a", attrs, ns_map, position)
actual = self.node.child("cc", attrs, ns_map, position)
self.assertIsInstance(actual, WildcardNode)
self.assertNotIn(wildcard.index, self.node.assigned)

Expand Down Expand Up @@ -364,7 +353,7 @@ def test_build_node_with_dataclass_union_var(self):
)
attrs = {"a": "b"}
ns_map = {"ns0": "xsdata"}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, UnionNode)
self.assertEqual(10, actual.position)
Expand All @@ -391,7 +380,7 @@ def test_build_node_with_dataclass_var(self, mock_ctx_fetch, mock_xsi_type):

attrs = {"a": "b"}
ns_map = {"ns0": "xsdata"}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, ElementNode)
self.assertEqual(10, actual.position)
Expand Down Expand Up @@ -420,7 +409,7 @@ def test_build_node_with_dataclass_var_and_mismatch_xsi_type(

attrs = {"a": "b"}
ns_map = {"ns0": "xsdata"}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, ElementNode)
self.assertEqual(10, actual.position)
Expand All @@ -439,13 +428,13 @@ def test_build_node_with_dataclass_var_validates_nillable(self, mock_ctx_fetch):
mock_ctx_fetch.side_effect = [self.meta, self.meta, nillable_meta]
attrs = {QNames.XSI_NIL: "false"}

self.assertIsNotNone(self.node.build_node(var, attrs, ns_map, 10))
self.assertIsNotNone(self.node.build_node(var.qname, var, attrs, ns_map, 10))

attrs = {QNames.XSI_NIL: "true"}
self.assertIsNotNone(self.node.build_node(var, attrs, ns_map, 10))
self.assertIsNone(self.node.build_node(var.qname, var, attrs, ns_map, 10))

attrs = {QNames.XSI_NIL: "false"}
self.assertIsNone(self.node.build_node(var, attrs, ns_map, 10))
self.assertIsNone(self.node.build_node(var.qname, var, attrs, ns_map, 10))

def test_build_node_with_any_type_var_with_matching_xsi_type(self):
var = XmlVarFactory.create(
Expand All @@ -457,7 +446,7 @@ def test_build_node_with_any_type_var_with_matching_xsi_type(self):
)
attrs = {QNames.XSI_TYPE: "bk:books"}
ns_map = {"bk": "urn:books"}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, ElementNode)
self.assertEqual(10, actual.position)
Expand All @@ -476,7 +465,7 @@ def test_build_node_with_any_type_var_with_datatype(self):
)
attrs = {QNames.XSI_TYPE: "xs:hexBinary"}
ns_map = {Namespace.XS.prefix: Namespace.XS.uri}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, StandardNode)
self.assertEqual(ns_map, actual.ns_map)
Expand All @@ -487,12 +476,12 @@ def test_build_node_with_any_type_var_with_no_matching_xsi_type(self):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
name="a",
qname="a",
qname="aaa",
types=(object,),
any_type=True,
)
attrs = {QNames.XSI_TYPE: "noMatch"}
actual = self.node.build_node(var, attrs, {}, 10)
actual = self.node.build_node(var.qname, var, attrs, {}, 10)

self.assertIsInstance(actual, WildcardNode)
self.assertEqual(10, actual.position)
Expand All @@ -504,23 +493,42 @@ def test_build_node_with_any_type_var_with_no_xsi_type(self):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
name="a",
qname="a",
qname="aaaa",
types=(object,),
any_type=True,
)
attrs = {}
actual = self.node.build_node(var, attrs, {}, 10)
actual = self.node.build_node(var.qname, var, attrs, {}, 10)

self.assertIsInstance(actual, WildcardNode)
self.assertEqual(10, actual.position)
self.assertEqual(var, actual.var)
self.assertEqual(attrs, actual.attrs)
self.assertEqual({}, actual.ns_map)

def test_build_node_with_any_type_var_with_no_xsi_type_and_type_Exists(self):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENT,
name="a",
qname="a",
types=(object,),
any_type=True,
)
attrs = {}
a = make_dataclass("a", [("a", int)])
actual = self.node.build_node(var.qname, var, attrs, {}, 10)

self.assertIsInstance(actual, ElementNode)
self.assertEqual(10, actual.position)
self.assertEqual(self.context.build(a), actual.meta)
self.assertEqual(attrs, actual.attrs)
self.assertEqual({}, actual.ns_map)
self.assertFalse(actual.mixed)

def test_build_node_with_wildcard_var(self):
var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="a")
var = XmlVarFactory.create(xml_type=XmlType.WILDCARD, qname="aaaaa")

actual = self.node.build_node(var, {}, {}, 10)
actual = self.node.build_node(var.qname, var, {}, {}, 10)

self.assertIsInstance(actual, WildcardNode)
self.assertEqual(10, actual.position)
Expand All @@ -532,7 +540,7 @@ def test_build_node_with_primitive_var(self):
)
attrs = {"a": "b"}
ns_map = {"ns0": "xsdata"}
actual = self.node.build_node(var, attrs, ns_map, 10)
actual = self.node.build_node(var.qname, var, attrs, ns_map, 10)

self.assertIsInstance(actual, PrimitiveNode)
self.assertEqual(ns_map, actual.ns_map)
Expand Down
23 changes: 23 additions & 0 deletions tests/formats/dataclass/serializers/test_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,29 @@ def test_write_choice_with_raw_value(self):
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_when_no_matching_choice_exists_but_value_is_model(self):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENTS,
name="compound",
qname="compound",
elements={
"a": XmlVarFactory.create(
xml_type=XmlType.WILDCARD, qname="a", types=(object,)
)
},
)

ebook = make_dataclass("eBook", [], bases=(BookForm,))
expected = [
(XmlWriterEvent.START, "eBook"),
(XmlWriterEvent.ATTR, "lang", "en"),
(XmlWriterEvent.END, "eBook"),
]

result = self.serializer.write_value(ebook(), var, None)
self.assertIsInstance(result, Generator)
self.assertEqual(expected, list(result))

def test_write_choice_when_no_matching_choice_exists(self):
var = XmlVarFactory.create(
xml_type=XmlType.ELEMENTS,
Expand Down
3 changes: 2 additions & 1 deletion tests/models/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,8 @@ def test_use_union_type_requires_310(self):
)

else:
self.assertTrue(GeneratorOutput(union_type=True).union_type)
output = GeneratorOutput(union_type=True, postponed_annotations=True)
self.assertTrue(output.union_type)

def test_format_slots_requires_310(self):
if sys.version_info < (3, 10):
Expand Down
48 changes: 31 additions & 17 deletions xsdata/formats/dataclass/parsers/nodes/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from xsdata.formats.dataclass.parsers.utils import PendingCollection
from xsdata.logger import logger
from xsdata.models.enums import DataType
from xsdata.utils import namespaces


class ElementNode(XmlNode):
Expand Down Expand Up @@ -222,8 +221,9 @@ def bind_wild_var(self, params: Dict, var: XmlVar, qname: str, value: Any) -> bo
items.append(value)
elif var.name in params:
previous = params[var.name]
if previous.qname:
factory = self.context.class_type.any_element
factory = self.context.class_type.any_element

if not isinstance(previous, factory) or previous.qname:
params[var.name] = factory(children=[previous])

params[var.name].children.append(value)
Expand All @@ -248,18 +248,9 @@ def prepare_generic_value(
) -> Any:
"""Prepare parsed value before binding to a wildcard field."""

if qname:
if qname and not self.context.class_type.is_model(value):
any_factory = self.context.class_type.any_element
derived_factory = self.context.class_type.derived_element

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)
) 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)
value = any_factory(qname=qname, text=converter.serialize(value))

return value

Expand Down Expand Up @@ -336,7 +327,7 @@ def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> XmlNode
for var in self.meta.find_children(qname):
unique = 0 if not var.is_element or var.list_element else var.index
if not unique or unique not in self.assigned:
node = self.build_node(var, attrs, ns_map, position)
node = self.build_node(qname, var, attrs, ns_map, position)

if node:
if unique:
Expand All @@ -350,7 +341,7 @@ def child(self, qname: str, attrs: Dict, ns_map: Dict, position: int) -> XmlNode
return nodes.SkipNode()

def build_node(
self, var: XmlVar, attrs: Dict, ns_map: Dict, position: int
self, qname: str, var: XmlVar, attrs: Dict, ns_map: Dict, position: int
) -> Optional[XmlNode]:
if var.is_clazz_union:
return nodes.UnionNode(
Expand All @@ -370,6 +361,7 @@ def build_node(
return self.build_element_node(
var.clazz,
var.derived,
var.nillable,
attrs,
ns_map,
position,
Expand Down Expand Up @@ -399,6 +391,7 @@ def build_node(
node = self.build_element_node(
clazz,
derived,
var.nillable,
attrs,
ns_map,
position,
Expand All @@ -410,6 +403,25 @@ def build_node(
if node:
return node

if var.process_contents != "skip":
clazz = self.context.find_type(qname)

if clazz:
node = self.build_element_node(
clazz,
False,
var.nillable,
attrs,
ns_map,
position,
None,
xsi_type,
xsi_nil,
)

if node:
return node

return nodes.WildcardNode(
var=var,
attrs=attrs,
Expand All @@ -422,6 +434,7 @@ def build_element_node(
self,
clazz: Type,
derived: bool,
nillable: bool,
attrs: Dict,
ns_map: Dict,
position: int,
Expand All @@ -430,8 +443,9 @@ def build_element_node(
xsi_nil: Optional[bool] = None,
) -> Optional[XmlNode]:
meta = self.context.fetch(clazz, self.meta.namespace, xsi_type)
nillable = nillable or meta.nillable

if not meta or (meta.nillable and xsi_nil is False):
if not meta or (xsi_nil is not None and nillable != xsi_nil):
return None

if xsi_type and not derived and not issubclass(meta.clazz, clazz):
Expand Down
1 change: 1 addition & 0 deletions xsdata/formats/dataclass/parsers/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def start(
format=None,
derived=False,
any_type=False,
process_contents="strict",
required=False,
nillable=False,
sequence=None,
Expand Down
Loading

0 comments on commit 62fb9e9

Please sign in to comment.