Repository: python/cpython
Pull request #131462: gh-131492: gh-131461:  handle exceptions in GzipFile constructor while owning resources
Author: login: graingert
Created: 2025-03-19T14:24:48Z | Merged: 2025-03-20T17:06:21Z

<!--
Thanks for your contribution!
Please read this comment in its entirety. It's quite important.

# Pull Request title

It should be in the following format:

```
gh-NNNNN: Summary of the changes made
```

Where: gh-NNNNN refers to the GitHub issue number.

Most PRs will require an issue number. Trivial changes, like fixing a typo, do not need an issue.

# Backport Pull Request title

If this is a backport PR (PR made against branches other than `main`),
please ensure that the PR title is in the following format:

```
[X.Y] <title from the original PR> (GH-NNNN)
```

Where: [X.Y] is the branch name, e.g. [3.6].

GH-NNNN refers to the PR number from `main`.

-->


<!-- gh-issue-number: gh-131461 -->
* Issue: gh-131461
<!-- /gh-issue-number -->


<!-- gh-issue-number: gh-131492 -->
* Issue: gh-131492
<!-- /gh-issue-number -->



Diff:
diff --git a/Lib/gzip.py b/Lib/gzip.py
index c9c088783bea65..2a6eea1b3939b7 100644
--- a/Lib/gzip.py
+++ b/Lib/gzip.py
@@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
+            if filename is None:
+                filename = getattr(fileobj, 'name', '')
+                if not isinstance(filename, (str, bytes)):
+                    filename = ''
+            else:
+                filename = os.fspath(filename)
+            origmode = mode
+            if mode is None:
+                mode = getattr(fileobj, 'mode', 'rb')
+
+
+            if mode.startswith('r'):
+                self.mode = READ
+                raw = _GzipReader(fileobj)
+                self._buffer = io.BufferedReader(raw)
+                self.name = filename
+
+            elif mode.startswith(('w', 'a', 'x')):
+                if origmode is None:
+                    import warnings
+                    warnings.warn(
+                        "GzipFile was opened for writing, but this will "
+                        "change in future Python releases.  "
+                        "Specify the mode argument for opening it for writing.",
+                        FutureWarning, 2)
+                self.mode = WRITE
+                self._init_write(filename)
+                self.compress = zlib.compressobj(compresslevel,
+                                                 zlib.DEFLATED,
+                                                 -zlib.MAX_WBITS,
+                                                 zlib.DEF_MEM_LEVEL,
+                                                 0)
+                self._write_mtime = mtime
+                self._buffer_size = _WRITE_BUFFER_SIZE
+                self._buffer = io.BufferedWriter(_WriteBufferStream(self),
+                                                 buffer_size=self._buffer_size)
+            else:
+                raise ValueError("Invalid mode: {!r}".format(mode))
+
+            self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except:
+            # Avoid a ResourceWarning if the write fails,
+            # eg read-only file or KeyboardInterrupt
+            self._close()
+            raise
 
     @property
     def mtime(self):
@@ -387,11 +394,14 @@ def close(self):
             elif self.mode == READ:
                 self._buffer.close()
         finally:
-            self.fileobj = None
-            myfileobj = self.myfileobj
-            if myfileobj:
-                self.myfileobj = None
-                myfileobj.close()
+            self._close()
+
+    def _close(self):
+        self.fileobj = None
+        myfileobj = self.myfileobj
+        if myfileobj is not None:
+            self.myfileobj = None
+            myfileobj.close()
 
     def flush(self,zlib_mode=zlib.Z_SYNC_FLUSH):
         self._check_not_closed()
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index 3e4cf8dd56db08..fcbaf854cc294f 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -19,6 +19,7 @@
 from test import support
 from test.support import os_helper
 from test.support import script_helper
+from test.support import warnings_helper
 
 # Check for our compression modules.
 try:
@@ -1638,10 +1639,13 @@ def write(self, data):
                         raise exctype
 
             f = BadFile()
-            with self.assertRaises(exctype):
-                tar = tarfile.open(tmpname, self.mode, fileobj=f,
-                                   format=tarfile.PAX_FORMAT,
-                                   pax_headers={'non': 'empty'})
+            with (
+                warnings_helper.check_no_resource_warning(self),
+                self.assertRaises(exctype),
+            ):
+                tarfile.open(tmpname, self.mode, fileobj=f,
+                             format=tarfile.PAX_FORMAT,
+                             pax_headers={'non': 'empty'})
             self.assertFalse(f.closed)
 
     def test_missing_fileobj(self):
