Skip to content

axis::*[pred][1] gives the wrong node on reverse axes #105

Description

@velle

I found a few expressions where lxml and elementpath disagree about the result.
I think elementpath has a bug in how it defines the order in reverse axis.

Example

<a attr="A">
  <b attr="B">
    <c attr="C">text</c>
  </b>
</a>

Context node: <c>.

lxml versus elementpath results

Run the reproduce.py (see below) to get results:

$ python reproduce.py
expression                           | lxml    | elementpath | diff
-------------------------------------------------------------------
ancestor-or-self::*[@attr]           | a, b, c | a, b, c     |
ancestor-or-self::*[@attr][1]        | c       | a           | x
(ancestor-or-self::*[@attr])[1]      | a       | a           |
ancestor-or-self::*[@attr][last()]   | a       | c           | x
(ancestor-or-self::*[@attr])[last()] | c       | c           |
ancestor-or-self::*                  | a, b, c | a, b, c     |
ancestor-or-self::*[1]               | c       | c           |
(ancestor-or-self::*)[1]             | a       | a           |
ancestor-or-self::*[last()]          | a       | a           |
(ancestor-or-self::*)[last()]        | c       | c           |

Environment

  • Python 3.12.3
  • elementpath 5.0.4
  • lxml 6.0.4 (reference)

reproduce.py

import elementpath
from elementpath.xpath2 import XPath2Parser
from lxml import etree

parser = XPath2Parser()

XML = '<a attr="A"><b attr="B"><c attr="C">text</c></b></a>'

EXPRESSIONS = [
    "ancestor-or-self::*[@attr]",
    "ancestor-or-self::*[@attr][1]",
    "(ancestor-or-self::*[@attr])[1]",
    "ancestor-or-self::*[@attr][last()]",
    "(ancestor-or-self::*[@attr])[last()]",
    "ancestor-or-self::*",
    "ancestor-or-self::*[1]",
    "(ancestor-or-self::*)[1]",
    "ancestor-or-self::*[last()]",
    "(ancestor-or-self::*)[last()]",
]

doc = etree.fromstring(XML.encode())
tree = etree.ElementTree(doc)
node = doc[0][0]  # <c>, always the context node


def run(expr):
    try:
        lxml_res = ", ".join(el.tag for el in node.xpath(expr))
    except Exception as e:
        lxml_res = f"ERROR: {e}"

    try:
        token = parser.parse(expr)
        ctx = elementpath.XPathContext(root=tree, item=node)
        ep_res = token.evaluate(ctx)
        if isinstance(ep_res, list):
            ep_res = ", ".join(n.elem.tag if hasattr(n, "elem") else str(n) for n in ep_res)
    except Exception as e:
        ep_res = f"ERROR: {e}"

    return lxml_res, ep_res


rows = []
for expr in EXPRESSIONS:
    lxml_res, ep_res = run(expr)
    diff = "x" if lxml_res != ep_res else ""
    rows.append((expr, lxml_res, ep_res, diff))

w0 = max(len(r[0]) for r in rows + [("expression", "", "", "")])
w1 = max(len(r[1]) for r in rows + [("", "lxml", "", "")])
w2 = max(len(r[2]) for r in rows + [("", "", "elementpath", "")])
w3 = max(len(r[3]) for r in rows + [("", "", "", "diff")])

header = f"{'expression':<{w0}} | {'lxml':<{w1}} | {'elementpath':<{w2}} | {'diff':<{w3}}"
print(header)
print("-" * len(header))
for expr, lxml_res, ep_res, diff in rows:
    print(f"{expr:<{w0}} | {lxml_res:<{w1}} | {ep_res:<{w2}} | {diff:<{w3}}")

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions