Skip to content

Commit 7b6c248

Browse files
picnixzhugovk
andauthored
gh-142307: deprecate legacy support for altering IMAP4.file (#142335)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
1 parent a5c7a74 commit 7b6c248

6 files changed

Lines changed: 83 additions & 21 deletions

File tree

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,12 @@ Pending removal in Python 3.19
3131
* :meth:`http.cookies.BaseCookie.js_output` is deprecated and will be
3232
removed in Python 3.19.
3333

34+
* :mod:`imaplib`:
35+
36+
* Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
37+
and slated for removal in Python 3.19. This property is now unused
38+
and changing its value does not automatically close the current file.
39+
40+
Before Python 3.14, this property was used to implement the corresponding
41+
``read()`` and ``readline()`` methods for :class:`~imaplib.IMAP4` but this
42+
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
@@ -695,6 +695,16 @@ The following attributes are defined on instances of :class:`IMAP4`:
695695
.. versionadded:: 3.5
696696

697697

698+
.. property:: IMAP4.file
699+
700+
Internal :class:`~io.BufferedReader` associated with the underlying socket.
701+
This property is documented for legacy purposes but not part of the public
702+
interface. The caller is responsible to ensure that the current file is
703+
closed before changing it.
704+
705+
.. deprecated-removed:: next 3.19
706+
707+
698708
.. _imap4-example:
699709

700710
IMAP4 Example

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2107,6 +2107,13 @@ New deprecations
21072107
(Contributed by kishorhange111 in :gh:`148849`.)
21082108

21092109

2110+
* :mod:`imaplib`:
2111+
2112+
* Altering :attr:`IMAP4.file <imaplib.IMAP4.file>` is now deprecated
2113+
and slated for removal in Python 3.19. This property is now unused
2114+
and changing its value does *not* explicitly close the current file.
2115+
2116+
21102117
* :mod:`re`:
21112118

21122119
* :func:`re.match` and :meth:`re.Pattern.match` are now

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:
@@ -1680,7 +1690,7 @@ def open(self, host=None, port=None, timeout=None):
16801690
self.host = None # For compatibility with parent class
16811691
self.port = None
16821692
self.sock = None
1683-
self._file = None
1693+
self._imaplib_file = None
16841694
self.process = subprocess.Popen(self.command,
16851695
bufsize=DEFAULT_BUFFER_SIZE,
16861696
stdin=subprocess.PIPE, stdout=subprocess.PIPE,

Lib/test/test_imaplib.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -665,11 +665,33 @@ def test_control_characters(self):
665665

666666
# property tests
667667

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

674696

675697
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)