Skip to content

Commit 84b1f48

Browse files
committed
Fix adding default and fixed values for attributes and elements
- Default an fixed values applied on omitted attributes and empty elements - Add xsd_element to element nodes and augment attributes with missing defaults in lazy mode. - Add test extracted from the issue and with another element for testing default value of attributes
1 parent 41ac5d1 commit 84b1f48

16 files changed

+336
-244
lines changed

elementpath/compare.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def etree_deep_equal(e1: ElementProtocol, e2: ElementProtocol) -> bool:
8383
return False
8484
elif isinstance(value1, etree_node_types):
8585
assert isinstance(value2, etree_node_types)
86-
if not etree_deep_equal(value1.obj, value2.obj):
86+
if not etree_deep_equal(value1.value, value2.value):
8787
return False
8888
elif isinstance(value1, EtreeDocumentNode):
8989
assert isinstance(value2, EtreeDocumentNode)
@@ -94,14 +94,14 @@ def etree_deep_equal(e1: ElementProtocol, e2: ElementProtocol) -> bool:
9494
return False
9595
elif isinstance(child1, etree_node_types):
9696
assert isinstance(child2, etree_node_types)
97-
if not etree_deep_equal(child1.obj, child2.obj):
97+
if not etree_deep_equal(child1.value, child2.value):
9898
return False
9999
elif isinstance(child1, TextNode):
100100
assert isinstance(child2, TextNode)
101-
if cm.ne(child1.obj, child2.obj):
101+
if cm.ne(child1.value, child2.value):
102102
return False
103103

104-
elif cm.ne(value1.obj, value2.obj):
104+
elif cm.ne(value1.value, value2.value):
105105
return False
106106
elif isinstance(value1, TextAttributeNode):
107107
if cm.ne(value1.name, value2.name):
@@ -257,7 +257,7 @@ def etree_deep_compare(e1: ElementProtocol, e2: ElementProtocol) -> int:
257257
raise xpath_error('XPTY0004', msg, token=token)
258258
elif isinstance(value1, etree_node_types):
259259
assert isinstance(value2, etree_node_types)
260-
result = etree_deep_compare(value1.obj, value2.obj)
260+
result = etree_deep_compare(value1.value, value2.value)
261261
if result:
262262
return result
263263
elif isinstance(value1, EtreeDocumentNode):
@@ -272,19 +272,19 @@ def etree_deep_compare(e1: ElementProtocol, e2: ElementProtocol) -> int:
272272
raise xpath_error('XPTY0004', msg, token=token)
273273
elif isinstance(child1, etree_node_types):
274274
assert isinstance(child2, etree_node_types)
275-
result = etree_deep_compare(child1.obj, child2.obj)
275+
result = etree_deep_compare(child1.value, child2.value)
276276
if result:
277277
return result
278278
elif isinstance(child1, TextNode):
279279
assert isinstance(child2, TextNode)
280280
result = cm.strcoll(
281-
child1.obj.strip(), child2.obj.strip()
281+
child1.value.strip(), child2.value.strip()
282282
)
283283
if result:
284284
return result
285285
elif isinstance(value1, TextNode):
286286
assert isinstance(value2, TextNode)
287-
result = cm.strcoll(value1.obj, value2.obj)
287+
result = cm.strcoll(value1.value, value2.value)
288288
if result:
289289
return result
290290
elif isinstance(value1, TextAttributeNode):

elementpath/extras/pathnodes.py

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
class PathElementNode(ElementNode):
2626
name: str
27-
obj: Path
27+
value: Path
2828
xsd_element: Optional[XsdElementProtocol]
2929

