Skip to content

Commit 98d20ac

Browse files
committed
Add a function for defining a functions sequence types
- Update release info
1 parent 638aa0b commit 98d20ac

9 files changed

Lines changed: 128 additions & 74 deletions

File tree

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
CHANGELOG
33
*********
44

5+
`v5.1.1`_ (2025-01-20)
6+
======================
7+
* Fix external function registrations (issue #099)
8+
* Add ExternalFunction and SchemaConstructor token classes
9+
510
`v5.1.0`_ (2025-12-28)
611
======================
712
* Drop Python 3.9 compatibility and add Pyton 3.15 support
@@ -531,3 +536,4 @@ CHANGELOG
531536
.. _v5.0.3: https://github.com/sissaschool/elementpath/compare/v5.0.2...v5.0.3
532537
.. _v5.0.4: https://github.com/sissaschool/elementpath/compare/v5.0.3...v5.0.4
533538
.. _v5.1.0: https://github.com/sissaschool/elementpath/compare/v5.0.4...v5.1.0
539+
.. _v5.1.1: https://github.com/sissaschool/elementpath/compare/v5.1.0...v5.1.1

elementpath/sequence_types.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from elementpath.datatypes import builtin_atomic_types, builtin_list_types, QName, \
1717
NumericProxy, AnyAtomicType
18-
from elementpath.exceptions import ElementPathKeyError, xpath_error
18+
from elementpath.exceptions import ElementPathKeyError, ElementPathValueError, xpath_error
1919
from elementpath.namespaces import XSD_NAMESPACE, XSD_ERROR, XSD_DATETIME_STAMP, \
2020
XSD_NUMERIC, XSD_UNTYPED, XSD_UNTYPED_ATOMIC, get_expanded_name
2121
from elementpath.helpers import collapse_white_spaces, Patterns
@@ -36,6 +36,53 @@
3636
)
3737

3838

39+
def get_function_signatures(qname: QName,
40+
nargs: ta.NargsType,
41+
sequence_types: tuple[str, ...] = ()) -> dict[tuple[QName, int], str]:
42+
"""
43+
Returns the signatures for a function.
44+
45+
:param qname: A QName instance that represents the FQDN of the function.
46+
:param nargs: The number of arguments that the function takes, could the `None`, \
47+
a non-negative integer or a couple non-negative integers or a non-negative integer \
48+
followed by `None`.
49+
:param sequence_types: A sequence of sequence type specifications, must match \
50+
the number of arguments that the function takes plus the return type.
51+
"""
52+
function_signatures: dict[tuple[QName, int], str] = {}
53+
if not sequence_types:
54+
return function_signatures
55+
56+
if any(not is_sequence_type(st) for st in sequence_types):
57+
msg = "Error in provided sequence types: {!r}"
58+
raise ElementPathValueError(msg.format(sequence_types))
59+
elif nargs is None:
60+
if len(sequence_types) != 1:
61+
raise ElementPathValueError("Mismatched number of sequence types provided")
62+
function_signatures[(qname, 0)] = f'function() as {sequence_types[0]}'
63+
elif isinstance(nargs, int):
64+
if len(sequence_types) != nargs + 1:
65+
raise ElementPathValueError("Mismatched number of sequence types provided")
66+
function_signatures[(qname, nargs)] = 'function({}) as {}'.format(
67+
', '.join(sequence_types[:-1]), sequence_types[-1]
68+
)
69+
elif nargs[1] is None:
70+
if len(sequence_types) != nargs[0] + 1:
71+
raise ElementPathValueError("Mismatched number of sequence types provided")
72+
function_signatures[(qname, nargs[0])] = 'function({}, ...) as {}'.format(
73+
', '.join(sequence_types[:-1]), sequence_types[-1]
74+
)
75+
else:
76+
if len(sequence_types) != nargs[1] + 1:
77+
raise ElementPathValueError("Mismatched number of sequence types provided")
78+
for arity in range(nargs[0], nargs[1] + 1):
79+
function_signatures[(qname, arity)] = 'function({}) as {}'.format(
80+
', '.join(sequence_types[:arity]), sequence_types[-1]
81+
)
82+
83+
return function_signatures
84+
85+
3986
###
4087
# Sequence type checking
4188
@cache

