diff --git a/.gitignore b/.gitignore index 6d9458f..30c9663 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,8 @@ .project .ipynb_checkpoints/ .tox/ -.coverage +.coverage* +!.coveragerc doc/_* __pycache__/ dist/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8355ab..3892347 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,9 +2,10 @@ CHANGELOG ********* -`v1.2.1`_ (TBD) -=============== +`v1.2.1`_ (2019-08-30) +====================== * Hashable XSD datatypes classes +* Fix Duration types comparison `v1.2.0`_ (2019-08-14) ====================== diff --git a/elementpath/xpath1_parser.py b/elementpath/xpath1_parser.py index d103f2b..b5360fb 100644 --- a/elementpath/xpath1_parser.py +++ b/elementpath/xpath1_parser.py @@ -40,8 +40,8 @@ class XPath1Parser(Parser): :param namespaces: A dictionary with mapping from namespace prefixes into URIs. :param variables: A dictionary with the static context's in-scope variables. - :param strict: If strict mode is `False` the parser enables parsing of QNames, \ - like the ElementPath library. Default is `True`. + :param strict: If strict mode is `False` the parser enables parsing of QNames \ + in extended format, like the Python's ElementPath library. Default is `True`. """ token_base_class = XPathToken @@ -160,11 +160,14 @@ class XPath1Parser(Parser): while k < min_args: if self.parser.next_token.symbol == ')': msg = 'Too few arguments: expected at least %s arguments' % min_args - self.wrong_nargs(msg[:-1] if min_args == 1 else msg) + self.wrong_nargs(msg if min_args > 1 else msg[:-1]) self[k:] = self.parser.expression(5), k += 1 if k < min_args: + if self.parser.next_token.symbol == ')': + msg = 'Too few arguments: expected at least %s arguments' % min_args + self.wrong_nargs(msg if min_args > 1 else msg[:-1]) self.parser.advance(',') while k < max_args: @@ -179,7 +182,7 @@ class XPath1Parser(Parser): if self.parser.next_token.symbol == ',': msg = 'Too many arguments: expected at most %s arguments' % max_args - self.wrong_nargs(msg[:-1] if max_args == 1 else msg) + self.wrong_nargs(msg if max_args > 1 else msg[:-1]) self.parser.advance(')') return self diff --git a/elementpath/xpath2_parser.py b/elementpath/xpath2_parser.py index 6911a9d..3f80090 100644 --- a/elementpath/xpath2_parser.py +++ b/elementpath/xpath2_parser.py @@ -637,7 +637,7 @@ def evaluate(self, context=None): elif self.symbol != 'cast': return False else: - self.wrong_value("atomic value is required") + self.wrong_context_type("an atomic value is required") try: if namespace != XSD_NAMESPACE: diff --git a/elementpath/xpath_context.py b/elementpath/xpath_context.py index 7488880..024ee3e 100644 --- a/elementpath/xpath_context.py +++ b/elementpath/xpath_context.py @@ -38,7 +38,9 @@ class XPathContext(object): def __init__(self, root, item=None, position=0, size=1, axis=None, variables=None, current_dt=None, timezone=None): if not is_element_node(root) and not is_document_node(root): - raise ElementPathTypeError("argument 'root' must be an Element: %r" % root) + raise ElementPathTypeError( + "invalid argument root={!r}, an Element is required.".format(root) + ) self._root = root if item is not None: self.item = item diff --git a/elementpath/xpath_token.py b/elementpath/xpath_token.py index c60e2f4..605a2c0 100644 --- a/elementpath/xpath_token.py +++ b/elementpath/xpath_token.py @@ -35,6 +35,9 @@ from .tdop_parser import Token def ordinal(n): + if n in {11, 12, 13}: + return '%dth' % n + least_significant_digit = n % 10 if least_significant_digit == 1: return '%dst' % n @@ -81,7 +84,7 @@ class XPathToken(Token): if symbol == '$': return '$%s variable reference' % (self[0].value if self else '') elif symbol == ',': - return 'comma operator' + return 'comma operator' if self.parser.version > '1.0' else 'comma symbol' elif label == 'function': return '%r function' % symbol elif label == 'axis': @@ -104,7 +107,7 @@ class XPathToken(Token): elif symbol == '$': return u'$%s' % self[0].source elif symbol == '{': - return u'{%s}%s' % (self.value, self[0].source) + return u'{%s}%s' % (self[0].value, self[1].value) elif symbol == 'instance': return u'%s instance of %s' % (self[0].source, ''.join(t.source for t in self[1:])) elif symbol == 'treat': diff --git a/publiccode.yml b/publiccode.yml index 62db2ea..02b2004 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -6,8 +6,8 @@ publiccodeYmlVersion: '0.2' name: elementpath url: 'https://github.com/sissaschool/elementpath' landingURL: 'https://github.com/sissaschool/elementpath' -releaseDate: '2019-08-14' -softwareVersion: v1.2.0 +releaseDate: '2019-08-30' +softwareVersion: v1.2.1 developmentStatus: stable platforms: - linux @@ -24,7 +24,7 @@ maintenance: contacts: - name: Davide Brunato email: davide.brunato@sissa.it - affiliation: ' Scuola Internazionale Superiore di Studi Avanzati' + affiliation: 'Scuola Internazionale Superiore di Studi Avanzati' legal: license: MIT mainCopyrightOwner: Scuola Internazionale Superiore di Studi Avanzati diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 2dcafdf..64c0b27 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -105,6 +105,10 @@ class UntypedAtomicTest(unittest.TestCase): self.assertEqual(UntypedAtomic(1) % 2, 1) self.assertEqual(UntypedAtomic('1') % 2, 1.0) + def test_hashing(self): + self.assertEqual(hash(UntypedAtomic(12345)), 12345) + self.assertIsInstance(hash(UntypedAtomic('alpha')), int) + class DateTimeTypesTest(unittest.TestCase): @@ -427,6 +431,10 @@ class DateTimeTypesTest(unittest.TestCase): self.assertEqual(date10("-2001-04-02-02:00") - date10("-2001-04-01"), DayTimeDuration.fromstring('P1DT2H')) + def test_hashing(self): + dt = DateTime.fromstring("2002-04-02T12:00:00-01:00") + self.assertIsInstance(hash(dt), int) + class DurationTypesTest(unittest.TestCase): @@ -582,6 +590,12 @@ class DurationTypesTest(unittest.TestCase): def test_year_month_duration(self): self.assertEqual(YearMonthDuration(10).months, 10) + def test_hashing(self): + if sys.version_info < (3, 8): + self.assertEqual(hash(Duration(16)), 3713063228956366931) + else: + self.assertEqual(hash(Duration(16)), 6141449309508620102) + class TimezoneTypeTest(unittest.TestCase): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b75cc36..36e08fe 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -22,12 +22,16 @@ from elementpath.xpath_nodes import AttributeNode, NamespaceNode, is_etree_eleme is_namespace_node, is_processing_instruction_node, is_text_node, node_attributes, \ node_base_uri, node_document_uri, node_children, node_is_id, node_is_idrefs, \ node_nilled, node_kind, node_name +from elementpath.xpath_token import ordinal from elementpath.xpath_helpers import boolean_value from elementpath.xpath1_parser import XPath1Parser class ExceptionHelpersTest(unittest.TestCase): - parser = XPath1Parser(namespaces={'xs': XSD_NAMESPACE, 'tst': "http://xpath.test/ns"}) + + @classmethod + def setUpClass(cls): + cls.parser = XPath1Parser(namespaces={'xs': XSD_NAMESPACE, 'tst': "http://xpath.test/ns"}) def test_exception_repr(self): err = ElementPathError("unknown error") @@ -228,7 +232,20 @@ class NodeHelpersTest(unittest.TestCase): self.assertEqual(node_name(namespace), 'xs') -class CompatibilityHelpersTest(unittest.TestCase): +class XPathTokenHelpersTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.parser = XPath1Parser(namespaces={'xs': XSD_NAMESPACE, 'tst': "http://xpath.test/ns"}) + + def test_ordinal_function(self): + self.assertEqual(ordinal(1), '1st') + self.assertEqual(ordinal(2), '2nd') + self.assertEqual(ordinal(3), '3rd') + self.assertEqual(ordinal(4), '4th') + self.assertEqual(ordinal(11), '11th') + self.assertEqual(ordinal(23), '23rd') + self.assertEqual(ordinal(34), '34th') def test_boolean_value_function(self): elem = ElementTree.Element('A') @@ -244,6 +261,13 @@ class CompatibilityHelpersTest(unittest.TestCase): self.assertFalse(boolean_value(0)) self.assertTrue(boolean_value(1)) + def test_get_argument_method(self): + token = self.parser.symbol_table['true'](self.parser) + + self.assertIsNone(token.get_argument(2)) + with self.assertRaises(TypeError): + token.get_argument(1, required=True) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_schema_proxy.py b/tests/test_schema_proxy.py index 14698c3..b2532eb 100644 --- a/tests/test_schema_proxy.py +++ b/tests/test_schema_proxy.py @@ -300,7 +300,7 @@ class XPath2ParserXMLSchemaTest(test_xpath2_parser.XPath2ParserTest): self.check_value("'5' cast as xs:integer", 5) self.check_value("'hello' cast as xs:integer", ValueError) self.check_value("('5', '6') cast as xs:integer", TypeError) - self.check_value("() cast as xs:integer", ValueError) + self.check_value("() cast as xs:integer", TypeError) self.check_value("() cast as xs:integer?", []) self.check_value('"1" cast as xs:boolean', True) self.check_value('"0" cast as xs:boolean', False) diff --git a/tests/test_xpath1_parser.py b/tests/test_xpath1_parser.py index e8f374e..efed458 100644 --- a/tests/test_xpath1_parser.py +++ b/tests/test_xpath1_parser.py @@ -323,6 +323,10 @@ class XPath1ParserTest(unittest.TestCase): self.check_token('(name)', 'literal', "'schema' name", "_name_literal_token(value='schema')", 'schema') + # Variables + self.check_token('$', 'operator', "$ variable reference", + "_DollarSign_operator_token()") + # Axes self.check_token('self', 'axis', "'self' axis", "_self_axis_token()") self.check_token('child', 'axis', "'child' axis", "_child_axis_token()") @@ -344,6 +348,10 @@ class XPath1ParserTest(unittest.TestCase): # Operators self.check_token('and', 'operator', "'and' operator", "_and_operator_token()") + if self.parser.version == '1.0': + self.check_token(',', 'symbol', "comma symbol", "_Comma_symbol_token()") + else: + self.check_token(',', 'operator', "comma operator", "_Comma_operator_token()") def test_token_tree(self): self.check_tree('child::B1', '(child (B1))') @@ -367,6 +375,12 @@ class XPath1ParserTest(unittest.TestCase): self.check_source('attribute::name="Galileo"', "attribute::name = 'Galileo'") self.check_source(".//eg:a | .//eg:b", '. // eg:a | . // eg:b') + try: + self.parser.strict = False + self.check_source("{tns1}name", '{tns1}name') + finally: + self.parser.strict = True + def test_wrong_syntax(self): self.wrong_syntax('') self.wrong_syntax(" \n \n )") diff --git a/tests/test_xpath2_parser.py b/tests/test_xpath2_parser.py index 5ac4faa..58c0ed4 100644 --- a/tests/test_xpath2_parser.py +++ b/tests/test_xpath2_parser.py @@ -500,6 +500,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest): self.check_value('fn:replace("abracadabra", "a.*a", "*")', "*") self.check_value('fn:replace("abracadabra", "a.*?a", "*")', "*c*bra") self.check_value('fn:replace("abracadabra", "a", "")', "brcdbr") + self.wrong_type('fn:replace("abracadabra")') self.check_value('fn:replace("abracadabra", "a(.)", "a$1$1")', "abbraccaddabbra") self.wrong_value('fn:replace("abracadabra", ".*?", "$1")')