Skip to content

Commit f28c0cc

Browse files
committed
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.
1 parent eba449a commit f28c0cc

File tree

6 files changed

+85
-21
lines changed

6 files changed

+85
-21
lines changed

Doc/deprecations/pending-removal-in-3.19.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ Pending removal in Python 3.19
2222
supported depending on the backend implementation of hash functions.
2323
Prefer passing the initial data as a positional argument for maximum
2424
backwards compatibility.
25+
26+
* :mod:`imaplib`:
27+
28+
* Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
29+
and slated for removal in Python 3.19. This property is now unused
30+
and changing its value does not automatically close the current file.
31+
32+
Before Python 3.14, this property was used to implement the corresponding
33+
``read()`` and ``readline()`` methods for :class:`~imaplib.IMAP4` but this
34+
is no longer the case since then.

Doc/library/imaplib.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,16 @@ The following attributes are defined on instances of :class:`IMAP4`:
703703
.. versionadded:: 3.5
704704

705705

706+
.. property:: IMAP4.file
707+
708+
Internal :class:`~io.BufferedReader` associated with the underlying socket.
709+
This property is documented for legacy purposes but not part of the public
710+
interface. The caller is responsible to ensure that the current file is
711+
closed before changing it.
712+
713+
.. deprecated-removed:: next 3.19
714+
715+
706716
.. _imap4-example:
707717

708718
IMAP4 Example

Doc/whatsnew/3.15.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,12 @@ New deprecations
10131013

10141014
(Contributed by Bénédikt Tran in :gh:`134978`.)
10151015

1016+
* :mod:`imaplib`:
1017+
1018+
* Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
1019+
and slated for removal in Python 3.19. This property is now unused
1020+
and changing its value does *not* explicitly close the current file.
1021+
10161022
* ``__version__``
10171023

10181024
* The ``__version__`` attribute has been deprecated in these standard library

Lib/imaplib.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -313,25 +313,34 @@ def open(self, host='', port=IMAP4_PORT, timeout=None):
313313
self.host = host
314314
self.port = port
315315
self.sock = self._create_socket(timeout)
316-
self._file = self.sock.makefile('rb')
317-
316+
# Since IMAP4 implements its own read() and readline() buffering,
317+
# the '_imaplib_file' attribute is unused. Nonetheless it is kept
318+
# and exposed solely for backward compatibility purposes.
319+
self._imaplib_file = self.sock.makefile('rb')
318320

319321
@property
320322
def file(self):
321-
# The old 'file' attribute is no longer used now that we do our own
322-
# read() and readline() buffering, with which it conflicts.
323-
# As an undocumented interface, it should never have been accessed by
324-
# external code, and therefore does not warrant deprecation.
325-
# Nevertheless, we provide this property for now, to avoid suddenly
326-
# breaking any code in the wild that might have been using it in a
327-
# harmless way.
328323
import warnings
329-
warnings.warn(
330-
'IMAP4.file is unsupported, can cause errors, and may be removed.',
331-
RuntimeWarning,
332-
stacklevel=2)
333-
return self._file
324+
warnings._deprecated("IMAP4.file", remove=(3, 19))
325+
return self._imaplib_file
334326

327+
@file.setter
328+
def file(self, value):
329+
import warnings
330+
warnings._deprecated("IMAP4.file", remove=(3, 19))
331+
# Ideally, we would want to close the previous file,
332+
# but since we do not know how subclasses will use
333+
# that setter, it is probably better to leave it to
334+
# the caller.
335+
self._imaplib_file = value
336+
337+
def _close_imaplib_file(self):
338+
file = self._imaplib_file
339+
if file is not None:
340+
try:
341+
file.close()
342+
except OSError:
343+
pass
335344

336345
def read(self, size):
337346
"""Read 'size' bytes from remote."""
@@ -417,7 +426,7 @@ def send(self, data):
417426

418427
def shutdown(self):
419428
"""Close I/O established in "open"."""
420-
self._file.close()
429+
self._close_imaplib_file()
421430
try:
422431
self.sock.shutdown(socket.SHUT_RDWR)
423432
except OSError as exc:
@@ -921,9 +930,10 @@ def starttls(self, ssl_context=None):
921930
ssl_context = ssl._create_stdlib_context()
922931
typ, dat = self._simple_command(name)
923932
if typ == 'OK':
933+
self._close_imaplib_file()
924934
self.sock = ssl_context.wrap_socket(self.sock,
925935
server_hostname=self.host)
926-
self._file = self.sock.makefile('rb')
936+
self._imaplib_file = self.sock.makefile('rb')
927937
self._tls_established = True
928938
self._get_capabilities()
929939
else:
@@ -1678,7 +1688,7 @@ def open(self, host=None, port=None, timeout=None):
16781688
self.host = None # For compatibility with parent class
16791689
self.port = None
16801690
self.sock = None
1681-
self._file = None
1691+
self._imaplib_file = None
16821692
self.process = subprocess.Popen(self.command,
16831693
bufsize=DEFAULT_BUFFER_SIZE,
16841694
stdin=subprocess.PIPE, stdout=subprocess.PIPE,

Lib/test/test_imaplib.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io
2+
13
from test import support
24
from test.support import socket_helper
35

@@ -659,11 +661,33 @@ def test_unselect(self):
659661

660662
# property tests
661663

662-
def test_file_property_should_not_be_accessed(self):
664+
def test_file_property_getter(self):
665+
client, _ = self._setup(SimpleIMAPHandler)
666+
with self.assertWarns(DeprecationWarning):
667+
self.assertIsInstance(client.file.raw, socket.SocketIO)
668+
669+
def test_file_property_setter(self):
670+
client, _ = self._setup(SimpleIMAPHandler)
671+
with self.assertWarns(DeprecationWarning):
672+
# ensure that the caller closes the existing file
673+
client.file.close()
674+
for new_file in [mock.Mock(), None]:
675+
with self.assertWarns(DeprecationWarning):
676+
client.file = new_file
677+
with self.assertWarns(DeprecationWarning):
678+
self.assertIs(client.file, new_file)
679+
680+
def test_file_property_setter_should_not_close_previous_file(self):
663681
client, _ = self._setup(SimpleIMAPHandler)
664-
# the 'file' property replaced a private attribute that is now unsafe
665-
with self.assertWarns(RuntimeWarning):
666-
client.file
682+
with mock.patch.object(client, "_imaplib_file", mock.Mock()) as f:
683+
f.close.assert_not_called()
684+
with self.assertWarns(DeprecationWarning):
685+
self.assertIs(client.file, f)
686+
with self.assertWarns(DeprecationWarning):
687+
client.file = None
688+
with self.assertWarns(DeprecationWarning):
689+
self.assertIsNone(client.file)
690+
f.close.assert_not_called()
667691

668692

669693
class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`imaplib`: deprecate support for :attr:`IMAP4.file <imaplib.IMAP4.file>`.
2+
This attribute was never meant to be part of the public interface and altering
3+
its value may result in unclosed files or other synchronization issues with
4+
the underlying socket. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)