elementpath/tdop.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
import re
1515
from abc import ABCMeta
1616
from collections.abc import Callable, Iterator, MutableMapping, MutableSequence
17-
from unicodedata import name as unicode_name
1817
from decimal import Decimal, DecimalException
1918
from typing import Any, cast, overload, Generic, TypeVar
19+
from unicodedata import name as unicode_name
2020

2121
#
2222
# Simple top-down parser based on Vaughan Pratt's algorithm (Top Down Operator Precedence).
@@ -49,7 +49,7 @@ class ParseError(SyntaxError):
4949
"""An error when parsing source with TDOP parser."""
5050

5151

52-
def _symbol_to_classname(symbol: str) -> str:
52+
def symbol_to_classname(symbol: str) -> str:
5353
"""
5454
Converts a symbol string to an identifier (only alphanumeric and '_').
5555
"""
@@ -693,7 +693,7 @@ def register(cls, symbol: str | type[TK_co], **kwargs: Any) -> type[TK_co]:
693693
token_class_name = kwargs.pop('class_name')
694694
else:
695695
token_class_name = "_%s%s" % (
696-
_symbol_to_classname(symbol),
696+
symbol_to_classname(symbol),
697697
str(label).title().replace(' ', '')
698698
)
699699

@@ -713,8 +713,13 @@ def register(cls, symbol: str | type[TK_co], **kwargs: Any) -> type[TK_co]:
713713
raise TypeError("A string or a %r subclass requested, not %r." % (Token, symbol))
714714
else:
715715
token_class = symbol
716-
if cls.symbol_table.get(symbol.lookup_name) is not token_class:
717-
raise ValueError("Token class %r is not registered." % token_class)
716+
lookup_name = token_class.lookup_name
717+
718+
if lookup_name not in cls.symbol_table:
719+
cls.symbol_table[lookup_name] = token_class
720+
elif cls.symbol_table[lookup_name] is not token_class:
721+
msg = "Token class {!r} collide on key {!r} with a different token class."
722+
raise ValueError(msg.format(token_class, lookup_name))
718723

719724
for key, value in kwargs.items():
720725
if key == 'lbp' and value > token_class.lbp:

