Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions tests/formats/dataclass/parsers/nodes/test_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,37 @@ class ElementWrapper:
self.assertIsInstance(obj.elements[1], ElementObject)
self.assertEqual(obj.elements[0].content, "Hello")
self.assertEqual(obj.elements[1].content, "World")

def test_reused_item_name(self) -> None:
@dataclass
class Foo:
class Meta:
name = "Property"

foo_id: int = field(metadata={"name": "Foo-Id", "type": "Attribute"})

@dataclass
class Bar:
class Meta:
name = "Property"

bar_id: str = field(metadata={"name": "Bar-Id", "type": "Attribute"})

@dataclass
class Response:
foos: list[Foo] = field(
metadata={"wrapper": "Foos", "name": "Property", "type": "Element"}
)
bars: list[Bar] = field(
metadata={"wrapper": "Bars", "name": "Property", "type": "Element"}
)

xml = (
"<Response>"
'<Foos><Property Foo-Id="1"/><Property Foo-Id="2"/></Foos>'
'<Bars><Property Bar-Id="3"/><Property Bar-Id="4"/></Bars>'
"</Response>"
)
obj = self.parser.from_string(xml, clazz=Response)
self.assertEqual([Foo(foo_id=1), Foo(foo_id=2)], obj.foos)
self.assertEqual([Bar(bar_id="3"), Bar(bar_id="4")], obj.bars)
2 changes: 1 addition & 1 deletion xsdata/formats/dataclass/parsers/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def start(
try:
item = queue[-1]
if isinstance(item, ElementNode) and qname in item.meta.wrappers:
child = cast(XmlNode, WrapperNode(parent=item))
child = cast(XmlNode, WrapperNode(parent=item, qname=qname))
else:
child = item.child(qname, attrs, ns_map, len(objects))
except IndexError:
Expand Down
49 changes: 48 additions & 1 deletion xsdata/formats/dataclass/parsers/nodes/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
Attributes:
assigned: A set to store the processed sub-nodes
tail_processed: Whether the tail process is consumed
wrappers: A mapping of child item qname to the queue of wrapper
qnames the items were parsed under, in document order
"""

__slots__ = (
Expand All @@ -48,6 +50,7 @@
"ns_map",
"position",
"tail_processed",
"wrappers",
"xsi_nil",
"xsi_type",
)
Expand Down Expand Up @@ -78,6 +81,11 @@
self.xsi_nil = xsi_nil
self.assigned: set[int] = set()
self.tail_processed: bool = False
# Queue of wrapper qnames per child item qname, recorded in document
# order as children are built. It lets binding disambiguate sibling
# wrappers that reuse the same item element name (e.g. two wrappers
# whose items are both named ``Property``).
self.wrappers: dict[str, list[str]] = {}

def bind(
self,
Expand Down Expand Up @@ -250,7 +258,11 @@
Whether the parsed object can fit in one of class
parameters or not.
"""
wrapper = self.pop_wrapper(qname)
for var in self.meta.find_children(qname):
if wrapper and var.wrapper_qname != wrapper:
continue

if var.is_wildcard:
return self.bind_wild_var(params, var, qname, value)

Expand All @@ -259,6 +271,26 @@

return False

def pop_wrapper(self, qname: str) -> str | None:
"""Return the wrapper qname for the next child object of the qname.

The wrapper qnames are recorded in document order as children
are built, so popping the first one keeps binding aligned with
the parsed objects and lets sibling wrappers that reuse the same
item element name route to the correct field.

Args:
qname: The qualified name of the child element

Returns:
The wrapper qualified name or None if the child isn't wrapped.
"""
wrappers = self.wrappers.get(qname)
if wrappers:
return wrappers.pop(0)

return None

@classmethod
def bind_var(cls, params: dict, var: XmlVar, value: Any) -> bool:
"""Bind a child object to an element field.
Expand Down Expand Up @@ -438,7 +470,14 @@

return True

def child(self, qname: str, attrs: dict, ns_map: dict, position: int) -> XmlNode:
def child(

Check failure on line 473 in xsdata/formats/dataclass/parsers/nodes/element.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 22 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=tefra_xsdata&issues=AZ9HM-jvHa9CphlRV2bd&open=AZ9HM-jvHa9CphlRV2bd&pullRequest=1224
self,
qname: str,
attrs: dict,
ns_map: dict,
position: int,
wrapper: str | None = None,
) -> XmlNode:
"""Initialize the next child node to be queued, when an element starts.

This entry point is responsible to create the next node type
Expand All @@ -450,11 +489,16 @@
attrs: The element attributes
ns_map: The element namespace prefix-URI map
position: The current length of the intermediate objects
wrapper: The qualified name of the wrapper element the child
is nested under, if any

Raises:
ParserError: If the child element is unknown
"""
for var in self.meta.find_children(qname):
if wrapper and var.wrapper_qname != wrapper:
continue

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(qname, var, attrs, ns_map, position)
Expand All @@ -463,6 +507,9 @@
if unique:
self.assigned.add(unique)

if wrapper:
self.wrappers.setdefault(qname, []).append(wrapper)

return node

if self.config.fail_on_unknown_properties:
Expand Down
6 changes: 4 additions & 2 deletions xsdata/formats/dataclass/parsers/nodes/wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ class WrapperNode(XmlNode):

Args:
parent: The parent node
qname: The wrapper element qualified name

Attributes:
ns_map: The node namespace prefix-URI map
"""

def __init__(self, parent: ElementNode):
def __init__(self, parent: ElementNode, qname: str):
"""Initialize the xml node."""
self.parent = parent
self.qname = qname
self.ns_map = parent.ns_map

def bind(
Expand Down Expand Up @@ -52,4 +54,4 @@ def child(self, qname: str, attrs: dict, ns_map: dict, position: int) -> XmlNode
Returns:
The child xml node instance.
"""
return self.parent.child(qname, attrs, ns_map, position)
return self.parent.child(qname, attrs, ns_map, position, wrapper=self.qname)
Loading