diff --git a/Doc/library/binascii.rst b/Doc/library/binascii.rst index 21960cb7972e6e..4e208e37d3e301 100644 --- a/Doc/library/binascii.rst +++ b/Doc/library/binascii.rst @@ -38,6 +38,10 @@ The :mod:`binascii` module defines the following functions: data. Lines normally contain 45 (binary) bytes, except for the last line. Line data may be followed by whitespace. + .. deprecated-removed:: 3.12 3.14 + This function and the legacy uuencode format it implements are deprecated + (see :pep:`PEP 594 <594#uu-and-the-uu-encoding>` for details). + .. function:: b2a_uu(data, *, backtick=False) @@ -48,6 +52,10 @@ The :mod:`binascii` module defines the following functions: .. versionchanged:: 3.7 Added the *backtick* parameter. + .. deprecated-removed:: 3.12 3.14 + This function and the legacy uuencode format it implements are deprecated + (see :pep:`PEP 594 <594#uu-and-the-uu-encoding>` for details). + .. function:: a2b_base64(string, /, *, strict_mode=False) diff --git a/Doc/library/codecs.rst b/Doc/library/codecs.rst index 8225236350d22e..d1fef1ad0a879a 100644 --- a/Doc/library/codecs.rst +++ b/Doc/library/codecs.rst @@ -1414,7 +1414,7 @@ to :class:`bytes` mappings. They are not supported by :meth:`bytes.decode` | | quoted_printable | | :meth:`quopri.decode` | +----------------------+------------------+------------------------------+------------------------------+ | uu_codec | uu | Convert the operand using | :meth:`uu.encode` / | -| | | uuencode. | :meth:`uu.decode` | +| | | uuencode (deprecated). | :meth:`uu.decode` | +----------------------+------------------+------------------------------+------------------------------+ | zlib_codec | zip, zlib | Compress the operand using | :meth:`zlib.compress` / | | | | gzip. | :meth:`zlib.decompress` | @@ -1430,6 +1430,10 @@ to :class:`bytes` mappings. They are not supported by :meth:`bytes.decode` .. versionchanged:: 3.4 Restoration of the aliases for the binary transforms. +.. deprecated-removed:: 3.12 3.14 + The uuencode codec (``uu_codec``) is deprecated + (see :pep:`PEP 594 <594#uu-and-the-uu-encoding>` for details). + .. _text-transforms: diff --git a/Doc/library/email.compat32-message.rst b/Doc/library/email.compat32-message.rst index 5bef155a4af310..10810ad343f7b8 100644 --- a/Doc/library/email.compat32-message.rst +++ b/Doc/library/email.compat32-message.rst @@ -227,6 +227,10 @@ Here are the methods of the :class:`Message` class: replaced by :meth:`~email.message.EmailMessage.get_content` and :meth:`~email.message.EmailMessage.iter_parts`. + .. deprecated-removed:: 3.12 3.14 + Decoding legacy uuencode payloads (with ``decode=True``) is deprecated + (see :pep:`PEP 594 <594#uu-and-the-uu-encoding>` for details). + .. method:: set_payload(payload, charset=None) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 4f952e2a37ef2f..dd9961b3286c9c 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -682,6 +682,24 @@ Deprecated and tailor them to your needs. (Contributed by Erlend E. Aasland in :gh:`90016`.) +* Per :pep:`PEP 594 <594#uu-and-the-uu-encoding>`, + the remaining functionality related to the legacy + `uuencode encoding `__ + (also exposed in the to-be-removed :mod:`uu` module) has been deprecated, + and will be removed in Python 3.14: + + * :func:`binascii.a2b_uu` and :func:`binascii.b2a_uu`, + low-level interfaces for decoding and encoding uuencode data. + * The ``uu_codec`` :ref:`binary transform ` + in the :mod:`codecs` module, + implementing uuencode as a Python codec + * Support for decoding non-MIME uuencode payloads + with the :meth:`email.message.Message.get_payload` method + of the legacy :ref:`email.message.Message ` + (:attr:`email.policy.Compat32`) API. + + (Contributed by C.A.M. Gerlach in :gh:`92613`.) + * In :meth:`~sqlite3.Cursor.execute`, :exc:`DeprecationWarning` is now emitted when :ref:`named placeholders ` are used together with parameters supplied as a :term:`sequence` instead of as a :class:`dict`. diff --git a/Lib/email/message.py b/Lib/email/message.py index 411118c74dabb4..205314858b58d4 100644 --- a/Lib/email/message.py +++ b/Lib/email/message.py @@ -7,8 +7,9 @@ __all__ = ['Message', 'EmailMessage'] import binascii -import re import quopri +import re +import warnings from io import BytesIO, StringIO # Intrapackage imports @@ -318,8 +319,15 @@ def get_payload(self, i=None, decode=False): self.policy.handle_defect(self, defect) return value elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): + warnings._deprecated( + 'Decoding legacy uuencoded payloads in messages', + remove=(3, 14)) try: - return _decode_uu(bpayload) + # We already issue our own warning here + with warnings.catch_warnings(): + warnings.filterwarnings( + 'ignore', message='.*uu.*', category=DeprecationWarning) + return _decode_uu(bpayload) except ValueError: # Some decoding problem. return bpayload diff --git a/Lib/encodings/uu_codec.py b/Lib/encodings/uu_codec.py index 4e58c62fe9ef0f..1750001137e30f 100644 --- a/Lib/encodings/uu_codec.py +++ b/Lib/encodings/uu_codec.py @@ -7,13 +7,23 @@ modified by Jack Jansen and Fredrik Lundh. """ -import codecs import binascii +import codecs +import warnings from io import BytesIO + +_uu_deprecation_warning_filter = { + 'action': 'ignore', + 'message': '.*uu.*', + 'category': DeprecationWarning, +} + + ### Codec APIs def uu_encode(input, errors='strict', filename='', mode=0o666): + warnings._deprecated(__name__, remove=(3, 14)) assert errors == 'strict' infile = BytesIO(input) outfile = BytesIO() @@ -27,14 +37,19 @@ def uu_encode(input, errors='strict', filename='', mode=0o666): # Encode write(('begin %o %s\n' % (mode & 0o777, filename)).encode('ascii')) chunk = read(45) - while chunk: - write(binascii.b2a_uu(chunk)) - chunk = read(45) + + # We already warn above on calling this function + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + while chunk: + write(binascii.b2a_uu(chunk)) + chunk = read(45) write(b' \nend\n') return (outfile.getvalue(), len(input)) def uu_decode(input, errors='strict'): + warnings._deprecated(__name__, remove=(3, 14)) assert errors == 'strict' infile = BytesIO(input) outfile = BytesIO() @@ -55,11 +70,17 @@ def uu_decode(input, errors='strict'): if not s or s == b'end\n': break try: - data = binascii.a2b_uu(s) + # We already warn above on calling this function + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + data = binascii.a2b_uu(s) except binascii.Error as v: # Workaround for broken uuencoders by /Fredrik Lundh nbytes = (((s[0]-32) & 63) * 4 + 5) // 3 - data = binascii.a2b_uu(s[:nbytes]) + # We already warn above on calling this function + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + data = binascii.a2b_uu(s[:nbytes]) #sys.stderr.write("Warning: %s\n" % str(v)) write(data) if not s: diff --git a/Lib/test/test_binascii.py b/Lib/test/test_binascii.py index a2d7d0293ce1ae..221df7d9e35346 100644 --- a/Lib/test/test_binascii.py +++ b/Lib/test/test_binascii.py @@ -1,10 +1,11 @@ """Test the binascii C module.""" -import unittest -import binascii import array +import binascii +import contextlib import re -from test.support import bigmemtest, _1G, _4G +import unittest +from test.support import bigmemtest, _1G, _4G, warnings_helper # Note: "*_hex" functions are aliases for "(un)hexlify" @@ -14,6 +15,16 @@ 'unhexlify'] all_functions = a2b_functions + b2a_functions + ['crc32', 'crc_hqx'] +deprecated_functions = ['b2a_uu', 'a2b_uu'] + + +def _check_function_warning(function_name): + """Helper to check that deprecated functions warn, and silence them.""" + if function_name not in deprecated_functions: + return contextlib.nullcontext() + return warnings_helper.check_warnings( + (f".*{function_name}.*", DeprecationWarning)) + class BinASCIITest(unittest.TestCase): @@ -46,8 +57,10 @@ def test_returned_value(self): a2b = getattr(binascii, fa) b2a = getattr(binascii, fb) try: - a = b2a(self.type2test(raw)) - res = a2b(self.type2test(a)) + with _check_function_warning(fb): + a = b2a(self.type2test(raw)) + with _check_function_warning(fa): + res = a2b(self.type2test(a)) except Exception as err: self.fail("{}/{} conversion raises {!r}".format(fb, fa, err)) self.assertEqual(res, raw, "{}/{} conversion: " @@ -185,6 +198,8 @@ def assertInvalidLength(data): assertInvalidLength(b'a' * (4 * 87 + 1)) assertInvalidLength(b'A\tB\nC ??DE') # only 5 valid characters + # Uuencode is deprecated + @warnings_helper.ignore_warnings(category=DeprecationWarning) def test_uu(self): MAX_UU = 45 for backtick in (True, False): @@ -383,7 +398,8 @@ def test_empty_string(self): continue f = getattr(binascii, func) try: - f(empty) + with _check_function_warning(func): + f(empty) except Exception as err: self.fail("{}({!r}) raises {!r}".format(func, empty, err)) @@ -405,10 +421,13 @@ def test_unicode_a2b(self): a2b = getattr(binascii, fa) b2a = getattr(binascii, fb) try: - a = b2a(self.type2test(raw)) - binary_res = a2b(a) + with _check_function_warning(fb): + a = b2a(self.type2test(raw)) + with _check_function_warning(fa): + binary_res = a2b(a) a = a.decode('ascii') - res = a2b(a) + with _check_function_warning(fa): + res = a2b(a) except Exception as err: self.fail("{}/{} conversion raises {!r}".format(fb, fa, err)) self.assertEqual(res, raw, "{}/{} conversion: " diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index 376175f90f63eb..b87141a09558b8 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -8,7 +8,7 @@ from unittest import mock from test import support -from test.support import os_helper +from test.support import os_helper, warnings_helper try: import _testcapi @@ -2711,6 +2711,11 @@ def test_seek0(self): "rot_13": ["rot13"], } +deprecated_transforms = { + "uu_codec", +} + + try: import zlib except ImportError: @@ -2727,6 +2732,14 @@ def test_seek0(self): transform_aliases["bz2_codec"] = ["bz2"] +def _check_transform_warning(encoding): + """Helper to check that deprecated transforms warn and silence them.""" + if encoding not in deprecated_transforms: + return contextlib.nullcontext() + return warnings_helper.check_warnings( + (f".*({encoding}).*", DeprecationWarning)) + + class TransformCodecTest(unittest.TestCase): def test_basics(self): @@ -2734,26 +2747,30 @@ def test_basics(self): for encoding in bytes_transform_encodings: with self.subTest(encoding=encoding): # generic codecs interface - (o, size) = codecs.getencoder(encoding)(binput) + with _check_transform_warning(encoding): + (o, size) = codecs.getencoder(encoding)(binput) self.assertEqual(size, len(binput)) - (i, size) = codecs.getdecoder(encoding)(o) + with _check_transform_warning(encoding): + (i, size) = codecs.getdecoder(encoding)(o) self.assertEqual(size, len(o)) self.assertEqual(i, binput) def test_read(self): for encoding in bytes_transform_encodings: with self.subTest(encoding=encoding): - sin = codecs.encode(b"\x80", encoding) - reader = codecs.getreader(encoding)(io.BytesIO(sin)) - sout = reader.read() + with _check_transform_warning(encoding): + sin = codecs.encode(b"\x80", encoding) + reader = codecs.getreader(encoding)(io.BytesIO(sin)) + sout = reader.read() self.assertEqual(sout, b"\x80") def test_readline(self): for encoding in bytes_transform_encodings: with self.subTest(encoding=encoding): - sin = codecs.encode(b"\x80", encoding) - reader = codecs.getreader(encoding)(io.BytesIO(sin)) - sout = reader.readline() + with _check_transform_warning(encoding): + sin = codecs.encode(b"\x80", encoding) + reader = codecs.getreader(encoding)(io.BytesIO(sin)) + sout = reader.readline() self.assertEqual(sout, b"\x80") def test_buffer_api_usage(self): @@ -2765,13 +2782,16 @@ def test_buffer_api_usage(self): with self.subTest(encoding=encoding): data = original view = memoryview(data) - data = codecs.encode(data, encoding) - view_encoded = codecs.encode(view, encoding) + with _check_transform_warning(encoding): + data = codecs.encode(data, encoding) + view_encoded = codecs.encode(view, encoding) self.assertEqual(view_encoded, data) view = memoryview(data) - data = codecs.decode(data, encoding) + with _check_transform_warning(encoding): + data = codecs.decode(data, encoding) self.assertEqual(data, original) - view_decoded = codecs.decode(view, encoding) + with _check_transform_warning(encoding): + view_decoded = codecs.decode(view, encoding) self.assertEqual(view_decoded, data) def test_text_to_binary_denylists_binary_transforms(self): @@ -2799,7 +2819,8 @@ def test_binary_to_text_denylists_binary_transforms(self): data = b"encode first to ensure we meet any format restrictions" for encoding in bytes_transform_encodings: with self.subTest(encoding=encoding): - encoded_data = codecs.encode(data, encoding) + with _check_transform_warning(encoding): + encoded_data = codecs.encode(data, encoding) fmt = (r"{!r} is not a text encoding; " r"use codecs.decode\(\) to handle arbitrary codecs") msg = fmt.format(encoding) @@ -2852,7 +2873,8 @@ def test_quopri_stateless(self): def test_uu_invalid(self): # Missing "begin" line - self.assertRaises(ValueError, codecs.decode, b"", "uu-codec") + with _check_transform_warning("uu_codec"): + self.assertRaises(ValueError, codecs.decode, b"", "uu-codec") # The codec system tries to add notes to exceptions in order to ensure diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index 44b405740c4403..0bbdc7417df262 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -39,6 +39,7 @@ from email import utils from test.support import threading_helper +from test.support.warnings_helper import check_warnings from test.support.os_helper import unlink from test.test_email import openfile, TestEmailBase @@ -50,6 +51,8 @@ EMPTYSTRING = '' SPACE = ' ' +UU_WARNING_FILTER = ('.*uu.*', DeprecationWarning) + # Test various aspects of the Message class's API class TestMessageAPI(TestEmailBase): @@ -263,10 +266,12 @@ def test_get_decoded_uu_payload(self): msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): msg['content-transfer-encoding'] = cte - eq(msg.get_payload(decode=True), b'hello world') + with check_warnings(UU_WARNING_FILTER): + eq(msg.get_payload(decode=True), b'hello world') # Now try some bogus data msg.set_payload('foo') - eq(msg.get_payload(decode=True), b'foo') + with check_warnings(UU_WARNING_FILTER): + eq(msg.get_payload(decode=True), b'foo') def test_get_payload_n_raises_on_non_multipart(self): msg = Message() @@ -740,12 +745,13 @@ def test_binary_uuencode_payload(self): msg['content-type'] = 'text/plain; charset=%s' % charset msg['content-transfer-encoding'] = encoding msg.set_payload(b"begin 666 -\n)9F]OYI:'8F%R\n \nend\n") - self.assertEqual( - msg.get_payload(decode=True), - b'foo\xe6\x96\x87bar', - str(('get_payload returns wrong result ', - 'with charset {0} and encoding {1}.')).\ - format(charset, encoding)) + with check_warnings(UU_WARNING_FILTER): + self.assertEqual( + msg.get_payload(decode=True), + b'foo\xe6\x96\x87bar', + str(('get_payload returns wrong result ', + 'with charset {0} and encoding {1}.')).\ + format(charset, encoding)) def test_add_header_with_name_only_param(self): msg = Message() @@ -3963,8 +3969,9 @@ def test_8bit_in_uuencode_body(self): cte='uuencode', bodyline='<,.V 0: - out_file.write(binascii.b2a_uu(data, backtick=backtick)) + # We already warn on import of this module + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + converted_data = binascii.b2a_uu(data, backtick=backtick) + out_file.write(converted_data) data = in_file.read(45) if backtick: out_file.write(b'`\nend\n') @@ -152,11 +163,17 @@ def decode(in_file, out_file=None, mode=None, quiet=False): s = in_file.readline() while s and s.strip(b' \t\r\n\f') != b'end': try: - data = binascii.a2b_uu(s) + # We already warn on import of this module + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + data = binascii.a2b_uu(s) except binascii.Error as v: # Workaround for broken uuencoders by /Fredrik Lundh nbytes = (((s[0]-32) & 63) * 4 + 5) // 3 - data = binascii.a2b_uu(s[:nbytes]) + # We already warn on import of this module + with warnings.catch_warnings(): + warnings.filterwarnings(**_uu_deprecation_warning_filter) + data = binascii.a2b_uu(s[:nbytes]) if not quiet: sys.stderr.write("Warning: %s\n" % v) out_file.write(data) diff --git a/Misc/NEWS.d/next/Library/2022-05-12-22-05-49.gh-issue-92613.yQjMHl.rst b/Misc/NEWS.d/next/Library/2022-05-12-22-05-49.gh-issue-92613.yQjMHl.rst new file mode 100644 index 00000000000000..236fe105115923 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-05-12-22-05-49.gh-issue-92613.yQjMHl.rst @@ -0,0 +1,11 @@ +Per :pep:`PEP 594 <594#uu-and-the-uu-encoding>`, +deprecate other uuencode-related functionality with appropriate warnings, +and document them as scheduled for removal in Python 3.14. +This includes :func:`binascii.a2b_uu`/:func:`binascii.b2a_uu`, +the ``uu_codec`` :ref:`binary transform ` +in the :mod:`codecs` module, +and support for decoding uuencode payloads +with the :meth:`email.message.Message.get_payload` method +of the legacy +:ref:`email.message.Message ` (``Compat32``) API. +Contributed by C.A.M. Gerlach. diff --git a/Modules/binascii.c b/Modules/binascii.c index 95ddb26988d6c9..ea0a1d40da661f 100644 --- a/Modules/binascii.c +++ b/Modules/binascii.c @@ -223,6 +223,13 @@ binascii_a2b_uu_impl(PyObject *module, Py_buffer *data) Py_ssize_t ascii_len, bin_len; binascii_state *state; + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "'binascii.a2b_uu' is deprecated and scheduled for " + "removal in Python 3.14", + 1)) { + return NULL; + } + ascii_data = data->buf; ascii_len = data->len; @@ -321,6 +328,13 @@ binascii_b2a_uu_impl(PyObject *module, Py_buffer *data, int backtick) Py_ssize_t bin_len, out_len; _PyBytesWriter writer; + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "'binascii.b2a_uu' is deprecated and scheduled for " + "removal in Python 3.14", + 1)) { + return NULL; + } + _PyBytesWriter_Init(&writer); bin_data = data->buf; bin_len = data->len;