Skip to content

Commit aceee9a

Browse files
gh-63102: Support custom targets in the pull parser
The list of events was collected by the TreeBuilder in the C implementation, so the pull parser only worked with the standard target. The parser itself now collects the events, and reports what the target returns. XMLPullParser and iterparse() get the target parameter, which makes it possible to parse a large document incrementally without building a tree for it. The namespace events no longer need a separate code path: the parser reports the prefix and the uri if the target does not implement start_ns()/end_ns(), as the Python implementation already did.
1 parent 5056ac5 commit aceee9a

6 files changed

Lines changed: 262 additions & 183 deletions

File tree

Doc/library/xml.etree.elementtree.rst

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -611,22 +611,28 @@ Functions
611611
element instance. Return ``True`` if this is an element object.
612612

613613

614-
.. function:: iterparse(source, events=None, parser=None)
614+
.. function:: iterparse(source, events=None, parser=None, *, target=None)
615615

616-
Parses an XML section into an element tree incrementally, and reports what's
617-
going on to the user. *source* is a filename or :term:`file object`
616+
Parses an XML section incrementally, and reports what's going on to the
617+
user. Unless a custom target is used, an element tree is built.
618+
*source* is a filename or :term:`file object`
618619
containing XML data. *events* is a sequence of events to report back. The
619620
supported events are the strings ``"start"``, ``"end"``, ``"comment"``,
620621
``"pi"``, ``"start-ns"`` and ``"end-ns"``
621622
(the "ns" events are used to get detailed namespace
622623
information). If *events* is omitted, only ``"end"`` events are reported.
623624
*parser* is an optional parser instance.
624625
If not given, the standard :class:`XMLParser` parser is used.
625-
*parser* must be an instance of :class:`XMLParser` or its subclass
626-
and can only use the default :class:`TreeBuilder` as a target.
627-
Returns an :term:`iterator` providing ``(event, elem)`` pairs;
626+
*parser* must be an instance of :class:`XMLParser` or its subclass.
627+
*target* is the target of the standard parser;
628+
it cannot be used together with *parser*.
629+
Returns an :term:`iterator` providing ``(event, obj)`` pairs,
630+
as described for :meth:`XMLPullParser.read_events`;
628631
it has a ``root`` attribute that references the root element of the
629632
resulting XML tree once *source* is fully read.
633+
If a custom target is used, it is set to the value returned
634+
by the :meth:`!close` method of the target.
635+
630636
The iterator has the :meth:`!close` method that closes the internal
631637
file object if *source* is a filename.
632638

@@ -658,6 +664,9 @@ Functions
658664
A :exc:`ResourceWarning` is now emitted if the iterator opened a file
659665
and is not explicitly closed.
660666

667+
.. versionchanged:: next
668+
Added the *target* parameter.
669+
661670

662671
.. function:: parse(source, parser=None)
663672

@@ -1491,7 +1500,7 @@ XMLParser Objects
14911500
XMLPullParser Objects
14921501
^^^^^^^^^^^^^^^^^^^^^
14931502

1494-
.. class:: XMLPullParser(events=None)
1503+
.. class:: XMLPullParser(events=None, *, target=None)
14951504

14961505
A pull parser suitable for non-blocking applications. Its input-side API is
14971506
similar to that of :class:`XMLParser`, but instead of pushing calls to a
@@ -1502,6 +1511,18 @@ XMLPullParser Objects
15021511
are used to get detailed namespace information). If *events* is omitted,
15031512
only ``"end"`` events are reported.
15041513

1514+
*target* is the target object of the underlying :class:`XMLParser`.
1515+
If omitted, the standard :class:`TreeBuilder` is used,
1516+
and the reported objects are :class:`Element` instances.
1517+
With other targets the reported object is the value returned
1518+
by the corresponding method of the target,
1519+
so no tree is built if the target does not build one.
1520+
The ``"start-ns"`` and ``"end-ns"`` events are reported as before
1521+
if the target does not implement :meth:`!start_ns` and :meth:`!end_ns`.
1522+
1523+
.. versionchanged:: next
1524+
Added the *target* parameter.
1525+
15051526
.. method:: feed(data)
15061527

