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}}")
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
Context node:
<c>.lxml versus elementpath results
Run the reproduce.py (see below) to get results:
Environment
elementpath5.0.4lxml6.0.4 (reference)reproduce.py