Skip to content
Merged
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
39 changes: 39 additions & 0 deletions language_tool_python/_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
This module provides a deprecated decorator for marking functions or classes as deprecated.
It first attempts to import the deprecated decorator from the warnings module, available in Python 3.13 and later.
If the import fails (indicating an earlier Python version), it defines a custom deprecated decorator.
The decorator from warnings issues a DeprecationWarning when the decorated object is used during runtime,
and triggers static linters to flag the usage as deprecated.
The custom decorator also issues a DeprecationWarning when the decorated object is used, but does not trigger static linters.
"""

try:
from warnings import deprecated # type: ignore [attr-defined]
except ImportError:
import functools
from typing import Any, Callable, Optional, Type, TypeVar, cast
from warnings import warn

F = TypeVar("F", bound=Callable[..., Any])

def deprecated(
message: str,
/,
*,
category: Optional[Type[Warning]] = DeprecationWarning,
stacklevel: int = 1,
) -> Callable[[F], F]:
"""Indicate that a function is deprecated."""

def decorator(func: F) -> F:
@functools.wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
warn(message, category=category, stacklevel=stacklevel)
return func(*args, **kwargs)

return cast(F, wrapper)

return decorator


__all__ = ["deprecated"]
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,4 @@ warn_unreachable = true
no_implicit_optional = true
show_error_codes = true
pretty = true
disable_error_code = ["import-not-found", "import-untyped"]
disable_error_code = ["import-not-found", "import-untyped", "unused-ignore"]
89 changes: 89 additions & 0 deletions tests/test_deprecated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Tests for the deprecated decorator."""

import warnings
from typing import Dict, Optional, Tuple

from language_tool_python._deprecated import deprecated


def test_deprecated_emits_warning() -> None:
"""Test that the deprecated decorator emits a DeprecationWarning."""

@deprecated("This function is deprecated") # type: ignore
def old_function() -> str:
return "result"

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = old_function()

assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "This function is deprecated" in str(w[0].message)
assert result == "result"


def test_deprecated_with_custom_category() -> None:
"""Test that the deprecated decorator can use a custom warning category."""

@deprecated("This is a user warning", category=UserWarning) # type: ignore
def old_function() -> int:
return 42

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = old_function()

assert len(w) == 1
assert issubclass(w[0].category, UserWarning)
assert "This is a user warning" in str(w[0].message)
assert result == 42


def test_deprecated_preserves_function_signature() -> None:
"""Test that the deprecated decorator preserves function metadata."""

@deprecated("Old function") # type: ignore
def my_function(x: int, y: int) -> int:
"""Add two numbers."""
return x + y

with warnings.catch_warnings(record=True):
assert my_function.__name__ == "my_function"
assert my_function.__doc__ is not None
assert "Add two numbers" in my_function.__doc__
assert my_function(2, 3) == 5


def test_deprecated_with_multiple_calls() -> None:
"""Test that warning is emitted on each call."""

@deprecated("Deprecated function") # type: ignore
def func() -> str:
return "value"

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
func()
func()
func()

assert len(w) == 3
assert all(issubclass(warning.category, DeprecationWarning) for warning in w)


def test_deprecated_with_args_and_kwargs() -> None:
"""Test that deprecated decorator works with functions that have args and kwargs."""

@deprecated("This function is obsolete") # type: ignore
def complex_function(
a: int, b: int, *args: int, c: Optional[int] = None, **kwargs: int
) -> Tuple[int, int, Tuple[int, ...], Optional[int], Dict[str, int]]:
return (a, b, args, c, kwargs)

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
result = complex_function(1, 2, 3, 4, c=5, d=6, e=7)

assert len(w) == 1
assert result == (1, 2, (3, 4), 5, {"d": 6, "e": 7})