15071528
Feed the given data to the parser. *data* is a string
@@ -1534,9 +1555,10 @@ XMLPullParser Objects
15341555

15351556
Return an iterator over the events which have been encountered in the
15361557
data fed to the
1537-
parser. The iterator yields ``(event, elem)`` pairs, where *event* is a
1538-
string representing the type of event (e.g. ``"end"``) and *elem* is the
1539-
encountered :class:`Element` object, or other context value as follows.
1558+
parser. The iterator yields ``(event, obj)`` pairs, where *event* is a
1559+
string representing the type of event (e.g. ``"end"``) and *obj* is the
1560+
object returned by the corresponding method of the target.
1561+
With the standard :class:`TreeBuilder` it is as follows.
15401562

15411563
* ``start``, ``end``: the current Element.
15421564
* ``comment``, ``pi``: the current comment / processing instruction

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,13 @@ xml
660660
rather than defaulted from the DTD.
661661
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
662662

663+
* :class:`~xml.etree.ElementTree.XMLPullParser` and
664+
:func:`~xml.etree.ElementTree.iterparse` now support the *target* parameter.
665+
The reported object is the value returned by the corresponding method of
666+
the target, so a large document can be parsed incrementally without
667+
building a tree for it.
668+
(Contributed by Serhiy Storchaka in :gh:`63102`.)
669+
663670
zipfile
664671
-------
665672

Lib/test/test_xml_etree.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1656,6 +1656,43 @@ def test_unknown_events(self):
16561656
del cm
16571657
gc_collect()
16581658

