1#
2# Copyright (c), 2018-2021, SISSA (International School for Advanced Studies).
3# All rights reserved.
4# This file is distributed under the terms of the MIT License.
5# See the file 'LICENSE' in the root directory of the present
6# distribution, or http://opensource.org/licenses/MIT.
7#
8# @author Davide Brunato <brunato@sissa.it>
9#
10"""
11XPathToken and helper functions for XPath nodes. XPath error messages and node helper functions
12are embedded in XPathToken class, in order to raise errors related to token instances.
13
14In XPath there are 7 kinds of nodes:
15
16    element, attribute, text, namespace, processing-instruction, comment, document
17
18Element-like objects are used for representing elements and comments, ElementTree-like objects
19for documents.
20XPathNode subclasses are used for representing other node types and typed elements/attributes.
21"""
22import locale
23import contextlib
24import math
25from copy import copy
26from decimal import Decimal
27from itertools import product
28from typing import TYPE_CHECKING, cast, Dict, Optional, List, Tuple, Union, \
29    Any, Iterator, SupportsFloat, Type
30import urllib.parse
31
32from .exceptions import ElementPathError, ElementPathValueError, ElementPathNameError, \
33    ElementPathTypeError, ElementPathSyntaxError, MissingContextError, XPATH_ERROR_CODES
34from .helpers import ordinal
35from .namespaces import XQT_ERRORS_NAMESPACE, XSD_NAMESPACE, XSD_SCHEMA, \
36    XPATH_FUNCTIONS_NAMESPACE, XPATH_MATH_FUNCTIONS_NAMESPACE, \
37    XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE, XSI_NIL
38from .xpath_nodes import XPathNode, TypedElement, AttributeNode, TextNode, \
39    NamespaceNode, TypedAttribute, is_etree_element, etree_iter_strings, \
40    is_comment_node, is_processing_instruction_node, is_element_node, \
41    is_document_node, is_xpath_node, is_schema_node
42from .datatypes import xsd10_atomic_types, xsd11_atomic_types, AbstractDateTime, \
43    AnyURI, UntypedAtomic, Timezone, DateTime10, Date10, DayTimeDuration, Duration, \
44    Integer, DoubleProxy10, DoubleProxy, QName, DatetimeValueType, AtomicValueType, \
45    AnyAtomicType
46from .protocols import ElementProtocol, DocumentProtocol, \
47    XsdAttributeProtocol, XsdTypeProtocol, XMLSchemaProtocol
48from .schema_proxy import AbstractSchemaProxy
49from .tdop import Token, MultiLabel
50from .xpath_context import XPathContext, XPathSchemaContext
51
52if TYPE_CHECKING:
53    from .xpath1 import XPath1Parser
54    from .xpath2 import XPath2Parser
55    from .xpath30 import XPath30Parser
56
57    XPathParserType = Union[XPath1Parser, XPath2Parser, XPath30Parser]
58else:
59    XPathParserType = Any
60
61UNICODE_CODEPOINT_COLLATION = "http://www.w3.org/2005/xpath-functions/collation/codepoint"
62XSD_SPECIAL_TYPES = {XSD_ANY_TYPE, XSD_ANY_SIMPLE_TYPE, XSD_ANY_ATOMIC_TYPE}
63
64# Type annotations aliases
65NargsType = Optional[Union[int, Tuple[int, Optional[int]]]]
66ClassCheckType = Union[Type[Any], Tuple[Type[Any], ...]]
67PrincipalNodeType = Union[ElementProtocol, AttributeNode, TypedAttribute, TypedElement]
68OperandsType = Tuple[Optional[AtomicValueType], Optional[AtomicValueType]]
69SelectResultType = Union[AtomicValueType, ElementProtocol, XsdAttributeProtocol, Tuple[str, str]]
70
71XPathTokenType = Union['XPathToken', 'XPathAxis', 'XPathFunction', 'XPathConstructor']
72
73
74class XPathToken(Token[XPathTokenType]):
75    """Base class for XPath tokens."""
76    parser: XPathParserType
77    xsd_types: Optional[Dict[str, Union[XsdTypeProtocol, List[XsdTypeProtocol]]]]
78    namespace: Optional[str]
79
80    xsd_types = None  # for XPath 2.0+ XML Schema types labeling
81    namespace = None  # for namespace binding of names and wildcards
82
83    def evaluate(self, context: Optional[XPathContext] = None) -> Any:
84        """
85        Evaluate default method for XPath tokens.
86
87        :param context: The XPath dynamic context.
88        """
89        return [x for x in self.select(context)]
90
91    def select(self, context: Optional[XPathContext] = None) -> Iterator[Any]:
92        """
93        Select operator that generates XPath results.
94
95        :param context: The XPath dynamic context.
96        """
97        item = self.evaluate(context)
98        if item is not None:
99            if isinstance(item, list):
100                yield from item
101            else:
102                if context is not None:
103                    context.item = item
104                yield item
105
106    def __str__(self) -> str:
107        symbol, label = self.symbol, self.label
108        if symbol == '$':
109            return '$%s variable reference' % (self[0].value if self._items else '')
110        elif symbol == ',':
111            return 'comma operator' if self.parser.version > '1.0' else 'comma symbol'
112        elif label.endswith('function') or label in ('axis', 'sequence type', 'kind test'):
113            return '%r %s' % (symbol, label)
114        return super(XPathToken, self).__str__()
115
116    @property
117    def source(self) -> str:
118        symbol, label = self.symbol, self.label
119        if label == 'axis':
120            return '%s::%s' % (self.symbol, self[0].source)
121        elif label.endswith('function') or label in ('sequence type', 'kind test'):
122            return '%s(%s)' % (self.symbol, ', '.join(item.source for item in self))
123        elif symbol == ':':
124            return '%s:%s' % (self[0].source, self[1].source)
125        elif symbol == '(':
126            return '()' if not self else '(%s)' % self[0].source
127        elif symbol == '[':
128            return '%s[%s]' % (self[0].source, self[1].source)
129        elif symbol == ',':
130            return '%s, %s' % (self[0].source, self[1].source)
131        elif symbol == '$':
132            return '$%s' % self[0].source
133        elif symbol == '{':
134            return '{%s}%s' % (self[0].value, self[1].value)
135        elif symbol == 'if':
136            return 'if (%s) then %s else %s' % (self[0].source, self[1].source, self[2].source)
137        elif symbol == 'instance':
138            return '%s instance of %s' % (self[0].source, ''.join(t.source for t in self[1:]))
139        elif symbol == 'treat':
140            return '%s treat as %s' % (self[0].source, ''.join(t.source for t in self[1:]))
141        elif symbol == 'for':
142            return 'for %s return %s' % (
143                ', '.join('%s in %s' % (self[k].source, self[k + 1].source)
144                          for k in range(0, len(self) - 1, 2)),
145                self[-1].source
146            )
147        return super(XPathToken, self).source
148
149    @property
150    def child_axis(self) -> bool:
151        """Is `True` if the token apply child axis for default, `False` otherwise."""
152        if self.symbol not in {'*', 'node', 'child', 'text', '(name)', ':',
153                               'document-node', 'element', 'schema-element'}:
154            return False
155        elif self.symbol != ':':
156            return True
157        return not self._items[1].label.endswith('function')
158
159    ###
160    # Tokens tree analysis methods
161    def iter_leaf_elements(self) -> Iterator[str]:
162        """
163        Iterates through the leaf elements of the token tree if there are any,
164        returning QNames in prefixed format. A leaf element is an element
165        positioned at last path step. Does not consider kind tests and wildcards.
166        """
167        if self.symbol in {'(name)', ':'}:
168            yield cast(str, self.value)
169        elif self.symbol in ('//', '/'):
170            if self._items[-1].symbol in {
171                '(name)', '*', ':', '..', '.', '[', 'self', 'child',
172                'parent', 'following-sibling', 'preceding-sibling',
173                'ancestor', 'ancestor-or-self', 'descendant',
174                'descendant-or-self', 'following', 'preceding'
175            }:
176                yield from self._items[-1].iter_leaf_elements()
177
178        elif self.symbol in ('[',):
179            yield from self._items[0].iter_leaf_elements()
180        else:
181            for tk in self._items:
182                yield from tk.iter_leaf_elements()
183
184    ###
185    # Dynamic context methods
186    def get_argument(self, context: Optional[XPathContext],
187                     index: int = 0,
188                     required: bool = False,
189                     default_to_context: bool = False,
190                     default: Optional[AtomicValueType] = None,
191                     cls: Optional[Type[Any]] = None,
192                     promote: Optional[ClassCheckType] = None) -> Any:
193        """
194        Get the argument value of a function of constructor token. A zero length sequence is
195        converted to a `None` value. If the function has no argument returns the context's
196        item if the dynamic context is not `None`.
197
198        :param context: the dynamic context.
199        :param index: an index for select the argument to be got, the first for default.
200        :param required: if set to `True` missing or empty sequence arguments are not allowed.
201        :param default_to_context: if set to `True` then the item of the dynamic context is \
202        returned when the argument is missing.
203        :param default: the default value returned in case the argument is an empty sequence. \
204        If not provided returns `None`.
205        :param cls: if a type is provided performs a type checking on item.
206        :param promote: a class or a tuple of classes that are promoted to `cls` class.
207        """
208        item: Union[None, ElementProtocol, DocumentProtocol, XPathNode, AnyAtomicType]
209
210        try:
211            selector = self._items[index].select
212        except IndexError:
213            if default_to_context:
214                if context is None:
215                    raise self.missing_context() from None
216                item = context.item if context.item is not None else context.root
217            elif required:
218                msg = "missing %s argument" % ordinal(index + 1)
219                raise self.error('XPST0017', msg) from None
220            else:
221                return default
222        else:
223            item = None
224            for k, result in enumerate(selector(copy(context))):
225                if k == 0:
226                    item = result
227                elif self.parser.compatibility_mode:
228                    break
229                elif isinstance(context, XPathSchemaContext):
230                    # Multiple schema nodes are ignored but do not raise. The target
231                    # of schema context selection is XSD type association and multiple
232                    # nodes coherency is already checked at schema level.
233                    break
234                else:
235                    raise self.wrong_context_type(
236                        "a sequence of more than one item is not allowed as argument"
237                    )
238            else:
239                if item is None:
240                    if not required:
241                        return default
242                    ord_arg = ordinal(index + 1)
243                    msg = "A not empty sequence required for {} argument"
244                    raise self.error('XPTY0004', msg.format(ord_arg))
245
246        # Type promotion checking (see "function conversion rules" in XPath 2.0 language definition)
247        if cls is not None and not isinstance(item, cls) and not issubclass(cls, XPathToken):
248            if promote and isinstance(item, promote):
249                return cls(item)
250
251            if self.parser.compatibility_mode:
252                if issubclass(cls, str):
253                    return self.string_value(item)
254                elif issubclass(cls, float) or issubclass(float, cls):
255                    return self.number_value(item)
256
257            if self.parser.version == '1.0':
258                code = 'XPTY0004'
259            else:
260                value = self.data_value(item)
261                if isinstance(value, cls):
262                    return value
263                elif isinstance(value, AnyURI) and issubclass(cls, str):
264                    return cls(value)
265                elif isinstance(value, UntypedAtomic):
266                    try:
267                        return cls(value)
268                    except (TypeError, ValueError):
269                        pass
270
271                code = 'FOTY0012' if value is None else 'XPTY0004'
272
273            message = "the type of the {} argument is {!r} instead of {!r}"
274            raise self.error(code, message.format(ordinal(index + 1), type(item), cls))
275
276        return item
277
278    def select_data_values(self, context: Optional[XPathContext] = None) \
279            -> Iterator[Optional[AtomicValueType]]:
280        """
281        Yields data value of selected items.
282
283        :param context: the XPath dynamic context.
284        """
285        for item in self.select(context):
286            yield self.data_value(item)
287
288    def atomization(self, context: Optional[XPathContext] = None) \
289            -> Iterator[AtomicValueType]:
290        """
291        Helper method for value atomization of a sequence.
292
293        Ref: https://www.w3.org/TR/xpath20/#id-atomization
294
295        :param context: the XPath dynamic context.
296        """
297        for item in self.select(context):
298            value = self.data_value(item)
299            if value is None:
300                msg = "argument node {!r} does not have a typed value"
301                raise self.error('FOTY0012', msg.format(item))
302            else:
303                yield value
304
305    def get_atomized_operand(self, context: Optional[XPathContext] = None) \
306            -> Optional[AtomicValueType]:
307        """
308        Get the atomized value for an XPath operator.
309
310        :param context: the XPath dynamic context.
311        :return: the atomized value of a single length sequence or `None` if the sequence is empty.
312        """
313        selector = iter(self.atomization(context))
314        try:
315            value = next(selector)
316        except StopIteration:
317            return None
318        else:
319            item = getattr(context, 'item', None)
320
321            try:
322                next(selector)
323            except StopIteration:
324                if isinstance(value, UntypedAtomic):
325                    value = str(value)
326
327                if not isinstance(context, XPathSchemaContext) and \
328                        item is not None and \
329                        self.xsd_types and \
330                        isinstance(value, str):
331
332                    xsd_type = self.get_xsd_type(item)
333                    if xsd_type is None or xsd_type.name in XSD_SPECIAL_TYPES:
334                        pass
335                    else:
336                        try:
337                            value = xsd_type.decode(value)
338                        except (TypeError, ValueError):
339                            msg = "Type {!r} is not appropriate for the context"
340                            raise self.wrong_context_type(msg.format(type(value)))
341
342                return value
343            else:
344                msg = "atomized operand is a sequence of length greater than one"
345                raise self.wrong_context_type(msg)
346
347    def iter_comparison_data(self, context: XPathContext) -> Iterator[OperandsType]:
348        """
349        Generates comparison data couples for the general comparison of sequences.
350        Different sequences maybe generated with an XPath 2.0 parser, depending on
351        compatibility mode setting.
352
353        Ref: https://www.w3.org/TR/xpath20/#id-general-comparisons
354
355        :param context: the XPath dynamic context.
356        """
357        if self.parser.compatibility_mode:
358            operand1 = [x for x in self._items[0].select(copy(context))]
359            operand2 = [x for x in self._items[1].select(copy(context))]
360
361            # Boolean comparison if one of the results is a single boolean value (1.)
362            try:
363                if isinstance(operand1[0], bool):
364                    if len(operand1) == 1:
365                        yield operand1[0], self.boolean_value(operand2)
366                        return
367                if isinstance(operand2[0], bool):
368                    if len(operand2) == 1:
369                        yield self.boolean_value(operand1), operand2[0]
370                        return
371            except IndexError:
372                return
373
374            # Converts to float for lesser-greater operators (3.)
375            if self.symbol in ('<', '<=', '>', '>='):
376                yield from product(
377                    map(float, map(self.data_value, operand1)),  # type: ignore[arg-type]
378                    map(float, map(self.data_value, operand2)),  # type: ignore[arg-type]
379                )
380                return
381            elif self.parser.version == '1.0':
382                yield from product(map(self.data_value, operand1), map(self.data_value, operand2))
383                return
384
385        for values in product(map(self.data_value, self._items[0].select(copy(context))),
386                              map(self.data_value, self._items[1].select(copy(context)))):
387            if any(isinstance(x, bool) for x in values):
388                if any(isinstance(x, (str, Integer)) for x in values):
389                    msg = "cannot compare {!r} and {!r}"
390                    raise TypeError(msg.format(type(values[0]), type(values[1])))
391            elif any(isinstance(x, Integer) for x in values) and \
392                    any(isinstance(x, str) for x in values):
393                msg = "cannot compare {!r} and {!r}"
394                raise TypeError(msg.format(type(values[0]), type(values[1])))
395            yield values
396
397    def select_results(self, context: Optional[XPathContext]) -> Iterator[SelectResultType]:
398        """
399        Generates formatted XPath results.
400
401        :param context: the XPath dynamic context.
402        """
403        if context is not None:
404            self.parser.check_variables(context.variables)
405
406        for result in self.select(context):
407            if not isinstance(result, XPathNode):
408                yield result
409            elif isinstance(result, (TextNode, AttributeNode)):
410                yield result.value
411            elif isinstance(result, TypedElement):
412                yield result.elem
413            elif isinstance(result, TypedAttribute):
414                if is_schema_node(result.attribute.value):
415                    yield result.attribute.value
416                else:
417                    yield result.value
418            elif isinstance(result, NamespaceNode):  # pragma: no cover
419                if self.parser.compatibility_mode:
420                    yield result.prefix, result.uri
421                else:
422                    yield result.uri
423
424    def get_results(self, context: XPathContext) \
425            -> Union[List[Any], AtomicValueType]:
426        """
427        Returns formatted XPath results.
428
429        :param context: the XPath dynamic context.
430        :return: a list or a simple datatype when the result is a single simple type \
431        generated by a literal or function token.
432        """
433        results = [x for x in self.select_results(context)]
434        if len(results) == 1:
435            res = results[0]
436            if isinstance(res, (bool, int, float, Decimal)):
437                return res
438            elif is_etree_element(res) or is_document_node(res) or is_schema_node(res):
439                return results
440            elif self.label in ('function', 'literal'):
441                return cast(AtomicValueType, res)
442            else:
443                return results
444        else:
445            return results
446
447    def get_operands(self, context: XPathContext, cls: Optional[Type[Any]] = None) \
448            -> OperandsType:
449        """
450        Returns the operands for a binary operator. Float arguments are converted
451        to decimal if the other argument is a `Decimal` instance.
452
453        :param context: the XPath dynamic context.
454        :param cls: if a type is provided performs a type checking on item.
455        :return: a couple of values representing the operands. If any operand \
456        is not available returns a `(None, None)` couple.
457        """
458        op1 = self.get_argument(context, cls=cls)
459        if op1 is None:
460            return None, None
461        elif is_element_node(op1):
462            op1 = self._items[0].data_value(op1)
463
464        op2 = self.get_argument(context, index=1, cls=cls)
465        if op2 is None:
466            return None, None
467        elif is_element_node(op2):
468            op2 = self._items[1].data_value(op2)
469
470        if isinstance(op1, AbstractDateTime) and isinstance(op2, AbstractDateTime):
471            if context is not None and context.timezone is not None:
472                if op1.tzinfo is None:
473                    op1.tzinfo = context.timezone
474                if op2.tzinfo is None:
475                    op2.tzinfo = context.timezone
476        else:
477            if isinstance(op1, UntypedAtomic):
478                op1 = self.cast_to_double(op1.value)
479                if isinstance(op2, Decimal):
480                    return op1, float(op2)
481            if isinstance(op2, UntypedAtomic):
482                op2 = self.cast_to_double(op2.value)
483                if isinstance(op1, Decimal):
484                    return float(op1), op2
485
486        if isinstance(op1, float):
487            if isinstance(op2, Duration):
488                return Decimal(op1), op2
489            if isinstance(op2, Decimal):
490                return op1, type(op1)(op2)
491        if isinstance(op2, float):
492            if isinstance(op1, Duration):
493                return op1, Decimal(op2)
494            if isinstance(op1, Decimal):
495                return type(op2)(op1), op2
496
497        return op1, op2
498
499    def get_absolute_uri(self, uri: str,
500                         base_uri: Optional[str] = None,
501                         as_string: bool = True) -> Union[str, AnyURI]:
502        """
503        Obtains an absolute URI from the argument and the static context.
504
505        :param uri: a string representing an URI.
506        :param base_uri: an alternative base URI, otherwise the base_uri \
507        of the static context is used.
508        :param as_string: if `True` then returns the URI as a string, otherwise \
509        returns the URI as xs:anyURI instance.
510        :returns: the argument if it's an absolute URI. Otherwise returns the URI
511        obtained by the join o the base_uri of the static context with the
512        argument. Returns the argument if the base_uri is `None'.
513        """
514        if not base_uri:
515            base_uri = self.parser.base_uri
516
517        url_parts: Union[urllib.parse.ParseResult, urllib.parse.SplitResult]
518        url_parts = urllib.parse.urlparse(uri)
519        if url_parts.scheme or url_parts.netloc \
520                or url_parts.path.startswith('/') \
521                or base_uri is None:
522            return uri if as_string else AnyURI(uri)
523
524        url_parts = urllib.parse.urlsplit(base_uri)
525        if url_parts.fragment or not url_parts.scheme and \
526                not url_parts.netloc and not url_parts.path.startswith('/'):
527            raise self.error('FORG0002', '{!r} is not suitable as base URI'.format(base_uri))
528
529        if as_string:
530            return urllib.parse.urljoin(base_uri, uri)
531        return AnyURI(urllib.parse.urljoin(base_uri, uri))
532
533    def get_namespace(self, prefix: str) -> str:
534        """
535        Resolves a prefix to a namespace raising an error (FONS0004) if the
536        prefix is not found in the namespace map.
537        """
538        try:
539            return self.parser.namespaces[prefix]
540        except KeyError as err:
541            msg = 'no namespace found for prefix %r' % str(err)
542            raise self.error('FONS0004', msg) from None
543
544    def bind_namespace(self, namespace: str) -> None:
545        """
546        Bind a token with a namespace. The token has to be a name, a name wildcard,
547        a function or a constructor, otherwise a syntax error is raised. Functions
548        and constructors must be limited to its namespaces.
549        """
550        if self.symbol in ('(name)', '*'):
551            pass
552        elif namespace == self.parser.function_namespace:
553            if self.label != 'function':
554                msg = "a name, a wildcard or a function expected"
555                raise self.wrong_syntax(msg, code='XPST0017')
556            elif isinstance(self.label, MultiLabel):
557                self.label = 'function'
558        elif namespace == XSD_NAMESPACE:
559            if self.label != 'constructor function':
560                msg = "a name, a wildcard or a constructor function expected"
561                raise self.wrong_syntax(msg, code='XPST0017')
562            elif isinstance(self.label, MultiLabel):
563                self.label = 'constructor function'
564        elif namespace == XPATH_MATH_FUNCTIONS_NAMESPACE:
565            if self.label != 'math function':
566                msg = "a name, a wildcard or a math function expected"
567                raise self.wrong_syntax(msg, code='XPST0017')
568            elif isinstance(self.label, MultiLabel):
569                self.label = 'math function'
570        else:
571            raise self.wrong_syntax("a name, a wildcard or a function expected")
572
573        self.namespace = namespace
574
575    def adjust_datetime(self, context: XPathContext, cls: Type[DatetimeValueType]) \
576            -> Optional[Union[DatetimeValueType, DayTimeDuration]]:
577        """
578        XSD datetime adjust function helper.
579
580        :param context: the XPath dynamic context.
581        :param cls: the XSD datetime subclass to use.
582        :return: an empty list if there is only one argument that is the empty sequence \
583        or the adjusted XSD datetime instance.
584        """
585        timezone: Optional[Any]
586        item: Optional[DatetimeValueType]
587        _item: Union[DatetimeValueType, DayTimeDuration]
588
589        if len(self) == 1:
590            item = self.get_argument(context, cls=cls)
591            if item is None:
592                return None
593            timezone = getattr(context, 'timezone', None)
594        else:
595            item = self.get_argument(context, cls=cls)
596            timezone = self.get_argument(context, 1, cls=DayTimeDuration)
597
598            if timezone is not None:
599                try:
600                    timezone = Timezone.fromduration(timezone)
601                except ValueError as err:
602                    raise self.error('FODT0003', str(err)) from None
603            if item is None:
604                return None
605
606        _item = item
607        _tzinfo = _item.tzinfo
608        try:
609            if _tzinfo is not None and timezone is not None:
610                if isinstance(_item, DateTime10):
611                    _item += timezone.offset
612                elif not isinstance(item, Date10):
613                    _item += timezone.offset - _tzinfo.offset
614                elif timezone.offset < _tzinfo.offset:
615                    _item -= timezone.offset - _tzinfo.offset
616                    _item -= DayTimeDuration.fromstring('P1D')
617        except OverflowError as err:
618            raise self.error('FODT0001', str(err)) from None
619
620        if not isinstance(_item, DayTimeDuration):
621            _item.tzinfo = timezone
622        return _item
623
624    @contextlib.contextmanager
625    def use_locale(self, collation: str) -> Iterator[None]:
626        """A context manager for use a locale setting for string comparison in a code block."""
627        loc = locale.getlocale(locale.LC_COLLATE)
628        if collation == UNICODE_CODEPOINT_COLLATION:
629            collation = 'en_US.UTF-8'
630        elif collation is None:
631            raise self.error('XPTY0004', 'collation cannot be an empty sequence')
632
633        try:
634            locale.setlocale(locale.LC_COLLATE, collation)
635        except locale.Error:
636            raise self.error('FOCH0002', 'Unsupported collation %r' % collation) from None
637        else:
638            yield
639        finally:
640            locale.setlocale(locale.LC_COLLATE, loc)
641
642    ###
643    # XSD types related methods
644    def select_xsd_nodes(self, schema_context: XPathSchemaContext, name: str) \
645            -> Iterator[Union[None, TypedElement, TypedAttribute, XMLSchemaProtocol]]:
646        """
647        Selector for XSD nodes (elements, attributes and schemas). If there is
648        a match with an attribute or an element the node's type is added to
649        matching types of the token. For each matching elements or attributes
650        yields tuple nodes containing the node, its type and a compatible value
651        for doing static evaluation. For matching schemas yields the original
652        instance.
653
654        :param schema_context: an XPathSchemaContext instance.
655        :param name: a QName in extended format.
656        """
657        xsd_node: Any
658        for xsd_node in schema_context.iter_children_or_self():
659            if xsd_node is None:
660                if name == XSD_SCHEMA == schema_context.root.tag:
661                    yield None
662                continue  # pragma: no cover
663
664            try:
665                if isinstance(xsd_node, AttributeNode):
666                    if isinstance(xsd_node.value, str):
667                        if xsd_node.name != name:
668                            continue
669                        xsd_node = schema_context.root.maps.attributes.get(name)
670                        if xsd_node is None:
671                            continue
672                    elif xsd_node.value.is_matching(name):
673                        if xsd_node.name is None:
674                            # node is an XSD attribute wildcard
675                            xsd_node = schema_context.root.maps.attributes.get(name)
676                            if xsd_node is None:
677                                continue
678                    else:
679                        continue
680
681                    xsd_type = self.add_xsd_type(xsd_node)
682                    if xsd_type is not None:
683                        value = self.parser.get_atomic_value(xsd_type)
684                        yield TypedAttribute(xsd_node, xsd_type, value)
685
686                elif name == XSD_SCHEMA == xsd_node.tag:
687                    # The element is a schema
688                    yield xsd_node
689
690                elif xsd_node.is_matching(name, self.parser.default_namespace):
691                    if xsd_node.name is None:
692                        # node is an XSD element wildcard
693                        xsd_node = schema_context.root.maps.elements.get(name)
694                        if xsd_node is None:
695                            continue
696
697                    xsd_type = self.add_xsd_type(xsd_node)
698                    if xsd_type is not None:
699                        value = self.parser.get_atomic_value(xsd_type)
700                        yield TypedElement(xsd_node, xsd_type, value)
701
702            except AttributeError:
703                pass
704
705    def add_xsd_type(self, item: Any) -> Optional[XsdTypeProtocol]:
706        """
707        Adds an XSD type association from an item. The association is
708        added using the item's name and type.
709        """
710        if isinstance(item, AttributeNode):
711            item = item.value
712        elif isinstance(item, TypedAttribute):
713            item = item.attribute.value
714        elif isinstance(item, TypedElement):
715            item = item.elem
716
717        if not is_schema_node(item):
718            return None
719
720        name: str = item.name
721        xsd_type: XsdTypeProtocol = item.type
722
723        if self.xsd_types is None:
724            self.xsd_types = {name: xsd_type}
725        else:
726            obj = self.xsd_types.get(name)
727            if obj is None:
728                self.xsd_types[name] = xsd_type
729            elif not isinstance(obj, list):
730                if obj is not xsd_type:
731                    self.xsd_types[name] = [obj, xsd_type]
732            elif xsd_type not in obj:
733                obj.append(xsd_type)
734
735        return xsd_type
736
737    def get_xsd_type(self, item: Union[str, PrincipalNodeType]) \
738            -> Optional[XsdTypeProtocol]:
739        """
740        Returns the XSD type associated with an item. Match by item's name
741        and XSD validity. Returns `None` if no XSD type is matching.
742
743        :param item: a string or an AttributeNode or an element.
744        """
745        if not self.xsd_types or isinstance(self.xsd_types, AbstractSchemaProxy):
746            return None
747        elif isinstance(item, str):
748            xsd_type = self.xsd_types.get(item)
749        elif isinstance(item, AttributeNode):
750            xsd_type = self.xsd_types.get(item.name)
751        elif isinstance(item, (TypedAttribute, TypedElement)):
752            return cast(XsdTypeProtocol, item.xsd_type)
753        else:
754            xsd_type = self.xsd_types.get(item.tag)
755
756        x: XsdTypeProtocol
757        if not xsd_type:
758            return None
759        elif not isinstance(xsd_type, list):
760            return xsd_type
761        elif isinstance(item, AttributeNode):
762            for x in xsd_type:
763                if x.is_valid(item.value):
764                    return x
765        elif is_etree_element(item):
766            for x in xsd_type:
767                if x.is_simple():
768                    if x.is_valid(item.text):  # type: ignore[union-attr]
769                        return x
770                elif x.is_valid(item):
771                    return x
772
773        return xsd_type[0]
774
775    def get_typed_node(self, item: PrincipalNodeType) -> PrincipalNodeType:
776        """
777        Returns a typed node if the item is matching an XSD type.
778
779        Ref:
780          https://www.w3.org/TR/xpath20/#id-processing-model
781          https://www.w3.org/TR/xpath20/#id-static-analysis
782          https://www.w3.org/TR/xquery-semantics/
783
784        :param item: an untyped attribute or element.
785        :return: a typed AttributeNode/ElementNode if the argument is matching \
786        any associated XSD type.
787        """
788        if isinstance(item, (TypedAttribute, TypedElement)):
789            return item
790
791        xsd_type = self.get_xsd_type(item)
792        if not xsd_type:
793            return item
794        elif xsd_type.name in XSD_SPECIAL_TYPES:
795            if isinstance(item, AttributeNode):
796                if not isinstance(item.value, str):
797                    return TypedAttribute(item, xsd_type, UntypedAtomic(''))
798                return TypedAttribute(item, xsd_type, UntypedAtomic(item.value))
799            return TypedElement(item, xsd_type, UntypedAtomic(item.text or ''))
800
801        elif isinstance(item, AttributeNode):
802            pass
803        elif xsd_type.has_mixed_content():
804            value = UntypedAtomic(item.text or '')
805            return TypedElement(item, xsd_type, value)
806        elif xsd_type.is_element_only():
807            return TypedElement(item, xsd_type, None)
808        elif xsd_type.is_empty():
809            return TypedElement(item, xsd_type, None)
810        elif item.get(XSI_NIL) and getattr(xsd_type.parent, 'nillable', None):
811            return TypedElement(item, xsd_type, None)
812
813        if self.parser.xsd_version == '1.0':
814            atomic_types = xsd10_atomic_types
815        else:
816            atomic_types = xsd11_atomic_types
817
818        try:
819            builder: Any = atomic_types[xsd_type.name]
820        except KeyError:
821            pass
822        else:
823            if issubclass(builder, (AbstractDateTime, Duration)):
824                builder = builder.fromstring
825            elif issubclass(builder, QName):
826                builder = self.cast_to_qname
827
828            try:
829                if isinstance(item, AttributeNode):
830                    return TypedAttribute(item, xsd_type, builder(item.value))
831                else:
832                    return TypedElement(item, xsd_type, builder(item.text))
833            except (TypeError, ValueError):
834                msg = "Type {!r} does not match sequence type of {!r}"
835                raise self.wrong_sequence_type(msg.format(xsd_type, item)) from None
836
837        if self.parser.schema is None:
838            builder = UntypedAtomic
839        else:
840            try:
841                primitive_type = self.parser.schema.get_primitive_type(xsd_type)
842                builder = atomic_types[primitive_type.name]
843            except KeyError:
844                builder = UntypedAtomic
845            else:
846                if isinstance(builder, (AbstractDateTime, Duration)):
847                    builder = builder.fromstring
848                elif issubclass(builder, QName):
849                    builder = self.cast_to_qname
850
851        try:
852            if isinstance(item, AttributeNode):
853                if xsd_type.is_valid(item.value):
854                    return TypedAttribute(item, xsd_type, builder(item.value))
855            elif xsd_type.is_valid(item.text):
856                return TypedElement(item, xsd_type, builder(item.text))
857        except (TypeError, ValueError):
858            pass
859
860        msg = "Type {!r} does not match sequence type of {!r}"
861        raise self.wrong_sequence_type(msg.format(xsd_type, item)) from None
862
863    def cast_to_qname(self, qname: str) -> QName:
864        """Cast a prefixed qname string to a QName object."""
865        try:
866            if ':' not in qname:
867                return QName(self.parser.namespaces.get(''), qname.strip())
868            pfx, _ = qname.strip().split(':')
869            return QName(self.parser.namespaces[pfx], qname)
870        except ValueError:
871            msg = 'invalid value {!r} for an xs:QName'.format(qname.strip())
872            raise self.error('FORG0001', msg)
873        except KeyError as err:
874            raise self.error('FONS0004', 'no namespace found for prefix {}'.format(err))
875
876    def cast_to_double(self, value: Union[SupportsFloat, str]) -> float:
877        """Cast a value to xs:double."""
878        try:
879            if self.parser.xsd_version == '1.0':
880                return cast(float, DoubleProxy10(value))
881            return cast(float, DoubleProxy(value))
882        except ValueError as err:
883            raise self.error('FORG0001', str(err))  # str or UntypedAtomic
884
885    ###
886    # XPath data accessors base functions
887    def boolean_value(self, obj: Any) -> bool:
888        """
889        The effective boolean value, as computed by fn:boolean().
890        """
891        if isinstance(obj, list):
892            if not obj:
893                return False
894            elif is_xpath_node(obj[0]):
895                return True
896            elif len(obj) > 1:
897                message = "effective boolean value is not defined for a sequence " \
898                          "of two or more items not starting with an XPath node."
899                raise self.error('FORG0006', message)
900            else:
901                obj = obj[0]
902
903        if isinstance(obj, (int, str, UntypedAtomic, AnyURI)):  # Include bool
904            return bool(obj)
905        elif isinstance(obj, (float, Decimal)):
906            return False if math.isnan(obj) else bool(obj)
907        elif obj is None:
908            return False
909        else:
910            message = "effective boolean value is not defined for {!r}.".format(type(obj))
911            raise self.error('FORG0006', message)
912
913    def data_value(self, obj: Any) -> Optional[AtomicValueType]:
914        """
915        The typed value, as computed by fn:data() on each item.
916        Returns an instance of UntypedAtomic for untyped data.
917
918        https://www.w3.org/TR/xpath20/#dt-typed-value
919        """
920        if obj is None:
921            return None
922        elif isinstance(obj, XPathNode):
923            if isinstance(obj, TextNode):
924                return UntypedAtomic(obj.value)
925            elif isinstance(obj, AttributeNode) and isinstance(obj.value, str):
926                return UntypedAtomic(obj.value)
927            return cast(Optional[AtomicValueType], obj.value)  # a typed node or a NamespaceNode
928
929        elif is_schema_node(obj):
930            return self.parser.get_atomic_value(obj.type)
931
932        elif hasattr(obj, 'tag'):
933            if is_comment_node(obj) or is_processing_instruction_node(obj):
934                return cast(str, obj.text)
935            elif hasattr(obj, 'attrib') and hasattr(obj, 'text'):
936                return UntypedAtomic(''.join(etree_iter_strings(obj)))
937            else:
938                return None
939        elif is_document_node(obj):
940            value = ''.join(etree_iter_strings(obj.getroot()))
941            return UntypedAtomic(value)
942        else:
943            return cast(AtomicValueType, obj)
944
945    def string_value(self, obj: Any) -> str:
946        """
947        The string value, as computed by fn:string().
948        """
949        if obj is None:
950            return ''
951        elif isinstance(obj, XPathNode):
952            if isinstance(obj, TypedElement):
953                if obj.value is None:
954                    return ''.join(etree_iter_strings(obj))
955                return str(obj.value)
956            elif isinstance(obj, (AttributeNode, TypedAttribute)):
957                return str(obj.value)
958            else:
959                return cast(str, obj.value)  # TextNode or NamespaceNode
960        elif is_schema_node(obj):
961            return str(self.parser.get_atomic_value(obj.type))
962        elif hasattr(obj, 'tag'):
963            if is_comment_node(obj) or is_processing_instruction_node(obj):
964                return cast(str, obj.text)
965            elif hasattr(obj, 'attrib') and hasattr(obj, 'text'):
966                return ''.join(etree_iter_strings(obj))
967        elif is_document_node(obj):
968            return ''.join(etree_iter_strings(obj.getroot()))
969        elif isinstance(obj, bool):
970            return 'true' if obj else 'false'
971        elif isinstance(obj, Decimal):
972            value = format(obj, 'f')
973            if '.' in value:
974                return value.rstrip('0').rstrip('.')
975            return value
976
977        elif isinstance(obj, float):
978            if math.isnan(obj):
979                return 'NaN'
980            elif math.isinf(obj):
981                return str(obj).upper()
982
983            value = str(obj)
984            if '.' in value:
985                value = value.rstrip('0').rstrip('.')
986            if '+' in value:
987                value = value.replace('+', '')
988            if 'e' in value:
989                return value.upper()
990            return value
991
992        return str(obj)
993
994    def number_value(self, obj: Any) -> float:
995        """
996        The numeric value, as computed by fn:number() on each item. Returns a float value.
997        """
998        try:
999            return float(self.string_value(obj) if is_xpath_node(obj) else obj)
1000        except (TypeError, ValueError):
1001            return float('nan')
1002
1003    ###
1004    # Error handling helpers
1005    def error_code(self, code: str) -> str:
1006        """Returns a prefixed error code."""
1007        if self.parser.namespaces.get('err') == XQT_ERRORS_NAMESPACE:
1008            return 'err:%s' % code
1009
1010        for pfx, uri in self.parser.namespaces.items():
1011            if uri == XQT_ERRORS_NAMESPACE:
1012                return '%s:%s' % (pfx, code) if pfx else code
1013
1014        return code  # returns an unprefixed code (without prefix the namespace is not checked)
1015
1016    def error(self, code: Union[str, QName],
1017              message_or_error: Union[None, str, Exception] = None) -> ElementPathError:
1018        """
1019        Returns an XPath error instance related with a code. An XPath/XQuery/XSLT
1020        error code is an alphanumeric token starting with four uppercase letters
1021        and ending with four digits.
1022
1023        :param code: the error code as QName or string.
1024        :param message_or_error: an optional custom message or an exception.
1025        """
1026        namespace: Optional[str]
1027
1028        if isinstance(code, QName):
1029            namespace = code.uri
1030            code = code.local_name
1031        elif ':' not in code:
1032            namespace = None
1033        else:
1034            try:
1035                prefix, code = code.split(':')
1036            except ValueError:
1037                raise ElementPathValueError(
1038                    message='%r is not a prefixed name' % code,
1039                    code=self.error_code('XPTY0004'),
1040                    token=self,
1041                )
1042            else:
1043                namespace = self.parser.namespaces.get(prefix)
1044
1045        if namespace and namespace != XQT_ERRORS_NAMESPACE:
1046            raise ElementPathValueError(
1047                message='%r namespace is required' % XQT_ERRORS_NAMESPACE,
1048                code=self.error_code('XPTY0004'),
1049                token=self,
1050            )
1051
1052        try:
1053            error_class, default_message = XPATH_ERROR_CODES[code]
1054        except KeyError:
1055            raise ElementPathValueError(
1056                message='unknown XPath error code %r' % code,
1057                code=self.error_code('XPTY0004'),
1058                token=self,
1059            )
1060
1061        if message_or_error is None:
1062            message = default_message
1063        elif isinstance(message_or_error, str):
1064            message = message_or_error
1065        elif isinstance(message_or_error, ElementPathError):
1066            message = message_or_error.message
1067        else:
1068            message = str(message_or_error)
1069
1070        return error_class(message, code=self.error_code(code), token=self)
1071
1072    # Shortcuts for XPath errors, only the wrong_syntax
1073    def expected(self, *symbols: str,
1074                 message: Optional[str] = None,
1075                 code: str = 'XPST0003') -> None:
1076        if symbols and self.symbol not in symbols:
1077            raise self.wrong_syntax(message, code)
1078
1079    def unexpected(self, *symbols: str,
1080                   message: Optional[str] = None,
1081                   code: str = 'XPST0003') -> None:
1082        if not symbols or self.symbol in symbols:
1083            raise self.wrong_syntax(message, code)
1084
1085    def wrong_syntax(self, message: Optional[str] = None,  # type: ignore[override]
1086                     code: str = 'XPST0003') -> ElementPathError:
1087        if self.label == 'function':
1088            code = 'XPST0017'
1089
1090        if message:
1091            return self.error(code, message)
1092
1093        error = super(XPathToken, self).wrong_syntax(message)
1094        return self.error(code, str(error))
1095
1096    def wrong_value(self, message: Optional[str] = None) -> ElementPathValueError:
1097        return cast(ElementPathValueError, self.error('FOCA0002', message))
1098
1099    def wrong_type(self, message: Optional[str] = None) -> ElementPathTypeError:
1100        return cast(ElementPathTypeError, self.error('FORG0006', message))
1101
1102    def missing_context(self, message: Optional[str] = None) -> MissingContextError:
1103        return cast(MissingContextError, self.error('XPDY0002', message))
1104
1105    def wrong_context_type(self, message: Optional[str] = None) -> ElementPathTypeError:
1106        return cast(ElementPathTypeError, self.error('XPTY0004', message))
1107
1108    def missing_name(self, message: Optional[str] = None) -> ElementPathNameError:
1109        return cast(ElementPathNameError, self.error('XPST0008', message))
1110
1111    def missing_axis(self, message: Optional[str] = None) \
1112            -> Union[ElementPathNameError, ElementPathSyntaxError]:
1113        if self.parser.compatibility_mode:
1114            return cast(ElementPathNameError, self.error('XPST0010', message))
1115        return cast(ElementPathSyntaxError, self.error('XPST0003', message))
1116
1117    def wrong_nargs(self, message: Optional[str] = None) -> ElementPathTypeError:
1118        return cast(ElementPathTypeError, self.error('XPST0017', message))
1119
1120    def wrong_sequence_type(self, message: Optional[str] = None) -> ElementPathTypeError:
1121        return cast(ElementPathTypeError, self.error('XPDY0050', message))
1122
1123    def unknown_atomic_type(self, message: Optional[str] = None) -> ElementPathNameError:
1124        return cast(ElementPathNameError, self.error('XPST0051', message))
1125
1126
1127class XPathAxis(XPathToken):
1128    pattern = r'\b[^\d\W][\w.\-\xb7\u0300-\u036F\u203F\u2040]*(?=\s*\:\:|\s*\(\:.*\:\)\s*\:\:)'
1129    label = 'axis'
1130    reverse_axis: bool = False
1131
1132    def nud(self) -> 'XPathAxis':
1133        self.parser.advance('::')
1134        self.parser.expected_name(
1135            '(name)', '*', 'text', 'node', 'document-node',
1136            'comment', 'processing-instruction', 'attribute',
1137            'schema-attribute', 'element', 'schema-element'
1138        )
1139        self._items[:] = self.parser.expression(rbp=self.rbp),
1140        return self
1141
1142
1143class ValueToken(XPathToken):
1144    """
1145    A dummy token for encapsulating a value.
1146    """
1147    symbol = '(value)'
1148
1149    def evaluate(self, context: Optional[XPathContext] = None) -> Any:
1150        return self.value
1151
1152    def select(self, context: Optional[XPathContext] = None) -> Iterator[Any]:
1153        yield self.value
1154
1155
1156class XPathFunction(XPathToken):
1157    """
1158    A token for processing XPath functions.
1159    """
1160    _name: Optional[QName] = None
1161    pattern = r'\b[^\d\W][\w.\-\xb7\u0300-\u036F\u203F\u2040]*(?=\s*(?:\(\:.*\:\))?\s*\((?!\:))'
1162
1163    sequence_types: Tuple[str, ...] = ()
1164    "Sequence types of arguments and of the return value of the function."
1165
1166    nargs: NargsType = None
1167    "Number of arguments: a single value or a couple with None that means unbounded."
1168
1169    def __init__(self, parser: 'XPath1Parser', nargs: Optional[int] = None) -> None:
1170        super().__init__(parser)
1171        if isinstance(nargs, int) and nargs != self.nargs:
1172            if nargs < 0:
1173                raise self.error('XPST0017', 'number of arguments must be non negative')
1174            elif self.nargs is None:
1175                self.nargs = nargs
1176            elif isinstance(self.nargs, int):
1177                raise self.error('XPST0017', 'incongruent number of arguments')
1178            elif self.nargs[0] > nargs or self.nargs[1] is not None and self.nargs[1] < nargs:
1179                raise self.error('XPST0017', 'incongruent number of arguments')
1180            else:
1181                self.nargs = nargs
1182
1183    def __call__(self, context: Optional[XPathContext] = None,
1184                 argument_list: Optional[Union[
1185                     XPathToken,
1186                     List[Union[XPathToken, AtomicValueType]],
1187                     Tuple[Union[XPathToken, AtomicValueType], ...]
1188                 ]] = None) -> Any:
1189
1190        args: List[Union[Token[XPathTokenType], AtomicValueType]] = []
1191        if isinstance(argument_list, (list, tuple)):
1192            args.extend(argument_list)
1193        elif isinstance(argument_list, XPathToken):
1194            if argument_list.symbol == '(':
1195                args.append(argument_list)
1196            else:
1197                for token in argument_list.iter():
1198                    if token.symbol not in ('(', ','):
1199                        args.append(token)
1200
1201        context = copy(context)
1202        if self.symbol == 'function':
1203            if context is None:
1204                raise self.missing_context()
1205
1206            for variable, sequence_type, value in zip(self, self.sequence_types, args):
1207                if not self.parser.match_sequence_type(value, sequence_type):
1208                    msg = "invalid type for argument {!r}"
1209                    raise self.error('XPTY0004', msg.format(variable[0].value))
1210                varname = cast(str, variable[0].value)
1211                context.variables[varname] = value
1212        elif any(tk.symbol == '?' for tk in self):
1213            for value, tk in zip(args, filter(lambda x: x.symbol == '?', self)):
1214                if isinstance(value, XPathToken):
1215                    tk.value = value.evaluate(context)
1216                else:
1217                    assert not isinstance(value, Token), "An atomic value or None expected"
1218                    tk.value = value
1219        else:
1220            self.clear()
1221            for value in args:
1222                if isinstance(value, XPathToken):
1223                    self.append(value)
1224                else:
1225                    assert not isinstance(value, Token), "An atomic value or None expected"
1226                    self.append(ValueToken(self.parser, value=value))
1227
1228        result = self.evaluate(context)
1229        if not self.parser.match_sequence_type(result, self.sequence_types[-1]):
1230            msg = "{!r} does not match sequence type {}"
1231            raise self.error('XPTY0004', msg.format(result, self.sequence_types[-1]))
1232
1233        return result
1234
1235    @property
1236    def name(self) -> Optional[QName]:
1237        if self.symbol == 'function':
1238            return None
1239        elif self._name is None:
1240            if not self.namespace or self.namespace == XPATH_FUNCTIONS_NAMESPACE:
1241                self._name = QName(XPATH_FUNCTIONS_NAMESPACE, 'fn:%s' % self.symbol)
1242            elif self.namespace == XSD_NAMESPACE:
1243                self._name = QName(XSD_NAMESPACE, 'xs:%s' % self.symbol)
1244            elif self.namespace == XPATH_MATH_FUNCTIONS_NAMESPACE:
1245                self._name = QName(XPATH_MATH_FUNCTIONS_NAMESPACE, 'math:%s' % self.symbol)
1246
1247        return self._name
1248
1249    @property
1250    def arity(self) -> int:
1251        return self.nargs if isinstance(self.nargs, int) else len(self)
1252
1253    def nud(self) -> 'XPathFunction':
1254        code = 'XPST0017' if self.label == 'function' else 'XPST0003'
1255        self.value = None
1256        self.parser.advance('(')
1257        if self.nargs is None:
1258            del self._items[:]
1259            if self.parser.next_token.symbol in (')', '(end)'):
1260                raise self.error(code, 'at least an argument is required')
1261            while True:
1262                self.append(self.parser.expression(5))
1263                if self.parser.next_token.symbol != ',':
1264                    break
1265                self.parser.advance()
1266            self.parser.advance(')')
1267            return self
1268        elif self.nargs == 0:
1269            if self.parser.next_token.symbol != ')':
1270                if self.parser.next_token.symbol != '(end)':
1271                    raise self.error(code, '%s has no arguments' % str(self))
1272                raise self.parser.next_token.wrong_syntax()
1273            self.parser.advance()
1274            return self
1275        elif isinstance(self.nargs, (tuple, list)):
1276            min_args, max_args = self.nargs
1277        else:
1278            min_args = max_args = self.nargs
1279
1280        k = 0
1281        while k < min_args:
1282            if self.parser.next_token.symbol in (')', '(end)'):
1283                msg = 'Too few arguments: expected at least %s arguments' % min_args
1284                raise self.wrong_nargs(msg if min_args > 1 else msg[:-1])
1285
1286            self._items[k:] = self.parser.expression(5),
1287            k += 1
1288            if k < min_args:
1289                if self.parser.next_token.symbol == ')':
1290                    msg = 'Too few arguments: expected at least %s arguments' % min_args
1291                    raise self.error(code, msg if min_args > 1 else msg[:-1])
1292                self.parser.advance(',')
1293
1294        while max_args is None or k < max_args:
1295            if self.parser.next_token.symbol == ',':
1296                self.parser.advance(',')
1297                self._items[k:] = self.parser.expression(5),
1298            elif k == 0 and self.parser.next_token.symbol != ')':
1299                self._items[k:] = self.parser.expression(5),
1300            else:
1301                break  # pragma: no cover
1302            k += 1
1303
1304        if self.parser.next_token.symbol == ',':
1305            msg = 'Too many arguments: expected at most %s arguments' % max_args
1306            raise self.error(code, msg if max_args != 1 else msg[:-1])
1307
1308        self.parser.advance(')')
1309        return self
1310
1311
1312class XPathConstructor(XPathFunction):
1313    """
1314    A token for processing XPath 2.0+ constructors.
1315    """
1316    @staticmethod
1317    def cast(value: Any) -> AtomicValueType:
1318        raise NotImplementedError()
1319