Skip to content

Commit

Permalink
Add tests for coverage and update release notes
Browse files Browse the repository at this point in the history
  • Loading branch information
brunato committed Apr 23, 2020
1 parent dd4c42f commit c884949
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 34 deletions.
3 changes: 3 additions & 0 deletions elementpath/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
"""
XSD atomic datatypes for XPath. Includes a class for UntypedAtomic data and some
classes for XSD datetime and duration types.
TODO for v1.5: Create special wrapper classes for xs:double and other numerical
types for the properly working of 'instance of' tests.
"""
from abc import ABCMeta, abstractmethod
import operator
Expand Down
2 changes: 1 addition & 1 deletion elementpath/xpath1_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@ def select(self, context=None):
@method(axis('parent'))
def select(self, context=None):
if context is not None:
for _ in context.iter_parent(axis=self.symbol):
for _ in context.iter_parent():
yield from self[0].select(context)


Expand Down
54 changes: 32 additions & 22 deletions elementpath/xpath_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,18 +227,20 @@ def iter_attributes(self):

self.item, self.size, self.position, self.axis = status

def iter_children_or_self(self, item=None, child_axis=False):
"""Iterator for 'child' forward axis and '/' step."""
def iter_children_or_self(self, child_axis=False):
"""
Iterator for 'child' forward axis and '/' step.
:param child_axis: if set to `True` use the child axis anyway.
"""
if not child_axis and self.axis is not None:
yield self.item
return

status = self.item, self.size, self.position, self.axis
self.axis = 'child'

if item is not None:
self.item = item[0] if isinstance(item, TypedElement) else item
elif isinstance(self.item, TypedElement):
if isinstance(self.item, TypedElement):
self.item = self.item[0]

if self.item is None:
Expand All @@ -256,7 +258,7 @@ def iter_children_or_self(self, item=None, child_axis=False):

self.item, self.size, self.position, self.axis = status

def iter_parent(self, axis=None):
def iter_parent(self):
"""Iterator for 'parent' reverse axis and '..' shortcut."""
if isinstance(self.item, TypedElement):
parent = self.get_parent(self.item[0])
Expand All @@ -265,7 +267,7 @@ def iter_parent(self, axis=None):

if parent is not None:
status = self.item, self.size, self.position, self.axis
self.axis = axis
self.axis = 'parent'

self.item = parent
self.size = self.position = 1
Expand All @@ -276,24 +278,27 @@ def iter_parent(self, axis=None):
def iter_siblings(self, axis=None):
"""
Iterator for 'following-sibling' forward axis and 'preceding-sibling' reverse axis.
:param axis: the context axis, default is 'following-sibling'.
"""
if isinstance(self.item, TypedElement):
parent = self.get_parent(self.item[0])
item = self.item[0]
elif not is_etree_element(self.item) or callable(self.item.tag):
return
else:
parent = self.get_parent(self.item)
item = self.item

parent = self.get_parent(item)
if parent is None:
return

status = self.item, self.size, self.position, self.axis
self.axis = axis
self.axis = axis or 'following-sibling'

siblings = []
if axis == 'preceding-sibling':
for child in parent:
if child is self.item:
for child in parent: # pragma: no cover
if child is item:
break
siblings.append(child)

Expand All @@ -306,7 +311,7 @@ def iter_siblings(self, axis=None):
for child in parent:
if follows:
siblings.append(child)
elif self.item is child:
elif child is item:
follows = True

self.size = len(siblings)
Expand All @@ -318,6 +323,9 @@ def iter_siblings(self, axis=None):
def iter_descendants(self, item=None, axis=None):
"""
Iterator for 'descendant' and 'descendant-or-self' forward axes and '//' shortcut.
:param item: use another item instead of the context's item.
:param axis: the context axis, for default has no explicit axis.
"""
status = self.item, self.size, self.position, self.axis
self.axis = axis
Expand All @@ -334,7 +342,7 @@ def iter_descendants(self, item=None, axis=None):
elif not is_element_node(self.item):
return

if axis in ('descendant', 'following'):
if axis == 'descendant':
descendants = [x for x in etree_iter_nodes(self.item, with_root=False)]
else:
descendants = [x for x in etree_iter_nodes(self.item)]
Expand All @@ -345,14 +353,16 @@ def iter_descendants(self, item=None, axis=None):

self.item, self.size, self.position, self.axis = status

def iter_ancestors(self, item=None, axis=None):
"""Iterator for 'ancestor-or-self' and 'ancestor' reverse axes."""
def iter_ancestors(self, axis=None):
"""
Iterator for 'ancestor-or-self' and 'ancestor' reverse axes.
:param axis: the context axis, default is 'ancestor-or-self'.
"""
status = self.item, self.size, self.position, self.axis
self.axis = axis
self.axis = axis or 'ancestor-or-self'

if item is not None:
self.item = item[0] if isinstance(item, TypedElement) else item
elif isinstance(self.item, TypedElement):
if isinstance(self.item, TypedElement):
self.item = self.item[0]

ancestors = [self.item] if axis == 'ancestor-or-self' else []
Expand All @@ -361,7 +371,7 @@ def iter_ancestors(self, item=None, axis=None):
ancestors.append(parent)
parent = self.get_parent(parent)

self.size = self.position = len(ancestors)
self.size = self.position = len(ancestors) or 1
for self.item in reversed(ancestors):
yield self.item
self.position -= 1
Expand All @@ -371,7 +381,7 @@ def iter_ancestors(self, item=None, axis=None):
def iter_preceding(self):
"""Iterator for 'preceding' reverse axis."""
item = self.item[0] if isinstance(self.item, TypedElement) else self.item
if not is_etree_element(item) or item is self.root:
if not is_etree_element(item) or item is self.root or callable(item.tag):
return

status = self.item, self.size, self.position, self.axis
Expand Down
5 changes: 2 additions & 3 deletions elementpath/xpath_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,10 @@ def etree_iter_nodes(elem, with_root=True, with_attributes=False):
def etree_iter_strings(elem):
if isinstance(elem, TypedElement):
elem = elem.elem
elif callable(elem.tag):
yield elem.text
return

for e in elem.iter():
if callable(e.tag):
continue
if e.text is not None:
yield e.text
if e.tail is not None and e is not elem:
Expand Down
5 changes: 2 additions & 3 deletions publiccode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ description:
Python library that provides XPath 1.0/2.0 parsers and selectors for
ElementTree and lxml
longDescription: |
This is a library for Python 2.7/3.5+ that provides XPath 1.0 and 2.0
This is a library for Python 3.5+ that provides XPath 1.0 and 2.0
selectors for Python's ElementTree XML data structures, both for the
standard **ElementTree** library and for the **lxml** library. For lxml
this package can be useful for providing XPath 2.0 selectors, because lxml
Expand All @@ -55,8 +55,7 @@ description:
## Installation and usage
You can install the package with _pip_ in a Python 2.7 or Python 3.5+
environment:
You can install the package with _pip_ in a Python 3.5+ environment:
~~~~
Expand Down
16 changes: 16 additions & 0 deletions tests/test_xpath2_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ def test_datetime_constructor(self):
context.item = AttributeNode('a', 'true')
self.check_value('xs:dateTime(.)', ValueError, context=context)

context.item = DateTime(1969, 7, 20, 20, 18)
self.check_value('xs:dateTime(.)', DateTime(1969, 7, 20, 20, 18), context=context)

def test_time_constructor(self):
tz0 = None
tz1 = Timezone(datetime.timedelta(hours=5, minutes=24))
Expand Down Expand Up @@ -341,6 +344,9 @@ def test_date_constructor(self):
context = XPathContext(root)
self.check_value('xs:date(.)', Date10(2017, 10, 2), context=context)

