elementpath 5.0.1__tar.gz → 5.0.2__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.2}/CHANGELOG.rst +6 -0
  2. {elementpath-5.0.1/elementpath.egg-info → elementpath-5.0.2}/PKG-INFO +1 -1
  3. {elementpath-5.0.1 → elementpath-5.0.2}/doc/conf.py +1 -1
  4. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/__init__.py +1 -1
  5. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/compare.py +15 -6
  6. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/datetime.py +1 -1
  7. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/etree.py +1 -1
  8. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/helpers.py +11 -10
  9. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/protocols.py +3 -2
  10. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/serialization.py +11 -6
  11. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/tree_builders.py +8 -13
  12. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath1/_xpath1_operators.py +6 -4
  13. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath1/xpath1_parser.py +4 -2
  14. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath2/_xpath2_functions.py +5 -0
  15. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath2/xpath2_parser.py +10 -3
  16. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/_xpath30_functions.py +10 -6
  17. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath31/_xpath31_functions.py +6 -4
  18. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath_context.py +1 -1
  19. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath_nodes.py +8 -7
  20. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath_selectors.py +22 -5
  21. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath_tokens.py +2 -2
  22. {elementpath-5.0.1 → elementpath-5.0.2/elementpath.egg-info}/PKG-INFO +1 -1
  23. {elementpath-5.0.1 → elementpath-5.0.2}/pyproject.toml +1 -1
  24. {elementpath-5.0.1 → elementpath-5.0.2}/tests/mypy_tests/protocols.py +1 -1
  25. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_helpers.py +12 -7
  26. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_selectors.py +11 -1
  27. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath1_parser.py +37 -13
  28. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath2_functions.py +6 -0
  29. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath2_parser.py +25 -0
  30. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath30.py +31 -0
  31. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath_nodes.py +3 -3
  32. {elementpath-5.0.1 → elementpath-5.0.2}/tox.ini +26 -13
  33. {elementpath-5.0.1 → elementpath-5.0.2}/LICENSE +0 -0
  34. {elementpath-5.0.1 → elementpath-5.0.2}/MANIFEST.in +0 -0
  35. {elementpath-5.0.1 → elementpath-5.0.2}/README.rst +0 -0
  36. {elementpath-5.0.1 → elementpath-5.0.2}/doc/Makefile +0 -0
  37. {elementpath-5.0.1 → elementpath-5.0.2}/doc/advanced.rst +0 -0
  38. {elementpath-5.0.1 → elementpath-5.0.2}/doc/index.rst +0 -0
  39. {elementpath-5.0.1 → elementpath-5.0.2}/doc/introduction.rst +0 -0
  40. {elementpath-5.0.1 → elementpath-5.0.2}/doc/make.bat +0 -0
  41. {elementpath-5.0.1 → elementpath-5.0.2}/doc/pratt_api.rst +0 -0
  42. {elementpath-5.0.1 → elementpath-5.0.2}/doc/requirements.txt +0 -0
  43. {elementpath-5.0.1 → elementpath-5.0.2}/doc/xpath_api.rst +0 -0
  44. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/aliases.py +0 -0
  45. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/collations.py +0 -0
  46. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/__init__.py +0 -0
  47. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/atomic_types.py +0 -0
  48. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/binary.py +0 -0
  49. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/numeric.py +0 -0
  50. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/proxies.py +0 -0
  51. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/qname.py +0 -0
  52. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/string.py +0 -0
  53. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/untyped.py +0 -0
  54. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/datatypes/uri.py +0 -0
  55. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/decoder.py +0 -0
  56. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/exceptions.py +0 -0
  57. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/extras/__init__.py +0 -0
  58. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/extras/pathnodes.py +0 -0
  59. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/namespaces.py +0 -0
  60. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/py.typed +0 -0
  61. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/__init__.py +0 -0
  62. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/categories_fallback.py +0 -0
  63. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/character_classes.py +0 -0
  64. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/codepoints.py +0 -0
  65. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/patterns.py +0 -0
  66. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/unicode_blocks.py +0 -0
  67. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/unicode_categories.py +0 -0
  68. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/regex/unicode_subsets.py +0 -0
  69. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/schema_proxy.py +0 -0
  70. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/sequence_types.py +0 -0
  71. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/tdop.py +0 -0
  72. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/validators/__init__.py +0 -0
  73. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/validators/analyze-string.xsd +0 -0
  74. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/validators/schema-for-json.xsd +0 -0
  75. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath1/__init__.py +0 -0
  76. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath1/_xpath1_axes.py +0 -0
  77. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath1/_xpath1_functions.py +0 -0
  78. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath2/__init__.py +0 -0
  79. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath2/_xpath2_constructors.py +0 -0
  80. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath2/_xpath2_operators.py +0 -0
  81. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath3.py +0 -0
  82. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/__init__.py +0 -0
  83. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/_translation_maps.py +0 -0
  84. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/_xpath30_operators.py +0 -0
  85. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/xpath30_helpers.py +0 -0
  86. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath30/xpath30_parser.py +0 -0
  87. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath31/__init__.py +0 -0
  88. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath31/_xpath31_operators.py +0 -0
  89. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath/xpath31/xpath31_parser.py +0 -0
  90. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath.egg-info/SOURCES.txt +0 -0
  91. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath.egg-info/dependency_links.txt +0 -0
  92. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath.egg-info/requires.txt +0 -0
  93. {elementpath-5.0.1 → elementpath-5.0.2}/elementpath.egg-info/top_level.txt +0 -0
  94. {elementpath-5.0.1 → elementpath-5.0.2}/requirements-dev.txt +0 -0
  95. {elementpath-5.0.1 → elementpath-5.0.2}/scripts/generate_codepoints.py +0 -0
  96. {elementpath-5.0.1 → elementpath-5.0.2}/setup.cfg +0 -0
  97. {elementpath-5.0.1 → elementpath-5.0.2}/tests/__init__.py +0 -0
  98. {elementpath-5.0.1 → elementpath-5.0.2}/tests/memory_profiling.py +0 -0
  99. {elementpath-5.0.1 → elementpath-5.0.2}/tests/mypy_tests/advanced.py +0 -0
  100. {elementpath-5.0.1 → elementpath-5.0.2}/tests/mypy_tests/selectors.py +0 -0
  101. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/analyze-string.xsd +0 -0
  102. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/external_entity.xml +0 -0
  103. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/sample.xml +0 -0
  104. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/schema-for-json.xsd +0 -0
  105. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/unparsed_entity.xml +0 -0
  106. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/unused_external_entity.xml +0 -0
  107. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/unused_unparsed_entity.xml +0 -0
  108. {elementpath-5.0.1 → elementpath-5.0.2}/tests/resources/with_entity.xml +0 -0
  109. {elementpath-5.0.1 → elementpath-5.0.2}/tests/run_all_tests.py +0 -0
  110. {elementpath-5.0.1 → elementpath-5.0.2}/tests/run_typing_tests.py +0 -0
  111. {elementpath-5.0.1 → elementpath-5.0.2}/tests/run_w3c_tests.py +0 -0
  112. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_collations.py +0 -0
  113. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_compare.py +0 -0
  114. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_datatypes.py +0 -0
  115. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_decoder.py +0 -0
  116. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_etree.py +0 -0
  117. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_exceptions.py +0 -0
  118. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_namespaces.py +0 -0
  119. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_package.py +0 -0
  120. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_pathnodes.py +0 -0
  121. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_regex.py +0 -0
  122. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_schema_context.py +0 -0
  123. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_schema_proxy.py +0 -0
  124. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_sequence_types.py +0 -0
  125. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_serialization.py +0 -0
  126. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_tdop_parser.py +0 -0
  127. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_tree_builders.py +0 -0
  128. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_validators.py +0 -0
  129. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath2_constructors.py +0 -0
  130. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath31.py +0 -0
  131. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath_context.py +0 -0
  132. {elementpath-5.0.1 → elementpath-5.0.2}/tests/test_xpath_tokens.py +0 -0
  133. {elementpath-5.0.1 → elementpath-5.0.2}/tests/xpath_test_class.py +0 -0
