Skip to content

Commit db6f9b4

Browse files
Update @deprecated implementation (#302)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 18ae2b3 commit db6f9b4

File tree

3 files changed

+55
-28
lines changed

3 files changed

+55
-28
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
deprecated class now raises a `DeprecationWarning`. Patch by Jelle Zijlstra.
88
- `@deprecated` now gives a better error message if you pass a non-`str`
99
argument to the `msg` parameter. Patch by Alex Waygood.
10+
- `@deprecated` is now implemented as a class for better introspectability.
11+
Patch by Jelle Zijlstra.
1012
- Exclude `__match_args__` from `Protocol` members,
1113
this is a backport of https://github.com/python/cpython/pull/110683
1214
- When creating a `typing_extensions.NamedTuple` class, ensure `__set_name__`

src/test_typing_extensions.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,18 +575,26 @@ def d():
575575
def test_only_strings_allowed(self):
576576
with self.assertRaisesRegex(
577577
TypeError,
578-
"Expected an object of type str for 'msg', not 'type'"
578+
"Expected an object of type str for 'message', not 'type'"
579579
):
580580
@deprecated
581581
class Foo: ...
582582

583583
with self.assertRaisesRegex(
584584
TypeError,
585-
"Expected an object of type str for 'msg', not 'function'"
585+
"Expected an object of type str for 'message', not 'function'"
586586
):
587587
@deprecated
588588
def foo(): ...
589589

590+
def test_no_retained_references_to_wrapper_instance(self):
591+
@deprecated('depr')
592+
def d(): pass
593+
594+
self.assertFalse(any(
595+
isinstance(cell.cell_contents, deprecated) for cell in d.__closure__
596+
))
597+
590598

591599
class AnyTests(BaseTestCase):
592600
def test_can_subclass(self):

src/typing_extensions.py

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,15 +2292,12 @@ def method(self) -> None:
22922292
else:
22932293
_T = typing.TypeVar("_T")
22942294

2295-
def deprecated(
2296-
msg: str,
2297-
/,
2298-
*,
2299-
category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
2300-
stacklevel: int = 1,
2301-
) -> typing.Callable[[_T], _T]:
2295+
class deprecated:
23022296
"""Indicate that a class, function or overload is deprecated.
23032297
2298+
When this decorator is applied to an object, the type checker
2299+
will generate a diagnostic on usage of the deprecated object.
2300+
23042301
Usage:
23052302
23062303
@deprecated("Use B instead")
@@ -2317,36 +2314,56 @@ def g(x: int) -> int: ...
23172314
@overload
23182315
def g(x: str) -> int: ...
23192316
2320-
When this decorator is applied to an object, the type checker
2321-
will generate a diagnostic on usage of the deprecated object.
2322-
2323-
The warning specified by ``category`` will be emitted on use
2324-
of deprecated objects. For functions, that happens on calls;
2325-
for classes, on instantiation. If the ``category`` is ``None``,
2326-
no warning is emitted. The ``stacklevel`` determines where the
2317+
The warning specified by *category* will be emitted at runtime
2318+
on use of deprecated objects. For functions, that happens on calls;
2319+
for classes, on instantiation and on creation of subclasses.
2320+
If the *category* is ``None``, no warning is emitted at runtime.
2321+
The *stacklevel* determines where the
23272322
warning is emitted. If it is ``1`` (the default), the warning
23282323
is emitted at the direct caller of the deprecated object; if it
23292324
is higher, it is emitted further up the stack.
2325+
Static type checker behavior is not affected by the *category*
2326+
and *stacklevel* arguments.
23302327
2331-
The decorator sets the ``__deprecated__``
2332-
attribute on the decorated object to the deprecation message
2333-
passed to the decorator. If applied to an overload, the decorator
2328+
The deprecation message passed to the decorator is saved in the
2329+
``__deprecated__`` attribute on the decorated object.
2330+
If applied to an overload, the decorator
23342331
must be after the ``@overload`` decorator for the attribute to
23352332
exist on the overload as returned by ``get_overloads()``.
23362333
23372334
See PEP 702 for details.
23382335
23392336
"""
2340-
if not isinstance(msg, str):
2341-
raise TypeError(
2342-
f"Expected an object of type str for 'msg', not {type(msg).__name__!r}"
2343-
)
2344-
2345-
def decorator(arg: _T, /) -> _T:
2337+
def __init__(
2338+
self,
2339+
message: str,
2340+
/,
2341+
*,
2342+
category: typing.Optional[typing.Type[Warning]] = DeprecationWarning,
2343+
stacklevel: int = 1,
2344+
) -> None:
2345+
if not isinstance(message, str):
2346+
raise TypeError(
2347+
"Expected an object of type str for 'message', not "
2348+
f"{type(message).__name__!r}"
2349+
)
2350+
self.message = message
2351+
self.category = category
2352+
self.stacklevel = stacklevel
2353+
2354+
def __call__(self, arg: _T, /) -> _T:
2355+
# Make sure the inner functions created below don't
2356+
# retain a reference to self.
2357+
msg = self.message
2358+
category = self.category
2359+
stacklevel = self.stacklevel
23462360
if category is None:
23472361
arg.__deprecated__ = msg
23482362
return arg
23492363
elif isinstance(arg, type):
2364+
import functools
2365+
from types import MethodType
2366+
23502367
original_new = arg.__new__
23512368

23522369
@functools.wraps(original_new)
@@ -2366,7 +2383,7 @@ def __new__(cls, *args, **kwargs):
23662383
original_init_subclass = arg.__init_subclass__
23672384
# We need slightly different behavior if __init_subclass__
23682385
# is a bound method (likely if it was implemented in Python)
2369-
if isinstance(original_init_subclass, _types.MethodType):
2386+
if isinstance(original_init_subclass, MethodType):
23702387
original_init_subclass = original_init_subclass.__func__
23712388

23722389
@functools.wraps(original_init_subclass)
@@ -2389,6 +2406,8 @@ def __init_subclass__(*args, **kwargs):
23892406
__init_subclass__.__deprecated__ = msg
23902407
return arg
23912408
elif callable(arg):
2409+
import functools
2410+
23922411
@functools.wraps(arg)
23932412
def wrapper(*args, **kwargs):
23942413
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
@@ -2402,8 +2421,6 @@ def wrapper(*args, **kwargs):
24022421
f"a class or callable, not {arg!r}"
24032422
)
24042423

2405-
return decorator
2406-
24072424

24082425
# We have to do some monkey patching to deal with the dual nature of
24092426
# Unpack/TypeVarTuple:

0 commit comments

Comments
 (0)