From 93e11d6a05a3eea8a038e1f69c391f1e74143a98 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Apr 2023 20:45:17 -0700 Subject: [PATCH 01/20] gh-104003: Implement PEP 702 --- Lib/test/test_typing.py | 148 +++++++++++++++++++++++++++++++++++++++- Lib/typing.py | 77 +++++++++++++++++++++ 2 files changed, 224 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index f36bb958c88ef9..22c7e73e160b29 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -23,7 +23,7 @@ from typing import assert_type, cast, runtime_checkable from typing import get_type_hints from typing import get_origin, get_args -from typing import override +from typing import override, deprecated from typing import is_typeddict from typing import reveal_type from typing import dataclass_transform @@ -4699,6 +4699,152 @@ def on_bottom(self, a: int) -> int: self.assertTrue(instance.on_bottom.__override__) +class DeprecatedTests(BaseTestCase): + 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.assertRaises(TypeError), self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): + A(42) + + @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) + + 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) + 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_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 warnings.catch_warnings(): + 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 warnings.catch_warnings(): + warnings.simplefilter("error") + d() + + class CastTests(BaseTestCase): def test_basics(self): diff --git a/Lib/typing.py b/Lib/typing.py index 354bc80eb3abfa..8f4bbd6eecea67 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -124,6 +124,7 @@ def _idfunc(_, x): 'cast', 'clear_overloads', 'dataclass_transform', + 'deprecated', 'final', 'get_args', 'get_origin', @@ -3551,3 +3552,79 @@ def method(self) -> None: # read-only property, TypeError if it's a builtin class. pass return method + + +def deprecated( + msg: str, + /, + *, + category: type[Warning] | None = DeprecationWarning, + stacklevel: int = 1, +) -> Callable[[T], T]: + """Indicate that a class, function or overload is deprecated. + + 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: ... + + When this decorator is applied to an object, the type checker + will generate a diagnostic on usage of the deprecated object. + + No runtime warning is issued. The decorator sets the ``__deprecated__`` + attribute on the decorated object to the deprecation message + passed to the decorator. 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 decorator(arg: T, /) -> T: + if category is None: + arg.__deprecated__ = msg + return arg + elif isinstance(arg, type): + original_new = arg.__new__ + has_init = arg.__init__ is not object.__init__ + + @functools.wraps(original_new) + def __new__(cls, *args, **kwargs): + warnings.warn(msg, category=category, stacklevel=stacklevel + 1) + # Mirrors a similar check in object.__new__. + if not has_init and (args or kwargs): + raise TypeError(f"{cls.__name__}() takes no arguments") + if original_new is not object.__new__: + return original_new(cls, *args, **kwargs) + else: + return original_new(cls) + + arg.__new__ = staticmethod(__new__) + arg.__deprecated__ = __new__.__deprecated__ = msg + return arg + elif callable(arg): + @functools.wraps(arg) + def wrapper(*args, **kwargs): + warnings.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}" + ) + + return decorator From d8d315770a924eeb583b23b9970fdd9500123a55 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 29 Apr 2023 20:49:20 -0700 Subject: [PATCH 02/20] blurb --- .../next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst 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 00000000000000..c61aa45533d68a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst @@ -0,0 +1,2 @@ +Add :func:`typing.deprecated`, a decorator to mark deprecated functions to +static type checkers. See :pep:`702`. Patch by Jelle Zijlstra. From 342f28b6ef83fcf2aa60fdd11f74df914d3ae0bf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 30 Apr 2023 08:16:54 -0700 Subject: [PATCH 03/20] Add more tests --- Lib/test/test_typing.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 22c7e73e160b29..ca328e4720030f 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -4726,8 +4726,9 @@ def h(x): overloads = get_overloads(h) self.assertEqual(len(overloads), 2) self.assertEqual(overloads[0].__deprecated__, "no more ints") + self.assertFalse(hasattr(overloads[1], "__deprecated__")) - def test_class(self): + def test_simple_class(self): @deprecated("A will go away soon") class A: pass @@ -4737,6 +4738,7 @@ class A: with self.assertRaises(TypeError), self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): A(42) + def test_class_with_init(self): @deprecated("HasInit will go away soon") class HasInit: def __init__(self, x): @@ -4746,6 +4748,7 @@ def __init__(self, x): instance = HasInit(42) self.assertEqual(instance.x, 42) + def test_class_with_new(self): has_new_called = False @deprecated("HasNew will go away soon") @@ -4762,6 +4765,8 @@ def __init__(self, x) -> None: instance = HasNew(42) self.assertEqual(instance.x, 42) self.assertTrue(has_new_called) + + def test_inherit_from_class_with_new(self): new_base_called = False class NewBase: @@ -4782,6 +4787,35 @@ class HasInheritedNew(NewBase): self.assertEqual(instance.x, 42) self.assertTrue(new_base_called) + def test_deprecated_base(self): + @deprecated("Base will go away soon") + class Base: + pass + + class Child(Base): + pass + + with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): + Child() + + def test_decorate_init(self): + class A: + @deprecated("cannot make any more As") + def __init__(self): + pass + + with self.assertWarnsRegex(DeprecationWarning, "cannot make any more As"): + A() + + def test_decorate_new(self): + class A: + @deprecated("cannot make any more As") + def __new__(cls): + return super().__new__(cls) + + with self.assertWarnsRegex(DeprecationWarning, "cannot make any more As"): + A() + def test_function(self): @deprecated("b will go away soon") def b(): From 9b167e876673c2d0f99a0154a1f7f1a142153cff Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 6 May 2023 08:30:03 -0700 Subject: [PATCH 04/20] describe runtime behavior --- Lib/typing.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/typing.py b/Lib/typing.py index 8f4bbd6eecea67..2c2a0e68673473 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3582,7 +3582,17 @@ def g(x: str) -> int: ... When this decorator is applied to an object, the type checker will generate a diagnostic on usage of the deprecated object. - No runtime warning is issued. The decorator sets the ``__deprecated__`` + If the deprecated object is a type, a warning will be + issued on instantiation, and if it is a callable, a warning + will be issued on calls. By default, the warning is a + ``DeprecationWarning``, but the *category* argument + can be used to issue a different warning class. If *category* + is ``None``, no warning is issued. + If *stacklevel* is 1 (the default), the warning is issued at + the immediate call site. To issue the warning higher in the stack, + *stacklevel* can be set to a different value. + + The decorator sets the ``__deprecated__`` attribute on the decorated object to the deprecation message passed to the decorator. If applied to an overload, the decorator must be after the ``@overload`` decorator for the attribute to From 0ed9d4157620134540ff711a5e62741abffb6b19 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 6 May 2023 08:33:07 -0700 Subject: [PATCH 05/20] steal the typing-extensions wording instead, it's better --- Lib/typing.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Lib/typing.py b/Lib/typing.py index 2c2a0e68673473..347e26feedf1b4 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3582,15 +3582,13 @@ def g(x: str) -> int: ... When this decorator is applied to an object, the type checker will generate a diagnostic on usage of the deprecated object. - If the deprecated object is a type, a warning will be - issued on instantiation, and if it is a callable, a warning - will be issued on calls. By default, the warning is a - ``DeprecationWarning``, but the *category* argument - can be used to issue a different warning class. If *category* - is ``None``, no warning is issued. - If *stacklevel* is 1 (the default), the warning is issued at - the immediate call site. To issue the warning higher in the stack, - *stacklevel* can be set to a different value. + At runtime, the warning specified by ``category`` will be emitted on use + of deprecated objects. For functions, that happens on calls; + for classes, on instantiation. If the ``category`` is ``None``, + no warning is emitted. 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. The decorator sets the ``__deprecated__`` attribute on the decorated object to the deprecation message From 815591bf3631b5324a667fab9cb57800213d1c9f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 19:11:13 -0800 Subject: [PATCH 06/20] Put it in warnings, copy over from typing-extensions --- Lib/test/test_typing.py | 180 -------------------- Lib/test/test_warnings/__init__.py | 258 +++++++++++++++++++++++++++++ Lib/typing.py | 82 --------- Lib/warnings.py | 115 +++++++++++++ 4 files changed, 373 insertions(+), 262 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 2d7c672e617133..6ff79e8eeed9aa 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -5390,186 +5390,6 @@ def on_bottom(self, a: int) -> int: self.assertTrue(instance.on_bottom.__override__) -class DeprecatedTests(BaseTestCase): - 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") - self.assertFalse(hasattr(overloads[1], "__deprecated__")) - - def test_simple_class(self): - @deprecated("A will go away soon") - class A: - pass - - with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): - A() - with self.assertRaises(TypeError), self.assertWarnsRegex(DeprecationWarning, "A will go away soon"): - 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_inherit_from_class_with_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_deprecated_base(self): - @deprecated("Base will go away soon") - class Base: - pass - - class Child(Base): - pass - - with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"): - Child() - - def test_decorate_init(self): - class A: - @deprecated("cannot make any more As") - def __init__(self): - pass - - with self.assertWarnsRegex(DeprecationWarning, "cannot make any more As"): - A() - - def test_decorate_new(self): - class A: - @deprecated("cannot make any more As") - def __new__(cls): - return super().__new__(cls) - - with self.assertWarnsRegex(DeprecationWarning, "cannot make any more As"): - A() - - 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 warnings.catch_warnings(): - 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 warnings.catch_warnings(): - warnings.simplefilter("error") - d() - - class CastTests(BaseTestCase): def test_basics(self): diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 2c523230e7e97f..5da91d0fe2e79d 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -5,6 +5,8 @@ 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 stacklevel as warning_tests import warnings as original_warnings +from warnings import deprecated py_warnings = import_helper.import_fresh_module('warnings', @@ -1377,6 +1380,261 @@ def test_late_resource_warning(self): 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 setUpModule(): py_warnings.onceregistry.clear() c_warnings.onceregistry.clear() diff --git a/Lib/typing.py b/Lib/typing.py index ce74a118b2c0ce..9f506030c594b4 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -3375,88 +3375,6 @@ def method(self) -> None: return method -def deprecated( - msg: str, - /, - *, - category: type[Warning] | None = DeprecationWarning, - stacklevel: int = 1, -) -> Callable[[T], T]: - """Indicate that a class, function or overload is deprecated. - - 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: ... - - When this decorator is applied to an object, the type checker - will generate a diagnostic on usage of the deprecated object. - - At runtime, the warning specified by ``category`` will be emitted on use - of deprecated objects. For functions, that happens on calls; - for classes, on instantiation. If the ``category`` is ``None``, - no warning is emitted. 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. - - The decorator sets the ``__deprecated__`` - attribute on the decorated object to the deprecation message - passed to the decorator. 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 decorator(arg: T, /) -> T: - if category is None: - arg.__deprecated__ = msg - return arg - elif isinstance(arg, type): - original_new = arg.__new__ - has_init = arg.__init__ is not object.__init__ - - @functools.wraps(original_new) - def __new__(cls, *args, **kwargs): - warnings.warn(msg, category=category, stacklevel=stacklevel + 1) - # Mirrors a similar check in object.__new__. - if not has_init and (args or kwargs): - raise TypeError(f"{cls.__name__}() takes no arguments") - if original_new is not object.__new__: - return original_new(cls, *args, **kwargs) - else: - return original_new(cls) - - arg.__new__ = staticmethod(__new__) - arg.__deprecated__ = __new__.__deprecated__ = msg - return arg - elif callable(arg): - @functools.wraps(arg) - def wrapper(*args, **kwargs): - warnings.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}" - ) - - return decorator def is_protocol(tp: type, /) -> bool: """Return True if the given type is a Protocol. diff --git a/Lib/warnings.py b/Lib/warnings.py index 32e58072b9cc33..ee356ce1beff53 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -508,6 +508,121 @@ def __exit__(self, *exc_info): self._module._showwarnmsg_impl = self._showwarnmsg_impl + +def deprecated( + msg: str, + /, + *, + category: type[Warning] | None = DeprecationWarning, + stacklevel: int = 1, +): + """Indicate that a class, function or overload is deprecated. + + 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: ... + + When this decorator is applied to an object, the type checker + will generate a diagnostic on usage of the deprecated object. + + The warning specified by ``category`` will be emitted on use + of deprecated objects. For functions, that happens on calls; + for classes, on instantiation. If the ``category`` is ``None``, + no warning is emitted. 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. + + The decorator sets the ``__deprecated__`` + attribute on the decorated object to the deprecation message + passed to the decorator. 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 decorator(arg, /): + 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 + # type'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}" + ) + + return decorator + + _DEPRECATED_MSG = "{name!r} is deprecated and slated for removal in Python {remove}" def _deprecated(name, message=_DEPRECATED_MSG, *, remove, _version=sys.version_info): From 4098e62cc428809aa893b7d0cc9f1673e585c356 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 19:33:09 -0800 Subject: [PATCH 07/20] docs --- Doc/glossary.rst | 9 +++- Doc/library/warnings.rst | 46 +++++++++++++++++++ Doc/whatsnew/3.13.rst | 7 +++ Lib/typing.py | 1 - Lib/warnings.py | 2 +- ...-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst | 2 +- 6 files changed, 62 insertions(+), 5 deletions(-) diff --git a/Doc/glossary.rst b/Doc/glossary.rst index 2d5412d6b43e8b..f93fc72d361d66 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -1138,6 +1138,11 @@ Glossary an :term:`expression` or one of several constructs with a keyword, such as :keyword:`if`, :keyword:`while` or :keyword:`for`. + static type checker + An external tool that reads Python code and analyzes it, looking for + issues such as incorrect types. See also :term:`type hints ` + and the :mod:`typing` module. + strong reference In Python's C API, a strong reference is a reference to an object which is owned by the code holding the reference. The strong @@ -1214,8 +1219,8 @@ Glossary attribute, or a function parameter or return value. Type hints are optional and are not enforced by Python but - they are useful to static type analysis tools, and aid IDEs with code - completion and refactoring. + they are useful to :term:`static type checkers `, + and aid IDEs with code completion and refactoring. Type hints of global variables, class attributes, and functions, but not local variables, can be accessed using diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 884de08eab1b16..2202a4d3057688 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -522,6 +522,52 @@ 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. + + 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: ... + + When this decorator is applied to an object, + :term:`static type checkers ` + will generate a diagnostic on usage of the deprecated object. + + The warning specified by ``category`` will be emitted on use + of deprecated objects. For functions, that happens on calls; + for classes, on instantiation. If the ``category`` is ``None``, + no warning is emitted. 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. + + The decorator sets the ``__deprecated__`` + attribute on the decorated object to the deprecation message + passed to the decorator. If applied to an overload, the decorator + must be after the :func:`@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 291e276dc67ce0..69223f7c456541 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -295,6 +295,13 @@ 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 `. See + :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.) + Optimizations ============= diff --git a/Lib/typing.py b/Lib/typing.py index 9f506030c594b4..14845b36028ca1 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -125,7 +125,6 @@ 'cast', 'clear_overloads', 'dataclass_transform', - 'deprecated', 'final', 'get_args', 'get_origin', diff --git a/Lib/warnings.py b/Lib/warnings.py index ee356ce1beff53..b50017ff9731d5 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -5,7 +5,7 @@ __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.""" 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 index c61aa45533d68a..bd637535f70099 100644 --- 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 @@ -1,2 +1,2 @@ -Add :func:`typing.deprecated`, a decorator to mark deprecated functions to +Add :func:`warnings.deprecated`, a decorator to mark deprecated functions to static type checkers. See :pep:`702`. Patch by Jelle Zijlstra. From d6625b985952d41afb766fef27caefa54eb3a66a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 7 Nov 2023 19:33:56 -0800 Subject: [PATCH 08/20] fix test --- Lib/test/test_warnings/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 5da91d0fe2e79d..a04759ee0678d0 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -93,7 +93,7 @@ def test_module_all_attribute(self): 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)) From 0870a16676a8aed20a9f1dc253a76c224d77f24a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 8 Nov 2023 06:27:25 -0800 Subject: [PATCH 09/20] Update Doc/library/warnings.rst Co-authored-by: Hugo van Kemenade --- Doc/library/warnings.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 2202a4d3057688..5a6ba5df00f735 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -549,10 +549,10 @@ Available Functions :term:`static type checkers ` will generate a diagnostic on usage of the deprecated object. - The warning specified by ``category`` will be emitted on use + The warning specified by *category* will be emitted on use of deprecated objects. For functions, that happens on calls; - for classes, on instantiation. If the ``category`` is ``None``, - no warning is emitted. The ``stacklevel`` determines where the + for classes, on instantiation. If the *category* is ``None``, + no warning is emitted. 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. From b9507bd262155af968e0fadeadd2a1dac81845c7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 8 Nov 2023 07:00:35 -0800 Subject: [PATCH 10/20] Code review --- Doc/library/warnings.rst | 17 +++++++++-------- Doc/whatsnew/3.13.rst | 5 +++-- Lib/warnings.py | 15 ++++++++------- ...23-04-29-20-49-13.gh-issue-104003.-8Ruk2.rst | 3 ++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 5a6ba5df00f735..bf7259c68d1947 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -526,6 +526,10 @@ Available Functions Decorator to indicate that a class, function or overload is deprecated. + When this decorator is applied to an object, + :term:`static type checkers ` + will generate a diagnostic on usage of the deprecated object. + Usage:: from warnings import deprecated @@ -545,21 +549,18 @@ Available Functions @overload def g(x: str) -> int: ... - When this decorator is applied to an object, - :term:`static type checkers ` - will generate a diagnostic on usage of the deprecated object. - The warning specified by *category* will be emitted on use of deprecated objects. For functions, that happens on calls; - for classes, on instantiation. If the *category* is ``None``, + for classes, on instantiation and on creation of subclasses. + If the *category* is ``None``, no warning is emitted. 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. - The decorator sets the ``__deprecated__`` - attribute on the decorated object to the deprecation message - passed to the decorator. If applied to an overload, the decorator + 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 ` decorator for the attribute to exist on the overload as returned by :func:`typing.get_overloads`. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 69223f7c456541..46af429b8e840c 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -299,8 +299,9 @@ warnings -------- * The new :func:`warnings.deprecated` decorator provides a way to communicate - deprecations to :term:`static type checkers `. See - :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.) + deprecations to :term:`static type checkers ` and + to warn on usage of deprecated classes and functions. + See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.) Optimizations ============= diff --git a/Lib/warnings.py b/Lib/warnings.py index b50017ff9731d5..36da0e75c63dc7 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -518,6 +518,9 @@ def 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") @@ -534,20 +537,18 @@ def g(x: int) -> int: ... @overload def g(x: str) -> int: ... - When this decorator is applied to an object, the type checker - will generate a diagnostic on usage of the deprecated object. - The warning specified by ``category`` will be emitted on use of deprecated objects. For functions, that happens on calls; - for classes, on instantiation. If the ``category`` is ``None``, + for classes, on instantiation and on creation of subclasses. + If the ``category`` is ``None``, no warning is emitted. 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. - The decorator sets the ``__deprecated__`` - attribute on the decorated object to the deprecation message - passed to the decorator. If applied to an overload, the decorator + 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()``. 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 index bd637535f70099..82d61ca8b8bc97 100644 --- 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 @@ -1,2 +1,3 @@ Add :func:`warnings.deprecated`, a decorator to mark deprecated functions to -static type checkers. See :pep:`702`. Patch by Jelle Zijlstra. +static type checkers and to warn on usage of deprecated classes and functions. +See :pep:`702`. Patch by Jelle Zijlstra. From ed9a10c32a9630930b11ae4ecc7014975dd5ed36 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 26 Nov 2023 17:25:19 -0800 Subject: [PATCH 11/20] make it a class --- Lib/warnings.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 36da0e75c63dc7..22a1b16907959f 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -509,13 +509,7 @@ def __exit__(self, *exc_info): -def deprecated( - msg: str, - /, - *, - category: type[Warning] | None = DeprecationWarning, - stacklevel: int = 1, -): +class deprecated: """Indicate that a class, function or overload is deprecated. When this decorator is applied to an object, the type checker @@ -555,8 +549,24 @@ def g(x: str) -> int: ... See PEP 702 for details. """ - - def decorator(arg, /): + def __init__( + self, + msg: str, + /, + *, + category: type[Warning] | None = DeprecationWarning, + stacklevel: int = 1, + ) -> None: + self.msg = msg + 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.msg + category = self.category + stacklevel = self.stacklevel if category is None: arg.__deprecated__ = msg return arg @@ -621,8 +631,6 @@ def wrapper(*args, **kwargs): f"a class or callable, not {arg!r}" ) - return decorator - _DEPRECATED_MSG = "{name!r} is deprecated and slated for removal in Python {remove}" From d528dc8c49677e8bff914b94813e8b32344ef560 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:37:54 -0800 Subject: [PATCH 12/20] Forward-port typing-extensions change --- Lib/test/test_warnings/__init__.py | 15 +++++++++++++++ Lib/warnings.py | 4 ++++ 2 files changed, 19 insertions(+) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index a04759ee0678d0..58afa59e76218a 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1634,6 +1634,21 @@ def d(): py_warnings.simplefilter("error") d() + def test_only_strings_allowed(self): + with self.assertRaisesRegex( + TypeError, + "Expected an object of type str for 'msg', not 'type'" + ): + @deprecated + class Foo: ... + + with self.assertRaisesRegex( + TypeError, + "Expected an object of type str for 'msg', not 'function'" + ): + @deprecated + def foo(): ... + def setUpModule(): py_warnings.onceregistry.clear() diff --git a/Lib/warnings.py b/Lib/warnings.py index 22a1b16907959f..ce592040a3cea6 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -557,6 +557,10 @@ def __init__( category: type[Warning] | None = DeprecationWarning, stacklevel: int = 1, ) -> None: + if not isinstance(msg, str): + raise TypeError( + f"Expected an object of type str for 'msg', not {type(msg).__name__!r}" + ) self.msg = msg self.category = category self.stacklevel = stacklevel From e3235028a6abc7ca2207ff07d728e37d67bbe229 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:38:37 -0800 Subject: [PATCH 13/20] Add test suggested by Alex --- Lib/test/test_warnings/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index 58afa59e76218a..f851e4e2d21089 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1649,6 +1649,13 @@ class Foo: ... @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() From acf49b3deac6deb04f40fdfc9b89cf7042f6f5e9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:41:24 -0800 Subject: [PATCH 14/20] Adjust docs --- Doc/library/warnings.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index bf7259c68d1947..1666e58aecec77 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -527,9 +527,9 @@ Available Functions 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 ` - will generate a diagnostic on usage of the deprecated object. - + will also generate a diagnostic on usage of the deprecated object. Usage:: from warnings import deprecated @@ -549,14 +549,16 @@ Available Functions @overload def g(x: str) -> int: ... - The warning specified by *category* will be emitted on use - of deprecated objects. For functions, that happens on calls; + 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. The *stacklevel* determines where the + 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. From 1d663efea0c50722e371907201a03b7395f1f4e0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:43:36 -0800 Subject: [PATCH 15/20] Add to Whats New --- Doc/whatsnew/3.13.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index d2e70eb936e52f..372e4a45468e68 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -353,7 +353,8 @@ warnings * The new :func:`warnings.deprecated` decorator provides a way to communicate deprecations to :term:`static type checkers ` and - to warn on usage of deprecated classes and functions. + 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 From b94255da139becbf8ec330af9144abc98b2204f5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:44:41 -0800 Subject: [PATCH 16/20] Now that this is semi-public, use a better name --- Lib/warnings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index ce592040a3cea6..65281ec5ffa7e5 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -551,24 +551,24 @@ def g(x: str) -> int: ... """ def __init__( self, - msg: str, + message: str, /, *, category: type[Warning] | None = DeprecationWarning, stacklevel: int = 1, ) -> None: - if not isinstance(msg, str): + if not isinstance(message, str): raise TypeError( - f"Expected an object of type str for 'msg', not {type(msg).__name__!r}" + f"Expected an object of type str for 'msg', not {type(message).__name__!r}" ) - self.msg = msg + 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.msg + msg = self.message category = self.category stacklevel = self.stacklevel if category is None: From e7a361b3b95b4d7ceabe244c5a375ff58fd626cb Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 18:45:50 -0800 Subject: [PATCH 17/20] sync docstring --- Lib/warnings.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 65281ec5ffa7e5..0def518b1ab1c0 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -531,14 +531,16 @@ def g(x: int) -> int: ... @overload def g(x: str) -> int: ... - The warning specified by ``category`` will be emitted on use - of deprecated objects. For functions, that happens on calls; + 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. The ``stacklevel`` determines where the + 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. From 08efe4dc8a40bb71ddba63e38bfd7c72747ea863 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 28 Nov 2023 20:11:24 -0800 Subject: [PATCH 18/20] Update error msg --- Lib/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 0def518b1ab1c0..33fbd908feaa31 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -561,7 +561,7 @@ def __init__( ) -> None: if not isinstance(message, str): raise TypeError( - f"Expected an object of type str for 'msg', not {type(message).__name__!r}" + f"Expected an object of type str for 'message', not {type(message).__name__!r}" ) self.message = message self.category = category From f4eb07533382e98e501a301bd0ec4bf6336ca73a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 29 Nov 2023 09:35:16 +0000 Subject: [PATCH 19/20] Apply suggestions from code review --- Doc/library/warnings.rst | 1 + Lib/test/test_warnings/__init__.py | 4 ++-- Lib/warnings.py | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 1666e58aecec77..a9c469707e8227 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -530,6 +530,7 @@ Available Functions deprecation warnings may be emitted at runtime when the object is used. :term:`static type checkers ` will also generate a diagnostic on usage of the deprecated object. + Usage:: from warnings import deprecated diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index f851e4e2d21089..cd989fe36bf26b 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1637,14 +1637,14 @@ def d(): def test_only_strings_allowed(self): with self.assertRaisesRegex( TypeError, - "Expected an object of type str for 'msg', not 'type'" + "Expected an object of type str for 'message', not 'type'" ): @deprecated class Foo: ... with self.assertRaisesRegex( TypeError, - "Expected an object of type str for 'msg', not 'function'" + "Expected an object of type str for 'message', not 'function'" ): @deprecated def foo(): ... diff --git a/Lib/warnings.py b/Lib/warnings.py index 33fbd908feaa31..39d01b34e3d9ca 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -508,7 +508,6 @@ def __exit__(self, *exc_info): self._module._showwarnmsg_impl = self._showwarnmsg_impl - class deprecated: """Indicate that a class, function or overload is deprecated. From c02f66fbb58ffe07d63b42112a8daf829420e1d6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 29 Nov 2023 06:40:42 -0800 Subject: [PATCH 20/20] Update Lib/warnings.py Co-authored-by: Alex Waygood --- Lib/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/warnings.py b/Lib/warnings.py index 39d01b34e3d9ca..924f872172d4d1 100644 --- a/Lib/warnings.py +++ b/Lib/warnings.py @@ -608,7 +608,7 @@ def __init_subclass__(*args, **kwargs): arg.__init_subclass__ = classmethod(__init_subclass__) # Or otherwise, which likely means it's a builtin such as - # type's implementation of __init_subclass__. + # object's implementation of __init_subclass__. else: @functools.wraps(original_init_subclass) def __init_subclass__(*args, **kwargs):