Skip to content

Commit

Permalink
Update @deprecated implementation (#302)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Waygood <[email protected]>
  • Loading branch information
JelleZijlstra and AlexWaygood authored Nov 29, 2023
1 parent 18ae2b3 commit db6f9b4
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 28 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__`
Expand Down
12 changes: 10 additions & 2 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
69 changes: 43 additions & 26 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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:
Expand Down

0 comments on commit db6f9b4

Please sign in to comment.