From 4c58e757c262ed44fde104385839c2f8f62a6619 Mon Sep 17 00:00:00 2001 From: mdevolde Date: Wed, 10 Jun 2026 12:06:56 +0200 Subject: [PATCH] chore(deps): add typing_extensions for py<3.13 to have a fallback for "deprecated" --- language_tool_python/_deprecated.py | 46 ++++++----------------------- language_tool_python/download_lt.py | 10 +++---- language_tool_python/match.py | 2 +- language_tool_python/utils.py | 14 ++++----- pyproject.toml | 3 +- tests/test_deprecated.py | 10 +++---- uv.lock | 2 ++ 7 files changed, 31 insertions(+), 56 deletions(-) diff --git a/language_tool_python/_deprecated.py b/language_tool_python/_deprecated.py index c63cf8c..0d772c1 100644 --- a/language_tool_python/_deprecated.py +++ b/language_tool_python/_deprecated.py @@ -1,44 +1,16 @@ """Provide 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. +If the Python version is 3.13 or higher, it uses the +built-in ``warnings.deprecated`` decorator. +If the Python version is lower than 3.13, it falls back to using the +``typing_extensions.deprecated`` decorator. """ -from __future__ import annotations - -try: - from warnings import deprecated # type: ignore [attr-defined, unused-ignore] -except ImportError: - import functools - from collections.abc import Callable - from typing import TypeVar, cast - from warnings import warn - - F = TypeVar("F", bound=Callable[..., object]) - - def deprecated( # type: ignore [no-redef, unused-ignore] - message: str, - /, - *, - category: type[Warning] | None = DeprecationWarning, - stacklevel: int = 1, - ) -> Callable[[F], F]: - """Indicate that a function is deprecated.""" - - def decorator(func: F) -> F: - @functools.wraps(func) - def wrapper(*args: object, **kwargs: object) -> object: - warn(message, category=category, stacklevel=stacklevel) - return func(*args, **kwargs) - - return cast("F", wrapper) - - return decorator +import sys +if sys.version_info >= (3, 13): + from warnings import deprecated +else: + from typing_extensions import deprecated __all__ = ["deprecated"] diff --git a/language_tool_python/download_lt.py b/language_tool_python/download_lt.py index 6a9a19a..5e875a5 100755 --- a/language_tool_python/download_lt.py +++ b/language_tool_python/download_lt.py @@ -275,7 +275,7 @@ def confirm_java_compatibility( @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def get_common_prefix(z: zipfile.ZipFile) -> str | None: """Determine the common prefix of all file names in a zip archive. @@ -297,7 +297,7 @@ def get_common_prefix(z: zipfile.ZipFile) -> str | None: @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def http_get( url: str, out_file: IO[bytes], @@ -329,7 +329,7 @@ def http_get( @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def unzip_file(temp_file_name: str, directory_to_extract_to: Path) -> None: """Unzips a zip file to a specified directory. @@ -358,7 +358,7 @@ def unzip_file(temp_file_name: str, directory_to_extract_to: Path) -> None: @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def download_zip(url: str, directory: Path) -> None: """Download a ZIP file from the given URL and extract it to the specified directory. @@ -389,7 +389,7 @@ def download_zip(url: str, directory: Path) -> None: "Use instead language_tool_python.download_lt.LocalLanguageTool.download." ), stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def download_lt(language_tool_version: str = LTP_DOWNLOAD_VERSION) -> None: """Download and extract the specified version of LanguageTool. diff --git a/language_tool_python/match.py b/language_tool_python/match.py index 381dd55..f111e94 100644 --- a/language_tool_python/match.py +++ b/language_tool_python/match.py @@ -63,7 +63,7 @@ def get_match_ordered_dict() -> OrderedDictType[str, type]: @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def auto_type(obj: SupportsInt | SupportsFloat | object) -> int | float | object: """Attempt to automatically convert the input object to an integer or float. diff --git a/language_tool_python/utils.py b/language_tool_python/utils.py index 8b3c1ef..87f117f 100644 --- a/language_tool_python/utils.py +++ b/language_tool_python/utils.py @@ -209,7 +209,7 @@ def get_language_tool_download_path() -> Path: "Replace its usage by an inline alternative." ), stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def find_existing_language_tool_downloads(download_folder: Path) -> list[Path]: """Find existing LanguageTool downloads in the specified folder. @@ -230,7 +230,7 @@ def find_existing_language_tool_downloads(download_folder: Path) -> list[Path]: @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def _extract_version(path: Path) -> version.Version: """Extract the version number from a LanguageTool directory path. @@ -265,7 +265,7 @@ def _extract_version(path: Path) -> version.Version: "get_latest_installed_version." ), stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def get_language_tool_directory() -> Path: """Get the directory path of the LanguageTool installation. @@ -307,7 +307,7 @@ def get_language_tool_directory() -> Path: "Use instead language_tool_python.download_lt.LocalLanguageTool.get_server_cmd." ), stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def get_server_cmd( port: int | None = None, config: LanguageToolConfig | None = None, @@ -350,7 +350,7 @@ def get_server_cmd( @deprecated( "This function is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) def get_jar_info() -> tuple[Path, Path]: """Retrieve the path to the Java executable and the LanguageTool JAR file. @@ -458,7 +458,7 @@ def __bool__(self) -> bool: @deprecated( "This protocol is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) @runtime_checkable class SupportsInt(Protocol): """Protocol for types that can be converted to an integer value.""" @@ -471,7 +471,7 @@ def __int__(self) -> int: @deprecated( "This protocol is no longer used internally and will be removed in 4.0.", stacklevel=2, -) # type: ignore[untyped-decorator, unused-ignore] +) @runtime_checkable class SupportsFloat(Protocol): """Protocol for types that can be converted to a float value.""" diff --git a/pyproject.toml b/pyproject.toml index 9147740..59a584a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,8 @@ dependencies = [ "tqdm", "packaging", "psutil", - "toml" + "toml", + "typing_extensions; python_version < '3.13'", # only needed for py < 3.13 because warnings.deprecated added in 3.13 is used in the codebase ] [project.urls] diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 13ec36a..299ef8a 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -14,7 +14,7 @@ def test_deprecated_emits_warning() -> None: """Test that the deprecated decorator emits a DeprecationWarning.""" - @deprecated("This function is deprecated") # type: ignore[untyped-decorator, unused-ignore] + @deprecated("This function is deprecated") def old_function() -> str: return "result" @@ -31,7 +31,7 @@ def old_function() -> str: 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[untyped-decorator, unused-ignore] + @deprecated("This is a user warning", category=UserWarning) def old_function() -> int: return 42 @@ -48,7 +48,7 @@ def old_function() -> int: def test_deprecated_preserves_function_signature() -> None: """Test that the deprecated decorator preserves function metadata.""" - @deprecated("Old function") # type: ignore[untyped-decorator, unused-ignore] + @deprecated("Old function") def my_function(x: int, y: int) -> int: """Add two numbers.""" return x + y @@ -63,7 +63,7 @@ def my_function(x: int, y: int) -> int: def test_deprecated_with_multiple_calls() -> None: """Test that warning is emitted on each call.""" - @deprecated("Deprecated function") # type: ignore[untyped-decorator, unused-ignore] + @deprecated("Deprecated function") def func() -> str: return "value" @@ -80,7 +80,7 @@ def func() -> str: 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[untyped-decorator, unused-ignore] + @deprecated("This function is obsolete") def complex_function( a: int, b: int, diff --git a/uv.lock b/uv.lock index a096d78..4eaed9f 100644 --- a/uv.lock +++ b/uv.lock @@ -648,6 +648,7 @@ dependencies = [ { name = "requests", version = "2.34.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "toml" }, { name = "tqdm" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] [package.dev-dependencies] @@ -687,6 +688,7 @@ requires-dist = [ { name = "requests" }, { name = "toml" }, { name = "tqdm" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] [package.metadata.requires-dev]