From c8f71c5a6dc758118c12227fdf2ebe8e2d990e7b Mon Sep 17 00:00:00 2001 From: lee Date: Tue, 19 May 2026 19:11:06 +0800 Subject: [PATCH 1/4] fix tarfile: zstopen uses except Exception --- Lib/tarfile.py | 2 +- Lib/test/test_tarfile.py | 15 +++++++++++++++ ...-19-18-25-00.gh-issue-150077.zstopen-close.rst | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 87500c726ce9a8..149d807ae0a776 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2083,7 +2083,7 @@ def zstopen(cls, name, mode="r", fileobj=None, level=None, options=None, if mode == 'r': raise ReadError("not a zstd file") from e raise - except Exception: + except: fileobj.close() raise t._extfileobj = False diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 02fd9620bcf33d..cf659d4370ec17 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1065,6 +1065,21 @@ class LzmaDetectReadTest(LzmaTest, DetectReadTest): class ZstdDetectReadTest(ZstdTest, DetectReadTest): pass +@support.requires_zstd() +class ZstdOpenTest(unittest.TestCase): + """ + See: https://github.com/python/cpython/issues/150077 + """ + def test_zstopen_closes_fileobj_on_base_exception(self): + fileobj = unittest.mock.Mock() + with unittest.mock.patch("compression.zstd.ZstdFile", + return_value=fileobj), \ + unittest.mock.patch.object(tarfile.TarFile, "taropen", + side_effect=KeyboardInterrupt): + with self.assertRaises(KeyboardInterrupt): + tarfile.TarFile.zstopen("foo.tar.zst") + fileobj.close.assert_called_once() + class GzipBrokenHeaderCorrectException(GzipTest, unittest.TestCase): """ See: https://github.com/python/cpython/issues/107396 diff --git a/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst new file mode 100644 index 00000000000000..3d948dba679173 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst @@ -0,0 +1,2 @@ +Fix :meth:`tarfile.TarFile.zstopen` to close the zstd file object when +opening the tar archive fails with a :exc:`BaseException`. From 034ded165b8547547362022f1e32ecb90aea24d7 Mon Sep 17 00:00:00 2001 From: lee Date: Tue, 19 May 2026 20:13:41 +0800 Subject: [PATCH 2/4] Fix NEWS entry --- .../2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst index 3d948dba679173..ffcc6db30ebd78 100644 --- a/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst +++ b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst @@ -1,2 +1,4 @@ -Fix :meth:`tarfile.TarFile.zstopen` to close the zstd file object when -opening the tar archive fails with a :exc:`BaseException`. +Fix ``tarfile.TarFile.zstopen`` to close the underlying zstd file object +when opening the tar archive is interrupted by a :exc:`BaseException` +subclass such as :exc:`KeyboardInterrupt`. + From 9035f785979e53e8d76ba357df305360bba03ecd Mon Sep 17 00:00:00 2001 From: lee Date: Wed, 20 May 2026 14:50:32 +0800 Subject: [PATCH 3/4] modify test case --- Lib/test/test_tarfile.py | 33 ++++++++++++++----- ...18-25-00.gh-issue-150077.zstopen-close.rst | 1 - 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index cf659d4370ec17..4743e538c0f63d 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1065,20 +1065,37 @@ class LzmaDetectReadTest(LzmaTest, DetectReadTest): class ZstdDetectReadTest(ZstdTest, DetectReadTest): pass + @support.requires_zstd() class ZstdOpenTest(unittest.TestCase): """ See: https://github.com/python/cpython/issues/150077 """ def test_zstopen_closes_fileobj_on_base_exception(self): - fileobj = unittest.mock.Mock() - with unittest.mock.patch("compression.zstd.ZstdFile", - return_value=fileobj), \ - unittest.mock.patch.object(tarfile.TarFile, "taropen", - side_effect=KeyboardInterrupt): - with self.assertRaises(KeyboardInterrupt): - tarfile.TarFile.zstopen("foo.tar.zst") - fileobj.close.assert_called_once() + path = os_helper.TESTFN + ".tar.zst" + self.addCleanup(os_helper.unlink, path) + with tarfile.open(path, "w:zst"): + pass + + opened = [] + real_ZstdFile = compression.zstd.ZstdFile + + def tracking_ZstdFile(*args, **kwargs): + fileobj = real_ZstdFile(*args, **kwargs) + opened.append(fileobj) + return fileobj + + with ( + unittest.mock.patch("compression.zstd.ZstdFile", tracking_ZstdFile), + unittest.mock.patch.object( + tarfile.TarFile, "taropen", side_effect=KeyboardInterrupt), + self.assertRaises(KeyboardInterrupt), + ): + tarfile.TarFile.zstopen(path) + + self.assertEqual(len(opened), 1) + self.assertTrue(opened[0].closed) + class GzipBrokenHeaderCorrectException(GzipTest, unittest.TestCase): """ diff --git a/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst index ffcc6db30ebd78..06d20e41e25037 100644 --- a/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst +++ b/Misc/NEWS.d/next/Library/2026-05-19-18-25-00.gh-issue-150077.zstopen-close.rst @@ -1,4 +1,3 @@ Fix ``tarfile.TarFile.zstopen`` to close the underlying zstd file object when opening the tar archive is interrupted by a :exc:`BaseException` subclass such as :exc:`KeyboardInterrupt`. - From bb36302b661883be52563bdae5ab77a13e4eceb1 Mon Sep 17 00:00:00 2001 From: lee Date: Wed, 20 May 2026 15:49:02 +0800 Subject: [PATCH 4/4] fix test failure --- Lib/test/test_tarfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 4743e538c0f63d..d730cfec2ff298 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1078,7 +1078,7 @@ def test_zstopen_closes_fileobj_on_base_exception(self): pass opened = [] - real_ZstdFile = compression.zstd.ZstdFile + real_ZstdFile = zstd.ZstdFile def tracking_ZstdFile(*args, **kwargs): fileobj = real_ZstdFile(*args, **kwargs)