mirror of https://github.com/python/cpython
gh-104003: Implement PEP 702 (#104004)
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
4038869423
commit
d4a6229afe
|
@ -522,6 +522,56 @@ Available Functions
|
|||
and calls to :func:`simplefilter`.
|
||||
|
||||
|
||||
.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1)
|
||||
|
||||
Decorator to indicate that a class, function or overload is deprecated.
|
||||
|
||||
When this decorator is applied to an object,
|
||||
deprecation warnings may be emitted at runtime when the object is used.
|
||||
:term:`static type checkers <static type checker>`
|
||||
will also generate a diagnostic on usage of the deprecated object.
|
||||
|
||||
Usage::
|
||||
|
||||
from warnings import deprecated
|
||||
from typing import overload
|
||||
|
||||
@deprecated("Use B instead")
|
||||
class A:
|
||||
pass
|
||||
|
||||
@deprecated("Use g instead")
|
||||
def f():
|
||||
pass
|
||||
|
||||
@overload
|
||||
@deprecated("int support is deprecated")
|
||||
def g(x: int) -> int: ...
|
||||
@overload
|
||||
def g(x: str) -> int: ...
|
||||
|
||||
The warning specified by *category* will be emitted at runtime
|
||||
on use of deprecated objects. For functions, that happens on calls;
|
||||
for classes, on instantiation and on creation of subclasses.
|
||||
If the *category* is ``None``, no warning is emitted at runtime.
|
||||
The *stacklevel* determines where the
|
||||
warning is emitted. If it is ``1`` (the default), the warning
|
||||
is emitted at the direct caller of the deprecated object; if it
|
||||
is higher, it is emitted further up the stack.
|
||||
Static type checker behavior is not affected by the *category*
|
||||
and *stacklevel* arguments.
|
||||
|
||||
The deprecation message passed to the decorator is saved in the
|
||||
``__deprecated__`` attribute on the decorated object.
|
||||
If applied to an overload, the decorator
|
||||
must be after the :func:`@overload <typing.overload>` decorator
|
||||
for the attribute to exist on the overload as returned by
|
||||
:func:`typing.get_overloads`.
|
||||
|
||||
.. versionadded:: 3.13
|
||||
See :pep:`702`.
|
||||
|
||||
|
||||
Available Context Managers
|
||||
--------------------------
|
||||
|
||||
|
|
|
@ -348,6 +348,15 @@ venv
|
|||
(using ``--without-scm-ignore-files``). (Contributed by Brett Cannon in
|
||||
:gh:`108125`.)
|
||||
|
||||
warnings
|
||||
--------
|
||||
|
||||
* The new :func:`warnings.deprecated` decorator provides a way to communicate
|
||||
deprecations to :term:`static type checkers <static type checker>` and
|
||||
to warn on usage of deprecated classes and functions. A runtime deprecation
|
||||
warning may also be emitted when a decorated function or class is used at runtime.
|
||||
See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.)
|
||||
|
||||
Optimizations
|
||||
=============
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ from io import StringIO
|
|||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
from typing import overload, get_overloads
|
||||
import unittest
|
||||
from test import support
|
||||
from test.support import import_helper
|
||||
|
@ -16,6 +18,7 @@ from test.test_warnings.data import package_helper
|
|||
from test.test_warnings.data import stacklevel as warning_tests
|
||||
|
||||
import warnings as original_warnings
|
||||
from warnings import deprecated
|
||||
|
||||
|
||||
py_warnings = import_helper.import_fresh_module('warnings',
|
||||
|
@ -90,7 +93,7 @@ class PublicAPITests(BaseTest):
|
|||
self.assertTrue(hasattr(self.module, '__all__'))
|
||||
target_api = ["warn", "warn_explicit", "showwarning",
|
||||
"formatwarning", "filterwarnings", "simplefilter",
|
||||
"resetwarnings", "catch_warnings"]
|
||||
"resetwarnings", "catch_warnings", "deprecated"]
|
||||
self.assertSetEqual(set(self.module.__all__),
|
||||
set(target_api))
|
||||
|
||||
|
@ -1377,6 +1380,283 @@ a=A()
|
|||
self.assertTrue(err.startswith(expected), ascii(err))
|
||||
|
||||
|
||||
class DeprecatedTests(unittest.TestCase):
|
||||
def test_dunder_deprecated(self):
|
||||
@deprecated("A will go away soon")
|
||||
class A:
|
||||
pass
|
||||
|
||||
self.assertEqual(A.__deprecated__, "A will go away soon")
|
||||
self.assertIsInstance(A, type)
|
||||
|
||||
@deprecated("b will go away soon")
|
||||
def b():
|
||||
pass
|
||||
|
||||
self.assertEqual(b.__deprecated__, "b will go away soon")
|
||||
self.assertIsInstance(b, types.FunctionType)
|
||||
|
||||
@overload
|
||||
@deprecated("no more ints")
|
||||
def h(x: int) -> int: ...
|
||||
@overload
|
||||
def h(x: str) -> str: ...
|
||||
def h(x):
|
||||
return x
|
||||
|
||||
overloads = get_overloads(h)
|
||||
self.assertEqual(len(overloads), 2)
|
||||
self.assertEqual(overloads[0].__deprecated__, "no more ints")
|
||||
|
||||
def test_class(self):
|
||||
@deprecated("A will go away soon")
|
||||
class A:
|
||||
pass
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"):
|
||||
A()
|
||||
with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"):
|
||||
with self.assertRaises(TypeError):
|
||||
A(42)
|
||||
|
||||
def test_class_with_init(self):
|
||||
@deprecated("HasInit will go away soon")
|
||||
class HasInit:
|
||||
def __init__(self, x):
|
||||
self.x = x
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "HasInit will go away soon"):
|
||||
instance = HasInit(42)
|
||||
self.assertEqual(instance.x, 42)
|
||||
|
||||
def test_class_with_new(self):
|
||||
has_new_called = False
|
||||
|
||||
@deprecated("HasNew will go away soon")
|
||||
class HasNew:
|
||||
def __new__(cls, x):
|
||||
nonlocal has_new_called
|
||||
has_new_called = True
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, x) -> None:
|
||||
self.x = x
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "HasNew will go away soon"):
|
||||
instance = HasNew(42)
|
||||
self.assertEqual(instance.x, 42)
|
||||
self.assertTrue(has_new_called)
|
||||
|
||||
def test_class_with_inherited_new(self):
|
||||
new_base_called = False
|
||||
|
||||
class NewBase:
|
||||
def __new__(cls, x):
|
||||
nonlocal new_base_called
|
||||
new_base_called = True
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, x) -> None:
|
||||
self.x = x
|
||||
|
||||
@deprecated("HasInheritedNew will go away soon")
|
||||
class HasInheritedNew(NewBase):
|
||||
pass
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "HasInheritedNew will go away soon"):
|
||||
instance = HasInheritedNew(42)
|
||||
self.assertEqual(instance.x, 42)
|
||||
self.assertTrue(new_base_called)
|
||||
|
||||
def test_class_with_new_but_no_init(self):
|
||||
new_called = False
|
||||
|
||||
@deprecated("HasNewNoInit will go away soon")
|
||||
class HasNewNoInit:
|
||||
def __new__(cls, x):
|
||||
nonlocal new_called
|
||||
new_called = True
|
||||
obj = super().__new__(cls)
|
||||
obj.x = x
|
||||
return obj
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "HasNewNoInit will go away soon"):
|
||||
instance = HasNewNoInit(42)
|
||||
self.assertEqual(instance.x, 42)
|
||||
self.assertTrue(new_called)
|
||||
|
||||
def test_mixin_class(self):
|
||||
@deprecated("Mixin will go away soon")
|
||||
class Mixin:
|
||||
pass
|
||||
|
||||
class Base:
|
||||
def __init__(self, a) -> None:
|
||||
self.a = a
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
|
||||
class Child(Base, Mixin):
|
||||
pass
|
||||
|
||||
instance = Child(42)
|
||||
self.assertEqual(instance.a, 42)
|
||||
|
||||
def test_existing_init_subclass(self):
|
||||
@deprecated("C will go away soon")
|
||||
class C:
|
||||
def __init_subclass__(cls) -> None:
|
||||
cls.inited = True
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
|
||||
C()
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
|
||||
class D(C):
|
||||
pass
|
||||
|
||||
self.assertTrue(D.inited)
|
||||
self.assertIsInstance(D(), D) # no deprecation
|
||||
|
||||
def test_existing_init_subclass_in_base(self):
|
||||
class Base:
|
||||
def __init_subclass__(cls, x) -> None:
|
||||
cls.inited = x
|
||||
|
||||
@deprecated("C will go away soon")
|
||||
class C(Base, x=42):
|
||||
pass
|
||||
|
||||
self.assertEqual(C.inited, 42)
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
|
||||
C()
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
|
||||
class D(C, x=3):
|
||||
pass
|
||||
|
||||
self.assertEqual(D.inited, 3)
|
||||
|
||||
def test_init_subclass_has_correct_cls(self):
|
||||
init_subclass_saw = None
|
||||
|
||||
@deprecated("Base will go away soon")
|
||||
class Base:
|
||||
def __init_subclass__(cls) -> None:
|
||||
nonlocal init_subclass_saw
|
||||
init_subclass_saw = cls
|
||||
|
||||
self.assertIsNone(init_subclass_saw)
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
|
||||
class C(Base):
|
||||
pass
|
||||
|
||||
self.assertIs(init_subclass_saw, C)
|
||||
|
||||
def test_init_subclass_with_explicit_classmethod(self):
|
||||
init_subclass_saw = None
|
||||
|
||||
@deprecated("Base will go away soon")
|
||||
class Base:
|
||||
@classmethod
|
||||
def __init_subclass__(cls) -> None:
|
||||
nonlocal init_subclass_saw
|
||||
init_subclass_saw = cls
|
||||
|
||||
self.assertIsNone(init_subclass_saw)
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
|
||||
class C(Base):
|
||||
pass
|
||||
|
||||
self.assertIs(init_subclass_saw, C)
|
||||
|
||||
def test_function(self):
|
||||
@deprecated("b will go away soon")
|
||||
def b():
|
||||
pass
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "b will go away soon"):
|
||||
b()
|
||||
|
||||
def test_method(self):
|
||||
class Capybara:
|
||||
@deprecated("x will go away soon")
|
||||
def x(self):
|
||||
pass
|
||||
|
||||
instance = Capybara()
|
||||
with self.assertWarnsRegex(DeprecationWarning, "x will go away soon"):
|
||||
instance.x()
|
||||
|
||||
def test_property(self):
|
||||
class Capybara:
|
||||
@property
|
||||
@deprecated("x will go away soon")
|
||||
def x(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def no_more_setting(self):
|
||||
return 42
|
||||
|
||||
@no_more_setting.setter
|
||||
@deprecated("no more setting")
|
||||
def no_more_setting(self, value):
|
||||
pass
|
||||
|
||||
instance = Capybara()
|
||||
with self.assertWarnsRegex(DeprecationWarning, "x will go away soon"):
|
||||
instance.x
|
||||
|
||||
with py_warnings.catch_warnings():
|
||||
py_warnings.simplefilter("error")
|
||||
self.assertEqual(instance.no_more_setting, 42)
|
||||
|
||||
with self.assertWarnsRegex(DeprecationWarning, "no more setting"):
|
||||
instance.no_more_setting = 42
|
||||
|
||||
def test_category(self):
|
||||
@deprecated("c will go away soon", category=RuntimeWarning)
|
||||
def c():
|
||||
pass
|
||||
|
||||
with self.assertWarnsRegex(RuntimeWarning, "c will go away soon"):
|
||||
c()
|
||||
|
||||
def test_turn_off_warnings(self):
|
||||
@deprecated("d will go away soon", category=None)
|
||||
def d():
|
||||
pass
|
||||
|
||||
with py_warnings.catch_warnings():
|
||||
py_warnings.simplefilter("error")
|
||||
d()
|
||||
|
||||
def test_only_strings_allowed(self):
|
||||
with self.assertRaisesRegex(
|
||||
TypeError,
|
||||
"Expected an object of type str for 'message', not 'type'"
|
||||
):
|
||||
@deprecated
|
||||
class Foo: ...
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
TypeError,
|
||||
"Expected an object of type str for 'message', not 'function'"
|
||||
):
|
||||
@deprecated
|
||||
def foo(): ...
|
||||
|
||||
def test_no_retained_references_to_wrapper_instance(self):
|
||||
@deprecated('depr')
|
||||
def d(): pass
|
||||
|
||||
self.assertFalse(any(
|
||||
isinstance(cell.cell_contents, deprecated) for cell in d.__closure__
|
||||
))
|
||||
|
||||
def setUpModule():
|
||||
py_warnings.onceregistry.clear()
|
||||
c_warnings.onceregistry.clear()
|
||||
|
|
131
Lib/warnings.py
131
Lib/warnings.py
|
@ -5,7 +5,7 @@ import sys
|
|||
|
||||
__all__ = ["warn", "warn_explicit", "showwarning",
|
||||
"formatwarning", "filterwarnings", "simplefilter",
|
||||
"resetwarnings", "catch_warnings"]
|
||||
"resetwarnings", "catch_warnings", "deprecated"]
|
||||
|
||||
def showwarning(message, category, filename, lineno, file=None, line=None):
|
||||
"""Hook to write a warning to a file; replace if you like."""
|
||||
|
@ -508,6 +508,135 @@ class catch_warnings(object):
|
|||
self._module._showwarnmsg_impl = self._showwarnmsg_impl
|
||||
|
||||
|
||||
class deprecated:
|
||||
"""Indicate that a class, function or overload is deprecated.
|
||||
|
||||
When this decorator is applied to an object, the type checker
|
||||
will generate a diagnostic on usage of the deprecated object.
|
||||
|
||||
Usage:
|
||||
|
||||
@deprecated("Use B instead")
|
||||
class A:
|
||||
pass
|
||||
|
||||
@deprecated("Use g instead")
|
||||
def f():
|
||||
pass
|
||||
|
||||
@overload
|
||||
@deprecated("int support is deprecated")
|
||||
def g(x: int) -> int: ...
|
||||
@overload
|
||||
def g(x: str) -> int: ...
|
||||
|
||||
The warning specified by *category* will be emitted at runtime
|
||||
on use of deprecated objects. For functions, that happens on calls;
|
||||
for classes, on instantiation and on creation of subclasses.
|
||||
If the *category* is ``None``, no warning is emitted at runtime.
|
||||
The *stacklevel* determines where the
|
||||
warning is emitted. If it is ``1`` (the default), the warning
|
||||
is emitted at the direct caller of the deprecated object; if it
|
||||
is higher, it is emitted further up the stack.
|
||||
Static type checker behavior is not affected by the *category*
|
||||
and *stacklevel* arguments.
|
||||
|
||||
The deprecation message passed to the decorator is saved in the
|
||||
``__deprecated__`` attribute on the decorated object.
|
||||
If applied to an overload, the decorator
|
||||
must be after the ``@overload`` decorator for the attribute to
|
||||
exist on the overload as returned by ``get_overloads()``.
|
||||
|
||||
See PEP 702 for details.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
/,
|
||||
*,
|
||||
category: type[Warning] | None = DeprecationWarning,
|
||||
stacklevel: int = 1,
|
||||
) -> None:
|
||||
if not isinstance(message, str):
|
||||
raise TypeError(
|
||||
f"Expected an object of type str for 'message', not {type(message).__name__!r}"
|
||||
)
|
||||
self.message = message
|
||||
self.category = category
|
||||
self.stacklevel = stacklevel
|
||||
|
||||
def __call__(self, arg, /):
|
||||
# Make sure the inner functions created below don't
|
||||
# retain a reference to self.
|
||||
msg = self.message
|
||||
category = self.category
|
||||
stacklevel = self.stacklevel
|
||||
if category is None:
|
||||
arg.__deprecated__ = msg
|
||||
return arg
|
||||
elif isinstance(arg, type):
|
||||
import functools
|
||||
from types import MethodType
|
||||
|
||||
original_new = arg.__new__
|
||||
|
||||
@functools.wraps(original_new)
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is arg:
|
||||
warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
if original_new is not object.__new__:
|
||||
return original_new(cls, *args, **kwargs)
|
||||
# Mirrors a similar check in object.__new__.
|
||||
elif cls.__init__ is object.__init__ and (args or kwargs):
|
||||
raise TypeError(f"{cls.__name__}() takes no arguments")
|
||||
else:
|
||||
return original_new(cls)
|
||||
|
||||
arg.__new__ = staticmethod(__new__)
|
||||
|
||||
original_init_subclass = arg.__init_subclass__
|
||||
# We need slightly different behavior if __init_subclass__
|
||||
# is a bound method (likely if it was implemented in Python)
|
||||
if isinstance(original_init_subclass, MethodType):
|
||||
original_init_subclass = original_init_subclass.__func__
|
||||
|
||||
@functools.wraps(original_init_subclass)
|
||||
def __init_subclass__(*args, **kwargs):
|
||||
warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return original_init_subclass(*args, **kwargs)
|
||||
|
||||
arg.__init_subclass__ = classmethod(__init_subclass__)
|
||||
# Or otherwise, which likely means it's a builtin such as
|
||||
# object's implementation of __init_subclass__.
|
||||
else:
|
||||
@functools.wraps(original_init_subclass)
|
||||
def __init_subclass__(*args, **kwargs):
|
||||
warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return original_init_subclass(*args, **kwargs)
|
||||
|
||||
arg.__init_subclass__ = __init_subclass__
|
||||
|
||||
arg.__deprecated__ = __new__.__deprecated__ = msg
|
||||
__init_subclass__.__deprecated__ = msg
|
||||
return arg
|
||||
elif callable(arg):
|
||||
import functools
|
||||
|
||||
@functools.wraps(arg)
|
||||
def wrapper(*args, **kwargs):
|
||||
warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return arg(*args, **kwargs)
|
||||
|
||||
arg.__deprecated__ = wrapper.__deprecated__ = msg
|
||||
return wrapper
|
||||
else:
|
||||
raise TypeError(
|
||||
"@deprecated decorator with non-None category must be applied to "
|
||||
f"a class or callable, not {arg!r}"
|
||||
)
|
||||
|
||||
|
||||
_DEPRECATED_MSG = "{name!r} is deprecated and slated for removal in Python {remove}"
|
||||
|
||||
def _deprecated(name, message=_DEPRECATED_MSG, *, remove, _version=sys.version_info):
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
Add :func:`warnings.deprecated`, a decorator to mark deprecated functions to
|
||||
static type checkers and to warn on usage of deprecated classes and functions.
|
||||
See :pep:`702`. Patch by Jelle Zijlstra.
|
Loading…
Reference in New Issue