1659+
class Target:
1660+
# a target which does not build a tree
1661+
def start(self, tag, attrib):
1662+
return tag
1663+
def end(self, tag):
1664+
return tag
1665+
def data(self, data):
1666+
pass
1667+
1668+
def test_target(self):
1669+
# gh-63102: a custom target reports its own objects
1670+
with open(SIMPLE_XMLFILE, 'rb') as f:
1671+
it = ET.iterparse(f, events=('start', 'end'), target=self.Target())
1672+
self.assertEqual(list(it), [
1673+
('start', 'root'),
1674+
('start', 'element'),
1675+
('end', 'element'),
1676+
('start', 'element'),
1677+
('end', 'element'),
1678+
('start', 'empty-element'),
1679+
('end', 'empty-element'),
1680+
('end', 'root'),
1681+
])
1682+
self.assertIsNone(it.root)
1683+
1684+
def test_parser_with_target(self):
1685+
with open(SIMPLE_XMLFILE, 'rb') as f:
1686+
parser = ET.XMLParser(target=self.Target())
1687+
it = ET.iterparse(f, events=('start',), parser=parser)
1688+
self.assertEqual(next(it), ('start', 'root'))
1689+
1690+
def test_target_and_parser(self):
1691+
with self.assertRaisesRegex(ValueError,
1692+
"can't specify both parser and target"):
1693+
ET.iterparse(SIMPLE_XMLFILE, parser=ET.XMLParser(),
1694+
target=self.Target())
1695+
16591696
def test_non_utf8(self):
16601697
source = io.BytesIO(
16611698
b"<?xml version='1.0' encoding='iso-8859-1'?>\n"
@@ -2067,6 +2104,58 @@ def __next__(self):
20672104
self._feed(parser, "<foo>bar</foo>")
20682105
self.assert_event_tags(parser, [('start', 'foo'), ('end', 'foo')])
20692106

2107+
# gh-63102: the pull parser reports events from any target
2108+
class SimpleTarget:
2109+
def start(self, tag, attrib):
2110+
return ('start', tag)
2111+
def end(self, tag):
2112+
return ('end', tag)
2113+
def data(self, data):
2114+
pass
2115+
def comment(self, text):
2116+
return ('comment', text)
2117+
def pi(self, target, data=None):
2118+
return ('pi', target)
2119+
def close(self):
2120+
return 'closed'
2121+
2122+
def test_custom_target(self):
2123+
parser = ET.XMLPullParser(events=('start', 'end'),
2124+
target=self.SimpleTarget())
2125+
self._feed(parser, "<root><element/></root>")
2126+
self.assert_event_tuples(parser, [
2127+
('start', ('start', 'root')),
2128+
('start', ('start', 'element')),
2129+
('end', ('end', 'element')),
2130+
('end', ('end', 'root')),
2131+
])
2132+
2133+
def test_custom_target_comment_pi(self):
2134+
parser = ET.XMLPullParser(events=('comment', 'pi'),
2135+
target=self.SimpleTarget())
2136+
self._feed(parser, "<root><!-- text --><?pitarget data?></root>")
2137+
self.assert_event_tuples(parser, [
2138+
('comment', ('comment', ' text ')),
2139+
('pi', ('pi', 'pitarget')),
2140+
])
2141+
2142+
def test_custom_target_ns_events(self):
2143+
# the target does not implement start_ns()/end_ns(),
2144+
# so the prefix and the uri are reported
2145+
parser = ET.XMLPullParser(events=('start-ns', 'end-ns'),
2146+
target=self.SimpleTarget())
2147+
self._feed(parser, "<root xmlns='namespace' />")
2148+
self.assert_event_tuples(parser, [
2149+
('start-ns', ('', 'namespace')),
2150+
('end-ns', None),
2151+
])
2152+
2153+
def test_custom_target_close(self):
2154+
parser = ET.XMLPullParser(events=('end',), target=self.SimpleTarget())
2155+
self._feed(parser, "<root/>")
2156+
parser.close()
2157+
self.assert_event_tuples(parser, [('end', ('end', 'root'))])
2158+
20702159
def test_unknown_event(self):
20712160
with self.assertRaises(ValueError):
20722161
ET.XMLPullParser(events=('start', 'end', 'bogus'))

Lib/xml/etree/ElementTree.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,7 +1239,7 @@ def parse(source, parser=None):
12391239
return tree
12401240

12411241

1242-
def iterparse(source, events=None, parser=None):
1242+
def iterparse(source, events=None, parser=None, *, target=None):
12431243
"""Incrementally parse XML document into ElementTree.
12441244
12451245
This class also reports what's going on to the user based on the
@@ -1250,14 +1250,14 @@ def iterparse(source, events=None, parser=None):
12501250
12511251
*source* is a filename or file object containing XML data, *events* is
12521252
a list of events to report back, *parser* is an optional parser
1253-
instance.
1253+
instance, *target* is an optional target of the standard parser.
12541254
12551255
Returns an iterator providing (event, elem) pairs.
12561256
12571257
"""
12581258
# Use the internal, undocumented _parser argument for now; When the
12591259
# parser argument of iterparse is removed, this can be killed.
1260-
pullparser = XMLPullParser(events=events, _parser=parser)
1260+
pullparser = XMLPullParser(events=events, target=target, _parser=parser)
12611261

12621262
if not hasattr(source, "read"):
12631263
source = open(source, "rb")
@@ -1309,13 +1309,19 @@ def __del__(self, _warn=warnings.warn):
13091309

13101310
class XMLPullParser:
13111311

1312-
def __init__(self, events=None, *, _parser=None):
1312+
def __init__(self, events=None, *, target=None, _parser=None):
13131313
# The _parser argument is for internal use only and must not be relied
13141314
# upon in user code. It will be removed in a future release.
13151315
# See https://bugs.python.org/issue17741 for more details.
13161316

13171317
self._events_queue = collections.deque()
1318-
self._parser = _parser or XMLParser(target=TreeBuilder())
1318+
if _parser is None:
1319+
if target is None:
1320+
target = TreeBuilder()
1321+
_parser = XMLParser(target=target)
1322+
elif target is not None:
1323+
raise ValueError("can't specify both parser and target")
1324+
self._parser = _parser
13191325
# wire up the parser for event reporting
13201326
if events is None:
13211327
events = ("end",)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:class:`~xml.etree.ElementTree.XMLPullParser` and
2+
:func:`~xml.etree.ElementTree.iterparse` now support the *target* parameter.
3+
The reported object is the value returned by the corresponding method
4+
of the target, so no tree is built if the target does not build one.
5+
Only the standard :class:`~xml.etree.ElementTree.TreeBuilder` was supported
6+
in the C implementation before.

0 commit comments

Comments
 (0)