-
Notifications
You must be signed in to change notification settings - Fork 787
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(typing):
@deprecated
versioning, IDE highlighting (#3455)
* feat(typing): DRAFT `@deprecated` improvements Supporting draft PR to attach to upcoming issue * feat: adds `deprecate` function This wrapper format seems to satisfy `pyright` enough to trigger a strikethrough * refactor: use singular `@deprecated`, export in `utils` - The export is safe and doesn't leak to top level * refactor: extract `_format_message`, add `PEP 702` reference * feat: adds `deprecated_warn` Non-decorator counterpart to `@deprecated` * test: add test for `deprecated_warn` * refactor: standardise warnings with `deprecated_warn` ] * fix: add explicit top-level import `AltairDeprecationWarning` Resolves #3455 (comment) Note: The other imports changes are simply to align with my other PRs in review
- Loading branch information
1 parent
2036b99
commit 9afd374
Showing
7 changed files
with
166 additions
and
170 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,94 +1,113 @@ | ||
from __future__ import annotations | ||
|
||
import sys | ||
from typing import Callable, TypeVar, TYPE_CHECKING | ||
import warnings | ||
import functools | ||
from typing import TYPE_CHECKING | ||
|
||
if sys.version_info >= (3, 10): | ||
from typing import ParamSpec | ||
if sys.version_info >= (3, 13): | ||
from warnings import deprecated as _deprecated | ||
else: | ||
from typing_extensions import ParamSpec | ||
from typing_extensions import deprecated as _deprecated | ||
|
||
|
||
if TYPE_CHECKING: | ||
from functools import _Wrapped | ||
if sys.version_info >= (3, 11): | ||
from typing import LiteralString | ||
else: | ||
from typing_extensions import LiteralString | ||
|
||
T = TypeVar("T") | ||
P = ParamSpec("P") | ||
R = TypeVar("R") | ||
|
||
class AltairDeprecationWarning(DeprecationWarning): ... | ||
|
||
class AltairDeprecationWarning(UserWarning): | ||
pass | ||
|
||
def _format_message( | ||
version: LiteralString, | ||
alternative: LiteralString | None, | ||
message: LiteralString | None, | ||
/, | ||
) -> LiteralString: | ||
output = f"Deprecated in `altair={version}`." | ||
if alternative: | ||
output = f"{output} Use {alternative} instead." | ||
return f"{output}\n{message}" if message else output | ||
|
||
|
||
# NOTE: Annotating the return type breaks `pyright` detecting [reportDeprecated] | ||
# NOTE: `LiteralString` requirement is introduced by stubs | ||
def deprecated( | ||
message: str | None = None, | ||
) -> Callable[..., type[T] | _Wrapped[P, R, P, R]]: | ||
"""Decorator to deprecate a function or class. | ||
*, | ||
version: LiteralString, | ||
alternative: LiteralString | None = None, | ||
message: LiteralString | None = None, | ||
category: type[AltairDeprecationWarning] | None = AltairDeprecationWarning, | ||
stacklevel: int = 1, | ||
): # te.deprecated | ||
"""Indicate that a class, function or overload is deprecated. | ||
When this decorator is applied to an object, the type checker | ||
will generate a diagnostic on usage of the deprecated object. | ||
Parameters | ||
---------- | ||
message : string (optional) | ||
The deprecation message | ||
version | ||
``altair`` version the deprecation first appeared. | ||
alternative | ||
Suggested replacement class/method/function. | ||
message | ||
Additional message appended to ``version``, ``alternative``. | ||
category | ||
If the *category* is ``None``, no warning is emitted at runtime. | ||
stacklevel | ||
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. | ||
Static type checker behavior is not affected by the *category* | ||
and *stacklevel* arguments. | ||
References | ||
---------- | ||
[PEP 702](https://peps.python.org/pep-0702/) | ||
""" | ||
msg = _format_message(version, alternative, message) | ||
return _deprecated(msg, category=category, stacklevel=stacklevel) | ||
|
||
def wrapper(obj: type[T] | Callable[P, R]) -> type[T] | _Wrapped[P, R, P, R]: | ||
return _deprecate(obj, message=message) | ||
|
||
return wrapper | ||
def deprecated_warn( | ||
message: LiteralString, | ||
*, | ||
version: LiteralString, | ||
alternative: LiteralString | None = None, | ||
category: type[AltairDeprecationWarning] = AltairDeprecationWarning, | ||
stacklevel: int = 2, | ||
) -> None: | ||
"""Indicate that the current code path is deprecated. | ||
|
||
def _deprecate( | ||
obj: type[T] | Callable[P, R], name: str | None = None, message: str | None = None | ||
) -> type[T] | _Wrapped[P, R, P, R]: | ||
"""Return a version of a class or function that raises a deprecation warning. | ||
This should be used for non-trivial cases *only*. ``@deprecated`` should | ||
always be preferred as it is recognized by static type checkers. | ||
Parameters | ||
---------- | ||
obj : class or function | ||
The object to create a deprecated version of. | ||
name : string (optional) | ||
The name of the deprecated object | ||
message : string (optional) | ||
The deprecation message | ||
Returns | ||
------- | ||
deprecated_obj : | ||
The deprecated version of obj | ||
Examples | ||
-------- | ||
>>> class Foo: pass | ||
>>> OldFoo = _deprecate(Foo, "OldFoo") | ||
>>> f = OldFoo() # doctest: +SKIP | ||
AltairDeprecationWarning: alt.OldFoo is deprecated. Use alt.Foo instead. | ||
message | ||
Explanation of the deprecated behaviour. | ||
.. note:: | ||
Unlike ``@deprecated``, this is *not* optional. | ||
version | ||
``altair`` version the deprecation first appeared. | ||
alternative | ||
Suggested replacement argument/method/function. | ||
category | ||
The runtime warning type emitted. | ||
stacklevel | ||
How far up the call stack to make this warning appear. | ||
A value of ``2`` attributes the warning to the caller | ||
of the code calling ``deprecated_warn()``. | ||
References | ||
---------- | ||
[warnings.warn](https://docs.python.org/3/library/warnings.html#warnings.warn) | ||
""" | ||
if message is None: | ||
message = f"alt.{name} is deprecated. Use alt.{obj.__name__} instead." "" | ||
if isinstance(obj, type): | ||
if name is None: | ||
msg = f"Requires name, but got: {name=}" | ||
raise TypeError(msg) | ||
else: | ||
return type( | ||
name, | ||
(obj,), | ||
{ | ||
"__doc__": obj.__doc__, | ||
"__init__": _deprecate(obj.__init__, "__init__", message), | ||
}, | ||
) | ||
elif callable(obj): | ||
|
||
@functools.wraps(obj) | ||
def new_obj(*args: P.args, **kwargs: P.kwargs) -> R: | ||
warnings.warn(message, AltairDeprecationWarning, stacklevel=1) | ||
return obj(*args, **kwargs) | ||
|
||
new_obj._deprecated = True # type: ignore[attr-defined] | ||
return new_obj | ||
else: | ||
msg = f"Cannot deprecate object of type {type(obj)}" | ||
raise ValueError(msg) | ||
msg = _format_message(version, alternative, message) | ||
warnings.warn(msg, category=category, stacklevel=stacklevel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.