diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 4f2497c8246be30..0a34d43b2b5578c 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -609,7 +609,8 @@ Functions Parses an XML section from a string constant. Same as :func:`XML`. *text* is a string containing XML data. *parser* is an optional parser instance. If not given, the standard :class:`XMLParser` parser is used. - Returns an :class:`Element` instance. + Returns an :class:`Element` instance + (the result of :meth:`XMLParser.close` with a custom *parser*). .. function:: fromstringlist(sequence, parser=None) @@ -617,7 +618,8 @@ Functions Parses an XML document from a sequence of string fragments. *sequence* is a list or other sequence containing XML data fragments. *parser* is an optional parser instance. If not given, the standard :class:`XMLParser` - parser is used. Returns an :class:`Element` instance. + parser is used. Returns an :class:`Element` instance + (the result of :meth:`XMLParser.close` with a custom *parser*). .. versionadded:: 3.2 @@ -815,7 +817,8 @@ Functions Parses an XML section from a string constant. This function can be used to embed "XML literals" in Python code. *text* is a string containing XML data. *parser* is an optional parser instance. If not given, the standard - :class:`XMLParser` parser is used. Returns an :class:`Element` instance. + :class:`XMLParser` parser is used. Returns an :class:`Element` instance + (the result of :meth:`XMLParser.close` with a custom *parser*). .. function:: XMLID(text, parser=None) @@ -824,7 +827,11 @@ Functions which maps from element id:s to elements. *text* is a string containing XML data. *parser* is an optional parser instance. If not given, the standard :class:`XMLParser` parser is used. Returns a tuple containing an - :class:`Element` instance and a dictionary. + :class:`Element` instance (the result of :meth:`XMLParser.close` with + a custom *parser*) and a dictionary. + + .. versionchanged:: next + Support a *parser* whose target is a :class:`DocumentBuilder`. .. _elementtree-xinclude: @@ -1194,6 +1201,34 @@ ElementTree Objects of the XML *file* if given. + .. attribute:: children + + A sequence of the children of the document: + the root element and the comments and processing instructions + which surround it. + It can contain at most one element, + which is the root element of the tree; + adding a second one raises :exc:`ValueError`, + and adding anything which is not an element raises :exc:`TypeError`. + + It supports ``len()``, iteration, the :keyword:`in` operator, + :func:`reversed`, indexing and slicing (for getting, setting and + deleting), and the methods :meth:`!append`, :meth:`!insert`, + :meth:`!extend`, :meth:`!remove` and :meth:`!clear`, + which have the same signatures as the methods of :class:`list`. + It is a view of the tree: it changes when the tree changes, + and changing it changes the tree. + + Comments and processing instructions are only added to it when parsing + if the parser target collects them; see :class:`TreeBuilder`. + + :meth:`iter` iterates over all children of the document, + but :meth:`find`, :meth:`findall` and :meth:`iterfind` + search from the root element, so they never return the other children. + + .. versionadded:: next + + .. method:: _setroot(element) Replaces the root element for this tree. This discards the current @@ -1223,9 +1258,14 @@ ElementTree Objects .. method:: iter(tag=None) - Creates and returns a tree iterator for the root element. The iterator - loops over all elements in this tree, in section order. *tag* is the tag - to look for (default is to return all elements). + Creates and returns a tree iterator for the document. + The iterator loops over all children of the document + and their descendants, in document order. + *tag* is the tag to look for (default is to return all elements). + + .. versionchanged:: next + It iterates over all children of the document, + not only over the root element and its descendants. .. method:: iterfind(match, namespaces=None) @@ -1241,6 +1281,12 @@ ElementTree Objects name or :term:`file object`. *parser* is an optional parser instance. If not given, the standard :class:`XMLParser` parser is used. Returns the section root element. + If the target of the parser is a :class:`DocumentBuilder`, + the comments and processing instructions outside of the root element + are loaded too, into :attr:`children`. + + .. versionchanged:: next + Added support for :class:`DocumentBuilder`. .. method:: write(file, encoding="us-ascii", xml_declaration=None, \ @@ -1355,7 +1401,8 @@ TreeBuilder Objects create comments and processing instructions. When not given, the default factories will be used. When *insert_comments* and/or *insert_pis* is true, comments/pis will be inserted into the tree if they appear within the root - element (but not outside of it). + element. Those which appear outside of it are discarded; + use :class:`DocumentBuilder` to keep them. .. method:: close() @@ -1425,6 +1472,27 @@ TreeBuilder Objects .. versionadded:: 3.8 +.. class:: DocumentBuilder(element_factory=None, *, comment_factory=None, \ + pi_factory=None, insert_comments=False, \ + insert_pis=False) + + A :class:`TreeBuilder` which builds the whole document, not only the tree + of the root element. + The arguments are the same as for :class:`TreeBuilder`. + When *insert_comments* and/or *insert_pis* is true, + comments/pis which appear outside of the root element are kept + as the children of the document. + + .. versionadded:: next + + .. method:: close() + + Flushes the builder buffers, and returns the children of the document: + the root element, and the comments and processing instructions + which were inserted outside of it. + Returns a list of :class:`Element` instances. + + .. class:: C14NWriterTarget(write, *, \ with_comments=False, strip_text=False, rewrite_prefixes=False, \ qname_aware_tags=None, qname_aware_attrs=None, \ diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 2eccc8e35605596..a254c9a5afddea1 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -702,6 +702,16 @@ xml and :meth:`!Document.createEntityReference`. (Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.) +* Comments and processing instructions which occur outside of the root element + are no longer lost in :mod:`xml.etree.ElementTree`. + :class:`~xml.etree.ElementTree.ElementTree` now has the + :attr:`~xml.etree.ElementTree.ElementTree.children` attribute, + a sequence of the children of the document, + and the new :class:`~xml.etree.ElementTree.DocumentBuilder` parser target + returns them from its ``close()`` method + when *insert_comments* or *insert_pis* is true. + (Contributed by Serhiy Storchaka in :gh:`68475`.) + * Add :meth:`!GetSpecifiedAttributeCount` method to the :mod:`XML parser ` objects. It tells how many of the reported attributes were given in the start tag diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index f87a47045dd1713..6ffb428a6c6c1dd 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -4000,7 +4000,7 @@ def test_basic(self): self.assertEqual(next(ET.iterparse(sourcefile, parser=parser))[0], 'end') tree = ET.ElementTree(None) - self.assertRaises(AttributeError, tree.iter) + self.assertEqual(list(tree.iter()), []) # Issue #16913 doc = ET.XML("a&b&c&") @@ -4097,6 +4097,212 @@ def test_pickle(self): pickle.dumps(it, proto) + +class DocumentChildrenTest(unittest.TestCase): + # gh-68475: comments and processing instructions outside the root element + + sample = ('') + + def parse(self, text=None): + builder = ET.DocumentBuilder(insert_comments=True, insert_pis=True) + tree = ET.ElementTree() + tree.parse(io.StringIO(text if text is not None else self.sample), + ET.XMLParser(target=builder)) + return tree + + def test_only_the_root_by_default(self): + tree = ET.ElementTree() + tree.parse(io.StringIO(self.sample)) + self.assertEqual(summarize_list(tree.children), ['r']) + self.assertEqual(len(tree.children), 1) + self.assertIs(tree.children[0], tree.getroot()) + + def test_tree_builder_discards_the_prolog_and_the_epilog(self): + builder = ET.TreeBuilder(insert_comments=True, insert_pis=True) + tree = ET.ElementTree() + tree.parse(io.StringIO(self.sample), ET.XMLParser(target=builder)) + self.assertEqual(summarize_list(tree.children), ['r']) + self.assertEqual(summarize_list(tree.getroot()), + [ET.ProcessingInstruction, 'a']) + + def test_parse_keeps_the_prolog_and_the_epilog(self): + tree = self.parse() + self.assertEqual(summarize_list(tree.children), + [ET.Comment, ET.ProcessingInstruction, 'r', + ET.ProcessingInstruction, ET.Comment]) + self.assertEqual(tree.children[0].text, 'lead') + self.assertEqual(tree.children[-1].text, 'tail') + self.assertIs(tree.getroot(), tree.children[2]) + + def test_write(self): + tree = self.parse() + file = io.StringIO() + tree.write(file, encoding='unicode') + self.assertEqual(file.getvalue(), + '' + '') + + def test_iter(self): + tree = self.parse() + self.assertEqual(summarize_list(tree.iter()), + [ET.Comment, ET.ProcessingInstruction, 'r', + ET.ProcessingInstruction, 'a', + ET.ProcessingInstruction, ET.Comment]) + self.assertEqual(summarize_list(tree.iter('*')), + [ET.Comment, ET.ProcessingInstruction, 'r', + ET.ProcessingInstruction, 'a', + ET.ProcessingInstruction, ET.Comment]) + self.assertEqual(summarize_list(tree.iter('a')), ['a']) + # comments and processing instructions can be selected by the factory + self.assertEqual(summarize_list(tree.iter(ET.ProcessingInstruction)), + [ET.ProcessingInstruction] * 3) + self.assertEqual(summarize_list(tree.iter(ET.Comment)), + [ET.Comment, ET.Comment]) + + def test_find_searches_from_the_root(self): + tree = self.parse() + # find() and friends search from the root element, so they return + # the processing instruction inside it, but not those outside + self.assertEqual(summarize_list(tree.findall('*')), + [ET.ProcessingInstruction, 'a']) + self.assertEqual(summarize_list(tree.findall('.//*')), + [ET.ProcessingInstruction, 'a']) + self.assertEqual(tree.find('a').tag, 'a') + + def test_append_and_insert(self): + tree = ET.ElementTree(ET.Element('r')) + tree.children.insert(0, ET.Comment('lead')) + tree.children.append(ET.ProcessingInstruction('pi', 'data')) + self.assertEqual(summarize_list(tree.children), + [ET.Comment, 'r', ET.ProcessingInstruction]) + self.assertIs(tree.getroot(), tree.children[1]) + + def test_the_first_element_becomes_the_root(self): + tree = ET.ElementTree() + tree.children.append(ET.Comment('lead')) + self.assertIsNone(tree.getroot()) + elem = ET.Element('r') + tree.children.append(elem) + self.assertIs(tree.getroot(), elem) + + def test_only_one_element(self): + tree = ET.ElementTree(ET.Element('r')) + children = tree.children + children.insert(0, ET.Comment('lead')) + self.assertRaises(ValueError, children.append, ET.Element('second')) + self.assertRaises(ValueError, children.insert, 0, ET.Element('second')) + self.assertRaises(ValueError, children.extend, [ET.Element('second')]) + # the comment cannot be replaced by an element either + self.assertRaises(ValueError, children.__setitem__, 0, + ET.Element('second')) + self.assertEqual(summarize_list(tree.children), [ET.Comment, 'r']) + self.assertEqual(tree.getroot().tag, 'r') + # but the root element can be replaced + children[1] = ET.Element('other') + self.assertEqual(tree.getroot().tag, 'other') + + def test_not_an_element(self): + tree = ET.ElementTree(ET.Element('r')) + self.assertRaises(TypeError, tree.children.append, 'text') + self.assertRaises(TypeError, tree.children.insert, 0, None) + self.assertEqual(summarize_list(tree.children), ['r']) + + def test_remove_and_delete(self): + tree = self.parse() + root = tree.getroot() + tree.children.remove(root) + self.assertIsNone(tree.getroot()) + self.assertEqual(summarize_list(tree.children), + [ET.Comment, ET.ProcessingInstruction, + ET.ProcessingInstruction, ET.Comment]) + del tree.children[0] + self.assertEqual(summarize_list(tree.children), + [ET.ProcessingInstruction, ET.ProcessingInstruction, + ET.Comment]) + tree.children.clear() + self.assertEqual(summarize_list(tree.children), []) + self.assertIsNone(tree.getroot()) + + def test_slices(self): + tree = self.parse() + root = tree.getroot() + tree.children[0:2] = [ET.Comment('one')] + self.assertEqual(summarize_list(tree.children), + [ET.Comment, 'r', ET.ProcessingInstruction, + ET.Comment]) + self.assertIs(tree.getroot(), root) + # the slice which replaces the root element can add another one + new = ET.Element('new') + tree.children[1:2] = [new] + self.assertIs(tree.getroot(), new) + # but not two + self.assertRaises(ValueError, tree.children.__setitem__, + slice(0, 2), [ET.Element('a'), ET.Element('b')]) + self.assertIs(tree.getroot(), new) + del tree.children[1:2] + self.assertIsNone(tree.getroot()) + + def test_the_root_cannot_be_a_comment(self): + self.assertRaises(ValueError, ET.ElementTree, ET.Comment('c')) + self.assertRaises(ValueError, ET.ElementTree, + ET.ProcessingInstruction('pi')) + tree = ET.ElementTree(ET.Element('r')) + self.assertRaises(ValueError, tree._setroot, ET.Comment('c')) + + def test_tostring_of_a_comment(self): + # tostring() serializes a single node, which can be a comment + self.assertEqual(ET.tostring(ET.Comment('c')), b'') + self.assertEqual(ET.tostring(ET.ProcessingInstruction('t', 'd')), + b'') + + def test_document_without_the_root(self): + tree = ET.ElementTree() + tree.children.extend([ET.Comment('a'), ET.ProcessingInstruction('p')]) + file = io.StringIO() + tree.write(file, encoding='unicode') + self.assertEqual(file.getvalue(), '') + + def test_document_builder(self): + builder = ET.DocumentBuilder(insert_comments=True, insert_pis=True) + parser = ET.XMLParser(target=builder) + parser.feed(self.sample) + document = parser.close() + self.assertIsInstance(document, list) + self.assertEqual(summarize_list(document), + [ET.Comment, ET.ProcessingInstruction, 'r', + ET.ProcessingInstruction, ET.Comment]) + self.assertEqual(summarize_list(document[2]), + [ET.ProcessingInstruction, 'a']) + + def test_document_builder_without_inserting(self): + builder = ET.DocumentBuilder() + parser = ET.XMLParser(target=builder) + parser.feed(self.sample) + document = parser.close() + self.assertEqual(summarize_list(document), ['r']) + self.assertEqual(summarize_list(document[0]), ['a']) + + def test_document_builder_subclass(self): + class Builder(ET.DocumentBuilder): + pass + builder = Builder(insert_comments=True) + parser = ET.XMLParser(target=builder) + parser.feed(self.sample) + self.assertEqual(summarize_list(parser.close()), + [ET.Comment, 'r', ET.Comment]) + + def test_document_builder_xmlid(self): + parser = ET.XMLParser(target=ET.DocumentBuilder(insert_comments=True)) + document, ids = ET.XMLID('', parser) + self.assertEqual(summarize_list(document), [ET.Comment, 'r']) + self.assertEqual(sorted(ids), ['x', 'y']) + self.assertIs(ids['x'], document[1]) + + def test_document_builder_fromstring(self): + parser = ET.XMLParser(target=ET.DocumentBuilder(insert_comments=True)) + document = ET.fromstring(self.sample, parser) + self.assertEqual(summarize_list(document), [ET.Comment, 'r', ET.Comment]) + class TreeBuilderTest(unittest.TestCase): sample1 = ('' % (self._tree._children,) + + def _check(self, value, root): + # Check a new child of a document whose root element is *root*, + # and return the root element after adding it. + if not iselement(value): + raise TypeError('expected an Element, not %s' + % type(value).__name__) + if _is_misc(value): + return root + if root is not None: + raise ValueError('a document can have only one element child') + return value + + def _contains_root(self, nodes): + root = self._tree._root + return root is not None and any(node is root for node in nodes) + + def append(self, value): + """Add a child at the end of the document.""" + root = self._check(value, self._tree._root) + self._tree._children.append(value) + self._tree._root = root + + def insert(self, index, value): + """Add a child at the given position.""" + root = self._check(value, self._tree._root) + self._tree._children.insert(index, value) + self._tree._root = root + + def extend(self, values): + """Add several children at the end of the document.""" + values = list(values) + root = self._tree._root + for item in values: + root = self._check(item, root) + self._tree._children.extend(values) + self._tree._root = root + + def remove(self, value): + """Remove the first child equal to the value.""" + self._tree._children.remove(value) + if value is self._tree._root: + self._tree._root = None + + def clear(self): + """Remove all children of the document.""" + self._tree._children.clear() + self._tree._root = None + + def __setitem__(self, index, value): + """Replace the child at the index, or the children in the slice.""" + children = self._tree._children + root = self._tree._root + if isinstance(index, slice): + value = list(value) + if self._contains_root(children[index]): + root = None + for item in value: + root = self._check(item, root) + else: + if children[index] is root: + root = None + root = self._check(value, root) + children[index] = value + self._tree._root = root + + def __delitem__(self, index): + """Remove the child at the index, or the children in the slice.""" + children = self._tree._children + if isinstance(index, slice): + root_removed = self._contains_root(children[index]) + else: + root_removed = children[index] is self._tree._root + del children[index] + if root_removed: + self._tree._root = None + + class ElementTree: """An XML element hierarchy. @@ -527,13 +635,18 @@ class ElementTree: """ def __init__(self, element=None, file=None): - if element is not None and not iselement(element): - raise TypeError('expected an Element, not %s' % - type(element).__name__) - self._root = element # first node + self._root = None # the root element + self._children = [] + if element is not None: + self._setroot(element) if file: self.parse(file) + @property + def children(self): + """A view of the children of the document.""" + return _DocumentChildren(self) + def getroot(self): """Return root element of this tree.""" return self._root @@ -548,6 +661,13 @@ def _setroot(self, element): if not iselement(element): raise TypeError('expected an Element, not %s' % type(element).__name__) + if _is_misc(element): + raise ValueError('the root element cannot be a comment ' + 'or a processing instruction') + if self._root is None: + self._children.append(element) + else: + self._children[self._children.index(self._root)] = element self._root = element def parse(self, source, parser=None): @@ -574,28 +694,35 @@ def parse(self, source, parser=None): # can define an internal _parse_whole API for efficiency. # It can be used to parse the whole source without feeding # it with chunks. - self._root = parser._parse_whole(source) + self._setroot(parser._parse_whole(source)) return self._root while data := source.read(65536): parser.feed(data) - self._root = parser.close() + result = parser.close() + if isinstance(result, list): + # a DocumentBuilder returns the children of the document + self.children[:] = result + else: + # a custom target can return anything, even None + self._root = result + self._children = [result] if iselement(result) else [] return self._root finally: if close_source: source.close() def iter(self, tag=None): - """Create and return tree iterator for the root element. + """Create and return tree iterator for the document. - The iterator loops over all elements in this tree, in document - order. + The iterator loops over all children of the document and their + descendants, in document order. *tag* is a string with the tag name to iterate over (default is to return all elements). """ - # assert self._root is not None - return self._root.iter(tag) + for child in self._children: + yield from child.iter(tag) def find(self, path, namespaces=None): """Find first matching element by tag name or path. @@ -724,7 +851,7 @@ def write(self, file_or_filename, standalone document declaration is omitted """ - if self._root is None: + if not self._children: raise TypeError('ElementTree not initialized') if not method: method = "xml" @@ -752,12 +879,20 @@ def write(self, file_or_filename, write("\n" % ( declared_encoding, sddecl)) if method == "text": - _serialize_text(write, self._root) + for child in self._children: + _serialize_text(write, child) else: - qnames, namespaces = _namespaces(self._root, default_namespace) + root = self._root + if root is None: + # the document has no element child + qnames, namespaces = {None: None}, {} + else: + qnames, namespaces = _namespaces(root, default_namespace) serialize = _serialize[method] - serialize(write, self._root, qnames, namespaces, - short_empty_elements=short_empty_elements) + for child in self._children: + serialize(write, child, qnames, + namespaces if child is root else None, + short_empty_elements=short_empty_elements) # -------------------------------------------------------------------- # serialization support @@ -1113,7 +1248,10 @@ def tostring(element, encoding=None, method=None, *, """ stream = io.StringIO() if encoding == 'unicode' else io.BytesIO() - ElementTree(element).write(stream, encoding, + # the element can also be a comment or a processing instruction + tree = ElementTree() + tree.children.append(element) + tree.write(stream, encoding, xml_declaration=xml_declaration, default_namespace=default_namespace, method=method, @@ -1143,7 +1281,10 @@ def tostringlist(element, encoding=None, method=None, *, short_empty_elements=True, standalone=None): lst = [] stream = _ListDataStream(lst) - ElementTree(element).write(stream, encoding, + # the element can also be a comment or a processing instruction + tree = ElementTree() + tree.children.append(element) + tree.write(stream, encoding, xml_declaration=xml_declaration, default_namespace=default_namespace, method=method, @@ -1403,10 +1544,13 @@ def XMLID(text, parser=None): parser.feed(text) tree = parser.close() ids = {} - for elem in tree.iter(): - id = elem.get("id") - if id: - ids[id] = elem + # a DocumentBuilder returns the children of the document + nodes = tree if isinstance(tree, list) else [tree] + for node in nodes: + for elem in node.iter(): + id = elem.get("id") + if id: + ids[id] = elem return tree, ids # Parse XML document from string constant. Alias for XML(). @@ -1551,6 +1695,37 @@ def _handle_single(self, factory, insert, *args): return elem +class DocumentBuilder(TreeBuilder): + """Generic document structure builder. + + This builder is like TreeBuilder, but its close() method returns + the list of the children of the document: the root element, and + the comments and processing instructions outside of it if + *insert_comments* or *insert_pis* is true. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._document = [] # the children of the document + + def close(self): + """Flush builder buffers and return the children of the document.""" + assert len(self._elem) == 0, "missing end tags" + return self._document + + def start(self, tag, attrs): + elem = super().start(tag, attrs) + if elem is self._root: + self._document.append(elem) + return elem + + def _handle_single(self, factory, insert, *args): + elem = super()._handle_single(factory, insert, *args) + if insert and not self._elem: + # outside the root element: the prolog or the epilog + self._document.append(elem) + return elem + + # also see ElementTree and TreeBuilder class XMLParser: """Element structure builder for XML source data based on the expat parser. @@ -2113,6 +2288,7 @@ def _escape_attrib_c14n(text): # the Python version of it accessible for some "creative" by external code # (see tests) _Element_Py = Element + _TreeBuilder_Py = TreeBuilder # Element, SubElement, ParseError, TreeBuilder, XMLParser, _set_factories from _elementtree import * diff --git a/Misc/NEWS.d/next/Library/2026-08-31-12-00-00.gh-issue-68475.Nq7Vt2.rst b/Misc/NEWS.d/next/Library/2026-08-31-12-00-00.gh-issue-68475.Nq7Vt2.rst new file mode 100644 index 000000000000000..9c45459aeae44a8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-31-12-00-00.gh-issue-68475.Nq7Vt2.rst @@ -0,0 +1,9 @@ +:class:`~xml.etree.ElementTree.ElementTree` now has +the :attr:`~xml.etree.ElementTree.ElementTree.children` attribute, +a sequence of the children of the document: +the root element and the comments and processing instructions around it. +The new :class:`~xml.etree.ElementTree.DocumentBuilder` parser target +keeps comments and processing instructions which occur outside of the root +element when *insert_comments* or *insert_pis* is true, +and returns the children of the document from its ``close()`` method, +which is used by :func:`~xml.etree.ElementTree.parse`. diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index 36115e61c2c2152..91ae5bd5946b3e2 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -99,6 +99,7 @@ typedef struct { PyTypeObject *Element_Type; PyTypeObject *ElementIter_Type; PyTypeObject *TreeBuilder_Type; + PyTypeObject *DocumentBuilder_Type; PyTypeObject *XMLParser_Type; PyObject *expat_capsule; @@ -158,6 +159,7 @@ elementtree_clear(PyObject *m) Py_CLEAR(st->Element_Type); Py_CLEAR(st->ElementIter_Type); Py_CLEAR(st->TreeBuilder_Type); + Py_CLEAR(st->DocumentBuilder_Type); Py_CLEAR(st->XMLParser_Type); Py_CLEAR(st->expat_capsule); @@ -179,6 +181,7 @@ elementtree_traverse(PyObject *m, visitproc visit, void *arg) Py_VISIT(st->Element_Type); Py_VISIT(st->ElementIter_Type); Py_VISIT(st->TreeBuilder_Type); + Py_VISIT(st->DocumentBuilder_Type); Py_VISIT(st->XMLParser_Type); Py_VISIT(st->expat_capsule); return 0; @@ -409,9 +412,10 @@ get_attrib_from_keywords(PyObject *kwds) module _elementtree class _elementtree.Element "ElementObject *" "clinic_state()->Element_Type" class _elementtree.TreeBuilder "TreeBuilderObject *" "clinic_state()->TreeBuilder_Type" +class _elementtree.DocumentBuilder "TreeBuilderObject *" "clinic_state()->DocumentBuilder_Type" class _elementtree.XMLParser "XMLParserObject *" "clinic_state()->XMLParser_Type" [clinic start generated code]*/ -/*[clinic end generated code: output=da39a3ee5e6b4b0d input=6c83ea832d2b0ef1]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=a64a192a4483f2e7]*/ static int element_init(PyObject *self, PyObject *args, PyObject *kwds) @@ -2484,12 +2488,17 @@ typedef struct { /* element tracing */ char insert_comments; char insert_pis; + PyObject *document; /* the children of the document (DocumentBuilder), or NULL */ elementtreestate *state; } TreeBuilderObject; #define _TreeBuilder_CAST(op) ((TreeBuilderObject *)(op)) -#define TreeBuilder_CheckExact(st, op) Py_IS_TYPE((op), (st)->TreeBuilder_Type) +/* True for the exact TreeBuilder and DocumentBuilder types, for which + the parser calls the handlers directly, bypassing the methods. */ +#define TreeBuilder_CheckExact(st, op) \ + (Py_IS_TYPE((op), (st)->TreeBuilder_Type) \ + || Py_IS_TYPE((op), (st)->DocumentBuilder_Type)) /* -------------------------------------------------------------------- */ /* constructor and destructor */ @@ -2516,11 +2525,50 @@ treebuilder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) t->index = 0; t->insert_comments = t->insert_pis = 0; + t->document = NULL; t->state = get_elementtree_state_by_type(type); } return (PyObject *)t; } +static int +treebuilder_init(TreeBuilderObject *self, PyObject *element_factory, + PyObject *comment_factory, PyObject *pi_factory, + int insert_comments, int insert_pis) +{ + if (element_factory != Py_None) { + Py_XSETREF(self->element_factory, Py_NewRef(element_factory)); + } else { + Py_CLEAR(self->element_factory); + } + + if (comment_factory == Py_None) { + elementtreestate *st = self->state; + comment_factory = st->comment_factory; + } + if (comment_factory) { + Py_XSETREF(self->comment_factory, Py_NewRef(comment_factory)); + self->insert_comments = insert_comments; + } else { + Py_CLEAR(self->comment_factory); + self->insert_comments = 0; + } + + if (pi_factory == Py_None) { + elementtreestate *st = self->state; + pi_factory = st->pi_factory; + } + if (pi_factory) { + Py_XSETREF(self->pi_factory, Py_NewRef(pi_factory)); + self->insert_pis = insert_pis; + } else { + Py_CLEAR(self->pi_factory); + self->insert_pis = 0; + } + + return 0; +} + /*[clinic input] _elementtree.TreeBuilder.__init__ @@ -2559,36 +2607,46 @@ _elementtree_TreeBuilder___init___impl(TreeBuilderObject *self, int insert_comments, int insert_pis) /*[clinic end generated code: output=8571d4dcadfdf952 input=24fb5a482d93f8e4]*/ { - if (element_factory != Py_None) { - Py_XSETREF(self->element_factory, Py_NewRef(element_factory)); - } else { - Py_CLEAR(self->element_factory); - } + return treebuilder_init(self, element_factory, comment_factory, + pi_factory, insert_comments, insert_pis); +} - if (comment_factory == Py_None) { - elementtreestate *st = self->state; - comment_factory = st->comment_factory; - } - if (comment_factory) { - Py_XSETREF(self->comment_factory, Py_NewRef(comment_factory)); - self->insert_comments = insert_comments; - } else { - Py_CLEAR(self->comment_factory); - self->insert_comments = 0; - } +/*[clinic input] +_elementtree.DocumentBuilder.__init__ - if (pi_factory == Py_None) { - elementtreestate *st = self->state; - pi_factory = st->pi_factory; + element_factory: object = None + * + comment_factory: object = None + pi_factory: object = None + insert_comments: bool = False + insert_pis: bool = False + +Generic document structure builder. + +This builder is like TreeBuilder, but its close() method returns +the list of the children of the document: the root element, and +the comments and processing instructions outside of it if +*insert_comments* or *insert_pis* is true. +[clinic start generated code]*/ + +static int +_elementtree_DocumentBuilder___init___impl(TreeBuilderObject *self, + PyObject *element_factory, + PyObject *comment_factory, + PyObject *pi_factory, + int insert_comments, + int insert_pis) +/*[clinic end generated code: output=273d144ac40e137f input=f4451df96264a514]*/ +{ + if (treebuilder_init(self, element_factory, comment_factory, + pi_factory, insert_comments, insert_pis) < 0) { + return -1; } - if (pi_factory) { - Py_XSETREF(self->pi_factory, Py_NewRef(pi_factory)); - self->insert_pis = insert_pis; - } else { - Py_CLEAR(self->pi_factory); - self->insert_pis = 0; + PyObject *document = PyList_New(0); + if (document == NULL) { + return -1; } - + Py_XSETREF(self->document, document); return 0; } @@ -2597,6 +2655,7 @@ treebuilder_gc_traverse(PyObject *op, visitproc visit, void *arg) { TreeBuilderObject *self = _TreeBuilder_CAST(op); Py_VISIT(Py_TYPE(self)); + Py_VISIT(self->document); Py_VISIT(self->root); Py_VISIT(self->this); Py_VISIT(self->last); @@ -2622,6 +2681,7 @@ treebuilder_gc_clear(PyObject *op) Py_CLEAR(self->comment_factory); Py_CLEAR(self->element_factory); Py_CLEAR(self->root); + Py_CLEAR(self->document); return 0; } @@ -2832,6 +2892,9 @@ treebuilder_handle_start(TreeBuilderObject* self, PyObject* tag, goto error; } self->root = Py_NewRef(node); + if (self->document && PyList_Append(self->document, node) < 0) { + goto error; + } } if (self->index < PyList_GET_SIZE(self->stack)) { @@ -2926,11 +2989,18 @@ treebuilder_handle_comment(TreeBuilderObject* self, PyObject* text) return NULL; this = self->this; - if (self->insert_comments && this != Py_None) { - if (treebuilder_add_subelement(self->state, this, comment) < 0) { + if (self->insert_comments) { + if (this != Py_None) { + if (treebuilder_add_subelement(self->state, this, comment) < 0) { + goto error; + } + Py_XSETREF(self->last_for_tail, Py_NewRef(comment)); + } + /* outside the root element: the prolog or the epilog */ + else if (self->document + && PyList_Append(self->document, comment) < 0) { goto error; } - Py_XSETREF(self->last_for_tail, Py_NewRef(comment)); } } else { comment = Py_NewRef(text); @@ -2961,11 +3031,18 @@ treebuilder_handle_pi(TreeBuilderObject* self, PyObject* target, PyObject* text) } this = self->this; - if (self->insert_pis && this != Py_None) { - if (treebuilder_add_subelement(self->state, this, pi) < 0) { + if (self->insert_pis) { + if (this != Py_None) { + if (treebuilder_add_subelement(self->state, this, pi) < 0) { + goto error; + } + Py_XSETREF(self->last_for_tail, Py_NewRef(pi)); + } + /* outside the root element: the prolog or the epilog */ + else if (self->document + && PyList_Append(self->document, pi) < 0) { goto error; } - Py_XSETREF(self->last_for_tail, Py_NewRef(pi)); } } else { pi = _PyTuple_FromPair(target, text); @@ -3065,7 +3142,10 @@ treebuilder_done(TreeBuilderObject* self) /* FIXME: check stack size? */ - if (self->root) + if (self->document) + /* DocumentBuilder: the children of the document */ + res = self->document; + else if (self->root) res = self->root; else res = Py_None; @@ -4512,6 +4592,22 @@ static PyType_Spec treebuilder_spec = { .slots = treebuilder_slots, }; +static PyType_Slot documentbuilder_slots[] = { + {Py_tp_doc, (void *)_elementtree_DocumentBuilder___init____doc__}, + {Py_tp_dealloc, treebuilder_dealloc}, + {Py_tp_traverse, treebuilder_gc_traverse}, + {Py_tp_clear, treebuilder_gc_clear}, + {Py_tp_init, _elementtree_DocumentBuilder___init__}, + {0, NULL}, +}; + +static PyType_Spec documentbuilder_spec = { + .name = "xml.etree.ElementTree.DocumentBuilder", + .basicsize = sizeof(TreeBuilderObject), + .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE, + .slots = documentbuilder_slots, +}; + static PyMethodDef xmlparser_methods[] = { _ELEMENTTREE_XMLPARSER_FEED_METHODDEF _ELEMENTTREE_XMLPARSER_CLOSE_METHODDEF @@ -4571,6 +4667,13 @@ module_exec(PyObject *m) /* Initialize object types */ CREATE_TYPE(m, st->ElementIter_Type, &elementiter_spec); CREATE_TYPE(m, st->TreeBuilder_Type, &treebuilder_spec); + if (st->DocumentBuilder_Type == NULL) { + st->DocumentBuilder_Type = (PyTypeObject *)PyType_FromModuleAndSpec( + m, &documentbuilder_spec, (PyObject *)st->TreeBuilder_Type); + if (st->DocumentBuilder_Type == NULL) { + goto error; + } + } CREATE_TYPE(m, st->Element_Type, &element_spec); CREATE_TYPE(m, st->XMLParser_Type, &xmlparser_spec); @@ -4645,6 +4748,7 @@ module_exec(PyObject *m) PyTypeObject *types[] = { st->Element_Type, st->TreeBuilder_Type, + st->DocumentBuilder_Type, st->XMLParser_Type }; diff --git a/Modules/clinic/_elementtree.c.h b/Modules/clinic/_elementtree.c.h index a39e738ec538b63..0c5ca631ab54f43 100644 --- a/Modules/clinic/_elementtree.c.h +++ b/Modules/clinic/_elementtree.c.h @@ -1076,6 +1076,118 @@ _elementtree_TreeBuilder___init__(PyObject *self, PyObject *args, PyObject *kwar return return_value; } +PyDoc_STRVAR(_elementtree_DocumentBuilder___init____doc__, +"DocumentBuilder(element_factory=None, *, comment_factory=None,\n" +" pi_factory=None, insert_comments=False,\n" +" insert_pis=False)\n" +"--\n" +"\n" +"Generic document structure builder.\n" +"\n" +"This builder is like TreeBuilder, but its close() method returns\n" +"the list of the children of the document: the root element, and\n" +"the comments and processing instructions outside of it if\n" +"*insert_comments* or *insert_pis* is true."); + +static int +_elementtree_DocumentBuilder___init___impl(TreeBuilderObject *self, + PyObject *element_factory, + PyObject *comment_factory, + PyObject *pi_factory, + int insert_comments, + int insert_pis); + +static int +_elementtree_DocumentBuilder___init__(PyObject *self, PyObject *args, PyObject *kwargs) +{ + int return_value = -1; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 5 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(element_factory), &_Py_ID(comment_factory), &_Py_ID(pi_factory), &_Py_ID(insert_comments), &_Py_ID(insert_pis), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"element_factory", "comment_factory", "pi_factory", "insert_comments", "insert_pis", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "DocumentBuilder", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[5]; + PyObject * const *fastargs; + Py_ssize_t nargs = PyTuple_GET_SIZE(args); + Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 0; + PyObject *element_factory = Py_None; + PyObject *comment_factory = Py_None; + PyObject *pi_factory = Py_None; + int insert_comments = 0; + int insert_pis = 0; + + fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, kwargs, NULL, &_parser, + /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!fastargs) { + goto exit; + } + if (!noptargs) { + goto skip_optional_pos; + } + if (fastargs[0]) { + element_factory = fastargs[0]; + if (!--noptargs) { + goto skip_optional_pos; + } + } +skip_optional_pos: + if (!noptargs) { + goto skip_optional_kwonly; + } + if (fastargs[1]) { + comment_factory = fastargs[1]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (fastargs[2]) { + pi_factory = fastargs[2]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (fastargs[3]) { + insert_comments = PyObject_IsTrue(fastargs[3]); + if (insert_comments < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + insert_pis = PyObject_IsTrue(fastargs[4]); + if (insert_pis < 0) { + goto exit; + } +skip_optional_kwonly: + return_value = _elementtree_DocumentBuilder___init___impl((TreeBuilderObject *)self, element_factory, comment_factory, pi_factory, insert_comments, insert_pis); + +exit: + return return_value; +} + PyDoc_STRVAR(_elementtree__set_factories__doc__, "_set_factories($module, comment_factory, pi_factory, /)\n" "--\n" @@ -1479,4 +1591,4 @@ _elementtree_XMLParser__setevents(PyObject *self, PyObject *const *args, Py_ssiz exit: return return_value; } -/*[clinic end generated code: output=e2e9cf288c4400f6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=29e26595ddcba924 input=a9049054013a1b77]*/