]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142307: deprecate legacy support for altering `IMAP4.file` (#142335)
authorBénédikt Tran <10796600+picnixz@users.noreply.github.com>
Wed, 6 May 2026 14:41:26 +0000 (16:41 +0200)
committerGitHub <noreply@github.com>
Wed, 6 May 2026 14:41:26 +0000 (17:41 +0300)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Doc/deprecations/pending-removal-in-3.19.rst
Doc/library/imaplib.rst
Doc/whatsnew/3.15.rst
Lib/imaplib.py
Lib/test/test_imaplib.py
Misc/NEWS.d/next/Library/2025-12-06-11-24-25.gh-issue-142307.w8evI9.rst [new file with mode: 0644]

index 044bb8a3934a2a76cff7359a354140fa93dfe58d..4a58c606ab7596e5e18dbb508f47a6a9abf7638e 100644 (file)
@@ -31,3 +31,12 @@ Pending removal in Python 3.19
   * :meth:`http.cookies.BaseCookie.js_output` is deprecated and will be
     removed in Python 3.19.
 
+* :mod:`imaplib`:
+
+  * Altering :attr:`IMAP4.file <imaplib.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.
index b29b02d3cf5fe8b049a9cc705ad6a2a21fa61fc0..fabe2ca9127984e4c86ef5446011e1f026cb9f89 100644 (file)
@@ -695,6 +695,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
index 698a9f88e1ee3954d1dc07bea6342bf3d12ac39e..2ca28378e6ef736f6d7ee96789f368a941a4ba05 100644 (file)
@@ -2107,6 +2107,13 @@ New deprecations
     (Contributed by kishorhange111 in :gh:`148849`.)
 
 
+* :mod:`imaplib`:
+
+  * Altering :attr:`IMAP4.file <imaplib.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.
+
+
 * :mod:`re`:
 
   * :func:`re.match` and :meth:`re.Pattern.match` are now
index cb3edceae0d9f1904650d432703e145ed590fea8..2fafd9322c609eea0686ace234fec61838457dc8 100644 (file)
@@ -313,25 +313,34 @@ class IMAP4:
         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 @@ class IMAP4:
 
     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 @@ class IMAP4:
             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:
@@ -1680,7 +1690,7 @@ class IMAP4_stream(IMAP4):
         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,
index cb5454b40eccf90e7f5a2eeeda2f0c1ffe9343e3..0b704d62655762cb4154813fa3de9a1a77630ce1 100644 (file)
@@ -665,11 +665,33 @@ class NewIMAPTestsMixin:
 
     # 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
+        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)
+        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 (file)
index 0000000..3c0eb0e
--- /dev/null
@@ -0,0 +1,4 @@
+:mod:`imaplib`: deprecate support for :attr:`IMAP4.file <imaplib.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.