Skip to content

Commit 97e8b1e

Browse files
authored
bpo-40624: Add support for the XPath != operator in xml.etree (GH-22147)
1 parent 4eb41d0 commit 97e8b1e

File tree

4 files changed

+78
-7
lines changed

4 files changed

+78
-7
lines changed

Doc/library/xml.etree.elementtree.rst

+18
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ Supported XPath syntax
455455
| | has the given value. The value cannot contain |
456456
| | quotes. |
457457
+-----------------------+------------------------------------------------------+
458+
| ``[@attrib!='value']``| Selects all elements for which the given attribute |
459+
| | does not have the given value. The value cannot |
460+
| | contain quotes. |
461+
| | |
462+
| | .. versionadded:: 3.10 |
463+
+-----------------------+------------------------------------------------------+
458464
| ``[tag]`` | Selects all elements that have a child named |
459465
| | ``tag``. Only immediate children are supported. |
460466
+-----------------------+------------------------------------------------------+
@@ -463,10 +469,22 @@ Supported XPath syntax
463469
| | |
464470
| | .. versionadded:: 3.7 |
465471
+-----------------------+------------------------------------------------------+
472+
| ``[.!='text']`` | Selects all elements whose complete text content, |
473+
| | including descendants, does not equal the given |
474+
| | ``text``. |
475+
| | |
476+
| | .. versionadded:: 3.10 |
477+
+-----------------------+------------------------------------------------------+
466478
| ``[tag='text']`` | Selects all elements that have a child named |
467479
| | ``tag`` whose complete text content, including |
468480
| | descendants, equals the given ``text``. |
469481
+-----------------------+------------------------------------------------------+
482+
| ``[tag!='text']`` | Selects all elements that have a child named |
483+
| | ``tag`` whose complete text content, including |
484+
| | descendants, does not equal the given ``text``. |
485+
| | |
486+
| | .. versionadded:: 3.10 |
487+
+-----------------------+------------------------------------------------------+
470488
| ``[position]`` | Selects all elements that are located at the given |
471489
| | position. The position can be either an integer |
472490
| | (1 is the first position), the expression ``last()`` |

Lib/test/test_xml_etree.py

