Skip to content

Commit 08db063

Browse files
committed
pythongh-106240: Add stdlib_deprecations module
1 parent cea9d4e commit 08db063

File tree

6 files changed

+269
-1
lines changed

6 files changed

+269
-1
lines changed

Doc/library/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ the `Python Package Index <https://pypi.org>`_.
7474
windows.rst
7575
unix.rst
7676
superseded.rst
77+
stdlib_deprecations.rst
7778
security_warnings.rst

Doc/library/stdlib_deprecations.rst

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
:mod:`stdlib_deprecations` --- Standard library deprecations
2+
============================================================
3+
4+
.. module:: stdlib_deprecations
5+
:synopsis: Standard library deprecations.
6+
7+
**Source code:** :source:`Lib/stdlib_deprecations.py`
8+
9+
--------------
10+
11+
Standard library deprecations.
12+
13+
.. versionadded:: 3.13
14+
15+
.. class:: Deprecated
16+
17+
Attributes::
18+
19+
.. attribute:: name
20+
21+
API name (string).
22+
23+
.. attribute:: version
24+
25+
Version when the API was deprecated (tuple of int).
26+
27+
.. attribute:: remove
28+
29+
Version when the API was or will be removed (tuple of int, or None).
30+
31+
.. attribute:: remove
32+
33+
Message about the deprecation, usually explain how to replace the
34+
deprecated API (str or None).
35+
36+
37+
.. function:: get_deprecated(name)
38+
39+
Return a :class:`Deprecated` object if the specified module or function is deprecated.
40+
For a function name, return an :class:`Deprecated` object if its module is deprecated.
41+
Return ``None`` if the API is not deprecated.
42+
43+
44+
.. function:: get_capi_deprecated(name)
45+
46+
Return a :class:`Deprecated` object if the specified C API symbol is deprecated.
47+
Return ``None`` if the C API is not deprecated.

Doc/whatsnew/3.13.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ Other Language Changes
8181
New Modules
8282
===========
8383

84-
* None yet.
84+
* Add :mod:`stdlib_deprecations` module.
85+
(Contributed by Victor Stinner in :gh:`106240`.)
8586

8687

8788
Improved Modules