diff --git a/Misc/NEWS.d/next/Library/2025-03-19-14-36-54.gh-issue-131461.uDUmdY.rst b/Misc/NEWS.d/next/Library/2025-03-19-14-36-54.gh-issue-131461.uDUmdY.rst
new file mode 100644
index 00000000000000..735b3ca429ca75
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-03-19-14-36-54.gh-issue-131461.uDUmdY.rst
@@ -0,0 +1 @@
+Fix :exc:`ResourceWarning` when constructing a :class:`gzip.GzipFile` in write mode with a broken file object.
diff --git a/Misc/NEWS.d/next/Library/2025-03-20-08-32-49.gh-issue-131492.saC2cA.rst b/Misc/NEWS.d/next/Library/2025-03-20-08-32-49.gh-issue-131492.saC2cA.rst
new file mode 100644
index 00000000000000..0f52dec7ce8a83
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-03-20-08-32-49.gh-issue-131492.saC2cA.rst
@@ -0,0 +1 @@
+Fix a resource leak when constructing a :class:`gzip.GzipFile` with a filename fails, for example when passing an invalid ``compresslevel``.


Reviews:
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T14:26:00Z
user: login: vstinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T15:04:51Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:06:28Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:06:53Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T15:09:22Z
user: login: vstinner
body: Would it be possible to write a test with such broken file object?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T15:36:20Z
user: login: vstinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T17:05:22Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:12:19Z
user: login: vstinner
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-19T17:23:11Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:34:02Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:46:44Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:37:15Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:38:47Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:51:41Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T22:53:28Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:36:30Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:40:52Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T03:37:14Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:06:20Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:10:00Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:18:41Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:36:23Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:38:31Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T04:59:15Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T05:00:52Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T05:20:36Z
user: login: serhiy-storchaka
body: LGTM.

I would add `support.gc_collect()` in tests.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T16:24:42Z
user: login: graingert
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:29:34Z
user: login: cmaloney
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T16:38:01Z
user: login: vstinner
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T16:39:29Z
user: login: vstinner
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T16:49:30Z

Review comments:
diff_hunk: @@ -245,8 +245,18 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
+            try:
+                if self.myfileobj is not None:
+                    self.myfileobj.close()
+            finally:
+                self.fileobj = None
+            self.fileobj = None
path: Lib/gzip.py
user: login: graingert
body: ```suggestion
```
created_at: 2025-03-19T14:25:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,9 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
-
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
path: Lib/gzip.py
user: login: vstinner
body: what does "KI" stand for?
created_at: 2025-03-19T15:02:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1 @@
+fix :exc:`ResourceWarning` when constructing a :class:`gzip.GzipFile` in write mode with a broken file object
path: Misc/NEWS.d/next/Library/2025-03-19-14-36-54.gh-issue-131461.uDUmdY.rst
user: login: vstinner
body: ```suggestion
Fix :exc:`ResourceWarning` when constructing a :class:`gzip.GzipFile` in write mode with a broken file object.
```
created_at: 2025-03-19T15:02:47Z
author_association: MEMBER
diff_hunk: @@ -245,9 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
-
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
+            try:
+                if self.myfileobj is not None:
+                    self.myfileobj.close()
+            finally:
+                self.fileobj = None
+            raise
path: Lib/gzip.py
user: login: vstinner
body: Please add an empty line.
created_at: 2025-03-19T15:04:47Z
author_association: MEMBER
diff_hunk: @@ -245,9 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
-
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
+            try:
+                if self.myfileobj is not None:
+                    self.myfileobj.close()
+            finally:
+                self.fileobj = None
+            raise
path: Lib/gzip.py
user: login: graingert
body: ```suggestion
            raise

```
created_at: 2025-03-19T15:06:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,9 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
-
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
path: Lib/gzip.py
user: login: graingert
body: KeyboardInterrupt
created_at: 2025-03-19T15:06:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,9 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
-
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KI
path: Lib/gzip.py
user: login: graingert
body: ```suggestion
            # Avoid a ResourceWarning if the write fails, eg read-only file or KeyboardInterrupt
```
created_at: 2025-03-19T15:09:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,8 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KeyboardInterrupt
+            try:
+                if self.myfileobj is not None:
+                    self.myfileobj.close()
+            finally:
+                self.fileobj = None
path: Lib/gzip.py
user: login: vstinner
body: What do you think of moving this code into a new _close() method, and modify close() to also call _close()?
created_at: 2025-03-19T17:05:03Z
author_association: MEMBER
diff_hunk: @@ -245,8 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
path: Lib/gzip.py
user: login: vstinner
body: IMO a bare except is fine here:

