elementpath 5.0.1__tar.gz → 5.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. {elementpath-5.0.1 → elementpath-5.0.3}/CHANGELOG.rst +11 -0
  2. {elementpath-5.0.1/elementpath.egg-info → elementpath-5.0.3}/PKG-INFO +1 -1
  3. {elementpath-5.0.1 → elementpath-5.0.3}/doc/conf.py +1 -1
  4. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/__init__.py +1 -1
  5. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/compare.py +15 -6
  6. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/datetime.py +1 -1
  7. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/etree.py +1 -1
  8. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/helpers.py +11 -10
  9. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/protocols.py +3 -2
  10. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/serialization.py +11 -6
  11. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/tree_builders.py +13 -17
  12. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath1/_xpath1_operators.py +6 -4
  13. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath1/xpath1_parser.py +4 -2
  14. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath2/_xpath2_functions.py +5 -0
  15. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath2/xpath2_parser.py +10 -3
  16. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/_xpath30_functions.py +10 -6
  17. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath31/_xpath31_functions.py +6 -4
  18. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath_context.py +1 -1
  19. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath_nodes.py +25 -10
  20. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath_selectors.py +22 -5
  21. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath_tokens.py +18 -10
  22. {elementpath-5.0.1 → elementpath-5.0.3/elementpath.egg-info}/PKG-INFO +1 -1
  23. {elementpath-5.0.1 → elementpath-5.0.3}/pyproject.toml +1 -1
  24. {elementpath-5.0.1 → elementpath-5.0.3}/tests/mypy_tests/protocols.py +1 -1
  25. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_helpers.py +12 -7
  26. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_selectors.py +11 -1
  27. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath1_parser.py +76 -13
  28. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath2_functions.py +6 -0
  29. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath2_parser.py +25 -0
  30. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath30.py +31 -0
  31. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath_nodes.py +3 -3
  32. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath_tokens.py +3 -1
  33. {elementpath-5.0.1 → elementpath-5.0.3}/tox.ini +26 -13
  34. {elementpath-5.0.1 → elementpath-5.0.3}/LICENSE +0 -0
  35. {elementpath-5.0.1 → elementpath-5.0.3}/MANIFEST.in +0 -0
  36. {elementpath-5.0.1 → elementpath-5.0.3}/README.rst +0 -0
  37. {elementpath-5.0.1 → elementpath-5.0.3}/doc/Makefile +0 -0
  38. {elementpath-5.0.1 → elementpath-5.0.3}/doc/advanced.rst +0 -0
  39. {elementpath-5.0.1 → elementpath-5.0.3}/doc/index.rst +0 -0
  40. {elementpath-5.0.1 → elementpath-5.0.3}/doc/introduction.rst +0 -0
  41. {elementpath-5.0.1 → elementpath-5.0.3}/doc/make.bat +0 -0
  42. {elementpath-5.0.1 → elementpath-5.0.3}/doc/pratt_api.rst +0 -0
  43. {elementpath-5.0.1 → elementpath-5.0.3}/doc/requirements.txt +0 -0
  44. {elementpath-5.0.1 → elementpath-5.0.3}/doc/xpath_api.rst +0 -0
  45. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/aliases.py +0 -0
  46. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/collations.py +0 -0
  47. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/__init__.py +0 -0
  48. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/atomic_types.py +0 -0
  49. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/binary.py +0 -0
  50. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/numeric.py +0 -0
  51. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/proxies.py +0 -0
  52. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/qname.py +0 -0
  53. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/string.py +0 -0
  54. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/untyped.py +0 -0
  55. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/datatypes/uri.py +0 -0
  56. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/decoder.py +0 -0
  57. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/exceptions.py +0 -0
  58. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/extras/__init__.py +0 -0
  59. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/extras/pathnodes.py +0 -0
  60. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/namespaces.py +0 -0
  61. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/py.typed +0 -0
  62. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/__init__.py +0 -0
  63. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/categories_fallback.py +0 -0
  64. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/character_classes.py +0 -0
  65. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/codepoints.py +0 -0
  66. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/patterns.py +0 -0
  67. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/unicode_blocks.py +0 -0
  68. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/unicode_categories.py +0 -0
  69. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/regex/unicode_subsets.py +0 -0
  70. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/schema_proxy.py +0 -0
  71. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/sequence_types.py +0 -0
  72. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/tdop.py +0 -0
  73. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/validators/__init__.py +0 -0
  74. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/validators/analyze-string.xsd +0 -0
  75. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/validators/schema-for-json.xsd +0 -0
  76. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath1/__init__.py +0 -0
  77. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath1/_xpath1_axes.py +0 -0
  78. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath1/_xpath1_functions.py +0 -0
  79. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath2/__init__.py +0 -0
  80. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath2/_xpath2_constructors.py +0 -0
  81. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath2/_xpath2_operators.py +0 -0
  82. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath3.py +0 -0
  83. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/__init__.py +0 -0
  84. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/_translation_maps.py +0 -0
  85. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/_xpath30_operators.py +0 -0
  86. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/xpath30_helpers.py +0 -0
  87. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath30/xpath30_parser.py +0 -0
  88. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath31/__init__.py +0 -0
  89. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath31/_xpath31_operators.py +0 -0
  90. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath/xpath31/xpath31_parser.py +0 -0
  91. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath.egg-info/SOURCES.txt +0 -0
  92. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath.egg-info/dependency_links.txt +0 -0
  93. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath.egg-info/requires.txt +0 -0
  94. {elementpath-5.0.1 → elementpath-5.0.3}/elementpath.egg-info/top_level.txt +0 -0
  95. {elementpath-5.0.1 → elementpath-5.0.3}/requirements-dev.txt +0 -0
  96. {elementpath-5.0.1 → elementpath-5.0.3}/scripts/generate_codepoints.py +0 -0
  97. {elementpath-5.0.1 → elementpath-5.0.3}/setup.cfg +0 -0
  98. {elementpath-5.0.1 → elementpath-5.0.3}/tests/__init__.py +0 -0
  99. {elementpath-5.0.1 → elementpath-5.0.3}/tests/memory_profiling.py +0 -0
  100. {elementpath-5.0.1 → elementpath-5.0.3}/tests/mypy_tests/advanced.py +0 -0
  101. {elementpath-5.0.1 → elementpath-5.0.3}/tests/mypy_tests/selectors.py +0 -0
  102. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/analyze-string.xsd +0 -0
  103. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/external_entity.xml +0 -0
  104. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/sample.xml +0 -0
  105. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/schema-for-json.xsd +0 -0
  106. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/unparsed_entity.xml +0 -0
  107. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/unused_external_entity.xml +0 -0
  108. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/unused_unparsed_entity.xml +0 -0
  109. {elementpath-5.0.1 → elementpath-5.0.3}/tests/resources/with_entity.xml +0 -0
  110. {elementpath-5.0.1 → elementpath-5.0.3}/tests/run_all_tests.py +0 -0
  111. {elementpath-5.0.1 → elementpath-5.0.3}/tests/run_typing_tests.py +0 -0
  112. {elementpath-5.0.1 → elementpath-5.0.3}/tests/run_w3c_tests.py +0 -0
  113. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_collations.py +0 -0
  114. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_compare.py +0 -0
  115. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_datatypes.py +0 -0
  116. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_decoder.py +0 -0
  117. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_etree.py +0 -0
  118. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_exceptions.py +0 -0
  119. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_namespaces.py +0 -0
  120. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_package.py +0 -0
  121. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_pathnodes.py +0 -0
  122. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_regex.py +0 -0
  123. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_schema_context.py +0 -0
  124. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_schema_proxy.py +0 -0
  125. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_sequence_types.py +0 -0
  126. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_serialization.py +0 -0
  127. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_tdop_parser.py +0 -0
  128. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_tree_builders.py +0 -0
  129. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_validators.py +0 -0
  130. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath2_constructors.py +0 -0
  131. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath31.py +0 -0
  132. {elementpath-5.0.1 → elementpath-5.0.3}/tests/test_xpath_context.py +0 -0
  133. {elementpath-5.0.1 → elementpath-5.0.3}/tests/xpath_test_class.py +0 -0
