Skip to content

ResourceWarning in GzipFile (write mode) if constructor raises (3.14 only) #131461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
graingert opened this issue Mar 19, 2025 · 8 comments
Closed
Labels
3.14 bugs and security fixes type-bug An unexpected behavior, bug, or error

Comments

@graingert
Copy link
Contributor

graingert commented Mar 19, 2025

Bug report

Bug description:

import io
import gzip

class BadFile(io.BytesIO):
    first = False
    def write(self, data):
        if self.first:
            self.first = False
            raise OSError

def main():
    try:
        gzip.GzipFile(fileobj=BadFile(), mode="w")
    except OSError:
        pass

main()

when run produces:

./python -W error ../../demo.py
Exception ignored while calling deallocator <function GzipFile.__del__ at 0x7b14defe09e0>:
Traceback (most recent call last):
  File "/home/graingert/projects/cpython/Lib/gzip.py", line 458, in __del__
    warnings.warn("unclosed GzipFile",
ResourceWarning: unclosed GzipFile

This warning is also raised by the test suite, in test_tarfile.WriteTestBase.test_open_nonwritable_fileobj

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

@graingert graingert added the type-bug An unexpected behavior, bug, or error label Mar 19, 2025
@graingert graingert changed the title ResourceWarning in GzipFile (write mode) if constructor raises ResourceWarning in GzipFile (write mode) if constructor raises (3.14 only) Mar 19, 2025
@aisk aisk added the 3.14 bugs and security fixes label Mar 19, 2025
@cmaloney
Copy link
Contributor

Want to separate out a couple pieces for clarity here, new to this code example, want to validate my reading of it:

  1. A GzipFile is partially constructed currently and warns on destruction about not being closed.
  2. The code sample doesn't close the GzipFile.
  3. (pulling in from PR) tarfile_test doesn't refer to GzipFile code directly, produces the warning but shouldn't

I think all three of these should definitely be improved, just that they're somewhat distinct. 1 and 3 should definitely be improved in CPython itself. Looks like your PR helps primarily with 1.

This warning was added in 3.14 after code inspection around user reports in Python 3.12 GzipFile (gh-129726, gh-129640), showed there could be data loss if not properly closed. There was a reference loop in 3.12 (now with backported fix) that was resulting in an unexpected close order if the GzipFile wasn't explicitly closed when passed temporary files.

For 2, I think the sample code would be more robust by using with / Context Managers and possibly .detach() (if it doesn't want to actually close the underlying). Full details of different ways to resolve the warning by making code robust are in in gh-130806.

@graingert
Copy link
Contributor Author

for 2 you shouldn't need to close a GzipFile if it raises in the constructor, we use self.assertRaises so we know the tarfile.open is responsible for closing its resources before raising, so we don't need to use with

@cmaloney
Copy link
Contributor

Agreed, 1 should mean you don't get a warning in the sample code. It's important for GzipFile to be well behaved when it encounters exceptions on construction just like general Python code should be.

@cmaloney
Copy link
Contributor

cmaloney commented Mar 19, 2025

Tests / samples can (and often do) reduce code to increase clarity and make sure that catch errors expect. As a test case, I think the sample code is reasonable. In general code which uses GzipFile it is better to be robust, that is part of why 3 is a distinct problem for me. tarfile should be well behaved; that it defers to a GzipFile internally should be largely an implementation detail. Fixing 1 I suspect will make the error for 3 go away, but 3 tells me to double check the tarfile code as something that could be strengthened.

vstinner added a commit that referenced this issue Mar 20, 2025
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Mar 20, 2025
…ructor while owning resources (pythonGH-131462)

(cherry picked from commit ce79274)

Co-authored-by: Thomas Grainger <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
miss-islington pushed a commit to miss-islington/cpython that referenced this issue Mar 20, 2025
…ructor while owning resources (pythonGH-131462)

(cherry picked from commit ce79274)

Co-authored-by: Thomas Grainger <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
vstinner added a commit that referenced this issue Mar 21, 2025
…r while owning resources (GH-131462) (#131518)

(cherry picked from commit ce79274)

Co-authored-by: Thomas Grainger <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
vstinner added a commit that referenced this issue Mar 21, 2025
…r while owning resources (GH-131462) (#131519)

(cherry picked from commit ce79274)

Co-authored-by: Thomas Grainger <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>
@vstinner
Copy link
Member

Fixed by the change ce79274.

@vstinner
Copy link
Member

Oh, I still get a ResourceWarning when I run the reproducer script:

/home/vstinner/python/main/demo.py:13: ResourceWarning: unclosed GzipFile
  gzip.GzipFile(fileobj=BadFile(), mode="w")
ResourceWarning: Enable tracemalloc to get the object allocation traceback

I reopen the issue.

@vstinner vstinner reopened this Mar 21, 2025
@graingert
Copy link
Contributor Author

apologies, the demo code was slightly off, it should have been if not self.first::

import io
import gzip

class BadFile(io.BytesIO):
    first = False

    def write(self, data):
        if not self.first:
            self.first = False
            raise OSError

def main():
    try:
        gzip.GzipFile(fileobj=BadFile(), mode="w")
    except OSError:
        pass
    else:
        assert False, "did not raise OSError!"

main()

@vstinner
Copy link
Member

Aha, with the adjusted reproducer, I don't get a ResourceWarning anymore. I close again the issue :-)

seehwan pushed a commit to seehwan/cpython that referenced this issue Apr 16, 2025
…ructor while owning resources (python#131462)

Co-authored-by: Victor Stinner <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.14 bugs and security fixes type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

4 participants