Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update @deprecated implementation #302

Merged
merged 6 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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

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 @@ -570,18 +570,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
71 changes: 44 additions & 27 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2287,15 +2287,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 @@ -2312,36 +2309,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 @@ -2361,7 +2378,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 @@ -2371,7 +2388,7 @@ def __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__.
# type's implementation of __init_subclass__.
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
else:
@functools.wraps(original_init_subclass)
def __init_subclass__(*args, **kwargs):
Expand All @@ -2384,6 +2401,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 @@ -2397,8 +2416,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