changeset 818:eab11d35c769

Merged soc2008-xpath branch back into trunk.
author cmlenz
date Wed, 11 Mar 2009 17:03:03 +0000
parents f7a5336dd389
children 185b9b1784d1
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()
Copyright (C) 2012-2017 Edgewall Software