Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-elementpath for 
openSUSE:Factory checked in at 2026-08-22 21:34:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-elementpath (Old)
 and      /work/SRC/openSUSE:Factory/.python-elementpath.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-elementpath"

Sat Aug 22 21:34:38 2026 rev:26 rq:1372818 version:5.1.4

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-elementpath/python-elementpath.changes    
2026-07-02 20:06:09.966558129 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-elementpath.new.1258/python-elementpath.changes
  2026-08-22 21:35:46.339403620 +0200
@@ -1,0 +2,10 @@
+Fri Aug 21 10:34:45 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 5.1.4:
+  * Add allow_environment and allow_external_resources arguments
+    to XPath30Parser
+  * some/every statements: revert to v4.8 implementation (fix
+    issue #106)
+  * reverse axis position: workaround until next minor-version
+
+-------------------------------------------------------------------

Old:
----
  v5.1.3.tar.gz

New:
----
  v5.1.4.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-elementpath.spec ++++++
--- /var/tmp/diff_new_pack.pyyw7g/_old  2026-08-22 21:35:47.502445127 +0200
+++ /var/tmp/diff_new_pack.pyyw7g/_new  2026-08-22 21:35:47.506445269 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-elementpath
-Version:        5.1.3
+Version:        5.1.4
 Release:        0
 Summary:        XPath 1.0/20 parsers and selectors for ElementTree and lxml
 License:        MIT

++++++ v5.1.3.tar.gz -> v5.1.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/.github/workflows/test-elementpath.yml 
new/elementpath-5.1.4/.github/workflows/test-elementpath.yml
--- old/elementpath-5.1.3/.github/workflows/test-elementpath.yml        
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/.github/workflows/test-elementpath.yml        
2026-08-08 22:03:05.000000000 +0200
@@ -35,10 +35,10 @@
         run: |
           pip install flake8
           flake8 elementpath --max-line-length=100 --statistics
-      - name: Lint with mypy==2.1.0
+      - name: Lint with mypy==2.3.0
         if: ${{ matrix.python-version != 'pypy-3.10' && matrix.python-version 
!= 'pypy-3.11' }}
         run: |
-          pip install mypy==2.1.0 xmlschema lxml-stubs
+          pip install mypy==2.3.0 xmlschema lxml-stubs
           mypy --show-error-codes --strict elementpath
       - name: Install lxml optional dependency
         if: |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/CHANGELOG.rst 
new/elementpath-5.1.4/CHANGELOG.rst
--- old/elementpath-5.1.3/CHANGELOG.rst 2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/CHANGELOG.rst 2026-08-08 22:03:05.000000000 +0200
@@ -2,6 +2,12 @@
 CHANGELOG
 *********
 
+`v5.1.4`_ (2026-08-08)
+======================
+* Add *allow_environment* and *allow_external_resources* arguments to 
XPath30Parser
+* some/every statements: revert to v4.8 implementation (fix issue #106)
+* reverse axis position: workaround until next minor-version (issue #105)
+
 `v5.1.3`_ (2026-06-28)
 ======================
 * Fix protection against type cast errors during the static analysis phase
@@ -548,3 +554,4 @@
 .. _v5.1.1: https://github.com/sissaschool/elementpath/compare/v5.1.0...v5.1.1
 .. _v5.1.2: https://github.com/sissaschool/elementpath/compare/v5.1.1...v5.1.2
 .. _v5.1.3: https://github.com/sissaschool/elementpath/compare/v5.1.2...v5.1.3
+.. _v5.1.4: https://github.com/sissaschool/elementpath/compare/v5.1.3...v5.1.4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/doc/conf.py 
new/elementpath-5.1.4/doc/conf.py
--- old/elementpath-5.1.3/doc/conf.py   2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/doc/conf.py   2026-08-08 22:03:05.000000000 +0200
@@ -31,7 +31,7 @@
 # The short X.Y version
 version = '5.1'
 # The full version, including alpha/beta/rc tags
-release = '5.1.2'
+release = '5.1.4'
 
 # -- General configuration ---------------------------------------------------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/__init__.py 
new/elementpath-5.1.4/elementpath/__init__.py
--- old/elementpath-5.1.3/elementpath/__init__.py       2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/__init__.py       2026-08-08 
22:03:05.000000000 +0200
@@ -7,7 +7,7 @@
 #
 # @author Davide Brunato <[email protected]>
 #
-__version__ = '5.1.3'
+__version__ = '5.1.4'
 __author__ = "Davide Brunato"
 __contact__ = "[email protected]"
 __copyright__ = "Copyright 2018-2026, SISSA"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/datatypes/any_types.py 
new/elementpath-5.1.4/elementpath/datatypes/any_types.py
--- old/elementpath-5.1.3/elementpath/datatypes/any_types.py    2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/datatypes/any_types.py    2026-08-08 
22:03:05.000000000 +0200
@@ -92,8 +92,8 @@
         """
         Versioned factory method to create XSD type instances.
 
-        :param value: the value to be converted to the XSD type.
-        :param kwargs: additional keyword arguments to be passed to factory.
+        :param value: The value to be converted to the XSD type.
+        :param kwargs: Additional keyword arguments to be passed to the 
factory.
         """
         return cls(value)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/datatypes/binary.py 
new/elementpath-5.1.4/elementpath/datatypes/binary.py
--- old/elementpath-5.1.3/elementpath/datatypes/binary.py       2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/datatypes/binary.py       2026-08-08 
22:03:05.000000000 +0200
@@ -83,6 +83,12 @@
     def decode(self) -> bytes:
         raise NotImplementedError()
 
+    def __str__(self) -> str:
+        return self.value.decode('utf-8')
+
+    def __hash__(self) -> int:
+        return hash(self.value)
+
     def __eq__(self, other: object) -> bool:
         if isinstance(other, AbstractBinary):
             return self.decode() == other.decode()
@@ -153,12 +159,6 @@
             if match is None or match.group(0) != value:
                 raise cls._invalid_value(value)
 
-    def __str__(self) -> str:
-        return self.value.decode('utf-8')
-
-    def __hash__(self) -> int:
-        return hash(self.value)
-
     def __len__(self) -> int:
         length = len(self.value)
         if length == 0:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/datatypes/datetime.py 
new/elementpath-5.1.4/elementpath/datatypes/datetime.py
--- old/elementpath-5.1.3/elementpath/datatypes/datetime.py     2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/datatypes/datetime.py     2026-08-08 
22:03:05.000000000 +0200
@@ -44,7 +44,7 @@
     A tzinfo implementation for XSD timezone offsets. Offsets must be specified
     between -14:00 and +14:00.
 
-    :param offset: a timedelta instance or an XSD timezone formatted string.
+    :param offset: A timedelta instance or an XSD timezone formatted string.
     """
     def __init__(self, offset: datetime.timedelta) -> None:
         super().__init__()
@@ -148,7 +148,7 @@
 
 class AbstractDateTime(AnyAtomicType):
     """
-    A class for representing XSD date/time objects. It uses and internal 
datetime.datetime
+    A class for representing XSD date/time objects. It uses an internal 
datetime.datetime
     attribute and an integer attribute for processing BCE years or for years 
after 9999 CE.
     """
     pattern = LazyPattern(r'^$')
@@ -386,17 +386,17 @@
         return self._dt.astimezone(tz)
 
     def isocalendar(self) -> tuple[int, int, int]:
-        return cast(tuple[int, int, int], self._dt.isocalendar())
+        return cast(tuple[int, int, int], cast(object, self._dt.isocalendar()))
 
     @classmethod
     def fromstring(cls: type[DT], datetime_string: str,
                    tzinfo: datetime.tzinfo | None = None) -> DT:
         """
-        Creates an XSD date/time instance from a string formatted value.
+        Creates an XSD date/time instance from a string-formatted value.
 
-        :param datetime_string: a string containing an XSD formatted date/time 
specification.
-        :param tzinfo: optional implicit timezone information (defaults to 
UTC).
-        :return: an AbstractDateTime concrete subclass instance.
+        :param datetime_string: A string containing an XSD formatted date/time 
specification.
+        :param tzinfo: Optional implicit timezone information (defaults to 
UTC).
+        :return: An AbstractDateTime concrete subclass instance.
         """
         if not isinstance(datetime_string, str):
             msg = '1st argument has an invalid type {!r}'
@@ -445,10 +445,10 @@
         """
         Creates an XSD date/time instance from a datetime.datetime/date/time 
instance.
 
-        :param dt: the datetime, date or time instance that stores the XSD 
Date/Time value.
-        :param year: if a year is provided the created instance refers to it 
and the \
+        :param dt: The datetime, date, or time instance that stores the XSD 
Date/Time value.
+        :param year: If a year is provided, the created instance refers to it 
and the \
         possibly present *dt.year* part is ignored.
-        :return: an AbstractDateTime concrete subclass instance.
+        :return: An AbstractDateTime concrete subclass instance.
         """
         if not isinstance(dt, (datetime.datetime, datetime.date, 
datetime.time)):
             raise TypeError('1st argument has an invalid type %r' % type(dt))
@@ -465,10 +465,10 @@
             -> 'AbstractDateTime':
         """
         Creates an XSD dateTime/date instance from a datetime.timedelta 
related to
-        0001-01-01T00:00:00 CE. In case of a date the time part is not counted.
+        0001-01-01T00:00:00 CE. In the case of a date, the time part is not 
counted.
 
-        :param delta: a datetime.timedelta instance.
-        :param adjust_timezone: if `True` adjusts the timezone of Date objects 
\
+        :param delta: A datetime.timedelta instance.
+        :param adjust_timezone: If `True` adjusts the timezone of Date objects 
\
         with eventually present hours and minutes.
         """
         try:
@@ -607,14 +607,14 @@
                  microsecond: int = 0,
                  tzinfo: datetime.tzinfo | None = None) -> None:
         """
-        :param year: the year, between -9999 and 9999
-        :param month: the month, between 1 and 12
-        :param day: the day, between 1 and 31
-        :param hour: the hour, between 0 and 23
-        :param minute: the minute, between 0 and 59
-        :param second: the second, between 0 and 59
-        :param microsecond: the microsecond, between 0 and 999999
-        :param tzinfo: optional implicit timezone information (defaults to UTC)
+        :param year: The year, between -9999 and 9999
+        :param month: The month, between 1 and 12
+        :param day: The day, between 1 and 31
+        :param hour: The hour, between 0 and 23
+        :param minute: The minute, between 0 and 59
+        :param second: The second, between 0 and 59
+        :param microsecond: The microsecond, between 0 and 999999
+        :param tzinfo: Optional implicit timezone information (defaults to UTC)
         """
         super().__init__(year, month, day, hour, minute, second, microsecond, 
tzinfo)
 
@@ -692,10 +692,10 @@
     def __init__(self, year: int, month: int, day: int,
                  tzinfo: datetime.tzinfo | None = None) -> None:
         """
-        :param year: the year, between -9999 and 9999
-        :param month: the month, between 1 and 12
-        :param day: the day, between 1 and 31
-        :param tzinfo: optional implicit timezone information (defaults to UTC)
+        :param year: The year, between -9999 and 9999
+        :param month: The month, between 1 and 12
+        :param day: The day, between 1 and 31
+        :param tzinfo: Optional implicit timezone information (defaults to UTC)
         """
         super().__init__(year, month, day, tzinfo=tzinfo)
 
@@ -847,8 +847,8 @@
 
     def __init__(self, year: int, tzinfo: datetime.tzinfo | None = None) -> 
None:
         """
-        :param year: the year, between -9999 and 9999
-        :param tzinfo: optional implicit timezone information (defaults to UTC)
+        :param year: The year, between -9999 and 9999
+        :param tzinfo: Optional implicit timezone information (defaults to UTC)
         """
         super().__init__(year, tzinfo=tzinfo)
 
@@ -907,9 +907,9 @@
 
     def __init__(self, year: int, month: int, tzinfo: datetime.tzinfo | None = 
None) -> None:
         """
-        :param year: the year, between -9999 and 9999
-        :param month: the month, between 1 and 12
-        :param tzinfo: optional implicit timezone information (defaults to UTC)
+        :param year: The year, between -9999 and 9999
+        :param month: The month, between 1 and 12
+        :param tzinfo: Optional implicit timezone information (defaults to UTC)
         """
         super().__init__(year, month, tzinfo=tzinfo)
 
@@ -951,11 +951,11 @@
                  second: int = 0, microsecond: int = 0,
                  tzinfo: datetime.tzinfo | None = None) -> None:
         """
-        :param hour: the hour, between 0 and 23
-        :param minute: the minute, between 0 and 59
-        :param second: the second, between 0 and 59
-        :param microsecond: the microsecond, between 0 and 999999
-        :param tzinfo: optional implicit timezone information (defaults to UTC)
+        :param hour: The hour, between 0 and 23
+        :param minute: The minute, between 0 and 59
+        :param second: The second, between 0 and 59
+        :param microsecond: The microsecond, between 0 and 999999
+        :param tzinfo: Optional implicit timezone information (defaults to UTC)
         """
         if hour == 24 and minute == second == microsecond == 0:
             hour = 0
@@ -1023,7 +1023,7 @@
         """
         :param months: an integer value that represents years and months.
         :param seconds: a decimal or an integer instance that represents \
-        days, hours, minutes, seconds and fractions of seconds.
+        days, hours, minutes, seconds, and fractions of seconds.
         """
         if seconds < 0 < months or months < 0 < seconds:
             raise ValueError('signs differ: (months=%d, seconds=%d)' % 
(months, seconds))
@@ -1074,10 +1074,10 @@
     @classmethod
     def fromstring(cls: type[_D], text: str) -> _D:
         """
-        Creates a Duration instance from a formatted XSD duration string.
+        Creates the Duration instance from a formatted XSD duration string.
 
-        :param text: an ISO 8601 representation without week fragment and an 
optional decimal part \
-        only for seconds fragment.
+        :param text: An ISO 8601 representation without week fragment and an 
optional decimal part \
+        only for the second fragment.
         """
         if not isinstance(text, str):
             msg = 'argument has an invalid type {!r}'
@@ -1260,7 +1260,7 @@
     def __init__(self, seconds: Union[Decimal, int] = 0) -> None:
         """
         :param seconds: a decimal or an integer instance that represents \
-        days, hours, minutes, seconds and fractions of seconds.
+        days, hours, minutes, seconds, and fractions of seconds.
         """
         super().__init__(0, seconds)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/datatypes/untyped.py 
new/elementpath-5.1.4/elementpath/datatypes/untyped.py
--- old/elementpath-5.1.3/elementpath/datatypes/untyped.py      2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/datatypes/untyped.py      2026-08-08 
22:03:05.000000000 +0200
@@ -26,8 +26,8 @@
     Class for xs:untypedAtomic data. Provides special methods for comparing
     and converting to basic data types.
 
-    :param value: the untyped value, usually a string.
-    :param parser: the XPath parser that creates the instance, if any.
+    :param value: The untyped value, usually a string.
+    :param parser: The XPath parser that creates the instance if any.
     """
     name = 'untypedAtomic'
     value: str
@@ -77,7 +77,7 @@
         Returns a couple of operands, applying a cast to the instance value 
based on
         the type of the *other* argument.
 
-        :param other: The other operand, that determines the cast for the 
untyped instance.
+        :param other: The other operand that determines the cast for the 
untyped instance.
         :param force_float: Force a conversion to float if *other* is an 
UntypedAtomic instance.
         :return: A couple of values.
         """
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/datatypes/uri.py 
new/elementpath-5.1.4/elementpath/datatypes/uri.py
--- old/elementpath-5.1.3/elementpath/datatypes/uri.py  2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/datatypes/uri.py  2026-08-08 
22:03:05.000000000 +0200
@@ -25,7 +25,7 @@
 
     def __init__(self, value: Union[str, bytes, UntypedAtomic, 'AnyURI']) -> 
None:
         """
-        :param value: string or an untyped atomic that represents a valid URI.
+        :param value: string or an untyped atomics that represents a valid URI.
         """
         match value:
             case str():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/helpers.py 
new/elementpath-5.1.4/elementpath/helpers.py
--- old/elementpath-5.1.3/elementpath/helpers.py        2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/helpers.py        2026-08-08 
22:03:05.000000000 +0200
@@ -14,7 +14,8 @@
 from collections.abc import Mapping
 from decimal import Decimal
 from typing import Any, Generic, Optional, SupportsFloat, SupportsIndex, 
TypeVar, Union
-from urllib.parse import urlsplit
+from os.path import normpath
+from urllib.parse import urlsplit, urlunsplit
 
 ###
 # Common sets constants
@@ -25,6 +26,9 @@
     ('inf', '+inf', '-inf', 'nan', 'infinity', '+infinity', '-infinity')
 )
 
+UNSAFE_PATHS = ('/etc/', '/proc/', '/run/', '/boot/', '/sbin/', '/root/', 
'/usr/')
+BASE_URI_PREFIXES = ('file:///', 'http://', 'https://', 'ftp://', 'ftps://', 
'urn:')
+
 MathArgType = Union[SupportsFloat, SupportsIndex]
 FloatArgType = Union[SupportsFloat, SupportsIndex, str]
 
@@ -64,7 +68,7 @@
 class LazyPattern:
     """
     A descriptor for creating lazy regexp patterns. The compiled pattern is 
built
-    only when the descriptor attribute is accessed (e.g. a hasattr() call).
+    only when the descriptor attribute is accessed (e.g., a hasattr() call).
     """
     _compiled: re.Pattern[str]
 
@@ -225,10 +229,10 @@
     Converts a delta of months to a delta of days, counting from the 1st day 
of the month,
     relative to the year and the month passed as arguments.
 
-    :param year: the reference start year, a negative or zero value means a 
BCE year \
+    :param year: The reference start year, a negative or zero value means a 
BCE year \
     (0 is 1 BCE, -1 is 2 BCE, -2 is 3 BCE, etc.).
-    :param month: the starting month (1-12).
-    :param months_delta: the number of months, if negative count backwards.
+    :param month: The starting month (1-12).
+    :param months_delta: The number of months, if negative, counts backwards.
     """
     if not months_delta:
         return 0
@@ -405,3 +409,44 @@
         return parts.scheme == 'urn' or \
             parts.scheme != '' and parts.netloc != '' or \
             parts.path.startswith('/')
+
+
+def is_allowed_uri(uri: str, allow_external_resources: bool | list[str] = 
False) -> bool:
+    if not uri.startswith(BASE_URI_PREFIXES):
+        return False
+    elif allow_external_resources is True:
+        return True
+    elif not isinstance(allow_external_resources, list):
+        return False
+
+    try:
+        parts = urlsplit(uri.strip())
+    except ValueError:
+        return False
+
+    for base_uri in allow_external_resources:
+        if not base_uri.startswith(parts.scheme) or not 
base_uri.startswith(BASE_URI_PREFIXES):
+            continue
+        elif parts.scheme == 'urn':
+            if uri.startswith(base_uri):
+                return True
+        else:
+            path = normpath(parts.path).replace('\\', '/')
+            if parts.scheme == 'file' and (path.count('/') < 2 or 
path.startswith(UNSAFE_PATHS)):
+                return False
+            url = urlunsplit((parts.scheme, parts.netloc, path, 
parts.fragment, parts.query))
+            if url.startswith(base_uri):
+                return True
+    else:
+        return False
+
+
+def is_allowed_variable(name: str, allow_environment: bool | list[str] = 
False) -> bool:
+    if allow_environment is True:
+        return True
+    elif isinstance(allow_environment, list) and \
+            all(isinstance(x, str) for x in allow_environment):
+        return name in allow_environment or \
+            any(name.startswith(x[:-1]) for x in allow_environment if 
x.endswith('*'))
+    else:
+        return False
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath1/_xpath1_functions.py 
new/elementpath-5.1.4/elementpath/xpath1/_xpath1_functions.py
--- old/elementpath-5.1.3/elementpath/xpath1/_xpath1_functions.py       
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath1/_xpath1_functions.py       
2026-08-08 22:03:05.000000000 +0200
@@ -112,7 +112,8 @@
         context = self.context
     elif context is None:
         raise self.missing_context()
-    elif context.size is None:
+
+    if context.size is None:
         raise self.missing_context("Context size is undefined")
     return context.size
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath1/_xpath1_operators.py 
new/elementpath-5.1.4/elementpath/xpath1/_xpath1_operators.py
--- old/elementpath-5.1.3/elementpath/xpath1/_xpath1_operators.py       
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath1/_xpath1_operators.py       
2026-08-08 22:03:05.000000000 +0200
@@ -15,7 +15,6 @@
 import operator
 from collections.abc import Iterator
 from copy import copy
-from typing import cast
 
 import elementpath.aliases as ta
 
@@ -26,7 +25,7 @@
 from elementpath.helpers import node_position
 from elementpath.xpath_context import XPathSchemaContext
 from elementpath.xpath_tokens import XPathToken, NameToken, VariableToken, \
-    ContextItemToken, AsteriskToken, ParentShortcutToken
+    ContextItemToken, AsteriskToken, ParentShortcutToken, XPathAxis
 
 from .xpath1_parser import XPath1Parser
 
@@ -255,13 +254,17 @@
     if context is None:
         raise self.missing_context()
 
-    results = {item for k in range(2) for item in 
self[k].select(copy(context))}
-    if any(not isinstance(x, XPathNode) for x in results):
-        raise self.error('XPTY0004', 'only XPath nodes are allowed')
-    elif self.concatenated:
-        yield from cast(set[XPathNode], results)
+    results: set[XPathNode] = set()
+    for k in range(2):
+        for item in self[k].select(copy(context)):
+            if not isinstance(item, XPathNode):
+                raise self.error('XPTY0004', 'only XPath nodes are allowed')
+            results.add(item)
+
+    if self.concatenated:
+        yield from results
     else:
-        yield from cast(list[XPathNode], sorted(results, key=node_position))
+        yield from sorted(results, key=node_position)
 
 
 ###
@@ -381,7 +384,7 @@
         for _ in context.iter_descendants():
             for result in self[0].select(context):
                 if not isinstance(result, XPathNode):
-                    items.add(result)
+                    yield result
                 elif result in items:
                     pass
                 elif isinstance(result, ElementNode):
@@ -407,7 +410,16 @@
     if context is None:
         raise self.missing_context()
 
-    for _ in self[0].select_with_focus(context):
+    left = self[0]
+    while left.symbol == '[':
+        left = left[0]
+    else:
+        if isinstance(left, XPathAxis):
+            reverse_axis = left.reverse_axis
+        else:
+            reverse_axis = False
+
+    for _ in self[0].select_with_focus(context, reverse_axis):
         if (self[1].label in ('axis', 'kind test') or self[1].symbol == '..') \
                 and not isinstance(context.item, XPathNode):
             raise self.error('XPTY0020')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath1/xpath1_parser.py 
new/elementpath-5.1.4/elementpath/xpath1/xpath1_parser.py
--- old/elementpath-5.1.3/elementpath/xpath1/xpath1_parser.py   2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath1/xpath1_parser.py   2026-08-08 
22:03:05.000000000 +0200
@@ -62,6 +62,11 @@
         '(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', 
'..', '.', '{'
     }
 
+    # Flag to skip argument parsing (set by XPath 3.1+ arrow operator)
+    # TODO: to revise with a TDOP instant parser created by parse() providing
+    #  XPath static context in a separated object.
+    parse_arguments: bool = True
+
     # Class attributes for compatibility with XPath 2.0+
     schema: Optional[AbstractSchemaProxy] = None
     variable_types: Optional[dict[str, str]] = None
@@ -71,9 +76,12 @@
     base_uri: Optional[str] = None
     function_namespace = XPATH_FUNCTIONS_NAMESPACE
     function_signatures: dict[tuple[QName, int], str] = {}
+
+    # Class attributes for compatibility with XPath 3.0+
     decimal_formats: dict[Optional[str], Any] = {}
-    parse_arguments: bool = True
     defuse_xml: bool = True
+    allow_environment: bool | list[str] = False
+    allow_external_resources: bool | list[str] = False
 
     compatibility_mode: bool = True
     """XPath 1.0 compatibility mode."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath2/_xpath2_operators.py 
new/elementpath-5.1.4/elementpath/xpath2/_xpath2_operators.py
--- old/elementpath-5.1.3/elementpath/xpath2/_xpath2_operators.py       
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath2/_xpath2_operators.py       
2026-08-08 22:03:05.000000000 +0200
@@ -164,7 +164,7 @@
     varnames = [cast(str, self[k][0].value) for k in range(0, len(self) - 1, 
2)]
     selectors = [self[k].select for k in range(1, len(self) - 1, 2)]
 
-    for results in context.iter_product(selectors, varnames):
+    for results in copy(context).iter_product(selectors, varnames):
         context.variables.update(x for x in zip(varnames, results))
         if self.boolean_value(self[-1].select(copy(context))):
             if some:
@@ -877,8 +877,7 @@
         for _ in context.iter_attributes():
             yield from cast(Iterator[AttributeNode], self[0].select(context))
     elif not self:
-        for attribute in context.iter_attributes():
-            yield attribute
+        yield from context.iter_attributes()
     else:
         name = self[0].value
         assert isinstance(name, str)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath30/_xpath30_functions.py 
new/elementpath-5.1.4/elementpath/xpath30/_xpath30_functions.py
--- old/elementpath-5.1.3/elementpath/xpath30/_xpath30_functions.py     
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath30/_xpath30_functions.py     
2026-08-08 22:03:05.000000000 +0200
@@ -28,7 +28,8 @@
 
 from elementpath.exceptions import ElementPathError
 from elementpath.tdop import MultiLabel
-from elementpath.helpers import Patterns, is_xml_codepoint, node_position
+from elementpath.helpers import Patterns, is_xml_codepoint, node_position, \
+    is_allowed_variable, is_allowed_uri
 from elementpath.namespaces import get_expanded_name, split_expanded_name, \
     XPATH_FUNCTIONS_NAMESPACE
 from elementpath.datatypes import NumericProxy, QName, Date, DateTime, Time, 
AnyURI
@@ -1114,8 +1115,8 @@
         raise self.error('XPTY0004', 'argument must contain only nodes')
 
     ancestors = {x for context.item in nodes for x in 
context.iter_ancestors(axis='ancestor')}
-    results = {x for x in nodes if x not in ancestors}
-    yield from cast(list[XPathNode], sorted(results, key=node_position))
+    results = {cast(XPathNode, x) for x in nodes if x not in ancestors}
+    yield from sorted(results, key=node_position)
 
 
 @method(function('outermost', nargs=1, sequence_types=('node()*', 'node()*')))
@@ -1138,6 +1139,7 @@
         ancestors = {x for x in context.iter_ancestors(axis='ancestor')}
         if any(x in nodes for x in ancestors):
             continue
+        assert isinstance(item, XPathNode)
         results.add(item)
 
     yield from cast(list[XPathNode], sorted(results, key=node_position))
@@ -1250,6 +1252,8 @@
 
     if context is not None and uri in context.text_resources:
         text = context.text_resources[uri]
+    elif not is_allowed_uri(uri, self.parser.allow_external_resources):
+        raise self.error('FOUT1170', f'URI {uri} is not allowed')
     else:
         try:
             with urlopen(uri) as rp:
@@ -1307,6 +1311,9 @@
         uri = self.get_absolute_uri(href)
     except ValueError:
         return False
+    else:
+        if not is_allowed_uri(uri, self.parser.allow_external_resources):
+            return False
 
     try:
         codecs.lookup(encoding)
@@ -1348,28 +1355,29 @@
         context = self.context
 
     name: str = self.get_argument(context, required=True, cls=str)
-    if context is None:
-        raise self.missing_context()
-    elif not context.allow_environment:
-        return []
-    else:
+    if is_allowed_variable(name, self.parser.allow_environment) or \
+            (not self.parser.allow_environment and context is not None and
+             is_allowed_variable(name, context.allow_environment)):
         value = os.environ.get(name)
         return value if value is not None else []
+    else:
+        return []
 
 
 @method(function('available-environment-variables', nargs=0,
                  sequence_types=('xs:string*',)))
 def evaluate__available_env_vars(self: XPathFunction, context: ta.ContextType 
= None) \
         -> list[str] | list[NoReturn]:
-    if self.context is not None:
-        context = self.context
-    elif context is None:
-        raise self.missing_context()
-
-    if not context.allow_environment:
-        return []
-    else:
+    if self.parser.allow_environment is True:
         return xlist(os.environ)
+    elif isinstance(self.parser.allow_environment, list):
+        varnames = []
+        for name in os.environ:
+            if is_allowed_variable(name, self.parser.allow_environment):
+                varnames.append(name)
+        return xlist(varnames) if varnames else varnames
+    else:
+        return []
 
 
 ###
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath30/xpath30_parser.py 
new/elementpath-5.1.4/elementpath/xpath30/xpath30_parser.py
--- old/elementpath-5.1.3/elementpath/xpath30/xpath30_parser.py 2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath30/xpath30_parser.py 2026-08-08 
22:03:05.000000000 +0200
@@ -17,6 +17,8 @@
 from copy import deepcopy
 from typing import Any, ClassVar, Optional
 
+from elementpath import ElementPathTypeError
+
 from elementpath.namespaces import XPATH_MATH_FUNCTIONS_NAMESPACE
 from elementpath.datatypes import QName
 from elementpath.xpath2 import XPath2Parser
@@ -34,6 +36,19 @@
     :param args: the same positional arguments of class 
:class:`elementpath.XPath2Parser`.
     :param decimal_formats: a mapping with statically known decimal formats.
     :param defuse_xml: if `True` defuse XML data before parsing, that is the 
default.
+    :param allow_environment: defines if the access to system environment 
variables is \
+    allowed, for default is `False`. Used by the XPath 3.0+ functions 
fn:environment-variable \
+    and fn:available-environment-variables. With `True` full access to the 
environment \
+    if allowed. Passing a list of names, the access is restricted to the 
variables that \
+    are in that list. To match all the variables that start with a name, use a 
'*' at \
+    the end (e.g., 'APP_*' matches 'APP_PORT' and 'APP_ADDRESS' but doesn't 
match \
+    'OTHER_APP_PORT').
+    :param allow_external_resources: defines the access to external resources 
by URL. \
+    Used by the XPath 3.0+ functions `fn:unparsed-text()`, 
`fn:unparsed-text-lines()`, \
+    `fn:unparsed-text-available()` and `fn:json-doc(). For default is `False` 
and no \
+    external resource can be accessed. Provide `True` the access is allowed 
for any \
+    URL. Provide a list of base paths to restrict access to only those URLs 
that match \
+    any of them.
     :param kwargs: the same keyword arguments of class 
:class:`elementpath.XPath2Parser`.
     """
     version = '3.0'
@@ -71,8 +86,12 @@
         }
     }
 
