Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
84 changes: 76 additions & 8 deletions Doc/library/xml.etree.elementtree.rst
Original file line number Diff line number Diff line change
Expand Up @@ -609,15 +609,17 @@ 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)

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

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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, \
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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, \
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <xml.parsers.expat>` objects.
It tells how many of the reported attributes were given in the start tag
Expand Down
208 changes: 207 additions & 1 deletion Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<root>a&amp;<sub>b&amp;</sub>c&amp;</root>")
Expand Down Expand Up @@ -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 = ('<!--lead--><?pi data?><r><?in?><a/></r><?after?><!--tail-->')

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(),
'<!--lead--><?pi data?><r><?in?><a /></r>'
'<?after?><!--tail-->')

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'<!--c-->')
self.assertEqual(ET.tostring(ET.ProcessingInstruction('t', 'd')),
b'<?t d?>')

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(), '<!--a--><?p?>')

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('<!--c--><r id="x"><a id="y"/></r>', 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 = ('<!DOCTYPE html PUBLIC'
' "-//W3C//DTD XHTML 1.0 Transitional//EN"'
Expand Down
Loading
Loading