bpo-30485: support a default prefix mapping in ElementPath by passing… · python/cpython@e9927e1 (original) (raw)
4 files changed
lines changed
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -764,15 +764,17 @@ Element Objects | ||
764 | 764 | Finds the first subelement matching *match*. *match* may be a tag name |
765 | 765 | or a :ref:`path <elementtree-xpath>`. Returns an element instance |
766 | 766 | 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. | |
768 | 769 | |
769 | 770 | |
770 | 771 | .. method:: findall(match, namespaces=None) |
771 | 772 | |
772 | 773 | Finds all matching subelements, by tag name or |
773 | 774 | :ref:`path <elementtree-xpath>`. Returns a list containing all matching |
774 | 775 | 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. | |
776 | 778 | |
777 | 779 | |
778 | 780 | .. method:: findtext(match, default=None, namespaces=None) |
@@ -782,7 +784,8 @@ Element Objects | ||
782 | 784 | of the first matching element, or *default* if no element was found. |
783 | 785 | Note that if the matching element has no text content an empty string |
784 | 786 | 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. | |
786 | 789 | |
787 | 790 | |
788 | 791 | .. method:: getchildren() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -2463,6 +2463,12 @@ def test_findall_different_nsmaps(self): | ||
2463 | 2463 | nsmap = {'xx': 'Y'} |
2464 | 2464 | self.assertEqual(len(root.findall(".//xx:b", namespaces=nsmap)), 1) |
2465 | 2465 | 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) | |
2466 | 2472 | |
2467 | 2473 | def test_bad_find(self): |
2468 | 2474 | e = ET.XML(SAMPLE_XML) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -71,16 +71,22 @@ | ||
71 | 71 | ) |
72 | 72 | |
73 | 73 | def xpath_tokenizer(pattern, namespaces=None): |
74 | +default_namespace = namespaces.get(None) if namespaces else None | |
74 | 75 | for token in xpath_tokenizer_re.findall(pattern): |
75 | 76 | tag = token[1] |
76 | -if tag and tag[0] != "{" and ":" in tag: | |
77 | -try: | |
77 | +if tag and tag[0] != "{": | |
78 | +if ":" in tag: | |
78 | 79 | 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 | |
84 | 90 | else: |
85 | 91 | yield token |
86 | 92 | |
@@ -264,10 +270,19 @@ def __init__(self, root): | ||
264 | 270 | |
265 | 271 | def iterfind(elem, path, namespaces=None): |
266 | 272 | # compile selector pattern |
267 | -cache_key = (path, None if namespaces is None | |
268 | -else tuple(sorted(namespaces.items()))) | |
269 | 273 | if path[-1:] == "/": |
270 | 274 | 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 | + | |
271 | 286 | try: |
272 | 287 | selector = _cache[cache_key] |
273 | 288 | except KeyError: |
Original file line number | Diff line number | Diff 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. |