-    def __init__(self, *args: Any, decimal_formats: 
Optional[DecimalFormatsType] = None,
-                 defuse_xml: bool = True, **kwargs: Any) -> None:
+    def __init__(self, *args: Any,
+                 decimal_formats: Optional[DecimalFormatsType] = None,
+                 defuse_xml: bool = True,
+                 allow_environment: bool | list[str] = False,
+                 allow_external_resources: bool | list[str] = False,
+                 **kwargs: Any) -> None:
         kwargs.pop('strict', None)
         super(XPath30Parser, self).__init__(*args, **kwargs)
 
@@ -86,9 +105,22 @@
 
             if None in decimal_formats:
                 self.decimal_formats[None].update(decimal_formats[None])
+
         if not defuse_xml:
             self.defuse_xml = defuse_xml
 
+        if isinstance(allow_environment, (bool, list)):
+            self.allow_environment = allow_environment
+        else:
+            raise ElementPathTypeError(f"invalid type 
{type(allow_environment)} "
+                                       f"for argument allow_environment")
+
+        if isinstance(allow_external_resources, (bool, list)):
+            self.allow_external_resources = allow_external_resources
+        else:
+            raise ElementPathTypeError(f"invalid type 
{type(allow_external_resources)} "
+                                       f"for argument 
allow_external_resources")
+
     def __str__(self) -> str:
         args = []
         if self.decimal_formats != self.__class__.decimal_formats:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/elementpath-5.1.3/elementpath/xpath31/_xpath31_functions.py 
new/elementpath-5.1.4/elementpath/xpath31/_xpath31_functions.py
--- old/elementpath-5.1.3/elementpath/xpath31/_xpath31_functions.py     
2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath31/_xpath31_functions.py     
2026-08-08 22:03:05.000000000 +0200
@@ -33,7 +33,7 @@
     Language, NumericProxy, Timezone, UntypedAtomic
 from elementpath.namespaces import XML_BASE, XPATH_FUNCTIONS_NAMESPACE
 from elementpath.helpers import collapse_white_spaces, is_xml_codepoint, \
-    escape_json_string, unescape_json_string, not_equal
+    escape_json_string, unescape_json_string, not_equal, is_allowed_uri
 from elementpath.sequences import xlist
 from elementpath.etree import etree_iter_strings, is_etree_element
 from elementpath.collations import CollationManager
@@ -77,7 +77,7 @@
         return cast(ta.NumericType, value)
 
     try:
-        return cast(float, NumericProxy(value))  # type: ignore[arg-type]
+        return NumericProxy(value)  # type: ignore[arg-type]
     except ValueError as err:
         if isinstance(value, (str, UntypedAtomic)):
             raise self.error('FORG0001', err)
@@ -697,16 +697,18 @@
         if href is None:
             return []
 
-        try:
-            if urlsplit(href).scheme:
-                with urlopen(href) as fp:
-                    json_text = fp.read().decode('utf-8')
-            else:
-                with pathlib.Path(href).open() as fp:
-                    json_text = fp.read()
-        except IOError:
-            raise self.error('FOUT1170') from None
-
+        if not is_allowed_uri(href, self.parser.allow_external_resources):
+            raise self.error('FOUT1170', f'URI {href} is not allowed')
+        else:
+            try:
+                if urlsplit(href).scheme:
+                    with urlopen(href) as fp:
+                        json_text = fp.read().decode('utf-8')
+                else:
+                    with pathlib.Path(href).open() as fp:
+                        json_text = fp.read()
+            except IOError:
+                raise self.error('FOUT1170') from None
     else:
         href = None
         json_text = self.get_argument(context, cls=str)
@@ -1174,7 +1176,7 @@
                 value = ''.join(etree_iter_strings(child))
                 try:
                     if self.parser.xsd_version == '1.0':
-                        number = cast(float, DoubleProxy10(value))
+                        number = DoubleProxy10(value)
                     else:
                         number = cast(float, DoubleProxy(value))
                 except ValueError:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/xpath_context.py 
new/elementpath-5.1.4/elementpath/xpath_context.py
--- old/elementpath-5.1.3/elementpath/xpath_context.py  2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath_context.py  2026-08-08 
22:03:05.000000000 +0200
@@ -9,6 +9,7 @@
 #
 import datetime
 import importlib
+import warnings
 from collections.abc import Iterator, Sequence, Callable
 from functools import cached_property
 from types import ModuleType
@@ -38,51 +39,52 @@
     The other optional arguments are needed only if a specific position on the 
context is
     required, but have to be used with the knowledge of what is their meaning.
 
-    :param root: the root of the XML document, usually an ElementTree instance 
or an \
-    Element. A schema or a schema element can also be provided, or an already 
built \
-    node tree. For default is `None`, in which case no XML root is set, and 
you have \
-    to provide an *item* argument.
-    :param namespaces: a dictionary with mapping from namespace prefixes into 
URIs, \
+    :param root: The root of the XML document, usually an ElementTree instance 
or an \
+    Element. A schema or a schema element can also be provided, or a built 
node tree. \
+    For default is `None`, in which case no XML root is set, and you have to 
provide \
+    an *item* argument.
+    :param namespaces: A dictionary with mapping from namespace prefixes into 
URIs, \
     used when namespace information is not available within document and 
element nodes. \
     This can be useful when the dynamic context has additional namespaces and 
root \
     is an Element or an ElementTree instance of the standard library.
-    :param uri: an optional URI associated with the root element or the 
document.
-    :param fragment: if `True` is provided the root is considered a fragment. 
In this \
+    :param uri: An optional URI associated with the root element or the 
document.
+    :param fragment: If `True` is provided the root is considered a fragment. 
In this \
     case if `root` is an ElementTree instance skips it and use the root 
Element. If \
     `False` is provided creates a dummy document when the root is an Element 
instance. \
-    In this case the dummy document value is not included in results. For 
default the \
+    In this case the dummy document value is not included in the results. For 
default the \
     root node kind is preserved.
-    :param item: the context item. A `None` value means that the context is 
positioned on \
+    :param item: The context item. A `None` value means that the context is 
positioned on \
     the document node.
-    :param position: the current position of the node within the input 
sequence.
-    :param size: the number of items in the input sequence.
-    :param axis: the active axis. Used to choose when apply the default axis 
('child' axis).
-    :param schema: an optional schema proxy instance to be applied on XDM root 
or item.
-    :param variables: dictionary of context variables that maps a QName to a 
value.
-    :param current_dt: current dateTime of the implementation, including 
explicit timezone.
-    :param timezone: implicit timezone to be used when a date, time, or 
dateTime value does \
+    :param position: The current position of the node within the input 
sequence.
+    :param size: The number of items in the input sequence.
+    :param axis: The active axis. Used to choose when apply the default axis 
('child' axis).
+    :param schema: An optional schema proxy instance to be applied on XDM root 
or item.
+    :param variables: Dictionary of context variables that maps a QName to a 
value.
+    :param current_dt: Current dateTime of the implementation, including 
explicit timezone.
+    :param timezone: Implicit timezone to be used when a date, time, or 
dateTime value does \
     not have a timezone.
-    :param documents: available documents. This is a mapping of absolute URI \
+    :param documents: Available documents. This is a mapping of absolute URI \
     strings into document nodes. Used by the function fn:doc.
-    :param collections: available collections. This is a mapping of absolute 
URI \
+    :param collections: Available collections. This is a mapping of absolute 
URI \
     strings onto sequences of nodes. Used by the XPath 2.0+ function 
fn:collection.
-    :param default_collection: this is the sequence of nodes used when 
fn:collection \
+    :param default_collection: This is the sequence of nodes used when 
fn:collection \
     is called with no arguments.
-    :param text_resources: available text resources. This is a mapping of 
absolute URI strings \
+    :param text_resources: Available text resources. This is a mapping of 
absolute URI strings \
     onto text resources. Used by XPath 3.0+ function 
fn:unparsed-text/fn:unparsed-text-lines.
-    :param resource_collections: available URI collections. This is a mapping 
of absolute \
-    URI strings to sequence of URIs. Used by the XPath 3.0+ function 
fn:uri-collection.
-    :param default_resource_collection: this is the sequence of URIs used when 
\
+    :param resource_collections: Available URI collections. This is a mapping 
of absolute \
+    URI strings to a sequence of URIs. Used by the XPath 3.0+ function 
fn:uri-collection.
+    :param default_resource_collection: This is the sequence of URIs used when 
\
     fn:uri-collection is called with no arguments.
-    :param allow_environment: defines if the access to system environment is 
allowed, \
-    for default is `False`. Used by the XPath 3.0+ functions 
fn:environment-variable \
-    and fn:available-environment-variables.
+    :param allow_environment: defines if the access to system environment 
variables is \
+    allowed, for default is `False`. Deprecated here, it has been extended and 
moved \
+    to XPath30Parser/XPath31Parser init.
     """
     _etree: Optional[ModuleType] = None
     _schema: Optional[ta.SchemaProxyType] = None
     root: Optional[ta.RootNodeType]
     document: DocumentNode | None
     item: ta.ItemType
+    size: int | None
 
     documents: dict[str, DocumentNode] | None = None
     collections: dict[str, list[XPathNode]] | None = None
@@ -177,11 +179,17 @@
         self.text_resources = text_resources if text_resources is not None 
else {}
         self.resource_collections = resource_collections
         self.default_resource_collection = default_resource_collection
-        self.allow_environment = allow_environment
         self.default_language = None if default_language is None else 
Language(default_language)
         self.default_calendar = default_calendar
         self.default_place = default_place
 
+        self.allow_environment = allow_environment
+        if allow_environment:
+            msg = ("Argument 'allow_environment' has been extended and moved 
to XPath "
+                   "3.0+ parser so that option here is deprecated and will be 
removed "
+                   "in the next major release.")
+            warnings.warn(DeprecationWarning(msg))
+
     def __repr__(self) -> str:
         if self.root is not None:
             return f'{self.__class__.__name__}(root={self.root.value})'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/xpath_tokens/axes.py 
new/elementpath-5.1.4/elementpath/xpath_tokens/axes.py
--- old/elementpath-5.1.3/elementpath/xpath_tokens/axes.py      2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath_tokens/axes.py      2026-08-08 
22:03:05.000000000 +0200
@@ -36,8 +36,9 @@
     def source(self) -> str:
         return '%s::%s' % (self.symbol, self[0].source)
 
-    def select_with_focus(self, context: XPathContext) -> 
Iterator[ta.ItemType]:
-        """Select item with an inner focus on dynamic context."""
+    def select_with_focus(self, context: XPathContext, reverse_axis: bool = 
False) \
+            -> Iterator[ta.ItemType]:
+        """Select an item with an inner focus on dynamic context."""
         status = context.item, context.size, context.position, context.axis
         results = [x for x in self.select(context)]
         context.item, context.size, context.position, context.axis = status
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/xpath_tokens/base.py 
new/elementpath-5.1.4/elementpath/xpath_tokens/base.py
--- old/elementpath-5.1.3/elementpath/xpath_tokens/base.py      2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath_tokens/base.py      2026-08-08 
22:03:05.000000000 +0200
@@ -208,16 +208,22 @@
             else:
                 yield item
 
-    def select_with_focus(self, context: XPathContext) -> 
Iterator[ta.ItemType]:
-        """Select item with an inner focus on dynamic context."""
+    def select_with_focus(self, context: XPathContext, reverse_axis: bool = 
False) \
+            -> Iterator[ta.ItemType]:
+        """Select an item with an inner focus on dynamic context."""
         status = context.item, context.size, context.position, context.axis
         context.axis = None
         results = [x for x in self.select(context)]
 
-        context.axis = None
-        context.size = len(results)
-        for context.position, context.item in enumerate(results, start=1):
-            yield context.item
+        if reverse_axis:
+            context.size = context.position = len(results)
+            for context.item in results:
+                yield context.item
+                context.position -= 1
+        else:
+            context.size = len(results)
+            for context.position, context.item in enumerate(results, start=1):
+                yield context.item
 
         context.item, context.size, context.position, context.axis = status
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/elementpath/xpath_tokens/tokens.py 
new/elementpath-5.1.4/elementpath/xpath_tokens/tokens.py
--- old/elementpath-5.1.3/elementpath/xpath_tokens/tokens.py    2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/elementpath/xpath_tokens/tokens.py    2026-08-08 
22:03:05.000000000 +0200
@@ -82,7 +82,7 @@
 
 
 ###
-# Name related tokens for matching elements and attributes
+# Name-related tokens for matching elements and attributes
 class NameToken(XPathToken):
     """
     The special '(name)' token for matching attributes or element nodes.
@@ -134,7 +134,7 @@
     def __init__(self, parser: ta.XPathParserType, value: Literal[':'] = ':') 
-> None:
         super().__init__(parser, value)
 
-        # Change bind powers if it cannot be a namespace related token
+        # Change bind powers if it cannot be a namespace-related token
         if self.is_spaced():
             self.lbp = self.rbp = 0
         elif self.parser.token.symbol not in ('*', '(name)', 'array'):
@@ -217,7 +217,7 @@
 
 
 class BracedNameToken(XPathToken):
-    """Braced expanded name for expressing namespace related names."""
+    """Braced expanded name for expressing namespace-related names."""
 
     symbol = lookup_name = '{'
     label = 'expanded name'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/publiccode.yml 
new/elementpath-5.1.4/publiccode.yml
--- old/elementpath-5.1.3/publiccode.yml        2026-06-28 13:17:12.000000000 
+0200
+++ new/elementpath-5.1.4/publiccode.yml        2026-08-08 22:03:05.000000000 
+0200
@@ -6,8 +6,8 @@
 name: elementpath
 url: 'https://github.com/sissaschool/elementpath'
 landingURL: 'https://github.com/sissaschool/elementpath'
-releaseDate: '2025-06-28'
-softwareVersion: v5.1.3
+releaseDate: '2025-08-08'
+softwareVersion: v5.1.4
 developmentStatus: stable
 platforms:
   - linux
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/pyproject.toml 
new/elementpath-5.1.4/pyproject.toml
--- old/elementpath-5.1.3/pyproject.toml        2026-06-28 13:17:12.000000000 
+0200
+++ new/elementpath-5.1.4/pyproject.toml        2026-08-08 22:03:05.000000000 
+0200
@@ -4,7 +4,7 @@
 
 [project]
 name = "elementpath"
-version = "5.1.3"
+version = "5.1.4"
 description = "XPath 1.0/2.0/3.0/3.1 parsers and selectors for ElementTree and 
lxml"
 readme = "README.rst"
 license = "MIT"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/tests/run_w3c_tests.py 
new/elementpath-5.1.4/tests/run_w3c_tests.py
--- old/elementpath-5.1.3/tests/run_w3c_tests.py        2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/tests/run_w3c_tests.py        2026-08-08 
22:03:05.000000000 +0200
@@ -770,6 +770,8 @@
             if environment.decimal_formats:
                 kwargs['decimal_formats'] = environment.decimal_formats
             kwargs['defuse_xml'] = False
+            kwargs['allow_environment'] = True
+            kwargs['allow_external_resources'] = True
 
         self.parser = xpath_parser(**kwargs)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/tests/test_helpers.py 
new/elementpath-5.1.4/tests/test_helpers.py
--- old/elementpath-5.1.3/tests/test_helpers.py 2026-06-28 13:17:12.000000000 
+0200
+++ new/elementpath-5.1.4/tests/test_helpers.py 2026-08-08 22:03:05.000000000 
+0200
@@ -17,7 +17,8 @@
 from elementpath.helpers import LazyPattern, days_from_common_era, \
     months2days, round_number, is_idrefs, collapse_white_spaces, 
escape_json_string, \
     get_double, numeric_equal, numeric_not_equal, equal, not_equal, \
-    match_wildcard, unescape_json_string, split_function_test
+    match_wildcard, unescape_json_string, split_function_test, 
is_allowed_variable, \
+    is_allowed_uri
 from elementpath.xpath30.xpath30_helpers import decimal_to_string, 
int_to_roman, \
     int_to_month, int_to_weekday, int_to_words, int_to_alphabetic, 
week_in_month, \
     to_ordinal_en, to_ordinal_it, format_digits, ordinal_suffix
@@ -253,6 +254,84 @@
             ['item()*', 'item()*', 'item()*', 'item()*']
         )
 
