Update to release v1.1.8

- Added coverage and flake8 to testing
  - Removed py34 from tox.ini
  - Added py38 with a limited testing
  - ElementPathMissingContextError renamed to MissingContextError
  - Added exceptions to API docs
This commit is contained in:
Davide Brunato 2019-05-20 23:17:04 +02:00
parent 46ac3a6a1c
commit 1d23f5b13f
15 changed files with 119 additions and 59 deletions

View File

@ -2,6 +2,13 @@
CHANGELOG
*********
`v1.1.8`_ (2019-05-20)
======================
* Added code coverage and flake8 checks
* Drop Python 3.4 support
* Use more specific XPath errors for functions and namespace resolving
* Fix for issue #4
`v1.1.7`_ (2019-04-25)
======================
* Added Parser.is_spaced() method for checking if the current token has extra spaces before or after
@ -123,3 +130,4 @@ CHANGELOG
.. _v1.1.5: https://github.com/brunato/elementpath/compare/v1.1.4...v1.1.5
.. _v1.1.6: https://github.com/brunato/elementpath/compare/v1.1.5...v1.1.6
.. _v1.1.7: https://github.com/brunato/elementpath/compare/v1.1.6...v1.1.7
.. _v1.1.8: https://github.com/brunato/elementpath/compare/v1.1.7...v1.1.8

View File

@ -31,7 +31,7 @@ author = 'Davide Brunato'
# The short X.Y version
version = ''
# The full version, including alpha/beta/rc tags
release = '1.1.7'
release = '1.1.8'
# -- General configuration ---------------------------------------------------

View File

@ -87,3 +87,23 @@ implementing concrete interfaces to other types of XML Schema processors.
.. automethod:: is_instance
.. automethod:: iter_atomic_types
.. automethod:: get_primitive_type
Exception classes
=================
.. autoexception:: elementpath.ElementPathError
.. autoexception:: elementpath.MissingContextError
Other exceptions
----------------
There are some exceptions derived from the base exception and Python built-in exceptions:
.. autoexception:: elementpath.ElementPathKeyError
.. autoexception:: elementpath.ElementPathLocaleError
.. autoexception:: elementpath.ElementPathNameError
.. autoexception:: elementpath.ElementPathSyntaxError
.. autoexception:: elementpath.ElementPathTypeError
.. autoexception:: elementpath.ElementPathValueError

View File

@ -8,7 +8,7 @@
#
# @author Davide Brunato <brunato@sissa.it>
#
__version__ = '1.1.7'
__version__ = '1.1.8'
__author__ = "Davide Brunato"
__contact__ = "brunato@sissa.it"
__copyright__ = "Copyright 2018-2019, SISSA"
@ -16,7 +16,10 @@ __license__ = "MIT"
__status__ = "Production/Stable"
from .exceptions import *
from .exceptions import ElementPathError, MissingContextError, \
ElementPathSyntaxError, ElementPathNameError, ElementPathKeyError, \
ElementPathTypeError, ElementPathLocaleError, ElementPathValueError
from . import datatypes
from .tdop_parser import Token, Parser
from .xpath_context import XPathContext, XPathSchemaContext

View File