Lib/stdlib_deprecations.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import dataclasses
2+
import re
3+
4+
5+
@dataclasses.dataclass(slots=True, frozen=True)
6+
class Deprecated:
7+
name: str
8+
version: tuple[int]
9+
remove: tuple[int] | None
10+
message: str | None
11+
12+
13+
def _parse_version(version_str):
14+
version = tuple(int(part) for part in version_str.split('.'))
15+
if len(version) < 2:
16+
raise ValueError(f"invalid Python version: {version_str!r}")
17+
return version
18+
19+
20+
_DEPRECATIONS = {}
21+
_DEPRECATIONS_CAPI = {}
22+
_REGEX_NAME = r'[A-Za-z_][A-Za-z0-9_]*'
23+
_REGEX_QUALNAME = fr'^{_REGEX_NAME}(\.{_REGEX_NAME})*$'
24+
25+
26+
def _deprecate_api(api_dict, name, version, remove, replace):
27+
if not re.match(_REGEX_QUALNAME, name):
28+
raise ValueError(f"invalid name: {name!a}")
29+
version = _parse_version(version)
30+
if remove is not None:
31+
remove = _parse_version(remove)
32+
if replace is not None:
33+
msg = f'use {replace}'
34+
else:
35+
msg = None
36+
deprecated = Deprecated(name, version, remove, msg)
37+
api_dict[deprecated.name] = deprecated
38+
39+
40+
def _deprecate(name, version, *, remove=None, replace=None):
41+
global _DEPRECATIONS
42+
_deprecate_api(_DEPRECATIONS, name, version, remove, replace)
43+
44+
45+
def _deprecate_capi(name, version, *, remove=None, replace=None):
46+
global _DEPRECATIONS_CAPI
47+
_deprecate_api(_DEPRECATIONS_CAPI, name, version, remove, replace)
48+
49+
50+
# Python 2.6
51+
_deprecate('gzip.GzipFile.filename', '2.6', remove='3.12',
52+
replace='gzip.GzipFile.name'),
53+
54+
# Python 3.6
55+
_deprecate('asyncore', '3.6', remove='3.12', replace='asyncio'),
56+
_deprecate('asynchat', '3.6', remove='3.12', replace='asyncio'),
57+
_deprecate('smtpd', '3.6', remove='3.12', replace='aiosmtp'),
58+
_deprecate('ssl.RAND_pseudo_bytes', '3.6', remove='3.12',
59+
replace='os.urandom()'),
60+
61+
# Python 3.7
62+
_deprecate('ssl.match_hostname', '3.7', remove='3.12'),
63+
_deprecate('ssl.wrap_socket', '3.7', remove='3.12',
64+
replace='ssl.SSLContext.wrap_socket()'),
65+
_deprecate('locale.format', '3.7', remove='3.12',
66+
replace='locale.format_string()'),
67+
68+
# Python 3.10
69+
_deprecate('io.OpenWrapper', '3.10', remove='3.12', replace='open()'),
70+
_deprecate('_pyio.OpenWrapper', '3.10', remove='3.12', replace='open()'),
71+
_deprecate('xml.etree.ElementTree.copy', '3.10', remove='3.12',
72+
replace='copy.copy()'),
73+
_deprecate('zipimport.zipimporter.find_loader', '3.10', remove='3.12',
74+
replace='find_spec() method: PEP 451'),
75+
_deprecate('zipimport.zipimporter.find_module', '3.10', remove='3.12',
76+
replace='find_spec() method: PEP 451'),
77+
78+
# Python 3.11
79+
_deprecate('aifc', '3.11', remove='3.13'),
80+
_deprecate('audioop', '3.11', remove='3.13'),
81+
_deprecate('cgi', '3.11', remove='3.13'),
82+
_deprecate('cgitb', '3.11', remove='3.13'),
83+
_deprecate('chunk', '3.11', remove='3.13'),
84+
_deprecate('crypt', '3.11', remove='3.13'),
85+
_deprecate('imghdr', '3.11', remove='3.13'),
86+
_deprecate('mailcap', '3.11', remove='3.13'),
87+
_deprecate('msilib', '3.11', remove='3.13'),
88+
_deprecate('nis', '3.11', remove='3.13'),
89+
_deprecate('nntplib', '3.11', remove='3.13'),
90+
_deprecate('ossaudiodev', '3.11', remove='3.13'),
91+
_deprecate('pipes', '3.11', remove='3.13'),
92+
_deprecate('sndhdr', '3.11', remove='3.13'),
93+
_deprecate('spwd', '3.11', remove='3.13'),
94+
_deprecate('sunau', '3.11', remove='3.13'),
95+
_deprecate('telnetlib', '3.11', remove='3.13'),
96+
_deprecate('uu', '3.11', remove='3.13'),
97+
_deprecate('xdrlib', '3.11', remove='3.13'),
98+
99+
# Python 3.12
100+
_deprecate('datetime.datetime.utcnow', '3.12',
101+
replace='datetime.datetime.now(tz=datetime.UTC)'),
102+
_deprecate('datetime.datetime.utcfromtimestamp', '3.12',
103+
replace='datetime.datetime.fromtimestamp(tz=datetime.UTC)'),
104+
_deprecate('calendar.January', '3.12'),
105+
_deprecate('calendar.February', '3.12'),
106+
_deprecate('sys.last_value', '3.12'),
107+
_deprecate('sys.last_traceback', '3.12'),
108+
_deprecate('sys.last_exc', '3.12'),
109+
_deprecate('xml.etree.ElementTree.__bool__', '3.12'),
110+
111+
# Python 3.13
112+
_deprecate('ctypes.SetPointerType', '3.13', remove='3.15'),
113+
_deprecate('ctypes.ARRAY', '3.13', remove='3.15'),
114+
_deprecate('wave.Wave_read.getmark', '3.13', remove='3.15'),
115+
_deprecate('wave.Wave_read.getmarkers', '3.13', remove='3.15'),
116+
_deprecate('wave.Wave_read.setmark', '3.13', remove='3.15'),
117+
118+
119+
# C API: Python 3.10
120+
for name in (
121+
'PyUnicode_AS_DATA',
122+
'PyUnicode_AS_UNICODE',
123+
'PyUnicode_AsUnicodeAndSize',
124+
'PyUnicode_AsUnicode',
125+
'PyUnicode_FromUnicode',
126+
'PyUnicode_GET_DATA_SIZE',
127+
'PyUnicode_GET_SIZE',
128+
'PyUnicode_GetSize',
129+
'PyUnicode_IS_COMPACT',
130+
'PyUnicode_IS_READY',
131+
'PyUnicode_READY',
132+
'Py_UNICODE_WSTR_LENGTH',
133+
'_PyUnicode_AsUnicode',
134+
'PyUnicode_WCHAR_KIND',
135+
'PyUnicodeObject',
136+
'PyUnicode_InternImmortal',
137+
):
138+
_deprecate_capi(name, '3.10', remove='3.12')
139+
140+
# C API: Python 3.12
141+
_deprecate_capi('PyDictObject.ma_version_tag', '3.12', remove='3.14'),
142+
for name, replace in (
143+
('Py_DebugFlag', 'PyConfig.parser_debug'),
144+
('Py_VerboseFlag', 'PyConfig.verbose'),
145+
('Py_QuietFlag', 'PyConfig.quiet'),
146+
('Py_InteractiveFlag', 'PyConfig.interactive'),
147+
('Py_InspectFlag', 'PyConfig.inspect'),
148+
('Py_OptimizeFlag', 'PyConfig.optimization_level'),
149+
('Py_NoSiteFlag', 'PyConfig.site_import'),
150+
('Py_BytesWarningFlag', 'PyConfig.bytes_warning'),
151+
('Py_FrozenFlag', 'PyConfig.pathconfig_warnings'),
152+
('Py_IgnoreEnvironmentFlag', 'PyConfig.use_environment'),
153+
('Py_DontWriteBytecodeFlag', 'PyConfig.write_bytecode'),
154+
('Py_NoUserSiteDirectory', 'PyConfig.user_site_directory'),
155+
('Py_UnbufferedStdioFlag', 'PyConfig.buffered_stdio'),
156+
('Py_HashRandomizationFlag', 'PyConfig.hash_seed'),
157+
('Py_IsolatedFlag', 'PyConfig.isolated'),
158+
('Py_LegacyWindowsFSEncodingFlag', 'PyPreConfig.legacy_windows_fs_encoding'),
159+
('Py_LegacyWindowsStdioFlag', 'PyConfig.legacy_windows_stdio'),
160+
('Py_FileSystemDefaultEncoding', 'PyConfig.filesystem_encoding'),
161+
('Py_FileSystemDefaultEncodeErrors', 'PyConfig.filesystem_errors'),
162+
('Py_UTF8Mode', 'PyPreConfig.utf8_mode'),
163+
):
164+
_deprecate_capi(name, '3.12', replace=replace)
165+
166+
_deprecate_capi('PyErr_Display', '3.12', replace='PyErr_DisplayException()'),
167+
_deprecate_capi('_PyErr_ChainExceptions', '3.12', replace='_PyErr_ChainExceptions1()'),
168+
169+
170+
def get_deprecated(name):
171+
try:
172+
return _DEPRECATIONS[name]
173+
except KeyError:
174+
pass
175+
176+
parts = name.split('.')
177+
if len(parts) == 1:
178+
return False
179+
180+
module_name = parts[0]
181+
return _DEPRECATIONS.get(module_name)
182+
183+
184+
def get_capi_deprecated(name):
185+
return _DEPRECATIONS_CAPI.get(name)