+    def test_is_allowed_variable_function(self):
+        self.assertFalse(is_allowed_variable('x'))
+        self.assertTrue(is_allowed_variable('var', allow_environment=True))
+        self.assertFalse(is_allowed_variable('var', allow_environment=False))
+
+        # Invalid variable names
+        self.assertFalse(is_allowed_variable('foo'))
+        self.assertFalse(is_allowed_variable('123'))
+        self.assertFalse(is_allowed_variable('1var'))
+        self.assertFalse(is_allowed_variable('var:name'))
+        self.assertFalse(is_allowed_variable('var name'))
+        self.assertFalse(is_allowed_variable('var@name'))
+        self.assertFalse(is_allowed_variable('var#name'))
+        self.assertFalse(is_allowed_variable('var!'))
+        self.assertFalse(is_allowed_variable('var$'))
+        self.assertFalse(is_allowed_variable('var%'))
+        self.assertFalse(is_allowed_variable('var&'))
+        self.assertFalse(is_allowed_variable('var*'))
+        self.assertFalse(is_allowed_variable('var+'))
+        self.assertFalse(is_allowed_variable('var='))
+
+    def test_is_allowed_uri_function(self):
+        self.assertFalse(is_allowed_uri('https://www.example.com'))
+        self.assertFalse(is_allowed_uri('ftp://ftp.example.com'))
+        self.assertFalse(is_allowed_uri('file:///path/to/file'))
+        self.assertFalse(is_allowed_uri('urn:isbn:0451450523'))
+        self.assertFalse(is_allowed_uri('mailto:[email protected]'))
+        self.assertFalse(is_allowed_uri('http://example.com/path?query=value'))
+        self.assertFalse(is_allowed_uri('http://example.com/path#fragment'))
+        self.assertFalse(is_allowed_uri('http://example.com:8080/path'))
+        self.assertFalse(is_allowed_uri('http://user:[email protected]'))
+        self.assertFalse(is_allowed_uri('http://192.168.1.1'))
+        self.assertFalse(is_allowed_uri('http://[::1]'))
+        
self.assertFalse(is_allowed_uri('http://example.com/path%20with%20spaces'))
+        self.assertFalse(is_allowed_uri('scheme://example.com'))
+        self.assertFalse(is_allowed_uri('a:b'))
+
+        self.assertTrue(is_allowed_uri('https://www.example.com', 
allow_external_resources=True))
+        self.assertTrue(is_allowed_uri('ftp://ftp.example.com', True))
+        self.assertTrue(is_allowed_uri('file:///path/to/file', True))
+        self.assertTrue(is_allowed_uri('urn:isbn:0451450523', True))
+        self.assertFalse(is_allowed_uri('mailto:[email protected]', True))
+        self.assertTrue(is_allowed_uri('http://example.com/path?query=value', 
True))
+        self.assertTrue(is_allowed_uri('http://example.com/path#fragment', 
True))
+        self.assertTrue(is_allowed_uri('http://example.com:8080/path', True))
+        self.assertTrue(is_allowed_uri('http://user:[email protected]', True))
+        self.assertTrue(is_allowed_uri('http://192.168.1.1', True))
+        self.assertTrue(is_allowed_uri('http://[::1]', True))
+        
self.assertTrue(is_allowed_uri('http://example.com/path%20with%20spaces', True))
+        self.assertFalse(is_allowed_uri('scheme://example.com', False))
+        self.assertFalse(is_allowed_uri('a:b', False))
+
+        allow_external_resources = ['https://www.example.com/',
+                                    'ftp://ftp.example.com',
+                                    'http://example.com/',
+                                    'file:///path/to/file']
+
+        self.assertTrue(is_allowed_uri('https://www.example.com', 
allow_external_resources))
+        self.assertTrue(is_allowed_uri('ftp://ftp.example.com', 
allow_external_resources))
+        self.assertTrue(is_allowed_uri('file:///path/to/file', 
allow_external_resources))
+        self.assertFalse(is_allowed_uri('urn:isbn:0451450523', 
allow_external_resources))
+        self.assertTrue(is_allowed_uri('urn:isbn:0451450523', ['urn:']))
+
+        self.assertFalse(is_allowed_uri('mailto:[email protected]', 
['mailto:[email protected]']))
+        self.assertTrue(is_allowed_uri('http://example.com/path?query=value',
+                                       allow_external_resources))
+        self.assertTrue(is_allowed_uri('http://example.com/path#fragment',
+                                       allow_external_resources))
+        self.assertFalse(is_allowed_uri('http://example.com:8080/path', 
allow_external_resources))
+        self.assertFalse(is_allowed_uri('http://user:[email protected]', 
allow_external_resources))
+        self.assertFalse(is_allowed_uri('http://192.168.1.1', 
allow_external_resources))
+        self.assertFalse(
+            is_allowed_uri('http://[::1]', allow_external_resources))
+        
self.assertFalse(is_allowed_uri('http://www.example.com/path%20with%20spaces',
+                                        allow_external_resources))
+        self.assertFalse(is_allowed_uri('scheme://example.com', 
allow_external_resources))
+        self.assertFalse(is_allowed_uri('a:b', allow_external_resources))
+
 
 class XPath30HelperFunctionsTest(unittest.TestCase):
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/tests/test_xpath30.py 
new/elementpath-5.1.4/tests/test_xpath30.py
--- old/elementpath-5.1.3/tests/test_xpath30.py 2026-06-28 13:17:12.000000000 
+0200
+++ new/elementpath-5.1.4/tests/test_xpath30.py 2026-08-08 22:03:05.000000000 
+0200
@@ -1019,24 +1019,30 @@
                 text = fp.read()
             self.assertEqual([x.strip() for x in text.strip().split('\n')], 
file_lines)
 
