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

gh-104003: Implement PEP 702 #104004

Merged
merged 25 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
93e11d6
gh-104003: Implement PEP 702
JelleZijlstra Apr 30, 2023
d8d3157
blurb
JelleZijlstra Apr 30, 2023
342f28b
Add more tests
JelleZijlstra Apr 30, 2023
9b167e8
describe runtime behavior
JelleZijlstra May 6, 2023
0ed9d41
steal the typing-extensions wording instead, it's better
JelleZijlstra May 6, 2023
2f69b4e
Merge remote-tracking branch 'upstream/main' into pep702
JelleZijlstra Nov 8, 2023
815591b
Put it in warnings, copy over from typing-extensions
JelleZijlstra Nov 8, 2023
4098e62
docs
JelleZijlstra Nov 8, 2023
d6625b9
fix test
JelleZijlstra Nov 8, 2023
0870a16
Update Doc/library/warnings.rst
JelleZijlstra Nov 8, 2023
2b48983
Merge branch 'main' into pep702
JelleZijlstra Nov 8, 2023
b9507bd
Code review
JelleZijlstra Nov 8, 2023
2f23654
Merge branch 'main' into pep702
JelleZijlstra Nov 8, 2023
ed9a10c
make it a class
JelleZijlstra Nov 27, 2023
6ab4584
Merge remote-tracking branch 'upstream/main' into pep702
JelleZijlstra Nov 27, 2023
e60ad5c
Merge remote-tracking branch 'upstream/main' into pep702
JelleZijlstra Nov 29, 2023
d528dc8
Forward-port typing-extensions change
JelleZijlstra Nov 29, 2023
e323502
Add test suggested by Alex
JelleZijlstra Nov 29, 2023
acf49b3
Adjust docs
JelleZijlstra Nov 29, 2023
1d663ef
Add to Whats New
JelleZijlstra Nov 29, 2023
b94255d
Now that this is semi-public, use a better name
JelleZijlstra Nov 29, 2023
e7a361b
sync docstring
JelleZijlstra Nov 29, 2023
08efe4d
Update error msg
JelleZijlstra Nov 29, 2023
f4eb075
Apply suggestions from code review
AlexWaygood Nov 29, 2023
c02f66f
Update Lib/warnings.py
JelleZijlstra Nov 29, 2023
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
148 changes: 147 additions & 1 deletion Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved

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)
gvanrossum marked this conversation as resolved.
Show resolved Hide resolved

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):
Expand Down
77 changes: 77 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def _idfunc(_, x):
'cast',
'clear_overloads',
'dataclass_transform',
'deprecated',
'final',
'get_args',
'get_origin',
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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.