"""Deprecation utilities for SQLSpec.
Provides decorators and warning functions for marking deprecated functionality.
Used to communicate API changes and migration paths to users.
"""
import functools
import inspect
import os
import sys
import types
from collections.abc import Callable
from typing import Generic, Literal, cast
from warnings import warn
from typing_extensions import ParamSpec, TypeVar
__all__ = ("deprecated", "warn_deprecation")
_PACKAGE_ROOT = os.path.dirname(os.path.dirname(__file__)) + os.sep # noqa: PTH120
T = TypeVar("T")
P = ParamSpec("P")
DeprecatedKind = Literal["function", "method", "classmethod", "attribute", "property", "class", "parameter", "import"]
[docs]
def warn_deprecation(
version: str,
deprecated_name: str,
kind: DeprecatedKind,
*,
removal_in: str | None = None,
alternative: str | None = None,
info: str | None = None,
pending: bool = False,
) -> None:
"""Warn about a call to a deprecated function, attributed to the first caller outside SQLSpec.
Args:
version: SQLSpec version where the deprecation will occur
deprecated_name: Name of the deprecated function
removal_in: SQLSpec version where the deprecated function will be removed
alternative: Name of a function that should be used instead
info: Additional information
pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
kind: Type of the deprecated thing
"""
parts = []
if kind == "import":
access_type = "Import of"
elif kind in {"function", "method"}:
access_type = "Call to"
else:
access_type = "Use of"
if pending:
parts.append(f"{access_type} {kind} awaiting deprecation '{deprecated_name}'") # pyright: ignore[reportUnknownMemberType]
else:
parts.append(f"{access_type} deprecated {kind} '{deprecated_name}'") # pyright: ignore[reportUnknownMemberType]
parts.extend( # pyright: ignore[reportUnknownMemberType]
(f"Deprecated in SQLSpec {version}", f"This {kind} will be removed in {removal_in or 'the next major version'}")
)
if alternative:
parts.append(f"Use {alternative!r} instead") # pyright: ignore[reportUnknownMemberType]
if info:
parts.append(info) # pyright: ignore[reportUnknownMemberType]
text = ". ".join(parts) # pyright: ignore[reportUnknownArgumentType]
warning_class = PendingDeprecationWarning if pending else DeprecationWarning
warn(text, warning_class, stacklevel=_external_stacklevel(sys._getframe(0)))
[docs]
def deprecated(
version: str,
*,
removal_in: str | None = None,
alternative: str | None = None,
info: str | None = None,
pending: bool = False,
kind: Literal["function", "method", "classmethod", "property"] | None = None,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Create a decorator wrapping a function, method or property with a deprecation warning.
Args:
version: SQLSpec version where the deprecation will occur
removal_in: SQLSpec version where the deprecated function will be removed
alternative: Name of a function that should be used instead
info: Additional information
pending: Use :class:`warnings.PendingDeprecationWarning` instead of :class:`warnings.DeprecationWarning`
kind: Type of the deprecated callable. If ``None``, will use ``inspect`` to figure
out if it's a function or method
Returns:
A decorator wrapping the function call with a warning
"""
return cast(
"Callable[[Callable[P, T]], Callable[P, T]]",
_DeprecatedFactory(
version=version, removal_in=removal_in, alternative=alternative, info=info, pending=pending, kind=kind
),
)
def _infer_deprecated_kind(func: Callable[P, T]) -> DeprecatedKind:
if inspect.ismethod(func):
return "method"
qualname = getattr(func, "__qualname__", "")
if inspect.isfunction(func) and "." in qualname and "<locals>" not in qualname:
return "method"
return "function"
class _DeprecatedWrapper(Generic[P, T]):
"""Callable class replacing the nested wrapper closure in _DeprecatedFactory.__call__."""
__slots__ = ("__dict__", "_alternative", "_func", "_info", "_kind", "_pending", "_removal_in", "_version")
def __init__(
self,
func: "Callable[P, T]",
version: str,
removal_in: "str | None",
alternative: "str | None",
info: "str | None",
pending: bool,
kind: DeprecatedKind,
) -> None:
self._func = func
self._version = version
self._removal_in = removal_in
self._alternative = alternative
self._info = info
self._pending = pending
self._kind: DeprecatedKind = kind
functools.update_wrapper(self, func)
def __call__(self, *args: "P.args", **kwargs: "P.kwargs") -> T:
warn_deprecation(
version=self._version,
deprecated_name=self._func.__name__,
info=self._info,
alternative=self._alternative,
pending=self._pending,
removal_in=self._removal_in,
kind=self._kind,
)
return self._func(*args, **kwargs)
def __get__(self, obj: object, _objtype: "type | None" = None) -> "Callable[P, T]":
if obj is None:
return self
return types.MethodType(self, obj)
class _DeprecatedFactory(Generic[P, T]):
__slots__ = ("_alternative", "_info", "_kind", "_pending", "_removal_in", "_version")
def __init__(
self,
*,
version: str,
removal_in: str | None,
alternative: str | None,
info: str | None,
pending: bool,
kind: Literal["function", "method", "classmethod", "property"] | None,
) -> None:
self._version = version
self._removal_in = removal_in
self._alternative = alternative
self._info = info
self._pending = pending
self._kind: Literal["function", "method", "classmethod", "property"] | None = kind
def __call__(self, func: "Callable[P, T]") -> "Callable[P, T]":
kind: DeprecatedKind = self._kind or _infer_deprecated_kind(func)
return _DeprecatedWrapper(
func, self._version, self._removal_in, self._alternative, self._info, self._pending, kind
)
def _external_stacklevel(frame: "types.FrameType | None") -> int:
"""Return the warn() stacklevel of the first frame outside the package, counting from ``frame``.
``frame`` is the frame ``warn()`` treats as level 1. Compiled frames do not appear
in the interpreter stack, so the count adapts to interpreted and mypyc builds alike.
"""
stacklevel = 1
while frame is not None and frame.f_code.co_filename.startswith(_PACKAGE_ROOT):
stacklevel += 1
frame = frame.f_back
return stacklevel