diff --git a/CHANGELOG.md b/CHANGELOG.md index 43569fae..d85aae3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra. - `@deprecated` now gives a better error message if you pass a non-`str` argument to the `msg` parameter. Patch by Alex Waygood. +- `@deprecated` is now implemented as a class for better introspectability. + Patch by Jelle Zijlstra. - Exclude `__match_args__` from `Protocol` members, this is a backport of https://github.com/python/cpython/pull/110683 - When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__` diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f4c211fc..562704e9 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -575,18 +575,26 @@ 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(): ... + 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__ + )) + class AnyTests(BaseTestCase): def test_can_subclass(self): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 5e4932bc..a875ec97 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -2292,15 +2292,12 @@ def method(self) -> None: else: _T = typing.TypeVar("_T") - def deprecated( - msg: str, - /, - *, - category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, - stacklevel: int = 1, - ) -> typing.Callable[[_T], _T]: + 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") @@ -2317,36 +2314,56 @@ 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 + 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 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()``. See PEP 702 for details. """ - if not isinstance(msg, str): - raise TypeError( - f"Expected an object of type str for 'msg', not {type(msg).__name__!r}" - ) - - def decorator(arg: _T, /) -> _T: + def __init__( + self, + message: str, + /, + *, + category: typing.Optional[typing.Type[Warning]] = DeprecationWarning, + stacklevel: int = 1, + ) -> None: + if not isinstance(message, str): + raise TypeError( + "Expected an object of type str for 'message', not " + f"{type(message).__name__!r}" + ) + self.message = message + self.category = category + self.stacklevel = stacklevel + + def __call__(self, arg: _T, /) -> _T: + # 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) @@ -2366,7 +2383,7 @@ def __new__(cls, *args, **kwargs): 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, _types.MethodType): + if isinstance(original_init_subclass, MethodType): original_init_subclass = original_init_subclass.__func__ @functools.wraps(original_init_subclass) @@ -2389,6 +2406,8 @@ def __init_subclass__(*args, **kwargs): __init_subclass__.__deprecated__ = msg return arg elif callable(arg): + import functools + @functools.wraps(arg) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) @@ -2402,8 +2421,6 @@ def wrapper(*args, **kwargs): f"a class or callable, not {arg!r}" ) - return decorator - # We have to do some monkey patching to deal with the dual nature of # Unpack/TypeVarTuple: