Skip to content

Commit

Permalink
Change constructor's label to 'constructor function'
Browse files Browse the repository at this point in the history
  - A function is checked on its label suffix
  • Loading branch information
brunato committed Sep 8, 2020
1 parent f706525 commit bbe4bc4
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 37 deletions.
23 changes: 16 additions & 7 deletions elementpath/tdop.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,20 @@ def __repr__(self):
return '%s%s' % (self.__class__.__name__, self.values)

def __str__(self):
return '_'.join(self.values)
return '__'.join(self.values).replace(' ', '_')

def __hash__(self):
return hash(self.values)

def __contains__(self, item):
return any(item in v for v in self.values)

def startswith(self, string):
return any(v.startswith(string) for v in self.values)

def endswith(self, string):
return any(v.endswith(string) for v in self.values)


class Token(MutableSequence):
"""
Expand Down Expand Up @@ -137,12 +146,12 @@ class Token(MutableSequence):
:cvar label: defines the typology of the token class. Its value is used in \
representations of the token instance and can be used to restrict code choices \
without more complicated analysis. The label value can be set as needed by the \
parser implementation (eg. 'function', 'axis', 'constructor' are used by the XPath \
parsers). In the base parser class defaults to 'symbol' with 'literal' and 'operator' \
as possible alternatives. If set by a tuple of values the token class label is \
transformed to a multi-value label, that means the token class can covers multiple \
roles (eg. as XPath function or axis). In those cases the definitive role is defined \
at parse time (nud and/or led methods) after the token instance creation.
parser implementation (eg. 'function', 'axis', 'constructor function' are used by \
the XPath parsers). In the base parser class defaults to 'symbol' with 'literal' \
and 'operator' as possible alternatives. If set by a tuple of values the token \
class label is transformed to a multi-value label, that means the token class can \
covers multiple roles (eg. as XPath function or axis). In those cases the definitive \
role is defined at parse time (nud and/or led methods) after the token instance creation.
"""
symbol = None # the token identifier, key in the token table.
lbp = 0 # left binding power
Expand Down
12 changes: 6 additions & 6 deletions elementpath/xpath1/xpath1_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ def led(self, left):
else:
left.expected('(name)', '*')

if self.parser.next_token.label not in ('function', 'constructor'):
if not self.parser.next_token.label.endswith('function'):
self.parser.expected_name('(name)', '*')
if self.parser.is_spaced():
raise self.wrong_syntax("a QName cannot contains spaces before or after ':'")
Expand All @@ -511,14 +511,14 @@ def led(self, left):

@method(':')
def evaluate(self, context=None):
if self[1].label in ('function', 'constructor'):
if self[1].label.endswith('function'):
return self[1].evaluate(context)
return [x for x in self.select(context)]


@method(':')
def select(self, context=None):
if self[1].label in ('function', 'constructor'):
if self[1].label.endswith('function'):
value = self[1].evaluate(context)
if isinstance(value, list):
yield from value
Expand Down Expand Up @@ -574,7 +574,7 @@ def nud(self):

namespace = self.parser.next_token.value + self.parser.advance_until('}')
self.parser.advance()
if self.parser.next_token.label not in ('function', 'constructor'):
if not self.parser.next_token.label.endswith('function'):
self.parser.expected_name('(name)', '*')
self.parser.next_token.bind_namespace(namespace)

Expand All @@ -585,15 +585,15 @@ def nud(self):

@method('{')
def evaluate(self, context=None):
if self[1].label == 'function':
if self[1].label.endswith('function'):
return self[1].evaluate(context)
else:
return '{%s}%s' % (self[0].value, self[1].value)


@method('{')
def select(self, context=None):
if self[1].label == 'function':
if self[1].label.endswith('function'):
yield self[1].evaluate(context)
return
elif context is None:
Expand Down
16 changes: 8 additions & 8 deletions elementpath/xpath2/xpath2_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def nud(self):
unregister('boolean')