context = XPathContext(root, item=Date10(2017, 10, 2))
self.check_value('xs:date(.)', Date10(2017, 10, 2), context=context)

def test_gregorian_day_constructor(self):
tz0 = None
tz1 = Timezone(datetime.timedelta(hours=5, minutes=24))
Expand Down Expand Up @@ -537,6 +543,16 @@ def test_base64_binary_constructor(self):
context.item = b'abcefghij'
self.check_value('xs:base64Binary(.)', b'YWJjZWZnaGlq\n', context=context)

def test_untyped_atomic_constructor(self):
self.check_value('xs:untypedAtomic(())', [])

root = self.etree.XML('<root>1999</root>')
context = XPathContext(root)
self.check_value('xs:untypedAtomic(.)', UntypedAtomic(1999), context=context)

context.item = UntypedAtomic('true')
self.check_value('xs:untypedAtomic(.)', UntypedAtomic(True), context=context)


@unittest.skipIf(lxml_etree is None, "The lxml library is not installed")
class LxmlXPath2ConstructorsTest(XPath2ConstructorsTest):
Expand Down
1 change: 0 additions & 1 deletion tests/test_xpath2_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,6 @@ def test_empty_function(self):
self.check_value('fn:empty(fn:remove(("hello"), 1))', True)
self.check_value('fn:empty((xs:double("0")))', False)


def test_exists_function(self):
self.check_value('fn:exists(("hello", "world"))', True)
self.check_value('fn:exists(())', False)
Expand Down
38 changes: 37 additions & 1 deletion tests/test_xpath_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ def test_iter_parent(self):
context = XPathContext(root, item=TypedElement(root[2][0], None))
self.assertListEqual(list(context.iter_parent()), [root[2]])

def test_iter_siblings(self):
root = ElementTree.XML('<A><B1><C1/></B1><B2/><B3><C1/></B3><B4/><B5/></A>')

context = XPathContext(root)
self.assertListEqual(list(context.iter_siblings()), [])

context = XPathContext(root, item=root[2])
self.assertListEqual(list(context.iter_siblings()), list(root[3:]))

context = XPathContext(root, item=TypedElement(root[2], None))
self.assertListEqual(list(context.iter_siblings()), list(root[3:]))

context = XPathContext(root, item=root[2])
self.assertListEqual(list(context.iter_siblings('preceding-sibling')), list(root[:2]))

context = XPathContext(root, item=TypedElement(root[2], None))
self.assertListEqual(list(context.iter_siblings('preceding-sibling')), list(root[:2]))

def test_iter_descendants(self):
root = ElementTree.XML('<A a1="10" a2="20"><B1/><B2/></A>')
attr = AttributeNode('a1', '10')
Expand All @@ -157,7 +175,6 @@ def test_iter_ancestors(self):
attr = AttributeNode('a1', '10')
self.assertListEqual(list(XPathContext(root).iter_ancestors()), [])
self.assertListEqual(list(XPathContext(root, item=root[1]).iter_ancestors()), [root])
self.assertListEqual(list(XPathContext(root).iter_ancestors(item=root[1])), [root])
self.assertListEqual(list(XPathContext(root, item=attr).iter_ancestors()), [])

context = XPathContext(root, item=TypedElement(root[1], None))
Expand Down Expand Up @@ -191,6 +208,25 @@ def test_iter_preceding(self):
self.assertListEqual(list(context.iter_preceding()),
[root[0], root[0][0], root[1], root[2][0]])

def test_iter_following(self):
root = ElementTree.XML('<A a="1"><B1><C1/></B1><B2/><B3><C1/></B3><B4/><B5/></A>')

context = XPathContext(root)
self.assertListEqual(list(context.iter_followings()), [])

context = XPathContext(root, item=AttributeNode('a', '1'))
self.assertListEqual(list(context.iter_followings()), [])