+            parser = self.parser.__class__()
             path = 'fn:unparsed-text("file://{}")'.format(str(filepath))
-            text = self.parser.parse(path).evaluate()
+            with self.assertRaises(ValueError) as ctx:
+                parser.parse(path).evaluate()
+            self.assertIn('FOUT1170', str(ctx.exception))
+
+            parser = self.parser.__class__(allow_external_resources=True)
+            path = 'fn:unparsed-text("file://{}")'.format(str(filepath))
+            text = parser.parse(path).evaluate()
             self.assertEqual([x.strip() for x in text.strip().split('\n')], 
file_lines)
 
             path = 'fn:unparsed-text("file://{}", 
"unknown")'.format(str(filepath))
             with self.assertRaises(ValueError) as ctx:
-                self.parser.parse(path).evaluate()
+                parser.parse(path).evaluate()
             self.assertIn('FOUT1190', str(ctx.exception))
 
     def test_environment_variable_function(self):
-        with self.assertRaises(MissingContextError):
-            self.parser.parse('fn:environment-variable("PATH")').evaluate()
+        parser = self.parser.__class__()
 
         root = self.etree.XML('<root/>')
         context = XPathContext(root=root)
         path = 'fn:environment-variable("PATH")'
-        self.assertEqual(self.parser.parse(path).evaluate(context), [])
-        context = XPathContext(root=root, allow_environment=True)
+        self.assertEqual(parser.parse(path).evaluate(context), [])
+        self.assertEqual(parser.parse(path).evaluate(), [])
 
         try:
             key = list(os.environ)[0]
