Skip to content

gh-106240: Add stdlib_deprecations module #106241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Doc/library/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ the `Python Package Index <https://pypi.org>`_.
windows.rst
unix.rst
superseded.rst
stdlib_deprecations.rst
security_warnings.rst
47 changes: 47 additions & 0 deletions Doc/library/stdlib_deprecations.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
:mod:`stdlib_deprecations` --- Standard library deprecations
============================================================

.. module:: stdlib_deprecations
:synopsis: Standard library deprecations.

**Source code:** :source:`Lib/stdlib_deprecations.py`

--------------

Standard library deprecations.

.. versionadded:: 3.13

.. class:: Deprecated

Attributes:

.. attribute:: name

API name (string).

.. attribute:: version

Version when the API was deprecated (tuple of int).

.. attribute:: remove

Version when the API was or will be removed (tuple of int, or None).

.. attribute:: message

Message about the deprecation, usually explain how to replace the
deprecated API (str or None).


.. function:: get_deprecated(name)

Return a :class:`Deprecated` object if the specified module or function is deprecated.
For a function name, return an :class:`Deprecated` object if its module is deprecated.
Return ``None`` if the API is not deprecated.


.. function:: get_capi_deprecated(name)

Return a :class:`Deprecated` object if the specified C API symbol is deprecated.
Return ``None`` if the C API is not deprecated.
3 changes: 2 additions & 1 deletion Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ Other Language Changes
New Modules
===========

* None yet.
* Add :mod:`stdlib_deprecations` module.
(Contributed by Victor Stinner in :gh:`106240`.)


Improved Modules
Expand Down
185 changes: 185 additions & 0 deletions Lib/stdlib_deprecations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import dataclasses
import re


@dataclasses.dataclass(slots=True, frozen=True)
class Deprecated:
name: str
version: tuple[int]
remove: tuple[int] | None
message: str | None


def _parse_version(version_str):
version = tuple(int(part) for part in version_str.split('.'))
if len(version) < 2:
raise ValueError(f"invalid Python version: {version_str!r}")
return version


_DEPRECATIONS = {}
_DEPRECATIONS_CAPI = {}
_REGEX_NAME = r'[A-Za-z_][A-Za-z0-9_]*'
_REGEX_QUALNAME = fr'^{_REGEX_NAME}(\.{_REGEX_NAME})*$'


def _deprecate_api(api_dict, name, version, remove, replace):
if not re.match(_REGEX_QUALNAME, name):
raise ValueError(f"invalid name: {name!a}")
version = _parse_version(version)
if remove is not None:
remove = _parse_version(remove)
if replace is not None:
msg = f'use {replace}'
else:
msg = None
deprecated = Deprecated(name, version, remove, msg)
api_dict[deprecated.name] = deprecated


def _deprecate(name, version, *, remove=None, replace=None):
global _DEPRECATIONS
_deprecate_api(_DEPRECATIONS, name, version, remove, replace)


def _deprecate_capi(name, version, *, remove=None, replace=None):
global _DEPRECATIONS_CAPI
_deprecate_api(_DEPRECATIONS_CAPI, name, version, remove, replace)


# Python 2.6
_deprecate('gzip.GzipFile.filename', '2.6', remove='3.12',
replace='gzip.GzipFile.name'),

