Skip to content

chore(DRAFT): Support runtime behavior of @deprecated #2705

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions narwhals/_compliant/dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ def drop_nulls(self, subset: Sequence[str] | None) -> Self: ...
def explode(self, columns: Sequence[str]) -> Self: ...
def filter(self, predicate: CompliantExprT_contra | Incomplete) -> Self: ...
@deprecated(
"`LazyFrame.gather_every` is deprecated and will be removed in a future version."
"`LazyFrame.gather_every` is deprecated and will be removed in a future version.",
category=None,
)
def gather_every(self, n: int, offset: int) -> Self: ...
def group_by(
Expand Down Expand Up @@ -352,7 +353,10 @@ def select(self, *exprs: CompliantExprT_contra) -> Self: ...
def sort(
self, *by: str, descending: bool | Sequence[bool], nulls_last: bool
) -> Self: ...
@deprecated("`LazyFrame.tail` is deprecated and will be removed in a future version.")
@deprecated(
"`LazyFrame.tail` is deprecated and will be removed in a future version.",
category=None,
)
def tail(self, n: int) -> Self: ...
def unique(
self, subset: Sequence[str] | None, *, keep: LazyUniqueKeepStrategy
Expand Down
2 changes: 1 addition & 1 deletion narwhals/_compliant/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def rolling_std(
self, window_size: int, *, min_samples: int, center: bool, ddof: int
) -> Self: ...

@deprecated("Since `1.22.0`")
@deprecated("Since `1.22.0`", category=None)
def gather_every(self, n: int, offset: int) -> Self: ...
def __and__(self, other: Any) -> Self: ...
def __or__(self, other: Any) -> Self: ...
Expand Down
50 changes: 44 additions & 6 deletions narwhals/_typing_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,50 @@
import sys
from typing import TYPE_CHECKING, Any


# TODO @dangotbanned: `version` validation
# - For static typing, `message` must be a `LiteralString`
# - So the `narwhals` version needs to be embedded in the string, without using fstrings/str.format/etc
# - We'll need to decide on a style to use, and then add **runtime** validation to ensure we stay conistent
# - E.g. "<thing> is deprecated since narwhals <version>. Use <alternative> instead. <Extended description>"
# - Where only the <alternative> and <Extended description> sections are optional.
def _deprecated_compat(
message: str, /, *, category: type[DeprecationWarning] | None = DeprecationWarning
) -> Callable[[Callable[P, R]], Callable[P, R]]: # pragma: no cover
def decorate(func: Callable[P, R], /) -> Callable[P, R]:
if category is None:
func.__deprecated__ = message # type: ignore[attr-defined]
return func

# TODO @dangotbanned: Coverage for this before `3.13`?
if isinstance(func, type) or not callable(func): # pragma: no cover
from narwhals._utils import qualified_type_name

# NOTE: The logic for that part is much more complex, leaving support out *for now*,
# as we don't have any deprecated classes.
# https://github.com/python/cpython/blob/eec7a8ff22dcf409717a21a9aeab28b55526ee24/Lib/_py_warnings.py#L745-L789
msg = f"@nw._typing_compat.deprecated` cannot be applied to {qualified_type_name(func)!r}"
raise NotImplementedError(msg)
cat = category
import functools

@functools.wraps(func)
def wrapper(*args: P.args, **kwds: P.kwargs) -> R:
from narwhals._utils import issue_deprecation_warning

issue_deprecation_warning(message, _version="???", category=cat)
return func(*args, **kwds)

return wrapper

return decorate


if TYPE_CHECKING:
from typing import Callable, Protocol as Protocol38

from typing_extensions import ParamSpec

if sys.version_info >= (3, 13):
from typing import TypeVar
from warnings import deprecated
Expand All @@ -38,7 +79,8 @@
else:
from typing_extensions import Never, assert_never

_Fn = TypeVar("_Fn", bound=Callable[..., Any])
P = ParamSpec("P")
R = TypeVar("R")


else: # pragma: no cover
Expand All @@ -64,11 +106,7 @@ def TypeVar(
contravariant=contravariant,
)

def deprecated(message: str, /) -> Callable[[_Fn], _Fn]:
def wrapper(func: _Fn, /) -> _Fn:
return func

return wrapper
deprecated = _deprecated_compat

_ASSERT_NEVER_REPR_MAX_LENGTH = 100
_BUG_URL = (
Expand Down
23 changes: 15 additions & 8 deletions narwhals/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1254,14 +1254,14 @@ def is_ordered_categorical(series: Series[Any]) -> bool:
return result


@deprecated(
"Use `generate_temporary_column_name` instead.\n"
"`generate_unique_token` is deprecated since **1.13.0** "
"and it will be removed in future versions."
)
def generate_unique_token(
n_bytes: int, columns: Container[str]
) -> str: # pragma: no cover
msg = (
"Use `generate_temporary_column_name` instead. `generate_unique_token` is "
"deprecated and it will be removed in future versions"
)
issue_deprecation_warning(msg, _version="1.13.0")
return generate_temporary_column_name(n_bytes=n_bytes, columns=columns)


Expand Down Expand Up @@ -1430,15 +1430,22 @@ def find_stacklevel() -> int:
return n


def issue_deprecation_warning(message: str, _version: str) -> None:
def issue_deprecation_warning(
message: str,
/,
*,
_version: str,
category: type[DeprecationWarning] = DeprecationWarning,
) -> None:
"""Issue a deprecation warning.

Arguments:
message: The message associated with the warning.
_version: Narwhals version when the warning was introduced. Just used for internal
bookkeeping.
category: The `DeprecationWarning` category subclass.
"""
warn(message=message, category=DeprecationWarning, stacklevel=find_stacklevel())
warn(message=message, category=category, stacklevel=find_stacklevel())


def validate_strict_and_pass_though(
Expand Down Expand Up @@ -1865,7 +1872,7 @@ def deprecated(cls, message: LiteralString, /) -> Self:
[descriptor]: https://docs.python.org/3/howto/descriptor.html
"""
obj = cls()
return deprecated(message)(obj)
return deprecated(message, category=None)(obj)


def _raise_not_implemented_error(what: str, who: str, /) -> NotImplementedError:
Expand Down
10 changes: 3 additions & 7 deletions narwhals/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,9 @@ def show_versions() -> None:


@deprecated(
"`get_level` is deprecated, as Narwhals no longer supports the Dataframe Interchange Protocol."
"`get_level` is deprecated since **1.43**, as Narwhals no longer supports the Dataframe Interchange Protocol.\n"
"DuckDB and Ibis now have full lazy support in Narwhals, and passing them to `nw.from_native` \n"
"returns `nw.LazyFrame`."
)
def get_level(
obj: DataFrame[Any] | LazyFrame[Any] | Series[IntoSeriesT],
Expand All @@ -622,12 +624,6 @@ def get_level(
- 'lazy': only lazy operations are supported. This excludes anything
which involves iterating over rows in Python.
"""
issue_deprecation_warning(
"`get_level` is deprecated, as Narwhals no longer supports the Dataframe Interchange Protocol.\n"
"DuckDB and Ibis now have full lazy support in Narwhals, and passing them to `nw.from_native` \n"
"returns `nw.LazyFrame`.",
"1.43",
)
return obj._level


Expand Down
Loading