From 7d8b94926863054a2fab70fe624ca2d440834cd4 Mon Sep 17 00:00:00 2001 From: mdevolde Date: Mon, 12 Jan 2026 22:34:13 +0100 Subject: [PATCH] feat: added a way to mark funcs as deprecated (compatible py 3.9-3.14) --- language_tool_python/_deprecated.py | 39 +++++++++++++ pyproject.toml | 2 +- tests/test_deprecated.py | 89 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 language_tool_python/_deprecated.py create mode 100644 tests/test_deprecated.py diff --git a/language_tool_python/_deprecated.py b/language_tool_python/_deprecated.py new file mode 100644 index 0000000..44658bd --- /dev/null +++ b/language_tool_python/_deprecated.py @@ -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"] diff --git a/pyproject.toml b/pyproject.toml index d2ec362..8268472 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py new file mode 100644 index 0000000..71b681e --- /dev/null +++ b/tests/test_deprecated.py @@ -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})