@constructor('boolean', bp=90, label=('function', 'constructor'))
@constructor('boolean', bp=90, label=('function', 'constructor function'))
def cast(self, value):
try:
return BooleanProxy(value)
Expand Down Expand Up @@ -420,7 +420,7 @@ def evaluate(self, context=None):
###
# Case 2: In XPath 2.0 the 'string' keyword is used both for fn:string() and xs:string().
unregister('string')
register('string', lbp=90, rbp=90, label=('function', 'constructor'), # pragma: no cover
register('string', lbp=90, rbp=90, label=('function', 'constructor function'), # pragma: no cover
pattern=r'\bstring(?=\s*\(|\s*\(\:.*\:\)\()', cast=XPathToken.string_value)


Expand Down Expand Up @@ -459,7 +459,7 @@ def evaluate(self, context=None):
# In those cases the label at parse time is set by the nud method, in dependence
# of the number of args.
#
@constructor('QName', bp=90, label=('function', 'constructor'))
@constructor('QName', bp=90, label=('function', 'constructor function'))
def cast(self, value):
if isinstance(value, QName):
return value
Expand All @@ -471,7 +471,7 @@ def cast(self, value):
raise self.error('XPTY0004', 'the argument has an invalid type %r' % type(value))


@constructor('dateTime', bp=90, label=('function', 'constructor'))
@constructor('dateTime', bp=90, label=('function', 'constructor function'))
def cast(self, value):
cls = DateTime if self.parser.xsd_version == '1.1' else DateTime10
if isinstance(value, cls):
Expand Down Expand Up @@ -501,10 +501,10 @@ def nud(self):
self.label = 'function'
self.parser.advance(',')
self[1:] = self.parser.expression(5),
elif self.label != 'constructor' or self.namespace != XSD_NAMESPACE:
elif self.label != 'constructor function' or self.namespace != XSD_NAMESPACE:
raise self.error('XPST0017', '2nd argument missing')
else:
self.label = 'constructor'
self.label = 'constructor function'
self.parser.advance(')')
except SyntaxError:
raise self.error('XPST0017') from None
Expand All @@ -514,7 +514,7 @@ def nud(self):

@method('QName')
def evaluate(self, context=None):
if self.label == 'constructor':
if self.label == 'constructor function':
arg = self.data_value(self.get_argument(context))
return [] if arg is None else self.cast(arg)
else:
Expand All @@ -532,7 +532,7 @@ def evaluate(self, context=None):

@method('dateTime')
def evaluate(self, context=None):
if self.label == 'constructor':
if self.label == 'constructor function':
arg = self.data_value(self.get_argument(context))
if arg is None:
return []
Expand Down
6 changes: 3 additions & 3 deletions elementpath/xpath2/xpath2_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def advance(self, *symbols):
return self.next_token

@classmethod
def constructor(cls, symbol, bp=0, label='constructor'):
def constructor(cls, symbol, bp=0, label='constructor function'):
"""Creates a constructor token class."""
def nud_(self):
try:
Expand Down Expand Up @@ -369,7 +369,7 @@ def evaluate_(self_, context=None):
token_class_name = str("_%s_constructor_token" % symbol.replace(':', '_'))
kwargs = {
'symbol': symbol,
'label': 'constructor',
'label': 'constructor function',
'pattern': r'\b%s(?=\s*\(|\s*\(\:.*\:\)\()' % symbol,
'lbp': bp,
'rbp': bp,
Expand Down Expand Up @@ -809,7 +809,7 @@ def evaluate(self, context=None):
else:
local_name = atomic_type.split('}')[1]
token_class = self.parser.symbol_table.get(local_name)
if token_class is None or token_class.label != 'constructor':
if token_class is None or token_class.label != 'constructor function':
msg = "atomic type %r not found in the in-scope schema types"
raise self.unknown_atomic_type(msg % self[1].source)

Expand Down
12 changes: 6 additions & 6 deletions elementpath/xpath_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def __str__(self):
return '$%s variable reference' % (self[0].value if self._items else '')
elif symbol == ',':
return 'comma operator' if self.parser.version > '1.0' else 'comma symbol'
elif label in ('axis', 'function', 'sequence type', 'kind test', 'constructor'):
elif label.endswith('function') or label in ('axis', 'sequence type', 'kind test'):
return '%r %s' % (symbol, label)
return super(XPathToken, self).__str__()

Expand All @@ -103,7 +103,7 @@ def source(self):
symbol, label = self.symbol, self.label
if label == 'axis':
return '%s::%s' % (self.symbol, self[0].source)
elif label in ('function', 'sequence type', 'kind test', 'constructor'):
elif label.endswith('function') or label in ('sequence type', 'kind test'):
return '%s(%s)' % (self.symbol, ', '.join(item.source for item in self))
elif symbol == ':':
return '%s:%s' % (self[0].source, self[1].source)
Expand Down Expand Up @@ -137,7 +137,7 @@ def child_axis(self):
return False
elif self.symbol != ':':
return True
return self[1].label not in ('function', 'constructor')
return not self[1].label.endswith('function')

###
# Tokens tree analysis methods
Expand Down Expand Up @@ -508,13 +508,13 @@ def bind_namespace(self, namespace):
elif isinstance(self.label, MultiLabel):
self.label = 'function'
elif namespace == XSD_NAMESPACE:
if self.label != 'constructor':
if self.label != 'constructor function':
msg = "a name, a wildcard or a constructor function expected"
raise self.wrong_syntax(msg, code='XPST0017')
elif isinstance(self.label, MultiLabel):
self.label = 'constructor'
self.label = 'constructor function'
else:
raise self.wrong_syntax("a name, a wildcard, a function or a constructor expected")
raise self.wrong_syntax("a name, a wildcard or a function expected")

self.namespace = namespace

Expand Down
23 changes: 19 additions & 4 deletions tests/test_tdop_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,27 @@ def evaluate(self, context=None):
class TdopParserTest(unittest.TestCase):

def test_multi_label_class(self):
label = MultiLabel('function', 'constructor')
label = MultiLabel('function', 'constructor function')
self.assertEqual(label, 'function')
self.assertEqual(label, 'constructor')
self.assertEqual(label, 'constructor function')
self.assertNotEqual(label, 'constructor')
self.assertNotEqual(label, 'operator')
self.assertEqual(str(label), 'function_constructor')
self.assertEqual(hash(label), hash(('function', 'constructor')))
self.assertEqual(str(label), 'function__constructor_function')
self.assertEqual(hash(label), hash(('function', 'constructor function')))

self.assertIn(label, ['function'])
self.assertNotIn(label, [])
self.assertNotIn(label, ['not a function'])
self.assertNotIn(label, {'function'}) # compares not equality but hash

self.assertIn('function', label)
self.assertIn('constructor', label)
self.assertNotIn('axis', label)

self.assertTrue(label.startswith('function'))
self.assertTrue(label.startswith('constructor'))
self.assertTrue(label.endswith('function'))
self.assertFalse(label.endswith('constructor'))

def test_symbol_to_identifier_function(self):
self.assertEqual(symbol_to_identifier('_cat10'), '_cat10')
Expand Down
6 changes: 3 additions & 3 deletions tests/test_xpath_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,13 @@ def test_bind_namespace_method(self):
with self.assertRaises(TypeError) as ctx:
token.bind_namespace(XSD_NAMESPACE)
self.assertIn('XPST0017', str(ctx.exception))
self.assertIn("a name, a wildcard or a constructor", str(ctx.exception))
self.assertIn("a name, a wildcard or a constructor function", str(ctx.exception))

token = self.parser.parse("xs:string(10.1)")
with self.assertRaises(TypeError) as ctx:
token.bind_namespace(XSD_NAMESPACE)
self.assertIn('XPST0017', str(ctx.exception))
self.assertIn("a name, a wildcard or a constructor", str(ctx.exception))
self.assertIn("a name, a wildcard or a constructor function", str(ctx.exception))

self.assertIsNone(token[1].bind_namespace(XSD_NAMESPACE))
with self.assertRaises(TypeError) as ctx:
Expand All @@ -426,7 +426,7 @@ def test_bind_namespace_method(self):
with self.assertRaises(SyntaxError) as ctx:
token.bind_namespace('http://xpath.test/ns')
self.assertIn('XPST0003', str(ctx.exception))
self.assertIn("a name, a wildcard, a function or a constructor", str(ctx.exception))
self.assertIn("a name, a wildcard or a function", str(ctx.exception))

@unittest.skipIf(xmlschema is None, "xmlschema library required.")
def test_add_xsd_type(self):
Expand Down

0 comments on commit bbe4bc4

Please sign in to comment.