Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions Doc/library/xml.dom.pulldom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ Example::

from xml.dom import pulldom

doc = pulldom.parse('sales_items.xml')
for event, node in doc:
if event == pulldom.START_ELEMENT and node.tagName == 'item':
if int(node.getAttribute('price')) > 50:
doc.expandNode(node)
print(node.toxml())
with pulldom.parse('sales_items.xml') as doc:
for event, node in doc:
if event == pulldom.START_ELEMENT and node.tagName == 'item':
if int(node.getAttribute('price')) > 50:
doc.expandNode(node)
print(node.toxml())

``event`` is a constant and can be one of:

Expand Down Expand Up @@ -114,6 +114,11 @@ DOMEventStream Objects
.. versionchanged:: 3.11
Support for :meth:`~object.__getitem__` method has been removed.

.. versionchanged:: next
:class:`DOMEventStream` can now be used as a :term:`context manager`.
On exit, :meth:`~DOMEventStream.clear` is called, closing file
handles opened by :func:`parse`.

.. method:: getEvent()

Return a tuple containing *event* and the current *node* as
Expand Down Expand Up @@ -141,3 +146,15 @@ DOMEventStream Objects
print(node.toxml())

.. method:: DOMEventStream.reset()

.. method:: clear()

Release references to the parser, the stream and the DOM builder,
and close the stream if it was opened by :func:`parse`. Streams
provided by the caller are not closed. It is safe to call this
method more than once. The event stream cannot be used after
calling this method.

.. versionchanged:: next
This method now closes the underlying stream if it was opened
by :func:`parse`.
28 changes: 27 additions & 1 deletion Lib/test/test_pulldom.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,39 @@ def test_parse(self):

# Test with a filename:
handler = pulldom.parse(tstfile)
self.addCleanup(handler.stream.close)
self.addCleanup(handler.clear)
list(handler)

# Test with a file object:
with open(tstfile, "rb") as fin:
list(pulldom.parse(fin))

def test_context_manager_closes_owned_stream(self):
with pulldom.parse(tstfile) as events:
stream = events.stream
list(events)
self.assertTrue(stream.closed)

def test_context_manager_does_not_close_user_stream(self):
with open(tstfile, 'rb') as f:
with pulldom.parse(f) as events:
list(events)
self.assertFalse(f.closed)

def test_clear_closes_owned_stream(self):
events = pulldom.parse(tstfile)
stream = events.stream
events.clear()
self.assertTrue(stream.closed)
events.clear() # should not raise

def test_clear_does_not_close_user_stream(self):
with open(tstfile, 'rb') as f:
events = pulldom.parse(f)
list(events)
events.clear()
self.assertFalse(f.closed)

def test_parse_semantics(self):
"""Test DOMEventStream parsing semantics."""

Expand Down
25 changes: 20 additions & 5 deletions Lib/xml/dom/pulldom.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,11 @@ def fatalError(self, exception):
raise exception

class DOMEventStream:
def __init__(self, stream, parser, bufsize):
def __init__(self, stream, parser, bufsize, _owns_stream=False):
self.stream = stream
self.parser = parser
self.bufsize = bufsize
self._owns_stream = _owns_stream
if not hasattr(self.parser, 'feed'):
self.getEvent = self._slurp
self.reset()
Expand All @@ -225,6 +226,12 @@ def __next__(self):
def __iter__(self):
return self

def __enter__(self):
return self

def __exit__(self, *args):
self.clear()

def expandNode(self, node):
event = self.getEvent()
parents = [node]
Expand Down Expand Up @@ -274,9 +281,15 @@ def _emit(self):
return rc

def clear(self):
"""clear(): Explicitly release parsing objects"""
self.pulldom.clear()
del self.pulldom
"""Release the parsing objects.

The stream is closed if it was opened by parse().
"""
if self._owns_stream and self.stream is not None:
self.stream.close()
if self.pulldom is not None:
self.pulldom.clear()
self.pulldom = None
self.parser = None
self.stream = None

Expand Down Expand Up @@ -320,11 +333,13 @@ def parse(stream_or_string, parser=None, bufsize=None):
bufsize = default_bufsize
if isinstance(stream_or_string, str):
stream = open(stream_or_string, 'rb')
owns_stream = True
else:
stream = stream_or_string
owns_stream = False
if not parser:
parser = xml.sax.make_parser()
return DOMEventStream(stream, parser, bufsize)
return DOMEventStream(stream, parser, bufsize, _owns_stream=owns_stream)

def parseString(string, parser=None):
from io import StringIO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:class:`xml.dom.pulldom.DOMEventStream` can now be used as a :term:`context
manager`, and its :meth:`~xml.dom.pulldom.DOMEventStream.clear` method now
closes the file opened by :func:`xml.dom.pulldom.parse`, fixing a resource
leak. This also fixes the file handle leak in :func:`xml.dom.minidom.parse`
when a custom *parser* is used.
Loading