summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/warnings.rst50
-rw-r--r--Doc/whatsnew/3.13.rst9
-rw-r--r--Lib/test/test_warnings/__init__.py282
-rw-r--r--Lib/warnings.py131
-rw-r--r--Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst3
5 files changed, 473 insertions, 2 deletions
diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst
index 884de08..a9c4697 100644
--- a/Doc/library/warnings.rst
+++ b/Doc/library/warnings.rst
@@ -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
--------------------------
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 198ea3a..372e4a4 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -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
=============
diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py
index 2c52323..cd989fe 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -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()
diff --git a/Lib/warnings.py b/Lib/warnings.py
index 32e5807..924f872 100644
--- a/Lib/warnings.py
+++ b/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):
diff --git a/Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst b/Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst
new file mode 100644
index 0000000..82d61ca
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst
@@ -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.