context = XPathContext(root, item=root[2])
self.assertListEqual(list(context.iter_followings()), list(root[3:]))

context = XPathContext(root, item=root[1])
result = [root[2], root[2][0], root[3], root[4]]
self.assertListEqual(list(context.iter_followings()), result)

context = XPathContext(root, item=TypedElement(root[1], None))
self.assertListEqual(list(context.iter_followings()), result)

def test_iter_results(self):
root = ElementTree.XML('<A><B1><C1/></B1><B2/><B3><C1/><C2 max="10"/></B3></A>')

Expand Down
29 changes: 26 additions & 3 deletions tests/test_xpath_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
etree_deep_equal, is_element_node, is_attribute_node, is_comment_node, \
is_document_node, is_namespace_node, is_processing_instruction_node, \
is_text_node, node_attributes, node_base_uri, node_document_uri, \
node_children, node_nilled, node_kind, node_name
node_children, node_nilled, node_kind, node_name, etree_iter_nodes


class XPathNodesTest(unittest.TestCase):
Expand All @@ -28,12 +28,33 @@ def test_is_etree_element_function(self):
self.assertFalse(is_etree_element('text'))
self.assertFalse(is_etree_element(None))

def test_elem_iter_nodes_function(self):
root = ElementTree.XML('<A>text1\n<B1 a="10">text2</B1><B2/><B3><C1>text3</C1></B3></A>')

result = [root, TextNode('text1\n'), root[0], TextNode('text2'),
root[1], root[2], root[2][0], TextNode('text3')]

self.assertListEqual(list(etree_iter_nodes(root)), result)
self.assertListEqual(list(etree_iter_nodes(root, with_root=False)), result[1:])
self.assertListEqual(list(etree_iter_nodes(TypedElement(root, 'text1'))), result)

result = result[:4] + [AttributeNode('a', '10')] + result[4:]
self.assertListEqual(list(etree_iter_nodes(root, with_attributes=True)), result)

comment = ElementTree.Comment('foo')
root[1].append(comment)
self.assertListEqual(list(etree_iter_nodes(root, with_attributes=True)), result)

def test_elem_iter_strings_function(self):
root = ElementTree.XML('<A>text1\n<B1>text2</B1>tail1<B2/><B3><C1>text3</C1></B3>tail2</A>')
result = ['text1\n', 'text2', 'tail1', 'tail2', 'text3']
self.assertListEqual(list(etree_iter_strings(root)), result)
self.assertListEqual(list(etree_iter_strings(TypedElement(root, 'text1'))), result)

comment = ElementTree.Comment('foo')
root[1].append(comment)
self.assertListEqual(list(etree_iter_strings(root)), result)

def test_etree_deep_equal_function(self):
root = ElementTree.XML('<A><B1>10</B1><B2 max="20"/>end</A>')
self.assertTrue(etree_deep_equal(root, root))
Expand Down Expand Up @@ -122,15 +143,17 @@ def test_node_document_uri_function(self):
document = ElementTree.parse(io.StringIO(xml_test))
self.assertEqual(node_document_uri(document), '/root')

xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:base="http://xpath.test" />'
xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" ' \
'xml:base="http://xpath.test" />'
document = ElementTree.parse(io.StringIO(xml_test))
self.assertEqual(node_document_uri(document), 'http://xpath.test')

xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:base="dir1/dir2" />'
document = ElementTree.parse(io.StringIO(xml_test))
self.assertIsNone(node_document_uri(document))

xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" xml:base="http://[xpath.test" />'
xml_test = '<A xmlns:xml="http://www.w3.org/XML/1998/namespace" ' \
'xml:base="http://[xpath.test" />'
document = ElementTree.parse(io.StringIO(xml_test))
self.assertIsNone(node_document_uri(document))

Expand Down

0 comments on commit c884949

Please sign in to comment.