Mercurial > genshi > mirror
changeset 818:2886abff9502 trunk
Merged soc2008-xpath branch back into trunk.
author | cmlenz |
---|---|
date | Wed, 11 Mar 2009 17:03:03 +0000 |
parents | f2c58252daa3 |
children | 22bf087580f6 |
files | examples/bench/xpath.py genshi/filters/transform.py genshi/path.py genshi/tests/path.py |
diffstat | 4 files changed, 1016 insertions(+), 383 deletions(-) [+] |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/examples/bench/xpath.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2006-2008 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + + +try: + from os import times + def time_func(): + tup = times() + #just user time + return tup[0] # + tup[1] +except ImportError: + from time import time as time_func + +from genshi.core import START, END +from genshi.path import Path +from genshi.input import XML + +def benchmark(f, acurate_time=1): + """Checks how much time does function f work. It runs it as + many times as needed for avoiding inaccuracy""" + + runs = 1 + while True: + start_time = time_func() + for _ in xrange(runs): + f() + dt = time_func() - start_time + if dt >= acurate_time: + break + runs *= 2 + return dt / runs + +def spell(t): + """Returns spelled representation of time""" + units = [(0.000001, 'microsecond', 'microseconds'), + (0.001, 'milisecond', 'miliseconds'), + (1, 'second', 'seconds'), + (60, 'minute', 'minutes'), + (60*60, 'hour', 'hours'), + ] + i = 0 + at = abs(t) + while i + 1 < len(units) and at >= units[i + 1][0]: + i += 1 + t /= units[i][0] + if t >= 2: + name = units[i][2] + else: + name = units[i][1] + return "%f %s"%(t, name) + +def test_paths_in_streams(exprs, streams, test_strategies=False): + for expr in exprs: + print "Testing path %r" % expr + for stream, sname in streams: + print '\tRunning on "%s" example:' % sname + + path = Path(expr) + def f(): + for e in path.select(stream): + pass + t = spell(benchmark(f)) + print "\t\tselect:\t\t%s" % t + + def f(): + path = Path(expr) + for e in path.select(stream): + pass + t = spell(benchmark(f)) + print "\t\tinit + select:\t%s" % t + + if test_strategies and len(path.paths) == 1: + from genshi.path import GenericStrategy, SingleStepStrategy, \ + SimplePathStrategy + from genshi.tests.path import FakePath + strategies = (GenericStrategy, SingleStepStrategy, + SimplePathStrategy) + for strategy in strategies: + if not strategy.supports(path.paths[0]): + continue + print "\t\t%s Strategy"%strategy.__name__ + fp = FakePath(strategy(path.paths[0])) + def f(): + for e in fp.select(stream): + pass + t = spell(benchmark(f)) + print "\t\t\tselect:\t\t%s"%t + + +def test_documents(test_strategies=False): + streams = [] + + s = XML("""\ +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" xmlns:py="http://genshi.edgewall.org/" py:strip="" lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <title>Foo</title> + </head> + <body> + <h1>Hello</h1> + </body> +</html> +""") + streams.append((s, "small document")) + + s = XML("""\ +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="pl" xmlns:py="http://genshi.edgewall.org/" py:strip="" lang="en"> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <title>Foo</title> + </head> + <body> + <h1>Hello</h1> + <div id="splash"> + <ul> + <li><a class="b1" href="http://genshi.edgewall.org/"> + <strong>Genshi</strong> + Python toolkit for generating output for the web</a></li> + <li><a class="b2" href="http://babel.edgewall.org/"> + <strong>Babel</strong> + Python library for I18n/L10n in web applications</a></li> + <li><a class="b3" href="http://bitten.edgewall.org/"> + <strong>Bitten</strong> + Continuous integration plugin for Trac</a></li> + <li><a class="b4" href="http://posterity.edgewall.org/"> + <strong>Posterity</strong> + Web-based email system</a></li> + </ul> + <div id="trac-splash"> + <a href="http://trac.edgewall.org/"> + <strong>Trac</strong> Web-based lightweight project management + system + </a> + </div> + </div> + </body> +</html> +""") + streams.append((s, "big document")) + + paths = [ + '.', + '*|text()', + 'html', + 'html[@lang="en"]', + 'html/body/h1/text()', + 'html/body/div/a/@href', + 'html/body/div[@id="splash"]/a[@class="b4"]/strong/text()', + 'descendant-or-self::text()', + 'descendant-or-self::h1/text()', + ] + test_paths_in_streams(paths, streams, test_strategies) + +if __name__ == '__main__': + from sys import argv + if "--strategies" in argv: + test_strategies = True + else: + test_strategies = False + test_documents(test_strategies)
--- a/genshi/filters/transform.py +++ b/genshi/filters/transform.py @@ -710,11 +710,12 @@ variables = {} test = self.path.test() stream = iter(stream) + next = stream.next for mark, event in stream: if mark is None: yield mark, event continue - result = test(event, {}, {}) + result = test(event, namespaces, variables) # XXX This is effectively genshi.core._ensure() for transform # streams. if result is True: @@ -722,7 +723,7 @@ yield ENTER, event depth = 1 while depth > 0: - mark, subevent = stream.next() + mark, subevent = next() if subevent[0] is START: depth += 1 elif subevent[0] is END: @@ -731,7 +732,7 @@ yield EXIT, subevent else: yield INSIDE, subevent - test(subevent, {}, {}, updateonly=True) + test(subevent, namespaces, variables, updateonly=True) else: yield OUTSIDE, event elif isinstance(result, Attrs):
--- a/genshi/path.py +++ b/genshi/path.py @@ -38,6 +38,7 @@ structures), it only implements a subset of the full XPath 1.0 language. """ +from collections import deque try: from functools import reduce except ImportError: @@ -45,6 +46,7 @@ from math import ceil, floor import operator import re +from itertools import chain from genshi.core import Stream, Attrs, Namespace, QName from genshi.core import START, END, TEXT, START_NS, END_NS, COMMENT, PI, \ @@ -78,14 +80,446 @@ SELF = Axis.SELF +class GenericStrategy(object): + + @classmethod + def supports(cls, path): + return True + + def __init__(self, path): + self.path = path + + def test(self, ignore_context): + p = self.path + if ignore_context: + if p[0][0] is ATTRIBUTE: + steps = [_DOTSLASHSLASH] + p + else: + steps = [(DESCENDANT_OR_SELF, p[0][1], p[0][2])] + p[1:] + elif p[0][0] is CHILD or p[0][0] is ATTRIBUTE \ + or p[0][0] is DESCENDANT: + steps = [_DOTSLASH] + p + else: + steps = p + + # for node it contains all positions of xpath expression + # where its child should start checking for matches + # with list of corresponding context counters + # there can be many of them, because position that is from + # descendant-like axis can be achieved from different nodes + # for example <a><a><b/></a></a> should match both //a//b[1] + # and //a//b[2] + # positions always form increasing sequence (invariant) + stack = [[(0, [[]])]] + + def _test(event, namespaces, variables, updateonly=False): + kind, data, pos = event[:3] + retval = None + + # Manage the stack that tells us "where we are" in the stream + if kind is END: + if stack: + stack.pop() + return None + if kind is START_NS or kind is END_NS \ + or kind is START_CDATA or kind is END_CDATA: + # should we make namespaces work? + return None + + pos_queue = deque([(pos, cou, []) for pos, cou in stack[-1]]) + next_pos = [] + + # length of real part of path - we omit attribute axis + real_len = len(steps) - ((steps[-1][0] == ATTRIBUTE) or 1 and 0) + last_checked = -1 + + # places where we have to check for match, are these + # provided by parent + while pos_queue: + x, pcou, mcou = pos_queue.popleft() + axis, nodetest, predicates = steps[x] + + # we need to push descendant-like positions from parent + # further + if (axis is DESCENDANT or axis is DESCENDANT_OR_SELF) and pcou: + if next_pos and next_pos[-1][0] == x: + next_pos[-1][1].extend(pcou) + else: + next_pos.append((x, pcou)) + + # nodetest first + if not nodetest(kind, data, pos, namespaces, variables): + continue + + # counters packs that were already bad + missed = set() + counters_len = len(pcou) + len(mcou) + + # number of counters - we have to create one + # for every context position based predicate + cnum = 0 + + # tells if we have match with position x + matched = True + + if predicates: + for predicate in predicates: + pretval = predicate(kind, data, pos, + namespaces, + variables) + if type(pretval) is float: # FIXME <- need to check + # this for other types that + # can be coerced to float + + # each counter pack needs to be checked + for i, cou in enumerate(chain(pcou, mcou)): + # it was bad before + if i in missed: + continue + + if len(cou) < cnum + 1: + cou.append(0) + cou[cnum] += 1 + + # it is bad now + if cou[cnum] != int(pretval): + missed.add(i) + + # none of counters pack was good + if len(missed) == counters_len: + pretval = False + cnum += 1 + + if not pretval: + matched = False + break + + if not matched: + continue + + # counter for next position with current node as context node + child_counter = [] + + if x + 1 == real_len: + # we reached end of expression, because x + 1 + # is equal to the length of expression + matched = True + axis, nodetest, predicates = steps[-1] + if axis is ATTRIBUTE: + matched = nodetest(kind, data, pos, namespaces, + variables) + if matched: + retval = matched + else: + next_axis = steps[x + 1][0] + + # if next axis allows matching self we have + # to add next position to our queue + if next_axis is DESCENDANT_OR_SELF or next_axis is SELF: + if not pos_queue or pos_queue[0][0] > x + 1: + pos_queue.appendleft((x + 1, [], [child_counter])) + else: + pos_queue[0][2].append(child_counter) + + # if axis is not self we have to add it to child's list + if next_axis is not SELF: + next_pos.append((x + 1, [child_counter])) + + if kind is START: + stack.append(next_pos) + + return retval + + return _test + + +class SimplePathStrategy(object): + """Strategy for path with only local names, attributes and text nodes.""" + + @classmethod + def supports(cls, path): + if path[0][0] is ATTRIBUTE: + return False + allowed_tests = (LocalNameTest, CommentNodeTest, TextNodeTest) + for _, nodetest, predicates in path: + if predicates: + return False + if not isinstance(nodetest, allowed_tests): + return False + return True + + def __init__(self, path): + # fragments is list of tuples (fragment, pi, attr, self_beginning) + # fragment is list of nodetests for fragment of path with only + # child:: axes between + # pi is KMP partial match table for this fragment + # attr is attribute nodetest if fragment ends with @ and None otherwise + # self_beginning is True if axis for first fragment element + # was self (first fragment) or descendant-or-self (farther fragment) + self.fragments = [] + + self_beginning = False + fragment = [] + + def nodes_equal(node1, node2): + """Tests if two node tests are equal""" + if node1.__class__ is not node2.__class__: + return False + if node1.__class__ == LocalNameTest: + return node1.name == node2.name + return True + + def calculate_pi(f): + """KMP prefix calculation for table""" + # the indexes in prefix table are shifted by one + # in comparision with common implementations + # pi[i] = NORMAL_PI[i + 1] + if len(f) == 0: + return [] + pi = [0] + s = 0 + for i in xrange(1, len(f)): + while s > 0 and not nodes_equal(f[s], f[i]): + s = pi[s-1] + if nodes_equal(f[s], f[i]): + s += 1 + pi.append(s) + return pi + + for axis in path: + if axis[0] is SELF: + if len(fragment) != 0: + # if element is not first in fragment it has to be + # the same as previous one + # for example child::a/self::b is always wrong + if axis[1] != fragment[-1][1]: + self.fragments = None + return + else: + self_beginning = True + fragment.append(axis[1]) + elif axis[0] is CHILD: + fragment.append(axis[1]) + elif axis[0] is ATTRIBUTE: + pi = calculate_pi(fragment) + self.fragments.append((fragment, pi, axis[1], self_beginning)) + # attribute has always to be at the end, so we can jump out + return + else: + pi = calculate_pi(fragment) + self.fragments.append((fragment, pi, None, self_beginning)) + fragment = [axis[1]] + if axis[0] is DESCENDANT: + self_beginning = False + else: # DESCENDANT_OR_SELF + self_beginning = True + pi = calculate_pi(fragment) + self.fragments.append((fragment, pi, None, self_beginning)) + + def test(self, ignore_context): + # stack of triples (fid, p, ic) + # fid is index of current fragment + # p is position in this fragment + # ic is if we ignore context in this fragment + stack = [] + stack_push = stack.append + stack_pop = stack.pop + frags = self.fragments + frags_len = len(frags) + + def _test(event, namespaces, variables, updateonly=False): + # expression found impossible during init + if frags is None: + return None + + kind, data, pos = event[:3] + + # skip events we don't care about + if kind is END: + if stack: + stack_pop() + return None + if kind is START_NS or kind is END_NS \ + or kind is START_CDATA or kind is END_CDATA: + return None + + if not stack: + # root node, nothing on stack, special case + fid = 0 + # skip empty fragments (there can be actually only one) + while not frags[fid][0]: + fid += 1 + p = 0 + # empty fragment means descendant node at beginning + ic = ignore_context or (fid > 0) + + # expression can match first node, if first axis is self::, + # descendant-or-self:: or if ignore_context is True and + # axis is not descendant:: + if not frags[fid][3] and (not ignore_context or fid > 0): + # axis is not self-beggining, we have to skip this node + stack_push((fid, p, ic)) + return None + else: + # take position of parent + fid, p, ic = stack[-1] + + if fid is not None and not ic: + # fragment not ignoring context - we can't jump back + frag, pi, attrib, _ = frags[fid] + frag_len = len(frag) + + if p == frag_len: + # that probably means empty first fragment + pass + elif frag[p](kind, data, pos, namespaces, variables): + # match, so we can go further + p += 1 + else: + # not matched, so there will be no match in subtree + fid, p = None, None + + if p == frag_len and fid + 1 != frags_len: + # we made it to end of fragment, we can go to following + fid += 1 + p = 0 + ic = True + + if fid is None: + # there was no match in fragment not ignoring context + if kind is START: + stack_push((fid, p, ic)) + return None + + if ic: + # we are in fragment ignoring context + while True: + frag, pi, attrib, _ = frags[fid] + frag_len = len(frag) + + # KMP new "character" + while p > 0 and (p >= frag_len or not \ + frag[p](kind, data, pos, namespaces, variables)): + p = pi[p-1] + if frag[p](kind, data, pos, namespaces, variables): + p += 1 + + if p == frag_len: + # end of fragment reached + if fid + 1 == frags_len: + # that was last fragment + break + else: + fid += 1 + p = 0 + ic = True + if not frags[fid][3]: + # next fragment not self-beginning + break + else: + break + + if kind is START: + # we have to put new position on stack, for children + + if not ic and fid + 1 == frags_len and p == frag_len: + # it is end of the only, not context ignoring fragment + # so there will be no matches in subtree + stack_push((None, None, ic)) + else: + stack_push((fid, p, ic)) + + # have we reached the end of the last fragment? + if fid + 1 == frags_len and p == frag_len: + if attrib: # attribute ended path, return value + return attrib(kind, data, pos, namespaces, variables) + return True + + return None + + return _test + + +class SingleStepStrategy(object): + + @classmethod + def supports(cls, path): + return len(path) == 1 + + def __init__(self, path): + self.path = path + + def test(self, ignore_context): + steps = self.path + if steps[0][0] is ATTRIBUTE: + steps = [_DOTSLASH] + steps + select_attr = steps[-1][0] is ATTRIBUTE and steps[-1][1] or None + + # for every position in expression stores counters' list + # it is used for position based predicates + counters = [] + depth = [0] + + def _test(event, namespaces, variables, updateonly=False): + kind, data, pos = event[:3] + + # Manage the stack that tells us "where we are" in the stream + if kind is END: + if not ignore_context: + depth[0] -= 1 + return None + elif kind is START_NS or kind is END_NS \ + or kind is START_CDATA or kind is END_CDATA: + # should we make namespaces work? + return None + + if not ignore_context: + outside = (steps[0][0] is SELF and depth[0] != 0) \ + or (steps[0][0] is CHILD and depth[0] != 1) \ + or (steps[0][0] is DESCENDANT and depth[0] < 1) + if kind is START: + depth[0] += 1 + if outside: + return None + + axis, nodetest, predicates = steps[0] + if not nodetest(kind, data, pos, namespaces, variables): + return None + + if predicates: + cnum = 0 + for predicate in predicates: + pretval = predicate(kind, data, pos, namespaces, variables) + if type(pretval) is float: # FIXME <- need to check this + # for other types that can be + # coerced to float + if len(counters) < cnum + 1: + counters.append(0) + counters[cnum] += 1 + if counters[cnum] != int(pretval): + pretval = False + cnum += 1 + if not pretval: + return None + + if select_attr: + return select_attr(kind, data, pos, namespaces, variables) + + return True + + return _test + + class Path(object): """Implements basic XPath support on streams. - Instances of this class represent a "compiled" XPath expression, and provide - methods for testing the path against a stream, as well as extracting a - substream matching that path. + Instances of this class represent a "compiled" XPath expression, and + provide methods for testing the path against a stream, as well as + extracting a substream matching that path. """ + STRATEGIES = (SingleStepStrategy, SimplePathStrategy, GenericStrategy) + def __init__(self, text, filename=None, lineno=-1): """Create the path object from a string. @@ -96,6 +530,14 @@ """ self.source = text self.paths = PathParser(text, filename, lineno).parse() + self.strategies = [] + for path in self.paths: + for strategy_class in self.STRATEGIES: + if strategy_class.supports(path): + self.strategies.append(strategy_class(path)) + break + else: + raise NotImplemented, "This path is not implemented" def __repr__(self): paths = [] @@ -133,23 +575,23 @@ if variables is None: variables = {} stream = iter(stream) - def _generate(): + def _generate(stream=stream, ns=namespaces, vs=variables): + next = stream.next test = self.test() for event in stream: - result = test(event, namespaces, variables) + result = test(event, ns, vs) if result is True: yield event if event[0] is START: depth = 1 while depth > 0: - subevent = stream.next() + subevent = next() if subevent[0] is START: depth += 1 elif subevent[0] is END: depth -= 1 yield subevent - test(subevent, namespaces, variables, - updateonly=True) + test(subevent, ns, vs, updateonly=True) elif result: yield result return Stream(_generate(), @@ -173,8 +615,9 @@ >>> from genshi.input import XML >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>') >>> test = Path('child').test() + >>> namespaces, variables = {}, {} >>> for event in xml: - ... if test(event, {}, {}): + ... if test(event, namespaces, variables): ... print event[0], repr(event[1]) START (QName(u'child'), Attrs([(QName(u'id'), u'2')])) @@ -185,123 +628,18 @@ stream against the path :rtype: ``function`` """ - paths = [(p, len(p), [0], [], [0] * len(p)) for p in [ - (ignore_context and [_DOTSLASHSLASH] or []) + p for p in self.paths - ]] - - def _test(event, namespaces, variables, updateonly=False): - kind, data, pos = event[:3] - retval = None - for steps, size, cursors, cutoff, counter in paths: - # Manage the stack that tells us "where we are" in the stream - if kind is END: - if cursors: - cursors.pop() - continue - elif kind is START: - cursors.append(cursors and cursors[-1] or 0) - elif kind is START_NS or kind is END_NS \ - or kind is START_CDATA or kind is END_CDATA: - continue - - if updateonly or retval or not cursors: - continue - cursor = cursors[-1] - depth = len(cursors) - - if cutoff and depth + int(kind is not START) > cutoff[0]: - continue - - ctxtnode = not ignore_context and kind is START \ - and depth == 2 - matched = None - while 1: - # Fetch the next location step - axis, nodetest, predicates = steps[cursor] - - # If this is the start event for the context node, and the - # axis of the location step doesn't include the current - # element, skip the test - if ctxtnode and (axis is CHILD or axis is DESCENDANT): - break - - # Is this the last step of the location path? - last_step = cursor + 1 == size - - # Perform the actual node test - matched = nodetest(kind, data, pos, namespaces, variables) - - # The node test matched - if matched: + tests = [s.test(ignore_context) for s in self.strategies] + if len(tests) == 1: + return tests[0] - # Check all the predicates for this step - if predicates: - for predicate in predicates: - pretval = predicate(kind, data, pos, namespaces, - variables) - if type(pretval) is float: # FIXME <- need to - # check this for - # other types that - # can be coerced to - # float - counter[cursor] += 1 - if counter[cursor] != int(pretval): - pretval = False - if not pretval: - matched = None - break - - # Both the node test and the predicates matched - if matched: - if last_step: - if not ctxtnode or kind is not START \ - or axis is ATTRIBUTE or axis is SELF: - retval = matched - elif not ctxtnode or axis is SELF \ - or axis is DESCENDANT_OR_SELF: - cursor += 1 - cursors[-1] = cursor - cutoff[:] = [] - - if kind is START: - if last_step and not (axis is DESCENDANT or - axis is DESCENDANT_OR_SELF): - cutoff[:] = [depth] - - elif steps[cursor][0] is ATTRIBUTE: - # If the axis of the next location step is the - # attribute axis, we need to move on to processing - # that step without waiting for the next markup - # event - continue - - # We're done with this step if it's the last step or the - # axis isn't "self" - if not matched or last_step or not ( - axis is SELF or axis is DESCENDANT_OR_SELF): - break - if ctxtnode and axis is DESCENDANT_OR_SELF: - ctxtnode = False - - if (retval or not matched) and kind is START and \ - not (axis is DESCENDANT or axis is DESCENDANT_OR_SELF): - # If this step is not a closure, it cannot be matched until - # the current element is closed... so we need to move the - # cursor back to the previous closure and retest that - # against the current element - backsteps = [(i, k, d, p) for i, (k, d, p) - in enumerate(steps[:cursor]) - if k is DESCENDANT or k is DESCENDANT_OR_SELF] - backsteps.reverse() - for cursor, axis, nodetest, predicates in backsteps: - if nodetest(kind, data, pos, namespaces, variables): - cutoff[:] = [] - break - cursors[-1] = cursor - + def _multi(event, namespaces, variables, updateonly=False): + retval = None + for test in tests: + val = test(event, namespaces, variables, updateonly=updateonly) + if retval is None: + retval = val return retval - - return _test + return _multi class PathSyntaxError(Exception): @@ -374,19 +712,28 @@ steps = [] while True: if self.cur_token.startswith('/'): - if self.cur_token == '//': + if not steps: + if self.cur_token == '//': + # hack to make //* match every node - also root + self.next_token() + axis, nodetest, predicates = self._location_step() + steps.append((DESCENDANT_OR_SELF, nodetest, + predicates)) + if self.at_end or not self.cur_token.startswith('/'): + break + continue + else: + raise PathSyntaxError('Absolute location paths not ' + 'supported', self.filename, + self.lineno) + elif self.cur_token == '//': steps.append((DESCENDANT_OR_SELF, NodeTest(), [])) - elif not steps: - raise PathSyntaxError('Absolute location paths not ' - 'supported', self.filename, - self.lineno) self.next_token() axis, nodetest, predicates = self._location_step() if not axis: axis = CHILD steps.append((axis, nodetest, predicates)) - if self.at_end or not self.cur_token.startswith('/'): break @@ -715,6 +1062,7 @@ value. """ __slots__ = ['expr'] + _return_type = bool def __init__(self, expr): self.expr = expr def __call__(self, kind, data, pos, namespaces, variables): @@ -1172,3 +1520,4 @@ _DOTSLASHSLASH = (DESCENDANT_OR_SELF, PrincipalTypeTest(None), ()) +_DOTSLASH = (SELF, PrincipalTypeTest(None), ())
--- a/genshi/tests/path.py +++ b/genshi/tests/path.py @@ -15,11 +15,56 @@ import unittest from genshi.input import XML -from genshi.path import Path, PathSyntaxError +from genshi.path import Path, PathParser, PathSyntaxError, GenericStrategy, \ + SingleStepStrategy, SimplePathStrategy +class FakePath(Path): + def __init__(self, strategy): + self.strategy = strategy + def test(self, ignore_context = False): + return self.strategy.test(ignore_context) + class PathTestCase(unittest.TestCase): + strategies = [GenericStrategy, SingleStepStrategy, SimplePathStrategy] + def _create_path(self, expression, expected): + return path + + def _test_strategies(self, stream, path, render, + namespaces=None, variables=None): + for strategy in self.strategies: + if not strategy.supports(path): + continue + s = strategy(path) + rendered = FakePath(s).select(stream,namespaces=namespaces, + variables=variables).render() + msg = "Bad render using %s strategy"%str(strategy) + msg += "\nExpected:\t'%s'"%render + msg += "\nRendered:\t'%s'"%rendered + self.assertEqual(render, rendered, msg) + + def _test_expression(self, text, expected, stream=None, render="", + namespaces=None, variables=None): + path = Path(text) + if expected is not None: + self.assertEqual(expected, repr(path)) + + if stream is None: + return + + rendered = path.select(stream, namespaces=namespaces, + variables=variables).render() + msg = "Bad render using whole path" + msg += "\nExpected:\t'%s'"%render + msg += "\nRendered:\t'%s'"%rendered + self.assertEqual(render, rendered, msg) + + if len(path.paths) == 1: + self._test_strategies(stream, path.paths[0], render, + namespaces=namespaces, variables=variables) + + def test_error_no_absolute_path(self): self.assertRaises(PathSyntaxError, Path, '/root') @@ -30,418 +75,441 @@ def test_1step(self): xml = XML('<root><elem/></root>') - path = Path('elem') - self.assertEqual('<Path "child::elem">', repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) - - path = Path('child::elem') - self.assertEqual('<Path "child::elem">', repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) + self._test_expression( 'elem', + '<Path "child::elem">', + xml, + '<elem/>') - path = Path('//elem') - self.assertEqual('<Path "descendant-or-self::node()/child::elem">', - repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) + self._test_expression( 'elem', + '<Path "child::elem">', + xml, + '<elem/>') - path = Path('descendant::elem') - self.assertEqual('<Path "descendant::elem">', repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) + self._test_expression( 'child::elem', + '<Path "child::elem">', + xml, + '<elem/>') + + self._test_expression( '//elem', + '<Path "descendant-or-self::elem">', + xml, + '<elem/>') + + self._test_expression( 'descendant::elem', + '<Path "descendant::elem">', + xml, + '<elem/>') def test_1step_self(self): xml = XML('<root><elem/></root>') - path = Path('.') - self.assertEqual('<Path "self::node()">', repr(path)) - self.assertEqual('<root><elem/></root>', path.select(xml).render()) + self._test_expression( '.', + '<Path "self::node()">', + xml, + '<root><elem/></root>') - path = Path('self::node()') - self.assertEqual('<Path "self::node()">', repr(path)) - self.assertEqual('<root><elem/></root>', path.select(xml).render()) + self._test_expression( 'self::node()', + '<Path "self::node()">', + xml, + '<root><elem/></root>') def test_1step_wildcard(self): xml = XML('<root><elem/></root>') - path = Path('*') - self.assertEqual('<Path "child::*">', repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) - - path = Path('child::*') - self.assertEqual('<Path "child::*">', repr(path)) - self.assertEqual('<elem/>', path.select(xml).render()) + self._test_expression( '*', + '<Path "child::*">', + xml, + '<elem/>') - path = Path('child::node()') - self.assertEqual('<Path "child::node()">', repr(path)) - self.assertEqual('<elem/>', Path('child::node()').select(xml).render()) + self._test_expression( 'child::*', + '<Path "child::*">', + xml, + '<elem/>') - path = Path('//*') - self.assertEqual('<Path "descendant-or-self::node()/child::*">', - repr(path)) - self.assertEqual('<root><elem/></root>', path.select(xml).render()) + self._test_expression( 'child::node()', + '<Path "child::node()">', + xml, + '<elem/>') + + self._test_expression( '//*', + '<Path "descendant-or-self::*">', + xml, + '<root><elem/></root>') def test_1step_attribute(self): - path = Path('@foo') - self.assertEqual('<Path "attribute::foo">', repr(path)) - - xml = XML('<root/>') - self.assertEqual('', path.select(xml).render()) + self._test_expression( '@foo', + '<Path "attribute::foo">', + XML('<root/>'), + '') xml = XML('<root foo="bar"/>') - self.assertEqual('bar', path.select(xml).render()) - path = Path('./@foo') - self.assertEqual('<Path "self::node()/attribute::foo">', repr(path)) - self.assertEqual('bar', path.select(xml).render()) + self._test_expression( '@foo', + '<Path "attribute::foo">', + xml, + 'bar') + + self._test_expression( './@foo', + '<Path "self::node()/attribute::foo">', + xml, + 'bar') def test_1step_text(self): xml = XML('<root>Hey</root>') - path = Path('text()') - self.assertEqual('<Path "child::text()">', repr(path)) - self.assertEqual('Hey', path.select(xml).render()) - - path = Path('./text()') - self.assertEqual('<Path "self::node()/child::text()">', repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( 'text()', + '<Path "child::text()">', + xml, + 'Hey') - path = Path('//text()') - self.assertEqual('<Path "descendant-or-self::node()/child::text()">', - repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( './text()', + '<Path "self::node()/child::text()">', + xml, + 'Hey') - path = Path('.//text()') - self.assertEqual('<Path "self::node()/descendant-or-self::node()/child::text()">', - repr(path)) - self.assertEqual('Hey', path.select(xml).render()) + self._test_expression( '//text()', + '<Path "descendant-or-self::text()">', + xml, + 'Hey') + + self._test_expression( './/text()', + '<Path "self::node()/descendant-or-self::node()/child::text()">', + xml, + 'Hey') def test_2step(self): xml = XML('<root><foo/><bar/></root>') - self.assertEqual('<foo/><bar/>', Path('*').select(xml).render()) - self.assertEqual('<bar/>', Path('bar').select(xml).render()) - self.assertEqual('', Path('baz').select(xml).render()) + self._test_expression('*', None, xml, '<foo/><bar/>') + self._test_expression('bar', None, xml, '<bar/>') + self._test_expression('baz', None, xml, '') def test_2step_attribute(self): xml = XML('<elem class="x"><span id="joe">Hey Joe</span></elem>') - self.assertEqual('x', Path('@*').select(xml).render()) - self.assertEqual('x', Path('./@*').select(xml).render()) - self.assertEqual('xjoe', Path('.//@*').select(xml).render()) - self.assertEqual('joe', Path('*/@*').select(xml).render()) + self._test_expression('@*', None, xml, 'x') + self._test_expression('./@*', None, xml, 'x') + self._test_expression('.//@*', None, xml, 'xjoe') + self._test_expression('*/@*', None, xml, 'joe') xml = XML('<elem><foo id="1"/><foo id="2"/></elem>') - self.assertEqual('', Path('@*').select(xml).render()) - self.assertEqual('12', Path('foo/@*').select(xml).render()) + self._test_expression('@*', None, xml, '') + self._test_expression('foo/@*', None, xml, '12') def test_2step_complex(self): xml = XML('<root><foo><bar/></foo></root>') - path = Path('foo/bar') - self.assertEqual('<Path "child::foo/child::bar">', repr(path)) - self.assertEqual('<bar/>', path.select(xml).render()) + self._test_expression( 'foo/bar', + '<Path "child::foo/child::bar">', + xml, + '<bar/>') - path = Path('./bar') - self.assertEqual('<Path "self::node()/child::bar">', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( './bar', + '<Path "self::node()/child::bar">', + xml, + '') - path = Path('foo/*') - self.assertEqual('<Path "child::foo/child::*">', repr(path)) - self.assertEqual('<bar/>', path.select(xml).render()) + self._test_expression( 'foo/*', + '<Path "child::foo/child::*">', + xml, + '<bar/>') xml = XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>') - path = Path('./bar') - self.assertEqual('<Path "self::node()/child::bar">', repr(path)) - self.assertEqual('<bar id="2"/>', path.select(xml).render()) + self._test_expression( './bar', + '<Path "self::node()/child::bar">', + xml, + '<bar id="2"/>') def test_2step_text(self): xml = XML('<root><item>Foo</item></root>') - path = Path('item/text()') - self.assertEqual('<Path "child::item/child::text()">', repr(path)) - self.assertEqual('Foo', path.select(xml).render()) - - path = Path('*/text()') - self.assertEqual('<Path "child::*/child::text()">', repr(path)) - self.assertEqual('Foo', path.select(xml).render()) + self._test_expression( 'item/text()', + '<Path "child::item/child::text()">', + xml, + 'Foo') - path = Path('//text()') - self.assertEqual('<Path "descendant-or-self::node()/child::text()">', - repr(path)) - self.assertEqual('Foo', path.select(xml).render()) + self._test_expression( '*/text()', + '<Path "child::*/child::text()">', + xml, + 'Foo') - path = Path('./text()') - self.assertEqual('<Path "self::node()/child::text()">', repr(path)) - self.assertEqual('', path.select(xml).render()) + self._test_expression( '//text()', + '<Path "descendant-or-self::text()">', + xml, + 'Foo') + + self._test_expression( './text()', + '<Path "self::node()/child::text()">', + xml, + '') xml = XML('<root><item>Foo</item><item>Bar</item></root>') - path = Path('item/text()') - self.assertEqual('<Path "child::item/child::text()">', repr(path)) - self.assertEqual('FooBar', path.select(xml).render()) - - xml = XML('<root><item>Foo</item><item>Bar</item></root>') - self.assertEqual('FooBar', path.select(xml).render()) + self._test_expression( 'item/text()', + '<Path "child::item/child::text()">', + xml, + 'FooBar') def test_3step(self): xml = XML('<root><foo><bar/></foo></root>') - path = Path('foo/*') - self.assertEqual('<Path "child::foo/child::*">', repr(path)) - self.assertEqual('<bar/>', path.select(xml).render()) + self._test_expression( 'foo/*', + '<Path "child::foo/child::*">', + xml, + '<bar/>') def test_3step_complex(self): xml = XML('<root><foo><bar/></foo></root>') - path = Path('*/bar') - self.assertEqual('<Path "child::*/child::bar">', repr(path)) - self.assertEqual('<bar/>', path.select(xml).render()) + self._test_expression( '*/bar', + '<Path "child::*/child::bar">', + xml, + '<bar/>') xml = XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>') - path = Path('//bar') - self.assertEqual('<Path "descendant-or-self::node()/child::bar">', - repr(path)) - self.assertEqual('<bar id="1"/><bar id="2"/>', - path.select(xml).render()) + self._test_expression( '//bar', + '<Path "descendant-or-self::bar">', + xml, + '<bar id="1"/><bar id="2"/>') def test_node_type_comment(self): xml = XML('<root><!-- commented --></root>') - path = Path('comment()') - self.assertEqual('<Path "child::comment()">', repr(path)) - self.assertEqual('<!-- commented -->', path.select(xml).render()) + self._test_expression( 'comment()', + '<Path "child::comment()">', + xml, + '<!-- commented -->') def test_node_type_text(self): xml = XML('<root>Some text <br/>in here.</root>') - path = Path('text()') - self.assertEqual('<Path "child::text()">', repr(path)) - self.assertEqual('Some text in here.', path.select(xml).render()) + self._test_expression( 'text()', + '<Path "child::text()">', + xml, + 'Some text in here.') def test_node_type_node(self): xml = XML('<root>Some text <br/>in here.</root>') - path = Path('node()') - self.assertEqual('<Path "child::node()">', repr(path)) - self.assertEqual('Some text <br/>in here.', path.select(xml).render()) + self._test_expression( 'node()', + '<Path "child::node()">', + xml, + 'Some text <br/>in here.',) def test_node_type_processing_instruction(self): xml = XML('<?python x = 2 * 3 ?><root><?php echo("x") ?></root>') - path = Path('processing-instruction()') - self.assertEqual('<Path "child::processing-instruction()">', - repr(path)) - self.assertEqual('<?python x = 2 * 3 ?><?php echo("x") ?>', - path.select(xml).render()) + self._test_expression( '//processing-instruction()', + '<Path "descendant-or-self::processing-instruction()">', + xml, + '<?python x = 2 * 3 ?><?php echo("x") ?>') - path = Path('processing-instruction("php")') - self.assertEqual('<Path "child::processing-instruction(\"php\")">', - repr(path)) - self.assertEqual('<?php echo("x") ?>', path.select(xml).render()) + self._test_expression( 'processing-instruction()', + '<Path "child::processing-instruction()">', + xml, + '<?php echo("x") ?>') + + self._test_expression( 'processing-instruction("php")', + '<Path "child::processing-instruction(\"php\")">', + xml, + '<?php echo("x") ?>') def test_simple_union(self): xml = XML("""<body>1<br />2<br />3<br /></body>""") - path = Path('*|text()') - self.assertEqual('<Path "child::*|child::text()">', repr(path)) - self.assertEqual('1<br/>2<br/>3<br/>', path.select(xml).render()) + self._test_expression( '*|text()', + '<Path "child::*|child::text()">', + xml, + '1<br/>2<br/>3<br/>') def test_predicate_name(self): xml = XML('<root><foo/><bar/></root>') - path = Path('*[name()="foo"]') - self.assertEqual('<foo/>', path.select(xml).render()) + self._test_expression('*[name()="foo"]', None, xml, '<foo/>') def test_predicate_localname(self): xml = XML('<root><foo xmlns="NS"/><bar/></root>') - path = Path('*[local-name()="foo"]') - self.assertEqual('<foo xmlns="NS"/>', path.select(xml).render()) + self._test_expression('*[local-name()="foo"]', None, xml, + '<foo xmlns="NS"/>') def test_predicate_namespace(self): xml = XML('<root><foo xmlns="NS"/><bar/></root>') - path = Path('*[namespace-uri()="NS"]') - self.assertEqual('<foo xmlns="NS"/>', path.select(xml).render()) + self._test_expression('*[namespace-uri()="NS"]', None, xml, + '<foo xmlns="NS"/>') def test_predicate_not_name(self): xml = XML('<root><foo/><bar/></root>') - path = Path('*[not(name()="foo")]') - self.assertEqual('<bar/>', path.select(xml).render()) + self._test_expression('*[not(name()="foo")]', None, xml, '<bar/>') def test_predicate_attr(self): xml = XML('<root><item/><item important="very"/></root>') - path = Path('item[@important]') - self.assertEqual('<item important="very"/>', path.select(xml).render()) - path = Path('item[@important="very"]') - self.assertEqual('<item important="very"/>', path.select(xml).render()) + self._test_expression('item[@important]', None, xml, + '<item important="very"/>') + self._test_expression('item[@important="very"]', None, xml, + '<item important="very"/>') def test_predicate_attr_equality(self): xml = XML('<root><item/><item important="notso"/></root>') - path = Path('item[@important="very"]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@important!="very"]') - self.assertEqual('<item/><item important="notso"/>', - path.select(xml).render()) + self._test_expression('item[@important="very"]', None, xml, '') + self._test_expression('item[@important!="very"]', None, xml, + '<item/><item important="notso"/>') def test_predicate_attr_greater_than(self): xml = XML('<root><item priority="3"/></root>') - path = Path('item[@priority>3]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@priority>2]') - self.assertEqual('<item priority="3"/>', path.select(xml).render()) + self._test_expression('item[@priority>3]', None, xml, '') + self._test_expression('item[@priority>2]', None, xml, + '<item priority="3"/>') def test_predicate_attr_less_than(self): xml = XML('<root><item priority="3"/></root>') - path = Path('item[@priority<3]') - self.assertEqual('', path.select(xml).render()) - path = Path('item[@priority<4]') - self.assertEqual('<item priority="3"/>', path.select(xml).render()) + self._test_expression('item[@priority<3]', None, xml, '') + self._test_expression('item[@priority<4]', None, xml, + '<item priority="3"/>') def test_predicate_attr_and(self): xml = XML('<root><item/><item important="very"/></root>') - path = Path('item[@important and @important="very"]') - self.assertEqual('<item important="very"/>', path.select(xml).render()) - path = Path('item[@important and @important="notso"]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@important and @important="very"]', + None, xml, '<item important="very"/>') + self._test_expression('item[@important and @important="notso"]', + None, xml, '') def test_predicate_attr_or(self): xml = XML('<root><item/><item important="very"/></root>') - path = Path('item[@urgent or @important]') - self.assertEqual('<item important="very"/>', path.select(xml).render()) - path = Path('item[@urgent or @notso]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('item[@urgent or @important]', None, xml, + '<item important="very"/>') + self._test_expression('item[@urgent or @notso]', None, xml, '') def test_predicate_boolean_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[boolean("")]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean("yo")]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[boolean(0)]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean(42)]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[boolean(false())]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[boolean(true())]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[boolean("")]', None, xml, '') + self._test_expression('*[boolean("yo")]', None, xml, '<foo>bar</foo>') + self._test_expression('*[boolean(0)]', None, xml, '') + self._test_expression('*[boolean(42)]', None, xml, '<foo>bar</foo>') + self._test_expression('*[boolean(false())]', None, xml, '') + self._test_expression('*[boolean(true())]', None, xml, + '<foo>bar</foo>') def test_predicate_ceil_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[ceiling("4.5")=5]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[ceiling("4.5")=5]', None, xml, + '<foo>bar</foo>') def test_predicate_concat_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[name()=concat("f", "oo")]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[name()=concat("f", "oo")]', None, xml, + '<foo>bar</foo>') def test_predicate_contains_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[contains(name(), "oo")]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[contains(name(), "oo")]', None, xml, + '<foo>bar</foo>') def test_predicate_matches_function(self): xml = XML('<root><foo>bar</foo><bar>foo</bar></root>') - path = Path('*[matches(name(), "foo|bar")]') - self.assertEqual('<foo>bar</foo><bar>foo</bar>', path.select(xml).render()) + self._test_expression('*[matches(name(), "foo|bar")]', None, xml, + '<foo>bar</foo><bar>foo</bar>') def test_predicate_false_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[false()]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[false()]', None, xml, '') def test_predicate_floor_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[floor("4.5")=4]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[floor("4.5")=4]', None, xml, + '<foo>bar</foo>') def test_predicate_normalize_space_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[normalize-space(" foo bar ")="foo bar"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[normalize-space(" foo bar ")="foo bar"]', + None, xml, '<foo>bar</foo>') def test_predicate_number_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[number("3.0")=3]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[number("3.0")=3.0]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[number("0.1")=.1]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[number("3.0")=3]', None, xml, + '<foo>bar</foo>') + self._test_expression('*[number("3.0")=3.0]', None, xml, + '<foo>bar</foo>') + self._test_expression('*[number("0.1")=.1]', None, xml, + '<foo>bar</foo>') def test_predicate_round_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[round("4.4")=4]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[round("4.6")=5]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[round("4.4")=4]', None, xml, + '<foo>bar</foo>') + self._test_expression('*[round("4.6")=5]', None, xml, + '<foo>bar</foo>') def test_predicate_starts_with_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[starts-with(name(), "f")]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[starts-with(name(), "b")]') - self.assertEqual('', path.select(xml).render()) + self._test_expression('*[starts-with(name(), "f")]', None, xml, + '<foo>bar</foo>') + self._test_expression('*[starts-with(name(), "b")]', None, xml, '') def test_predicate_string_length_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[string-length(name())=3]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[string-length(name())=3]', None, xml, + '<foo>bar</foo>') def test_predicate_substring_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[substring(name(), 1)="oo"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) - path = Path('*[substring(name(), 1, 1)="o"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[substring(name(), 1)="oo"]', None, xml, + '<foo>bar</foo>') + self._test_expression('*[substring(name(), 1, 1)="o"]', None, xml, + '<foo>bar</foo>') def test_predicate_substring_after_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[substring-after(name(), "f")="oo"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[substring-after(name(), "f")="oo"]', None, xml, + '<foo>bar</foo>') def test_predicate_substring_before_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[substring-before(name(), "oo")="f"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[substring-before(name(), "oo")="f"]', + None, xml, '<foo>bar</foo>') def test_predicate_translate_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[translate(name(), "fo", "ba")="baa"]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[translate(name(), "fo", "ba")="baa"]', + None, xml, '<foo>bar</foo>') def test_predicate_true_function(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[true()]') - self.assertEqual('<foo>bar</foo>', path.select(xml).render()) + self._test_expression('*[true()]', None, xml, '<foo>bar</foo>') def test_predicate_variable(self): xml = XML('<root><foo>bar</foo></root>') - path = Path('*[name()=$bar]') variables = {'bar': 'foo'} - self.assertEqual('<foo>bar</foo>', - path.select(xml, variables=variables).render()) + self._test_expression('*[name()=$bar]', None, xml, '<foo>bar</foo>', + variables = variables) def test_predicate_position(self): xml = XML('<root><foo id="a1"/><foo id="a2"/><foo id="a3"/></root>') - path = Path('*[2]') - self.assertEqual('<foo id="a2"/>', path.select(xml).render()) + self._test_expression('*[2]', None, xml, '<foo id="a2"/>') def test_predicate_attr_and_position(self): xml = XML('<root><foo/><foo id="a1"/><foo id="a2"/></root>') - path = Path('*[@id][2]') - self.assertEqual('<foo id="a2"/>', path.select(xml).render()) + self._test_expression('*[@id][2]', None, xml, '<foo id="a2"/>') def test_predicate_position_and_attr(self): xml = XML('<root><foo/><foo id="a1"/><foo id="a2"/></root>') - path = Path('*[1][@id]') - self.assertEqual('', path.select(xml).render()) - path = Path('*[2][@id]') - self.assertEqual('<foo id="a1"/>', path.select(xml).render()) + self._test_expression('*[1][@id]', None, xml, '') + self._test_expression('*[2][@id]', None, xml, '<foo id="a1"/>') + + def test_predicate_advanced_position(self): + xml = XML('<root><a><b><c><d><e/></d></c></b></a></root>') + self._test_expression( 'descendant-or-self::*/' + 'descendant-or-self::*/' + 'descendant-or-self::*[2]/' + 'self::*/descendant::*[3]', None, xml, + '<d><e/></d>') + + def test_predicate_child_position(self): + xml = XML('\ +<root><a><b>1</b><b>2</b><b>3</b></a><a><b>4</b><b>5</b></a></root>') + self._test_expression('//a/b[2]', None, xml, '<b>2</b><b>5</b>') + self._test_expression('//a/b[3]', None, xml, '<b>3</b>') def test_name_with_namespace(self): xml = XML('<root xmlns:f="FOO"><f:foo>bar</f:foo></root>') - path = Path('f:foo') - self.assertEqual('<Path "child::f:foo">', repr(path)) - namespaces = {'f': 'FOO'} - self.assertEqual('<foo xmlns="FOO">bar</foo>', - path.select(xml, namespaces=namespaces).render()) + self._test_expression('f:foo', '<Path "child::f:foo">', xml, + '<foo xmlns="FOO">bar</foo>', + namespaces = {'f': 'FOO'}) def test_wildcard_with_namespace(self): xml = XML('<root xmlns:f="FOO"><f:foo>bar</f:foo></root>') - path = Path('f:*') - self.assertEqual('<Path "child::f:*">', repr(path)) - namespaces = {'f': 'FOO'} - self.assertEqual('<foo xmlns="FOO">bar</foo>', - path.select(xml, namespaces=namespaces).render()) + self._test_expression('f:*', '<Path "child::f:*">', xml, + '<foo xmlns="FOO">bar</foo>', + namespaces = {'f': 'FOO'}) def test_predicate_termination(self): """ @@ -449,25 +517,70 @@ cause an infinite loop. See <http://genshi.edgewall.org/ticket/82>. """ xml = XML('<ul flag="1"><li>a</li><li>b</li></ul>') - path = Path('.[@flag="1"]/*') - self.assertEqual('<li>a</li><li>b</li>', path.select(xml).render()) + self._test_expression('.[@flag="1"]/*', None, xml, + '<li>a</li><li>b</li>') xml = XML('<ul flag="1"><li>a</li><li>b</li></ul>') - path = Path('.[@flag="0"]/*') - self.assertEqual('', path.select(xml).render()) + self._test_expression('.[@flag="0"]/*', None, xml, '') def test_attrname_with_namespace(self): xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>') - path = Path('foo[@f:bar]') - self.assertEqual('<foo xmlns:ns1="FOO" ns1:bar="baz"/>', - path.select(xml, namespaces={'f': 'FOO'}).render()) + self._test_expression('foo[@f:bar]', None, xml, + '<foo xmlns:ns1="FOO" ns1:bar="baz"/>', + namespaces={'f': 'FOO'}) def test_attrwildcard_with_namespace(self): xml = XML('<root xmlns:f="FOO"><foo f:bar="baz"/></root>') - path = Path('foo[@f:*]') - self.assertEqual('<foo xmlns:ns1="FOO" ns1:bar="baz"/>', - path.select(xml, namespaces={'f': 'FOO'}).render()) + self._test_expression('foo[@f:*]', None, xml, + '<foo xmlns:ns1="FOO" ns1:bar="baz"/>', + namespaces={'f': 'FOO'}) + def test_self_and_descendant(self): + xml = XML('<root><foo/></root>') + self._test_expression('self::root', None, xml, '<root><foo/></root>') + self._test_expression('self::foo', None, xml, '') + self._test_expression('descendant::root', None, xml, '') + self._test_expression('descendant::foo', None, xml, '<foo/>') + self._test_expression('descendant-or-self::root', None, xml, + '<root><foo/></root>') + self._test_expression('descendant-or-self::foo', None, xml, '<foo/>') + def test_long_simple_paths(self): + xml = XML('<root><a><b><a><d><a><b><a><b><a><b><a><c>!' + '</c></a></b></a></b></a></b></a></d></a></b></a></root>') + self._test_expression('//a/b/a/b/a/c', None, xml, '<c>!</c>') + self._test_expression('//a/b/a/c', None, xml, '<c>!</c>') + self._test_expression('//a/c', None, xml, '<c>!</c>') + self._test_expression('//c', None, xml, '<c>!</c>') + # Please note that a//b is NOT the same as a/descendant::b + # it is a/descendant-or-self::node()/b, which SimplePathStrategy + # does NOT support + self._test_expression('a/b/descendant::a/c', None, xml, '<c>!</c>') + self._test_expression('a/b/descendant::a/d/descendant::a/c', + None, xml, '<c>!</c>') + self._test_expression('a/b/descendant::a/d/a/c', None, xml, '') + self._test_expression('//d/descendant::b/descendant::b/descendant::b' + '/descendant::c', None, xml, '<c>!</c>') + self._test_expression('//d/descendant::b/descendant::b/descendant::b' + '/descendant::b/descendant::c', None, xml, '') + def _test_support(self, strategy_class, text): + path = PathParser(text, None, -1).parse()[0] + return strategy_class.supports(path) + def test_simple_strategy_support(self): + self.assert_(self._test_support(SimplePathStrategy, 'a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'self::a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'descendant::a/b')) + self.assert_(self._test_support(SimplePathStrategy, + 'descendant-or-self::a/b')) + self.assert_(self._test_support(SimplePathStrategy, '//a/b')) + self.assert_(self._test_support(SimplePathStrategy, 'a/@b')) + self.assert_(self._test_support(SimplePathStrategy, 'a/text()')) + + # a//b is a/descendant-or-self::node()/b + self.assert_(not self._test_support(SimplePathStrategy, 'a//b')) + self.assert_(not self._test_support(SimplePathStrategy, 'node()/@a')) + self.assert_(not self._test_support(SimplePathStrategy, '@a')) + self.assert_(not self._test_support(SimplePathStrategy, 'foo:bar')) + self.assert_(not self._test_support(SimplePathStrategy, 'a/@foo:bar')) def suite(): suite = unittest.TestSuite()