Skip to content

Commit 5bae975

Browse files
gh-68475: Add DocumentBuilder instead of TreeBuilder.get_document_children()
TreeBuilder is unchanged: it discards comments and processing instructions outside of the root element, and close() returns the root element. The new DocumentBuilder subclass keeps them (when insert_comments or insert_pis is true) and returns the list of the children of the document from close(), the only result channel of the target protocol. ElementTree.parse() loads such a list into children, XMLID() accepts it. In C, the DocumentBuilder type shares the TreeBuilder struct and handlers; the document list is NULL for a plain TreeBuilder. The parser calls the handlers directly for both exact types.
1 parent 811ff1f commit 5bae975

7 files changed

Lines changed: 365 additions & 135 deletions

File tree

Doc/library/xml.etree.elementtree.rst

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -609,15 +609,17 @@ Functions
609609
Parses an XML section from a string constant. Same as :func:`XML`. *text*
610610
is a string containing XML data. *parser* is an optional parser instance.
611611
If not given, the standard :class:`XMLParser` parser is used.
612-
Returns an :class:`Element` instance.
612+
Returns an :class:`Element` instance
613+
(the result of :meth:`XMLParser.close` with a custom *parser*).
613614

614615

615616
.. function:: fromstringlist(sequence, parser=None)
616617

617618
Parses an XML document from a sequence of string fragments. *sequence* is a
618619
list or other sequence containing XML data fragments. *parser* is an
619620
optional parser instance. If not given, the standard :class:`XMLParser`
620-
parser is used. Returns an :class:`Element` instance.
621+
parser is used. Returns an :class:`Element` instance
622+
(the result of :meth:`XMLParser.close` with a custom *parser*).
621623

622624
.. versionadded:: 3.2
623625

@@ -815,7 +817,8 @@ Functions
815817
Parses an XML section from a string constant. This function can be used to
816818
embed "XML literals" in Python code. *text* is a string containing XML
817819
data. *parser* is an optional parser instance. If not given, the standard
818-
:class:`XMLParser` parser is used. Returns an :class:`Element` instance.
820+
:class:`XMLParser` parser is used. Returns an :class:`Element` instance
821+
(the result of :meth:`XMLParser.close` with a custom *parser*).
819822

820823

821824
.. function:: XMLID(text, parser=None)
@@ -824,7 +827,11 @@ Functions
824827
which maps from element id:s to elements. *text* is a string containing XML
825828
data. *parser* is an optional parser instance. If not given, the standard
826829
:class:`XMLParser` parser is used. Returns a tuple containing an
827-
:class:`Element` instance and a dictionary.
830+
:class:`Element` instance (the result of :meth:`XMLParser.close` with
831+
a custom *parser*) and a dictionary.
832+
833+
.. versionchanged:: next
834+
Support a *parser* whose target is a :class:`DocumentBuilder`.
828835

829836

830837
.. _elementtree-xinclude:
@@ -1274,6 +1281,12 @@ ElementTree Objects
12741281
name or :term:`file object`. *parser* is an optional parser instance.
12751282
If not given, the standard :class:`XMLParser` parser is used. Returns the
12761283
section root element.
1284+
If the target of the parser is a :class:`DocumentBuilder`,
1285+
the comments and processing instructions outside of the root element
1286+
are loaded too, into :attr:`children`.
1287+
1288+
.. versionchanged:: next
1289+
Added support for :class:`DocumentBuilder`.
12771290

12781291