# Python 3.6
_deprecate('asyncore', '3.6', remove='3.12', replace='asyncio'),
_deprecate('asynchat', '3.6', remove='3.12', replace='asyncio'),
_deprecate('smtpd', '3.6', remove='3.12', replace='aiosmtp'),
_deprecate('ssl.RAND_pseudo_bytes', '3.6', remove='3.12',
replace='os.urandom()'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the exact call os.urandom() without args, or is it a reference to the function with parentheses added like in C documentation?

Being a Python dev and not a C dev, I take os.urandom to mean one thing and os.urandom() to mean another thing!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.urandom() has no arguments. Adding parenthesis is a hint to show that it's a function

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK but my point is that it’s not a great doc convention for Python. I think the functions that follow need arguments but have () too, that’s misleading.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume that ssl.RAND_pseudo_bytes is the name of a function. The exact replacement is os.random, without (). I think it should be given than way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In practice, I expect that the most usual usage of a function like ssl.RAND_pseudo_bytes is to call it: ssl.RAND_pseudo_bytes(). So I prefer to specify the replacement with parenthesis as well: os.urandom(). In my current implementation, Deprecation.message is an arbitrary string, it's not designed to automate replacement by a linter or a similar tool.


# Python 3.7
_deprecate('ssl.match_hostname', '3.7', remove='3.12'),
_deprecate('ssl.wrap_socket', '3.7', remove='3.12',
replace='ssl.SSLContext.wrap_socket()'),
_deprecate('locale.format', '3.7', remove='3.12',
replace='locale.format_string()'),

# Python 3.10
_deprecate('io.OpenWrapper', '3.10', remove='3.12', replace='open()'),
_deprecate('_pyio.OpenWrapper', '3.10', remove='3.12', replace='open()'),
_deprecate('xml.etree.ElementTree.copy', '3.10', remove='3.12',
replace='copy.copy()'),
_deprecate('zipimport.zipimporter.find_loader', '3.10', remove='3.12',
replace='find_spec() method: PEP 451'),
_deprecate('zipimport.zipimporter.find_module', '3.10', remove='3.12',
replace='find_spec() method: PEP 451'),

# Python 3.11
_deprecate('aifc', '3.11', remove='3.13'),
_deprecate('audioop', '3.11', remove='3.13'),
_deprecate('cgi', '3.11', remove='3.13'),
_deprecate('cgitb', '3.11', remove='3.13'),
_deprecate('chunk', '3.11', remove='3.13'),
_deprecate('crypt', '3.11', remove='3.13'),
_deprecate('imghdr', '3.11', remove='3.13'),
_deprecate('mailcap', '3.11', remove='3.13'),
_deprecate('msilib', '3.11', remove='3.13'),
_deprecate('nis', '3.11', remove='3.13'),
_deprecate('nntplib', '3.11', remove='3.13'),
_deprecate('ossaudiodev', '3.11', remove='3.13'),
_deprecate('pipes', '3.11', remove='3.13'),
_deprecate('sndhdr', '3.11', remove='3.13'),
_deprecate('spwd', '3.11', remove='3.13'),
_deprecate('sunau', '3.11', remove='3.13'),
_deprecate('telnetlib', '3.11', remove='3.13'),
_deprecate('uu', '3.11', remove='3.13'),
_deprecate('xdrlib', '3.11', remove='3.13'),

# Python 3.12
_deprecate('datetime.datetime.utcnow', '3.12',
replace='datetime.datetime.now(tz=datetime.UTC)'),
_deprecate('datetime.datetime.utcfromtimestamp', '3.12',
replace='datetime.datetime.fromtimestamp(tz=datetime.UTC)'),
_deprecate('calendar.January', '3.12'),
_deprecate('calendar.February', '3.12'),
_deprecate('sys.last_value', '3.12'),
_deprecate('sys.last_traceback', '3.12'),
_deprecate('sys.last_exc', '3.12'),
_deprecate('xml.etree.ElementTree.__bool__', '3.12'),

# Python 3.13
_deprecate('ctypes.SetPointerType', '3.13', remove='3.15'),
_deprecate('ctypes.ARRAY', '3.13', remove='3.15'),
_deprecate('wave.Wave_read.getmark', '3.13', remove='3.15'),
_deprecate('wave.Wave_read.getmarkers', '3.13', remove='3.15'),
_deprecate('wave.Wave_read.setmark', '3.13', remove='3.15'),


# C API: Python 3.10
for name in (
'PyUnicode_AS_DATA',
'PyUnicode_AS_UNICODE',
'PyUnicode_AsUnicodeAndSize',
'PyUnicode_AsUnicode',
'PyUnicode_FromUnicode',
'PyUnicode_GET_DATA_SIZE',
'PyUnicode_GET_SIZE',
'PyUnicode_GetSize',
'PyUnicode_IS_COMPACT',
'PyUnicode_IS_READY',
'PyUnicode_READY',
'Py_UNICODE_WSTR_LENGTH',
'_PyUnicode_AsUnicode',
'PyUnicode_WCHAR_KIND',
'PyUnicodeObject',
'PyUnicode_InternImmortal',
):
_deprecate_capi(name, '3.10', remove='3.12')

# C API: Python 3.12
_deprecate_capi('PyDictObject.ma_version_tag', '3.12', remove='3.14'),
for name, replace in (
('Py_DebugFlag', 'PyConfig.parser_debug'),
('Py_VerboseFlag', 'PyConfig.verbose'),
('Py_QuietFlag', 'PyConfig.quiet'),
('Py_InteractiveFlag', 'PyConfig.interactive'),
('Py_InspectFlag', 'PyConfig.inspect'),
('Py_OptimizeFlag', 'PyConfig.optimization_level'),
('Py_NoSiteFlag', 'PyConfig.site_import'),
('Py_BytesWarningFlag', 'PyConfig.bytes_warning'),
('Py_FrozenFlag', 'PyConfig.pathconfig_warnings'),
('Py_IgnoreEnvironmentFlag', 'PyConfig.use_environment'),
('Py_DontWriteBytecodeFlag', 'PyConfig.write_bytecode'),
('Py_NoUserSiteDirectory', 'PyConfig.user_site_directory'),
('Py_UnbufferedStdioFlag', 'PyConfig.buffered_stdio'),
('Py_HashRandomizationFlag', 'PyConfig.hash_seed'),
('Py_IsolatedFlag', 'PyConfig.isolated'),
('Py_LegacyWindowsFSEncodingFlag', 'PyPreConfig.legacy_windows_fs_encoding'),
('Py_LegacyWindowsStdioFlag', 'PyConfig.legacy_windows_stdio'),
('Py_FileSystemDefaultEncoding', 'PyConfig.filesystem_encoding'),
('Py_FileSystemDefaultEncodeErrors', 'PyConfig.filesystem_errors'),
('Py_UTF8Mode', 'PyPreConfig.utf8_mode'),
):
_deprecate_capi(name, '3.12', replace=replace)

_deprecate_capi('PyErr_Display', '3.12', replace='PyErr_DisplayException()'),
_deprecate_capi('_PyErr_ChainExceptions', '3.12', replace='_PyErr_ChainExceptions1()'),


def get_deprecated(name):
try:
return _DEPRECATIONS[name]
except KeyError:
pass

parts = name.split('.')
if len(parts) == 1:
return False

module_name = parts[0]
return _DEPRECATIONS.get(module_name)


def get_capi_deprecated(name):
return _DEPRECATIONS_CAPI.get(name)
33 changes: 33 additions & 0 deletions Lib/test/test_stdlib_deprecations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import unittest
import stdlib_deprecations


class Tests(unittest.TestCase):
def test_python_api(self):
obj = stdlib_deprecations.get_deprecated('asyncore')
self.assertEqual(obj.name, 'asyncore')
self.assertEqual(obj.version, (3, 6))
self.assertEqual(obj.remove, (3, 12))
self.assertEqual(obj.message, 'use asyncio')

self.assertIs(stdlib_deprecations.get_deprecated('asyncore.loop'),
stdlib_deprecations.get_deprecated('asyncore'))

self.assertIsNone(stdlib_deprecations.get_deprecated('builtins.open'))

def test_c_api(self):
obj = stdlib_deprecations.get_capi_deprecated('Py_VerboseFlag')
self.assertEqual(obj.name, 'Py_VerboseFlag')
self.assertEqual(obj.version, (3, 12))
self.assertIsNone(obj.remove)
self.assertEqual(obj.message, 'use PyConfig.verbose')

self.assertIsNotNone(stdlib_deprecations.get_capi_deprecated('Py_VerboseFlag'))
self.assertIsNotNone(stdlib_deprecations.get_capi_deprecated('PyUnicode_InternImmortal'))

self.assertIsNone(stdlib_deprecations.get_capi_deprecated('Py_Initialize'))



if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :mod:`stdlib_deprecations` module. Patch by Victor Stinner.
1 change: 1 addition & 0 deletions Python/stdlib_module_names.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.