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