Skip to content

Commit 69c0132

Browse files
committed
Fix XSD attributes lazy build
- Remove AbstractSchemaProxy useless helper methods get_attribute_type and get_child_type. Code moved to EtreeElementNode.apply_schema() and attributes property.
1 parent 5d78691 commit 69c0132

File tree

2 files changed

+66
-125
lines changed

2 files changed

+66
-125
lines changed

elementpath/schema_proxy.py

Lines changed: 7 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,11 @@
1010
from abc import ABCMeta, abstractmethod
1111
from collections.abc import Iterator
1212
from functools import lru_cache
13-
from typing import cast, Any, Optional, Union
13+
from typing import Any, Optional, Union
1414

1515
from elementpath.exceptions import ElementPathTypeError
1616
from elementpath.protocols import XsdTypeProtocol, XsdAttributeProtocol, \
17-
XsdElementProtocol, XsdSchemaProtocol, XsdAttributeGroupProtocol
18-
from elementpath.namespaces import XSD_ANY_ATOMIC_TYPE
17+
XsdElementProtocol, XsdSchemaProtocol
1918
from elementpath.datatypes import AtomicType
2019
from elementpath.etree import is_etree_element
2120
from elementpath.xpath_context import XPathSchemaContext
@@ -143,47 +142,21 @@ def get_type(self, qname: str) -> Optional[XsdTypeProtocol]:
143142
"""
144143
return self._schema.maps.types.get(qname)
145144

146-
def get_attribute(self, qname: str, parent_type: Optional[XsdTypeProtocol] = None) \
147-
-> Optional[XsdAttributeProtocol]:
145+
def get_attribute(self, qname: str) -> Optional[XsdAttributeProtocol]:
148146
"""
149147
Get the XSD global attribute from the schema's scope.
150148
151149
:param qname: the fully qualified name of the attribute to retrieve.
152-
:param parent_type: an optional XSD type that represents the scope where matching \
153-
the attribute name. If not provided, the scope is assumed to be the global scope.
154-
:returns: an object that represents an XSD type or `None`.
155150
"""
156-
if parent_type is None:
157-
return self._schema.maps.attributes.get(qname)
158-
elif hasattr(parent_type, 'attributes'):
159-
attributes = cast(XsdAttributeGroupProtocol, parent_type.attributes)
160-
if qname in attributes:
161-
return attributes[qname]
162-
elif None in attributes and attributes[None].is_matching(qname):
163-
return self._schema.maps.attributes[qname]
164-
return None
165-
166-
def get_element(self, qname: str, parent_type: Optional[XsdTypeProtocol] = None) \
167-
-> Optional[XsdElementProtocol]:
151+
return self._schema.maps.attributes.get(qname)
152+
153+
def get_element(self, qname: str,) -> Optional[XsdElementProtocol]:
168154
"""
169155
Get the XSD global element from the schema's scope.
170156
171157
:param qname: the fully qualified name of the attribute to retrieve.
172-
:param parent_type: an optional XSD type that represents the scope where matching \
173-
the child element name. If `None` or not provided the scope is the schema.
174-
:returns: an object that represents an XSD type or `None`.
175158
"""
176-
if parent_type is None:
177-
return self._schema.maps.elements.get(qname)
178-
elif (content := parent_type.model_group) is not None:
179-
for xsd_element in content.iter_elements():
180-
if xsd_element.is_matching(qname):
181-
if xsd_element.name == qname:
182-
return xsd_element
183-
else:
184-
# a wildcard or a substitute
185-
return self._schema.maps.elements.get(qname)
186-
return None
159+
return self._schema.maps.elements.get(qname)
187160

188161
def get_substitution_group(self, qname: str) -> Optional[set[XsdElementProtocol]]:
189162
"""
@@ -195,63 +168,6 @@ def get_substitution_group(self, qname: str) -> Optional[set[XsdElementProtocol]
195168
"""
196169
return self._schema.maps.substitution_groups.get(qname)
197170

198-
def get_attribute_type(self, name: str, parent_type: Optional[XsdTypeProtocol] = None) \
199-
-> Optional[XsdTypeProtocol]:
200-
"""
201-
Get the XSD attribute type if the provided name is matching in the scope,
202-
otherwise return None.
203-
204-
:param name: the name of the attribute to retrieve.
205-
:param parent_type: an optional XSD type that represents the scope where matching \
206-
the attribute name. If not provided, the scope is assumed to be the global scope.
207-
"""
208-
if parent_type is None:
209-
try:
210-
return self._schema.maps.attributes[name].type
211-
except KeyError:
212-
return None
213-
elif hasattr(parent_type, 'attributes'):
214-
attributes = cast(XsdAttributeGroupProtocol, parent_type.attributes)
215-
if name in attributes:
216-
return attributes[name].type
217-
elif None in attributes and attributes[None].is_matching(name):
218-
try:
219-
return self._schema.maps.attributes[name].type
220-
except KeyError:
221-
return None
222-
elif name.startswith('{http://www.w3.org/2001/XMLSchema-instance}'):
223-
return self._schema.maps.types.get(XSD_ANY_ATOMIC_TYPE)
224-
225-
return None
226-
227-
def get_child_type(self, name: str, parent_type: Optional[XsdTypeProtocol] = None) \
228-
-> Optional[XsdTypeProtocol]:
229-
"""
230-
Get the child XSD type if the provided name is matching in the scope,
231-
otherwise return None.
232-
233-
:param name: the name of the child element to match.
234-
:param parent_type: an optional XSD type that represents the scope where matching \
235-
the child element name. If `None` or not provided the scope is the schema.
236-
"""
237-
if parent_type is None:
238-
try:
239-
return self._schema.maps.elements[name].type
240-
except KeyError:
241-
return None
242-
elif (content := parent_type.model_group) is not None:
243-
for xsd_element in content.iter_elements():
244-
if xsd_element.is_matching(name):
245-
if xsd_element.name == name:
246-
return xsd_element.type
247-
else:
248-
# a wildcard or a substitute
249-
try:
250-
return self._schema.maps.elements[name].type
251-
except KeyError:
252-
return None
253-
return None
254-
255171
@abstractmethod
256172
def is_instance(self, obj: Any, type_qname: str) -> bool:
257173
"""

elementpath/xpath_nodes.py

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,29 +1123,41 @@ def attributes(self) -> list[AttributeNode]:
11231123
for pos, (name, value) in enumerate(self.value.attrib.items(), position)
11241124
]
11251125

