PEP 702: Runtime warnings by default (#3006)

This commit is contained in:
Jelle Zijlstra 2023-02-06 20:36:32 -08:00 committed by GitHub
parent f292166dba
commit 5f9a9e7f10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 33 additions and 50 deletions

View File

@ -16,7 +16,8 @@ Abstract
========
This PEP adds an ``@typing.deprecated()`` decorator that marks a class or function
as deprecated, enabling static checkers to warn when it is used.
as deprecated, enabling static checkers to warn when it is used. By default, this
decorator will also raise a runtime ``DeprecationWarning``.
Motivation
==========
@ -175,10 +176,30 @@ Here is how type checkers should handle usage of this library:
Runtime behavior
----------------
At runtime, the decorator sets an attribute ``__deprecated__`` on the decorated
object. The value of the attribute is the message passed to the decorator.
The decorator returns the original object. Notably, it does not issue a runtime
:exc:`DeprecationWarning`.
The ``@deprecated`` parameter takes two keyword-only arguments:
* ``category``: A warning class. Defaults to :exc:`DeprecationWarning`. If this
is set to ``None``, no warning is issued at runtime and the decorator returns
the original object, except for setting the ``__deprecated__`` attribute (see below).
* ``stacklevel``: The number of stack frames to skip when issuing the warning.
Defaults to 1, indicating that the warning should be issued at the site where the
deprecated object is called. Internally, the implementation will add the number of
stack frames it uses in wrapper code.
If the decorated object is a class, the decorator wraps the ``__new__`` methods
such that instantiating the class issues a warning. If the decorated object is a
callable, the decorator returns a new callable that wraps the original callable but
raises a warning when called. Otherwise, the decorator raises a ``TypeError``
(unless ``category=None`` is passed).
There are several scenarios where use of the decorated object cannot issue a warning,
including overloads, ``Protocol`` classes, and abstract methods. Type checkers may show a
warning if ``@deprecated`` is used without ``category=None`` in these cases.
To accommodate runtime introspection, the decorator sets an attribute
``__deprecated__`` on the object it is passed, as well as on the wrapper
callables it generates for deprecated classes and functions.
The value of the attribute is the message passed to the decorator.
For compatibility with :func:`typing.get_overloads`, the ``@deprecated``
decorator should be placed after the ``@overload`` decorator.
@ -277,52 +298,14 @@ PEP.
Open issues
===========
Runtime warnings
----------------
``skip_file_prefixes``
----------------------
Users might expect usage of the ``@deprecated`` decorator to issue a
:exc:`DeprecationWarning` at runtime. However, this would raise a number of
thorny issues:
* When the decorator is applied to a class or an overload, the warning
would not be raised as expected. For classes, the warning could be
raised on instantiation, but this would not cover usage in type
annotations or :func:`isinstance` checks.
* Users may want to control the :func:`~warnings.warn` call in more detail
(e.g., changing the warning class).
* ``typing.py`` generally aims to avoid affecting runtime behavior.
* To raise a warning, the ``@deprecated`` decorator would have to wrap
functions and patch ``__new__`` on classes. This would complicate runtime
introspection.
* Users may not expect usage of an object from the ``typing`` module to
affect runtime behavior.
Users who want to use ``@deprecated`` while also issuing a runtime warning
can use the ``if TYPE_CHECKING:`` idiom, for example:
.. code-block:: python
from typing import TYPE_CHECKING
import functools
import warnings
if TYPE_CHECKING:
from typing import deprecated
else:
def deprecated(msg):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return func(*args, **kwargs)
wrapper.__deprecated__ = msg
return wrapper
return decorator
While this code block looks complex, it could be encapsulated in a library.
Still, the behavior could be made opt-in, and perhaps the benefits of
incorporating a runtime warning outweigh the costs.
Python 3.12 `will have support <https://github.com/python/cpython/issues/39615>`__
for a ``skip_file_prefixes`` argument to :func:`warnings.warn`, which allows
control of where a warning is raised in a more intuitive manner than ``stacklevel``.
Should this feature be exposed in ``@deprecated``? If so, how would we provide a
backport in ``typing_extensions``?
Acknowledgments
===============