From 5dcd4f40c410cee37eef8b788f45fcef08785294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sat, 6 Dec 2025 13:30:27 +0100 Subject: [PATCH] gh-142307: restore legacy support for altering ``IMAP4.file`` --- Lib/imaplib.py | 44 +++++++++++-------- Lib/test/test_imaplib.py | 23 ++++++++-- ...-12-06-13-29-11.gh-issue-142307.QTUYvG.rst | 2 + 3 files changed, 46 insertions(+), 23 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-06-13-29-11.gh-issue-142307.QTUYvG.rst diff --git a/Lib/imaplib.py b/Lib/imaplib.py index cbe129b3e7c214..605ae08c385e66 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -316,25 +316,30 @@ def open(self, host='', port=IMAP4_PORT, timeout=None): self.host = host self.port = port self.sock = self._create_socket(timeout) - self._file = self.sock.makefile('rb') - + # Since IMAP4 implements its own read() and readline() buffering, + # the '_imaplib_file' attribute is unused. Nonetheless it is kept + # and exposed solely for backward compatibility purposes. + self._imaplib_file = self.sock.makefile('rb') @property def file(self): - # The old 'file' attribute is no longer used now that we do our own - # read() and readline() buffering, with which it conflicts. - # As an undocumented interface, it should never have been accessed by - # external code, and therefore does not warrant deprecation. - # Nevertheless, we provide this property for now, to avoid suddenly - # breaking any code in the wild that might have been using it in a - # harmless way. - import warnings - warnings.warn( - 'IMAP4.file is unsupported, can cause errors, and may be removed.', - RuntimeWarning, - stacklevel=2) - return self._file - + return self._imaplib_file + + @file.setter + def file(self, value): + # Ideally, we would want to close the previous file, + # but since we do not know how subclasses will use + # that setter, it is probably better to leave it to + # the caller. + self._imaplib_file = value + + def _close_imaplib_file(self): + file = self._imaplib_file + if file is not None: + try: + file.close() + except OSError: + pass def read(self, size): """Read 'size' bytes from remote.""" @@ -420,7 +425,7 @@ def send(self, data): def shutdown(self): """Close I/O established in "open".""" - self._file.close() + self._close_imaplib_file() try: self.sock.shutdown(socket.SHUT_RDWR) except OSError as exc: @@ -924,9 +929,10 @@ def starttls(self, ssl_context=None): ssl_context = ssl._create_stdlib_context() typ, dat = self._simple_command(name) if typ == 'OK': + self._close_imaplib_file() self.sock = ssl_context.wrap_socket(self.sock, server_hostname=self.host) - self._file = self.sock.makefile('rb') + self._imaplib_file = self.sock.makefile('rb') self._tls_established = True self._get_capabilities() else: @@ -1681,7 +1687,7 @@ def open(self, host=None, port=None, timeout=None): self.host = None # For compatibility with parent class self.port = None self.sock = None - self._file = None + self._imaplib_file = None self.process = subprocess.Popen(self.command, bufsize=DEFAULT_BUFFER_SIZE, stdin=subprocess.PIPE, stdout=subprocess.PIPE, diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index a03d7b8bb2a42c..35904c8d5af0a9 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -665,11 +665,26 @@ def test_unselect(self): # property tests - def test_file_property_should_not_be_accessed(self): + def test_file_property_getter(self): client, _ = self._setup(SimpleIMAPHandler) - # the 'file' property replaced a private attribute that is now unsafe - with self.assertWarns(RuntimeWarning): - client.file + self.assertIsInstance(client.file.raw, socket.SocketIO) + + def test_file_property_setter(self): + client, _ = self._setup(SimpleIMAPHandler) + # ensure that the caller closes the existing file + client.file.close() + for new_file in [mock.Mock(), None]: + client.file = new_file + self.assertIs(client.file, new_file) + + def test_file_property_setter_should_not_close_previous_file(self): + client, _ = self._setup(SimpleIMAPHandler) + with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f: + f.close.assert_not_called() + self.assertIs(client.file, f) + client.file = None + self.assertIsNone(client.file) + f.close.assert_not_called() class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-12-06-13-29-11.gh-issue-142307.QTUYvG.rst b/Misc/NEWS.d/next/Library/2025-12-06-13-29-11.gh-issue-142307.QTUYvG.rst new file mode 100644 index 00000000000000..02ec3bd73f80dd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-06-13-29-11.gh-issue-142307.QTUYvG.rst @@ -0,0 +1,2 @@ +:mod:`imaplib`: restore legacy support for altering ``IMAP4.file``. Patch by +Bénédikt Tran.