+35
Original file line numberDiff line numberDiff line change
@@ -2852,8 +2852,12 @@ def test_findall(self):
28522852
['tag'] * 3)
28532853
self.assertEqual(summarize_list(e.findall('.//tag[@class="a"]')),
28542854
['tag'])
2855+
self.assertEqual(summarize_list(e.findall('.//tag[@class!="a"]')),
2856+
['tag'] * 2)
28552857
self.assertEqual(summarize_list(e.findall('.//tag[@class="b"]')),
28562858
['tag'] * 2)
2859+
self.assertEqual(summarize_list(e.findall('.//tag[@class!="b"]')),
2860+
['tag'])
28572861
self.assertEqual(summarize_list(e.findall('.//tag[@id]')),
28582862
['tag'])
28592863
self.assertEqual(summarize_list(e.findall('.//section[tag]')),
@@ -2875,6 +2879,19 @@ def test_findall(self):
28752879
self.assertEqual(summarize_list(e.findall(".//section[ tag = 'subtext' ]")),
28762880
['section'])
28772881

2882+
# Negations of above tests. They match nothing because the sole section
2883+
# tag has subtext.
2884+
self.assertEqual(summarize_list(e.findall(".//section[tag!='subtext']")),
2885+
[])
2886+
self.assertEqual(summarize_list(e.findall(".//section[tag !='subtext']")),
2887+
[])
2888+
self.assertEqual(summarize_list(e.findall(".//section[tag!= 'subtext']")),
2889+
[])
2890+
self.assertEqual(summarize_list(e.findall(".//section[tag != 'subtext']")),
2891+
[])
2892+
self.assertEqual(summarize_list(e.findall(".//section[ tag != 'subtext' ]")),
2893+
[])
2894+
28782895
self.assertEqual(summarize_list(e.findall(".//tag[.='subtext']")),
28792896
['tag'])
28802897
self.assertEqual(summarize_list(e.findall(".//tag[. ='subtext']")),
@@ -2890,6 +2907,24 @@ def test_findall(self):
28902907
self.assertEqual(summarize_list(e.findall(".//tag[.= ' subtext']")),
28912908
[])
28922909

2910+
# Negations of above tests.
2911+
# Matches everything but the tag containing subtext
2912+
self.assertEqual(summarize_list(e.findall(".//tag[.!='subtext']")),
2913+
['tag'] * 3)
2914+
self.assertEqual(summarize_list(e.findall(".//tag[. !='subtext']")),
2915+
['tag'] * 3)
2916+
self.assertEqual(summarize_list(e.findall('.//tag[.!= "subtext"]')),
2917+
['tag'] * 3)
2918+
self.assertEqual(summarize_list(e.findall('.//tag[ . != "subtext" ]')),
2919+
['tag'] * 3)
2920+
self.assertEqual(summarize_list(e.findall(".//tag[. != 'subtext']")),
2921+
['tag'] * 3)
2922+
# Matches all tags.
2923+
self.assertEqual(summarize_list(e.findall(".//tag[. != 'subtext ']")),
2924+
['tag'] * 4)
2925+
self.assertEqual(summarize_list(e.findall(".//tag[.!= ' subtext']")),
2926+
['tag'] * 4)
2927+
28932928
# duplicate section => 2x tag matches
28942929
e[1] = e[2]
28952930
self.assertEqual(summarize_list(e.findall(".//section[tag = 'subtext']")),

Lib/xml/etree/ElementPath.py

+24-7
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@
6565
r"//?|"
6666
r"\.\.|"
6767
r"\(\)|"
68+
r"!=|"
6869
r"[/.*:\[\]\(\)@=])|"
69-
r"((?:\{[^}]+\})?[^/\[\]\(\)@=\s]+)|"
70+
r"((?:\{[^}]+\})?[^/\[\]\(\)@!=\s]+)|"
7071
r"\s+"
7172
)
7273

@@ -253,15 +254,19 @@ def select(context, result):
253254
if elem.get(key) is not None:
254255
yield elem
255256
return select
256-
if signature == "@-='":
257-
# [@attribute='value']
257+
if signature == "@-='" or signature == "@-!='":
258+
# [@attribute='value'] or [@attribute!='value']
258259
key = predicate[1]
259260
value = predicate[-1]
260261
def select(context, result):
261262
for elem in result:
262263
if elem.get(key) == value:
263264
yield elem
264-
return select
265+
def select_negated(context, result):
266+
for elem in result:
267+
if (attr_value := elem.get(key)) is not None and attr_value != value:
268+
yield elem
269+
return select_negated if '!=' in signature else select
265270
if signature == "-" and not re.match(r"\-?\d+$", predicate[0]):
266271
# [tag]
267272
tag = predicate[0]
@@ -270,8 +275,10 @@ def select(context, result):
270275
if elem.find(tag) is not None:
271276
yield elem
272277
return select
273-
if signature == ".='" or (signature == "-='" and not re.match(r"\-?\d+$", predicate[0])):
274-
# [.='value'] or [tag='value']
278+
if signature == ".='" or signature == ".!='" or (
279+
(signature == "-='" or signature == "-!='")
280+
and not re.match(r"\-?\d+$", predicate[0])):
281+
# [.='value'] or [tag='value'] or [.!='value'] or [tag!='value']
275282
tag = predicate[0]
276283
value = predicate[-1]
277284
if tag:
@@ -281,12 +288,22 @@ def select(context, result):
281288
if "".join(e.itertext()) == value:
282289
yield elem
283290
break
291+
def select_negated(context, result):
292+
for elem in result:
293+
for e in elem.iterfind(tag):
294+
if "".join(e.itertext()) != value:
295+
yield elem
296+
break
284297
else:
285298
def select(context, result):
286299
for elem in result:
287300
if "".join(elem.itertext()) == value:
288301
yield elem
289-
return select
302+
def select_negated(context, result):
303+
for elem in result:
304+
if "".join(elem.itertext()) != value:
305+
yield elem
306+
return select_negated if '!=' in signature else select
290307
if signature == "-" or signature == "-()" or signature == "-()-":
291308
# [index] or [last()] or [last()-index]
292309
if signature == "-":
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for the XPath ``!=`` operator in xml.etree

0 commit comments

Comments
 (0)