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
