Fix function conversion rules in XPathToken.get_argument()

- Added type proxy classes for XSD numeric and datetime data
  - Added tests for 'mod' operator token
This commit is contained in:
Davide Brunato 2019-08-07 12:21:10 +02:00
parent 9994139e17
commit 5ae6d0f0ff
6 changed files with 209 additions and 55 deletions

View File

@ -2,8 +2,12 @@
CHANGELOG
*********
`v1.1.9`_ (TBD)
`v1.2.0`_ (TBD)
===============
* Added special XSD datatypes
* Better handling of schema contexts
* Added validators for numeric types
* Fixed function conversion rules
* Added tests for uncovered code
`v1.1.8`_ (2019-05-20)

View File

@ -1054,6 +1054,89 @@ LANGUAGE_CODE_PATTERN = re.compile(r'^([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{
WRONG_ESCAPE_PATTERN = re.compile(r'%(?![a-eA-E\d]{2})')
class TypeProxyMeta(type):
"""
A metaclass for creating type proxy classes, that can be used for instance
and subclass checking and for building instances of related types. A type
proxy class has to implement three methods as concrete class/static methods.
"""
def __instancecheck__(cls, instance):
return cls.instance_check(instance)
def __subclasscheck__(cls, subclass):
return cls.subclass_check(subclass)
def __call__(cls, *args, **kwargs):
return cls.instance_build(*args, **kwargs)
def instance_check(cls, instance):
"""Checks the if the argument is an instance of one of related types."""
raise NotImplementedError
def subclass_check(cls, subclass):
"""Checks the if the argument is a subclass of one of related types."""
raise NotImplementedError
def instance_build(cls, *args, **kwargs):
"""Builds an instance belonging to one of related types."""
raise NotImplementedError
@add_metaclass(TypeProxyMeta)
class NumericTypeProxy(object):
"""
A type proxy class for xs:numeric related types (xs:float, xs:decimal and
derived types). Builds xs:float instances.
"""
@staticmethod
def instance_check(other):
return isinstance(other, (int, float, decimal.Decimal)) and not isinstance(other, bool)
@staticmethod
def subclass_check(subclass):
if issubclass(subclass, bool):
return False
return issubclass(subclass, int) or issubclass(subclass, float) or issubclass(subclass, decimal.Decimal)
@staticmethod
def instance_build(x=0):
return float(x)
@add_metaclass(TypeProxyMeta)
class ArithmeticTypeProxy(object):
"""
A type proxy class for XSD types related to arithmetic operators, including
types related to xs:numeric and datetime or duration types. Builds xs:float
instances.
"""
@staticmethod
def instance_check(other):
return isinstance(other, (int, float, decimal.Decimal, AbstractDateTime, Duration)) \
and not isinstance(other, bool)
@staticmethod
def subclass_check(subclass):
if issubclass(subclass, bool):
return False
return issubclass(subclass, int) or issubclass(subclass, float) or \
issubclass(subclass, decimal.Decimal) or issubclass(subclass, Duration) \
or issubclass(subclass, AbstractDateTime)
@staticmethod
def instance_build(x=0):
return float(x)
def decimal_validator(x):
return isinstance(x, (int, decimal.Decimal)) and not isinstance(x, bool)
def integer_validator(x):
return isinstance(x, int) and not isinstance(x, bool)
def base64_binary_validator(x):
if not isinstance(x, string_base_type) or NOT_BASE64_BINARY_PATTERN.match(x) is None:
return False
@ -1092,7 +1175,7 @@ XSD_BUILTIN_TYPES = {
value=' alpha\t'
),
'decimal': XsdBuiltin(
lambda x: isinstance(x, (int, float, decimal.Decimal)) and not isinstance(x, bool),
decimal_validator,
value=decimal.Decimal('1.0')
),
'double': XsdBuiltin(
@ -1208,55 +1291,55 @@ XSD_BUILTIN_TYPES = {
value='2000-01-01T12:00:00+01:00'
),
'integer': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool),
integer_validator,
value=1
),
'long': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (-2**63 <= x < 2**63),
lambda x: integer_validator(x) and (-2**63 <= x < 2**63),
value=1
),
'int': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (-2**31 <= x < 2**31),
lambda x: integer_validator(x) and (-2**31 <= x < 2**31),
value=1
),
'short': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (-2**15 <= x < 2**15),
lambda x: integer_validator(x) and (-2**15 <= x < 2**15),
value=1
),
'byte': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (-2**7 <= x < 2**7),
lambda x: integer_validator(x) and (-2**7 <= x < 2**7),
value=1
),
'positiveInteger': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and x > 0,
lambda x: integer_validator(x) and x > 0,
value=1
),
'negativeInteger': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and x < 0,
lambda x: integer_validator(x) and x < 0,
value=-1
),
'nonPositiveInteger': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and x <= 0,
lambda x: integer_validator(x) and x <= 0,
value=0
),
'nonNegativeInteger': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and x >= 0,
lambda x: integer_validator(x) and x >= 0,
value=0
),
'unsignedLong': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (0 <= x < 2**64),
lambda x: integer_validator(x) and (0 <= x < 2**64),
value=1
),
'unsignedInt': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (0 <= x < 2**32),
lambda x: integer_validator(x) and (0 <= x < 2**32),
value=1
),
'unsignedShort': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (0 <= x < 2**16),
lambda x: integer_validator(x) and (0 <= x < 2**16),
value=1
),
'unsignedByte': XsdBuiltin(
lambda x: isinstance(x, int) and not isinstance(x, bool) and (0 <= x < 2**8),
lambda x: integer_validator(x) and (0 <= x < 2**8),
value=1
),
'boolean': XsdBuiltin(

View File

@ -13,9 +13,9 @@ import math
import decimal
from .compat import PY3, string_base_type
from .exceptions import ElementPathSyntaxError, ElementPathTypeError, \
ElementPathNameError, MissingContextError
from .datatypes import UntypedAtomic, DayTimeDuration, YearMonthDuration, XSD_BUILTIN_TYPES
from .exceptions import ElementPathSyntaxError, ElementPathNameError, MissingContextError
from .datatypes import UntypedAtomic, DayTimeDuration, YearMonthDuration, \
NumericTypeProxy, XSD_BUILTIN_TYPES
from .xpath_context import XPathSchemaContext
from .tdop_parser import Parser, MultiLabel
from .namespaces import XML_ID, XML_LANG, XPATH_1_DEFAULT_NAMESPACES, \
@ -560,15 +560,28 @@ def evaluate(self, context=None):
if not self:
return
elif len(self) == 1:
arg = self.get_argument(context, cls=NumericTypeProxy)
if arg is None:
return
try:
return +self[0].evaluate(context)
return +arg
except TypeError:
raise ElementPathTypeError("numeric values are required: %r." % self[:])
raise self.wrong_type("numeric value is required: %r" % arg)
else:
arg1 = self.get_argument(context)
arg2 = self.get_argument(context, index=1)
if arg1 is None or arg2 is None:
return
elif isinstance(arg1, string_base_type):
if isinstance(arg2, string_base_type):
raise self.wrong_type("unsupported operands %r and %r" % (arg1, arg2))
elif isinstance(arg2, NumericTypeProxy):
arg1 = float(arg1)
try:
return self[0].evaluate(context) + self[1].evaluate(context)
except TypeError as err:
raise ElementPathTypeError(str(err))
raise self.wrong_type(str(err))
@method(infix('-', bp=40))
@ -607,7 +620,13 @@ def evaluate(self, context=None):
@method(infix('mod', bp=45))
def evaluate(self, context=None):
return self[0].evaluate(context) % self[1].evaluate(context)
arg1 = self.get_argument(context, cls=NumericTypeProxy)
arg2 = self.get_argument(context, index=1, cls=NumericTypeProxy)
if arg1 is not None and arg2 is not None:
try:
return arg1 % arg2
except TypeError as err:
raise self.wrong_type(str(err))
###

View File

@ -21,14 +21,13 @@ for documents. Generic tuples are used for representing attributes and named-tup
"""
import locale
import contextlib
import decimal
from .compat import string_base_type
from .exceptions import xpath_error
from .namespaces import XQT_ERRORS_NAMESPACE
from .xpath_nodes import AttributeNode, is_etree_element, \
is_element_node, is_document_node, is_xpath_node, node_string_value
from .datatypes import UntypedAtomic, Timezone, DayTimeDuration, XSD_BUILTIN_TYPES
from .datatypes import UntypedAtomic, Timezone, DayTimeDuration, NumericTypeProxy, XSD_BUILTIN_TYPES
from .tdop_parser import Token
@ -167,7 +166,7 @@ class XPathToken(Token):
if self.parser.compatibility_mode:
if issubclass(cls, string_base_type):
return self.string_value(item)
elif issubclass(cls, float):
elif issubclass(cls, float) or cls is NumericTypeProxy:
return self.number_value(item)
if self.parser.version > '1.0':
@ -176,15 +175,19 @@ class XPathToken(Token):
return value
elif isinstance(value, UntypedAtomic):
try:
return str(value) if issubclass(cls, string_base_type) else cls(value)
if cls is NumericTypeProxy:
return float(value)
elif issubclass(cls, string_base_type):
return str(value)
else:
return cls(value)
except (TypeError, ValueError):
pass
elif issubclass(cls, float) and not isinstance(value, bool) \
and isinstance(value, (int, float, decimal.Decimal)):
elif issubclass(cls, float) and isinstance(value, NumericTypeProxy):
return self.number_value(value)
code = 'XPTY0004' if self.label == 'function' else 'FORG0006'
message = "the %s argument %r is not a %r instance"
message = "the %s argument %r is not an instance of %r"
raise self.error(code, message % (ordinal(index + 1), item, cls))
return item

View File

@ -16,10 +16,11 @@ import operator
import random
from decimal import Decimal
from calendar import isleap
from elementpath.datatypes import MONTH_DAYS, MONTH_DAYS_LEAP, days_from_common_era, months2days, \
DateTime, DateTime10, Date, Date10, Time, Timezone, Duration, DayTimeDuration, YearMonthDuration, \
UntypedAtomic, GregorianYear, GregorianYear10, GregorianYearMonth, GregorianYearMonth10, \
GregorianMonthDay, GregorianMonth, GregorianDay, AbstractDateTime, OrderedDateTime
from elementpath.datatypes import MONTH_DAYS, MONTH_DAYS_LEAP, days_from_common_era, \
months2days, DateTime, DateTime10, Date, Date10, Time, Timezone, Duration, \
DayTimeDuration, YearMonthDuration, UntypedAtomic, GregorianYear, GregorianYear10, \
GregorianYearMonth, GregorianYearMonth10, GregorianMonthDay, GregorianMonth, \
GregorianDay, AbstractDateTime, OrderedDateTime, NumericTypeProxy, ArithmeticTypeProxy
class UntypedAtomicTest(unittest.TestCase):
@ -632,5 +633,18 @@ class TimezoneTypeTest(unittest.TestCase):
self.assertEqual(hash(Timezone.fromstring('+05:00')), 7009945331308913293)
class TypeProxiesTest(unittest.TestCase):
def test_numeric_type_proxy(self):
self.assertIsInstance(10, NumericTypeProxy)
self.assertIsInstance(17.8, NumericTypeProxy)
self.assertIsInstance(Decimal('18.12'), NumericTypeProxy)
self.assertNotIsInstance(True, NumericTypeProxy)
self.assertNotIsInstance(Duration.fromstring('P1Y'), NumericTypeProxy)
def test_arithmetic_type_proxy(self):
self.assertIsInstance(10, ArithmeticTypeProxy)
if __name__ == '__main__':
unittest.main()

View File

@ -38,22 +38,25 @@ from elementpath import *
from elementpath.namespaces import XML_NAMESPACE, XSD_NAMESPACE, XSI_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE
XML_GENERIC_TEST = """<root>
XML_GENERIC_TEST = """
<root>
<a id="a_id">
<b>some content</b>
<c> space space \t .</c></a>
</root>"""
XML_DATA_TEST = """<values>
XML_DATA_TEST = """
<values>
<a>3.4</a>
<a>20</a>
<a>-10.1</a>
<b>alpha</b>
<c>true</c>
</values>
"""
<d>44</d>
</values>"""
# noinspection PyPropertyAccess
class XPath1ParserTest(unittest.TestCase):
namespaces = {
'xml': XML_NAMESPACE,
@ -466,7 +469,7 @@ class XPath1ParserTest(unittest.TestCase):
def test_string_function(self):
self.check_value("string(10.0)", '10.0')
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("string(())")
else:
self.check_value("string(())", '')
@ -483,7 +486,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//none[string-length(.) = 10]", root, [])
self.check_value('fn:string-length("Harp not on that string, madam; that is past.")', 45)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("string-length(())")
self.check_value("string-length(12345)", 5)
else:
@ -504,7 +507,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//c[normalize-space(.) = 'space space .']", root, [root[0][1]])
self.check_value('fn:normalize-space(" The wealthy curled darlings of our nation. ")',
'The wealthy curled darlings of our nation.')
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax('fn:normalize-space(())')
self.check_value("normalize-space(1000)", '1000')
self.check_value("normalize-space(true())", 'True')
@ -563,7 +566,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("substring('12345', 1.5, 2.6)", '234')
self.check_value("substring('12345', 0, 3)", '12')
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.check_value("substring('12345', 0 div 0, 3)", '')
self.check_value("substring('12345', 1, 0 div 0)", '')
self.check_value("substring('12345', -42, 1 div 0)", '12345')
@ -614,7 +617,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value('fn:starts-with("abracadabra", "a")', True)
self.check_value('fn:starts-with("abracadabra", "bra")', False)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("starts-with((), ())")
self.check_value("starts-with('1999', 19)", True)
else:
@ -644,7 +647,7 @@ class XPath1ParserTest(unittest.TestCase):
self.wrong_syntax("concat()")
self.wrong_syntax("concat()")
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("concat((), (), ())")
else:
self.check_value("concat((), (), ())", '')
@ -667,7 +670,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//b[contains(., ' -con')]", root, [])
self.check_selector("//none[contains(., ' -con')]", root, [])
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("contains((), ())")
self.check_value("contains('XPath', 20)", False)
else:
@ -693,7 +696,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//b[substring-before(., 'con') = 'some']", root, [])
self.check_selector("//none[substring-before(., 'con') = 'some']", root, [])
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.check_value("substring-before('2017-10-27', 10)", '2017-')
self.wrong_syntax("fn:substring-before((), ())")
else:
@ -724,7 +727,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//b[substring-after(., 'con') = 'content']", root, [])
self.check_selector("//none[substring-after(., 'con') = 'content']", root, [])
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("fn:substring-after((), ())")
else:
self.check_value('fn:substring-after("tattoo", "tat")', 'too')
@ -747,7 +750,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("boolean(' ')", True)
self.check_value("boolean('')", False)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("boolean(())")
else:
self.check_value("boolean(())", False)
@ -804,14 +807,42 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("8 - 5", 3)
self.check_value("-8 - 5", -13)
self.check_value("5 div 2", 2.5)
self.check_value("11 mod 3", 2)
self.check_value("4.5 mod 1.2", Decimal('0.9'))
self.check_value("1.23E2 mod 0.6E1", 3.0E0)
self.check_value("-3 * 7", -21)
self.check_value("9 - 1 + 6", 14)
self.check_value("(5 * 7) + 9", 44)
self.check_value("-3 * 7", -21)
def test_numerical_add_operator(self):
self.check_value("3 + 8", 11)
self.check_value("9 - 5.0", 4)
root = self.etree.XML(XML_DATA_TEST)
if self.parser.version == '1.0':
self.check_value("'9' - 5.0", 4)
self.check_selector("/values/a mod 2", root, [1.4])
self.check_value("/values/b mod 2", float('nan'), context=XPathContext(root))
else:
self.check_selector("/values/a mod 2", root, TypeError)
self.check_value("/values/b mod 2", TypeError, context=XPathContext(root))
self.check_selector("/values/d mod 3", root, [2])
def test_numerical_mod_operator(self):
self.check_value("11 mod 3", 2)
self.check_value("4.5 mod 1.2", Decimal('0.9'))
self.check_value("1.23E2 mod 0.6E1", 3.0E0)
root = self.etree.XML(XML_DATA_TEST)
if self.parser.version == '1.0':
self.check_selector("/values/a mod 2", root, [1.4])
self.check_value("/values/b mod 2", float('nan'), context=XPathContext(root))
else:
self.check_selector("/values/a mod 2", root, TypeError)
self.check_value("/values/b mod 2", TypeError, context=XPathContext(root))
self.check_selector("/values/d mod 3", root, [2])
def test_number_function(self):
root = self.etree.XML('<root>15</root>')
@ -823,7 +854,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("number('-11')", -11)
self.check_selector("number(9)", root, 9.0)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("number(())")
else:
self.check_value("number(())", float('nan'), context=XPathContext(root))
@ -837,7 +868,7 @@ class XPath1ParserTest(unittest.TestCase):
def test_sum_function(self):
root = self.etree.XML(XML_DATA_TEST)
self.check_value("sum($values)", 35)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("sum(())")
else:
self.check_value("sum(())", 0)
@ -851,7 +882,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("ceiling(-10.5)", -10)
self.check_selector("//a[ceiling(.) = 10]", root, [])
self.check_selector("//a[ceiling(.) = -10]", root, [root[2]])
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("ceiling(())")
else:
self.check_value("ceiling(())", [])
@ -865,7 +896,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("//a[floor(.) = 10]", root, [])
self.check_selector("//a[floor(.) = 20]", root, [root[1]])
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("floor(())")
self.check_selector("//ab[floor(.) = 10]", root, [])
else:
@ -877,7 +908,7 @@ class XPath1ParserTest(unittest.TestCase):
self.check_value("round(2.5)", 3)
self.check_value("round(2.4999)", 2)
self.check_value("round(-2.5)", -2)
if not isinstance(self.parser, XPath2Parser):
if self.parser.version == '1.0':
self.wrong_syntax("round(())")
else:
self.check_value("round(())", [])