Skip to content

Commit a2f4f19

Browse files
gh-63102: Support custom targets in the pull parser (GH-156888)
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 f154574 commit a2f4f19

6 files changed

Lines changed: 315 additions & 188 deletions

File tree

Doc/library/xml.etree.elementtree.rst

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -640,22 +640,30 @@ Functions
640640
element instance. Return ``True`` if this is an element object.
641641

642642

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

645-
Parses an XML section into an element tree incrementally, and reports what's
646-
going on to the user. *source* is a filename or :term:`file object`
645+
Parses an XML section incrementally, and reports what's going on to the
646+
user. Unless a custom target is used, an element tree is built.
647+
*source* is a filename or :term:`file object`
647648
containing XML data. *events* is a sequence of events to report back. The
648649
supported events are the strings ``"start"``, ``"end"``, ``"comment"``,
649650
``"pi"``, ``"start-ns"`` and ``"end-ns"``
650651
(the "ns" events are used to get detailed namespace
651652
information). If *events* is omitted, only ``"end"`` events are reported.
652653
*parser* is an optional parser instance.
653654
If not given, the standard :class:`XMLParser` parser is used.
654-
*parser* must be an instance of :class:`XMLParser` or its subclass
655-
and can only use the default :class:`TreeBuilder` as a target.
656-
Returns an :term:`iterator` providing ``(event, elem)`` pairs;
655+
*parser* must be an instance of :class:`XMLParser` or its subclass.
656+
*target* is the target of the standard parser,
657+
as for :class:`XMLPullParser`;
658+
it cannot be used together with *parser*.
659+
Returns an :term:`iterator` providing ``(event, obj)`` pairs,
660+
as described for :meth:`XMLPullParser.read_events`;
657661
it has a ``root`` attribute that references the root element of the
658-
resulting XML tree once *source* is fully read.
662+
resulting XML tree, or the value returned by the ``close()`` method
663+
of a custom target, once *source* is fully read.
664+
If a custom target is used, it is set to the value returned
665+
by the :meth:`!close` method of the target.
666+
659667
The iterator has the :meth:`!close` method that closes the internal
660668
file object if *source* is a filename.
661669

@@ -691,6 +699,9 @@ Functions
691699
A :exc:`ResourceWarning` is now emitted if the iterator opened a file
692700
and is not explicitly closed.
693701

702+
.. versionchanged:: next
703+
Added the *target* parameter.
704+
694705

695706
.. function:: parse(source, parser=None)
696707

@@ -1524,7 +1535,7 @@ XMLParser Objects
15241535
XMLPullParser Objects
15251536
^^^^^^^^^^^^^^^^^^^^^
15261537

1527-
.. class:: XMLPullParser(events=None)
1538+
.. class:: XMLPullParser(events=None, *, target=None)
15281539

15291540
A pull parser suitable for non-blocking applications. Its input-side API is
15301541
similar to that of :class:`XMLParser`, but instead of pushing calls to a
@@ -1535,6 +1546,20 @@ XMLPullParser Objects
15351546
are used to get detailed namespace information). If *events* is omitted,
15361547
only ``"end"`` events are reported.
15371548

1549+
*target* is the target object of the underlying :class:`XMLParser`.
1550+
If omitted, the standard :class:`TreeBuilder` is used,
1551+
and the reported objects are :class:`Element` instances.
1552+
With other targets the reported object is the value returned
1553+
by the corresponding method of the target,
1554+
so no tree is built if the target does not build one.
1555+
The target must implement the methods for all requested events,
1556+
except :meth:`!start_ns` and :meth:`!end_ns`:
1557+
if they are not implemented, a ``(prefix, uri)`` tuple and ``None``
1558+
are reported for the ``"start-ns"`` and ``"end-ns"`` events.
1559+
1560+
.. versionchanged:: next
1561+
Added the *target* parameter.
1562+
15381563
.. method:: feed(data)
15391564

15401565
Feed the given data to the parser. *data* is a string
@@ -1567,9 +1592,10 @@ XMLPullParser Objects
15671592

15681593
Return an iterator over the events which have been encountered in the
15691594
data fed to the
1570-
parser. The iterator yields ``(event, elem)`` pairs, where *event* is a
1571-
string representing the type of event (e.g. ``"end"``) and *elem* is the
1572-
encountered :class:`Element` object, or other context value as follows.
1595+
parser. The iterator yields ``(event, obj)`` pairs, where *event* is a
1596+
string representing the type of event (e.g. ``"end"``) and *obj* is the
1597+
object returned by the corresponding method of the target.
1598+
With the standard :class:`TreeBuilder` it is as follows.
15731599