3030
__slots__ = ('stat',)
@@ -53,7 +53,7 @@ def __init__(self,
5353
self.parent = parent
5454

5555
self.name = path.name
56-
self.obj = path
56+
self.value = path
5757
self.stat = path.stat()
5858
self.position = self.stat.st_ino
5959
self.children = []
@@ -63,9 +63,9 @@ def __init__(self,
6363

6464
@property
6565
def content(self) -> Path:
66-
return self.obj
66+
return self.value
6767

68-
elem = value = content
68+
elem = content
6969

7070
@property
7171
def attributes(self) -> list[AttributeNode]:
@@ -101,10 +101,10 @@ def path(self) -> str:
101101

102102
@property
103103
def string_value(self) -> str:
104-
if self.obj.is_dir():
104+
if self.value.is_dir():
105105
return ''
106-
if self.obj.is_file():
107-
return self.obj.read_text()
106+
if self.value.is_file():
107+
return self.value.read_text()
108108
else:
109109
return ''
110110

@@ -125,12 +125,12 @@ def match_name(self, name: str, default_namespace: Optional[str] = None) -> bool
125125
return self.name == f'{{{default_namespace}}}{name}'
126126

127127
def get_document_node(self, replace: bool = True, as_parent: bool = True) -> 'DocumentNode':
128-
return PathDocumentNode(Path(self.obj.absolute().root))
128+
return PathDocumentNode(Path(self.value.absolute().root))
129129

130130
def __iter__(self) -> Iterator[ChildNodeType]:
131131
if not self.children:
132-
if self.obj.is_dir():
133-
for path in self.obj.iterdir():
132+
if self.value.is_dir():
133+
for path in self.value.iterdir():
134134
if path in self.tree.elements:
135135
self.children.append(self.tree.elements[path])
136136
else:
@@ -150,7 +150,7 @@ def iter_descendants(self, with_self: bool = True) -> Iterator[ChildNodeType]:
150150

151151

152152
class PathDocumentNode(DocumentNode):
153-
obj: Path
153+
value: Path
154154

155155
__slots__ = ('stat',)
156156

@@ -159,7 +159,7 @@ def __init__(self, document: Path, uri: Optional[str] = None) -> None:
159159
if not document.is_dir() or len(document.parts) > 1 or not document.is_absolute():
160160
raise ValueError(f'{document} must be a root directory')
161161

162-
self.obj = document
162+
self.value = document
163163
self.name = None
164164
self.parent = None
165165
self.position = document.stat().st_ino
@@ -168,15 +168,15 @@ def __init__(self, document: Path, uri: Optional[str] = None) -> None:
168168

169169
@property
170170
def document(self) -> Path:
171-
return self.obj
171+
return self.value
172172

173173
@property
174174
def string_value(self) -> str:
175-
return self.obj.read_text()
175+
return self.value.read_text()
176176

177177
def __iter__(self) -> Iterator[ChildNodeType]:
178178
if not self.children:
179-
for path in self.obj.iterdir():
179+
for path in self.value.iterdir():
180180
if path in self.tree.elements:
181181
self.children.append(self.tree.elements[path])
182182
else:
@@ -187,7 +187,7 @@ def __iter__(self) -> Iterator[ChildNodeType]:
187187

188188
class IntAttributeNode(AttributeNode):
189189
name: str
190-
obj: int
190+
value: int
191191
parent: Optional['PathElementNode']
192192

193193
__slots__ = ()
@@ -198,22 +198,18 @@ def __init__(self,
198198
parent: Optional['PathElementNode'] = None) -> None:
199199

200200
self.name = name
201-
self.obj = value
201+
self.value = value
202202
self.parent = parent
203203
self.position = parent.position if parent is not None else 1
204204
self.xsd_type = None
205205

206-
@property
207-
def value(self) -> int:
208-
return self.obj
209-
210206
@property
211207
def string_value(self) -> str:
212-
return str(self.obj)
208+
return str(self.value)
213209

214210
@property
215211
def iter_typed_values(self) -> Iterator[AtomicType]:
216-
yield self.obj
212+
yield self.value
217213

218214
@property
219215
def type_name(self) -> Optional[str]:
@@ -224,12 +220,12 @@ class ModeAttributeNode(IntAttributeNode):
224220

225221
@property
226222
def string_value(self) -> str:
227-
return oct(self.obj)
223+
return oct(self.value)
228224

229225

230226
class DatetimeAttributeNode(AttributeNode):
231227
name: str
232-
obj: datetime
228+
value: datetime
233229
parent: Optional['PathElementNode']
234230

235231
__slots__ = ()
@@ -240,22 +236,18 @@ def __init__(self,
240236
parent: Optional['PathElementNode'] = None) -> None:
241237

242238
self.name = name
243-
self.obj = value
239+
self.value = value
244240
self.parent = parent
245241
self.position = parent.position if parent is not None else 1
246242
self.xsd_type = None
247243

248-
@property
249-
def value(self) -> datetime:
250-
return self.obj
251-
252244
@property
253245
def string_value(self) -> str:
254-
return str(self.obj)
246+
return str(self.value)
255247

256248
@property
257249
def iter_typed_values(self) -> Iterator[AtomicType]:
258-
yield DateTime.fromdatetime(self.obj)
250+
yield DateTime.fromdatetime(self.value)
259251

260252
@property
261253
def type_name(self) -> Optional[str]:

elementpath/schema_proxy.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,23 +143,47 @@ def get_type(self, qname: str) -> Optional[XsdTypeProtocol]:
143143
"""
144144
return self._schema.maps.types.get(qname)
145145

146-
def get_attribute(self, qname: str) -> Optional[XsdAttributeProtocol]:
146+
def get_attribute(self, qname: str, parent_type: Optional[XsdTypeProtocol] = None) \
147+
-> Optional[XsdAttributeProtocol]:
147148
"""
148149
Get the XSD global attribute from the schema's scope.
149150
150151
: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.
151154
:returns: an object that represents an XSD type or `None`.
152155
"""
153-
return self._schema.maps.attributes.get(qname)
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
154165

155-
def get_element(self, qname: str) -> Optional[XsdElementProtocol]:
166+
def get_element(self, qname: str, parent_type: Optional[XsdTypeProtocol] = None) \
167+
-> Optional[XsdElementProtocol]:
156168
"""
157169
Get the XSD global element from the schema's scope.
158170
159-
:param qname: the fully qualified name of the element to retrieve.
171+
: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.
160174
:returns: an object that represents an XSD type or `None`.
161175
"""
162-
return self._schema.maps.elements.get(qname)
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
163187

164188
def get_substitution_group(self, qname: str) -> Optional[set[XsdElementProtocol]]:
165189
"""

elementpath/serialization.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def get_serialization_params(params: Union[None, ElementNode, XPathMap] = None,
139139
kwargs[key] = value
140140

141141
elif isinstance(params, ElementNode):
142-
root = cast(Union[EtreeElementProtocol, LxmlElementProtocol], params.obj)
142+
root = cast(Union[EtreeElementProtocol, LxmlElementProtocol], params.value)
143143
if root.tag != SERIALIZATION_PARAMS:
144144
msg = 'output:serialization-parameters tag expected'
145145
raise xpath_error('XPTY0004', msg, token)
@@ -293,14 +293,14 @@ def serialize_to_xml(elements: Iterable[Any],
293293
for item in iter_normalized(elements, item_separator):
294294
if isinstance(item, ElementNode):
295295
assert isinstance(item, EtreeElementNode)
296-
elem = item.obj
296+
elem = item.value
297297
elif isinstance(item, (AttributeNode, NamespaceNode)):
298298
raise xpath_error('SENR0001', token=token)
299299
elif isinstance(item, TextNode):
300300
if item.parent is not None and item.parent.name in cdata_section:
301-
chunks.append(f'<![CDATA[{item.obj}]]>')
301+
chunks.append(f'<![CDATA[{item.value}]]>')
302302
else:
303-
chunks.append(item.obj)
303+
chunks.append(item.value)
304304
continue
305305
elif not isinstance(item, str):
306306
raise xpath_error('SENR0001', token=token)
@@ -352,7 +352,7 @@ def default(self, obj: Any) -> Any:
352352
return ''.join(self.default(child) for child in obj)
353353
elif isinstance(obj, ElementNode):
354354
assert isinstance(obj, EtreeElementNode)
355-
elem = obj.obj
355+
elem = obj.value
356356
assert etree_module is not None
357357

358358
try:
@@ -368,7 +368,7 @@ def default(self, obj: Any) -> Any:
368368
elif isinstance(obj, (AttributeNode, NamespaceNode)):
369369
return f'{obj.name}="{obj.string_value}"'
370370
elif isinstance(obj, TextNode):
371-
return obj.obj
371+
return obj.value
372372
elif isinstance(obj, CommentNode):
373373
return f'<!--{obj.string_value}-->'
374374
else:

elementpath/tree_builders.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def get_node_tree(root: RootArgType,
5656
return root.getroot()
5757
elif fragment is False and \
5858
isinstance(root, ElementNode) and \
59-
is_etree_element_instance(root.obj):
59+
is_etree_element_instance(root.value):
6060
return root.get_document_node()
6161

6262
return root

elementpath/xpath1/_xpath1_functions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def select_id_function(self: XPathFunction, context: ContextType = None) \
146146

147147
if isinstance(item, (ElementNode, DocumentNode)):
148148
for element in item.iter_descendants():
149-
if isinstance(element, EtreeElementNode) and element.obj.get(XML_ID) == value:
149+
if isinstance(element, EtreeElementNode) and element.value.get(XML_ID) == value:
150150
yield element
151151

152152

@@ -380,11 +380,11 @@ def evaluate_lang_function(self: XPathFunction, context: ContextType = None) ->
380380
return False
381381
else:
382382
try:
383-
attr = context.item.obj.attrib[XML_LANG]
383+
attr = context.item.value.attrib[XML_LANG]
384384
except KeyError:
385385
for e in context.iter_ancestors():
386-
if isinstance(e, EtreeElementNode) and XML_LANG in e.obj.attrib:
387-
lang = e.obj.attrib[XML_LANG]
386+
if isinstance(e, EtreeElementNode) and XML_LANG in e.value.attrib:
387+
lang = e.value.attrib[XML_LANG]
388388
if not isinstance(lang, str):
389389
return False
390390
break

elementpath/xpath1/_xpath1_operators.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ def select_child_path(self: XPathToken, context: ContextType = None) \
673673
elif result in items:
674674
pass
675675
elif isinstance(result, ElementNode):
676-
if result.obj not in items:
676+
if result.value not in items:
677677
items.add(result)
678678
yield result
679679
else:
@@ -700,7 +700,7 @@ def select_descendant_path(self: XPathToken, context: ContextType = None) \
700700
elif result in items:
701701
pass
702702
elif isinstance(result, ElementNode):
703-
if result.obj not in items:
703+
if result.value not in items:
704704
items.add(result)
705705
yield result
706706
else:
@@ -723,7 +723,7 @@ def select_descendant_path(self: XPathToken, context: ContextType = None) \
723723
elif result in items:
724724
pass
725725
elif isinstance(result, ElementNode):
726-
if result.obj not in items:
726+
if result.value not in items:
727727
items.add(result)
728728
else:
729729
items.add(result)

0 commit comments

Comments
 (0)