From 8c094eb3488330396ae7372660f7c2183ae6884c Mon Sep 17 00:00:00 2001 From: Davide Brunato Date: Fri, 31 Aug 2018 21:00:31 +0200 Subject: [PATCH] Update to new release 1.0.12 - Added default_namespace property to XPath parser classes - XPath1Parser ignores the default namespace setting - Fixed the '(name)' token methods to use the default namespace --- CHANGELOG.rst | 6 +- README.rst | 2 +- doc/conf.py | 2 +- doc/xpath_api.rst | 2 + elementpath/__init__.py | 4 +- elementpath/xpath1_parser.py | 29 ++++- elementpath/xpath2_parser.py | 4 + elementpath/xpath_helpers.py | 225 ++++++++++++++++++----------------- setup.py | 4 +- tests/test_elementpath.py | 29 +++-- 10 files changed, 173 insertions(+), 134 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2db2306c..331e65dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,10 @@ CHANGELOG ********* +`v1.0.12`_ (TDB) +================ +* Fixed the default namespace use for names without prefix. + `v1.0.11`_ (2018-07-25) ======================= * Added two recursive protected methods to context class @@ -63,4 +67,4 @@ handling of static context evaluation .. _v1.0.8: https://github.com/brunato/elementpath/compare/v1.0.7...v1.0.8 .. _v1.0.10: https://github.com/brunato/elementpath/compare/v1.0.8...v1.0.10 .. _v1.0.11: https://github.com/brunato/elementpath/compare/v1.0.10...v1.0.11 - +.. _v1.0.12: https://github.com/brunato/elementpath/compare/v1.0.11...v1.0.12 diff --git a/README.rst b/README.rst index 9da36799..d87990a4 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ data structures, both for the standard ElementTree library and for the For `lxml.etree `_ this package can be useful for providing XPath 2.0 selectors, because `lxml.etree `_ already has it's own implementation of XPath 1.0. -The XPath 2.0 functions implementation not completed yet, due to wide number of functions that this +The XPath 2.0 functions implementation is not completed yet, due to wide number of functions that this language provides. If you want you can contribute to add an unimplemented function see the section below. diff --git a/doc/conf.py b/doc/conf.py index eb3a5377..5928e9e6 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -31,7 +31,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '1.0.11' +release = '1.0.12' # -- General configuration --------------------------------------------------- diff --git a/doc/xpath_api.rst b/doc/xpath_api.rst index abbea23e..1666b741 100644 --- a/doc/xpath_api.rst +++ b/doc/xpath_api.rst @@ -19,6 +19,8 @@ XPath parsers .. autoclass:: elementpath.XPath1Parser .. autoattribute:: DEFAULT_NAMESPACES + .. autoattribute:: version + .. autoattribute:: default_namespace .. autoclass:: elementpath.XPath2Parser diff --git a/elementpath/__init__.py b/elementpath/__init__.py index bcc5bcf8..77653196 100644 --- a/elementpath/__init__.py +++ b/elementpath/__init__.py @@ -8,7 +8,7 @@ # # @author Davide Brunato # -__version__ = '1.0.11' +__version__ = '1.0.12' __author__ = "Davide Brunato" __contact__ = "brunato@sissa.it" __copyright__ = "Copyright 2018, SISSA" @@ -18,7 +18,7 @@ from .exceptions import * from .tdop_parser import Token, Parser -from .xpath_helpers import AttributeNode, NamespaceNode, UntypedAtomic +from .xpath_helpers import UntypedAtomic, AttributeNode, NamespaceNode from .xpath_token import XPathToken from .xpath_context import XPathContext from .xpath1_parser import XPath1Parser diff --git a/elementpath/xpath1_parser.py b/elementpath/xpath1_parser.py index a40de41f..a132a254 100644 --- a/elementpath/xpath1_parser.py +++ b/elementpath/xpath1_parser.py @@ -81,8 +81,8 @@ class XPath1Parser(Parser): DEFAULT_NAMESPACES = XPATH_1_DEFAULT_NAMESPACES """ - The default namespaces to use on the XPath instance. Those namespaces are updated in the - instance with the ones passed with the *namespaces* argument. + The default prefix-to-namespace associations of the XPath class. Those namespaces are updated + in the instance with the ones passed with the *namespaces* argument. """ def __init__(self, namespaces=None, variables=None, strict=True, *args, **kwargs): @@ -99,8 +99,17 @@ def build_tokenizer(cls, name_pattern=XML_NCNAME_PATTERN): @property def version(self): + """The XPath version string.""" return '1.0' + @property + def default_namespace(self): + """ + The default namespace. For XPath 1.0 this value is always `None` because the default + namespace is ignored (see https://www.w3.org/TR/1999/REC-xpath-19991116/#node-tests). + """ + return + @classmethod def axis(cls, symbol, bp=0): """Register a token for a symbol that represents an XPath *axis*.""" @@ -216,16 +225,24 @@ def next_is_path_step_token(self): @method('(name)') def evaluate(self, context=None): - if context is None: - return None - elif is_element_node(context.item, self.value) or is_attribute_node(context.item, self.value): - return context.item + if context is not None: + value = self.value + if value[0] != '{' and self.parser.default_namespace: + value = u'{%s}%s' % (self.parser.default_namespace, value) + + if is_element_node(context.item, value): + return context.item + elif is_attribute_node(context.item, value): + return context.item @method('(name)') def select(self, context=None): if context is not None: value = self.value + if value[0] != '{' and self.parser.default_namespace: + value = u'{%s}%s' % (self.parser.default_namespace, value) + for item in context.iter_children_or_self(): if is_attribute_node(item, value): yield item[1] diff --git a/elementpath/xpath2_parser.py b/elementpath/xpath2_parser.py index 4425d182..0bcf703c 100644 --- a/elementpath/xpath2_parser.py +++ b/elementpath/xpath2_parser.py @@ -126,6 +126,10 @@ def __init__(self, namespaces=None, variables=None, strict=True, default_namespa def version(self): return '2.0' + @property + def default_namespace(self): + return self.namespaces.get('') + def advance(self, *symbols): super(XPath2Parser, self).advance(*symbols) if self.next_token.symbol == '(:': diff --git a/elementpath/xpath_helpers.py b/elementpath/xpath_helpers.py index e56d22ce..b7a5b903 100644 --- a/elementpath/xpath_helpers.py +++ b/elementpath/xpath_helpers.py @@ -14,6 +14,7 @@ """ import operator from collections import namedtuple + from .compat import PY3 from .exceptions import ElementPathTypeError from .namespaces import ( @@ -21,6 +22,118 @@ ) +class UntypedAtomic(object): + """ + Class for xs:untypedAtomic data. Provides special methods for comparing + and converting to basic data types. + + :param value: The untyped value, usually a string. + """ + def __init__(self, value): + self.value = value + + def __repr__(self): + return '%s(value=%r)' % (self.__class__.__name__, self.value) + + def _get_operands(self, other, force_float=True): + """ + Returns a couple of operands, applying a cast to the instance value based on + the type of the *other* argument. + + :param other: The other operand, that determines the cast for the untyped instance. + :param force_float: Force a conversion to float if *other* is an UntypedAtomic instance. + :return: A couple of values. + """ + if isinstance(other, UntypedAtomic): + if force_float: + return float(self.value), float(other.value) + else: + return self.value, other.value + elif isinstance(other, int): + return float(self.value), other + else: + return type(other)(self.value), other + + def __eq__(self, other): + return operator.eq(*self._get_operands(other, force_float=False)) + + def __ne__(self, other): + return not operator.eq(*self._get_operands(other, force_float=False)) + + def __lt__(self, other): + return operator.lt(*self._get_operands(other)) + + def __le__(self, other): + return operator.le(*self._get_operands(other)) + + def __gt__(self, other): + return operator.gt(*self._get_operands(other)) + + def __ge__(self, other): + return operator.ge(*self._get_operands(other)) + + def __add__(self, other): + return operator.add(*self._get_operands(other)) + __radd__ = __add__ + + def __sub__(self, other): + return operator.sub(*self._get_operands(other)) + + def __rsub__(self, other): + return operator.sub(*reversed(self._get_operands(other))) + + def __mul__(self, other): + return operator.mul(*self._get_operands(other)) + __rmul__ = __mul__ + + def __truediv__(self, other): + return operator.truediv(*self._get_operands(other)) + + def __rtruediv__(self, other): + return operator.truediv(*reversed(self._get_operands(other))) + + def __int__(self): + return int(self.value) + + def __float__(self): + return float(self.value) + + def __bool__(self): + return bool(self.value) + + def __abs__(self): + return abs(self.value) + + if PY3: + def __str__(self): + return str(self.value) + + def __bytes__(self): + return bytes(self.value, encoding='utf-8') + + else: + def __unicode__(self): + return unicode(self.value) + + def __str__(self): + try: + return str(self.value) + except UnicodeEncodeError: + return self.value.encode('utf-8') + + def __bytes__(self): + return self.value.encode('utf-8') + + def __div__(self, other): + return operator.truediv(*self._get_operands(other)) + + def __rdiv__(self, other): + return operator.truediv(*reversed(self._get_operands(other))) + + def __long__(self): + return int(self.value) + + ### # Utility functions for ElementTree's Element instances def is_etree_element(obj): @@ -288,115 +401,3 @@ def number_value(obj): return float(node_string_value(obj) if is_xpath_node(obj) else obj) except (TypeError, ValueError): return float('nan') - - -class UntypedAtomic(object): - """ - Class for xs:untypedAtomic data. Provides special methods for comparing - and converting to basic data types. - - :param value: The untyped value, usually a string. - """ - def __init__(self, value): - self.value = value - - def __repr__(self): - return '%s(value=%r)' % (self.__class__.__name__, self.value) - - def _get_operands(self, other, force_float=True): - """ - Returns a couple of operands, applying a cast to the instance value based on - the type of the *other* argument. - - :param other: The other operand, that determines the cast for the untyped instance. - :param force_float: Force a conversion to float if *other* is an UntypedAtomic instance. - :return: A couple of values. - """ - if isinstance(other, UntypedAtomic): - if force_float: - return float(self.value), float(other.value) - else: - return self.value, other.value - elif isinstance(other, int): - return float(self.value), other - else: - return type(other)(self.value), other - - def __eq__(self, other): - return operator.eq(*self._get_operands(other, force_float=False)) - - def __ne__(self, other): - return not operator.eq(*self._get_operands(other, force_float=False)) - - def __lt__(self, other): - return operator.lt(*self._get_operands(other)) - - def __le__(self, other): - return operator.le(*self._get_operands(other)) - - def __gt__(self, other): - return operator.gt(*self._get_operands(other)) - - def __ge__(self, other): - return operator.ge(*self._get_operands(other)) - - def __add__(self, other): - return operator.add(*self._get_operands(other)) - __radd__ = __add__ - - def __sub__(self, other): - return operator.sub(*self._get_operands(other)) - - def __rsub__(self, other): - return operator.sub(*reversed(self._get_operands(other))) - - def __mul__(self, other): - return operator.mul(*self._get_operands(other)) - __rmul__ = __mul__ - - def __truediv__(self, other): - return operator.truediv(*self._get_operands(other)) - - def __rtruediv__(self, other): - return operator.truediv(*reversed(self._get_operands(other))) - - def __int__(self): - return int(self.value) - - def __float__(self): - return float(self.value) - - def __bool__(self): - return bool(self.value) - - def __abs__(self): - return abs(self.value) - - if PY3: - def __str__(self): - return str(self.value) - - def __bytes__(self): - return bytes(self.value, encoding='utf-8') - - else: - def __unicode__(self): - return unicode(self.value) - - def __str__(self): - try: - return str(self.value) - except UnicodeEncodeError: - return self.value.encode('utf-8') - - def __bytes__(self): - return self.value.encode('utf-8') - - def __div__(self, other): - return operator.truediv(*self._get_operands(other)) - - def __rdiv__(self, other): - return operator.truediv(*reversed(self._get_operands(other))) - - def __long__(self): - return int(self.value) diff --git a/setup.py b/setup.py index f5d9c005..47b1e1ad 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,12 @@ setup( name='elementpath', - version='1.0.11', + version='1.0.12', packages=['elementpath'], author='Davide Brunato', author_email='brunato@sissa.it', url='https://github.com/brunato/elementpath', - license='MIT', + license='MIT', description='XPath 1.0/2.0 parsers and selectors for ElementTree.', long_description=long_description, classifiers=[ diff --git a/tests/test_elementpath.py b/tests/test_elementpath.py index e7e135fc..44efd81f 100644 --- a/tests/test_elementpath.py +++ b/tests/test_elementpath.py @@ -708,6 +708,22 @@ def test_union(self): self.check_selector('/A/B2 | /A/B1', root, root[:2]) self.check_selector('/A/B2 | /A/*', root, root[:]) + def test_default_namespace(self): + root = self.etree.XML('bar') + self.check_selector('/foo', root, [root]) + if type(self.parser) is XPath1Parser: + # XPath 1.0 ignores the default namespace + self.check_selector('/foo', root, [root], namespaces={'': 'ns'}) # foo --> foo + else: + self.check_selector('/foo', root, [], namespaces={'': 'ns'}) # foo --> {ns}foo + + root = self.etree.XML('bar') + self.check_selector('/foo', root, []) + if type(self.parser) is XPath1Parser: + self.check_selector('/foo', root, [], namespaces={'': 'ns'}) + else: + self.check_selector('/foo', root, [root], namespaces={'': 'ns'}) + class LxmlXPath1ParserTest(XPath1ParserTest): @@ -723,18 +739,13 @@ def check_selector(self, path, root, expected, namespaces=None, **kwargs): else: results = select(root, path, namespaces, self.parser.__class__, **kwargs) variables = kwargs.get('variables', {}) - if namespaces is None: - lxml_namespaces = None - elif isinstance(namespaces, list): - lxml_namespaces = {k: v for k, v in namespaces if k} - else: - lxml_namespaces = {k: v for k, v in namespaces.items() if k} - if isinstance(expected, set): - self.assertEqual(set(root.xpath(path, namespaces=lxml_namespaces, **variables)), expected) + if namespaces and '' not in namespaces: + self.assertEqual(set(root.xpath(path, namespaces=namespaces, **variables)), expected) self.assertEqual(set(results), expected) elif not callable(expected): - self.assertEqual(root.xpath(path, namespaces=lxml_namespaces, **variables), expected) + if namespaces and '' not in namespaces: + self.assertEqual(root.xpath(path, namespaces=namespaces, **variables), expected) self.assertEqual(results, expected) elif isinstance(expected, type): self.assertTrue(isinstance(results, expected))