15741600
* ``start``, ``end``: the current Element.
15751601
* ``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
@@ -708,6 +708,13 @@ xml
708708
rather than defaulted from the DTD.
709709
(Contributed by Jason Orendorff and Serhiy Storchaka in :gh:`44871`.)
710710

711+
* :class:`~xml.etree.ElementTree.XMLPullParser` and
712+
:func:`~xml.etree.ElementTree.iterparse` now support the *target* parameter.
713+
The reported object is the value returned by the corresponding method of
714+
the target, so a large document can be parsed incrementally without
715+
building a tree for it.
716+
(Contributed by Serhiy Storchaka in :gh:`63102`.)
717+
711718
zipfile
712719
-------
713720

Lib/test/test_xml_etree.py

Lines changed: 107 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,76 @@ 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_without_method(self):
2143+
class Target:
2144+
def close(self):
2145+
pass
2146+
for event in ('start', 'end', 'comment', 'pi'):
2147+
with self.subTest(event=event):
2148+
with self.assertRaisesRegex(TypeError,
2149+
"the target does not support %r events" % event):
2150+
ET.XMLPullParser(events=(event,), target=Target())
2151+
# the namespace events do not need methods of the target
2152+
parser = ET.XMLPullParser(events=('start-ns', 'end-ns'),
2153+
target=Target())
2154+
self._feed(parser, "<root xmlns='namespace' />")
2155+
self.assert_event_tuples(parser, [
2156+
('start-ns', ('', 'namespace')),
2157+
('end-ns', None),
2158+
])
2159+
2160+
def test_custom_target_ns_events(self):
2161+
# the target does not implement start_ns()/end_ns(),
2162+
# so the prefix and the uri are reported
2163+
parser = ET.XMLPullParser(events=('start-ns', 'end-ns'),
2164+
target=self.SimpleTarget())
2165+
self._feed(parser, "<root xmlns='namespace' />")
2166+
self.assert_event_tuples(parser, [
2167+
('start-ns', ('', 'namespace')),
2168+
('end-ns', None),
2169+
])
2170+
2171+
def test_custom_target_close(self):
2172+
parser = ET.XMLPullParser(events=('end',), target=self.SimpleTarget())
2173+
self._feed(parser, "<root/>")
2174+
parser.close()
2175+
self.assert_event_tuples(parser, [('end', ('end', 'root'))])
2176+
20702177
def test_unknown_event(self):
20712178
with self.assertRaises(ValueError):
20722179
ET.XMLPullParser(events=('start', 'end', 'bogus'))

Lib/xml/etree/ElementTree.py

Lines changed: 20 additions & 9 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",)
@@ -1611,6 +1617,10 @@ def _setevents(self, events_queue, events_to_report):
16111617
parser = self._parser
16121618
append = events_queue.append
16131619
for event_name in events_to_report:
1620+
if (event_name in ("start", "end", "comment", "pi")
1621+
and not hasattr(self.target, event_name)):
1622+
raise TypeError("the target does not support %r events"
1623+
% event_name)
16141624
if event_name == "start":
16151625
parser.ordered_attributes = 1
16161626
def handler(tag, attrib_in, event=event_name, append=append,
@@ -1643,13 +1653,14 @@ def handler(prefix, event=event_name, append=append):
16431653
append((event, None))
16441654
parser.EndNamespaceDeclHandler = handler
16451655
elif event_name == 'comment':
1646-
def handler(text, event=event_name, append=append, self=self):
1647-
append((event, self.target.comment(text)))
1656+
def handler(text, event=event_name, append=append,
1657+
comment=self.target.comment):
1658+
append((event, comment(text)))
16481659
parser.CommentHandler = handler
16491660
elif event_name == 'pi':
16501661
def handler(pi_target, data, event=event_name, append=append,
1651-
self=self):
1652-
append((event, self.target.pi(pi_target, data)))
1662+
pi=self.target.pi):
1663+
append((event, pi(pi_target, data)))
16531664
parser.ProcessingInstructionHandler = handler
16541665
else:
16551666
raise ValueError("unknown event %r" % event_name)
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)