Skip to content

gh-148428: Fix file handle leak in pulldom.parse() - #148437

Open
WYSIATI wants to merge 11 commits into
python:mainfrom
WYSIATI:fix-pulldom-resource-leak-148428
Open

gh-148428: Fix file handle leak in pulldom.parse()#148437
WYSIATI wants to merge 11 commits into
python:mainfrom
WYSIATI:fix-pulldom-resource-leak-148428

Conversation

@WYSIATI

@WYSIATI WYSIATI commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

When pulldom.parse() is called with a filename string, it opens a file but the resulting DOMEventStream never closes it — no close(), no context manager, and clear() just sets self.stream = None without closing.

This adds resource management to DOMEventStream:

The __del__ binds _warn=warnings.warn as a default argument to handle interpreter shutdown safely, matching the pattern in ElementTree.iterparse, subprocess.Popen, etc.

User-provided streams (passed as file objects to parse(), or via parseString()) are never closed by DOMEventStream.


📚 Documentation preview 📚: https://cpython-previews--148437.org.readthedocs.build/

When pulldom.parse() is called with a filename, the opened file handle
is never closed. Add close(), context manager support, and __del__ with
ResourceWarning to DOMEventStream. Update clear() to close owned streams.
WYSIATI added 5 commits April 12, 2026 20:59
Keep the existing test_parse unchanged; context manager behavior is
covered by the new dedicated tests.
Remove test_context_manager_closes_file (covered by test_close_is_idempotent)
and revert the unrelated modification to test_parse.
Use handler.close() instead of handler.stream.close() in addCleanup
so the DOMEventStream is properly closed before GC triggers __del__.
@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label May 18, 2026

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good approach.

Note that __enter__ and __exit__ are not strictly needed -- contextlib.closing() works with any object having close(). They add convenience, but if there are more that one way to finish the object (clear() and close()), their semantic is ambiguous.

On other hand, why do you need close() if there is clear()? There are two variants of simplification:

  • Close the file in clear(), remove close(), and use clear() in __exit__().
  • Close the file in clear(), make close() an alias of clear(), remove __enter__ and __exit__.

The example in the documentation, which the issue quotes as the reason users hit this, is not updated.

Comment thread Lib/test/test_pulldom.py Outdated
Comment thread Lib/test/test_pulldom.py Outdated
Comment thread Lib/xml/dom/pulldom.py Outdated
The file object opened by parse() already emits its own ResourceWarning
when collected unclosed, so DOMEventStream.__del__ is not needed.

Rewrite test_context_manager_does_not_close_user_stream to actually use
the context manager and pulldom.parse(), and drop comments that repeat
the test names.
Per review: close() is removed, clear() now closes the stream when it
was opened by parse() (and is safe to call more than once), and the
context manager calls clear() on exit.

Since minidom's _do_pulldom_parse() already calls clear(), this also
fixes the file handle leak in xml.dom.minidom.parse() with a custom
parser.

Also update the leaking example in the documentation to use the with
statement.
@WYSIATI

WYSIATI commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

This is a good approach.

Note that __enter__ and __exit__ are not strictly needed -- contextlib.closing() works with any object having close(). They add convenience, but if there are more that one way to finish the object (clear() and close()), their semantic is ambiguous.

On other hand, why do you need close() if there is clear()? There are two variants of simplification:

  • Close the file in clear(), remove close(), and use clear() in __exit__().
  • Close the file in clear(), make close() an alias of clear(), remove __enter__ and __exit__.

The example in the documentation, which the issue quotes as the reason users hit this, is not updated.

The ambiguity is a valid point, I have cleaned up close() while keeping clear()

@read-the-docs-community

read-the-docs-community Bot commented Sep 1, 2026

Copy link
Copy Markdown

Documentation build overview

📚 cpython-previews | 🛠️ Build #34344293 | 📁 Comparing 7112293 against main (374851f)

  🔍 Preview build  

4 files changed
± library/concurrent.interpreters.html
± library/xml.dom.pulldom.html
± library/xml.etree.elementtree.html
± whatsnew/changelog.html

Comment thread Doc/library/xml.dom.pulldom.rst Outdated

.. method:: DOMEventStream.reset()

.. method:: DOMEventStream.clear()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, so it was not even documented?

Suggested change
.. method:: DOMEventStream.clear()
.. method:: clear()

This is already in the "class:: DOMEventStream" block.

I think that it would be better to fix the documentation of clear() and reset() in separate PR before merging this PR (it can be easily backported).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you referred to outdated code? Current implementation has method:: clear() without prefix.

Do you suggest submitting another PR just to fix the naming of DOMEventStream.reset()

Comment thread Lib/xml/dom/pulldom.py Outdated
@@ -275,8 +282,11 @@ def _emit(self):

def clear(self):
"""clear(): Explicitly release parsing objects"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"clear():" is redundant. The docstring still doesn't mention closing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest code has such wording:

"""Release the parsing objects.

        The stream is closed if it was opened by parse().
        """

Do you think this is not appropriate?

@serhiy-storchaka serhiy-storchaka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is there a test for clear() with user stream?

@WYSIATI

WYSIATI commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Also, is there a test for clear() with user stream?

Added test_clear_does_not_close_user_stream.

@serhiy-storchaka

Copy link
Copy Markdown
Member

I just fixed the clear() documentation together with other documentation (see #156839). Please merge main into your branch and resolve conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review stale Stale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants