Fix count() function and '//', '/' binding power

This commit is contained in:
Davide Brunato 2019-09-24 10:05:16 +02:00
parent 18871936cb
commit 307d638f96
4 changed files with 51 additions and 37 deletions

View File

@ -652,24 +652,24 @@ def select(self, context=None):
###
# Path expressions
@method('//', bp=80)
@method('/', bp=80)
@method('//', bp=75)
@method('/', bp=75)
def nud(self):
next_token = self.parser.next_token
if next_token.symbol == '(end)' and self.symbol == '/':
return self
elif not self.parser.next_is_path_step_token():
next_token.wrong_syntax()
self[:] = self.parser.expression(80),
self[:] = self.parser.expression(75),
return self
@method('//', bp=80)
@method('/', bp=80)
@method('//')
@method('/')
def led(self, left):
if not self.parser.next_is_path_step_token():
self.parser.next_token.wrong_syntax()
self[:] = left, self.parser.expression(80)
self[:] = left, self.parser.expression(75)
return self
@ -743,7 +743,7 @@ def select(self, context=None):
###
# Predicate filters
@method('[', bp=75)
@method('[', bp=80)
def led(self, left):
self.parser.next_token.unexpected(']')
self[:] = left, self.parser.expression()
@ -995,13 +995,7 @@ def evaluate(self, context=None):
@method(function('count', nargs=1))
def evaluate(self, context=None):
results = self[0].evaluate(context)
if isinstance(results, list):
return len(results)
elif results is not None:
return 1
else:
return 0
return len(list(self[0].select(context)))
@method(function('id', nargs=1))

View File