@@ -2,6 +2,15 @@
2
2
  CHANGELOG
3
3
  *********
4
4
 
5
+ `v5.0.3`_ (2025-06-28)
6
+ ======================
7
+ * Fix for XPath 1.0 processing of schema annotated XML data (issue #93)
8
+
9
+ `v5.0.2`_ (2025-06-18)
10
+ ======================
11
+ * Fix for XPath 2.0 *fn:node-name* (issue #91)
12
+ * Workaround for processing arguments with multiple occurrences for external functions (issue #92)
13
+
5
14
  `v5.0.1`_ (2025-05-11)
6
15
  ======================
7
16
  * Fix XDM type labeling with element and xsi:type substitutions (issue #89)
@@ -507,3 +516,5 @@ CHANGELOG
507
516
  .. _v4.8.0: https://github.com/sissaschool/elementpath/compare/v4.7.0...v4.8.0
508
517
  .. _v5.0.0: https://github.com/sissaschool/elementpath/compare/v4.8.0...v5.0.0
509
518
  .. _v5.0.1: https://github.com/sissaschool/elementpath/compare/v5.0.0...v5.0.1
519
+ .. _v5.0.2: https://github.com/sissaschool/elementpath/compare/v5.0.1...v5.0.2
520
+ .. _v5.0.3: https://github.com/sissaschool/elementpath/compare/v5.0.2...v5.0.3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elementpath
3
- Version: 5.0.1
3
+ Version: 5.0.3
4
4
  Summary: XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml
5
5
  Author-email: Davide Brunato <brunato@sissa.it>
6
6
  License-Expression: MIT
@@ -31,7 +31,7 @@ author = 'Davide Brunato'
31
31
  # The short X.Y version
32
32
  version = '5.0'
33
33
  # The full version, including alpha/beta/rc tags
34
- release = '5.0.1'
34
+ release = '5.0.3'
35
35
 
36
36
  # -- General configuration ---------------------------------------------------
37
37
 
@@ -7,7 +7,7 @@
7
7
  #
8
8
  # @author Davide Brunato <brunato@sissa.it>
9
9
  #
10
- __version__ = '5.0.1'
10
+ __version__ = '5.0.3'
11
11
  __author__ = "Davide Brunato"
12
12
  __contact__ = "brunato@sissa.it"
13
13
  __copyright__ = "Copyright 2018-2025, SISSA"
@@ -66,10 +66,17 @@ def deep_equal(seq1: Iterable[Any],
66
66
 
67
67
  if (value1 is None) ^ (value2 is None):
68
68
  return False
69
+ elif isinstance(value1, (XPathArray, XPathMap, XPathNode)) ^ \
70
+ isinstance(value2, (XPathArray, XPathMap, XPathNode)):
71
+ return False
69
72
  elif value1 is None:
70
73
  return True
71
- elif isinstance(value1, XPathNode) ^ isinstance(value2, XPathNode):
72
- return False
74
+ elif isinstance(value1, XPathMap):
75
+ assert isinstance(value2, XPathMap)
76
+ return value1 == value2
77
+ elif isinstance(value1, XPathArray):
78
+ assert isinstance(value2, XPathArray)
79
+ return value1 == value2
73
80
  elif isinstance(value1, XPathNode):
74
81
  assert isinstance(value2, XPathNode)
75
82
  if value1.__class__ != value2.__class__:
@@ -187,9 +194,10 @@ def deep_compare(obj1: Any,
187
194
 
188
195
  def etree_deep_compare(e1: ElementProtocol, e2: ElementProtocol) -> int:
189
196
  nonlocal result
190
- result = cm.strcoll(e1.tag, e2.tag)
191
- if result:
192
- return result
197
+ if not callable(e1.tag) and not callable(e2.tag):
198
+ result = cm.strcoll(e1.tag, e2.tag)
199
+ if result:
200
+ return result
193
201
 
194
202
  result = cm.strcoll((e1.text or '').strip(), (e2.text or '').strip())
195
203
  if result:
@@ -315,7 +323,8 @@ def deep_compare(obj1: Any,
315
323
 
316
324
  elif isinstance(value1, float):
317
325
  if math.isnan(value1):
318
- if not math.isnan(value2):
326
+ if not isinstance(value2, (float, Decimal)) \
327
+ or not math.isnan(value2):
319
328
  return -1
320
329
  elif math.isinf(value1):
321
330
  if value1 != value2:
@@ -258,7 +258,7 @@ class AbstractDateTime(AnyAtomicType):
258
258
  return cast(Timezone, self._dt.tzinfo)
259
259
 
260
260
  @tzinfo.setter
261
- def tzinfo(self, tz: Timezone) -> None:
261
+ def tzinfo(self, tz: Optional[Timezone]) -> None:
262
262
  self._dt = self._dt.replace(tzinfo=tz)
263
263
 
264
264
  def tzname(self) -> Optional[str]:
@@ -156,7 +156,7 @@ def etree_iter_paths(elem: ElementProtocol, path: str = '.') \
156
156
 
157
157
  for child in elem:
158
158
  if callable(child.tag):
159
- if child.tag.__name__ == 'Comment': # type: ignore[attr-defined]
159
+ if child.tag.__name__ == 'Comment':
160
160
  comment_nodes += 1
161
161
  yield child, f'{path}/comment()[{comment_nodes}]'
162
162
  continue
@@ -84,7 +84,7 @@ class Patterns:
84
84
  # Regex patterns related to names and namespaces
85
85
  namespace_uri = LazyPattern(r'{([^}]+)}')
86
86
  expanded_name = LazyPattern(
87
- r'^(?:{(?P<namespace>[^}]+)})?'
87
+ r'^(?:(?:Q{|{)(?P<namespace>[^}]*)})?'
88
88
  r'(?P<local>[^\d\W][\w\-.\u00B7\u0300-\u036F\u0387\u06DD\u06DE\u203F\u2040]*)$',
89
89
  )
90
90
  unbound_expanded_name = LazyPattern(
@@ -259,19 +259,20 @@ def not_equal(op1: Any, op2: Any) -> bool:
259
259
 
260
260
 
261
261
  def match_wildcard(name: Optional[str], wildcard: str) -> bool:
262
- if name is None:
262
+ if not name:
263
263
  return False
264
- elif wildcard == '*' or wildcard == '*:*':
264
+ elif wildcard in ('*', '{*}*'):
265
265
  return True
266
- elif wildcard.startswith('*:'):
267
- if name.startswith('{'):
268
- return name.endswith(f'}}{wildcard[2:]}')
269
- else:
270
- return name == wildcard[2:]
271
- elif wildcard.startswith('{') and wildcard.endswith('}*') or wildcard.endswith(':*'):
266
+ elif wildcard == '{}*':
267
+ return name[0] != '{' or name[:2] == '{}'
268
+ elif wildcard[-1] == '*':
272
269
  return name.startswith(wildcard[:-1])
273
- else:
270
+ elif not wildcard.startswith('{*}'):
274
271
  return False
272
+ elif name[0] == '{':
273
+ return name.endswith(wildcard[2:])
274
+ else:
275
+ return name == wildcard[3:]
275
276
 
276
277
 
277
278
  def escape_json_string(s: str, escaped: bool = False) -> str:
@@ -10,7 +10,8 @@
10
10
  """
11
11
  Define type hints protocols for XPath related objects.
12
12
  """
13
- from collections.abc import Hashable, Iterator, Iterable, ItemsView, Mapping, Sequence, Sized
13
+ from collections.abc import Callable, Hashable, Iterator, Iterable, \
14
+ ItemsView, Mapping, Sequence, Sized
14
15
  from typing import overload, Any, Optional, Protocol, Union, TypeVar
15
16
  from xml.etree.ElementTree import Element, ElementTree
16
17
 
@@ -64,7 +65,7 @@ class ElementProtocol(Sized, Hashable, Protocol):
64
65
  def get(self, key: str, default: Optional[_T] = None) -> Union[str, _T, None]: ...
65
66
 
66
67
  @property
67
- def tag(self) -> str: ...
68
+ def tag(self) -> Union[str, Callable[[], 'ElementProtocol']]: ...
68
69
 
69
70
  @property
70
71
  def text(self) -> Optional[str]: ...
@@ -20,6 +20,7 @@ from elementpath.datatypes import AnyAtomicType, AnyURI, AbstractDateTime, \
20
20
  AbstractBinary, UntypedAtomic, QName
21
21
  from elementpath.xpath_nodes import XPathNode, ElementNode, AttributeNode, DocumentNode, \
22
22
  NamespaceNode, TextNode, CommentNode
23
+ from elementpath.xpath_nodes import EtreeElementNode
23
24
  from elementpath.xpath_tokens import XPathToken, XPathMap, XPathArray
24
25
  from elementpath.protocols import EtreeElementProtocol, LxmlElementProtocol
25
26
 
@@ -147,7 +148,9 @@ def get_serialization_params(params: Union[None, ElementNode, XPathMap] = None,
147
148
  raise xpath_error('SEPM0019', token=token)
148
149
 
149
150
  for child in root:
150
- if child.tag == SER_PARAM_OMIT_XML_DECLARATION:
151
+ if callable(child.tag):
152
+ continue
153
+ elif child.tag == SER_PARAM_OMIT_XML_DECLARATION:
151
154
  value = child.get('value')
152
155
  if value not in ('yes', 'no') or len(child.attrib) > 1:
153
156
  raise xpath_error('SEPM0017', token=token)
@@ -289,7 +292,8 @@ def serialize_to_xml(elements: Iterable[Any],
289
292
  chunks = []
290
293
  for item in iter_normalized(elements, item_separator):
291
294
  if isinstance(item, ElementNode):
292
- item = item.obj
295
+ assert isinstance(item, EtreeElementNode)
296
+ elem = item.obj
293
297
  elif isinstance(item, (AttributeNode, NamespaceNode)):
294
298
  raise xpath_error('SENR0001', token=token)
295
299
  elif isinstance(item, TextNode):
@@ -306,15 +310,15 @@ def serialize_to_xml(elements: Iterable[Any],
306
310
 
307
311
  try:
308
312
  cks = etree_module.tostringlist(
309
- item, encoding='utf-8', method=method, **kwargs
313
+ elem, encoding='utf-8', method=method, **kwargs
310
314
  )
311
315
  except TypeError:
312
- ck = etree_module.tostring(item, encoding='utf-8', method=method)
313
- chunks.append(ck.decode('utf-8').rstrip(item.tail))
316
+ ck = etree_module.tostring(elem, encoding='utf-8', method=method)
317
+ chunks.append(ck.decode('utf-8').rstrip(elem.tail))
314
318
  else:
315
319
  if cks and cks[0].startswith(b'<?'):
316
320
  cks[0] = cks[0].replace(b'\'', b'"')
317
- chunks.append(b'\n'.join(cks).decode('utf-8').rstrip(item.tail))
321
+ chunks.append(b'\n'.join(cks).decode('utf-8').rstrip(elem.tail))
318
322
 
319
323
  if not character_map:
320
324
  return (item_separator or '').join(chunks)
@@ -347,6 +351,7 @@ def serialize_to_json(elements: Iterable[Any],
347
351
  if isinstance(obj, DocumentNode):
348
352
  return ''.join(self.default(child) for child in obj)
349
353
  elif isinstance(obj, ElementNode):
354
+ assert isinstance(obj, EtreeElementNode)
350
355
  elem = obj.obj
351
356
  assert etree_module is not None
352
357
 
@@ -7,6 +7,7 @@
7
7
  #
8
8
  # @author Davide Brunato <brunato@sissa.it>
9
9
  #
10
+ from collections import deque
10
11
  from collections.abc import Iterator
11
12
  from typing import cast, Any, Optional, Union
12
13
 
@@ -46,18 +47,13 @@ def get_node_tree(root: RootArgType,
46
47
  `False` is provided creates a dummy document when the root is an Element instance. \
47
48
  For default the root node kind is preserved.
48
49
  """
49
- root_node: RootNodeType
50
-
51
50
  if isinstance(root, (DocumentNode, ElementNode)):
52
51
  if uri is not None and root.uri is None:
53
52
  root.tree.uri = uri
54
53
 
55
54
  if fragment:
56
55
  if isinstance(root, DocumentNode):
57
- root_node = root.getroot()
58
- if root_node.uri is None:
59
- root_node.uri = root.uri
60
- return root_node
56
+ return root.getroot()
61
57
  elif fragment is False and \
62
58
  isinstance(root, ElementNode) and \
63
59
  is_etree_element_instance(root.obj):
@@ -80,9 +76,7 @@ def get_node_tree(root: RootArgType,
80
76
  cast(SchemaElemType, root), uri
81
77
  )
82
78
  else:
83
- return build_node_tree(
84
- cast(ElementTreeRootType, root), namespaces, uri, fragment
85
- )
79
+ return build_node_tree(root, namespaces, uri, fragment)
86
80
 
87
81
 
88
82
  def build_node_tree(root: ElementTreeRootType,
@@ -146,8 +140,8 @@ def build_node_tree(root: ElementTreeRootType,
146
140
  position += 1
147
141
 
148
142
  children = iter(elem)
149
- iterators: list[Any] = []
150
- ancestors: list[Any] = []
143
+ iterators: deque[Any] = deque()
144
+ ancestors: deque[Any] = deque()
151
145
  parent = root_node
152
146
 
153
147
  while True:
@@ -160,7 +154,7 @@ def build_node_tree(root: ElementTreeRootType,
160
154
  TextNode(elem.text, child, position)
161
155
  position += 1
162
156
 
163
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
157
+ elif elem.tag.__name__ == 'Comment':
164
158
  child = CommentNode(elem, parent, position)
165
159
  position += 1
166
160
  else:
@@ -239,7 +233,8 @@ def build_lxml_node_tree(root: LxmlRootType,
239
233
 
240
234
  # Add root siblings (comments and processing instructions)
241
235
  for elem in reversed([x for x in root_elem.itersiblings(preceding=True)]):
242
- if elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
236
+ assert callable(elem.tag)
237
+ if elem.tag.__name__ == 'Comment':
243
238
  CommentNode(elem, document_node, position)
244
239
  position += 1
245
240
  else:
@@ -276,8 +271,8 @@ def build_lxml_node_tree(root: LxmlRootType,
276
271
  position += 1
277
272
 
278
273
  children = iter(root_elem)
279
- iterators: list[Any] = []
280
- ancestors: list[Any] = []
274
+ iterators: deque[Any] = deque()
275
+ ancestors: deque[Any] = deque()
281
276
  parent = root_node
282
277
 
283
278
  while True:
@@ -293,7 +288,7 @@ def build_lxml_node_tree(root: LxmlRootType,
293
288
  TextNode(elem.text, child, position)
294
289
  position += 1
295
290
 
296
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
291
+ elif elem.tag.__name__ == 'Comment':
297
292
  child = CommentNode(elem, parent, position)
298
293
  position += 1
299
294
  else:
@@ -319,7 +314,8 @@ def build_lxml_node_tree(root: LxmlRootType,
319
314
 
320
315
  # Add root following siblings (comments and processing instructions)
321
316
  for elem in root_elem.itersiblings():
322
- if elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
317
+ assert callable(elem.tag)
318
+ if elem.tag.__name__ == 'Comment':
323
319
  CommentNode(elem, document_node, position)
324
320
  position += 1
325
321
  else:
@@ -124,7 +124,7 @@ class _PrefixedReferenceToken(XPathToken):
124
124
  prefix = self[0].value
125
125
  assert isinstance(prefix, str)
126
126
  if prefix == '*':
127
- return '*:%s' % self[1].value
127
+ return '{*}%s' % self[1].value
128
128
  else:
129
129
  return f'{{{self.get_namespace(prefix)}}}{self[1].value}'
130
130
 
@@ -224,10 +224,12 @@ def nud_namespace_uri(self: XPathToken) -> XPathToken:
224
224
  cls: type[XPathToken] = self.parser.symbol_table['(string)']
225
225
  self[:] = cls(self.parser, namespace), self.parser.expression(90)
226
226
 
227
- if not self[0].value:
228
- self.value = self[1].value
229
- else:
227
+ if self[0].value:
230
228
  self.value = f'{{{self[0].value}}}{self[1].value}'
229
+ elif self[1].value == '*':
230
+ self.value = '{}*'
231
+ else:
232
+ self.value = self[1].value
231
233
  return self
232
234
 
233
235
 
@@ -277,8 +277,10 @@ class XPath1Parser(Parser[XPathTokenType]):
277
277
  not isinstance(self.next_token, (XPathFunction, XPathAxis)) and \
278
278
  self.name_pattern.match(self.next_token.symbol) is not None:
279
279
  # Disambiguation replacing the next token with a '(name)' token
280
- cls = cast(type[XPathToken], self.symbol_table['(name)'])
281
- self.next_token = cls(self, self.next_token.symbol)
280
+ cls = self.symbol_table['(name)']
281
+ assert not issubclass(cls, XPathFunction)
282
+ value = self.next_token.symbol
283
+ self.next_token = cls(self, value)
282
284
  else:
283
285
  raise self.next_token.wrong_syntax(message)
284
286
 
@@ -272,8 +272,13 @@ def evaluate_node_name_function(self: XPathFunction, context: ContextType = None
272
272
  elif name.startswith('{'):
273
273
  # name is a QName in extended format
274
274
  namespace, local_name = split_expanded_name(name)
275
+ if not namespace:
276
+ return QName('', local_name)
277
+
275
278
  for pfx, uri in self.parser.namespaces.items():
276
279
  if uri == namespace:
280
+ if not pfx:
281
+ return QName(uri, local_name)
277
282
  return QName(uri, '{}:{}'.format(pfx, local_name))
278
283
  raise self.error('FONS0004', 'no prefix found for namespace {}'.format(namespace))
279
284
  else:
@@ -10,6 +10,7 @@
10
10
  """
11
11
  XPath 2.0 implementation - part 1 (parser class and symbols)
12
12
  """
13
+ import copy
13
14
  from abc import ABCMeta
14
15
  import locale
15
16
  from collections.abc import Callable, MutableMapping
@@ -355,7 +356,7 @@ class XPath2Parser(XPath1Parser):
355
356
  )
356
357
 
357
358
  if self.symbol_table is self.__class__.symbol_table:
358
- self.symbol_table = dict(self.__class__.symbol_table)
359
+ self.symbol_table = copy.copy(self.symbol_table)
359
360
  self.symbol_table[symbol] = token_class
360
361
  self.tokenizer = None
361
362
 
@@ -399,7 +400,7 @@ class XPath2Parser(XPath1Parser):
399
400
  lookup_name = qname.expanded_name
400
401
 
401
402
  if self.symbol_table is self.__class__.symbol_table:
402
- self.symbol_table = dict(self.__class__.symbol_table)
403
+ self.symbol_table = copy.copy(self.symbol_table)
403
404
 
404
405
  if lookup_name in self.symbol_table:
405
406
  msg = f'function {qname.qname!r} is already registered'
@@ -439,7 +440,13 @@ class XPath2Parser(XPath1Parser):
439
440
  context: Optional[XPathContext] = None) -> Any:
440
441
  args = []
441
442
  for k in range(len(self_)):
442
- arg = self_.get_argument(context, index=k)
443
+ try:
444
+ if sequence_types[k][-1] in '+*':
445
+ arg = self_[k].evaluate(context)
446
+ else:
447
+ arg = self_.get_argument(context, index=k)
448
+ except IndexError:
449
+ arg = self_.get_argument(context, index=k)
443
450
  args.append(arg)
444
451
 
445
452
  if sequence_types:
@@ -25,7 +25,6 @@ from urllib.request import urlopen
25
25
  from urllib.error import URLError
26
26
 
27
27
  from elementpath.aliases import Emptiable
28
- from elementpath.protocols import ElementProtocol
29
28
  from elementpath.exceptions import ElementPathError
30
29
  from elementpath.tdop import MultiLabel
31
30
  from elementpath.helpers import OCCURRENCE_INDICATORS, Patterns, \
@@ -660,7 +659,7 @@ def evaluate_format_number_function(self: XPathFunction, context: ContextType =
660
659
 
661
660
  minus_sign = decimal_format['minus-sign']
662
661
 
663
- prefix = ''
662
+ prefix: str = ''
664
663
  if value >= 0:
665
664
  subpic = sub_pictures[0]
666
665
  else:
@@ -683,7 +682,8 @@ def evaluate_format_number_function(self: XPathFunction, context: ContextType =
683
682
  suffix = percent_sign
684
683
  subpic = subpic[:-1]
685
684
 
686
- if value.as_tuple().exponent < 0:
685
+ exponent = value.as_tuple().exponent
686
+ if isinstance(exponent, int) and exponent < 0:
687
687
  value *= 100
688
688
  else:
689
689
  value = decimal.Decimal(int(value) * 100)
@@ -692,7 +692,8 @@ def evaluate_format_number_function(self: XPathFunction, context: ContextType =
692
692
  suffix = per_mille_sign
693
693
  subpic = subpic[:-1]
694
694
 
695
- if value.as_tuple().exponent < 0:
695
+ exponent = value.as_tuple().exponent
696
+ if isinstance(exponent, int) and exponent < 0:
696
697
  value *= 1000
697
698
  else:
698
699
  value = decimal.Decimal(int(value) * 1000)
@@ -931,7 +932,7 @@ def evaluate_format_date_time_functions(self: XPathFunction, context: ContextTyp
931
932
  sequence_types=('xs:string?', 'xs:string', 'xs:string',
932
933
  'element(fn:analyze-string-result)')))
933
934
  def evaluate_analyze_string_function(self: XPathFunction, context: ContextType = None) \
934
- -> ElementProtocol:
935
+ -> ElementNode:
935
936
  if self.context is not None:
936
937
  context = self.context
937
938
 
@@ -1055,7 +1056,7 @@ def evaluate_analyze_string_function(self: XPathFunction, context: ContextType =
1055
1056
  else:
1056
1057
  root = context.etree.XML(''.join(lines))
1057
1058
 
1058
- return cast(ElementProtocol, get_node_tree(root=root, namespaces=self.parser.namespaces))
1059
+ return cast(ElementNode, get_node_tree(root=root, namespaces=self.parser.namespaces))
1059
1060
 
1060
1061
 
1061
1062
  ###
@@ -1760,6 +1761,9 @@ def evaluate_node_name_function(self: XPathFunction, context: ContextType = None
1760
1761
  elif name.startswith('{'):
1761
1762
  # name is a QName in extended format
1762
1763
  namespace, local_name = split_expanded_name(name)
1764
+ if not namespace:
1765
+ return QName('', local_name)
1766
+
1763
1767
  for pfx, uri in self.parser.namespaces.items():
1764
1768
  if uri == namespace:
1765
1769
  if not pfx:
@@ -709,7 +709,7 @@ def evaluate_parse_json_functions(self: XPathFunction, context: ContextType = No
709
709
  if json_text is None:
710
710
  return []
711
711
 
712
- def _fallback(*args: Any, context: ContextType = None) -> str:
712
+ def _fallback(*a: Any, **kw: Any) -> str:
713
713
  return '\uFFFD'
714
714
 
715
715
  liberal = False
@@ -748,7 +748,7 @@ def evaluate_parse_json_functions(self: XPathFunction, context: ContextType = No
748
748
  fallback = cast(Callable[..., str], v)
749
749
  escape = False
750
750
 
751
- def decode_value(value: SequenceType[ItemType]) -> ItemType:
751
+ def decode_value(value: SequenceType[ItemType]) -> Emptiable[ItemType]:
752
752
  if value is None:
753
753
  return []
754
754
  elif isinstance(value, list):
@@ -765,6 +765,8 @@ def evaluate_parse_json_functions(self: XPathFunction, context: ContextType = No
765
765
 
766
766
  def json_object_pairs_to_map(obj: Iterable[tuple[str, SequenceType[ItemType]]]) -> XPathMap:
767
767
  items: dict[ItemType, SequenceType[ItemType]] = {}
768
+ key: Any
769
+ value: Any
768
770
 
769
771
  for item in obj:
770
772
  key, value = decode_value(item[0]), decode_value(item[1])
@@ -775,7 +777,7 @@ def evaluate_parse_json_functions(self: XPathFunction, context: ContextType = No
775
777
  raise self.error('FOJS0003')
776
778
 
777
779
  if isinstance(value, list):
778
- values = [decode_value(x) for x in value]
780
+ values: Any = [decode_value(x) for x in value]
779
781
  items[key] = XPathArray(self.parser, values) if values else values
780
782
  else:
781
783
  items[key] = value
@@ -1262,7 +1264,7 @@ def evaluate_json_to_xml_function(self: XPathFunction, context: ContextType = No
1262
1264
  else:
1263
1265
  raise self.missing_context()
1264
1266
 
1265
- def _fallback(*args: Any, context: ContextType = None) -> str:
1267
+ def _fallback(*a: Any, **kw: Any) -> str:
1266
1268
  return '&#xFFFD;'
1267
1269
 
1268
1270
  liberal = False
@@ -617,7 +617,7 @@ class XPathContext:
617
617
  for item in root.iter_descendants(with_self=False):
618
618
  if position < item.position and item not in descendants:
619
619
  self.item = item
620
- yield cast(ChildNodeType, self.item)
620
+ yield item
621
621
 
622
622
  self.item, self.axis = status
623
623
 
@@ -194,6 +194,10 @@ class XPathNode:
194
194
  """Access to wrapped object using the old API."""
195
195
  return self.obj
196
196
 
197
+ @property
198
+ def compat_string_value(self) -> str:
199
+ return self.string_value
200
+
197
201
  @property
198
202
  def root_node(self) -> 'XPathNode':
199
203
  return self if self.parent is None else self.parent.tree.root_node
@@ -1099,6 +1103,7 @@ class EtreeElementNode(ElementNode):
1099
1103
  position: int = 1,
1100
1104
  nsmap: Union[NsmapType, NamespacesType, None] = None) -> None:
1101
1105
 
1106
+ assert not callable(elem.tag)
1102
1107
  self.name = elem.tag
1103
1108
  self.obj = elem
1104
1109
  self.parent = parent
@@ -1177,10 +1182,14 @@ class EtreeElementNode(ElementNode):
1177
1182
  else:
1178
1183
  yield from get_atomic_sequence(self.xsd_type, '')
1179
1184
 
1185
+ @property
1186
+ def compat_string_value(self) -> str:
1187
+ return ''.join(etree_iter_strings(self.obj))
1188
+
1180
1189
  def apply_schema(self, schema: 'AbstractSchemaProxy') -> None:
1181
- if self.schema is schema and not schema.is_assertion_based():
1190
+ if self.tree.schema is schema and not schema.is_assertion_based():
1182
1191
  return
1183
- self.schema = schema
1192
+ self.tree.schema = schema
1184
1193
 
1185
1194
  if not schema.is_fully_valid():
1186
1195
  element_type = schema.get_type(XSD_ANY_TYPE)
@@ -1213,7 +1222,7 @@ class EtreeElementNode(ElementNode):
1213
1222
  xsd_types = [None]
1214
1223
  children = iter((root_node,))
1215
1224
 
1216
- iterators: list[Any] = []
1225
+ iterators: deque[Any] = deque()
1217
1226
  while True:
1218
1227
  for elem in children:
1219
1228
  if not isinstance(elem, EtreeElementNode):
@@ -1266,15 +1275,15 @@ class EtreeElementNode(ElementNode):
1266
1275
 
1267
1276
  def match_name(self, name: str, default_namespace: Optional[str] = None) -> bool:
1268
1277
  if '*' in name:
1269
- return match_wildcard(self.obj.tag, name)
1278
+ return match_wildcard(self.name, name)
1270
1279
  elif not name:
1271
- return not self.obj.tag
1280
+ return not self.name
1272
1281
  elif hasattr(self.obj, 'type') and hasattr(self.obj, 'is_matching'):
1273
1282
  return cast(XsdElementProtocol, self.obj).is_matching(name, default_namespace)
1274
1283
  elif name[0] == '{' or not default_namespace:
1275
- return self.obj.tag == name
1284
+ return self.name == name
1276
1285
  else:
1277
- return self.obj.tag == f'{{{default_namespace}}}{name}'
1286
+ return self.name == f'{{{default_namespace}}}{name}'
1278
1287
 
1279
1288
  def get_document_node(self, replace: bool = False, as_parent: bool = True) -> 'DocumentNode':
1280
1289
  """
@@ -1353,7 +1362,7 @@ class LazyElementNode(EtreeElementNode):
1353
1362
  for elem in self.obj:
1354
1363
  if not callable(elem.tag):
1355
1364
  LazyElementNode(elem, self)
1356
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
1365
+ elif elem.tag.__name__ == 'Comment':
1357
1366
  CommentNode(elem, self)
1358
1367
  else:
1359
1368
  ProcessingInstructionNode(elem, parent=self)
@@ -1439,9 +1448,9 @@ class SchemaElementNode(ElementNode):
1439
1448
 
1440
1449
  def match_name(self, name: str, default_namespace: Optional[str] = None) -> bool:
1441
1450
  if '*' in name:
1442
- return match_wildcard(self.obj.tag, name)
1451
+ return match_wildcard(self.name, name)
1443
1452
  elif not name:
1444
- return not self.obj.tag
1453
+ return not self.name
1445
1454
  elif hasattr(self.obj, 'type'):
1446
1455
  return self.obj.is_matching(name, default_namespace)
1447
1456
  else:
@@ -1733,6 +1742,12 @@ class EtreeDocumentNode(DocumentNode):
1733
1742
  return ''.join(etree_iter_strings(root))
1734
1743
  return ''.join(child.string_value for child in self.children)
1735
1744
 
1745
+ @property
1746
+ def compat_string_value(self) -> str:
1747
+ if not self.children:
1748
+ return self.string_value
1749
+ return ''.join(child.compat_string_value for child in self.children)
1750
+
1736
1751
  @property
1737
1752
  def is_extended(self) -> bool:
1738
1753
  """