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 15 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
47 changes: 47 additions & 0 deletions Doc/library/warnings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,53 @@ Available Functions
and calls to :func:`simplefilter`.


.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1)

Decorator to indicate that a class, function or overload is deprecated.

When this decorator is applied to an object,
:term:`static type checkers <static type checker>`
will generate a diagnostic on usage of the deprecated object.
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved

Usage::
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

from warnings import deprecated
from typing import overload

@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: ...

The warning specified by *category* will be emitted 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. 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.

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 :func:`@overload <typing.overload>` decorator
for the attribute to exist on the overload as returned by
:func:`typing.get_overloads`.
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

.. versionadded:: 3.13
See :pep:`702`.


Available Context Managers
--------------------------

Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ venv
(using ``--without-scm-ignore-files``). (Contributed by Brett Cannon in
:gh:`108125`.)

warnings
--------

* The new :func:`warnings.deprecated` decorator provides a way to communicate
deprecations to :term:`static type checkers <static type checker>` and
to warn on usage of deprecated classes and functions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Despite static typing being the original motivation for the feature, I would actually put the runtime effect first here:

Suggested change
* The new :func:`warnings.deprecated` decorator provides a way to communicate
deprecations to :term:`static type checkers <static type checker>` and
to warn on usage of deprecated classes and functions.
* The new :func:`warnings.deprecated` decorator provides an ergonomic way to
mark a class or function as deprecated. A deprecation warning will be
emitted whenever a decorated function or class is used at runtime. The
decorator is also understood by
:term:`static type checkers <static type checker>`, which will emit warnings
if they identify a decorated function or class being used.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still want to put the type checker effect first because that's the unique part. You can write a decorator that generates runtime warnings yourself; the new and exciting part is that this decorator is also understood by static type checkers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do, sure, but do people? It was possible to reimplement itertools.batched in a few lines of code before Python 3.12, but lots of people were still very excited by its inclusion in the stdlib in Python 3.12.

I think this new decorator here could prove pretty popular with people who don't use static typing! But, I don't feel strongly; it looks fine to me now :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, there are lots of third-party deprecation decorators. The Deprecated library, I think Flask has one internally (David Lord mentioned it in PEP 702 discussions), I know at Quora we have one internally.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if that's meant to be a point in agreement or in disagreement with the point I'm making that this decorator could prove pretty popular with people who don't use static typing. Anyway, as I say, I'm happy with the docs now!

See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.)

Optimizations
=============

Expand Down
260 changes: 259 additions & 1 deletion Lib/test/test_warnings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import re
import sys
import textwrap
import types
from typing import overload, get_overloads
import unittest
from test import support
from test.support import import_helper
Expand All @@ -16,6 +18,7 @@
from test.test_warnings.data import stacklevel as warning_tests

import warnings as original_warnings
from warnings import deprecated


py_warnings = import_helper.import_fresh_module('warnings',
Expand Down Expand Up @@ -90,7 +93,7 @@ def test_module_all_attribute(self):
self.assertTrue(hasattr(self.module, '__all__'))
target_api = ["warn", "warn_explicit", "showwarning",
"formatwarning", "filterwarnings", "simplefilter",
"resetwarnings", "catch_warnings"]
"resetwarnings", "catch_warnings", "deprecated"]
self.assertSetEqual(set(self.module.__all__),
set(target_api))

Expand Down Expand Up @@ -1377,6 +1380,261 @@ def test_late_resource_warning(self):
self.assertTrue(err.startswith(expected), ascii(err))


class DeprecatedTests(unittest.TestCase):
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")

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.assertWarnsRegex(DeprecationWarning, "A will go away soon"):
with self.assertRaises(TypeError):
A(42)

def test_class_with_init(self):
@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)

def test_class_with_new(self):
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)

def test_class_with_inherited_new(self):
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)

def test_class_with_new_but_no_init(self):
new_called = False

@deprecated("HasNewNoInit will go away soon")
class HasNewNoInit:
def __new__(cls, x):
nonlocal new_called
new_called = True
obj = super().__new__(cls)
obj.x = x
return obj

with self.assertWarnsRegex(DeprecationWarning, "HasNewNoInit will go away soon"):
instance = HasNewNoInit(42)
self.assertEqual(instance.x, 42)
self.assertTrue(new_called)

def test_mixin_class(self):
@deprecated("Mixin will go away soon")
class Mixin:
pass

class Base:
def __init__(self, a) -> None:
self.a = a

with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
class Child(Base, Mixin):
pass

instance = Child(42)
self.assertEqual(instance.a, 42)

def test_existing_init_subclass(self):
@deprecated("C will go away soon")
class C:
def __init_subclass__(cls) -> None:
cls.inited = True

with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
C()

with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
class D(C):
pass

self.assertTrue(D.inited)
self.assertIsInstance(D(), D) # no deprecation

def test_existing_init_subclass_in_base(self):
class Base:
def __init_subclass__(cls, x) -> None:
cls.inited = x

@deprecated("C will go away soon")
class C(Base, x=42):
pass

self.assertEqual(C.inited, 42)

with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
C()

with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
class D(C, x=3):
pass

self.assertEqual(D.inited, 3)

def test_init_subclass_has_correct_cls(self):
init_subclass_saw = None

@deprecated("Base will go away soon")
class Base:
def __init_subclass__(cls) -> None:
nonlocal init_subclass_saw
init_subclass_saw = cls

self.assertIsNone(init_subclass_saw)

with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
class C(Base):
pass

self.assertIs(init_subclass_saw, C)

def test_init_subclass_with_explicit_classmethod(self):
init_subclass_saw = None

@deprecated("Base will go away soon")
class Base:
@classmethod
def __init_subclass__(cls) -> None:
nonlocal init_subclass_saw
init_subclass_saw = cls

self.assertIsNone(init_subclass_saw)

with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
class C(Base):
pass

self.assertIs(init_subclass_saw, C)

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 py_warnings.catch_warnings():
py_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 py_warnings.catch_warnings():
py_warnings.simplefilter("error")
d()


def setUpModule():
py_warnings.onceregistry.clear()
c_warnings.onceregistry.clear()
Expand Down