elementpath/xpath1/xpath1_parser.py

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from elementpath.collations import UNICODE_CODEPOINT_COLLATION
2525
from elementpath.datatypes import QName
2626
from elementpath.tdop import Parser
27-
from elementpath.sequence_types import match_sequence_type
27+
from elementpath.sequence_types import get_function_signatures, match_sequence_type
2828
from elementpath.schema_proxy import AbstractSchemaProxy
2929
from elementpath.xpath_tokens import XPathToken, XPathAxis, XPathFunction, ProxyToken, \
3030
NameToken, PrefixedNameToken, BracedNameToken
@@ -218,27 +218,11 @@ def function(cls, symbol: str,
218218

219219
if sequence_types:
220220
# Register function signature(s)
221+
cls.function_signatures.update(
222+
get_function_signatures(qname, nargs, sequence_types)
223+
)
221224
kwargs['sequence_types'] = sequence_types
222225

223-
if nargs is None:
224-
pass # pragma: no cover
225-
elif isinstance(nargs, int):
226-
assert len(sequence_types) == nargs + 1
227-
cls.function_signatures[(qname, nargs)] = 'function({}) as {}'.format(
228-
', '.join(sequence_types[:-1]), sequence_types[-1]
229-
)
230-
elif nargs[1] is None:
231-
assert len(sequence_types) == nargs[0] + 1
232-
cls.function_signatures[(qname, nargs[0])] = 'function({}, ...) as {}'.format(
233-
', '.join(sequence_types[:-1]), sequence_types[-1]
234-
)
235-
else:
236-
assert len(sequence_types) == nargs[1] + 1
237-
for arity in range(nargs[0], nargs[1] + 1):
238-
cls.function_signatures[(qname, arity)] = 'function({}) as {}'.format(
239-
', '.join(sequence_types[:arity]), sequence_types[-1]
240-
)
241-
242226
return cast(type[XPathFunction], cls.register(symbol, **kwargs))
243227

244228
def parse(self, source: str) -> XPathToken:

elementpath/xpath2/xpath2_parser.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
from elementpath.collations import UNICODE_COLLATION_BASE_URI, UNICODE_CODEPOINT_COLLATION
2929
from elementpath.xpath_tokens import XPathToken, ProxyToken, XPathFunction, \
3030
XPathConstructor, SchemaConstructor, ExternalFunction
31-
from elementpath.sequence_types import is_sequence_type, match_sequence_type
31+
from elementpath.sequence_types import get_function_signatures, is_sequence_type, \
32+
match_sequence_type
3233
from elementpath.schema_proxy import AbstractSchemaProxy
3334
from elementpath.xpath1 import XPath1Parser
3435

@@ -274,7 +275,7 @@ def bind(func: Callable[..., Any]) -> Callable[..., Any]:
274275
return func
275276
return bind
276277

277-
def dynamic_register(self, symbol: str, class_name: str, **kwargs: Any) \
278+
def _register(self, symbol: str, class_name: str, **kwargs: Any) \
278279
-> type[ta.XPathTokenType]:
279280
"""
280281
Register/update a token class in the symbol table of a parser instance.
@@ -328,7 +329,7 @@ def schema_constructor(self, atomic_type_name: str, bp: int = 90) -> type[XPathF
328329
'lbp': bp,
329330
'rbp': bp,
330331
}
331-
return cast(type[XPathFunction], self.dynamic_register(symbol, class_name, **kwargs))
332+
return cast(type[XPathFunction], self._register(symbol, class_name, **kwargs))
332333

333334
def external_function(self,
334335
callback: Callable[..., Any],
@@ -364,27 +365,7 @@ def external_function(self,
364365
namespace = XPATH_FUNCTIONS_NAMESPACE
365366
qname = QName(XPATH_FUNCTIONS_NAMESPACE, f'fn:{symbol}')
366367

367-
function_signatures: dict[tuple[QName, int], str] = {}
368-
if sequence_types:
369-
if nargs is None:
370-
assert len(sequence_types) == 1
371-
function_signatures[(qname, 0)] = f'function() as {sequence_types[0]}'
372-
elif isinstance(nargs, int):
373-
assert len(sequence_types) == nargs + 1
374-
function_signatures[(qname, nargs)] = 'function({}) as {}'.format(
375-
', '.join(sequence_types[:-1]), sequence_types[-1]
376-
)
377-
elif nargs[1] is None:
378-
assert len(sequence_types) == nargs[0] + 1
379-
function_signatures[(qname, nargs[0])] = 'function({}, ...) as {}'.format(
380-
', '.join(sequence_types[:-1]), sequence_types[-1]
381-
)
382-
else:
383-
assert len(sequence_types) == nargs[1] + 1
384-
for arity in range(nargs[0], nargs[1] + 1):
385-
function_signatures[(qname, arity)] = 'function({}) as {}'.format(
386-
', '.join(sequence_types[:arity]), sequence_types[-1]
387-
)
368+
function_signatures = get_function_signatures(qname, nargs, sequence_types)
388369

389370
if qname.expanded_name in self.symbol_table:
390371
msg = f'function {qname.qname!r} is already registered'
@@ -414,7 +395,7 @@ def external_function(self,
414395
'lbp': bp,
415396
'rbp': bp,
416397
}
417-
self.dynamic_register(symbol, class_name, **kwargs)
398+
self._register(symbol, class_name, **kwargs)
418399

419400
class_name = f'{upper_camel_case(qname.qname)}ExternalFunction'
420401
kwargs = {
@@ -428,7 +409,7 @@ def external_function(self,
428409
'sequence_types': sequence_types,
429410
'callback': staticmethod(callback),
430411
}
431-
token_class = self.dynamic_register(symbol, class_name, **kwargs)
412+
token_class = self._register(symbol, class_name, **kwargs)
432413

433414
if function_signatures:
434415
# Register function signatures

elementpath/xpath_tokens/functions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def nud(self) -> 'XPathFunction':
236236
if not self.parser.parse_arguments:
237237
return self
238238

239-
code = 'XPST0017' if self.label == 'function' else 'XPST0003'
239+
code = 'XPST0017' if 'function' in self.label else 'XPST0003'
240240
self.parser.advance('(')
241241
if self.nargs is None:
242242
del self._items[:]

publiccode.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name: elementpath
77
url: 'https://github.com/sissaschool/elementpath'
88
landingURL: 'https://github.com/sissaschool/elementpath'
99
releaseDate: '2025-12-28'
10-
softwareVersion: v5.1.0
10+
softwareVersion: v5.1.1
1111
developmentStatus: stable
1212
platforms:
1313
- linux

tests/test_tdop_parser.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import sys
1414
from collections import namedtuple
1515

16-
from elementpath.tdop import _symbol_to_classname, ParseError, Token, \
16+
from elementpath.tdop import symbol_to_classname, ParseError, Token, \
1717
ParserMeta, Parser, MultiLabel
1818

1919

@@ -68,21 +68,21 @@ def test_multi_label_class(self):
6868
self.assertFalse(label.endswith('constructor'))
6969

7070
def test_symbol_to_classname_function(self):
71-
self.assertEqual(_symbol_to_classname('_cat10'), 'Cat10')
72-
self.assertEqual(_symbol_to_classname('&'), 'Ampersand')
73-
self.assertEqual(_symbol_to_classname('('), 'LeftParenthesis')
74-
self.assertEqual(_symbol_to_classname(')'), 'RightParenthesis')
71+
self.assertEqual(symbol_to_classname('_cat10'), 'Cat10')
72+
self.assertEqual(symbol_to_classname('&'), 'Ampersand')
73+
self.assertEqual(symbol_to_classname('('), 'LeftParenthesis')
74+
self.assertEqual(symbol_to_classname(')'), 'RightParenthesis')
7575

76-
self.assertEqual(_symbol_to_classname('(name)'), 'Name')
77-
self.assertEqual(_symbol_to_classname('(name'), 'LeftParenthesisname')
76+
self.assertEqual(symbol_to_classname('(name)'), 'Name')
77+
self.assertEqual(symbol_to_classname('(name'), 'LeftParenthesisname')
7878

79-
self.assertEqual(_symbol_to_classname('-'), 'HyphenMinus')
80-
self.assertEqual(_symbol_to_classname('_'), 'LowLine')
81-
self.assertEqual(_symbol_to_classname('-_'), 'HyphenMinusLowLine')
82-
self.assertEqual(_symbol_to_classname('--'), 'HyphenMinusHyphenMinus')
79+
self.assertEqual(symbol_to_classname('-'), 'HyphenMinus')
80+
self.assertEqual(symbol_to_classname('_'), 'LowLine')
81+
self.assertEqual(symbol_to_classname('-_'), 'HyphenMinusLowLine')
82+
self.assertEqual(symbol_to_classname('--'), 'HyphenMinusHyphenMinus')
8383

84-
self.assertEqual(_symbol_to_classname('my-api-call'), 'MyApiCall')
85-
self.assertEqual(_symbol_to_classname('call-'), 'Call')
84+
self.assertEqual(symbol_to_classname('my-api-call'), 'MyApiCall')
85+
self.assertEqual(symbol_to_classname('call-'), 'Call')
8686

8787
def test_create_tokenizer_method(self):
8888
FakeToken = namedtuple('Token', 'symbol pattern label')
@@ -336,10 +336,13 @@ class AnotherParser(Parser):
336336
AnotherParser.register(9)
337337
self.assertIn("A string or a", str(ec.exception))
338338

339+
AnotherParser.register(self.parser.symbol_table['+'])
340+
AnotherParser.symbol_table['-'] = self.parser.symbol_table['+']
341+
339342
with self.assertRaises(ValueError) as ec:
340-
AnotherParser.register(self.parser.symbol_table['+'])
343+
AnotherParser.register(self.parser.symbol_table['-'])
341344
self.assertIn("Token class ", str(ec.exception))
342-
self.assertIn("is not registered", str(ec.exception))
345+
self.assertIn("collide", str(ec.exception))
343346

344347
def test_other_operators(self):
345348

0 commit comments

Comments
 (0)