elementpath/datatypes/datetime.py
class Duration(AnyAtomicType):
    """
    Base class for the XSD duration types.

    :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.
    """
    name = 'duration'
    pattern = re.compile(
        r'^(-)?P(?=[0-9]|T)(?:([0-9]+)Y)?(?:([0-9]+)M)?(?:([0-9]+)D)?'
        r'(?:T(?=[0-9])(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+(?:\.[0-9]+)?)S)?)?$'
    )

    def __init__(self, months: int = 0, seconds: Union[Decimal, int] = 0) -> None:
        if seconds < 0 < months or months < 0 < seconds:
            raise ValueError('signs differ: (months=%d, seconds=%d)' % (months, seconds))
        elif abs(months) > 2 ** 31:
            raise OverflowError("months duration overflow")
        elif abs(seconds) > 2 ** 63:  # type: ignore[operator]
            raise OverflowError("seconds duration overflow")

        self.months = months
        self.seconds = Decimal(seconds).quantize(Decimal('1.000000'))

    def __repr__(self) -> str:
        return '{}(months={!r}, seconds={})'.format(
            self.__class__.__name__, self.months, normalized_seconds(self.seconds)
        )

    def __str__(self) -> str:
        m = abs(self.months)
        years, months = m // 12, m % 12
        s = self.seconds.copy_abs()
        days = int(s // 86400)
        hours = int(s // 3600 % 24)
        minutes = int(s // 60 % 60)
        seconds = s % 60

        value = '-P' if self.sign else 'P'
        if years or months or days:
            if years:
                value += '%dY' % years
            if months:
                value += '%dM' % months
            if days:
                value += '%dD' % days

        if hours or minutes or seconds:
            value += 'T'
            if hours:
                value += '%dH' % hours
            if minutes:
                value += '%dM' % minutes
            if seconds:
                value += '%sS' % normalized_seconds(seconds)

        elif value[-1] == 'P':
            value += 'T0S'
        return value

    @classmethod
    def fromstring(cls, text: str) -> 'Duration':
        """
        Creates a 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.
        """
        if not isinstance(text, str):
            msg = 'argument has an invalid type {!r}'
            raise TypeError(msg.format(type(text)))

        match = cls.pattern.match(text.strip())
        if match is None:
            raise ValueError('%r is not an xs:duration value' % text)

        sign, y, mo, d, h, mi, s = match.groups()
        seconds = Decimal(s or 0)
        minutes = int(mi or 0) + int(seconds // 60)
        seconds = seconds % 60
        hours = int(h or 0) + minutes // 60
        minutes = minutes % 60
        days = int(d or 0) + hours // 24
        hours = hours % 24
        months = int(mo or 0) + 12 * int(y or 0)

        if sign is None:
            seconds = seconds + (days * 24 + hours) * 3600 + minutes * 60
        else:
            months = -months
            seconds = -seconds - (days * 24 + hours) * 3600 - minutes * 60

        if cls is DayTimeDuration:
            if months:
                raise ValueError('months must be 0 for %r' % cls.__name__)
            return cls(seconds=seconds)
        elif cls is YearMonthDuration:
            if seconds:
                raise ValueError('seconds must be 0 for %r' % cls.__name__)
            return cls(months=months)
        return cls(months=months, seconds=seconds)

    @property
    def sign(self) -> str:
        return '-' if self.months < 0 or self.seconds < 0 else ''

    def _compare_durations(self, other: object, op: Callable[[Any, Any], Any]) -> bool:
        """
        Ordering is defined through comparison of four datetime.datetime values.

        Ref: https://www.w3.org/TR/2012/REC-xmlschema11-2-20120405/#duration
        """
        if not isinstance(other, self.__class__):
            raise TypeError("wrong type %r for operand %r" % (type(other), other))

        m1, s1 = self.months, int(self.seconds)
        m2, s2 = other.months, int(other.seconds)
        ms1, ms2 = int((self.seconds - s1) * 1000000), int((other.seconds - s2) * 1000000)
        return all([
            op(datetime.timedelta(months2days(1696, 9, m1), s1, ms1),
               datetime.timedelta(months2days(1696, 9, m2), s2, ms2)),
            op(datetime.timedelta(months2days(1697, 2, m1), s1, ms1),
               datetime.timedelta(months2days(1697, 2, m2), s2, ms2)),
            op(datetime.timedelta(months2days(1903, 3, m1), s1, ms1),
               datetime.timedelta(months2days(1903, 3, m2), s2, ms2)),
            op(datetime.timedelta(months2days(1903, 7, m1), s1, ms1),
               datetime.timedelta(months2days(1903, 7, m2), s2, ms2)),
        ])

    def __hash__(self) -> int:
        return hash((self.months, self.seconds))

    def __eq__(self, other: object) -> bool:
        if isinstance(other, self.__class__):
            return self.months == other.months and self.seconds == other.seconds
        elif isinstance(other, UntypedAtomic):
            return self.__eq__(self.fromstring(other.value))
        else:
            return other == (self.months, self.seconds)

    def __ne__(self, other: object) -> bool:
        if isinstance(other, self.__class__):
            return self.months != other.months or self.seconds != other.seconds
        elif isinstance(other, UntypedAtomic):
            return self.__ne__(self.fromstring(other.value))
        else:
            return other != (self.months, self.seconds)

    def __lt__(self, other: object) -> bool:
        return self._compare_durations(other, operator.lt)

    def __le__(self, other: object) -> bool:
        return self == other or self._compare_durations(other, operator.le)

    def __gt__(self, other: object) -> bool:
        return self._compare_durations(other, operator.gt)

    def __ge__(self, other: object) -> bool:
        return self == other or self._compare_durations(other, operator.ge)
elementpath/xpath_token.py
class XPathFunction(XPathToken):
    """
    A token for processing XPath functions.
    """
    _name: Optional[QName] = None
    pattern = r'(?<!\$)\b[^\d\W][\w.\-\xb7\u0300-\u036F\u203F\u2040]*' \
              r'(?=\s*(?:\(\:.*\:\))?\s*\((?!\:))'

    sequence_types: Tuple[str, ...] = ()
    "Sequence types of arguments and of the return value of the function."

    nargs: NargsType = None
    "Number of arguments: a single value or a couple with None that means unbounded."

    body: Optional[XPathToken] = None
    "Body of anonymous inline function."

    variables: Optional[Dict[str, Any]] = None
    "Optional variables linked by let and for expressions."

    def __init__(self, parser: 'XPath1Parser', nargs: Optional[int] = None) -> None:
        super().__init__(parser)
        if isinstance(nargs, int) and nargs != self.nargs:
            if nargs < 0:
                raise self.error('XPST0017', 'number of arguments must be non negative')
            elif self.nargs is None:
                self.nargs = nargs
            elif isinstance(self.nargs, int):
                raise self.error('XPST0017', 'incongruent number of arguments')
            elif self.nargs[0] > nargs or self.nargs[1] is not None and self.nargs[1] < nargs:
                raise self.error('XPST0017', 'incongruent number of arguments')
            else:
                self.nargs = nargs

    def __call__(self, context: Optional[XPathContext] = None,
                 argument_list: Optional[Union[
                     XPathToken,
                     List[Union[XPathToken, AtomicValueType]],
                     Tuple[Union[XPathToken, AtomicValueType], ...]
                 ]] = None) -> Any:

        args: List[Any] = []
        if isinstance(argument_list, (list, tuple)):
            args.extend(argument_list)
        elif isinstance(argument_list, XPathToken):
            tk = argument_list
            while True:
                if tk.symbol == ',':
                    args.append(tk[1].evaluate(context))
                    tk = tk[0]
                else:
                    args.append(tk.evaluate(context))
                    break
            args.reverse()

        # Check provided argument with arity
        if self.nargs is None or self.nargs == len(args):
            pass
        elif isinstance(self.nargs, tuple):
            if len(args) < self.nargs[0]:
                raise self.error('XPTY0004', "missing required arguments")
            elif self.nargs[1] is not None and len(args) > self.nargs[1]:
                raise self.error('XPTY0004', "too many arguments")
        elif self.nargs > len(args):
            raise self.error('XPTY0004', "missing required arguments")
        else:
            raise self.error('XPTY0004', "too many arguments")

        context = copy(context)
        if self.variables is not None and context is not None:
            context.variables.update(self.variables)

        if self.symbol == 'function':
            if context is None:
                raise self.missing_context()
            elif not args and self:
                if context.item is None:
                    if is_document_node(context.root):
                        context.item = cast(DocumentProtocol, context.root).getroot()
                    else:
                        context.item = context.root

                args.append(context.item)

            partial_function = False
            if self.variables is None:
                self.variables = {}

            for variable, sequence_type, value in zip(self, self.sequence_types, args):
                varname = cast(str, variable[0].value)

                if isinstance(value, XPathToken) and value.symbol == '?':
                    partial_function = True
                    continue
                elif isinstance(value, XPathFunction) and sequence_type.startswith('function('):
                    if not value.match_function_test(sequence_type, as_argument=True):
                        msg = "argument {!r}: {} does not match sequence type {}"
                        raise self.error('XPTY0004', msg.format(varname, value, sequence_type))

                elif not self.parser.match_sequence_type(value, sequence_type):
                    value = self.cast_to_primitive_type(value, sequence_type)
                    if not self.parser.match_sequence_type(value, sequence_type):
                        msg = "argument {!r}: {} does not match sequence type {}"
                        raise self.error('XPTY0004', msg.format(varname, value, sequence_type))

                context.variables[varname] = self.variables[varname] = value

            if partial_function:
                return self

        elif self.label == 'partial function':
            for value, tk in zip(args, filter(lambda x: x.symbol == '?', self)):
                if isinstance(value, XPathToken):
                    tk.value = value.evaluate(context)
                else:
                    assert not isinstance(value, Token), "An atomic value or None expected"
                    tk.value = value
        else:
            self.clear()
            for value in args:
                if isinstance(value, XPathToken):
                    self.append(value)
                else:
                    assert not isinstance(value, Token), "An atomic value or None expected"
                    self.append(ValueToken(self.parser, value=value))

            if any(tk.symbol == '?' for tk in self._items):
                self._partial_function()
                return self

        if isinstance(self.label, MultiLabel):
            # Disambiguate multi-label tokens
            if self.namespace == XSD_NAMESPACE and \
                    'constructor function' in self.label.values:
                self.label = 'constructor function'
            else:
                for label in self.label.values:
                    if label.endswith('function'):
                        self.label = label
                        break

        if self.label == 'partial function':
            result = self._partial_evaluate(context)
        elif self.body is not None:
            assert self.label == 'inline function'
            result = self.body.evaluate(context)
        else:
            result = self.evaluate(context)

        if isinstance(result, XPathToken) and result.symbol == '?':
            pass
        elif not self.parser.match_sequence_type(result, self.sequence_types[-1]):
            result = self.cast_to_primitive_type(result, self.sequence_types[-1])
            if not self.parser.match_sequence_type(result, self.sequence_types[-1]):
                msg = "{!r} does not match sequence type {}"
                self.parser.match_sequence_type(result, self.sequence_types[-1])
                raise self.error('XPTY0004', msg.format(result, self.sequence_types[-1]))

        return result

    @property
    def source(self) -> str:
        if self.label == 'function test':
            if len(self.sequence_types) == 1 and self.sequence_types[0] == '*':
                return 'function(*)'
            else:
                return 'function(%s) as %s' % (
                    ', '.join(self.sequence_types[:-1]), self.sequence_types[-1]
                )
        elif self.label in ('sequence type', 'kind test', ''):
            return '%s(%s)%s' % (
                self.symbol, ', '.join(item.source for item in self), self.occurrence or ''
            )
        return '%s(%s)' % (self.symbol, ', '.join(item.source for item in self))

    @property
    def name(self) -> Optional[QName]:
        if self._name is not None:
            return self._name
        elif self.symbol == 'function':
            return None
        elif self.label == 'partial function':
            return None
        elif not self.namespace or self.namespace == XPATH_FUNCTIONS_NAMESPACE:
            self._name = QName(XPATH_FUNCTIONS_NAMESPACE, 'fn:%s' % self.symbol)
        elif self.namespace == XSD_NAMESPACE:
            self._name = QName(XSD_NAMESPACE, 'xs:%s' % self.symbol)
        elif self.namespace == XPATH_MATH_FUNCTIONS_NAMESPACE:
            self._name = QName(XPATH_MATH_FUNCTIONS_NAMESPACE, 'math:%s' % self.symbol)
        else:
            for pfx, uri in self.parser.namespaces.items():
                if uri == self.namespace:
                    self._name = QName(uri, f'{pfx}:{self.symbol}')
                    break
            else:
                self._name = QName(self.namespace, self.symbol)

        return self._name

    @property
    def arity(self) -> int:
        if isinstance(self.nargs, int):
            return self.nargs
        return len(self._items)

    def nud(self) -> 'XPathFunction':
        code = 'XPST0017' if self.label == 'function' else 'XPST0003'
        self.value = None
        self.parser.advance('(')
        if self.nargs is None:
            del self._items[:]
            if self.parser.next_token.symbol in (')', '(end)'):
                raise self.error(code, 'at least an argument is required')
            while True:
                self.append(self.parser.expression(5))
                if self.parser.next_token.symbol != ',':
                    break
                self.parser.advance()
        elif self.nargs == 0:
            if self.parser.next_token.symbol != ')':
                if self.parser.next_token.symbol != '(end)':
                    raise self.error(code, '%s has no arguments' % str(self))
                raise self.parser.next_token.wrong_syntax()
            self.parser.advance()
            return self
        else:
            if isinstance(self.nargs, (tuple, list)):
                min_args, max_args = self.nargs
            else:
                min_args = max_args = self.nargs

            k = 0
            while k < min_args:
                if self.parser.next_token.symbol in (')', '(end)'):
                    msg = 'Too few arguments: expected at least %s arguments' % min_args
                    raise self.wrong_nargs(msg if min_args > 1 else msg[:-1])

                self._items[k:] = self.parser.expression(5),
                k += 1
                if k < min_args:
                    if self.parser.next_token.symbol == ')':
                        msg = 'Too few arguments: expected at least %s arguments' % min_args
                        raise self.error(code, msg if min_args > 1 else msg[:-1])
                    self.parser.advance(',')

            while max_args is None or k < max_args:
                if self.parser.next_token.symbol == ',':
                    self.parser.advance(',')
                    self._items[k:] = self.parser.expression(5),
                elif k == 0 and self.parser.next_token.symbol != ')':
                    self._items[k:] = self.parser.expression(5),
                else:
                    break  # pragma: no cover
                k += 1

            if self.parser.next_token.symbol == ',':
                msg = 'Too many arguments: expected at most %s arguments' % max_args
                raise self.error(code, msg if max_args != 1 else msg[:-1])

        self.parser.advance(')')
        if any(tk.symbol == '?' for tk in self._items):
            self._partial_function()

        return self

    def match_function_test(self, function_test: str, as_argument: bool = False) -> bool:
        """
        Match if function signature is a subtype of provided *function_test*.
        For default return type is covariant and arguments are contravariant.
        If *as_argument* is `True` the match is inverted and also the return
        type is considered contravariant.

        References:
          https://www.w3.org/TR/xpath-31/#id-function-test
          https://www.w3.org/TR/xpath-31/#id-sequencetype-subtype
        """
        if not function_test.startswith('function('):
            return False
        elif function_test == 'function(*)':
            return True

        parts = function_test[9:].partition(') as ')
        if not parts[1] or not parts[2]:
            return False

        sequence_types = parts[0].split(', ')
        sequence_types.append(parts[2])

        signature = [x for x in self.sequence_types[:self.arity]]
        signature.append(self.sequence_types[-1])

        if len(sequence_types) != len(signature):
            return False

        if as_argument:
            iterator = zip(sequence_types, signature)
        else:
            iterator = zip(signature, sequence_types)

        k = 0
        for fst, st in iterator:
            k += 1
            if not as_argument and k == len(sequence_types):
                st, fst = fst, st

            if st[-1] in '*+?':
                st_occurs = st[-1]
                st = st[:-1]
            else:
                st_occurs = ''

            if fst[-1] in '*+?':
                fst_occurs = fst[-1]
                fst = fst[:-1]
            else:
                fst_occurs = ''

            if st_occurs == fst_occurs or fst_occurs == '*':
                pass
            elif not fst_occurs:
                if st_occurs not in '?*':
                    return False
            elif fst_occurs == '+':
                if st_occurs:
                    return False
            elif st_occurs:
                return False

            if st == fst:
                continue
            elif fst == 'item()':
                continue
            elif st == 'item()':
                return False
            elif fst.startswith('xs:') ^ st.startswith('xs:'):
                return False
            elif fst.startswith('xs:'):
                if not issubclass(xsd11_atomic_types[st[3:]],
                                  xsd11_atomic_types[fst[3:]]):
                    return False
            elif fst != 'node()':
                return False

        return True

    def _partial_function(self) -> None:
        """Convert a named function to an anonymous partial function."""
        def evaluate(context: Optional[XPathContext] = None) -> Any:
            return self

        def select(context: Optional[XPathContext] = None) -> Any:
            yield self

        if self.__class__.evaluate is not XPathToken.evaluate:
            setattr(self, '_partial_evaluate', self.evaluate)
        if self.__class__.select is not XPathToken.select:
            setattr(self, '_partial_select', self.select)

        setattr(self, 'evaluate', evaluate)
        setattr(self, 'select', select)

        self._name = None
        self.label = 'partial function'
        self.nargs = len([tk for tk in self._items if tk.symbol == '?'])

    def _partial_evaluate(self, context: Optional[XPathContext] = None) -> Any:
        return [x for x in self._partial_select(context)]

    def _partial_select(self, context: Optional[XPathContext] = None) -> Iterator[Any]:
        item = self._partial_evaluate(context)
        if item is not None:
            if isinstance(item, list):
                yield from item
            else:
                if context is not None:
                    context.item = item
                yield item
elementpath/helpers.py
OCCURRENCE_INDICATORS = frozenset(('?', '*', '+'))
elementpath/datatypes/uri.py
class AnyURI(AnyAtomicType):
    """
    Class for xs:anyURI data.

    :param value: a string or an untyped atomic instance.
    """
    value: str
    name = 'anyURI'

    def __init__(self, value: Union[str, bytes, UntypedAtomic, 'AnyURI']) -> None:
        if isinstance(value, str):
            self.value = collapse_white_spaces(value)
        elif isinstance(value, bytes):
            self.value = collapse_white_spaces(value.decode('utf-8'))
        elif isinstance(value, self.__class__):
            self.value = value.value
        elif isinstance(value, UntypedAtomic):
            self.value = collapse_white_spaces(value.value)
        else:
            raise TypeError('the argument has an invalid type %r' % type(value))

        self.validate(self.value)

    def __repr__(self) -> str:
        return '%s(%r)' % (self.__class__.__name__, self.value)

    def __str__(self) -> str:
        return self.value

    def __bool__(self) -> bool:
        return bool(self.value)  # For effective boolean value

    def __hash__(self) -> int:
        return hash(self.value)

    def __contains__(self, item: str) -> bool:
        return item in self.value

    def __eq__(self, other: object) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value == other.value
        elif isinstance(other, (bool, float, Decimal, Integer)):
            raise TypeError("cannot compare {} with xs:{}".format(type(other), self.name))
        return self.value == other

    def __ne__(self, other: object) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value != other.value
        elif isinstance(other, (bool, float, Decimal, Integer)):
            raise TypeError("cannot compare {} with xs:{}".format(type(other), self.name))
        return self.value != other

    def __lt__(self, other: Union[str, 'AnyURI', UntypedAtomic]) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value < other.value
        return self.value < other

    def __le__(self, other: Union[str, 'AnyURI', UntypedAtomic]) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value <= other.value
        return self.value <= other

    def __gt__(self, other: Union[str, 'AnyURI', UntypedAtomic]) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value > other.value
        return self.value > other

    def __ge__(self, other: Union[str, 'AnyURI', UntypedAtomic]) -> bool:
        if isinstance(other, (AnyURI, UntypedAtomic)):
            return self.value >= other.value
        return self.value >= other

    @classmethod
    def validate(cls, value: object) -> None:
        if isinstance(value, cls):
            return
        elif isinstance(value, bytes):
            value = value.decode()
        elif not isinstance(value, str):
            raise cls.invalid_type(value)

        try:
            url_parts = urlparse(value)
            _ = url_parts.port  # check invalid port!
        except ValueError as err:
            msg = 'invalid value {!r} for xs:{} ({})'
            raise ValueError(msg.format(value, cls.name, str(err))) from None
        else:
            if url_parts.path.startswith(':'):
                raise cls.invalid_value(value)
            elif value.count('#') > 1:
                msg = 'invalid value {!r} for xs:{} (too many # characters)'
                raise ValueError(msg.format(value, cls.name))
            elif WRONG_ESCAPE_PATTERN.search(value) is not None:
                msg = 'invalid value {!r} for xs:{} (wrong escaping)'
                raise ValueError(msg.format(value, cls.name))
elementpath/namespaces.py
XSD_ANY_ATOMIC_TYPE = '{%s}anyAtomicType' % XSD_NAMESPACE
elementpath/xpath2/xpath2_parser.py
class XPath2Parser(XPath1Parser):
    """
    XPath 2.0 expression parser class. This is the default parser used by XPath selectors.
    A parser instance represents also the XPath static context. With *variable_types* you
    can pass a dictionary with the types of the in-scope variables.
    Provide a *namespaces* dictionary argument for mapping namespace prefixes to URI inside
    expressions. If *strict* is set to `False` the parser enables also the parsing of QNames,
    like the ElementPath library. There are some additional XPath 2.0 related arguments.

    :param namespaces: a dictionary with mapping from namespace prefixes into URIs.
    :param variable_types: a dictionary with the static context's in-scope variable \
    types. It defines the associations between variables and static types.
    :param strict: if strict mode is `False` the parser enables parsing of QNames, \
    like the ElementPath library. Default is `True`.
    :param compatibility_mode: if set to `True` the parser instance works with \
    XPath 1.0 compatibility rules.
    :param default_namespace: the default namespace to apply to unprefixed names. \
    For default no namespace is applied (empty namespace '').
    :param function_namespace: the default namespace to apply to unprefixed function \
    names. For default the namespace "http://www.w3.org/2005/xpath-functions" is used.
    :param schema: the schema proxy class or instance to use for types, attributes and \
    elements lookups. If an `AbstractSchemaProxy` subclass is provided then a schema \
    proxy instance is built without the optional argument, that involves a mapping of \
    only XSD builtin types. If it's not provided the XPath 2.0 schema's related \
    expressions cannot be used.
    :param base_uri: an absolute URI maybe provided, used when necessary in the \
    resolution of relative URIs.
    :param default_collation: the default string collation to use. If not set the \
    environment's default locale setting is used.
    :param document_types: statically known documents, that is a dictionary from \
    absolute URIs onto types. Used for type check when calling the *fn:doc* function \
    with a sequence of URIs. The default type of a document is 'document-node()'.
    :param collection_types: statically known collections, that is a dictionary from \
    absolute URIs onto types. Used for type check when calling the *fn:collection* \
    function with a sequence of URIs. The default type of a collection is 'node()*'.
    :param default_collection_type: this is the type of the sequence of nodes that \
    would result from calling the *fn:collection* function with no arguments. \
    Default is 'node()*'.
    """
    version = '2.0'

    SYMBOLS: ClassVar[FrozenSet[str]] = XPath1Parser.SYMBOLS | {
        'union', 'intersect', 'instance', 'castable', 'if', 'then', 'else', 'for', 'to',
        'some', 'every', 'in', 'satisfies', 'item', 'satisfies', 'cast', 'treat',
        'return', 'except', '?', 'as', 'of',

        # Comments
        '(:', ':)',

        # Value comparison operators
        'eq', 'ne', 'lt', 'le', 'gt', 'ge',

        # Node comparison operators
        'is', '<<', '>>',

        # Mathematical operators
        'idiv',

        # Node type functions
        'document-node', 'schema-attribute', 'element', 'schema-element',
        'attribute', 'empty-sequence',

        # Accessor functions
        'node-name', 'nilled', 'data', 'base-uri', 'document-uri',

        # Number functions
        'abs', 'round-half-to-even',

        # Aggregate functions
        'avg', 'min', 'max',

        # String functions
        'codepoints-to-string', 'string-to-codepoints', 'compare', 'codepoint-equal',
        'string-join', 'normalize-unicode', 'upper-case', 'lower-case', 'encode-for-uri',
        'iri-to-uri', 'escape-html-uri', 'ends-with',

        # General functions for sequences
        'distinct-values', 'empty', 'exists', 'index-of', 'insert-before', 'remove',
        'reverse', 'subsequence', 'unordered',

        # Cardinality functions for sequences
        'zero-or-one', 'one-or-more', 'exactly-one',

        # Comparing function for sequences
        'deep-equal',

        # Pattern matching functions
        'matches', 'replace', 'tokenize',

        # Functions on anyURI
        'resolve-uri',

        # Functions for extracting fragments from xs:duration
        'years-from-duration', 'months-from-duration', 'days-from-duration',
        'hours-from-duration', 'minutes-from-duration', 'seconds-from-duration',

        # Functions for extracting fragments from xs:dateTime
        'year-from-dateTime', 'month-from-dateTime', 'day-from-dateTime', 'hours-from-dateTime',
        'minutes-from-dateTime', 'seconds-from-dateTime', 'timezone-from-dateTime',

        # Functions for extracting fragments from xs:date
        'year-from-date', 'month-from-date', 'day-from-date', 'timezone-from-date',

        # Functions for extracting fragments from xs:time
        'hours-from-time', 'minutes-from-time', 'seconds-from-time', 'timezone-from-time',

        # Timezone adjustment functions
        'adjust-dateTime-to-timezone', 'adjust-date-to-timezone', 'adjust-time-to-timezone',

        # Functions Related to QNames (QName function is also a constructor)
        'QName', 'local-name-from-QName', 'prefix-from-QName', 'local-name-from-QName',
        'namespace-uri-from-QName', 'namespace-uri-for-prefix', 'in-scope-prefixes',
        'resolve-QName',

        # Static context functions
        'default-collation', 'static-base-uri',

        # Dynamic context functions
        'current-dateTime', 'current-date', 'current-time', 'implicit-timezone',

        # Node set functions
        'root',

        # Error function and trace function
        'error', 'trace',

        # XSD builtins constructors ('string', 'boolean' and 'QName' are
        # already registered as functions)
        'normalizedString', 'token', 'language', 'Name', 'NCName', 'ENTITY', 'ID',
        'IDREF', 'NMTOKEN', 'anyURI', 'NOTATION', 'decimal', 'int', 'integer', 'long',
        'short', 'byte', 'double', 'float', 'nonNegativeInteger', 'positiveInteger',
        'nonPositiveInteger', 'negativeInteger', 'unsignedLong', 'unsignedInt',
        'unsignedShort', 'unsignedByte', 'dateTime', 'date', 'time', 'gDay', 'gMonth',
        'gYear', 'gMonthDay', 'gYearMonth', 'duration', 'dayTimeDuration',
        'yearMonthDuration', 'dateTimeStamp', 'base64Binary', 'hexBinary', 'untypedAtomic',

        # Functions and Operators that Generate Sequences ('id' changes but
        # is already registered)
        'element-with-id', 'idref', 'doc', 'doc-available', 'collection',
    }

    DEFAULT_NAMESPACES: ClassVar[Dict[str, str]] = {
        'xml': XML_NAMESPACE,
        'xs': XSD_NAMESPACE,
        # 'xlink': XLINK_NAMESPACE,
        'fn': XPATH_FUNCTIONS_NAMESPACE,
        'err': XQT_ERRORS_NAMESPACE
    }

    PATH_STEP_LABELS = ('axis', 'function', 'kind test')
    PATH_STEP_SYMBOLS = {
        '(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', '..', '.', '(', '{'
    }

    # https://www.w3.org/TR/xpath20/#id-reserved-fn-names
    RESERVED_FUNCTION_NAMES = {
        'attribute', 'comment', 'document-node', 'element', 'empty-sequence',
        'if', 'item', 'node', 'processing-instruction', 'schema-attribute',
        'schema-element', 'text', 'typeswitch',
    }

    function_signatures: Dict[Tuple[QName, int], str] = XPath1Parser.function_signatures.copy()
    namespaces: Dict[str, str]
    token: XPathToken
    next_token: XPathToken

    def __init__(self, namespaces: Optional[NamespacesType] = None,
                 variable_types: Optional[Dict[str, str]] = None,
                 strict: bool = True,
                 compatibility_mode: bool = False,
                 default_collation: Optional[str] = None,
                 default_namespace: Optional[str] = None,
                 function_namespace: Optional[str] = None,
                 xsd_version: Optional[str] = None,
                 schema: Optional[AbstractSchemaProxy] = None,
                 base_uri: Optional[str] = None,
                 document_types: Optional[Dict[str, str]] = None,
                 collection_types: Optional[Dict[str, str]] = None,
                 default_collection_type: str = 'node()*') -> None:

        super(XPath2Parser, self).__init__(namespaces, strict)
        self._compatibility_mode = compatibility_mode
        self._default_collation = default_collation
        self._xsd_version = xsd_version if xsd_version is not None else '1.0'

        if default_namespace is not None:
            self.namespaces[''] = default_namespace

        if function_namespace is not None:
            self.function_namespace = function_namespace

        if schema is None:
            pass
        elif not isinstance(schema, AbstractSchemaProxy):
            msg = "argument 'schema' must be an instance of AbstractSchemaProxy"
            raise ElementPathTypeError(msg)
        else:
            schema.bind_parser(self)

        if not variable_types:
            self.variable_types = {}
        elif all(self.is_sequence_type(v) for v in variable_types.values()):
            self.variable_types = {
                k: normalize_sequence_type(v) for k, v in variable_types.items()
            }
        else:
            raise ElementPathValueError('invalid sequence type for in-scope variable types')

        self.base_uri = None if base_uri is None else urlparse(base_uri).geturl()

        if document_types:
            if any(not self.is_sequence_type(v) for v in document_types.values()):
                raise ElementPathValueError('invalid sequence type in document_types argument')
        self.document_types = document_types

        if collection_types:
            if any(not self.is_sequence_type(v) for v in collection_types.values()):
                raise ElementPathValueError('invalid sequence type in collection_types argument')
        self.collection_types = collection_types

        if not self.is_sequence_type(default_collection_type):
            raise ElementPathValueError('invalid sequence type for '
                                        'default_collection_type argument')
        self.default_collection_type = default_collection_type

    def __getstate__(self) -> Dict[str, Any]:
        state = self.__dict__.copy()
        state.pop('symbol_table', None)
        state.pop('tokenizer', None)
        return state

    @property
    def compatibility_mode(self) -> bool:
        return self._compatibility_mode

    @compatibility_mode.setter
    def compatibility_mode(self, value: bool) -> None:
        self._compatibility_mode = value

    @property
    def default_collation(self) -> str:
        if self._default_collation is not None:
            return self._default_collation

        language_code, encoding = locale.getdefaultlocale()

        if language_code is None:
            return UNICODE_CODEPOINT_COLLATION
        elif encoding is None or not encoding:
            return language_code
        else:
            collation = f'{language_code}.{encoding}'
            if collation != 'en_US.UTF-8':
                return collation
            else:
                return UNICODE_CODEPOINT_COLLATION

    @property
    def default_namespace(self) -> Optional[str]:
        return self.namespaces.get('')

    @property
    def xsd_version(self) -> str:
        if self.schema is None:
            return self._xsd_version

        try:
            return self.schema.xsd_version
        except (AttributeError, NotImplementedError):
            return self._xsd_version

    def advance(self, *symbols: str) -> XPathToken:
        super(XPath2Parser, self).advance(*symbols)

        if self.next_token.symbol == '(:':
            try:
                self.token.unexpected(':')
            except AttributeError:
                pass

            # Parses and consumes an XPath 2.0 comment. A comment is
            # delimited by symbols '(:' and ':)' and can be nested.
            comment_level = 1
            while comment_level:
                self.advance_until('(:', ':)')
                if self.next_token.symbol == ':)':
                    comment_level -= 1
                else:
                    comment_level += 1
            self.advance(':)')
            self.next_token.unexpected(':')

        return self.next_token

    @classmethod
    def constructor(cls, symbol: str, bp: int = 0, nargs: NargsType = 1,
                    sequence_types: Union[Tuple[()], Tuple[str, ...], List[str]] = (),
                    label: Union[str, Tuple[str, ...]] = 'constructor function') \
            -> Callable[[Callable[..., Any]], Callable[..., Any]]:
        """Creates a constructor token class."""
        def nud_(self: XPathConstructor) -> XPathConstructor:
            try:
                self.parser.advance('(')
                self[0:] = self.parser.expression(5),
                if self.parser.next_token.symbol == ',':
                    raise self.wrong_nargs('Too many arguments: expected at most 1 argument')
                self.parser.advance(')')
                self.value = None
            except SyntaxError:
                raise self.error('XPST0017') from None

            if self[0].symbol == '?':
                self._partial_function()
            return self

        def evaluate_(self: XPathConstructor, context: Optional[XPathContext] = None) \
                -> Union[List[None], AtomicValueType]:
            arg = self.data_value(self.get_argument(context))
            if arg is None:
                return []
            elif arg == '?' and self[0].symbol == '?':
                raise self.error('XPTY0004', "cannot evaluate a partial function")

            try:
                if isinstance(arg, UntypedAtomic):
                    return self.cast(arg.value)
                return self.cast(arg)
            except ElementPathError:
                raise
            except (TypeError, ValueError) as err:
                raise self.error('FORG0001', err) from None

        if not sequence_types:
            assert nargs == 1
            sequence_types = ('xs:anyAtomicType?', 'xs:%s?' % symbol)

        token_class = cls.register(symbol, nargs=nargs, sequence_types=sequence_types,
                                   label=label, bases=(XPathConstructor,), lbp=bp, rbp=bp,
                                   nud=nud_, evaluate=evaluate_)

        def bind(func: Callable[..., Any]) -> Callable[..., Any]:
            method_name = func.__name__.partition('_')[0]
            if method_name != 'cast':
                raise ValueError("The function name must be 'cast' or starts with 'cast_'")
            setattr(token_class, method_name, func)
            return func
        return bind

    def schema_constructor(self, atomic_type_name: str, bp: int = 90) \
            -> Type[XPathFunction]:
        """Registers a token class for a schema atomic type constructor function."""
        if atomic_type_name in (XSD_ANY_ATOMIC_TYPE, XSD_NOTATION):
            raise xpath_error('XPST0080')

        def nud_(self_: XPathFunction) -> XPathFunction:
            self_.parser.advance('(')
            self_[0:] = self_.parser.expression(5),
            self_.parser.advance(')')

            try:
                self_.value = self_.evaluate()  # Static context evaluation
            except MissingContextError:
                self_.value = None
            return self_

        def evaluate_(self_: XPathFunction, context: Optional[XPathContext] = None) \
                -> Union[List[None], AtomicValueType]:
            arg = self_.get_argument(context)
            if arg is None or self_.parser.schema is None:
                return []

            value = self_.string_value(arg)
            try:
                return self_.parser.schema.cast_as(value, atomic_type_name)
            except (TypeError, ValueError) as err:
                raise self_.error('FORG0001', err)

        symbol = get_prefixed_name(atomic_type_name, self.namespaces)
        token_class_name = "_%sConstructorFunction" % symbol.replace(':', '_')
        kwargs = {
            'symbol': symbol,
            'nargs': 1,
            'label': 'constructor function',
            'pattern': r'\b%s(?=\s*\(|\s*\(\:.*\:\)\()' % symbol,
            'lbp': bp,
            'rbp': bp,
            'nud': nud_,
            'evaluate': evaluate_,
            '__module__': self.__module__,
            '__qualname__': token_class_name,
            '__return__': None
        }
        token_class = cast(
            Type[XPathFunction], ABCMeta(token_class_name, (XPathFunction,), kwargs)
        )
        MutableSequence.register(token_class)
        self.symbol_table[symbol] = token_class
        return token_class

    def is_schema_bound(self) -> bool:
        return 'symbol_table' in self.__dict__

    def parse(self, source: str) -> XPathToken:
        root_token = super(XPath1Parser, self).parse(source)
        if root_token.label in ('sequence type', 'function test'):
            raise root_token.error('XPST0003', "not allowed in XPath expression")

        if self.schema is None:
            try:
                root_token.evaluate()  # Static context evaluation
            except MissingContextError:
                pass
        else:
            # Static context evaluation with a dynamic schema context
            context = self.schema.get_context()
            for _ in root_token.select(context):
                pass

        return root_token

    def check_variables(self, values: MutableMapping[str, Any]) -> None:
        if self.variable_types is None:
            return

        for varname, xsd_type in self.variable_types.items():
            if varname not in values:
                raise xpath_error('XPST0008', "missing variable {!r}".format(varname))

        for varname, value in values.items():
            try:
                sequence_type = self.variable_types[varname]
            except KeyError:
                sequence_type = 'item()*' if isinstance(value, list) else 'item()'

            if not self.match_sequence_type(value, sequence_type):
                message = "Unmatched sequence type for variable {!r}".format(varname)
                raise xpath_error('XPDY0050', message)
elementpath/datatypes/qname.py
class QName(AbstractQName):
    name = 'QName'
elementpath/datatypes/numeric.py
class Integer(int, metaclass=AtomicTypeMeta):
    """A wrapper for emulating xs:integer and limited integer types."""
    name = 'integer'
    pattern = re.compile(r'^[\-+]?[0-9]+$')
    lower_bound: Optional[int] = None
    higher_bound: Optional[int] = None

    def __init__(self, value: Union[str, SupportsInt]) -> None:
        if self.lower_bound is not None and self < self.lower_bound:
            raise ValueError("value {} is too low for {!r}".format(value, self.__class__))
        elif self.higher_bound is not None and self >= self.higher_bound:
            raise ValueError("value {} is too high for {!r}".format(value, self.__class__))
        super(Integer, self).__init__()

    @classmethod
    def __subclasshook__(cls, subclass: Type[Any]) -> bool:
        if cls is Integer:
            return issubclass(subclass, int) and not issubclass(subclass, bool)
        return NotImplemented

    @classmethod
    def validate(cls, value: object) -> None:
        if isinstance(value, cls):
            return
        elif isinstance(value, str):
            if cls.pattern.match(value) is None:
                raise cls.invalid_value(value)
        else:
            raise cls.invalid_type(value)
elementpath/exceptions.py
class ElementPathError(Exception):
    """
    Base exception class for elementpath package.

    :param message: the message related to the error.
    :param code: an optional error code.
    :param token: an optional token instance related with the error.
    """
    def __init__(self, message: str,
                 code: Optional[str] = None,
                 token: Optional[Token[Any]] = None) -> None:
        super(ElementPathError, self).__init__(message)
        self.message = message
        self.code = code
        self.token = token

    def __str__(self) -> str:
        if self.token is None or not isinstance(self.token.value, (str, bytes)):
            if not self.code:
                return self.message
            return '[{}] {}'.format(self.code, self.message)
        elif not self.code:
            return '{1} at line {2}, column {3}: {0}'.format(
                self.message, self.token, *self.token.position
            )
        return '{2} at line {3}, column {4}: [{1}] {0}'.format(
            self.message, self.code, self.token, *self.token.position
        )
elementpath/helpers.py
def numeric_equal(op1: SupportsFloat, op2: SupportsFloat) -> bool:
    if op1 == op2:
        return True
    return math.isclose(op1, op2, rel_tol=1e-7, abs_tol=0.0)
elementpath/helpers.py
def numeric_not_equal(op1: SupportsFloat, op2: SupportsFloat) -> bool:
    if op1 == op2:
        return False
    return not math.isclose(op1, op2, rel_tol=1e-7, abs_tol=0.0)
elementpath/xpath_context.py
class XPathSchemaContext(XPathContext):
    """
    The XPath dynamic context base class for schema bounded parsers. Use this class
    as dynamic context for schema instances in order to perform a schema-based type
    checking during the static analysis phase. Don't use this as dynamic context on
    XML instances.
    """
    iter_children_or_self: Callable[..., Iterator[Union[XsdElementProtocol, XMLSchemaProtocol]]]
    root: XMLSchemaProtocol
elementpath/xpath_nodes.py
class TypedElement(XPathNode):
    """
    A class for processing typed element nodes.

    :param elem: the linked element. Can be an Element, or an XSD element \
    when XPath is applied on a schema.
    :param xsd_type: the reference XSD type.
    :param value: the decoded value. Can be `None` for empty or element-only elements."
    """
    def __init__(self, elem: ElementProtocol, xsd_type: Any, value: Any) -> None:
        self.elem = elem
        self.xsd_type = xsd_type
        self.value = value

    @property
    def kind(self) -> str:
        return 'element'

    @property
    def name(self) -> str:
        return self.elem.tag

    @property
    def tag(self) -> str:
        return self.elem.tag

    def __repr__(self) -> str:
        return '%s(tag=%r)' % (self.__class__.__name__, self.elem.tag)

    def __eq__(self, other: Any) -> bool:
        return isinstance(other, self.__class__) and \
            self.elem is other.elem and \
            self.value == other.value

    def __hash__(self) -> int:
        return hash((self.elem, self.value))
elementpath/datatypes/untyped.py
class UntypedAtomic(metaclass=AtomicTypeMeta):
    """
    Class for xs:untypedAtomic data. Provides special methods for comparing
    and converting to basic data types.

    :param value: the untyped value, usually a string.
    """
    name = 'untypedAtomic'
    value: str

    @classmethod
    def validate(cls, value: object) -> None:
        if not isinstance(value, cls):
            raise cls.invalid_type(value)

    def __init__(self, value: Union[str, bytes, bool, float, Decimal,
                                    'UntypedAtomic', AnyAtomicType]) -> None:
        if isinstance(value, str):
            self.value = value
        elif isinstance(value, bytes):
            self.value = value.decode('utf-8')
        elif isinstance(value, bool):
            self.value = 'true' if value else 'false'
        elif isinstance(value, float):
            self.value = str(value).rstrip('0').rstrip('.')
        elif isinstance(value, Decimal):
            self.value = str(value.normalize())
        elif isinstance(value, UntypedAtomic):
            self.value = value.value
        elif isinstance(value, AnyAtomicType):
            self.value = str(value)
        else:
            raise TypeError("{!r} is not an atomic value".format(value))

    def __repr__(self) -> str:
        return '%s(%r)' % (self.__class__.__name__, self.value)

    def _get_operands(self, other: Any, force_float: bool = True) -> Tuple[Any, Any]:
        """
        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 force_float: Force a conversion to float if *other* is an UntypedAtomic instance.
        :return: A couple of values.
        """
        if isinstance(other, UntypedAtomic):
            if force_float:
                return float(self.value), float(other.value)
            return self.value, other.value
        elif isinstance(other, bool):
            # Cast to xs:boolean
            value = self.value.strip()
            if value not in BOOLEAN_VALUES:
                raise ValueError("{!r} cannot be cast to xs:boolean".format(self.value))
            return value in ('1', 'true'), other
        elif isinstance(other, int):
            return float(self.value), other
        elif other is None or isinstance(other, (str, list)):
            return self.value, other

        try:
            return type(other).fromstring(self.value), other
        except AttributeError:
            return type(other)(self.value), other

    def __hash__(self) -> int:
        return hash(self.value)

    def __eq__(self, other: Any) -> Any:
        return operator.eq(*self._get_operands(other, force_float=False))

    def __ne__(self, other: Any) -> Any:
        return not operator.eq(*self._get_operands(other, force_float=False))

    def __lt__(self, other: Any) -> Any:
        return operator.lt(*self._get_operands(other))

    def __le__(self, other: Any) -> Any:
        return operator.le(*self._get_operands(other))

    def __gt__(self, other: Any) -> Any:
        return operator.gt(*self._get_operands(other))

    def __ge__(self, other: Any) -> Any:
        return operator.ge(*self._get_operands(other))

    def __add__(self, other: Any) -> Any:
        return operator.add(*self._get_operands(other))
    __radd__ = __add__

    def __sub__(self, other: Any) -> Any:
        return operator.sub(*self._get_operands(other))

    def __rsub__(self, other: Any) -> Any:
        return operator.sub(*reversed(self._get_operands(other)))

    def __mul__(self, other: Any) -> Any:
        return operator.mul(*self._get_operands(other))
    __rmul__ = __mul__

    def __truediv__(self, other: Any) -> Any:
        return operator.truediv(*self._get_operands(other))

    def __rtruediv__(self, other: Any) -> Any:
        return operator.truediv(*reversed(self._get_operands(other)))

    def __int__(self) -> int:
        return int(self.value)

    def __float__(self) -> float:
        return float(self.value)

    def __bool__(self) -> bool:
        return bool(self.value)  # For effective boolean value, not for cast to xs:boolean.

    def __abs__(self) -> Decimal:
        return abs(Decimal(self.value))

    def __mod__(self, other: Any) -> Any:
        return operator.mod(*self._get_operands(other))

    def __str__(self) -> str:
        return self.value

    def __bytes__(self) -> bytes:
        return bytes(self.value, encoding='utf-8')
elementpath/namespaces.py
XSD_NOTATION = '{%s}NOTATION' % XSD_NAMESPACE
elementpath/namespaces.py
XSD_NAMESPACE = "http://www.w3.org/2001/XMLSchema"
elementpath/xpath_nodes.py
def is_document_node(obj: Any) -> bool:
    return hasattr(obj, 'getroot') and hasattr(obj, 'parse') and hasattr(obj, 'iter')
elementpath/xpath_nodes.py
def is_xpath_node(obj: Any) -> bool:
    return isinstance(obj, XPathNode) or \
        hasattr(obj, 'tag') and hasattr(obj, 'attrib') and hasattr(obj, 'text') or \
        hasattr(obj, 'local_name') and hasattr(obj, 'type') and hasattr(obj, 'name') or \
        hasattr(obj, 'getroot') and hasattr(obj, 'parse') and hasattr(obj, 'iter')
elementpath/datatypes/proxies.py
class DoubleProxy10(metaclass=AtomicTypeMeta):
    name = 'double'
    xsd_version = '1.0'
    pattern = re.compile(
        r'^(?:[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?|[+-]?INF|NaN)$'
    )

    def __new__(cls, value: Union[SupportsFloat, str]) -> float:  # type: ignore[misc]
        if isinstance(value, str):
            value = collapse_white_spaces(value)
            if value in NUMERIC_INF_OR_NAN or cls.xsd_version != '1.0' and value == '+INF':
                pass
            elif value.lower() in INVALID_NUMERIC:
                raise ValueError('invalid value {!r} for xs:{}'.format(value, cls.name))
        return float(value)

    @classmethod
    def __subclasshook__(cls, subclass: type) -> bool:
        return issubclass(subclass, float) and not issubclass(subclass, Float10)

    @classmethod
    def validate(cls, value: object) -> None:
        if isinstance(value, float) and not isinstance(value, Float10):
            return
        elif isinstance(value, str):
            if cls.pattern.match(value) is None:
                raise cls.invalid_value(value)
        else:
            raise cls.invalid_type(value)
elementpath/namespaces.py
XSI_NIL = '{%s}nil' % XSI_NAMESPACE
elementpath/xpath_nodes.py
def match_attribute_node(obj: Any, name: Optional[str] = None) -> bool:
    """
    Returns `True` if the first argument is an attribute node matching the name, `False` otherwise.
    Raises a ValueError if the argument name has to be used, but it's in a wrong format.

    :param obj: the node to be tested.
    :param name: a fully qualified name, a local name or a wildcard. The accepted wildcard formats \
    are '*', '*:*', '*:local-name' and '{namespace}*'.
    """
    if name is None or name == '*' or name == '*:*':
        return isinstance(obj, (AttributeNode, TypedAttribute))
    elif not isinstance(obj, (AttributeNode, TypedAttribute)):
        return False
    elif isinstance(obj, TypedAttribute):
        obj = obj.attribute

    if not name:
        return not obj.name
    elif name[0] == '*':
        try:
            _, _name = name.split(':')
        except (ValueError, IndexError):
            raise ElementPathValueError("unexpected format %r for argument 'name'" % name)
        else:
            if obj.name.startswith('{'):
                return obj.name.split('}')[1] == _name
            else:
                return obj.name == _name

    elif name[-1] == '*':
        if name[0] != '{' or '}' not in name:
            raise ElementPathValueError("unexpected format %r for argument 'name'" % name)
        elif obj.name.startswith('{'):
            return obj.name.split('}')[0][1:] == name.split('}')[0][1:]
        else:
            return False
    else:
        return obj.name == name
elementpath/namespaces.py
def get_expanded_name(
        qname: str, namespaces: Union[Dict[str, str], Dict[Optional[str], str]]) -> str:
    """
    Get the expanded form of a QName, using a namespace map.
    Local names are mapped to the default namespace.

    :param qname: a prefixed QName or a local name or an extended QName.
    :param namespaces: a dictionary with a map from prefixes to namespace URIs.
    :return: the expanded format of a QName or a local name.
    """
    try:
        if qname[0] == '{' or qname[1] == '{' and qname[0] == 'Q':
            return qname
    except IndexError:
        return qname

    try:
        prefix, local_name = qname.split(':')
    except ValueError:
        if ':' in qname:
            raise ValueError("wrong format for prefixed QName %r" % qname)
        elif '' in namespaces:
            uri = namespaces['']
        elif None in namespaces:
            uri = cast(Dict[Optional[str], str], namespaces)[None]  # lxml nsmap
        else:
            return qname

        return '{%s}%s' % (uri, qname) if uri else qname
    else:
        if not prefix or not local_name:
            raise ValueError("wrong format for reference name %r" % qname)
        uri = namespaces[prefix]
        return '{%s}%s' % (uri, local_name) if uri else local_name
elementpath/namespaces.py
def get_namespace(name: str) -> str:
    try:
        return NAMESPACE_URI_PATTERN.match(name).group(1)  # type: ignore[union-attr]
    except AttributeError:
        return ''
elementpath/exceptions.py
class ElementPathTypeError(ElementPathError, TypeError):
    pass
elementpath/xpath_nodes.py
def is_element_node(obj: Any) -> bool:
    return isinstance(obj, TypedElement) or \
        hasattr(obj, 'tag') and not callable(obj.tag) and \
        hasattr(obj, 'attrib') and hasattr(obj, 'text')


Question: import math
import operator
from copy import copy
from decimal import Decimal, DivisionByZero
from ..exceptions import ElementPathError, ElementPathTypeError
from ..helpers import OCCURRENCE_INDICATORS, numeric_equal, numeric_not_equal
from ..namespaces import XSD_NAMESPACE, XSD_NOTATION, XSD_ANY_ATOMIC_TYPE, \
    XSI_NIL, get_namespace, get_expanded_name
from ..datatypes import UntypedAtomic, QName, AnyURI, Duration, Integer, DoubleProxy10
from ..xpath_nodes import TypedElement, is_xpath_node, \
    match_attribute_node, is_element_node, is_document_node
from ..xpath_context import XPathSchemaContext
from ..xpath_token import XPathFunction
from .xpath2_parser import XPath2Parser
            token_class = self.parser.symbol_table.get(local_name)
            if token_class is None or token_class.label != 'constructor function':
                msg = "atomic type %r not found in the in-scope schema types"
                raise self.unknown_atomic_type(msg % self[1].source)
            elif local_name == 'QName':
                if isinstance(arg, QName):
                    pass
                elif self.parser.version < '3.0' and self[0].symbol != '(string)':
                    raise self.error('XPTY0004', "Non literal string to QName cast")

            token = token_class(self.parser)
            value = token.cast(arg)

    except ElementPathError:
        if self.symbol != 'cast':
            return False
        raise
    except (TypeError, ValueError) as err:
        if self.symbol != 'cast':
            return False
        elif isinstance(arg, (UntypedAtomic, str)):
            raise self.error('FORG0001', err) from None
        raise self.error('XPTY0004', err) from None
    else:
        return value if self.symbol == 'cast' else True


###
# Comma operator - concatenate items or sequences
@method(infix(',', bp=5))
def evaluate_comma_operator(self, context=None):
    results = []
    for op in self:
        result = op.evaluate(context)
        if isinstance(result, list):
            results.extend(result)
        elif result is not None:
            results.append(result)
    return results


@method(',')
def select_comma_operator(self, context=None):
    for op in self:
        yield from op.select(context=copy(context))


###
# Parenthesized expression: XPath 2.0 admits the empty case ().
@method(register('(', lbp=80, rpb=80, label='expression'))
def nud_parenthesized_expression(self):
    if self.parser.next_token.symbol != ')':
        self[:] = self.parser.expression(),
    self.parser.advance(')')
    return self


@method('(')
def led_parenthesized_expression(self, left):
    if left.symbol == '(name)':
        if left.value in self.parser.RESERVED_FUNCTION_NAMES:
            msg = f"{left.value!r} is not allowed as function name"
            raise left.error('XPST0003', msg)
        else:
            raise left.error('XPST0017', 'unknown function {!r}'.format(left.value))

    elif left.symbol == ':' and left[1].symbol == '(name)':
        if left[1].namespace == XSD_NAMESPACE:
            msg = 'unknown constructor function {!r}'.format(left[1].value)
            raise left[1].error('XPST0017', msg)
        raise left.error('XPST0017', 'unknown function {!r}'.format(left.value))

    if self.parser.next_token.symbol != ')':
        self[:] = left, self.parser.expression()
    else:
        self[:] = left,
    self.parser.advance(')')
    return self


@method('(')
def evaluate_parenthesized_expression(self, context=None):
    return self[0].evaluate(context) if self else []


@method('(')
def select_parenthesized_expression(self, context=None):
    return self[0].select(context) if self else iter(())


###
# Value comparison operators (eq, ne, lt, le, gt, and ge)
#
# Ref: https://www.w3.org/TR/xpath20/#id-value-comparisons
#
@method('eq', bp=30)
@method('ne', bp=30)
@method('lt', bp=30)
@method('gt', bp=30)
@method('le', bp=30)
@method('ge', bp=30)
def led_value_comparison_operators(self, left):
    if left.symbol in COMPARISON_OPERATORS:
        raise self.wrong_syntax()
    self[:] = left, self.parser.expression(rbp=30)
    return self


@method('eq')
@method('ne')
@method('lt')
@method('gt')
@method('le')
@method('ge')
def evaluate_value_comparison_operators(self, context=None):
    operands = [self[0].get_atomized_operand(context=copy(context)),
                self[1].get_atomized_operand(context=copy(context))]

    if any(x is None for x in operands):
        return None

Answer:     elif any(isinstance(x, XPathFunction) for x in operands):