Lib/test/test_stdlib_deprecations.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
import stdlib_deprecations
3+
4+
5+
class Tests(unittest.TestCase):
6+
def test_python_api(self):
7+
obj = stdlib_deprecations.get_deprecated('asyncore')
8+
self.assertEqual(obj.name, 'asyncore')
9+
self.assertEqual(obj.version, (3, 6))
10+
self.assertEqual(obj.remove, (3, 12))
11+
self.assertEqual(obj.message, 'use asyncio')
12+
13+
self.assertIs(stdlib_deprecations.get_deprecated('asyncore.loop'),
14+
stdlib_deprecations.get_deprecated('asyncore'))
15+
16+
self.assertIsNone(stdlib_deprecations.get_deprecated('builtins.open'))
17+
18+
def test_c_api(self):
19+
obj = stdlib_deprecations.get_capi_deprecated('Py_VerboseFlag')
20+
self.assertEqual(obj.name, 'Py_VerboseFlag')
21+
self.assertEqual(obj.version, (3, 12))
22+
self.assertIsNone(obj.remove)
23+
self.assertEqual(obj.message, 'use PyConfig.verbose')
24+
25+
self.assertIsNotNone(stdlib_deprecations.get_capi_deprecated('Py_VerboseFlag'))
26+
self.assertIsNotNone(stdlib_deprecations.get_capi_deprecated('PyUnicode_InternImmortal'))
27+
28+
self.assertIsNone(stdlib_deprecations.get_capi_deprecated('Py_Initialize'))
29+
30+
31+
32+
if __name__ == '__main__':
33+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add :mod:`stdlib_deprecations` module. Patch by Victor Stinner.

0 commit comments

Comments
 (0)