1126-
if self._attributes and self.xsd_type is not None \
1127-
and (schema := self.tree.schema) is not None:
1128-
if self.xsd_type.name == XSD_ANY_TYPE:
1129-
attribute_type = schema.get_type(XSD_ANY_SIMPLE_TYPE)
1130-
for attr in self._attributes:
1131-
attr.xsd_type = attribute_type
1132-
else:
1133-
for attr in self._attributes:
1134-
assert attr.name is not None
1135-
attr.xsd_type = schema.get_attribute_type(attr.name, self.xsd_type)
1126+
if self.xsd_type is None or (schema := self.tree.schema) is None:
1127+
return self._attributes
1128+
elif not schema.is_fully_valid():
1129+
any_simple_type = schema.get_type(XSD_ANY_SIMPLE_TYPE)
1130+
for attr in self._attributes:
1131+
attr.xsd_type = any_simple_type
1132+
return self._attributes
1133+
elif self.xsd_element is None or XSI_TYPE in self.value.attrib:
1134+
xsd_type = self.xsd_type
1135+
else:
1136+
xsd_type = cast(XsdTypeProtocol, self.xsd_element.type)
1137+
1138+
if hasattr(xsd_type, 'attributes'):
1139+
attributes = xsd_type.attributes
1140+
for attr in self._attributes:
1141+
assert attr.name is not None
1142+
if attr.name.startswith('{http://www.w3.org/2001/XMLSchema-instance}'):
1143+
attr.xsd_type = schema.get_type(XSD_ANY_ATOMIC_TYPE)
1144+
if attr.name in attributes:
1145+
attr.xsd_type = attributes[attr.name].type
1146+
elif None in attributes and attributes[None].is_matching(attr.name):
1147+
xsd_attribute = schema.get_attribute(attr.name)
1148+
if xsd_attribute is not None:
1149+
attr.xsd_type = xsd_attribute.type
11361150