@@ -1044,18 +1050,30 @@
             pass
         else:
             path = 'fn:environment-variable("{}")'.format(key)
-            self.assertEqual(self.parser.parse(path).evaluate(context), 
os.environ[key])
 
-    def test_available_environment_variables_function(self):
-        with self.assertRaises(MissingContextError):
-            
self.parser.parse('fn:available-environment-variables()').evaluate()
+            parser = self.parser.__class__(allow_environment=True)
+            self.assertEqual(parser.parse(path).evaluate(context), 
os.environ[key])
+            self.assertEqual(parser.parse(path).evaluate(context), 
os.environ[key])
+
+            parser = self.parser.__class__(allow_environment=[key])
+            self.assertEqual(parser.parse(path).evaluate(context), 
os.environ[key])
+            self.assertEqual(parser.parse(path).evaluate(context), 
os.environ[key])
+
+            parser = self.parser.__class__(allow_environment=['FOO'])
+            self.assertEqual(parser.parse(path).evaluate(context), [])
+            self.assertEqual(parser.parse(path).evaluate(context), [])
 
+    def test_available_environment_variables_function(self):
+        parser = self.parser.__class__()
         root = self.etree.XML('<root/>')
         context = XPathContext(root=root)
         path = 'fn:available-environment-variables()'
