Skip to content

Commit 35b89a0

Browse files
authored
feat: added a way to mark funcs as deprecated (compatible py 3.9-3.14) (jxmorris12#153)
1 parent eb8ee39 commit 35b89a0

File tree

3 files changed

+129
-1
lines changed

3 files changed

+129
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
This module provides a deprecated decorator for marking functions or classes as deprecated.
3+
It first attempts to import the deprecated decorator from the warnings module, available in Python 3.13 and later.
4+
If the import fails (indicating an earlier Python version), it defines a custom deprecated decorator.
5+
The decorator from warnings issues a DeprecationWarning when the decorated object is used during runtime,
6+
and triggers static linters to flag the usage as deprecated.
7+
The custom decorator also issues a DeprecationWarning when the decorated object is used, but does not trigger static linters.
8+
"""
9+
10+
try:
11+
from warnings import deprecated # type: ignore [attr-defined]
12+
except ImportError:
13+
import functools
14+
from typing import Any, Callable, Optional, Type, TypeVar, cast
15+
from warnings import warn
16+
17+
F = TypeVar("F", bound=Callable[..., Any])
18+
19+
def deprecated(
20+
message: str,
21+
/,
22+
*,
23+
category: Optional[Type[Warning]] = DeprecationWarning,
24+
stacklevel: int = 1,
25+
) -> Callable[[F], F]:
26+
"""Indicate that a function is deprecated."""
27+
28+
def decorator(func: F) -> F:
29+
@functools.wraps(func)
30+
def wrapper(*args: Any, **kwargs: Any) -> Any:
31+
warn(message, category=category, stacklevel=stacklevel)
32+
return func(*args, **kwargs)
33+
34+
return cast(F, wrapper)
35+
36+
return decorator
37+
38+
39+
__all__ = ["deprecated"]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,4 @@ warn_unreachable = true
8787
no_implicit_optional = true
8888
show_error_codes = true
8989
pretty = true
90-
disable_error_code = ["import-not-found", "import-untyped"]
90+
disable_error_code = ["import-not-found", "import-untyped", "unused-ignore"]

tests/test_deprecated.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Tests for the deprecated decorator."""
2+
3+
import warnings
4+
from typing import Dict, Optional, Tuple
5+
6+
from language_tool_python._deprecated import deprecated
7+
8+
9+
def test_deprecated_emits_warning() -> None:
10+
"""Test that the deprecated decorator emits a DeprecationWarning."""
11+
12+
@deprecated("This function is deprecated") # type: ignore
13+
def old_function() -> str:
14+
return "result"
15+
16+
with warnings.catch_warnings(record=True) as w:
17+
warnings.simplefilter("always")
18+
result = old_function()
19+
20+
assert len(w) == 1
21+
assert issubclass(w[0].category, DeprecationWarning)
22+
assert "This function is deprecated" in str(w[0].message)
23+
assert result == "result"
24+
25+
26+
def test_deprecated_with_custom_category() -> None:
27+
"""Test that the deprecated decorator can use a custom warning category."""
28+
29+
@deprecated("This is a user warning", category=UserWarning) # type: ignore
30+
def old_function() -> int:
31+
return 42
32+
33+
with warnings.catch_warnings(record=True) as w:
34+
warnings.simplefilter("always")
35+
result = old_function()
36+
37+
assert len(w) == 1
38+
assert issubclass(w[0].category, UserWarning)
39+
assert "This is a user warning" in str(w[0].message)
40+
assert result == 42
41+
42+
43+
def test_deprecated_preserves_function_signature() -> None:
44+
"""Test that the deprecated decorator preserves function metadata."""
45+
46+
@deprecated("Old function") # type: ignore
47+
def my_function(x: int, y: int) -> int:
48+
"""Add two numbers."""
49+
return x + y
50+
51+
with warnings.catch_warnings(record=True):
52+
assert my_function.__name__ == "my_function"
53+
assert my_function.__doc__ is not None
54+
assert "Add two numbers" in my_function.__doc__
55+
assert my_function(2, 3) == 5
56+
57+
58+
def test_deprecated_with_multiple_calls() -> None:
59+
"""Test that warning is emitted on each call."""
60+
61+
@deprecated("Deprecated function") # type: ignore
62+
def func() -> str:
63+
return "value"
64+
65+
with warnings.catch_warnings(record=True) as w:
66+
warnings.simplefilter("always")
67+
func()
68+
func()
69+
func()
70+
71+
assert len(w) == 3
72+
assert all(issubclass(warning.category, DeprecationWarning) for warning in w)
73+
74+
75+
def test_deprecated_with_args_and_kwargs() -> None:
76+
"""Test that deprecated decorator works with functions that have args and kwargs."""
77+
78+
@deprecated("This function is obsolete") # type: ignore
79+
def complex_function(
80+
a: int, b: int, *args: int, c: Optional[int] = None, **kwargs: int
81+
) -> Tuple[int, int, Tuple[int, ...], Optional[int], Dict[str, int]]:
82+
return (a, b, args, c, kwargs)
83+
84+
with warnings.catch_warnings(record=True) as w:
85+
warnings.simplefilter("always")
86+
result = complex_function(1, 2, 3, 4, c=5, d=6, e=7)
87+
88+
assert len(w) == 1
89+
assert result == (1, 2, (3, 4), 5, {"d": 6, "e": 7})

0 commit comments

Comments
 (0)