1137-
if self.xsd_element is not None:
11381151
# Add missing attributes with a default value, at the same
11391152
# position of the last attribute.
11401153
position = position + len(self.value.attrib)
1141-
for name, xsd_attribute in self.xsd_element.attrib.items():
1142-
if name in self.value.attrib or name is None:
1143-
continue
1144-
1145-
if (value := getattr(xsd_attribute, 'value_constraint', None)) is not None:
1146-
attr = TextAttributeNode(name, value, self, position)
1147-
attr.xsd_type = xsd_attribute.type
1148-
self._attributes.append(attr)
1154+
for name, xsd_attribute in attributes.items():
1155+
if name is not None and name not in self.value.attrib:
1156+
value = getattr(xsd_attribute, 'value_constraint', None)
1157+
if value is not None:
1158+
attr = TextAttributeNode(name, value, self, position)
1159+
attr.xsd_type = xsd_attribute.type
1160+
self._attributes.append(attr)
11491161

11501162
return self._attributes
11511163

@@ -1216,7 +1228,8 @@ def apply_schema(self, schema: 'AbstractSchemaProxy') -> None:
12161228
return
12171229

12181230
if schema.base_element is not None:
1219-
xsd_types = [schema.base_element.type]
1231+
self.xsd_element = schema.base_element
1232+
xsd_types: list[Optional[XsdTypeProtocol]] = [schema.base_element.type]
12201233
children: Iterator[Any] = iter(self)
12211234
if schema.is_assertion_based():
12221235
self.xsd_type = schema.get_type(XSD_ANY_TYPE)
@@ -1250,15 +1263,27 @@ def apply_schema(self, schema: 'AbstractSchemaProxy') -> None:
12501263
continue
12511264
else:
12521265
xsd_type = schema.get_type(type_name)
1253-
if xsd_type is None:
1254-
node.clear_types()
1255-
continue
12561266
else:
1257-
node.xsd_element = schema.get_element(node.name, xsd_types[-1])
1258-
if node.xsd_element is None:
1259-
node.clear_types()
1260-
continue
1261-
xsd_type = node.xsd_element.type
1267+
if xsd_types[-1] is None:
1268+
xsd_element = schema.get_element(node.name)
1269+
elif (content := xsd_types[-1].model_group) is None:
1270+
xsd_element = None
1271+
else:
1272+
for xsd_element in content.iter_elements():
1273+
if xsd_element.is_matching(node.name):
1274+
if xsd_element.name != node.name:
1275+
# a wildcard or a substitute
1276+
xsd_element = schema.get_element(node.name)
1277+
break
1278+
else:
1279+
xsd_element = None
1280+
1281+
xsd_type = getattr(xsd_element, 'type', None)
1282+
node.xsd_element = xsd_element
1283+
1284+
if xsd_type is None:
1285+
node.clear_types()
1286+
continue
12621287

12631288
node.xsd_type = xsd_type
12641289
if hasattr(node, '_attributes'):
@@ -1278,12 +1303,12 @@ def apply_schema(self, schema: 'AbstractSchemaProxy') -> None:
12781303

12791304
def clear_types(self) -> None:
12801305
"""Clear XSD types for element node subtree."""
1281-
for elem in self.iter_descendants(with_self=True):
1282-
if isinstance(elem, EtreeElementNode):
1283-
elem.xsd_type = None
1284-
elem.xsd_element = None
1285-
for attr in elem.attributes:
1286-
attr.xsd_type = None
1306+
for node in self.iter_descendants(with_self=True):
1307+
if isinstance(node, EtreeElementNode):
1308+
node.xsd_type = None
1309+
node.xsd_element = None
1310+
if hasattr(node, '_attributes'):
1311+
delattr(node, '_attributes')
12871312

12881313
@property
12891314
def is_typed(self) -> bool:

0 commit comments

Comments
 (0)