From f4f3f190f5cf86a9bd1c471d31c7058782403836 Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Thu, 10 Jul 2025 23:26:03 -0400 Subject: [PATCH 1/7] gh-136523: fix Wave_write.__del__ raise after wb open --- Lib/test/support/os_helper.py | 20 ++++++++++++++++++++ Lib/test/test_wave.py | 16 ++++++++++++++++ Lib/wave.py | 12 ++++++------ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index 2c45fe2369ec36..4fcaf1aa54fd4e 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -169,6 +169,26 @@ def make_bad_fd(): unlink(TESTFN) +@contextlib.contextmanager +def unwritable_filepath(): + """ + Create a filepath that is not writable by the current user. + """ + import tempfile + fd, path = tempfile.mkstemp() + original_permissions = stat.S_IMODE(os.lstat(path).st_mode) + os.close(fd) + + try: + os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) + yield path + finally: + try: + os.chmod(path, original_permissions) + os.remove(path) + except OSError as e: + pass + _can_symlink = None diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index 6c3362857fc2ba..f0e0b17ed376be 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -1,6 +1,7 @@ import unittest from test import audiotests from test import support +from test.support.os_helper import unwritable_filepath import io import struct import sys @@ -196,6 +197,21 @@ def test_read_wrong_sample_width(self): with self.assertRaisesRegex(wave.Error, 'bad sample width'): wave.open(io.BytesIO(b)) + def test_write_to_protected_file(self): + # gh-136523: Wave_write.__del__ should not throw + stderr = io.StringIO() + sys.stderr = stderr + try: + try: + with unwritable_filepath() as path: + with wave.open(path, "wb"): + pass + except PermissionError: + pass + self.assertEqual(stderr.getvalue(), "") + finally: + sys.stderr = sys.__stderr__ + if __name__ == '__main__': unittest.main() diff --git a/Lib/wave.py b/Lib/wave.py index 929609fa52409d..3ed1a697489811 100644 --- a/Lib/wave.py +++ b/Lib/wave.py @@ -429,15 +429,15 @@ class Wave_write: def __init__(self, f): self._i_opened_the_file = None - if isinstance(f, str): - f = builtins.open(f, 'wb') - self._i_opened_the_file = f try: - self.initfp(f) + if isinstance(f, str): + f = builtins.open(f, 'wb') + self._i_opened_the_file = f except: - if self._i_opened_the_file: - f.close() + f = None raise + finally: + self.initfp(f) def initfp(self, file): self._file = file From d4a598624ce4c68cacdb1be5f336fe14707a2a6b Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 03:39:16 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst diff --git a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst new file mode 100644 index 00000000000000..3e1267b2666826 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst @@ -0,0 +1 @@ +Fix :func:`wave.Wave_write.__del__` raising :exc:`AttributeError` when attempting to open an unwritable file. From 66e64b655d898c4b8b3d759a8858603f1ff4c7f9 Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Fri, 11 Jul 2025 00:52:42 -0400 Subject: [PATCH 3/7] skip test when no chmod (wasi), fix doc lint --- Lib/test/test_wave.py | 3 ++- .../Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index f0e0b17ed376be..d2611c294d4f9c 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -1,7 +1,7 @@ import unittest from test import audiotests from test import support -from test.support.os_helper import unwritable_filepath +from test.support.os_helper import unwritable_filepath, skip_unless_working_chmod import io import struct import sys @@ -197,6 +197,7 @@ def test_read_wrong_sample_width(self): with self.assertRaisesRegex(wave.Error, 'bad sample width'): wave.open(io.BytesIO(b)) + @skip_unless_working_chmod def test_write_to_protected_file(self): # gh-136523: Wave_write.__del__ should not throw stderr = io.StringIO() diff --git a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst index 3e1267b2666826..9bf4a75e5c779b 100644 --- a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst +++ b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst @@ -1 +1 @@ -Fix :func:`wave.Wave_write.__del__` raising :exc:`AttributeError` when attempting to open an unwritable file. +Fix ``wave.Wave_write.__del__`` raising :exc:`AttributeError` when attempting to open an unwritable file. From 288f5278ae3a8aea569f0e333f9849cb5d2dfeee Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Sat, 12 Jul 2025 14:41:22 -0400 Subject: [PATCH 4/7] simplify patch, fix test robustness to running env --- Lib/test/support/os_helper.py | 20 ------------------ Lib/test/test_wave.py | 21 +++++++------------ Lib/wave.py | 14 +++++++------ ...-07-11-03-39-15.gh-issue-136523.s7caKL.rst | 2 +- 4 files changed, 17 insertions(+), 40 deletions(-) diff --git a/Lib/test/support/os_helper.py b/Lib/test/support/os_helper.py index 4fcaf1aa54fd4e..2c45fe2369ec36 100644 --- a/Lib/test/support/os_helper.py +++ b/Lib/test/support/os_helper.py @@ -169,26 +169,6 @@ def make_bad_fd(): unlink(TESTFN) -@contextlib.contextmanager -def unwritable_filepath(): - """ - Create a filepath that is not writable by the current user. - """ - import tempfile - fd, path = tempfile.mkstemp() - original_permissions = stat.S_IMODE(os.lstat(path).st_mode) - os.close(fd) - - try: - os.chmod(path, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) - yield path - finally: - try: - os.chmod(path, original_permissions) - os.remove(path) - except OSError as e: - pass - _can_symlink = None diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index d2611c294d4f9c..a8b9cf379d9b19 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -1,7 +1,7 @@ import unittest from test import audiotests from test import support -from test.support.os_helper import unwritable_filepath, skip_unless_working_chmod +from test.support import os_helper import io import struct import sys @@ -197,21 +197,16 @@ def test_read_wrong_sample_width(self): with self.assertRaisesRegex(wave.Error, 'bad sample width'): wave.open(io.BytesIO(b)) - @skip_unless_working_chmod - def test_write_to_protected_file(self): + def test_write_to_protected_location(self): # gh-136523: Wave_write.__del__ should not throw - stderr = io.StringIO() - sys.stderr = stderr - try: + with support.catch_unraisable_exception() as cm: try: - with unwritable_filepath() as path: - with wave.open(path, "wb"): - pass - except PermissionError: + with os_helper.temp_dir() as path: + wave.open(path, "wb") + except IsADirectoryError: pass - self.assertEqual(stderr.getvalue(), "") - finally: - sys.stderr = sys.__stderr__ + support.gc_collect() + self.assertIsNone(cm.unraisable) if __name__ == '__main__': diff --git a/Lib/wave.py b/Lib/wave.py index 3ed1a697489811..5af745e2217ec3 100644 --- a/Lib/wave.py +++ b/Lib/wave.py @@ -427,17 +427,19 @@ class Wave_write: _datawritten -- the size of the audio samples actually written """ + _file = None + def __init__(self, f): self._i_opened_the_file = None + if isinstance(f, str): + f = builtins.open(f, 'wb') + self._i_opened_the_file = f try: - if isinstance(f, str): - f = builtins.open(f, 'wb') - self._i_opened_the_file = f + self.initfp(f) except: - f = None + if self._i_opened_the_file: + f.close() raise - finally: - self.initfp(f) def initfp(self, file): self._file = file diff --git a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst index 9bf4a75e5c779b..7b87b74ffc398c 100644 --- a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst +++ b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst @@ -1 +1 @@ -Fix ``wave.Wave_write.__del__`` raising :exc:`AttributeError` when attempting to open an unwritable file. +Fix :class:`wave.Wave_write` emitting an unraisable :exc:`AttributeError` when attempting to open an unwritable file. From c33b9080c5a95f53e5d5763b5c361b6d3ff44133 Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:12:13 -0400 Subject: [PATCH 5/7] fix test on Windows by catching OSError --- Lib/test/test_wave.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index a8b9cf379d9b19..c9d62dd3eb57ea 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -2,6 +2,7 @@ from test import audiotests from test import support from test.support import os_helper +import contextlib import io import struct import sys @@ -200,11 +201,9 @@ def test_read_wrong_sample_width(self): def test_write_to_protected_location(self): # gh-136523: Wave_write.__del__ should not throw with support.catch_unraisable_exception() as cm: - try: + with contextlib.suppress(OSError): with os_helper.temp_dir() as path: wave.open(path, "wb") - except IsADirectoryError: - pass support.gc_collect() self.assertIsNone(cm.unraisable) From 78ac6b2e6d87cbb2203f52969219133e60ae5cb9 Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Sat, 12 Jul 2025 15:40:37 -0400 Subject: [PATCH 6/7] test updates: assert open raises, use curdir, rename --- Lib/test/test_wave.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py index c9d62dd3eb57ea..226b1aa84bd73c 100644 --- a/Lib/test/test_wave.py +++ b/Lib/test/test_wave.py @@ -1,9 +1,8 @@ import unittest from test import audiotests from test import support -from test.support import os_helper -import contextlib import io +import os import struct import sys import wave @@ -198,12 +197,11 @@ def test_read_wrong_sample_width(self): with self.assertRaisesRegex(wave.Error, 'bad sample width'): wave.open(io.BytesIO(b)) - def test_write_to_protected_location(self): + def test_open_in_write_raises(self): # gh-136523: Wave_write.__del__ should not throw with support.catch_unraisable_exception() as cm: - with contextlib.suppress(OSError): - with os_helper.temp_dir() as path: - wave.open(path, "wb") + with self.assertRaises(OSError): + wave.open(os.curdir, "wb") support.gc_collect() self.assertIsNone(cm.unraisable) From 9c10b37090967be084250d05a4cb269cc8529e41 Mon Sep 17 00:00:00 2001 From: inventshah <39803835+inventshah@users.noreply.github.com> Date: Sat, 12 Jul 2025 16:01:47 -0400 Subject: [PATCH 7/7] update NEWS --- .../next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst index 7b87b74ffc398c..71ec66a37ef4c3 100644 --- a/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst +++ b/Misc/NEWS.d/next/Library/2025-07-11-03-39-15.gh-issue-136523.s7caKL.rst @@ -1 +1 @@ -Fix :class:`wave.Wave_write` emitting an unraisable :exc:`AttributeError` when attempting to open an unwritable file. +Fix :class:`wave.Wave_write` emitting an unraisable when open raises.