Skip to content

Commit d4a6229

Browse files
JelleZijlstrahugovkAlexWaygood
authored
gh-104003: Implement PEP 702 (#104004)
Co-authored-by: Hugo van Kemenade <[email protected]> Co-authored-by: Alex Waygood <[email protected]>
1 parent 4038869 commit d4a6229

File tree

5 files changed

+473
-2
lines changed

5 files changed

+473
-2
lines changed

Doc/library/warnings.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,56 @@ Available Functions
522522
and calls to :func:`simplefilter`.
523523

524524

525+
.. decorator:: deprecated(msg, *, category=DeprecationWarning, stacklevel=1)
526+
527+
Decorator to indicate that a class, function or overload is deprecated.
528+
529+
When this decorator is applied to an object,
530+
deprecation warnings may be emitted at runtime when the object is used.
531+
:term:`static type checkers <static type checker>`
532+
will also generate a diagnostic on usage of the deprecated object.
533+
534+
Usage::
535+
536+
from warnings import deprecated
537+
from typing import overload
538+
539+
@deprecated("Use B instead")
540+
class A:
541+
pass
542+
543+
@deprecated("Use g instead")
544+
def f():
545+
pass
546+
547+
@overload
548+
@deprecated("int support is deprecated")
549+
def g(x: int) -> int: ...
550+
@overload
551+
def g(x: str) -> int: ...
552+
553+
The warning specified by *category* will be emitted at runtime
554+
on use of deprecated objects. For functions, that happens on calls;
555+
for classes, on instantiation and on creation of subclasses.
556+
If the *category* is ``None``, no warning is emitted at runtime.
557+
The *stacklevel* determines where the
558+
warning is emitted. If it is ``1`` (the default), the warning
559+
is emitted at the direct caller of the deprecated object; if it
560+
is higher, it is emitted further up the stack.
561+
Static type checker behavior is not affected by the *category*
562+
and *stacklevel* arguments.
563+
564+
The deprecation message passed to the decorator is saved in the
565+
``__deprecated__`` attribute on the decorated object.
566+
If applied to an overload, the decorator
567+
must be after the :func:`@overload <typing.overload>` decorator
568+
for the attribute to exist on the overload as returned by
569+
:func:`typing.get_overloads`.
570+
571+
.. versionadded:: 3.13
572+
See :pep:`702`.
573+
574+
525575
Available Context Managers
526576
--------------------------
527577

Doc/whatsnew/3.13.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,15 @@ venv
348348
(using ``--without-scm-ignore-files``). (Contributed by Brett Cannon in
349349
:gh:`108125`.)
350350

351+
warnings
352+
--------
353+
354+
* The new :func:`warnings.deprecated` decorator provides a way to communicate
355+
deprecations to :term:`static type checkers <static type checker>` and
356+
to warn on usage of deprecated classes and functions. A runtime deprecation
357+
warning may also be emitted when a decorated function or class is used at runtime.
358+
See :pep:`702`. (Contributed by Jelle Zijlstra in :gh:`104003`.)
359+
351360
Optimizations
352361
=============
353362

Lib/test/test_warnings/__init__.py

Lines changed: 281 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import re
66
import sys
77
import textwrap
8+
import types
9+
from typing import overload, get_overloads
810
import unittest
911
from test import support
1012
from test.support import import_helper
@@ -16,6 +18,7 @@
1618
from test.test_warnings.data import stacklevel as warning_tests
1719

1820
import warnings as original_warnings
21+
from warnings import deprecated
1922

2023

2124
py_warnings = import_helper.import_fresh_module('warnings',
@@ -90,7 +93,7 @@ def test_module_all_attribute(self):
9093
self.assertTrue(hasattr(self.module, '__all__'))
9194
target_api = ["warn", "warn_explicit", "showwarning",
9295
"formatwarning", "filterwarnings", "simplefilter",
93-
"resetwarnings", "catch_warnings"]
96+
"resetwarnings", "catch_warnings", "deprecated"]
9497
self.assertSetEqual(set(self.module.__all__),
9598
set(target_api))
9699

@@ -1377,6 +1380,283 @@ def test_late_resource_warning(self):
13771380
self.assertTrue(err.startswith(expected), ascii(err))
13781381

13791382

1383+
class DeprecatedTests(unittest.TestCase):
1384+
def test_dunder_deprecated(self):
1385+
@deprecated("A will go away soon")
1386+
class A:
1387+
pass
1388+
1389+
self.assertEqual(A.__deprecated__, "A will go away soon")
1390+
self.assertIsInstance(A, type)
1391+
1392+
@deprecated("b will go away soon")
1393+
def b():
1394+
pass
1395+
1396+
self.assertEqual(b.__deprecated__, "b will go away soon")
1397+
self.assertIsInstance(b, types.FunctionType)
1398+
1399+
@overload
1400+
@deprecated("no more ints")
1401+
def h(x: int) -> int: ...
1402+
@overload
1403+
def h(x: str) -> str: ...
1404+
def h(x):
1405+
return x
1406+
1407+
overloads = get_overloads(h)
1408+
self.assertEqual(len(overloads), 2)
1409+
self.assertEqual(overloads[0].__deprecated__, "no more ints")
1410+
1411+
def test_class(self):
1412+
@deprecated("A will go away soon")
1413+
class A:
1414+
pass
1415+
1416+
with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"):
1417+
A()
1418+
with self.assertWarnsRegex(DeprecationWarning, "A will go away soon"):
1419+
with self.assertRaises(TypeError):
1420+
A(42)
1421+
1422+
def test_class_with_init(self):
1423+
@deprecated("HasInit will go away soon")
1424+
class HasInit:
1425+
def __init__(self, x):
1426+
self.x = x
1427+
1428+
with self.assertWarnsRegex(DeprecationWarning, "HasInit will go away soon"):
1429+
instance = HasInit(42)
1430+
self.assertEqual(instance.x, 42)
1431+
1432+
def test_class_with_new(self):
1433+
has_new_called = False
1434+
1435+
@deprecated("HasNew will go away soon")
1436+
class HasNew:
1437+
def __new__(cls, x):
1438+
nonlocal has_new_called
1439+
has_new_called = True
1440+
return super().__new__(cls)
1441+
1442+
def __init__(self, x) -> None:
1443+
self.x = x
1444+
1445+
with self.assertWarnsRegex(DeprecationWarning, "HasNew will go away soon"):
1446+
instance = HasNew(42)
1447+
self.assertEqual(instance.x, 42)
1448+
self.assertTrue(has_new_called)
1449+
1450+
def test_class_with_inherited_new(self):
1451+
new_base_called = False
1452+
1453+
class NewBase:
1454+
def __new__(cls, x):
1455+
nonlocal new_base_called
1456+
new_base_called = True
1457+
return super().__new__(cls)
1458+
1459+
def __init__(self, x) -> None:
1460+
self.x = x
1461+
1462+
@deprecated("HasInheritedNew will go away soon")
1463+
class HasInheritedNew(NewBase):
1464+
pass
1465+
1466+
with self.assertWarnsRegex(DeprecationWarning, "HasInheritedNew will go away soon"):
1467+
instance = HasInheritedNew(42)
1468+
self.assertEqual(instance.x, 42)
1469+
self.assertTrue(new_base_called)
1470+
1471+
def test_class_with_new_but_no_init(self):
1472+
new_called = False
1473+
1474+
@deprecated("HasNewNoInit will go away soon")
1475+
class HasNewNoInit:
1476+
def __new__(cls, x):
1477+
nonlocal new_called
1478+
new_called = True
1479+
obj = super().__new__(cls)
1480+
obj.x = x
1481+
return obj
1482+
1483+
with self.assertWarnsRegex(DeprecationWarning, "HasNewNoInit will go away soon"):
1484+
instance = HasNewNoInit(42)
1485+
self.assertEqual(instance.x, 42)
1486+
self.assertTrue(new_called)
1487+
1488+
def test_mixin_class(self):
1489+
@deprecated("Mixin will go away soon")
1490+
class Mixin:
1491+
pass
1492+
1493+
class Base:
1494+
def __init__(self, a) -> None:
1495+
self.a = a
1496+
1497+
with self.assertWarnsRegex(DeprecationWarning, "Mixin will go away soon"):
1498+
class Child(Base, Mixin):
1499+
pass
1500+
1501+
instance = Child(42)
1502+
self.assertEqual(instance.a, 42)
1503+
1504+
def test_existing_init_subclass(self):
1505+
@deprecated("C will go away soon")
1506+
class C:
1507+
def __init_subclass__(cls) -> None:
1508+
cls.inited = True
1509+
1510+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
1511+
C()
1512+
1513+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
1514+
class D(C):
1515+
pass
1516+
1517+
self.assertTrue(D.inited)
1518+
self.assertIsInstance(D(), D) # no deprecation
1519+
1520+
def test_existing_init_subclass_in_base(self):
1521+
class Base:
1522+
def __init_subclass__(cls, x) -> None:
1523+
cls.inited = x
1524+
1525+
@deprecated("C will go away soon")
1526+
class C(Base, x=42):
1527+
pass
1528+
1529+
self.assertEqual(C.inited, 42)
1530+
1531+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
1532+
C()
1533+
1534+
with self.assertWarnsRegex(DeprecationWarning, "C will go away soon"):
1535+
class D(C, x=3):
1536+
pass
1537+
1538+
self.assertEqual(D.inited, 3)
1539+
1540+
def test_init_subclass_has_correct_cls(self):
1541+
init_subclass_saw = None
1542+
1543+
@deprecated("Base will go away soon")
1544+
class Base:
1545+
def __init_subclass__(cls) -> None:
1546+
nonlocal init_subclass_saw
1547+
init_subclass_saw = cls
1548+
1549+
self.assertIsNone(init_subclass_saw)
1550+
1551+
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
1552+
class C(Base):
1553+
pass
1554+
1555+
self.assertIs(init_subclass_saw, C)
1556+
1557+
def test_init_subclass_with_explicit_classmethod(self):
1558+
init_subclass_saw = None
1559+
1560+
@deprecated("Base will go away soon")
1561+
class Base:
1562+
@classmethod
1563+
def __init_subclass__(cls) -> None:
1564+
nonlocal init_subclass_saw
1565+
init_subclass_saw = cls
1566+
1567+
self.assertIsNone(init_subclass_saw)
1568+
1569+
with self.assertWarnsRegex(DeprecationWarning, "Base will go away soon"):
1570+
class C(Base):
1571+
pass
1572+
1573+
self.assertIs(init_subclass_saw, C)
1574+
1575+
def test_function(self):
1576+
@deprecated("b will go away soon")
1577+
def b():
1578+
pass
1579+
1580+
with self.assertWarnsRegex(DeprecationWarning, "b will go away soon"):
1581+
b()
1582+
1583+
def test_method(self):
1584+
class Capybara:
1585+
@deprecated("x will go away soon")
1586+
def x(self):
1587+
pass
1588+
1589+
instance = Capybara()
1590+
with self.assertWarnsRegex(DeprecationWarning, "x will go away soon"):
1591+
instance.x()
1592+
1593+
def test_property(self):
1594+
class Capybara:
1595+
@property
1596+
@deprecated("x will go away soon")
1597+
def x(self):
1598+
pass
1599+
1600+
@property
1601+
def no_more_setting(self):
1602+
return 42
1603+
1604+
@no_more_setting.setter
1605+
@deprecated("no more setting")
1606+
def no_more_setting(self, value):
1607+
pass
1608+
1609+
instance = Capybara()
1610+
with self.assertWarnsRegex(DeprecationWarning, "x will go away soon"):
1611+
instance.x
1612+
1613+
with py_warnings.catch_warnings():
1614+
py_warnings.simplefilter("error")
1615+
self.assertEqual(instance.no_more_setting, 42)
1616+
1617+
with self.assertWarnsRegex(DeprecationWarning, "no more setting"):
1618+
instance.no_more_setting = 42
1619+
1620+
def test_category(self):
1621+
@deprecated("c will go away soon", category=RuntimeWarning)
1622+
def c():
1623+
pass
1624+
1625+
with self.assertWarnsRegex(RuntimeWarning, "c will go away soon"):
1626+
c()
1627+
1628+
def test_turn_off_warnings(self):
1629+
@deprecated("d will go away soon", category=None)
1630+
def d():
1631+
pass
1632+
1633+
with py_warnings.catch_warnings():
1634+
py_warnings.simplefilter("error")
1635+
d()
1636+
1637+
def test_only_strings_allowed(self):
1638+
with self.assertRaisesRegex(
1639+
TypeError,
1640+
"Expected an object of type str for 'message', not 'type'"
1641+
):
1642+
@deprecated
1643+
class Foo: ...
1644+
1645+
with self.assertRaisesRegex(
1646+
TypeError,
1647+
"Expected an object of type str for 'message', not 'function'"
1648+
):
1649+
@deprecated
1650+
def foo(): ...
1651+
1652+
def test_no_retained_references_to_wrapper_instance(self):
1653+
@deprecated('depr')
1654+
def d(): pass
1655+
1656+
self.assertFalse(any(
1657+
isinstance(cell.cell_contents, deprecated) for cell in d.__closure__
1658+
))
1659+
13801660
def setUpModule():
13811661
py_warnings.onceregistry.clear()
13821662
c_warnings.onceregistry.clear()

0 commit comments

Comments
 (0)