-        self.assertEqual(self.parser.parse(path).evaluate(context), [])
-        context = XPathContext(root=root, allow_environment=True)
-        self.assertEqual(self.parser.parse(path).evaluate(context), 
list(os.environ))
+        self.assertEqual(parser.parse(path).evaluate(context), [])
+        self.assertEqual(parser.parse(path).evaluate(), [])
+
+        parser = self.parser.__class__(allow_environment=True)
+        self.assertEqual(parser.parse(path).evaluate(context), 
list(os.environ))
+        self.assertEqual(parser.parse(path).evaluate(), list(os.environ))
 
     def test_inline_function_expression(self):
         expression = "function() as xs:integer+ {2, 3, 5, 7, 11, 13}"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/tests/test_xpath_context.py 
new/elementpath-5.1.4/tests/test_xpath_context.py
--- old/elementpath-5.1.3/tests/test_xpath_context.py   2026-06-28 
13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/tests/test_xpath_context.py   2026-08-08 
22:03:05.000000000 +0200
@@ -311,6 +311,43 @@
             )
 
     @unittest.skipIf(lxml_etree is None, 'lxml library is not installed')
+    def test_inner_focus_context_position(self):
+        xml_source = '<a attr="A"><b/><b/><b attr="B"><c 
attr="C">text</c></b></a>'
+        root = lxml_etree.XML(xml_source)
+
+        expressions = [
+            '/a/b[3]/*[position()=1]',
+            '/a/b[3]/*[position()=2]'
+        ]
+        for expr in expressions:
+            lxml_result = root.xpath(expr)
+            result = select(root, expr)
+            self.assertEqual(lxml_result, result, msg=expr)
+
+    @unittest.skipIf(lxml_etree is None, 'lxml library is not installed')
+    def test_position_with_reverse_axis__issue_105(self):
+        xml_source = '<a attr="A"><b/><b/><b attr="B"><c 
attr="C">text</c></b></a>'
+        root = lxml_etree.XML(xml_source)
+
+        expressions = [
+            "ancestor-or-self::*[@attr]",
+            "ancestor-or-self::*[@attr][1]",
+            "(ancestor-or-self::*[@attr])[1]",
+            "ancestor-or-self::*[@attr][last()]",
+            "(ancestor-or-self::*[@attr])[last()]",
+            "ancestor-or-self::*",
+            "ancestor-or-self::*[1]",
+            "(ancestor-or-self::*)[1]",
+            "ancestor-or-self::*[last()]",
+            "(ancestor-or-self::*)[last()]",
+        ]
+
+        for expr in expressions:
+            lxml_result = root.xpath(expr)
+            result = select(root, expr)
+            self.assertEqual(lxml_result, result, msg=expr)
+
+    @unittest.skipIf(lxml_etree is None, 'lxml library is not installed')
     def test_iter_siblings__issue_44(self):
         root = lxml_etree.XML('<root>text 1<!-- comment -->text 2<!-- comment 
--> text 3</root>')
         result = select(root, 'node()[1]/following-sibling::node()')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/elementpath-5.1.3/tox.ini 
new/elementpath-5.1.4/tox.ini
--- old/elementpath-5.1.3/tox.ini       2026-06-28 13:17:12.000000000 +0200
+++ new/elementpath-5.1.4/tox.ini       2026-08-08 22:03:05.000000000 +0200
@@ -68,7 +68,7 @@
 
 [testenv:mypy-py{310,311,312,313,314,315}]
 deps =
-    mypy==2.1.0
+    mypy==2.3.0
     xmlschema~=4.2.0
     lxml-stubs
 commands =
@@ -100,7 +100,7 @@
     xmlschema420: VERSION = 4.2.0
 change_dir = {env_tmp_dir}
 deps =
-    mypy==2.1.0
+    mypy==2.3.0
     lxml
     lxml-stubs
     jinja2

Reply via email to