12791292
.. method:: write(file, encoding="us-ascii", xml_declaration=None, \
@@ -1388,12 +1401,8 @@ TreeBuilder Objects
13881401
create comments and processing instructions. When not given, the default
13891402
factories will be used. When *insert_comments* and/or *insert_pis* is true,
13901403
comments/pis will be inserted into the tree if they appear within the root
1391-
element. Those which appear outside of it are returned by
1392-
:meth:`get_document_children`.
1393-
1394-
.. versionchanged:: next
1395-
Comments and processing instructions outside the root element
1396-
are no longer discarded.
1404+
element. Those which appear outside of it are discarded;
1405+
use :class:`DocumentBuilder` to keep them.
13971406

13981407
.. method:: close()
13991408

@@ -1406,16 +1415,6 @@ TreeBuilder Objects
14061415
Adds text to the current element. *data* is a string.
14071416

14081417

1409-
.. method:: get_document_children()
1410-
1411-
Returns the children of the document:
1412-
the root element, and the comments and processing instructions
1413-
which were inserted outside of it.
1414-
Returns a list of :class:`Element` instances.
1415-
1416-
.. versionadded:: next
1417-
1418-
14191418
.. method:: end(tag)
14201419

14211420
Closes the current element. *tag* is the element name. Returns the
@@ -1473,6 +1472,27 @@ TreeBuilder Objects
14731472
.. versionadded:: 3.8
14741473

14751474

1475+
.. class:: DocumentBuilder(element_factory=None, *, comment_factory=None, \
1476+
pi_factory=None, insert_comments=False, \
1477+
insert_pis=False)
1478+
1479+
A :class:`TreeBuilder` which builds the whole document, not only the tree
1480+
of the root element.
1481+
The arguments are the same as for :class:`TreeBuilder`.
1482+
When *insert_comments* and/or *insert_pis* is true,
1483+
comments/pis which appear outside of the root element are kept
1484+
as the children of the document.
1485+
1486+
.. versionadded:: next
1487+
1488+
.. method:: close()
1489+
1490+
Flushes the builder buffers, and returns the children of the document:
1491+
the root element, and the comments and processing instructions
1492+
which were inserted outside of it.
1493+
Returns a list of :class:`Element` instances.
1494+
1495+
14761496
.. class:: C14NWriterTarget(write, *, \
14771497
with_comments=False, strip_text=False, rewrite_prefixes=False, \
14781498
qname_aware_tags=None, qname_aware_attrs=None, \

Doc/whatsnew/3.16.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,8 +707,8 @@ xml
707707
:class:`~xml.etree.ElementTree.ElementTree` now has the
708708
:attr:`~xml.etree.ElementTree.ElementTree.children` attribute,
709709
a sequence of the children of the document,
710-
and :class:`~xml.etree.ElementTree.TreeBuilder` returns them
711-
from the new :meth:`!get_document_children` method
710+
and the new :class:`~xml.etree.ElementTree.DocumentBuilder` parser target
711+
returns them from its ``close()`` method
712712
when *insert_comments* or *insert_pis* is true.
713713
(Contributed by Serhiy Storchaka in :gh:`68475`.)
714714

Lib/test/test_xml_etree.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4104,7 +4104,7 @@ class DocumentChildrenTest(unittest.TestCase):
41044104
sample = ('<!--lead--><?pi data?><r><?in?><a/></r><?after?><!--tail-->')
41054105

41064106
def parse(self, text=None):
4107-
builder = ET.TreeBuilder(insert_comments=True, insert_pis=True)
4107+
builder = ET.DocumentBuilder(insert_comments=True, insert_pis=True)
41084108
tree = ET.ElementTree()
41094109
tree.parse(io.StringIO(text if text is not None else self.sample),
41104110
ET.XMLParser(target=builder))
@@ -4117,6 +4117,14 @@ def test_only_the_root_by_default(self):
41174117
self.assertEqual(len(tree.children), 1)
41184118
self.assertIs(tree.children[0], tree.getroot())
41194119

4120+
def test_tree_builder_discards_the_prolog_and_the_epilog(self):
4121+
builder = ET.TreeBuilder(insert_comments=True, insert_pis=True)
4122+
tree = ET.ElementTree()
4123+
tree.parse(io.StringIO(self.sample), ET.XMLParser(target=builder))
4124+
self.assertEqual(summarize_list(tree.children), ['r'])
4125+
self.assertEqual(summarize_list(tree.getroot()),
4126+
[ET.ProcessingInstruction, 'a'])
4127+
41204128
def test_parse_keeps_the_prolog_and_the_epilog(self):
41214129
tree = self.parse()
41224130
self.assertEqual(summarize_list(tree.children),
@@ -4254,21 +4262,46 @@ def test_document_without_the_root(self):
42544262
tree.write(file, encoding='unicode')
42554263
self.assertEqual(file.getvalue(), '<!--a--><?p?>')
42564264

4257-
def test_builder_document(self):
4258-
builder = ET.TreeBuilder(insert_comments=True, insert_pis=True)
4265+
def test_document_builder(self):
4266+
builder = ET.DocumentBuilder(insert_comments=True, insert_pis=True)
42594267
parser = ET.XMLParser(target=builder)
42604268
parser.feed(self.sample)
4261-
root = parser.close()
4262-
self.assertEqual(root.tag, 'r')
4263-
self.assertEqual(len(builder.get_document_children()), 5)
4264-
self.assertIs(builder.get_document_children()[2], root)
4269+
document = parser.close()
4270+
self.assertIsInstance(document, list)
4271+
self.assertEqual(summarize_list(document),
4272+
[ET.Comment, ET.ProcessingInstruction, 'r',
4273+
ET.ProcessingInstruction, ET.Comment])
4274+
self.assertEqual(summarize_list(document[2]),
4275+
[ET.ProcessingInstruction, 'a'])
42654276

4266-
def test_builder_document_without_inserting(self):
4267-
builder = ET.TreeBuilder()
4277+
def test_document_builder_without_inserting(self):
4278+
builder = ET.DocumentBuilder()
42684279
parser = ET.XMLParser(target=builder)
42694280
parser.feed(self.sample)
4270-
root = parser.close()
4271-
self.assertEqual(builder.get_document_children(), [root])
4281+
document = parser.close()
4282+
self.assertEqual(summarize_list(document), ['r'])
4283+
self.assertEqual(summarize_list(document[0]), ['a'])
4284+
4285+
def test_document_builder_subclass(self):
4286+
class Builder(ET.DocumentBuilder):
4287+
pass
4288+
builder = Builder(insert_comments=True)
4289+
parser = ET.XMLParser(target=builder)
4290+
parser.feed(self.sample)
4291+
self.assertEqual(summarize_list(parser.close()),
4292+
[ET.Comment, 'r', ET.Comment])
4293+
4294+
def test_document_builder_xmlid(self):
4295+
parser = ET.XMLParser(target=ET.DocumentBuilder(insert_comments=True))
4296+
document, ids = ET.XMLID('<!--c--><r id="x"><a id="y"/></r>', parser)
4297+
self.assertEqual(summarize_list(document), [ET.Comment, 'r'])
4298+
self.assertEqual(sorted(ids), ['x', 'y'])
4299+
self.assertIs(ids['x'], document[1])
4300+
4301+
def test_document_builder_fromstring(self):
4302+
parser = ET.XMLParser(target=ET.DocumentBuilder(insert_comments=True))
4303+
document = ET.fromstring(self.sample, parser)
4304+
self.assertEqual(summarize_list(document), [ET.Comment, 'r', ET.Comment])
42724305

42734306
class TreeBuilderTest(unittest.TestCase):
42744307
sample1 = ('<!DOCTYPE html PUBLIC'

Lib/xml/etree/ElementTree.py

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"QName",
8383
"SubElement",
8484
"tostring", "tostringlist",
85-
"TreeBuilder",
85+
"TreeBuilder", "DocumentBuilder",
8686
"XML", "XMLID",
8787
"XMLParser", "XMLPullParser",
8888
"register_namespace",
@@ -698,15 +698,13 @@ def parse(self, source, parser=None):
698698
return self._root
699699
while data := source.read(65536):
700700
parser.feed(data)
701-
# close() releases the target, ask it for the document first
702-
document = getattr(getattr(parser, 'target', None),
703-
'get_document_children', None)
704701
result = parser.close()
705-
# a custom target can return anything, even None
706-
self._root = result
707-
if document is not None:
708-
self._children[:] = document()
702+
if isinstance(result, list):
703+
# a DocumentBuilder returns the children of the document
704+
self.children[:] = result
709705
else:
706+
# a custom target can return anything, even None
707+
self._root = result
710708
self._children = [result] if iselement(result) else []
711709
return self._root
712710
finally:
@@ -1546,10 +1544,13 @@ def XMLID(text, parser=None):
15461544
parser.feed(text)
15471545
tree = parser.close()
15481546
ids = {}
1549-
for elem in tree.iter():
1550-
id = elem.get("id")
1551-
if id:
1552-
ids[id] = elem
1547+
# a DocumentBuilder returns the children of the document
1548+
nodes = tree if isinstance(tree, list) else [tree]
1549+
for node in nodes:
1550+
for elem in node.iter():
1551+
id = elem.get("id")
1552+
if id:
1553+
ids[id] = elem
15531554
return tree, ids
15541555

15551556
# Parse XML document from string constant. Alias for XML().
@@ -1600,7 +1601,6 @@ def __init__(self, element_factory=None, *,
16001601
self._elem = [] # element stack
16011602
self._last = None # last element
16021603
self._root = None # root element
1603-
self._document = [] # the children of the document
16041604
self._tail = None # true if we're after an end tag
16051605
if comment_factory is None:
16061606
comment_factory = Comment
@@ -1649,7 +1649,6 @@ def start(self, tag, attrs):
16491649
self._elem[-1].append(elem)
16501650
elif self._root is None:
16511651
self._root = elem
1652-
self._document.append(elem)
16531652
self._elem.append(elem)
16541653
self._tail = 0
16551654
return elem
@@ -1692,19 +1691,39 @@ def _handle_single(self, factory, insert, *args):
16921691
self._last = elem
16931692
if self._elem:
16941693
self._elem[-1].append(elem)
1695-
else:
1696-
# outside the root element: the prolog or the epilog
1697-
self._document.append(elem)
16981694
self._tail = 1
16991695
return elem
17001696

1701-
def get_document_children(self):
1702-
"""Return the children of the document.
17031697

1704-
These are the root element and the comments and processing
1705-
instructions which were inserted outside of it.
1706-
"""
1707-
return list(self._document)
1698+
class DocumentBuilder(TreeBuilder):
1699+
"""Generic document structure builder.
1700+
1701+
This builder is like TreeBuilder, but its close() method returns
1702+
the list of the children of the document: the root element, and
1703+
the comments and processing instructions outside of it if
1704+
*insert_comments* or *insert_pis* is true.
1705+
"""
1706+
def __init__(self, *args, **kwargs):
1707+
super().__init__(*args, **kwargs)
1708+
self._document = [] # the children of the document
1709+
1710+
def close(self):
1711+
"""Flush builder buffers and return the children of the document."""
1712+
assert len(self._elem) == 0, "missing end tags"
1713+
return self._document
1714+
1715+
def start(self, tag, attrs):
1716+
elem = super().start(tag, attrs)
1717+
if elem is self._root:
1718+
self._document.append(elem)
1719+
return elem
1720+
1721+
def _handle_single(self, factory, insert, *args):
1722+
elem = super()._handle_single(factory, insert, *args)
1723+
if insert and not self._elem:
1724+
# outside the root element: the prolog or the epilog
1725+
self._document.append(elem)
1726+
return elem
17081727

17091728

17101729
# also see ElementTree and TreeBuilder

Misc/NEWS.d/next/Library/2026-08-31-12-00-00.gh-issue-68475.Nq7Vt2.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
the :attr:`~xml.etree.ElementTree.ElementTree.children` attribute,
33
a sequence of the children of the document:
44
the root element and the comments and processing instructions around it.
5-
When *insert_comments* or *insert_pis* is true,
6-
:class:`~xml.etree.ElementTree.TreeBuilder` no longer discards comments
7-
and processing instructions which occur outside of the root element;
8-
they are returned by the new :meth:`!get_document_children` method
9-
and are used by :func:`~xml.etree.ElementTree.parse`.
5+
The new :class:`~xml.etree.ElementTree.DocumentBuilder` parser target
6+
keeps comments and processing instructions which occur outside of the root
7+
element when *insert_comments* or *insert_pis* is true,
8+
and returns the children of the document from its ``close()`` method,
9+
which is used by :func:`~xml.etree.ElementTree.parse`.

0 commit comments

Comments
 (0)