Skip to content

Commit

Permalink
feat(typing): @deprecated versioning, IDE highlighting (#3455)
Browse files Browse the repository at this point in the history
* 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
dangotbanned committed Jul 16, 2024
1 parent 2036b99 commit 9afd374
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 170 deletions.
7 changes: 4 additions & 3 deletions altair/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,11 +617,12 @@ def __dir__():
return __all__


from .vegalite import *
from .jupyter import JupyterChart
from altair.vegalite import *
from altair.jupyter import JupyterChart
from altair.utils import AltairDeprecationWarning


def load_ipython_extension(ipython):
from ._magics import vegalite
from altair._magics import vegalite

ipython.register_magic_function(vegalite, "cell")
4 changes: 3 additions & 1 deletion altair/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
)
from .html import spec_to_html
from .plugin_registry import PluginRegistry
from .deprecation import AltairDeprecationWarning
from .deprecation import AltairDeprecationWarning, deprecated, deprecated_warn
from .schemapi import Undefined, Optional


Expand All @@ -21,6 +21,8 @@
"PluginRegistry",
"SchemaBase",
"Undefined",
"deprecated",
"deprecated_warn",
"display_traceback",
"infer_encoding_types",
"infer_vegalite_type_for_pandas",
Expand Down
157 changes: 88 additions & 69 deletions altair/utils/deprecation.py
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)
12 changes: 5 additions & 7 deletions altair/utils/save.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .mimebundle import spec_to_mimebundle
from ..vegalite.v5.data import data_transformers
from altair.utils._vegafusion_data import using_vegafusion
from altair.utils.deprecation import AltairDeprecationWarning
from altair.utils.deprecation import deprecated_warn

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -135,12 +135,10 @@ def save(
additional kwargs passed to spec_to_mimebundle.
"""
if webdriver is not None:
warnings.warn(
"The webdriver argument is deprecated as it's not relevant for"
+ " the new vl-convert engine which replaced altair_saver."
+ " The argument will be removed in a future release.",
AltairDeprecationWarning,
stacklevel=1,
deprecated_warn(
"The webdriver argument is not relevant for the new vl-convert engine which replaced altair_saver. "
"The argument will be removed in a future release.",
version="5.0.0",
)

if json_kwds is None:
Expand Down
Loading

0 comments on commit 9afd374

Please sign in to comment.