Update tests and docs

- Add 2019 to copyright information
  - Update Shphinx docs about XPath 2 status
  - Tests splitted into several modules (all tests are runned as before
    by the script test_elementpath.py or by "python -m unitest")
This commit is contained in:
Davide Brunato 2019-01-12 10:28:44 +01:00
parent af65450764
commit c03e88906f
25 changed files with 2590 additions and 2459 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c), 2018, SISSA (Scuola Internazionale Superiore di Studi Avanzati -
Copyright (c), 2018-2019, SISSA (Scuola Internazionale Superiore di Studi Avanzati -
International School for Advanced Studies).
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -11,9 +11,6 @@ data structures, both for the standard ElementTree library and for the
For `lxml.etree <http://lxml.de>`_ this package can be useful for providing XPath 2.0 selectors,
because `lxml.etree <http://lxml.de>`_ already has it's own implementation of XPath 1.0.
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.
Installation and usage
======================

View File

@ -25,7 +25,7 @@ sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
project = 'elementpath'
copyright = '2018, Davide Brunato'
copyright = '2018-2019, SISSA (International School for Advanced Studies)'
author = 'Davide Brunato'
# The short X.Y version

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
@ -650,6 +650,14 @@ class Duration(object):
months = -months
seconds = -seconds - (days * 24 + hours) * 3600 - minutes * 60
if cls is DayTimeDuration:
if months:
raise ElementPathValueError('months must be 0 for %r.' % cls.__name__)
return cls(seconds=seconds)
elif cls is YearMonthDuration:
if seconds:
raise ElementPathValueError('seconds must be 0 for %r.' % cls.__name__)
return cls(months=months)
return cls(months=months, seconds=seconds)
@property
@ -705,10 +713,8 @@ class Duration(object):
class YearMonthDuration(Duration):
def __init__(self, months=0, seconds=0):
super(YearMonthDuration, self).__init__(months, seconds)
if self.seconds:
raise ElementPathValueError('seconds must be 0 for %r.' % self.__class__.__name__)
def __init__(self, months=0):
super(YearMonthDuration, self).__init__(months, 0)
def __repr__(self):
return '%s(months=%r)' % (self.__class__.__name__, self.months)
@ -743,10 +749,8 @@ class YearMonthDuration(Duration):
class DayTimeDuration(Duration):
def __init__(self, months=0, seconds=0):
super(DayTimeDuration, self).__init__(months, seconds)
if self.months:
raise ElementPathValueError('months must be 0 for %r.' % self.__class__.__name__)
def __init__(self, seconds=0):
super(DayTimeDuration, self).__init__(0, seconds)
@classmethod
def fromtimedelta(cls, td):

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

522
tests/test_datatypes.py Normal file
View File

@ -0,0 +1,522 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
import unittest
import sys
import datetime
import operator
from decimal import Decimal
from elementpath.datatypes import months2days, DateTime, DateTime10, Date, Date10, Time, Timezone, \
Duration, DayTimeDuration, YearMonthDuration, UntypedAtomic, GregorianYear, GregorianYear10, \
GregorianYearMonth, GregorianYearMonth10, GregorianMonthDay, GregorianMonth, GregorianDay, \
AbstractDateTime, OrderedDateTime
class UntypedAtomicTest(unittest.TestCase):
def test_eq(self):
self.assertTrue(UntypedAtomic(-10) == UntypedAtomic(-10))
self.assertTrue(UntypedAtomic(5.2) == UntypedAtomic(5.2))
self.assertTrue(UntypedAtomic('-6.09') == UntypedAtomic('-6.09'))
self.assertTrue(UntypedAtomic(Decimal('8.91')) == UntypedAtomic(Decimal('8.91')))
self.assertTrue(UntypedAtomic(False) == UntypedAtomic(False))
self.assertTrue(UntypedAtomic(-10) == -10)
self.assertTrue(-10 == UntypedAtomic(-10))
self.assertTrue('-10' == UntypedAtomic(-10))
self.assertTrue(bool(False) == UntypedAtomic(False))
self.assertTrue(Decimal('8.91') == UntypedAtomic(Decimal('8.91')))
self.assertTrue(UntypedAtomic(Decimal('8.91')) == Decimal('8.91'))
self.assertFalse(bool(False) == UntypedAtomic(10))
self.assertFalse(-10.9 == UntypedAtomic(-10))
self.assertFalse(UntypedAtomic(-10) == -11)
self.assertFalse(UntypedAtomic(-10.5) == UntypedAtomic(-10))
self.assertFalse(-10.5 == UntypedAtomic(-10))
self.assertFalse(-17 == UntypedAtomic(-17.3))
def test_ne(self):
self.assertTrue(UntypedAtomic(True) != UntypedAtomic(False))
self.assertTrue(UntypedAtomic(5.12) != UntypedAtomic(5.2))
self.assertTrue('29' != UntypedAtomic(5.2))
self.assertFalse('2.0' != UntypedAtomic('2.0'))
def test_lt(self):
self.assertTrue(UntypedAtomic(9.0) < UntypedAtomic(15))
self.assertTrue(False < UntypedAtomic(True))
self.assertTrue(UntypedAtomic('78') < 100.0)
self.assertFalse(UntypedAtomic('100.1') < 100.0)
def test_le(self):
self.assertTrue(UntypedAtomic(9.0) <= UntypedAtomic(15))
self.assertTrue(False <= UntypedAtomic(False))
self.assertTrue(UntypedAtomic('78') <= 100.0)
self.assertFalse(UntypedAtomic('100.001') <= 100.0)
def test_gt(self):
self.assertTrue(UntypedAtomic(25) > UntypedAtomic(15))
self.assertTrue(25 > UntypedAtomic(15))
self.assertTrue(UntypedAtomic(25) > 15)
self.assertTrue(UntypedAtomic(25) > '15')
def test_ge(self):
self.assertTrue(UntypedAtomic(25) >= UntypedAtomic(25))
self.assertFalse(25 >= UntypedAtomic(25.1))
def test_conversion(self):
self.assertEqual(str(UntypedAtomic(25.1)), '25.1')
self.assertEqual(int(UntypedAtomic(25.1)), 25)
self.assertEqual(float(UntypedAtomic(25.1)), 25.1)
self.assertEqual(bool(UntypedAtomic(True)), True)
if sys.version_info >= (3,):
self.assertEqual(str(UntypedAtomic(u'Joan Miró')), u'Joan Miró')
else:
self.assertEqual(unicode(UntypedAtomic(u'Joan Miró')), u'Joan Miró')
self.assertEqual(bytes(UntypedAtomic(u'Joan Miró')), b'Joan Mir\xc3\xb3')
def test_numerical_operators(self):
self.assertEqual(0.25 * UntypedAtomic(1000), 250)
self.assertEqual(1200 - UntypedAtomic(1000.0), 200.0)
self.assertEqual(UntypedAtomic(1000.0) - 250, 750.0)
self.assertEqual(UntypedAtomic('1000.0') - 250, 750.0)
self.assertEqual(UntypedAtomic('1000.0') - UntypedAtomic(250), 750.0)
self.assertEqual(UntypedAtomic(0.75) * UntypedAtomic(100), 75)
self.assertEqual(UntypedAtomic('0.75') * UntypedAtomic('100'), 75)
self.assertEqual(UntypedAtomic('9.0') / UntypedAtomic('3'), 3.0)
self.assertEqual(9.0 / UntypedAtomic('3'), 3.0)
self.assertEqual(UntypedAtomic('15') * UntypedAtomic('4'), 60)
class DateTimeTypesTest(unittest.TestCase):
def test_abstract_classes(self):
self.assertRaises(TypeError, AbstractDateTime)
self.assertRaises(TypeError, OrderedDateTime)
def test_init_fromstring(self):
self.assertIsInstance(DateTime.fromstring('2000-10-07T00:00:00'), DateTime)
self.assertIsInstance(DateTime.fromstring('-2000-10-07T00:00:00'), DateTime)
self.assertRaises(ValueError, DateTime.fromstring, '00-10-07')
self.assertIsInstance(Date.fromstring('2000-10-07'), Date)
self.assertIsInstance(Date.fromstring('-2000-10-07'), Date)
def test_datetime_repr(self):
dt = DateTime.fromstring('2000-10-07')
self.assertEqual(repr(dt), "DateTime(2000, 10, 7, 0, 0, 0)")
self.assertEqual(str(dt), '2000-10-07T00:00:00')
dt = DateTime.fromstring('-0100-04-13T23:59:59')
self.assertEqual(repr(dt), "DateTime(-101, 4, 13, 23, 59, 59)")
self.assertEqual(str(dt), '-0100-04-13T23:59:59')
dt = DateTime10.fromstring('-0100-04-13T10:30:00-04:00')
if sys.version_info >= (3, 7):
self.assertEqual(repr(dt), "DateTime10(-100, 4, 13, 10, 30, 0, "
"tzinfo=Timezone(datetime.timedelta(days=-1, seconds=72000)))")
else:
self.assertEqual(repr(dt), "DateTime10(-100, 4, 13, 10, 30, 0, "
"tzinfo=Timezone(datetime.timedelta(-1, 72000)))")
self.assertEqual(str(dt), '-0100-04-13T10:30:00-04:00')
def test_date_repr(self):
dt = Date.fromstring('2000-10-07')
self.assertEqual(repr(dt), "Date(2000, 10, 7)")
self.assertEqual(str(dt), '2000-10-07')
dt = Date.fromstring('-0100-04-13')
self.assertEqual(repr(dt), "Date(-101, 4, 13)")
self.assertEqual(str(dt), '-0100-04-13')
dt = Date10.fromstring('-0100-04-13')
self.assertEqual(repr(dt), "Date10(-100, 4, 13)")
self.assertEqual(str(dt), '-0100-04-13')
def test_gregorian_year_repr(self):
dt = GregorianYear.fromstring('1991')
self.assertEqual(repr(dt), "GregorianYear(1991)")
self.assertEqual(str(dt), '1991')
dt = GregorianYear.fromstring('0000')
self.assertEqual(repr(dt), "GregorianYear(-1)")
self.assertEqual(str(dt), '0000')
dt = GregorianYear10.fromstring('-0050')
self.assertEqual(repr(dt), "GregorianYear10(-50)")
self.assertEqual(str(dt), '-0050')
def test_gregorian_day_repr(self):
dt = GregorianDay.fromstring('---31')
self.assertEqual(repr(dt), "GregorianDay(31)")
self.assertEqual(str(dt), '---31')
dt = GregorianDay.fromstring('---05Z')
self.assertEqual(repr(dt), "GregorianDay(5, tzinfo=Timezone(datetime.timedelta(0)))")
self.assertEqual(str(dt), '---05Z')
def test_gregorian_month_repr(self):
dt = GregorianMonth.fromstring('--09')
self.assertEqual(repr(dt), "GregorianMonth(9)")
self.assertEqual(str(dt), '--09')
def test_gregorian_month_day_repr(self):
dt = GregorianMonthDay.fromstring('--07-23')
self.assertEqual(repr(dt), "GregorianMonthDay(7, 23)")
self.assertEqual(str(dt), '--07-23')
def test_gregorian_year_month_repr(self):
dt = GregorianYearMonth.fromstring('-1890-12')
self.assertEqual(repr(dt), "GregorianYearMonth(-1891, 12)")
self.assertEqual(str(dt), '-1890-12')
dt = GregorianYearMonth10.fromstring('-0050-04')
self.assertEqual(repr(dt), "GregorianYearMonth10(-50, 4)")
self.assertEqual(str(dt), '-0050-04')
def test_time_repr(self):
dt = Time.fromstring('20:40:13')
self.assertEqual(repr(dt), "Time(20, 40, 13)")
self.assertEqual(str(dt), '20:40:13')
dt = Time.fromstring('24:00:00')
self.assertEqual(repr(dt), "Time(0, 0, 0)")
self.assertEqual(str(dt), '00:00:00')
def test_eq_operator(self):
tz = Timezone.fromstring('-05:00')
mkdt = DateTime.fromstring
self.assertTrue(mkdt("2002-04-02T12:00:00-01:00") == mkdt("2002-04-02T17:00:00+04:00"))
self.assertFalse(mkdt("2002-04-02T12:00:00") == mkdt("2002-04-02T23:00:00+06:00"))
self.assertFalse(mkdt("2002-04-02T12:00:00") == mkdt("2002-04-02T17:00:00"))
self.assertTrue(mkdt("2002-04-02T12:00:00") == mkdt("2002-04-02T12:00:00"))
self.assertTrue(mkdt("2002-04-02T23:00:00-04:00") == mkdt("2002-04-03T02:00:00-01:00"))
self.assertTrue(mkdt("1999-12-31T24:00:00") == mkdt("1999-12-31T00:00:00"))
self.assertTrue(mkdt("2005-04-04T24:00:00") == mkdt("2005-04-04T00:00:00"))
self.assertTrue(mkdt("2002-04-02T12:00:00-01:00", tz) == mkdt("2002-04-02T17:00:00+04:00", tz))
self.assertTrue(mkdt("2002-04-02T12:00:00", tz) == mkdt("2002-04-02T23:00:00+06:00", tz))
self.assertFalse(mkdt("2002-04-02T12:00:00", tz) == mkdt("2002-04-02T17:00:00", tz))
self.assertTrue(mkdt("2002-04-02T12:00:00", tz) == mkdt("2002-04-02T12:00:00", tz))
self.assertTrue(mkdt("2002-04-02T23:00:00-04:00", tz) == mkdt("2002-04-03T02:00:00-01:00", tz))
self.assertTrue(mkdt("1999-12-31T24:00:00", tz) == mkdt("1999-12-31T00:00:00", tz))
self.assertTrue(mkdt("2005-04-04T24:00:00", tz) == mkdt("2005-04-04T00:00:00", tz))
self.assertFalse(mkdt("2005-04-04T24:00:00", tz) != mkdt("2005-04-04T00:00:00", tz))
self.assertTrue(DateTime.fromstring("-1000-01-01") == DateTime.fromstring("-1000-01-01"))
self.assertTrue(DateTime.fromstring("-10000-01-01") == DateTime.fromstring("-10000-01-01"))
self.assertFalse(DateTime.fromstring("20000-01-01") != DateTime.fromstring("20000-01-01"))
self.assertFalse(DateTime.fromstring("-10000-01-02") == DateTime.fromstring("-10000-01-01"))
def test_lt_operator(self):
mkdt = DateTime.fromstring
mkdate = Date.fromstring
self.assertTrue(mkdt("2002-04-02T12:00:00-01:00") < mkdt("2002-04-02T17:00:00-01:00"))
self.assertFalse(mkdt("2002-04-02T18:00:00-01:00") < mkdt("2002-04-02T17:00:00-01:00"))
self.assertTrue(mkdt("2002-04-02T18:00:00+02:00") < mkdt("2002-04-02T17:00:00Z"))
self.assertTrue(mkdt("2002-04-02T18:00:00+02:00") < mkdt("2002-04-03T00:00:00Z"))
self.assertTrue(mkdt("-2002-01-01T10:00:00") < mkdt("2001-01-01T17:00:00Z"))
self.assertFalse(mkdt("2002-01-01T10:00:00") < mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-2002-01-01T10:00:00") < mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-12002-01-01T10:00:00") < mkdt("-12001-01-01T17:00:00Z"))
self.assertFalse(mkdt("12002-01-01T10:00:00") < mkdt("12001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-10000-01-01T10:00:00Z") < mkdt("-10000-01-01T17:00:00Z"))
self.assertRaises(TypeError, operator.lt, mkdt("2002-04-02T18:00:00+02:00"), mkdate("2002-04-03"))
def test_le_operator(self):
mkdt = DateTime.fromstring
mkdate = Date.fromstring
self.assertTrue(mkdt("2002-04-02T12:00:00-01:00") <= mkdt("2002-04-02T12:00:00-01:00"))
self.assertFalse(mkdt("2002-04-02T18:00:00-01:00") <= mkdt("2002-04-02T17:00:00-01:00"))
self.assertTrue(mkdt("2002-04-02T18:00:00+01:00") <= mkdt("2002-04-02T17:00:00Z"))
self.assertTrue(mkdt("-2002-01-01T10:00:00") <= mkdt("2001-01-01T17:00:00Z"))
self.assertFalse(mkdt("2002-01-01T10:00:00") <= mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-2002-01-01T10:00:00") <= mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-10000-01-01T10:00:00Z") <= mkdt("-10000-01-01T10:00:00Z"))
self.assertTrue(mkdt("-190000-01-01T10:00:00Z") <= mkdt("0100-01-01T10:00:00Z"))
self.assertRaises(TypeError, operator.le, mkdt("2002-04-02T18:00:00+02:00"), mkdate("2002-04-03"))
def test_gt_operator(self):
mkdt = DateTime.fromstring
mkdate = Date.fromstring
self.assertFalse(mkdt("2002-04-02T12:00:00-01:00") > mkdt("2002-04-02T17:00:00-01:00"))
self.assertTrue(mkdt("2002-04-02T18:00:00-01:00") > mkdt("2002-04-02T17:00:00-01:00"))
self.assertFalse(mkdt("2002-04-02T18:00:00+02:00") > mkdt("2002-04-02T17:00:00Z"))
self.assertFalse(mkdt("2002-04-02T18:00:00+02:00") > mkdt("2002-04-03T00:00:00Z"))
self.assertTrue(mkdt("2002-01-01T10:00:00") > mkdt("-2001-01-01T17:00:00Z"))
self.assertFalse(mkdt("-2002-01-01T10:00:00") > mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("13567-04-18T10:00:00Z") > datetime.datetime.now())
self.assertFalse(mkdt("15032-11-12T23:17:59Z") > mkdt("15032-11-12T23:17:59Z"))
self.assertRaises(TypeError, operator.lt, mkdt("2002-04-02T18:00:00+02:00"), mkdate("2002-04-03"))
def test_ge_operator(self):
mkdt = DateTime.fromstring
mkdate = Date.fromstring
self.assertTrue(mkdt("2002-04-02T12:00:00-01:00") >= mkdt("2002-04-02T12:00:00-01:00"))
self.assertTrue(mkdt("2002-04-02T18:00:00-01:00") >= mkdt("2002-04-02T17:00:00-01:00"))
self.assertTrue(mkdt("2002-04-02T18:00:00+01:00") >= mkdt("2002-04-02T17:00:00Z"))
self.assertFalse(mkdt("-2002-01-01T10:00:00") >= mkdt("2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("2002-01-01T10:00:00") >= mkdt("-2001-01-01T17:00:00Z"))
self.assertFalse(mkdt("-2002-01-01T10:00:00") >= mkdt("-2001-01-01T17:00:00Z"))
self.assertTrue(mkdt("-3000-06-21T00:00:00Z") >= mkdt("-3000-06-21T00:00:00Z"))
self.assertFalse(mkdt("-3000-06-21T00:00:00Z") >= mkdt("-3000-06-21T01:00:00Z"))
self.assertTrue(mkdt("15032-11-12T23:17:59Z") >= mkdt("15032-11-12T23:17:59Z"))
self.assertRaises(TypeError, operator.le, mkdt("2002-04-02T18:00:00+02:00"), mkdate("2002-04-03"))
def test_months2days_function(self):
# self.assertEqual(months2days(-119, 1, 12 * 319), 116512)
self.assertEqual(months2days(200, 1, -12 * 320) - 1, -116877 - 2)
# 0000 BCE tests
self.assertEqual(months2days(0, 1, 12), 366)
self.assertEqual(months2days(0, 1, -12), -365)
self.assertEqual(months2days(1, 1, 12), 365)
self.assertEqual(months2days(1, 1, -12), -366)
def test_common_era_delta(self):
self.assertEqual(Date.fromstring("0001-01-01").common_era_delta, datetime.timedelta(days=0))
self.assertEqual(Date.fromstring("0001-02-01").common_era_delta, datetime.timedelta(days=31))
self.assertEqual(Date.fromstring("0001-03-01").common_era_delta, datetime.timedelta(days=59))
self.assertEqual(Date.fromstring("0001-06-01").common_era_delta, datetime.timedelta(days=151))
self.assertEqual(Date.fromstring("0001-06-03").common_era_delta, datetime.timedelta(days=153))
self.assertEqual(DateTime.fromstring("0001-06-03T20:00:00").common_era_delta,
datetime.timedelta(days=153, seconds=72000))
self.assertEqual(Date.fromstring("0002-01-01").common_era_delta, datetime.timedelta(days=365))
self.assertEqual(Date.fromstring("0002-02-01").common_era_delta, datetime.timedelta(days=396))
self.assertEqual(Date.fromstring("-0000-01-01").common_era_delta, datetime.timedelta(days=-366))
self.assertEqual(Date.fromstring("-0000-02-01").common_era_delta, datetime.timedelta(days=-335))
self.assertEqual(Date.fromstring("-0000-12-31").common_era_delta, datetime.timedelta(days=-1))
self.assertEqual(Date10.fromstring("-0001-01-01").common_era_delta, datetime.timedelta(days=-366))
self.assertEqual(Date10.fromstring("-0001-02-10").common_era_delta, datetime.timedelta(days=-326))
self.assertEqual(Date10.fromstring("-0001-12-31Z").common_era_delta, datetime.timedelta(days=-1))
self.assertEqual(Date10.fromstring("-0001-12-31-02:00").common_era_delta, datetime.timedelta(hours=-22))
self.assertEqual(Date10.fromstring("-0001-12-31+03:00").common_era_delta, datetime.timedelta(hours=-27))
def test_sub_operator(self):
date = Date.fromstring
date10 = Date10.fromstring
self.assertEqual(date("2002-04-02") - date("2002-04-01"), DayTimeDuration(seconds=86400))
self.assertEqual(date("-2002-04-02") - date("-2002-04-01"), DayTimeDuration(seconds=86400))
self.assertEqual(date("-0002-01-01") - date("-0001-12-31"), DayTimeDuration.fromstring('-P729D'))
self.assertEqual(date("-0101-01-01") - date("-0100-12-31"), DayTimeDuration.fromstring('-P729D'))
self.assertEqual(date("15032-11-12") - date("15032-11-11"), DayTimeDuration(seconds=86400))
self.assertEqual(date("-9999-11-12") - date("-9999-11-11"), DayTimeDuration(seconds=86400))
self.assertEqual(date("-9999-11-12") - date("-9999-11-12"), DayTimeDuration(seconds=0))
self.assertEqual(date10("-2001-04-02-02:00") - date10("-2001-04-01"), DayTimeDuration.fromstring('P1DT2H'))
class DurationTypesTest(unittest.TestCase):
def test_months2days_function(self):
# xs:duration ordering related tests
self.assertEqual(months2days(year=1696, month=9, months_delta=0), 0)
self.assertEqual(months2days(1696, 9, 1), 30)
self.assertEqual(months2days(1696, 9, 2), 61)
self.assertEqual(months2days(1696, 9, 3), 91)
self.assertEqual(months2days(1696, 9, 4), 122)
self.assertEqual(months2days(1696, 9, 5), 153)
self.assertEqual(months2days(1696, 9, 12), 365)
self.assertEqual(months2days(1696, 9, -1), -31)
self.assertEqual(months2days(1696, 9, -2), -62)
self.assertEqual(months2days(1696, 9, -12), -366)
self.assertEqual(months2days(1697, 2, 0), 0)
self.assertEqual(months2days(1697, 2, 1), 28)
self.assertEqual(months2days(1697, 2, 12), 365)
self.assertEqual(months2days(1697, 2, -1), -31)
self.assertEqual(months2days(1697, 2, -2), -62)
self.assertEqual(months2days(1697, 2, -3), -92)
self.assertEqual(months2days(1697, 2, -12), -366)
self.assertEqual(months2days(1697, 2, -14), -428)
self.assertEqual(months2days(1697, 2, -15), -458)
self.assertEqual(months2days(1903, 3, 0), 0)
self.assertEqual(months2days(1903, 3, 1), 31)
self.assertEqual(months2days(1903, 3, 2), 61)
self.assertEqual(months2days(1903, 3, 3), 92)
self.assertEqual(months2days(1903, 3, 4), 122)
self.assertEqual(months2days(1903, 3, 11), 366 - 29)
self.assertEqual(months2days(1903, 3, 12), 366)
self.assertEqual(months2days(1903, 3, -1), -28)
self.assertEqual(months2days(1903, 3, -2), -59)
self.assertEqual(months2days(1903, 3, -3), -90)
self.assertEqual(months2days(1903, 3, -12), -365)
self.assertEqual(months2days(1903, 7, 0), 0)
self.assertEqual(months2days(1903, 7, 1), 31)
self.assertEqual(months2days(1903, 7, 2), 62)
self.assertEqual(months2days(1903, 7, 3), 92)
self.assertEqual(months2days(1903, 7, 6), 184)
self.assertEqual(months2days(1903, 7, 12), 366)
self.assertEqual(months2days(1903, 7, -1), -30)
self.assertEqual(months2days(1903, 7, -2), -61)
self.assertEqual(months2days(1903, 7, -6), -181)
self.assertEqual(months2days(1903, 7, -12), -365)
# Extra tests
self.assertEqual(months2days(1900, 3, 0), 0)
self.assertEqual(months2days(1900, 3, 1), 31)
self.assertEqual(months2days(1900, 3, 24), 730)
self.assertEqual(months2days(1900, 3, -1), -28)
self.assertEqual(months2days(1900, 3, -24), -730)
self.assertEqual(months2days(1000, 4, 0), 0)
self.assertEqual(months2days(1000, 4, 1), 30)
self.assertEqual(months2days(1000, 4, 24), 730)
self.assertEqual(months2days(1000, 4, -1), -31)
self.assertEqual(months2days(1000, 4, -24), -730)
self.assertEqual(months2days(2001, 10, -12), -365)
self.assertEqual(months2days(2000, 10, -12), -366)
self.assertEqual(months2days(2000, 2, -12), -365)
self.assertEqual(months2days(2000, 3, -12), -366)
def test_init_fromstring(self):
self.assertIsInstance(Duration.fromstring('P1Y'), Duration)
self.assertIsInstance(Duration.fromstring('P1M'), Duration)
self.assertIsInstance(Duration.fromstring('P1D'), Duration)
self.assertIsInstance(Duration.fromstring('PT0H'), Duration)
self.assertIsInstance(Duration.fromstring('PT1M'), Duration)
self.assertIsInstance(Duration.fromstring('PT0.0S'), Duration)
self.assertRaises(ValueError, Duration.fromstring, 'P')
self.assertRaises(ValueError, Duration.fromstring, 'PT')
self.assertRaises(ValueError, Duration.fromstring, '1Y')
self.assertRaises(ValueError, Duration.fromstring, 'P1W1DT5H3M23.9S')
self.assertRaises(ValueError, Duration.fromstring, 'P1.5Y')
self.assertRaises(ValueError, Duration.fromstring, 'PT1.1H')
self.assertRaises(ValueError, Duration.fromstring, 'P1.0DT5H3M23.9S')
def test_as_string(self):
self.assertEqual(str(Duration.fromstring('P3Y1D')), 'P3Y1D')
self.assertEqual(str(Duration.fromstring('PT2M10.4S')), 'PT2M10.4S')
self.assertEqual(str(Duration.fromstring('PT2400H')), 'P100D')
self.assertEqual(str(Duration.fromstring('-P15M')), '-P1Y3M')
self.assertEqual(str(Duration.fromstring('-P809YT3H5M5S')), '-P809YT3H5M5S')
def test_eq(self):
self.assertEqual(Duration.fromstring('PT147.5S'), (0, 147.5))
self.assertEqual(Duration.fromstring('PT147.3S'), (0, Decimal("147.3")))
self.assertEqual(Duration.fromstring('PT2M10.4S'), (0, Decimal("130.4")))
self.assertEqual(Duration.fromstring('PT5H3M23.9S'), (0, Decimal("18203.9")))
self.assertEqual(Duration.fromstring('P1DT5H3M23.9S'), (0, Decimal("104603.9")))
self.assertEqual(Duration.fromstring('P31DT5H3M23.9S'), (0, Decimal("2696603.9")))
self.assertEqual(Duration.fromstring('P1Y1DT5H3M23.9S'), (12, Decimal("104603.9")))
self.assertEqual(Duration.fromstring('-P809YT3H5M5S'), (-9708, -11105))
self.assertEqual(Duration.fromstring('P15M'), (15, 0))
self.assertEqual(Duration.fromstring('P1Y'), (12, 0))
self.assertEqual(Duration.fromstring('P3Y1D'), (36, 3600 * 24))
self.assertEqual(Duration.fromstring('PT2400H'), (0, 8640000))
self.assertEqual(Duration.fromstring('PT4500M'), (0, 4500 * 60))
self.assertEqual(Duration.fromstring('PT4500M70S'), (0, 4500 * 60 + 70))
self.assertEqual(Duration.fromstring('PT5529615.3S'), (0, Decimal('5529615.3')))
def test_ne(self):
self.assertNotEqual(Duration.fromstring('PT147.3S'), None)
self.assertNotEqual(Duration.fromstring('PT147.3S'), (0, 147.3))
self.assertNotEqual(Duration.fromstring('P3Y1D'), (36, 3600 * 2))
self.assertNotEqual(Duration.fromstring('P3Y1D'), (36, 3600 * 24, 0))
self.assertNotEqual(Duration.fromstring('P3Y1D'), None)
def test_lt(self):
self.assertTrue(Duration(months=15) < Duration(months=16))
self.assertFalse(Duration(months=16) < Duration(months=16))
self.assertTrue(Duration(months=16) < Duration.fromstring('P16M1D'))
self.assertTrue(Duration(months=16) < Duration.fromstring('P16MT1H'))
self.assertTrue(Duration(months=16) < Duration.fromstring('P16MT1M'))
self.assertTrue(Duration(months=16) < Duration.fromstring('P16MT1S'))
self.assertFalse(Duration(months=16) < Duration.fromstring('P16MT0S'))
def test_le(self):
self.assertTrue(Duration(months=15) <= Duration(months=16))
self.assertTrue(Duration(months=16) <= Duration(16))
self.assertTrue(Duration(months=16) <= Duration.fromstring('P16M1D'))
self.assertTrue(Duration(months=16) <= Duration.fromstring('P16MT1H'))
self.assertTrue(Duration(months=16) <= Duration.fromstring('P16MT1M'))
self.assertTrue(Duration(months=16) <= Duration.fromstring('P16MT1S'))
self.assertTrue(Duration(months=16) <= Duration.fromstring('P16MT0S'))
def test_gt(self):
self.assertTrue(Duration(months=16) > Duration(15))
self.assertFalse(Duration(months=16) > Duration(16))
def test_ge(self):
self.assertTrue(Duration(16) >= Duration(15))
self.assertTrue(Duration(16) >= Duration(16))
self.assertTrue(Duration.fromstring('P1Y1DT1S') >= Duration.fromstring('P1Y1D'))
def test_incomparable_values(self):
self.assertFalse(Duration(1) < Duration.fromstring('P30D'))
self.assertFalse(Duration(1) <= Duration.fromstring('P30D'))
self.assertFalse(Duration(1) > Duration.fromstring('P30D'))
self.assertFalse(Duration(1) >= Duration.fromstring('P30D'))
def test_day_time_duration(self):
self.assertEqual(DayTimeDuration(300).seconds, 300)
def test_year_month_duration(self):
self.assertEqual(YearMonthDuration(10).months, 10)
class TimezoneTypeTest(unittest.TestCase):
def test_init_format(self):
self.assertEqual(Timezone.fromstring('Z').offset, datetime.timedelta(0))
self.assertEqual(Timezone.fromstring('00:00').offset, datetime.timedelta(0))
self.assertEqual(Timezone.fromstring('+00:00').offset, datetime.timedelta(0))
self.assertEqual(Timezone.fromstring('-00:00').offset, datetime.timedelta(0))
self.assertEqual(Timezone.fromstring('-0:0').offset, datetime.timedelta(0))
self.assertEqual(Timezone.fromstring('+05:15').offset, datetime.timedelta(hours=5, minutes=15))
self.assertEqual(Timezone.fromstring('-11:00').offset, datetime.timedelta(hours=-11))
self.assertEqual(Timezone.fromstring('+13:59').offset, datetime.timedelta(hours=13, minutes=59))
self.assertEqual(Timezone.fromstring('-13:59').offset, datetime.timedelta(hours=-13, minutes=-59))
self.assertEqual(Timezone.fromstring('+14:00').offset, datetime.timedelta(hours=14))
self.assertEqual(Timezone.fromstring('-14:00').offset, datetime.timedelta(hours=-14))
self.assertRaises(ValueError, Timezone.fromstring, '-15:00')
self.assertRaises(ValueError, Timezone.fromstring, '-14:01')
self.assertRaises(ValueError, Timezone.fromstring, '+14:01')
self.assertRaises(ValueError, Timezone.fromstring, '+10')
self.assertRaises(ValueError, Timezone.fromstring, '+10:00:00')
def test_init_timedelta(self):
td0 = datetime.timedelta(0)
td1 = datetime.timedelta(hours=5, minutes=15)
td2 = datetime.timedelta(hours=-14, minutes=0)
td3 = datetime.timedelta(hours=-14, minutes=-1)
self.assertEqual(Timezone(td0).offset, td0)
self.assertEqual(Timezone(td1).offset, td1)
self.assertEqual(Timezone(td2).offset, td2)
self.assertRaises(ValueError, Timezone, td3)
self.assertRaises(TypeError, Timezone, 0)
def test_as_string(self):
self.assertEqual(str(Timezone.fromstring('+05:00')), '+05:00')
self.assertEqual(str(Timezone.fromstring('-13:15')), '-13:15')
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018, SISSA (International School for Advanced Studies).
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present

125
tests/test_schema_proxy.py Normal file
View File

@ -0,0 +1,125 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
import unittest
import lxml.etree
from elementpath import *
from elementpath.namespaces import XML_LANG_QNAME
try:
# noinspection PyPackageRequirements
import xmlschema
except (ImportError, AttributeError):
xmlschema = None
try:
from tests import test_xpath2_parser
except ImportError:
# Python2 fallback
import test_xpath2_parser
@unittest.skipIf(xmlschema is None, "xmlschema library >= v1.0.7 required.")
class XPath2ParserXMLSchemaTest(test_xpath2_parser.XPath2ParserTest):
if xmlschema:
schema = XMLSchemaProxy(
schema=xmlschema.XMLSchema('''
<!-- Dummy schema, only for tests -->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" targetNamespace="http://xpath.test/ns">
<xs:element name="test_element" type="xs:string"/>
<xs:attribute name="test_attribute" type="xs:string"/>
</xs:schema>''')
)
else:
schema = None
def setUp(self):
self.parser = XPath2Parser(namespaces=self.namespaces, schema=self.schema, variables=self.variables)
def test_xmlschema_proxy(self):
context = XPathContext(root=self.etree.XML('<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"/>'))
self.wrong_name("schema-element(nil)")
self.wrong_name("schema-element(xs:string)")
self.check_value("schema-element(xs:complexType)", None)
self.check_value("schema-element(xs:schema)", context.item, context)
self.check_tree("schema-element(xs:group)", '(schema-element (: (xs) (group)))')
context.item = AttributeNode(XML_LANG_QNAME, 'en')
self.wrong_name("schema-attribute(nil)")
self.wrong_name("schema-attribute(xs:string)")
self.check_value("schema-attribute(xml:lang)", None)
self.check_value("schema-attribute(xml:lang)", context.item, context)
self.check_tree("schema-attribute(xsi:schemaLocation)", '(schema-attribute (: (xsi) (schemaLocation)))')
def test_instance_expression(self):
element = self.etree.Element('schema')
context = XPathContext(element)
# Test cases from https://www.w3.org/TR/xpath20/#id-instance-of
self.check_value("5 instance of xs:integer", True)
self.check_value("5 instance of xs:decimal", True)
self.check_value("9.0 instance of xs:integer", False if xmlschema.__version__ >= '1.0.8' else True)
self.check_value("(5, 6) instance of xs:integer+", True)
self.check_value(". instance of element()", True, context)
self.check_value("(5, 6) instance of xs:integer", False)
self.check_value("(5, 6) instance of xs:integer*", True)
self.check_value("(5, 6) instance of xs:integer?", False)
self.check_value("5 instance of empty-sequence()", False)
self.check_value("() instance of empty-sequence()", True)
def test_treat_expression(self):
element = self.etree.Element('schema')
context = XPathContext(element)
self.check_value("5 treat as xs:integer", [5])
# self.check_value("5 treat as xs:string", ElementPathTypeError) # FIXME: a bug of xmlschema!
self.check_value("5 treat as xs:decimal", [5])
self.check_value("(5, 6) treat as xs:integer+", [5, 6])
self.check_value(". treat as element()", [element], context)
self.check_value("(5, 6) treat as xs:integer", ElementPathTypeError)
self.check_value("(5, 6) treat as xs:integer*", [5, 6])
self.check_value("(5, 6) treat as xs:integer?", ElementPathTypeError)
self.check_value("5 treat as empty-sequence()", ElementPathTypeError)
self.check_value("() treat as empty-sequence()", [])
def test_castable_expression(self):
self.check_value("5 castable as xs:integer", True)
self.check_value("'5' castable as xs:integer", True)
self.check_value("'hello' castable as xs:integer", False)
self.check_value("('5', '6') castable as xs:integer", False)
self.check_value("() castable as xs:integer", False)
self.check_value("() castable as xs:integer?", True)
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("() 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.")
class LxmlXPath2ParserXMLSchemaTest(XPath2ParserXMLSchemaTest):
etree = lxml.etree
if __name__ == '__main__':
unittest.main()

734
tests/test_xpath1_parser.py Normal file
View File

@ -0,0 +1,734 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c), 2018-2019, SISSA (International School for Advanced Studies).
# All rights reserved.
# This file is distributed under the terms of the MIT License.
# See the file 'LICENSE' in the root directory of the present
# distribution, or http://opensource.org/licenses/MIT.
#
# @author Davide Brunato <brunato@sissa.it>
#
#
# Note: Many tests are built using the examples of the XPath standards,
# published by W3C under the W3C Document License.
#
# References:
# http://www.w3.org/TR/1999/REC-xpath-19991116/
# http://www.w3.org/TR/2010/REC-xpath20-20101214/
# http://www.w3.org/TR/2010/REC-xpath-functions-20101214/
# https://www.w3.org/Consortium/Legal/2015/doc-license
# https://www.w3.org/TR/charmod-norm/
#
import unittest
import sys
import io
import math
import pickle
from decimal import Decimal
from collections import namedtuple
from xml.etree import ElementTree
import lxml.etree
from elementpath import *
from elementpath.namespaces import XML_NAMESPACE, XSD_NAMESPACE, XSI_NAMESPACE, XPATH_FUNCTIONS_NAMESPACE
class XPath1ParserTest(unittest.TestCase):
namespaces = {
'xml': XML_NAMESPACE,
'xs': XSD_NAMESPACE,
'xsi': XSI_NAMESPACE,
'fn': XPATH_FUNCTIONS_NAMESPACE,
'eg': 'http://www.example.com/example',
}
variables = {'values': [10, 20, 5]}
etree = ElementTree
def setUp(self):
self.parser = XPath1Parser(namespaces=self.namespaces, variables=self.variables, strict=True)
self.token = XPath1Parser.symbol_table['(name)'](self.parser, 'test')
#
# Helper methods
def check_tokenizer(self, path, expected):
"""
Checks the list of lexemes generated by the parser tokenizer.
:param path: the XPath expression to be checked.
:param expected: a list with lexemes generated by the tokenizer.
"""
self.assertEqual([
lit or op or ref or unexpected
for lit, op, ref, unexpected in self.parser.__class__.tokenizer.findall(path)
], expected)
def check_token(self, symbol, expected_label=None, expected_str=None, expected_repr=None, value=None):
"""
Checks a token class of an XPath parser class. The instance of the token is created
using the value argument and than is checked against other optional arguments.
:param symbol: the string that identifies the token class in the parser's symbol table.
:param expected_label: the expected label for the token instance.
:param expected_str: the expected string conversion of the token instance.
:param expected_repr: the expected string representation of the token instance.
:param value: the value used to create the token instance.
"""
token = self.parser.symbol_table[symbol](self.parser, value)
self.assertEqual(token.symbol, symbol)
if expected_label is not None:
self.assertEqual(token.label, expected_label)
if expected_str is not None:
self.assertEqual(str(token), expected_str)
if expected_repr is not None:
self.assertEqual(repr(token), expected_repr)
def check_tree(self, path, expected):
"""
Checks the tree string representation of a parsed path.
:param path: an XPath expression.
:param expected: the expected result string.
"""
self.assertEqual(self.parser.parse(path).tree, expected)
def check_source(self, path, expected):
"""
Checks the source representation of a parsed path.
:param path: an XPath expression.
:param expected: the expected result string.
"""
self.assertEqual(self.parser.parse(path).source, expected)
def check_value(self, path, expected=None, context=None):
"""
Checks the result of the *evaluate* method with an XPath expression. The evaluation
is applied on the root token of the parsed XPath expression.
:param path: an XPath expression.
:param expected: the expected result. Can be a data instance to compare to the result, a type \
to be used to check the type of the result, a function that accepts the result as argument and \
returns a boolean value, an exception class that is raised by running the evaluate method.
:param context: an optional `XPathContext` instance to be passed to evaluate method.
"""
if context is not None:
context = context.copy()
root_token = self.parser.parse(path)
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected, root_token.evaluate, context)
elif not callable(expected):
self.assertEqual(root_token.evaluate(context), expected)
elif isinstance(expected, type):
value = root_token.evaluate(context)
self.assertTrue(isinstance(value, expected), "%r is not a %r instance." % (value, expected))
else:
self.assertTrue(expected(root_token.evaluate(context)))
def check_select(self, path, expected, context=None):
"""
Checks the materialized result of the *select* method with an XPath expression.
The selection is applied on the root token of the parsed XPath expression.
:param path: an XPath expression.
:param expected: the expected result. Can be a data instance to compare to the result, \
a function that accepts the result as argument and returns a boolean value, an exception \
class that is raised by running the evaluate method.
:param context: an optional `XPathContext` instance to be passed to evaluate method. If no \
context is provided the method is called with a dummy context.
"""
if context is None:
context = XPathContext(root=self.etree.Element(u'dummy_root'))
else:
context = context.copy()
root_token = self.parser.parse(path)
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected, root_token.select, context)
elif not callable(expected):
self.assertEqual(list(root_token.select(context)), expected)
else:
self.assertTrue(expected(list(root_token.parse(path).select(context))))
def check_selector(self, path, root, expected, namespaces=None, **kwargs):
"""
Checks the selector API, namely the *select* function at package level.
:param path: an XPath expression.
:param root: an Element or an ElementTree instance.
:param expected: the expected result. Can be a data instance to compare to the result, a type \
to be used to check the type of the result, a function that accepts the result as argument and \
returns a boolean value, an exception class that is raised by running the evaluate method.
:param namespaces: an optional mapping from prefixes to namespace URIs.
:param kwargs: other optional arguments for the parser class.
"""
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected, select, root, path, namespaces, self.parser.__class__, **kwargs)
else:
results = select(root, path, namespaces, self.parser.__class__, **kwargs)
if isinstance(expected, set):
self.assertEqual(set(results), expected)
elif not callable(expected):
self.assertEqual(results, expected)
elif isinstance(expected, type):
self.assertTrue(isinstance(results, expected))
else:
self.assertTrue(expected(results))
# Wrong XPath expression checker shortcuts
def wrong_syntax(self, path):
self.assertRaises(ElementPathSyntaxError, self.parser.parse, path)
def wrong_value(self, path):
self.assertRaises(ElementPathValueError, self.parser.parse, path)
def wrong_type(self, path):
self.assertRaises(ElementPathTypeError, self.parser.parse, path)
def wrong_name(self, path):
self.assertRaises(ElementPathNameError, self.parser.parse, path)
#
# Test methods
@unittest.skipIf(sys.version_info < (3,), "Python 2 pickling is not supported.")
def test_parser_pickling(self):
if getattr(self.parser, 'schema', None) is None:
obj = pickle.dumps(self.parser)
parser = pickle.loads(obj)
obj = pickle.dumps(self.parser.symbol_table)
symbol_table = pickle.loads(obj)
self.assertEqual(self.parser, parser)
self.assertEqual(self.parser.symbol_table, symbol_table)
def test_xpath_tokenizer(self):
# tests from the XPath specification
self.check_tokenizer("*", ['*'])
self.check_tokenizer("text()", ['text', '(', ')'])
self.check_tokenizer("@name", ['@', 'name'])
self.check_tokenizer("@*", ['@', '*'])
self.check_tokenizer("para[1]", ['para', '[', '1', ']'])
self.check_tokenizer("para[last()]", ['para', '[', 'last', '(', ')', ']'])
self.check_tokenizer("*/para", ['*', '/', 'para'])
self.check_tokenizer("/doc/chapter[5]/section[2]",
['/', 'doc', '/', 'chapter', '[', '5', ']', '/', 'section', '[', '2', ']'])
self.check_tokenizer("chapter//para", ['chapter', '//', 'para'])
self.check_tokenizer("//para", ['//', 'para'])
self.check_tokenizer("//olist/item", ['//', 'olist', '/', 'item'])
self.check_tokenizer(".", ['.'])
self.check_tokenizer(".//para", ['.', '//', 'para'])
self.check_tokenizer("..", ['..'])
self.check_tokenizer("../@lang", ['..', '/', '@', 'lang'])
self.check_tokenizer("chapter[title]", ['chapter', '[', 'title', ']'])
self.check_tokenizer("employee[@secretary and @assistant]",
['employee', '[', '@', 'secretary', '', 'and', '', '@', 'assistant', ']'])
# additional tests from Python XML etree test cases
self.check_tokenizer("{http://spam}egg", ['{', 'http', ':', '//', 'spam', '}', 'egg'])
self.check_tokenizer("./spam.egg", ['.', '/', 'spam.egg'])
self.check_tokenizer(".//spam:egg", ['.', '//', 'spam', ':', 'egg'])
# additional tests
self.check_tokenizer("substring-after()", ['substring-after', '(', ')'])
self.check_tokenizer("contains('XML','XM')", ['contains', '(', "'XML'", ',', "'XM'", ')'])
self.check_tokenizer("concat('XML', true(), 10)",
['concat', '(', "'XML'", ',', '', 'true', '(', ')', ',', '', '10', ')'])
self.check_tokenizer("concat('a', 'b', 'c')", ['concat', '(', "'a'", ',', '', "'b'", ',', '', "'c'", ')'])
self.check_tokenizer("_last()", ['_last', '(', ')'])
self.check_tokenizer("last ()", ['last', '', '(', ')'])
self.check_tokenizer('child::text()', ['child', '::', 'text', '(', ')'])
def test_tokens(self):
# Literals
self.check_token('(string)', 'literal', "'hello' string",
"_string_literal_token(value='hello')", 'hello')
self.check_token('(integer)', 'literal', "1999 integer",
"_integer_literal_token(value=1999)", 1999)
self.check_token('(float)', 'literal', "3.1415 float",
"_float_literal_token(value=3.1415)", 3.1415)
self.check_token('(decimal)', 'literal', "217.35 decimal",
"_decimal_literal_token(value=217.35)", 217.35)
self.check_token('(name)', 'literal', "'schema' name",
"_name_literal_token(value='schema')", 'schema')
# Axes
self.check_token('self', 'axis', "'self' axis", "_self_axis_token()")
self.check_token('child', 'axis', "'child' axis", "_child_axis_token()")
self.check_token('parent', 'axis', "'parent' axis", "_parent_axis_token()")
self.check_token('ancestor', 'axis', "'ancestor' axis", "_ancestor_axis_token()")
self.check_token('preceding', 'axis', "'preceding' axis", "_preceding_axis_token()")
self.check_token('descendant-or-self', 'axis', "'descendant-or-self' axis")
self.check_token('following-sibling', 'axis', "'following-sibling' axis")
self.check_token('preceding-sibling', 'axis', "'preceding-sibling' axis")
self.check_token('ancestor-or-self', 'axis', "'ancestor-or-self' axis")
self.check_token('descendant', 'axis', "'descendant' axis")
if self.parser.version == '1.0':
self.check_token('attribute', 'axis', "'attribute' axis")
self.check_token('following', 'axis', "'following' axis")
self.check_token('namespace', 'axis', "'namespace' axis")
# Functions
self.check_token('position', 'function', "'position' function", "_position_function_token()")
# Operators
self.check_token('and', 'operator', "'and' operator", "_and_operator_token()")
def test_token_tree(self):
self.check_tree('child::B1', '(child (B1))')
self.check_tree('A/B//C/D', '(/ (// (/ (A) (B)) (C)) (D))')
self.check_tree('child::*/child::B1', '(/ (child (*)) (child (B1)))')
self.check_tree('attribute::name="Galileo"', "(= (attribute (name)) ('Galileo'))")
self.check_tree('1 + 2 * 3', '(+ (1) (* (2) (3)))')
self.check_tree('(1 + 2) * 3', '(* (+ (1) (2)) (3))')
self.check_tree("false() and true()", '(and (false) (true))')
self.check_tree("false() or true()", '(or (false) (true))')
self.check_tree("./A/B[C][D]/E", '(/ ([ ([ (/ (/ (.) (A)) (B)) (C)) (D)) (E))')
self.check_tree("string(xml:lang)", '(string (: (xml) (lang)))')
def test_token_source(self):
self.check_source(' child ::B1', 'child::B1')
self.check_source('false()', 'false()')
self.check_source("concat('alpha', 'beta', 'gamma')", "concat('alpha', 'beta', 'gamma')")
self.check_source('1 +2 * 3 ', '1 + 2 * 3')
self.check_source('(1 + 2) * 3', '(1 + 2) * 3')
self.check_source(' xs : string ', 'xs:string')
self.check_source('attribute::name="Galileo"', "attribute::name = 'Galileo'")
def test_wrong_syntax(self):
self.wrong_syntax('')
self.wrong_syntax(" \n \n )")
self.wrong_syntax('child::1')
self.wrong_syntax("count(0, 1, 2)")
self.wrong_syntax("{}egg")
self.wrong_syntax("./*:*")
# Features tests
def test_references(self):
namespaces = {'tst': "http://xpath.test/ns"}
root = self.etree.XML("""
<A xmlns:tst="http://xpath.test/ns">
<tst:B1 b1="beta1"/>
<tst:B2/>
<tst:B3 b2="tst:beta2" b3="beta3"/>
</A>""")
# Prefix references
self.check_tree('xs:string', '(: (xs) (string))')
self.check_tree('string(xs:unknown)', '(string (: (xs) (unknown)))')
self.check_value("fn:true()", True)
self.check_selector("./tst:B1", root, [root[0]], namespaces=namespaces)
self.check_selector("./tst:*", root, root[:], namespaces=namespaces)
# Namespace wildcard works only for XPath > 1.0
if self.parser.version == '1.0':
self.check_selector("./*:B2", root, Exception, namespaces=namespaces)
else:
self.check_selector("./*:B2", root, [root[1]], namespaces=namespaces)
# QName URI references
self.parser.strict = False
self.check_tree('{%s}string' % XSD_NAMESPACE, "({ ('http://www.w3.org/2001/XMLSchema') (string))")
self.check_tree('string({%s}unknown)' % XSD_NAMESPACE,
"(string ({ ('http://www.w3.org/2001/XMLSchema') (unknown)))")
self.wrong_syntax("{%s" % XSD_NAMESPACE)
self.check_value("{%s}true()" % XPATH_FUNCTIONS_NAMESPACE, True)
self.parser.strict = True
self.wrong_syntax('{%s}string' % XSD_NAMESPACE)
if not hasattr(self.etree, 'LxmlError') or self.parser.version > '1.0':
# Do not test with XPath 1.0 on lxml.
self.check_selector("./{http://www.w3.org/2001/04/xmlenc#}EncryptedData", root, [], strict=False)
self.check_selector("./{http://xpath.test/ns}B1", root, [root[0]], strict=False)
self.check_selector("./{http://xpath.test/ns}*", root, root[:], strict=False)
def test_node_types(self):
document = self.etree.parse(io.StringIO(u'<A/>'))
element = self.etree.Element('schema')
attribute = 'id', '0212349350'
namespace = namedtuple('Namespace', 'prefix uri')('xs', 'http://www.w3.org/2001/XMLSchema')
comment = self.etree.Comment('nothing important')
pi = self.etree.ProcessingInstruction('action', 'nothing to do')
text = u'aldebaran'
context = XPathContext(element)
self.check_select("node()", [document.getroot()], context=XPathContext(document))
self.check_selector("node()", element, [])
context.item = attribute
self.check_select("self::node()", [attribute], context)
context.item = namespace
self.check_select("self::node()", [namespace], context)
context.item = comment
self.check_select("self::node()", [comment], context)
self.check_select("self::comment()", [comment], context)
context.item = pi
self.check_select("self::node()", [pi], context)
self.check_select("self::processing-instruction()", [pi], context)
context.item = text
self.check_select("self::node()", [text], context)
self.check_select("text()", [], context) # Selects the children
self.check_selector("node()", self.etree.XML('<author>Dickens</author>'), ['Dickens'])
self.check_selector("text()", self.etree.XML('<author>Dickens</author>'), ['Dickens'])
self.check_selector("//self::text()", self.etree.XML('<author>Dickens</author>'), ['Dickens'])
def test_node_set_id_function(self):
# XPath 1.0 id() function: https://www.w3.org/TR/1999/REC-xpath-19991116/#function-id
root = self.etree.XML('<A><B1 xml:id="foo"/><B2/><B3 xml:id="bar"/><B4 xml:id="baz"/></A>')
self.check_selector('id("foo")', root, [root[0]])
def test_node_set_functions(self):
root = self.etree.XML('<A><B1><C1/><C2/></B1><B2/><B3><C3/><C4/><C5/></B3></A>')
context = XPathContext(root, item=root[1], size=3, position=3)
self.check_value("position()", 0)
self.check_value("position()", 4, context=context)
self.check_value("position()<=2", True)
self.check_value("position()<=2", False, context=context)
self.check_value("position()=4", True, context=context)
self.check_value("position()=3", False, context=context)
self.check_value("last()", 0)
self.check_value("last()", 3, context=context)
self.check_value("last()-1", 2, context=context)
self.check_selector("name(.)", root, 'A')
self.check_selector("name(A)", root, '')
self.check_selector("local-name(A)", root, '')
self.check_selector("namespace-uri(A)", root, '')
self.check_selector("name(B2)", root, 'B2')
self.check_selector("local-name(B2)", root, 'B2')
self.check_selector("namespace-uri(B2)", root, '')
if self.parser.version <= '1.0':
self.check_selector("name(*)", root, 'B1')
root = self.etree.XML('<tst:A xmlns:tst="http://xpath.test/ns"><tst:B1/></tst:A>')
self.check_selector("name(.)", root, 'tst:A', namespaces={'tst': "http://xpath.test/ns"})
self.check_selector("local-name(.)", root, 'A')
self.check_selector("namespace-uri(.)", root, 'http://xpath.test/ns')
self.check_selector("name(tst:B1)", root, 'tst:B1', namespaces={'tst': "http://xpath.test/ns"})
self.check_selector("name(tst:B1)", root, 'tst:B1', namespaces={'tst': "http://xpath.test/ns", '': ''})
def test_string_functions(self):
self.check_value("string(10.0)", '10.0')
self.check_value("contains('XPath','XP')", True)
self.check_value("contains('XP','XPath')", False)
self.wrong_type("contains('XPath', 20)")
self.wrong_syntax("contains('XPath', 'XP', 20)")
self.check_value("concat('alpha', 'beta', 'gamma')", 'alphabetagamma')
self.wrong_type("concat('alpha', 10, 'gamma')")
self.wrong_syntax("concat()")
self.check_value("string-length('hello world')", 11)
self.check_value("string-length('')", 0)
self.check_value("normalize-space(' hello \t world ')", 'hello world')
self.check_value("starts-with('Hello World', 'Hello')", True)
self.check_value("starts-with('Hello World', 'hello')", False)
self.check_value("translate('hello world', 'hw', 'HW')", 'Hello World')
self.wrong_value("translate('hello world', 'hwx', 'HW')")
self.check_value("substring('Preem Palver', 1)", 'Preem Palver')
self.check_value("substring('Preem Palver', 2)", 'reem Palver')
self.check_value("substring('Preem Palver', 7)", 'Palver')
self.check_value("substring('Preem Palver', 1, 5)", 'Preem')
self.wrong_type("substring('Preem Palver', 'c', 5)")
self.wrong_type("substring('Preem Palver', 1, '5')")
self.check_value("substring-before('Wolfgang Amadeus Mozart', 'Wolfgang')", '')
self.check_value("substring-before('Wolfgang Amadeus Mozart', 'Amadeus')", 'Wolfgang ')
self.wrong_type("substring-before('2017-10-27', 10)")
self.check_value("substring-after('Wolfgang Amadeus Mozart', 'Amadeus ')", 'Mozart')
self.check_value("substring-after('Wolfgang Amadeus Mozart', 'Mozart')", '')
root = self.etree.XML('<ups-units>'
' <unit><power>40kW</power></unit>'
' <unit><power>20kW</power></unit>'
' <unit><power>30kW</power><model>XYZ</model></unit>'
'</ups-units>')
variables = {'ups1': root[0], 'ups2': root[1], 'ups3': root[2]}
self.check_selector('string($ups1/power)', root, '40kW', variables=variables)
def test_boolean_functions(self):
self.check_value("true()", True)
self.check_value("false()", False)
self.check_value("not(false())", True)
self.check_value("not(true())", False)
self.check_value("boolean(0)", False)
self.check_value("boolean(1)", True)
self.check_value("boolean(-1)", True)
self.check_value("boolean('hello!')", True)
self.check_value("boolean(' ')", True)
self.check_value("boolean('')", False)
self.wrong_syntax("boolean()") # Argument required
self.wrong_syntax("boolean(1, 5)") # Too much arguments
# From https://www.w3.org/TR/1999/REC-xpath-19991116/#section-Boolean-Functions
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="en"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<div xml:lang="en"><para/></div>'), True)
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="EN"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="en-us"/>'), True)
self.check_selector('lang("en")', self.etree.XML('<para xml:lang="it"/>'), False)
def test_logical_expressions(self):
self.check_value("false() and true()", False)
self.check_value("false() or true()", True)
self.check_value("true() or false()", True)
self.check_value("true() and true()", True)
self.check_value("1 and 0", False)
self.check_value("1 and 1", True)
self.check_value("1 and 'jupiter'", True)
self.check_value("0 and 'mars'", False)
self.check_value("1 and mars", False)
def test_comparison_operators(self):
self.check_value("0.05 = 0.05", True)
self.check_value("19.03 != 19.02999", True)
self.check_value("-1.0 = 1.0", False)
self.check_value("1 <= 2", True)
self.check_value("5 >= 9", False)
self.check_value("5 > 3", True)
self.check_value("5 < 20.0", True)
self.check_value("false() = 1", False)
self.check_value("0 = false()", True)
self.check_value("2 * 2 = 4", True)
root = self.etree.XML('<table>'
' <unit id="1"><cost>50</cost></unit>'
' <unit id="2"><cost>30</cost></unit>'
' <unit id="3"><cost>20</cost></unit>'
' <unit id="2"><cost>40</cost></unit>'
'</table>')
self.check_selector("/table/unit[2]/cost <= /table/unit[1]/cost", root, True)
self.check_selector("/table/unit[2]/cost > /table/unit[position()!=2]/cost", root, True)
self.check_selector("/table/unit[3]/cost > /table/unit[position()!=3]/cost", root, False)
self.check_selector(". = 'Dickens'", self.etree.XML('<author>Dickens</author>'), True)
def test_numerical_expressions(self):
self.check_value("9", 9)
self.check_value("-3", -3)
self.check_value("7.1", Decimal('7.1'))
self.check_value("0.45e3", 0.45e3)
self.check_value(" 7+5 ", 12)
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_number_functions(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C1/><C2/></B3></A>')
self.check_value("number(5.0)", 5.0)
self.check_value("number('text')", math.isnan)
self.check_value("number('-11')", -11)
self.check_selector("number(9)", root, 9.0)
self.check_value("sum($values)", 35)
# Test cases taken from https://www.w3.org/TR/xquery-operators/#numeric-value-functions
self.check_value("ceiling(10.5)", 11)
self.check_value("ceiling(-10.5)", -10)
self.check_value("floor(10.5)", 10)
self.check_value("floor(-10.5)", -11)
self.check_value("round(2.5)", 3)
self.check_value("round(2.4999)", 2)
self.check_value("round(-2.5)", -2)
def test_context_variables(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C1/><C2/></B3></A>')
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("$id", '19273222', context=context)
self.wrong_syntax("$id()")
def test_child_operator(self):
root = self.etree.XML('<A><B1><C1/></B1><B2/><B3><C1/><C2/></B3></A>')
self.check_selector('/', root, [])
self.check_selector('/B1', root, [])
self.check_selector('/A1', root, [])
self.check_selector('/A', root, [root])
self.check_selector('/A/B1', root, [root[0]])
self.check_selector('/A/*', root, [root[0], root[1], root[2]])
self.check_selector('/*/*', root, [root[0], root[1], root[2]])
self.check_selector('/A/B1/C1', root, [root[0][0]])
self.check_selector('/A/B1/*', root, [root[0][0]])
self.check_selector('/A/B3/*', root, [root[2][0], root[2][1]])
self.check_selector('child::*/child::C1', root, [root[0][0], root[2][0]])
self.check_selector('/A/child::B3', root, [root[2]])
self.check_selector('/A/child::C1', root, [])
def test_context_item_expression(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C1/><C2/></B3></A>')
self.check_selector('.', root, [root])
self.check_selector('/././.', root, [])
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)
def test_self_axis(self):
root = self.etree.XML('<A>A text<B1>B1 text</B1><B2/><B3>B3 text</B3></A>')
self.check_selector('self::node()', root, [root])
self.check_selector('self::text()', root, [])
def test_child_axis(self):
root = self.etree.XML('<A>A text<B1>B1 text</B1><B2/><B3>B3 text</B3></A>')
self.check_selector('child::B1', root, [root[0]])
self.check_selector('child::A', root, [])
self.check_selector('child::text()', root, ['A text'])
self.check_selector('child::node()', root, ['A text'] + root[:])
self.check_selector('child::*', root, root[:])
def test_descendant_axis(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C1/><C2/></B3></A>')
self.check_selector('descendant::node()', root, [e for e in root.iter()][1:])
self.check_selector('/descendant::node()', root, [e for e in root.iter()])
def test_descendant_or_self_axis(self):
root = self.etree.XML('<A><B1><C/></B1><B2/><B3><C/><C1/></B3></A>')
self.check_selector('//.', root, [e for e in root.iter()])
self.check_selector('/A//.', root, [e for e in root.iter()])
self.check_selector('//C1', root, [root[2][1]])
self.check_selector('//B2', root, [root[1]])
self.check_selector('//C', root, [root[0][0], root[2][0]])
self.check_selector('//*', root, [e for e in root.iter()])
self.check_selector('descendant-or-self::node()', root, [e for e in root.iter()])
self.check_selector('descendant-or-self::node()/.', root, [e for e in root.iter()])
def test_following_axis(self):
root = self.etree.XML('<A><B1><C1/></B1><B2/><B3><C1/><C2/></B3><B4><C1><D1/></C1></B4></A>')
self.check_selector('/A/B1/C1/following::*', root, [
root[1], root[2], root[2][0], root[2][1], root[3], root[3][0], root[3][0][0]
])
self.check_selector('/A/B1/following::C1', root, [root[2][0], root[3][0]])
def test_following_sibling_axis(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
self.check_selector('/A/B1/C1/following-sibling::*', root, [root[0][1], root[0][2]])
self.check_selector('/A/B2/C1/following-sibling::*', root, [root[1][1], root[1][2], root[1][3]])
self.check_selector('/A/B1/C1/following-sibling::C3', root, [root[0][2]])
def test_attribute_abbreviation_and_axis(self):
root = self.etree.XML('<A id="1" a="alpha"><B1 b1="beta1"/><B2/><B3 b2="beta2" b3="beta3"/></A>')
self.check_selector('/A/B1/attribute::*', root, ['beta1'])
self.check_selector('/A/B1/@*', root, ['beta1'])
self.check_selector('/A/B3/attribute::*', root, {'beta2', 'beta3'})
self.check_selector('/A/attribute::*', root, {'1', 'alpha'})
def test_namespace_axis(self):
root = self.etree.XML('<A xmlns:tst="http://xpath.test/ns"><tst:B1/></A>')
namespaces = list(self.parser.DEFAULT_NAMESPACES.items()) + [('tst', 'http://xpath.test/ns')]
self.check_selector('/A/namespace::*', root, expected=set(namespaces), namespaces=namespaces[-1:])
def test_parent_abbreviation_and_axis(self):
root = self.etree.XML('<A><B1><C1/></B1><B2/><B3><C1/><C2/></B3><B4><C3><D1/></C3></B4></A>')
self.check_selector('/A/*/C2/..', root, [root[2]])
self.check_selector('/A/*/*/..', root, [root[0], root[2], root[3]])
self.check_selector('//C2/..', root, [root[2]])
self.check_selector('/A/*/C2/parent::node()', root, [root[2]])
self.check_selector('/A/*/*/parent::node()', root, [root[0], root[2], root[3]])
self.check_selector('//C2/parent::node()', root, [root[2]])
def test_ancestor_axes(self):
root = self.etree.XML('<A><B1><C1/></B1><B2><C1/><D2><E1/><E2/></D2><C2/></B2><B3><C1><D1/></C1></B3></A>')
self.check_selector('/A/B3/C1/ancestor::*', root, [root, root[2]])
self.check_selector('/A/B4/C1/ancestor::*', root, [])
self.check_selector('/A/*/C1/ancestor::*', root, [root, root[0], root[1], root[2]])
self.check_selector('/A/*/C1/ancestor::B3', root, [root[2]])
self.check_selector('/A/B3/C1/ancestor-or-self::*', root, [root, root[2], root[2][0]])
self.check_selector('/A/*/C1/ancestor-or-self::*', root, [
root, root[0], root[0][0], root[1], root[1][0], root[2], root[2][0]
])
def test_preceding_axes(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
self.check_selector('/A/B1/C2/preceding::*', root, [root[0][0]])
self.check_selector('/A/B2/C4/preceding::*', root, [
root[0], root[0][0], root[0][1], root[0][2], root[1][0], root[1][1], root[1][2]
])
self.check_selector('/A/B1/C2/preceding-sibling::*', root, [root[0][0]])
self.check_selector('/A/B2/C4/preceding-sibling::*', root, [root[1][0], root[1][1], root[1][2]])
self.check_selector('/A/B1/C2/preceding-sibling::C3', root, [])
def test_default_axis(self):
"""Tests about when child:: default axis is applied."""
root = self.etree.XML('<root><a id="1">first<b/></a><a id="2">second</a></root>')
self.check_selector('/root/a/*', root, [root[0][0]])
self.check_selector('/root/a/node()', root, ['first', root[0][0], 'second'])
self.check_selector('/root/a/text()', root, ['first', 'second'])
self.check_selector('/root/a/attribute::*', root, ['1', '2'])
if self.parser.version > '1.0':
# Functions are not allowed after path step in XPath 1.0
self.check_selector('/root/a/attribute()', root, ['1', '2'])
self.check_selector('/root/a/element()', root, [root[0][0]])
self.check_selector('/root/a/name()', root, ['a', 'a'])
self.check_selector('/root/a/last()', root, [2, 2])
self.check_selector('/root/a/position()', root, [1, 2])
def test_predicate(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2></A>')
self.check_selector('/A/B1[C2]', root, [root[0]])
self.check_selector('/A/B1[1]', root, [root[0]])
self.check_selector('/A/B1[2]', root, [])
self.check_selector('/A/*[2]', root, [root[1]])
self.check_selector('/A/*[position()<2]', root, [root[0]])
self.check_selector('/A/*[last()-1]', root, [root[0]])
self.check_selector('/A/B2/*[position()>=2]', root, root[1][1:])
root = self.etree.XML("<bib><book><author>Asimov</author></book></bib>")
self.check_selector("book/author[. = 'Asimov']", root, [root[0][0]])
self.check_selector("book/author[. = 'Dickens']", root, [])
self.check_selector("book/author[text()='Asimov']", root, [root[0][0]])
root = self.etree.XML('<A><B1>hello</B1><B2/><B3> </B3></A>')
self.check_selector("/A/*[' ']", root, root[:])
self.check_selector("/A/*['']", root, [])
def test_union(self):
root = self.etree.XML('<A><B1><C1/><C2/><C3/></B1><B2><C1/><C2/><C3/><C4/></B2><B3/></A>')
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('<foo>bar</foo>')
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
self.check_selector('/*:foo', root, [root], namespaces={'': 'ns'}) # foo --> {ns}foo
root = self.etree.XML('<foo xmlns="ns">bar</foo>')
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):
etree = lxml.etree
def check_selector(self, path, root, expected, namespaces=None, **kwargs):
"""Check using the selector API (the *select* function of the package)."""
if isinstance(expected, type) and issubclass(expected, Exception):
self.assertRaises(expected, select, root, path, namespaces, self.parser.__class__, **kwargs)
else:
results = select(root, path, namespaces, self.parser.__class__, **kwargs)
variables = kwargs.get('variables', {})
if isinstance(expected, set):
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):
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))
else:
self.assertTrue(expected(results))
if __name__ == '__main__':
unittest.main()

1161
tests/test_xpath2_parser.py Normal file

File diff suppressed because it is too large Load Diff