@ -19,6 +19,7 @@ In XPath there are 7 kinds of nodes:
Element-like objects are used for representing elements and comments, ElementTree-like objects
for documents. Generic tuples are used for representing attributes and named-tuples for namespaces.
"""
from __future__ import unicode_literals
import locale
import contextlib
from decimal import Decimal
@ -95,23 +96,25 @@ class XPathToken(Token):
def source(self):
symbol, label = self.symbol, self.label
if label == 'axis':
return u'%s::%s' % (self.symbol, self[0].source)
return '%s::%s' % (self.symbol, self[0].source)
elif label in ('function', 'constructor'):
return u'%s(%s)' % (self.symbol, ', '.join(item.source for item in self))
return '%s(%s)' % (self.symbol, ', '.join(item.source for item in self))
elif symbol == ':':
return u'%s:%s' % (self[0].source, self[1].source)
return '%s:%s' % (self[0].source, self[1].source)
elif symbol == '(':
return '()' if not self else u'(%s)' % self[0].source
return '()' if not self else '(%s)' % self[0].source
elif symbol == '[':
return '%s[%s]' % (self[0].source, self[1].source)
elif symbol == ',':
return u'%s, %s' % (self[0].source, self[1].source)
return '%s, %s' % (self[0].source, self[1].source)
elif symbol == '$':
return u'$%s' % self[0].source
return '$%s' % self[0].source
elif symbol == '{':
return u'{%s}%s' % (self[0].value, self[1].value)
return '{%s}%s' % (self[0].value, self[1].value)
elif symbol == 'instance':
return u'%s instance of %s' % (self[0].source, ''.join(t.source for t in self[1:]))
return '%s instance of %s' % (self[0].source, ''.join(t.source for t in self[1:]))
elif symbol == 'treat':
return u'%s treat as %s' % (self[0].source, ''.join(t.source for t in self[1:]))
return '%s treat as %s' % (self[0].source, ''.join(t.source for t in self[1:]))
return super(XPathToken, self).source
@property
@ -463,13 +466,13 @@ class XPathToken(Token):
if obj is None:
return ''
elif is_element_node(obj):
return u''.join(elem_iter_strings(obj))
return ''.join(elem_iter_strings(obj))
elif is_attribute_node(obj):
return obj[1]
elif is_text_node(obj):
return obj
elif is_document_node(obj):
return u''.join(e.text for e in obj.getroot().iter() if e.text is not None)
return ''.join(e.text for e in obj.getroot().iter() if e.text is not None)
elif is_namespace_node(obj):
return obj[1]
elif is_comment_node(obj):

View File

@ -362,7 +362,7 @@ class XPath1ParserTest(unittest.TestCase):
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("./A/B[C][D]/E", '(/ (/ (/ (.) (A)) ([ ([ (B) (C)) (D))) (E))')
self.check_tree("string(xml:lang)", '(string (: (xml) (lang)))')
def test_token_source(self):
@ -906,6 +906,11 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("number(/values/d)", root, 44.0)
self.check_selector("number(/values/a)", root, TypeError)
def test_count_function(self):
root = self.etree.XML('<A><B><C/><C/></B><B/><B><C/><C/><C/></B></A>')
self.check_selector("count(B)", root, 3)
self.check_selector("count(.//C)", root, 5)
def test_sum_function(self):
root = self.etree.XML(XML_DATA_TEST)
self.check_value("sum($values)", 35)
@ -1121,6 +1126,17 @@ class XPath1ParserTest(unittest.TestCase):
self.check_selector("/A/*[' ']", root, root[:])
self.check_selector("/A/*['']", root, [])
root = self.etree.XML("<root><a><b/></a><a><b/><c/></a><a><c/></a></root>")
self.check_tree("child::a[b][c]", '([ ([ (child (a)) (b)) (c))')
self.check_selector("child::a[b][c]", root, [root[1]])
root = self.etree.XML("<root><e><a><b/></a><a><b/></a></e><e><a/></e></root>")
self.check_selector("e/a[not(b)]", root, [root[1][0]])
self.check_selector("preceding::e/a[not(b)]", root, [])
self.check_selector("e/a[preceding::e/a[not(b)]]", root, [])
self.check_selector("not(e/a[preceding::e/a[not(b)]])", root, True)
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])

View File

@ -187,22 +187,22 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
# Test step-by-step, testing also other basic features.
self.check_selector("author[1]", root[0], [root[0][1]])
self.check_selector("book/author[. = $a]", root, [root[0][1], root[1][1]], variables={'a': 'Stevens'})
self.check_tree("book/author[. = $a][1]", '([ ([ (/ (book) (author)) (= (.) ($ (a)))) (1))')
self.check_selector("book/author[. = $a][1]", root, [root[0][1]], variables={'a': 'Stevens'})
self.check_selector("book/author[. = 'Stevens'][2]", root, [root[1][1]])
self.check_tree("book/author[. = $a][1]", '(/ (book) ([ ([ (author) (= (.) ($ (a)))) (1)))')
self.check_selector("book/author[. = $a][1]", root, [root[0][1], root[1][1]], variables={'a': 'Stevens'})
self.check_selector("book/author[. = 'Stevens'][2]", root, [])
self.check_selector("for $a in fn:distinct-values(book/author) return $a",
root, ['Stevens', 'Abiteboul', 'Buneman', 'Suciu'])
self.check_selector("for $a in fn:distinct-values(book/author) "
"return book/author[. = $a]", root, [root[0][1], root[1][1]] + root[2][1:4])
self.check_selector("for $a in fn:distinct-values(book/author) return book/author[. = $a]",
root, [root[0][1], root[1][1]] + root[2][1:4])
self.check_selector("for $a in fn:distinct-values(book/author) "
"return book/author[. = $a][1]", root, [root[0][1]] + root[2][1:4])
self.check_selector("for $a in fn:distinct-values(book/author) return book/author[. = $a][1]",
root, [root[0][1], root[1][1]] + root[2][1:4])
self.check_selector(
"for $a in fn:distinct-values(book/author) "
"return (book/author[. = $a][1], book[author = $a]/title)", root,
[root[0][1], root[0][0], root[1][0], root[2][1], root[2][0], root[2][2], root[2][0],
"for $a in fn:distinct-values(book/author) return (book/author[. = $a][1], book[author = $a]/title)",
root,
[root[0][1], root[1][1], root[0][0], root[1][0], root[2][1], root[2][0], root[2][2], root[2][0],
root[2][3], root[2][0]]
)
@ -1112,7 +1112,8 @@ class XPath2ParserTest(test_xpath1_parser.XPath1ParserTest):
self.check_select("node()", [], context)
def test_count_function(self):
root = self.etree.XML('<A><B1><C1/><C2/></B1><B2/><B3><C3/><C4/><C5/></B3></A>')
super(XPath2ParserTest, self).test_count_function()
root = self.etree.XML('<root/>')
self.check_selector("count(5)", root, 1)
self.check_value("count((0, 1, 2 + 1, 3 - 1))", 4)