@@ -2,6 +2,11 @@
2
2
  CHANGELOG
3
3
  *********
4
4
 
5
+ `v5.0.2`_ (2025-06-18)
6
+ ======================
7
+ * Fix for XPath 2.0 *fn:node-name* (issue #91)
8
+ * Workaround for processing arguments with multiple occurrences for external functions (issue #92)
9
+
5
10
  `v5.0.1`_ (2025-05-11)
6
11
  ======================
7
12
  * Fix XDM type labeling with element and xsi:type substitutions (issue #89)
@@ -507,3 +512,4 @@ CHANGELOG
507
512
  .. _v4.8.0: https://github.com/sissaschool/elementpath/compare/v4.7.0...v4.8.0
508
513
  .. _v5.0.0: https://github.com/sissaschool/elementpath/compare/v4.8.0...v5.0.0
509
514
  .. _v5.0.1: https://github.com/sissaschool/elementpath/compare/v5.0.0...v5.0.1
515
+ .. _v5.0.2: https://github.com/sissaschool/elementpath/compare/v5.0.1...v5.0.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elementpath
3
- Version: 5.0.1
3
+ Version: 5.0.2
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.2'
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.2'
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
 
@@ -46,18 +46,13 @@ def get_node_tree(root: RootArgType,
46
46
  `False` is provided creates a dummy document when the root is an Element instance. \
47
47
  For default the root node kind is preserved.
48
48
  """
49
- root_node: RootNodeType
50
-
51
49
  if isinstance(root, (DocumentNode, ElementNode)):
52
50
  if uri is not None and root.uri is None:
53
51
  root.tree.uri = uri
54
52
 
55
53
  if fragment:
56
54
  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
55
+ return root.getroot()
61
56
  elif fragment is False and \
62
57
  isinstance(root, ElementNode) and \
63
58
  is_etree_element_instance(root.obj):
@@ -80,9 +75,7 @@ def get_node_tree(root: RootArgType,
80
75
  cast(SchemaElemType, root), uri
81
76
  )
82
77
  else:
83
- return build_node_tree(
84
- cast(ElementTreeRootType, root), namespaces, uri, fragment
85
- )
78
+ return build_node_tree(root, namespaces, uri, fragment)
86
79
 
87
80
 
88
81
  def build_node_tree(root: ElementTreeRootType,
@@ -160,7 +153,7 @@ def build_node_tree(root: ElementTreeRootType,
160
153
  TextNode(elem.text, child, position)
161
154
  position += 1
162
155
 
163
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
156
+ elif elem.tag.__name__ == 'Comment':
164
157
  child = CommentNode(elem, parent, position)
165
158
  position += 1
166
159
  else:
@@ -239,7 +232,8 @@ def build_lxml_node_tree(root: LxmlRootType,
239
232
 
240
233
  # Add root siblings (comments and processing instructions)
241
234
  for elem in reversed([x for x in root_elem.itersiblings(preceding=True)]):
242
- if elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
235
+ assert callable(elem.tag)
236
+ if elem.tag.__name__ == 'Comment':
243
237
  CommentNode(elem, document_node, position)
244
238
  position += 1
245
239
  else:
@@ -293,7 +287,7 @@ def build_lxml_node_tree(root: LxmlRootType,
293
287
  TextNode(elem.text, child, position)
294
288
  position += 1
295
289
 
296
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
290
+ elif elem.tag.__name__ == 'Comment':
297
291
  child = CommentNode(elem, parent, position)
298
292
  position += 1
299
293
  else:
@@ -319,7 +313,8 @@ def build_lxml_node_tree(root: LxmlRootType,
319
313
 
320
314
  # Add root following siblings (comments and processing instructions)
321
315
  for elem in root_elem.itersiblings():
322
- if elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
316
+ assert callable(elem.tag)
317
+ if elem.tag.__name__ == 'Comment':
323
318
  CommentNode(elem, document_node, position)
324
319
  position += 1
325
320
  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
 
@@ -1099,6 +1099,7 @@ class EtreeElementNode(ElementNode):
1099
1099
  position: int = 1,
1100
1100
  nsmap: Union[NsmapType, NamespacesType, None] = None) -> None:
1101
1101
 
1102
+ assert not callable(elem.tag)
1102
1103
  self.name = elem.tag
1103
1104
  self.obj = elem
1104
1105
  self.parent = parent
@@ -1266,15 +1267,15 @@ class EtreeElementNode(ElementNode):
1266
1267
 
1267
1268
  def match_name(self, name: str, default_namespace: Optional[str] = None) -> bool:
1268
1269
  if '*' in name:
1269
- return match_wildcard(self.obj.tag, name)
1270
+ return match_wildcard(self.name, name)
1270
1271
  elif not name:
1271
- return not self.obj.tag
1272
+ return not self.name
1272
1273
  elif hasattr(self.obj, 'type') and hasattr(self.obj, 'is_matching'):
1273
1274
  return cast(XsdElementProtocol, self.obj).is_matching(name, default_namespace)
1274
1275
  elif name[0] == '{' or not default_namespace:
1275
- return self.obj.tag == name
1276
+ return self.name == name
1276
1277
  else:
1277
- return self.obj.tag == f'{{{default_namespace}}}{name}'
1278
+ return self.name == f'{{{default_namespace}}}{name}'
1278
1279
 
1279
1280
  def get_document_node(self, replace: bool = False, as_parent: bool = True) -> 'DocumentNode':
1280
1281
  """
@@ -1353,7 +1354,7 @@ class LazyElementNode(EtreeElementNode):
1353
1354
  for elem in self.obj:
1354
1355
  if not callable(elem.tag):
1355
1356
  LazyElementNode(elem, self)
1356
- elif elem.tag.__name__ == 'Comment': # type: ignore[attr-defined]
1357
+ elif elem.tag.__name__ == 'Comment':
1357
1358
  CommentNode(elem, self)
1358
1359
  else:
1359
1360
  ProcessingInstructionNode(elem, parent=self)
@@ -1439,9 +1440,9 @@ class SchemaElementNode(ElementNode):
1439
1440
 
1440
1441
  def match_name(self, name: str, default_namespace: Optional[str] = None) -> bool:
1441
1442
  if '*' in name:
1442
- return match_wildcard(self.obj.tag, name)
1443
+ return match_wildcard(self.name, name)
1443
1444
  elif not name:
1444
- return not self.obj.tag
1445
+ return not self.name
1445
1446
  elif hasattr(self.obj, 'type'):
1446
1447
  return self.obj.is_matching(name, default_namespace)
1447
1448
  else:
@@ -8,6 +8,7 @@
8
8
  # @author Davide Brunato <brunato@sissa.it>
9
9
  #
10
10
  import datetime
11
+ import warnings
11
12
  from collections.abc import Iterator
12
13
  from typing import Any, Optional, Union
13
14
 
@@ -66,8 +67,12 @@ def select(root: Optional[RootArgType],
66
67
  :return: a list with XPath nodes or a basic type for expressions based \
67
68
  on a function or literal.
68
69
  """
69
- _parser = (parser or XPath2Parser)(namespaces, **kwargs)
70
- root_token = _parser.parse(path)
70
+ if parser is None:
71
+ parser = XPath2Parser
72
+ if schema is not None and parser.version > '1.0':
73
+ kwargs['schema'] = schema
74
+
75
+ root_token = parser(namespaces, **kwargs).parse(path)
71
76
  context = XPathContext(root, namespaces, uri, fragment, item, position, size,
72
77
  axis, schema, variables, current_dt, timezone)
73
78
  return root_token.get_results(context)
@@ -119,8 +124,12 @@ def iter_select(root: Optional[RootArgType],
119
124
  :param kwargs: other optional parameters for the parser instance.
120
125
  :return: a generator of the XPath expression results.
121
126
  """
122
- _parser = (parser or XPath2Parser)(namespaces, **kwargs)
123
- root_token = _parser.parse(path)
127
+ if parser is None:
128
+ parser = XPath2Parser
129
+ if schema is not None and parser.version > '1.0':
130
+ kwargs['schema'] = schema
131
+
132
+ root_token = parser(namespaces, **kwargs).parse(path)
124
133
  context = XPathContext(root, namespaces, uri, fragment, item, position, size,
125
134
  axis, schema, variables, current_dt, timezone)
126
135
  return root_token.select_results(context)
@@ -148,7 +157,13 @@ class Selector(object):
148
157
  parser: Optional['ParserClassType'] = None,
149
158
  **kwargs: Any) -> None:
150
159
 
151
- self._variables = kwargs.pop('variables', None) # For backward compatibility
160
+ if 'variables' in kwargs:
161
+ msg = ("Argument 'variables' here is deprecated and will be"
162
+ "be removed in the next major release. Provide this "
163
+ "argument later to Selector.select/iter_select.")
164
+ warnings.warn(DeprecationWarning(msg))
165
+
166
+ self._variables = kwargs.pop('variables', None)
152
167
  self.parser = (parser or XPath2Parser)(namespaces, **kwargs)
153
168
  self.path = path
154
169
  self.root_token = self.parser.parse(path)
@@ -191,6 +206,8 @@ class Selector(object):
191
206
  :param kwargs: other optional parameters for the XPath dynamic context.
192
207
  :return: a generator of the XPath expression results.
193
208
  """
209
+ if 'schema' not in kwargs:
210
+ kwargs['schema'] = self.parser.schema
194
211
  if 'variables' not in kwargs and self._variables:
195
212
  kwargs['variables'] = self._variables
196
213
 
@@ -567,7 +567,7 @@ class XPathToken(Token[XPathTokenType]):
567
567
 
568
568
  for result in self.select(context):
569
569
  if not isinstance(result, XPathNode):
570
- yield cast(Union[AtomicType, XPathFunction], result)
570
+ yield result
571
571
  elif isinstance(result, NamespaceNode):
572
572
  if self.parser.compatibility_mode:
573
573
  yield result.prefix, result.uri
@@ -797,7 +797,7 @@ class XPathToken(Token[XPathTokenType]):
797
797
  return _item
798
798
  raise self.error('FODT0001', str(err)) from None
799
799
 
800
- if not isinstance(_item, DayTimeDuration):
800
+ if isinstance(_item, AbstractDateTime):
801
801
  _item.tzinfo = timezone
802
802
  return _item
803
803
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elementpath
3
- Version: 5.0.1
3
+ Version: 5.0.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "elementpath"
7
- version = "5.0.1"
7
+ version = "5.0.2"
8
8
  description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and lxml"
9
9
  readme = "README.rst"
10
10
  license = "MIT"