From f28c0cc341c3ba29732d63253b818cff92500b75 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:23:01 +0100 Subject: [PATCH 1/2] gh-142307: restore legacy support for altering ``IMAP4.file`` The ``IMAP4.file`` attribute was never meant to be part of the public API. Previously, this attribute was created when opening a connection but subsequent changes may introduce a desynchronization with the underlying socket as the previous file is not closed. --- Doc/deprecations/pending-removal-in-3.19.rst | 10 +++++ Doc/library/imaplib.rst | 10 +++++ Doc/whatsnew/3.15.rst | 6 +++ Lib/imaplib.py | 44 ++++++++++++------- Lib/test/test_imaplib.py | 32 ++++++++++++-- ...-12-06-11-24-25.gh-issue-142307.w8evI9.rst | 4 ++ 6 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst diff --git a/Doc/deprecations/pending-removal-in-3.19.rst b/Doc/deprecations/pending-removal-in-3.19.rst index 25f9cba390de68..502e64cf84ec94 100644 --- a/Doc/deprecations/pending-removal-in-3.19.rst +++ b/Doc/deprecations/pending-removal-in-3.19.rst @@ -22,3 +22,13 @@ Pending removal in Python 3.19 supported depending on the backend implementation of hash functions. Prefer passing the initial data as a positional argument for maximum backwards compatibility. + +* :mod:`imaplib`: + + * Altering :attr:`IMAP4.file ` is now deprecated + and slated for removal in Python 3.19. This property is now unused + and changing its value does not automatically close the current file. + + Before Python 3.14, this property was used to implement the corresponding + ``read()`` and ``readline()`` methods for :class:`~imaplib.IMAP4` but this + is no longer the case since then. diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 0b0537d3bbd104..4e012029b5e78d 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -703,6 +703,16 @@ The following attributes are defined on instances of :class:`IMAP4`: .. versionadded:: 3.5 +.. property:: IMAP4.file + + Internal :class:`~io.BufferedReader` associated with the underlying socket. + This property is documented for legacy purposes but not part of the public + interface. The caller is responsible to ensure that the current file is + closed before changing it. + + .. deprecated-removed:: next 3.19 + + .. _imap4-example: IMAP4 Example diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 27e3f23e47c875..2d6109a452cf11 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1013,6 +1013,12 @@ New deprecations (Contributed by Bénédikt Tran in :gh:`134978`.) +* :mod:`imaplib`: + + * Altering :attr:`IMAP4.file ` is now deprecated + and slated for removal in Python 3.19. This property is now unused + and changing its value does *not* explicitly close the current file. + * ``__version__`` * The ``__version__`` attribute has been deprecated in these standard library diff --git a/Lib/imaplib.py b/Lib/imaplib.py index c176736548188c..52219025c9541a 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -313,25 +313,34 @@ 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 + warnings._deprecated("IMAP4.file", remove=(3, 19)) + return self._imaplib_file + @file.setter + def file(self, value): + import warnings + warnings._deprecated("IMAP4.file", remove=(3, 19)) + # 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.""" @@ -417,7 +426,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: @@ -921,9 +930,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: @@ -1678,7 +1688,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 430fa71fa29f59..68a426a7d72b9c 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -1,3 +1,5 @@ +import io + from test import support from test.support import socket_helper @@ -659,11 +661,33 @@ 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) + with self.assertWarns(DeprecationWarning): + self.assertIsInstance(client.file.raw, socket.SocketIO) + + def test_file_property_setter(self): + client, _ = self._setup(SimpleIMAPHandler) + with self.assertWarns(DeprecationWarning): + # ensure that the caller closes the existing file + client.file.close() + for new_file in [mock.Mock(), None]: + with self.assertWarns(DeprecationWarning): + client.file = new_file + with self.assertWarns(DeprecationWarning): + self.assertIs(client.file, new_file) + + def test_file_property_setter_should_not_close_previous_file(self): client, _ = self._setup(SimpleIMAPHandler) - # the 'file' property replaced a private attribute that is now unsafe - with self.assertWarns(RuntimeWarning): - client.file + with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f: + f.close.assert_not_called() + with self.assertWarns(DeprecationWarning): + self.assertIs(client.file, f) + with self.assertWarns(DeprecationWarning): + client.file = None + with self.assertWarns(DeprecationWarning): + 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-11-24-25.gh-issue-142307.w8evI9.rst b/Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst new file mode 100644 index 00000000000000..3c0eb0edcfba48 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst @@ -0,0 +1,4 @@ +:mod:`imaplib`: deprecate support for :attr:`IMAP4.file `. +This attribute was never meant to be part of the public interface and altering +its value may result in unclosed files or other synchronization issues with +the underlying socket. Patch by Bénédikt Tran. From f807145b8ddea70637b1c3be0b60e84d242a6fa1 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:28:12 +0100 Subject: [PATCH 2/2] Update Lib/test/test_imaplib.py --- Lib/test/test_imaplib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 68a426a7d72b9c..7a7230485c369a 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -1,5 +1,3 @@ -import io - from test import support from test.support import socket_helper