```suggestion
        except:
```
created_at: 2025-03-19T17:05:19Z
author_association: MEMBER
diff_hunk: @@ -245,8 +245,17 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except BaseException:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KeyboardInterrupt
+            try:
+                if self.myfileobj is not None:
+                    self.myfileobj.close()
+            finally:
+                self.fileobj = None
path: Lib/gzip.py
user: login: graingert
body: yeah that's much nicer, done!
created_at: 2025-03-19T17:12:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -245,8 +245,13 @@ def __init__(self, filename=None, mode=None,
 
         self.fileobj = fileobj
 
-        if self.mode == WRITE:
-            self._write_gzip_header(compresslevel)
+        try:
+            if self.mode == WRITE:
+                self._write_gzip_header(compresslevel)
+        except:
+            # Avoid a ResourceWarning if the write fails, eg read-only file or KeyboardInterrupt
path: Lib/gzip.py
user: login: vstinner
body: Please truncate to 80 columns:

```suggestion
            # Avoid a ResourceWarning if the write fails,
            # eg read-only file or KeyboardInterrupt
```
created_at: 2025-03-19T17:22:46Z
author_association: MEMBER
diff_hunk: @@ -1638,10 +1639,13 @@ def write(self, data):
                         raise exctype
 
             f = BadFile()
-            with self.assertRaises(exctype):
-                tar = tarfile.open(tmpname, self.mode, fileobj=f,
-                                   format=tarfile.PAX_FORMAT,
-                                   pax_headers={'non': 'empty'})
+            with (
+                warnings_helper.check_no_resource_warning(),
path: Lib/test/test_tarfile.py
user: login: graingert
body: ```suggestion
                warnings_helper.check_no_resource_warning(self),
```
created_at: 2025-03-19T17:34:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: I pushed a more thorough change inspired by https://github.com/python/cpython/blob/a4832f6b9a62771725b159bc7cd6c49fb45e3bc8/Lib/ssl.py#L1014-L1015

previously it was possible for this self.myfileobj to be left open if any of the constructor failed
created_at: 2025-03-19T17:46:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: Rather than the try/except wrapping, why not add a flag which is set to `False` at the beginning of the constructor and `True` at the end, in `__del__` (which is what emits the resource warning), check that flag. If it's not fully constructed then don't emit the resource warning?

The I/O stack (GzipFile inherits from IOBase) in [`IOBase.__del__`](https://github.com/python/cpython/blob/main/Modules/_io/iobase.c#L292-L326) always does the close call if `self.closed` is False, so shouldn't need to manually add a special case here. GzipFile.closed is:

```
    @property
    def closed(self):
        return self.fileobj is None
```

(so if there is a fileobj set, close should always be called).
created_at: 2025-03-19T22:28:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: This wouldn't help with myfileobj closing
created_at: 2025-03-19T22:38:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: How about using `self.fileobj is not None` as the "fully initialized" flag (already set to None always, only set to non-None at end of `__init__`), and checking around `if self.filobj is None and self.myfileobj is not None` in `__del__`?

It feels a lot like this is adding more cases / code around closing. Trying to find a way to get this change to be smaller / more minimal to reduce risks. IO stack construction, dealloc warnings, and close + destruction order is already really intricate and hard to understand in full.
created_at: 2025-03-19T22:51:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: We can't rely on `self.myfileobj` being closed in `__del__` and we don't return an instance of `GzipFile` for the caller to close so `self.myfileobj` needs to be closed at the end of `__init__` if there's an exception: so we need the try/catch
created_at: 2025-03-19T22:53:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: Are you saying: when the underlying `fileobj` / `raw` throws an exception because it was written to during `GzipFile.__init__`, the passed in `fileobj` must be cleaned up before `GzipFile.__init__` is exited and the caller should have a guarantee of that timing?

I added a print to the first line of `GzipFile.__del__`: 
```
        print(f"fileobj at close|{self.fileobj=}|{self.myfileobj=}")
```
and running: `./python.exe -Wd -m test -uall -M8g -v test_tarfile -m test_open_nonwritable_fileobj` on MacOS debug build of `main` I get:

```
fileobj at close|self.fileobj=<test.test_tarfile.WriteTest.test_open_nonwritable_fileobj.<locals>.BadFile object at 0x1056072a0>|self.myfileobj=None
```

Which seems to indicate that the `GzipFile` in the test _did_ reach the `self.fileobj = fileobj` line, that the fileobj was passed in by a caller (`self.myfileobj` is None). It does get closed today (by way of `IOBase` / `super.__del__()`). Am I missing something in that print? (Scanning the tarfile code, it seems to pass in a custom `fileobj`, which is why `self.myfileobj` is `None`)
created_at: 2025-03-19T23:36:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: In local testing, adding a `self._initialized` flag in `__init__` and 

```
    def __del__(self):
        print(f"fileobj at close|{self.fileobj=}|{self.myfileobj=}")
        if not self._initialized:
            self._close()
        elif self.mode == WRITE and not self.closed:
            import warnings
            warnings.warn("unclosed GzipFile",
                          ResourceWarning, source=self, stacklevel=2)

        super().__del__()
```

seems to resolve the "not fully initialized shouldn't warn" in the tarfile test. The code for all this is really intricate... Just getting the current warning working reasonably took me a couple days. I'm still trying to get one working for `BufferedWriter` generally...
created_at: 2025-03-19T23:40:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: > Are you saying: when the underlying `fileobj` / `raw` throws an exception because it was written to during `GzipFile.__init__`, the passed in `fileobj` must be cleaned up before `GzipFile.__init__` is exited and the caller should have a guarantee of that timing?

No I'm talking about the newly opened `self.myfileobj` it needs to be closed deterministicly if the constructor fails
created_at: 2025-03-20T03:37:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: Neither the case in the bug report sample code or the `test_tarfile` test with warning being fixed here use `self.myfileobj`. They both pass a explicit `fileobj` which ends up only in `self.fileobj`.

Code that sets `self.myfileobj` is guaraded by `fileobj is None`
https://github.com/python/cpython/blob/f141e8ec2a2e8d21fc08c1f56ef40104c7a7fad2/Lib/gzip.py#L205-L206

I double checked that for myself by adding the print statement I gave above printing out `self.fileobj` and `self.myfileobj` then running the sample and test.

---
Can definitely look at the "GzipFile opens a file itself" case as well; and happy to improve behavior around those cases. I read through some other I/O object code (`io.BufferedWriter` and `io.BufferedReader`), and neither guarantees  the underlying `raw` stream is closed at end of `__init__` on error. If `_buffered_init` C fails in them, they just return early and rely on GC / `__del__` of `io.IOBase` to clean up the `io` objects by calling `close()`.
created_at: 2025-03-20T04:06:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: > Neither the case in the bug report sample code or the test_tarfile test with warning being fixed here use self.myfileobj. They both pass a explicit fileobj which ends up only in self.fileobj.

Yes this is a drive-by fix. I'll add a new test case

> (io.BufferedWriter and io.BufferedReader), and neither guarantees the underlying raw stream is closed at end of __init__ on error.

These constructors should definitely close `raw` deterministicly on exceptions in the constructor

created_at: 2025-03-20T04:10:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: > These constructors should definitely close `raw` deterministicly on exceptions in the constructor

Ah actually it's not clear that BufferredReader/BufferredWriter owns their passed in `raw` so callers need to prepare for this case instead
created_at: 2025-03-20T04:18:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: I'm hesitant to change existing behavior or give a new guarantee about cleanup order as a part of a bugfix. The construction/destruction order of these classes is very specific; there may be a number of IO objects all "piped" together, potentially even with `tee` instances with one read multiple writes).
created_at: 2025-03-20T04:36:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: Right but this is closing objects that are inaccessible to the caller
created_at: 2025-03-20T04:38:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: The `GzipFile` / `self` is equally inaccessible, and cleaned up by its `__del__` / finalizer `tp_finalize` (or `tp_dealloc`). It's guaranteed `__del__` is called and able to release any held resources (such as `self.myfileobj`) see PEP-442.

I'm okay adding a cleanup for the case where `self.fileobj is None and self.myfileobj is not None` inside `__del__`. Matches the behavior of other `io` objects and is a lot smaller diff.
created_at: 2025-03-20T04:59:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: graingert
body: This is not correct, you cannot rely on `__del__` to be called when an exception is raised because the exception traceback holds a reference to self
created_at: 2025-03-20T05:00:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: That statement seems to contradict [PEP 442 Predictability](https://peps.python.org/pep-0442/#predictability) section to me...

>Following this scheme, an object’s finalizer is always called exactly once, even if it was resurrected afterwards.
> For CI objects, the order in which finalizers are called (step 2 above) is undefined.

Even if there is an exception traceback that keeps the reference to self, eventually that traceback will go out of scope and file will be closed / not leaked...
created_at: 2025-03-20T05:20:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1638,10 +1639,13 @@ def write(self, data):
                         raise exctype
 
             f = BadFile()
-            with self.assertRaises(exctype):
-                tar = tarfile.open(tmpname, self.mode, fileobj=f,
-                                   format=tarfile.PAX_FORMAT,
-                                   pax_headers={'non': 'empty'})
+            with (
+                warnings_helper.check_no_resource_warning(self),
+                self.assertRaises(exctype),
+            ):
+                tarfile.open(tmpname, self.mode, fileobj=f,
+                             format=tarfile.PAX_FORMAT,
+                             pax_headers={'non': 'empty'})
path: Lib/test/test_tarfile.py
user: login: serhiy-storchaka
body: ```suggestion
                tarfile.open(tmpname, self.mode, fileobj=f,
                             format=tarfile.PAX_FORMAT,
                             pax_headers={'non': 'empty'})
                support.gc_collect()
```
Just in case, this may matters on PyPy.
created_at: 2025-03-20T16:23:45Z
author_association: MEMBER
diff_hunk: @@ -1638,10 +1639,13 @@ def write(self, data):
                         raise exctype
 
             f = BadFile()
-            with self.assertRaises(exctype):
-                tar = tarfile.open(tmpname, self.mode, fileobj=f,
-                                   format=tarfile.PAX_FORMAT,
-                                   pax_headers={'non': 'empty'})
+            with (
+                warnings_helper.check_no_resource_warning(self),
+                self.assertRaises(exctype),
+            ):
+                tarfile.open(tmpname, self.mode, fileobj=f,
+                             format=tarfile.PAX_FORMAT,
+                             pax_headers={'non': 'empty'})
path: Lib/test/test_tarfile.py
user: login: graingert
body: The check_no_resource_warnings cmgr already gc collects
created_at: 2025-03-20T16:29:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -202,51 +202,58 @@ def __init__(self, filename=None, mode=None,
             raise ValueError("Invalid mode: {!r}".format(mode))
         if mode and 'b' not in mode:
             mode += 'b'
-        if fileobj is None:
-            fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
-        if filename is None:
-            filename = getattr(fileobj, 'name', '')
-            if not isinstance(filename, (str, bytes)):
-                filename = ''
-        else:
-            filename = os.fspath(filename)
-        origmode = mode
-        if mode is None:
-            mode = getattr(fileobj, 'mode', 'rb')
-
-
-        if mode.startswith('r'):
-            self.mode = READ
-            raw = _GzipReader(fileobj)
-            self._buffer = io.BufferedReader(raw)
-            self.name = filename
-
-        elif mode.startswith(('w', 'a', 'x')):
-            if origmode is None:
-                import warnings
-                warnings.warn(
-                    "GzipFile was opened for writing, but this will "
-                    "change in future Python releases.  "
-                    "Specify the mode argument for opening it for writing.",
-                    FutureWarning, 2)
-            self.mode = WRITE
-            self._init_write(filename)
-            self.compress = zlib.compressobj(compresslevel,
-                                             zlib.DEFLATED,
-                                             -zlib.MAX_WBITS,
-                                             zlib.DEF_MEM_LEVEL,
-                                             0)
-            self._write_mtime = mtime
-            self._buffer_size = _WRITE_BUFFER_SIZE
-            self._buffer = io.BufferedWriter(_WriteBufferStream(self),
-                                             buffer_size=self._buffer_size)
-        else:
-            raise ValueError("Invalid mode: {!r}".format(mode))
 
-        self.fileobj = fileobj
+        try:
+            if fileobj is None:
+                fileobj = self.myfileobj = builtins.open(filename, mode or 'rb')
path: Lib/gzip.py
user: login: cmaloney
body: I'll defer to @vstinner and @serhiy-storchaka for approval. From my perspective, this is a lot more complex of a bugfix than required, didn't match stated description and arguments around `self.myfileobj`. Happy to do quick fixes, can produce a much smaller patch here if desired with less behavior change.
created_at: 2025-03-20T16:38:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1 @@
+Fix a resource leak when constructing a :class:`gzip.GzipFile` with a filename fails, for example when passing an invalid ``compresslevel``
path: Misc/NEWS.d/next/Library/2025-03-20-08-32-49.gh-issue-131492.saC2cA.rst
user: login: vstinner
body: ```suggestion
Fix a resource leak when constructing a :class:`gzip.GzipFile` with a filename fails, for example when passing an invalid ``compresslevel``.
```
created_at: 2025-03-20T16:39:28Z
author_association: MEMBER

PR comments:
user: login: vstinner
created_at: 2025-03-19T15:35:17Z
body: @cmaloney: Would you mind to review this fix?
author_association: MEMBER
user: login: graingert
created_at: 2025-03-19T16:32:22Z
body: > Would it be possible to write a test with such broken file object?

There's already a test in test_tarfile: test_open_nonwritable_fileobj the failure will be enforced by https://github.com/python/cpython/pull/128973

I added check_no_resource_warning to make the assert hold until then
author_association: CONTRIBUTOR
user: login: vstinner
created_at: 2025-03-19T17:23:49Z
body: The CI fails:

```
TypeError: check_no_resource_warning() missing 1 required positional argument: 'testcase'
```
author_association: MEMBER
user: login: graingert
created_at: 2025-03-19T17:32:37Z
body: > The CI fails:
> 
> ```
> TypeError: check_no_resource_warning() missing 1 required positional argument: 'testcase'
> ```

🤦
author_association: CONTRIBUTOR
user: login: cmaloney
created_at: 2025-03-19T17:37:29Z
body: About to get my morning coffee, working on getting my head around, I think a couple related but distinct bugs happening, tried separating them out for clarity in a comment on the issue as I suspect people will find similar warnings as there is broader deployment of 3.14 with the warning, want people to not need to read all PR comments to figure out what to do (https://github.com/python/cpython/issues/131461#issuecomment-2737501197). 

Definitely a partially-constructed `GzipFile` shouldn't warn, this seems to improve that case, will read the code more thoroughly shortly.
author_association: CONTRIBUTOR
user: login: miss-islington-app[bot]
created_at: 2025-03-20T17:06:25Z
body: Thanks @graingert for the PR, and @vstinner for merging it 🌮🎉.. I'm working now to backport this PR to: 3.12, 3.13.
🐍🍒⛏🤖 
author_association: NONE
user: login: bedevere-app[bot]
created_at: 2025-03-20T17:06:36Z
body: [GH-131518](https://github.com/python/cpython/pull/131518) is a backport of this pull request to the [3.13 branch](https://github.com/python/cpython/tree/3.13).
author_association: NONE
user: login: bedevere-app[bot]
created_at: 2025-03-20T17:06:40Z
body: [GH-131519](https://github.com/python/cpython/pull/131519) is a backport of this pull request to the [3.12 branch](https://github.com/python/cpython/tree/3.12).
author_association: NONE
user: login: vstinner
created_at: 2025-03-20T17:27:20Z
body: Hum, the backports mention ResourceWarning, whereas ResourceWarning only exists in Python 3.14. If we want to backport this change to 3.13 and 3.12, maybe references to ResourceWarning should be removed?

What do you think @graingert?
author_association: MEMBER
user: login: graingert
created_at: 2025-03-20T17:31:03Z
body: Oh yeah nix the 3.14 specific bits of the changelog from the backports
author_association: CONTRIBUTOR
user: login: graingert
created_at: 2025-03-20T17:37:24Z
body: I've removed the references to a ResourceWarning in the backports
author_association: CONTRIBUTOR