@ -42,6 +42,10 @@ class ElementPathError(Exception):
__str__ = __unicode__
class MissingContextError(ElementPathError):
"""Raised when the dynamic context is required for evaluate the XPath expression."""
class ElementPathNameError(ElementPathError, NameError):
pass
@ -66,10 +70,6 @@ class ElementPathLocaleError(ElementPathError, locale.Error):
pass
class ElementPathMissingContextError(ElementPathError):
pass
def xpath_error(code, message=None, token=None, prefix='err'):
"""
Returns an XPath error instance related with a code. An XPath/XQuery/XSLT error code
@ -93,7 +93,7 @@ def xpath_error(code, message=None, token=None, prefix='err'):
if code == 'XPST0001':
return ElementPathValueError(message or 'Parser not bound to a schema', pcode, token)
elif code == 'XPDY0002':
return ElementPathMissingContextError(message or 'Dynamic context required for evaluate', pcode, token)
return MissingContextError(message or 'Dynamic context required for evaluate', pcode, token)
elif code == 'XPTY0004':
return ElementPathTypeError(message or 'Type is not appropriate for the context', pcode, token)
elif code == 'XPST0005':

View File

@ -699,9 +699,9 @@ class Parser(object):
def infixr(cls, symbol, bp=0):
"""Register a token for a symbol that represents an *infixr* binary operator."""
def led(self, left):
self[:] = left, self.parser.expression(rbp=bp-1)
self[:] = left, self.parser.expression(rbp=bp - 1)
return self
return cls.register(symbol, label='operator', lbp=bp, rbp=bp-1, led=led)
return cls.register(symbol, label='operator', lbp=bp, rbp=bp - 1, led=led)
@classmethod
def method(cls, symbol, bp=0):

View File

@ -14,7 +14,7 @@ import decimal
from .compat import PY3, string_base_type
from .exceptions import ElementPathSyntaxError, ElementPathTypeError, ElementPathNameError, \
ElementPathMissingContextError
MissingContextError
from .datatypes import UntypedAtomic, DayTimeDuration, YearMonthDuration, XSD_BUILTIN_TYPES
from .xpath_context import XPathSchemaContext
from .tdop_parser import Parser, MultiLabel
@ -78,7 +78,7 @@ class XPath1Parser(Parser):
DEFAULT_NAMESPACES = XPATH_1_DEFAULT_NAMESPACES
"""
The default prefix-to-namespace associations of the XPath class. Those namespaces are updated
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.
"""
@ -115,7 +115,7 @@ class XPath1Parser(Parser):
@classmethod
def axis(cls, symbol, bp=80):
"""Register a token for a symbol that represents an XPath *axis*."""
def nud_(self):
def nud_(self):
self.parser.advance('::')
self.parser.next_token.expected(
'(name)', '*', 'text', 'node', 'document-node', 'comment', 'processing-instruction',
@ -187,7 +187,7 @@ class XPath1Parser(Parser):
def next_is_path_step_token(self):
return self.next_token.label == 'axis' or self.next_token.symbol in {
'(integer)', '(string)', '(float)', '(decimal)', '(name)', 'node', 'text', '*',
'(integer)', '(string)', '(float)', '(decimal)', '(name)', 'node', 'text', '*',
'@', '..', '.', '(', '{'
}
@ -195,7 +195,7 @@ class XPath1Parser(Parser):
root_token = super(XPath1Parser, self).parse(source)
try:
root_token.evaluate() # Static context evaluation
except ElementPathMissingContextError:
except MissingContextError:
pass
return root_token
@ -469,7 +469,7 @@ def select(self, context=None):
context.item = item
yield item
elif context is None:
raise ElementPathMissingContextError("Context required to evaluate `*`")
self.missing_context()
else:
# Wildcard literal
for item in context.iter_children_or_self():
@ -483,7 +483,7 @@ def select(self, context=None):
@method(nullary('.'))
def select(self, context=None):
if context is None:
raise ElementPathMissingContextError("Context required to evaluate `.`")
self.missing_context()
elif context.item is not None:
yield context.item
elif is_document_node(context.root):
@ -493,8 +493,7 @@ def select(self, context=None):
@method(nullary('..'))
def select(self, context=None):
if context is None:
raise ElementPathMissingContextError("Context required to evaluate `..`")
self.missing_context()
else:
try:
parent = context.parent_map[context.item]
@ -833,7 +832,7 @@ def nud(self):
@method(axis('attribute'))
def select(self, context=None):
if context is None:
raise ElementPathMissingContextError("Context required to evaluate `@`")
self.missing_context()
for _ in context.iter_attributes():
for result in self[0].select(context):

View File

@ -19,7 +19,7 @@ import math
import operator
from .compat import MutableSequence, urlparse
from .exceptions import ElementPathError, ElementPathTypeError, ElementPathMissingContextError
from .exceptions import ElementPathError, ElementPathTypeError, MissingContextError
from .namespaces import XSD_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE, XPATH_2_DEFAULT_NAMESPACES, \
XSD_NOTATION, XSD_ANY_ATOMIC_TYPE, get_namespace, qname_to_prefixed, prefixed_to_qname
from .datatypes import XSD_BUILTIN_TYPES
@ -64,7 +64,7 @@ class XPath2Parser(XPath1Parser):
# Value comparison operators
'eq', 'ne', 'lt', 'le', 'gt', 'ge',
# Node comparison operators
'is', '<<', '>>',
@ -258,7 +258,7 @@ class XPath2Parser(XPath1Parser):
raise self.error('FORG0006', str(err))
def cast(value):
raise NotImplemented
raise NotImplementedError
pattern = r'\b%s(?=\s*\(|\s*\(\:.*\:\)\()' % symbol
token_class = cls.register(symbol, pattern=pattern, label='constructor', lbp=bp, rbp=bp,
@ -282,7 +282,7 @@ class XPath2Parser(XPath1Parser):
try:
self_.value = self_.evaluate() # Static context evaluation
except ElementPathMissingContextError:
except MissingContextError:
self_.value = None
return self_
@ -314,7 +314,7 @@ class XPath2Parser(XPath1Parser):
def next_is_path_step_token(self):
return self.next_token.label in ('axis', 'function') or self.next_token.symbol in {
'(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', '..', '.', '(', '{'
'(integer)', '(string)', '(float)', '(decimal)', '(name)', '*', '@', '..', '.', '(', '{'
}
def next_is_sequence_type_token(self):
@ -334,7 +334,7 @@ class XPath2Parser(XPath1Parser):
context = None if self.schema is None else self.schema.get_context()
try:
root_token.evaluate(context) # Static context evaluation
except ElementPathMissingContextError:
except MissingContextError:
pass
return root_token

View File

@ -99,5 +99,3 @@ class Selector(object):
"""
context = XPathContext(root)
return self.root_token.select(context)
# 45-48, 74, 81

View File

@ -15,7 +15,7 @@ with open("README.rst") as readme:
setup(
name='elementpath',
version='1.1.7',
version='1.1.8',
packages=['elementpath'],
author='Davide Brunato',
author_email='brunato@sissa.it',

View File

@ -619,7 +619,10 @@ class TimezoneTypeTest(unittest.TestCase):
self.assertNotEqual(Timezone.fromstring('+05:00'), Timezone.fromstring('+06:00'))
def test_hashing(self):
self.assertEqual(hash(Timezone.fromstring('+05:00')), 1289844826723787395)
if sys.version_info < (3, 8):
self.assertEqual(hash(Timezone.fromstring('+05:00')), 1289844826723787395)
else:
self.assertEqual(hash(Timezone.fromstring('+05:00')), 7009945331308913293)
if __name__ == '__main__':

View File

@ -10,9 +10,13 @@
# @author Davide Brunato <brunato@sissa.it>
#
import unittest
import lxml.etree
try:
import lxml.etree as lxml_etree
except ImportError:
lxml_etree = None
from elementpath import *
from elementpath.compat import PY3
from elementpath.namespaces import XML_LANG, XSD_NAMESPACE
try:
@ -231,17 +235,17 @@ class XPath2ParserXMLSchemaTest(test_xpath2_parser.XPath2ParserTest):
def test_cast_expression(self):
self.check_value("5 cast as xs:integer", 5)
self.check_value("'5' cast as xs:integer", 5)
self.check_value("'hello' cast as xs:integer", ElementPathValueError)
self.check_value("('5', '6') cast as xs:integer", ElementPathTypeError)
self.check_value("() cast as xs:integer", ElementPathValueError)
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?", [])
self.check_value('"1" cast as xs:boolean', True)
self.check_value('"0" cast as xs:boolean', False)
@unittest.skipIf(xmlschema is None, "xmlschema library >= v1.0.7 required.")
@unittest.skipIf(xmlschema is None or lxml_etree is None, "both xmlschema and lxml required")
class LxmlXPath2ParserXMLSchemaTest(XPath2ParserXMLSchemaTest):
etree = lxml.etree
etree = lxml_etree
if __name__ == '__main__':

View File

@ -28,7 +28,11 @@ import pickle
from decimal import Decimal
from collections import namedtuple
from xml.etree import ElementTree
import lxml.etree
try:
import lxml.etree as lxml_etree
except ImportError:
lxml_etree = None
from elementpath import *
from elementpath.namespaces import XML_NAMESPACE, XSD_NAMESPACE, XSI_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE
@ -206,16 +210,16 @@ class XPath1ParserTest(unittest.TestCase):
# Wrong XPath expression checker shortcuts
def wrong_syntax(self, path):
self.assertRaises(ElementPathSyntaxError, self.parser.parse, path)
self.assertRaises(SyntaxError, self.parser.parse, path)
def wrong_value(self, path):
self.assertRaises(ElementPathValueError, self.parser.parse, path)
self.assertRaises(ValueError, self.parser.parse, path)
def wrong_type(self, path):
self.assertRaises(ElementPathTypeError, self.parser.parse, path)
self.assertRaises(TypeError, self.parser.parse, path)
def wrong_name(self, path):
self.assertRaises(ElementPathNameError, self.parser.parse, path)
self.assertRaises(NameError, self.parser.parse, path)
#
# Test methods
@ -870,7 +874,7 @@ class XPath1ParserTest(unittest.TestCase):
context = XPathContext(root, variables={'alpha': 10, 'id': '19273222'})
self.check_value("$alpha", None) # Do not raise if the dynamic context is None
self.check_value("$alpha", 10, context=context)
self.check_value("$beta", ElementPathNameError, context=context)
self.check_value("$beta", NameError, context=context)
self.check_value("$id", '19273222', context=context)
self.wrong_syntax("$id()")
@ -897,7 +901,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector('/A/.', root, [root])
self.check_selector('/A/B1/.', root, [root[0]])
self.check_selector('/A/B1/././.', root, [root[0]])
self.check_selector('1/.', root, ElementPathTypeError)
self.check_selector('1/.', root, TypeError)
def test_self_axis(self):
root = self.etree.XML('<A>A text<B1>B1 text</B1><B2/><B3>B3 text</B3></A>')
@ -1062,8 +1066,9 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("name(B1)", root, '', namespaces={'': "http://xpath.test/ns"})
@unittest.skipIf(lxml_etree is None, "The lxml library is not installed")
class LxmlXPath1ParserTest(XPath1ParserTest):
etree = lxml.etree
etree = lxml_etree
def check_selector(self, path, root, expected, namespaces=None, **kwargs):
"""Check using the selector API (the *select* function of the package)."""

View File

@ -23,10 +23,15 @@
import unittest
import datetime
import io
import locale
import math
import time
from decimal import Decimal
import lxml.etree
try:
import lxml.etree as lxml_etree
except ImportError:
lxml_etree = None
from elementpath import *
from elementpath.namespaces import XSI_NAMESPACE
@ -204,7 +209,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
def test_boolean_functions2(self):
root = self.etree.XML('<A><B1/><B2/><B3/></A>')
self.check_selector("boolean(/A)", root, True)
self.check_selector("boolean((-10, 35))", root, ElementPathTypeError) # Sequence with two numeric values
self.check_selector("boolean((-10, 35))", root, TypeError) # Sequence with two numeric values
self.check_selector("boolean((/A, 35))", root, True)
def test_numerical_expressions2(self):
@ -350,7 +355,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
self.check_value(u"fn:compare('Strasse', 'Straße', 'de_DE')", -1)
self.check_value(u"fn:compare('Strasse', 'Straße', 'deutsch')", -1)
with self.assertRaises(ElementPathLocaleError):
with self.assertRaises(locale.Error):
self.check_value(u"fn:compare('Strasse', 'Straße', 'invalid_collation')")
self.wrong_type(u"fn:compare('Strasse', 111)")
@ -1113,7 +1118,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
'<B2 /><B3>simple text</B3></A>' % XSI_NAMESPACE)
self.check_selector("node-name(.)", root, 'A')
self.check_selector("node-name(/A/B1)", root, 'B1')
self.check_selector("node-name(/A/*)", root, ElementPathTypeError) # Not allowed more than one item!
self.check_selector("node-name(/A/*)", root, TypeError) # Not allowed more than one item!
self.check_selector("nilled(./B1/C1)", root, False)
self.check_selector("nilled(./B1/C2)", root, True)
@ -1185,7 +1190,7 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
)
self.check_selector(
'/transactions/purchase[parcel="10-639"] >> /transactions/sale[parcel="33-870"]',
root, ElementPathTypeError
root, TypeError
)
def test_adjust_datetime_to_timezone_function(self):
@ -1278,8 +1283,9 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
self.check_value('fn:concat($unknown, fn:lower-case(10))', TypeError)
@unittest.skipIf(lxml_etree is None, "The lxml library is not installed")
class LxmlXPath2ParserTest(XPath2ParserTest):
etree = lxml.etree
etree = lxml_etree
if __name__ == '__main__':

32
tox.ini
View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = py27, py34, py35, py36, py37
envlist = py27, py35, py36, py37, py38, docs, flake8, coverage
skip_missing_interpreters = true
toxworkdir = {homedir}/.tox/elementpath
@ -12,20 +12,34 @@ toxworkdir = {homedir}/.tox/elementpath
deps =
lxml
xmlschema~=1.0.9
docs: Sphinx
flake8: flake8
coverage: coverage
commands = python -m unittest
whitelist_externals = make
[testenv:py27]
deps =
lxml
xmlschema~=1.0.9
commands = python tests/test_elementpath.py
[testenv:py37]
[testenv:py38]
deps = xmlschema~=1.0.9
commands = python -m unittest
[testenv:docs]
commands =
make -C doc html
make -C doc latexpdf
make -C doc doctest
[flake8]
max-line-length = 119
[testenv:flake8]
commands =
flake8 --ignore=F401,F811,F821 elementpath
[testenv:coverage]
commands =
coverage run -p setup.py test -q
coverage combine
coverage report -m
deps =
lxml
xmlschema~=1.0.9
coverage