Skip to content

Commit e9927e1

Browse files
authored
bpo-30485: support a default prefix mapping in ElementPath by passing None as prefix (python#1823)
1 parent ffca16e commit e9927e1

File tree

4 files changed

+39
-12
lines changed

4 files changed

+39
-12
lines changed

Doc/library/xml.etree.elementtree.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -764,15 +764,17 @@ Element Objects
764764
Finds the first subelement matching *match*. *match* may be a tag name
765765
or a :ref:`path <elementtree-xpath>`. Returns an element instance
766766
or ``None``. *namespaces* is an optional mapping from namespace prefix
767-
to full name.
767+
to full name. Pass ``None`` as prefix to move all unprefixed tag names
768+
in the expression into the given namespace.
768769

769770

770771
.. method:: findall(match, namespaces=None)
771772

772773
Finds all matching subelements, by tag name or
773774
:ref:`path <elementtree-xpath>`. Returns a list containing all matching
774775
elements in document order. *namespaces* is an optional mapping from
775-
namespace prefix to full name.
776+
namespace prefix to full name. Pass ``None`` as prefix to move all
777+
unprefixed tag names in the expression into the given namespace.
776778

777779

778780
.. method:: findtext(match, default=None, namespaces=None)
@@ -782,7 +784,8 @@ Element Objects
782784
of the first matching element, or *default* if no element was found.
783785
Note that if the matching element has no text content an empty string
784786
is returned. *namespaces* is an optional mapping from namespace prefix
785-
to full name.
787+
to full name. Pass ``None`` as prefix to move all unprefixed tag names
788+
in the expression into the given namespace.
786789

787790

788791
.. method:: getchildren()

Lib/test/test_xml_etree.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,6 +2463,12 @@ def test_findall_different_nsmaps(self):
24632463
nsmap = {'xx': 'Y'}
24642464
self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 1)
24652465
self.assertEqual(len(root.findall(".//b", namespaces=nsmap)), 2)
2466+
nsmap = {'xx': 'X', None: 'Y'}
2467+
self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 2)
2468+
self.assertEqual(len(root.findall(".//b", namespaces=nsmap)), 1)
2469+
nsmap = {'xx': 'X', '': 'Y'}
2470+
with self.assertRaisesRegex(ValueError, 'namespace prefix'):
2471+
root.findall(".//xx:b", namespaces=nsmap)
24662472

24672473
def test_bad_find(self):
24682474
e = ET.XML(SAMPLE_XML)

Lib/xml/etree/ElementPath.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,22 @@
7171
)
7272

7373
def xpath_tokenizer(pattern, namespaces=None):
74+
default_namespace = namespaces.get(None) if namespaces else None
7475
for token in xpath_tokenizer_re.findall(pattern):
7576
tag = token[1]
76-
if tag and tag[0] != "{" and ":" in tag:
77-
try:
77+
if tag and tag[0] != "{":
78+
if ":" in tag:
7879
prefix, uri = tag.split(":", 1)
79-
if not namespaces:
80-
raise KeyError
81-
yield token[0], "{%s}%s" % (namespaces[prefix], uri)
82-
except KeyError:
83-
raise SyntaxError("prefix %r not found in prefix map" % prefix) from None
80+
try:
81+
if not namespaces:
82+
raise KeyError
83+
yield token[0], "{%s}%s" % (namespaces[prefix], uri)
84+
except KeyError:
85+
raise SyntaxError("prefix %r not found in prefix map" % prefix) from None
86+
elif default_namespace:
87+
yield token[0], "{%s}%s" % (default_namespace, tag)
88+
else:
89+
yield token
8490
else:
8591
yield token
8692

@@ -264,10 +270,19 @@ def __init__(self, root):
264270

265271
def iterfind(elem, path, namespaces=None):
266272
# compile selector pattern
267-
cache_key = (path, None if namespaces is None
268-
else tuple(sorted(namespaces.items())))
269273
if path[-1:] == "/":
270274
path = path + "*" # implicit all (FIXME: keep this?)
275+
276+
cache_key = (path,)
277+
if namespaces:
278+
if '' in namespaces:
279+
raise ValueError("empty namespace prefix must be passed as None, not the empty string")
280+
if None in namespaces:
281+
cache_key += (namespaces[None],) + tuple(sorted(
282+
item for item in namespaces.items() if item[0] is not None))
283+
else:
284+
cache_key += tuple(sorted(namespaces.items()))
285+
271286
try:
272287
selector = _cache[cache_key]
273288
except KeyError:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Path expressions in xml.etree.ElementTree can now avoid explicit namespace
2+
prefixes for tags (or the "{namespace}tag" notation) by passing a default
3+
namespace with a 'None' prefix.

0 commit comments

Comments
 (0)