From a9cd57fdfb6a9ecbc0c11d420680a74156eb7c20 Mon Sep 17 00:00:00 2001 From: mdevolde Date: Fri, 19 Jun 2026 16:49:52 +0200 Subject: [PATCH] refactor: makes internal utilities private --- language_tool_python/__main__.py | 2 +- language_tool_python/_internals/__init__.py | 0 .../{ => _internals}/api_types.py | 10 +- .../{_compat.py => _internals/compat.py} | 0 .../{ => _internals}/safe_zip.py | 6 +- language_tool_python/_internals/utils.py | 209 ++++++++++++++++++ language_tool_python/config_file.py | 35 +-- language_tool_python/download_lt.py | 122 +++++----- language_tool_python/exceptions.py | 8 + language_tool_python/language_tag.py | 2 + language_tool_python/match.py | 62 ++++-- language_tool_python/server.py | 46 ++-- language_tool_python/utils.py | 199 +---------------- tests/test_download.py | 38 ++-- tests/test_safe_zip.py | 4 +- tests/test_server_local.py | 2 +- 16 files changed, 408 insertions(+), 337 deletions(-) create mode 100644 language_tool_python/_internals/__init__.py rename language_tool_python/{ => _internals}/api_types.py (93%) rename language_tool_python/{_compat.py => _internals/compat.py} (100%) rename language_tool_python/{ => _internals}/safe_zip.py (99%) create mode 100644 language_tool_python/_internals/utils.py diff --git a/language_tool_python/__main__.py b/language_tool_python/__main__.py index 8b03086..554dbb9 100644 --- a/language_tool_python/__main__.py +++ b/language_tool_python/__main__.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import TYPE_CHECKING, TypedDict, cast -from ._compat import toml_loads +from ._internals.compat import toml_loads from .exceptions import LanguageToolError from .server import LanguageTool diff --git a/language_tool_python/_internals/__init__.py b/language_tool_python/_internals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/language_tool_python/api_types.py b/language_tool_python/_internals/api_types.py similarity index 93% rename from language_tool_python/api_types.py rename to language_tool_python/_internals/api_types.py index f39f260..e314ca2 100644 --- a/language_tool_python/api_types.py +++ b/language_tool_python/_internals/api_types.py @@ -17,7 +17,9 @@ "LanguageInfo", "MatchType", "Replacement", + "ReplacementOptional", "Rule", + "RuleOptional", "WarningInfo", "is_check_response", "is_language_info", @@ -50,11 +52,11 @@ def is_language_info(value: object) -> TypeGuard[LanguageInfo]: ) -class _ReplacementOptional(TypedDict, total=False): +class ReplacementOptional(TypedDict, total=False): shortDescription: str -class Replacement(_ReplacementOptional): +class Replacement(ReplacementOptional): """A suggested replacement returned by LanguageTool.""" value: str @@ -75,12 +77,12 @@ class Category(TypedDict): name: str -class _RuleOptional(TypedDict, total=False): +class RuleOptional(TypedDict, total=False): sourceFile: str subId: str -class Rule(_RuleOptional): +class Rule(RuleOptional): """LanguageTool rule metadata for a match.""" id: str diff --git a/language_tool_python/_compat.py b/language_tool_python/_internals/compat.py similarity index 100% rename from language_tool_python/_compat.py rename to language_tool_python/_internals/compat.py diff --git a/language_tool_python/safe_zip.py b/language_tool_python/_internals/safe_zip.py similarity index 99% rename from language_tool_python/safe_zip.py rename to language_tool_python/_internals/safe_zip.py index 5f85ed2..aa2c757 100644 --- a/language_tool_python/safe_zip.py +++ b/language_tool_python/_internals/safe_zip.py @@ -12,12 +12,14 @@ from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING -from .exceptions import PathError -from .utils import get_env_float, get_env_int +from language_tool_python._internals.utils import get_env_float, get_env_int +from language_tool_python.exceptions import PathError if TYPE_CHECKING: import zipfile +__all__ = ["SafeZipExtractor", "SafeZipLimits"] + logger = logging.getLogger(__name__) CONTROL_CHARACTER_MAX = 32 diff --git a/language_tool_python/_internals/utils.py b/language_tool_python/_internals/utils.py new file mode 100644 index 0000000..dce0031 --- /dev/null +++ b/language_tool_python/_internals/utils.py @@ -0,0 +1,209 @@ +import contextlib +import locale +import logging +import math +import os +import urllib.parse +from pathlib import Path +from typing import Protocol, runtime_checkable + +import psutil + +from language_tool_python.exceptions import PathError + +__all__ = [ + "SupportsBool", + "get_env_float", + "get_env_int", + "get_language_tool_download_path", + "get_locale_language", + "kill_process_force", + "parse_url", + "version_tuple", +] + +logger = logging.getLogger(__name__) + +JAR_NAMES = [ + "languagetool-server.jar", + "LanguageTool.jar", +] +FAILSAFE_LANGUAGE = "en" + +LTP_PATH_ENV_VAR = "LTP_PATH" # LanguageTool download path + +# Directory containing the LanguageTool jar file: +LTP_JAR_DIR_PATH_ENV_VAR = "LTP_JAR_DIR_PATH" + + +def parse_url(url_str: str) -> str: + """Parse the given URL string and ensure it has a scheme. + + If the input URL string does not contain 'http', 'http://' is prepended to it. The + function then parses the URL and returns its canonical form. + + :param url_str: The URL string to be parsed. + :type url_str: str + :return: The parsed URL in its canonical form. + :rtype: str + """ + if "http" not in url_str: + url_str = "http://" + url_str + + return urllib.parse.urlparse(url_str).geturl() + + +def get_env_int(env_var: str, default: int) -> int: + """Read a positive integer from the environment. + + :param env_var: Environment variable name. + :type env_var: str + :param default: Value to use when the environment variable is absent. + :type default: int + :return: Configured integer value, or the default. + :rtype: int + :raises PathError: If the configured value is invalid. + """ + configured = os.environ.get(env_var) + + if configured is None: + return default + + try: + value = int(configured) + except ValueError as e: + err = f"Invalid integer configured by {env_var}: {configured!r}." + raise PathError(err) from e + + if value <= 0: + err = f"Invalid integer configured by {env_var}: {configured!r}." + raise PathError(err) + + return value + + +def get_env_float(env_var: str, default: float) -> float: + """Read a positive float from the environment. + + :param env_var: Environment variable name. + :type env_var: str + :param default: Value to use when the environment variable is absent. + :type default: float + :return: Configured float value, or the default. + :rtype: float + :raises PathError: If the configured value is invalid. + """ + configured = os.environ.get(env_var) + + if configured is None: + return default + + try: + value = float(configured) + except ValueError as e: + err = f"Invalid float configured by {env_var}: {configured!r}." + raise PathError(err) from e + + if not math.isfinite(value) or value <= 0: + err = f"Invalid float configured by {env_var}: {configured!r}." + raise PathError(err) + + return value + + +def get_language_tool_download_path() -> Path: + """Get the download path for LanguageTool. + + This function retrieves the download path for LanguageTool from the environment + variable specified by ``LTP_PATH_ENV_VAR``. If the environment variable is not set, + it defaults to a path in the user's home directory under + ``.cache/language_tool_python``. The function ensures that the directory exists + before returning it. + + :return: The download path for LanguageTool. + :rtype: Path + """ + # Get download path from environment or use default. + path_str = os.environ.get( + LTP_PATH_ENV_VAR, + str(Path.home() / ".cache" / "language_tool_python"), + ) + path = Path(path_str) + path.mkdir(parents=True, exist_ok=True) + return path + + +def get_locale_language() -> str: + """Get the current locale language. + + This function retrieves the current locale language setting of the system. It first + attempts to get the locale using ``locale.getlocale()``. If that fails, it falls + back to using ``locale.getdefaultlocale()``. If both methods fail to provide a valid + language code, it returns a default failsafe language code. + + :return: The language code of the current locale. + :rtype: str + """ + return locale.getlocale()[0] or locale.getdefaultlocale()[0] or FAILSAFE_LANGUAGE + + +def kill_process_force( + *, + pid: int | None = None, + proc: psutil.Process | None = None, +) -> None: + """Terminate a process and all of its child processes forcefully. + + This function attempts to kill a process specified either by its PID or by a + psutil.Process object. If the process has any child processes, they will be killed + first. + + :param pid: The process ID of the process to be killed. Either ``pid`` or ``proc`` + must be provided. + :type pid: int | None + :param proc: A psutil.Process object representing the process to be killed. Either + ``pid`` or ``proc`` must be provided. + :type proc: psutil.Process | None + :raises ValueError: If neither ``pid`` nor ``proc`` is provided. + """ + if not any([pid, proc]): + err = "Must pass either pid or proc" + raise ValueError(err) + try: + proc = psutil.Process(pid) if proc is None else proc + except psutil.NoSuchProcess: + logger.debug("Process %s does not exist, nothing to kill", pid) + return + logger.debug("Killing process %s and its children", proc.pid) + for child in proc.children(recursive=True): + with contextlib.suppress(psutil.NoSuchProcess): + logger.debug("Killing child process %s", child.pid) + child.kill() + with contextlib.suppress(psutil.NoSuchProcess): + proc.kill() + + +@runtime_checkable +class SupportsBool(Protocol): + """Protocol for types that can be converted to a boolean value.""" + + def __bool__(self) -> bool: + """Define the interface for types that can be evaluated in a boolean context.""" + ... + + +def version_tuple(v: str) -> tuple[int, int]: + """Convert a version string into a tuple of integers. + + This function takes a version string in the format 'X.Y' and converts it into a + tuple of integers (X, Y). This can be useful for comparing version numbers. + + :param v: The version string to be converted, expected in the format 'X.Y'. + :type v: str + :return: A tuple of integers representing the version, in the format (X, Y). + :rtype: tuple[int, int] + :raises ValueError: If the version string is not in the expected format or contains + non-integer components. + """ + major, minor = v.split(".") + return int(major), int(minor) diff --git a/language_tool_python/config_file.py b/language_tool_python/config_file.py index f01a145..4e3987e 100644 --- a/language_tool_python/config_file.py +++ b/language_tool_python/config_file.py @@ -11,17 +11,22 @@ from pathlib import Path from typing import TypeVar, cast +from ._internals.utils import SupportsBool from .exceptions import PathError -from .utils import SupportsBool + +__all__ = [ + "ConfigValue", + "LanguageToolConfig", +] ConfigValue = PathLike[str] | SupportsBool | str | int | float | Iterable[str] -ConfigValueT = TypeVar("ConfigValueT", bound=ConfigValue) +_ConfigValueT = TypeVar("_ConfigValueT", bound=ConfigValue) logger = logging.getLogger(__name__) -LANGUAGE_KEY_PARTS = 2 -LANGUAGE_KEY_WITH_DICT_PATH_PARTS = 3 -LANGUAGE_DICT_PATH_SEPARATOR_COUNT = 2 +_LANGUAGE_KEY_PARTS = 2 +_LANGUAGE_KEY_WITH_DICT_PATH_PARTS = 3 +_LANGUAGE_DICT_PATH_SEPARATOR_COUNT = 2 def _reject_line_breaks(field_name: str, value: str) -> None: @@ -45,7 +50,7 @@ def _reject_line_breaks(field_name: str, value: str) -> None: @dataclass(frozen=True) -class OptionSpec: +class _OptionSpec: """Specification for a configuration option. This class defines the structure and behavior of a configuration option, including @@ -67,11 +72,11 @@ class OptionSpec: def _option_spec( py_types: type[object] | tuple[type[object], ...], - encoder: Callable[[ConfigValueT], str], - validator: Callable[[ConfigValueT], None] | None = None, -) -> OptionSpec: + encoder: Callable[[_ConfigValueT], str], + validator: Callable[[_ConfigValueT], None] | None = None, +) -> _OptionSpec: """Create a schema entry for a runtime-checked configuration option.""" - return OptionSpec( + return _OptionSpec( py_types=py_types, encoder=cast("Callable[[ConfigValue], str]", encoder), validator=cast("Callable[[ConfigValue], None] | None", validator), @@ -152,7 +157,7 @@ def _path_validator(v: PathLike[str] | str) -> None: raise PathError(err) -CONFIG_SCHEMA: dict[str, OptionSpec] = { +_CONFIG_SCHEMA: dict[str, _OptionSpec] = { "maxTextLength": _option_spec(int, _int_encoder), "maxTextHardLength": _option_spec(int, _int_encoder), "maxCheckTimeMillis": _option_spec(int, _int_encoder), @@ -199,8 +204,8 @@ def _is_lang_key(key: str) -> bool: return False parts = key.split("-") - return (len(parts) == LANGUAGE_KEY_PARTS and len(parts[1]) > 0) or ( # lang- - len(parts) == LANGUAGE_KEY_WITH_DICT_PATH_PARTS + return (len(parts) == _LANGUAGE_KEY_PARTS and len(parts[1]) > 0) or ( # lang- + len(parts) == _LANGUAGE_KEY_WITH_DICT_PATH_PARTS and len(parts[1]) > 0 and parts[2] == "dictPath" # lang--dictPath ) @@ -234,7 +239,7 @@ def _encode_config(config: Mapping[str, ConfigValue]) -> dict[str, str]: _reject_line_breaks(key, encoded[key]) continue if ( - _is_lang_key(key) and key.count("-") == LANGUAGE_DICT_PATH_SEPARATOR_COUNT + _is_lang_key(key) and key.count("-") == _LANGUAGE_DICT_PATH_SEPARATOR_COUNT ): # lang--dictPath logger.debug("Encoding language dictPath %s=%r", key, value) path_value = cast("PathLike[str] | str", value) @@ -243,7 +248,7 @@ def _encode_config(config: Mapping[str, ConfigValue]) -> dict[str, str]: _reject_line_breaks(key, encoded[key]) continue - spec = CONFIG_SCHEMA.get(key) + spec = _CONFIG_SCHEMA.get(key) if spec is None: err = f"unexpected key in config: {key}" raise ValueError(err) diff --git a/language_tool_python/download_lt.py b/language_tool_python/download_lt.py index 0a66595..d487392 100755 --- a/language_tool_python/download_lt.py +++ b/language_tool_python/download_lt.py @@ -23,15 +23,15 @@ import requests import tqdm -from ._compat import toml_loads -from .exceptions import JavaError, PathError -from .safe_zip import SafeZipExtractor -from .utils import ( +from ._internals.compat import toml_loads +from ._internals.safe_zip import SafeZipExtractor +from ._internals.utils import ( LTP_JAR_DIR_PATH_ENV_VAR, get_env_int, get_language_tool_download_path, version_tuple, ) +from .exceptions import JavaError, PathError if TYPE_CHECKING: from collections.abc import Mapping @@ -39,41 +39,48 @@ from .config_file import LanguageToolConfig +__all__ = [ + "LTP_DOWNLOAD_VERSION", + "LocalLanguageTool", + "ReleaseLocalLanguageTool", + "SnapshotLocalLanguageTool", +] + logger = logging.getLogger(__name__) -MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL = 9 -MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL = 17 -HTTP_STATUS_NOT_FOUND = 404 -HTTP_STATUS_FORBIDDEN = 403 -HTTP_STATUS_OK = 200 +_MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL = 9 +_MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL = 17 +_HTTP_STATUS_NOT_FOUND = 404 +_HTTP_STATUS_FORBIDDEN = 403 +_HTTP_STATUS_OK = 200 # Get download host from environment or default. -BASE_URL_SNAPSHOT = os.environ.get( +_BASE_URL_SNAPSHOT = os.environ.get( "LTP_DOWNLOAD_HOST_SNAPSHOT", "https://internal1.languagetool.org/snapshots/", ) -FILENAME_SNAPSHOT = "LanguageTool-{version}-snapshot.zip" -BASE_URL_NEW_RELEASES = os.environ.get( +_FILENAME_SNAPSHOT = "LanguageTool-{version}-snapshot.zip" +_BASE_URL_NEW_RELEASES = os.environ.get( "LTP_DOWNLOAD_HOST_NEW_RELEASES", "https://github.com/jxmorris12/language_tool_python/releases/download/LanguageTool-{version}/", ) -BASE_URL_RELEASE = os.environ.get( +_BASE_URL_RELEASE = os.environ.get( "LTP_DOWNLOAD_HOST_RELEASE", "https://languagetool.org/download/", ) -BASE_URL_ARCHIVE = os.environ.get( +_BASE_URL_ARCHIVE = os.environ.get( "LTP_DOWNLOAD_HOST_ARCHIVE", "https://languagetool.org/download/archive/", ) -FILENAME_RELEASE = "LanguageTool-{version}.zip" +_FILENAME_RELEASE = "LanguageTool-{version}.zip" LTP_DOWNLOAD_VERSION = "6.8" -LT_SNAPSHOT_LATEST_VERSION = "latest" -LTP_DOWNLOAD_SHA256_ENV_VAR = "LTP_DOWNLOAD_SHA256" -LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR = "LTP_BYPASS_VERIFIED_DOWNLOADS" -LTP_MAX_DOWNLOAD_BYTES_ENV_VAR = "LTP_MAX_DOWNLOAD_BYTES" -DOWNLOAD_CHUNK_BYTES = 1024 * 1024 +_LT_SNAPSHOT_LATEST_VERSION = "latest" +_LTP_DOWNLOAD_SHA256_ENV_VAR = "LTP_DOWNLOAD_SHA256" +_LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR = "LTP_BYPASS_VERIFIED_DOWNLOADS" +_LTP_MAX_DOWNLOAD_BYTES_ENV_VAR = "LTP_MAX_DOWNLOAD_BYTES" +_DOWNLOAD_CHUNK_BYTES = 1024 * 1024 _SAFE_ZIP_EXTRACTOR = SafeZipExtractor() @@ -117,24 +124,24 @@ def _load_expected_download_sha256(raw_manifest: str) -> dict[str, str]: ) as hashes_path, hashes_path.open("rb") as f, ): - EXPECTED_DOWNLOAD_SHA256 = _load_expected_download_sha256( + _EXPECTED_DOWNLOAD_SHA256 = _load_expected_download_sha256( f.read().decode("utf-8"), ) -JAVA_VERSION_REGEX = re.compile( +_JAVA_VERSION_REGEX = re.compile( r'^(?:java|openjdk) version "(?P\d+)(|\.(?P\d+)\.[^"]+)"', re.MULTILINE, ) # Updated for later versions of java -JAVA_VERSION_REGEX_UPDATED = re.compile( +_JAVA_VERSION_REGEX_UPDATED = re.compile( r"^(?:java|openjdk) [version ]?(?P\d+)\.(?P\d+)", re.MULTILINE, ) -MAX_DOWNLOAD_BYTES = get_env_int( - LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, +_MAX_DOWNLOAD_BYTES = get_env_int( + _LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, 512 * 1024 * 1024, ) # 512 MiB, latest snapshot: 246.58 MiB archive @@ -156,10 +163,11 @@ def _get_zip_hash(version_name: str) -> str | None: :rtype: str | None :raises PathError: If a configured checksum is not a valid SHA-256 value. """ - if os.environ.get(LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, "").lower() == "true": + if os.environ.get(_LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, "").lower() == "true": err = ( f"Verified downloads are bypassed. No SHA-256 checksum will be used for " - f"LanguageTool {version_name}. Set {LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR}=" + f"LanguageTool {version_name}. Set " + f"{_LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR}=" f"false to re-enable verification." ) warn(err, RuntimeWarning, stacklevel=2) @@ -168,8 +176,8 @@ def _get_zip_hash(version_name: str) -> str | None: version_env_var = f"LTP_DOWNLOAD_SHA256_{suffix}" configured = ( (os.environ.get(version_env_var), version_env_var), - (os.environ.get(LTP_DOWNLOAD_SHA256_ENV_VAR), LTP_DOWNLOAD_SHA256_ENV_VAR), - (EXPECTED_DOWNLOAD_SHA256.get(version_name), f"manifest:{version_name}"), + (os.environ.get(_LTP_DOWNLOAD_SHA256_ENV_VAR), _LTP_DOWNLOAD_SHA256_ENV_VAR), + (_EXPECTED_DOWNLOAD_SHA256.get(version_name), f"manifest:{version_name}"), ) for checksum, source in configured: if checksum: @@ -203,22 +211,22 @@ def _validate_download_size(content_length: str | None) -> int | None: err = f"Invalid Content-Length header: {content_length!r}." raise PathError(err) - if total > MAX_DOWNLOAD_BYTES: + if total > _MAX_DOWNLOAD_BYTES: err = ( f"Refusing to download {total} bytes. " - f"Maximum allowed download size is {MAX_DOWNLOAD_BYTES} bytes." + f"Maximum allowed download size is {_MAX_DOWNLOAD_BYTES} bytes." ) raise PathError(err) return total -def parse_java_version(version_text: str) -> tuple[int, int]: +def _parse_java_version(version_text: str) -> tuple[int, int]: """Parse the Java version from a given version text. This function attempts to extract the major version numbers from the provided Java version string using regular expressions. It supports two different version formats - defined by JAVA_VERSION_REGEX and JAVA_VERSION_REGEX_UPDATED. + defined by _JAVA_VERSION_REGEX and _JAVA_VERSION_REGEX_UPDATED. :param version_text: The Java version string to parse. :type version_text: str @@ -226,8 +234,8 @@ def parse_java_version(version_text: str) -> tuple[int, int]: :rtype: tuple[int, int] :raises SystemExit: If the version string cannot be parsed. """ - match = re.search(JAVA_VERSION_REGEX, version_text) or re.search( - JAVA_VERSION_REGEX_UPDATED, + match = re.search(_JAVA_VERSION_REGEX, version_text) or re.search( + _JAVA_VERSION_REGEX_UPDATED, version_text, ) if not match: @@ -238,7 +246,7 @@ def parse_java_version(version_text: str) -> tuple[int, int]: return (major1, major2) -def confirm_java_compatibility( +def _confirm_java_compatibility( language_tool_version: str = LTP_DOWNLOAD_VERSION, ) -> None: """Confirm that the installed Java version is compatible with language-tool-python. @@ -270,7 +278,7 @@ def confirm_java_compatibility( logger.debug("java -version output: %s", output.strip()) - major_version, minor_version = parse_java_version(output) + major_version, minor_version = _parse_java_version(output) is_old_version = language_tool_version != LTP_DOWNLOAD_VERSION and ( re.match(r"^\d+\.\d+$", language_tool_version) @@ -284,10 +292,10 @@ def confirm_java_compatibility( if is_old_version: if ( major_version == 1 - and minor_version < MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL + and minor_version < _MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL ) or ( major_version != 1 - and major_version < MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL + and major_version < _MIN_JAVA_VERSION_FOR_OLD_LANGUAGE_TOOL ): err = ( f"Detected java {major_version}.{minor_version}. LanguageTool " @@ -296,10 +304,10 @@ def confirm_java_compatibility( raise SystemError(err) elif ( major_version == 1 - and minor_version < MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL + and minor_version < _MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL ) or ( major_version != 1 - and major_version < MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL + and major_version < _MIN_JAVA_VERSION_FOR_CURRENT_LANGUAGE_TOOL ): err = ( f"Detected java {major_version}.{minor_version}. LanguageTool " @@ -336,7 +344,7 @@ def from_version_name( """ if ( re.match(r"^\d{8}$", version_name) - or version_name == LT_SNAPSHOT_LATEST_VERSION + or version_name == _LT_SNAPSHOT_LATEST_VERSION ): return SnapshotLocalLanguageTool(version_name) if re.match(r"^\d+\.\d+$", version_name): @@ -410,13 +418,13 @@ def _get_remote_zip( except requests.exceptions.Timeout as e: err = f"Request to {self.download_url} timed out." raise TimeoutError(err) from e - if req.status_code == HTTP_STATUS_NOT_FOUND: + if req.status_code == _HTTP_STATUS_NOT_FOUND: err = ( f"Could not find at URL {self.download_url}. " f"The given version may not exist or is no longer available." ) raise PathError(err) - if req.status_code == HTTP_STATUS_FORBIDDEN: + if req.status_code == _HTTP_STATUS_FORBIDDEN: err = ( f"Access forbidden to URL {self.download_url}. " f"You may not have permission to access this resource. " @@ -424,7 +432,7 @@ def _get_remote_zip( f"proxy settings)." ) raise PathError(err) - if req.status_code != HTTP_STATUS_OK: + if req.status_code != _HTTP_STATUS_OK: err = ( f"Failed to download from {self.download_url}. " f"HTTP status code: {req.status_code}." @@ -439,13 +447,13 @@ def _get_remote_zip( desc=f"Downloading LanguageTool {self.version_name}", ) downloaded_bytes = 0 - for chunk in req.iter_content(chunk_size=DOWNLOAD_CHUNK_BYTES): + for chunk in req.iter_content(chunk_size=_DOWNLOAD_CHUNK_BYTES): if chunk: # filter out keep-alive new chunks downloaded_bytes += len(chunk) - if downloaded_bytes > MAX_DOWNLOAD_BYTES: + if downloaded_bytes > _MAX_DOWNLOAD_BYTES: progress.close() err = ( - f"Refusing to download more than {MAX_DOWNLOAD_BYTES} bytes " + f"Refusing to download more than {_MAX_DOWNLOAD_BYTES} bytes " f"from {self.download_url}." ) raise PathError(err) @@ -730,7 +738,7 @@ def download(self) -> None: :raises PathError: If the version is unsupported, the download fails, checksum validation fails, or ZIP extraction is unsafe. """ - confirm_java_compatibility(self._version_name) + _confirm_java_compatibility(self._version_name) download_folder = get_language_tool_download_path() @@ -787,14 +795,14 @@ def download_url(self) -> str: :raises PathError: If the version is below 4.0 (unsupported). """ version_num = version_tuple(self._version_name) - filename = FILENAME_RELEASE.format(version=self._version_name) + filename = _FILENAME_RELEASE.format(version=self._version_name) # Versions >= 6.7 from new release page if version_num >= (6, 7): # 6.7 - base_url = BASE_URL_NEW_RELEASES.format(version=self._version_name) + base_url = _BASE_URL_NEW_RELEASES.format(version=self._version_name) return urljoin(base_url, filename) # Versions >= 6.0 from main download page if version_num >= (6, 0): # 6.0 - return urljoin(BASE_URL_RELEASE, filename) + return urljoin(_BASE_URL_RELEASE, filename) if version_num < (4, 0): # 4.0 err = ( "LanguageTool versions below 4.0 are no longer supported for download." @@ -803,7 +811,7 @@ def download_url(self) -> str: ) raise PathError(err) # Versions < 6.0 from archive - return urljoin(BASE_URL_ARCHIVE, filename) + return urljoin(_BASE_URL_ARCHIVE, filename) class SnapshotLocalLanguageTool(LocalLanguageTool): @@ -823,7 +831,7 @@ def __init__(self, version_name: str) -> None: self._version_name = version_name self._install_version_name = ( datetime.now(timezone.utc).strftime("%Y%m%d") - if version_name == LT_SNAPSHOT_LATEST_VERSION + if version_name == _LT_SNAPSHOT_LATEST_VERSION else version_name ) @@ -839,7 +847,7 @@ def download(self) -> None: :raises PathError: If the download fails, checksum validation fails, ZIP extraction is unsafe, or the extracted snapshot layout is invalid. """ - confirm_java_compatibility(self._version_name) + _confirm_java_compatibility(self._version_name) download_folder = get_language_tool_download_path() @@ -920,5 +928,5 @@ def download_url(self) -> str: :return: The download URL for this snapshot. :rtype: str """ - filename = FILENAME_SNAPSHOT.format(version=self._version_name) - return urljoin(BASE_URL_SNAPSHOT, filename) + filename = _FILENAME_SNAPSHOT.format(version=self._version_name) + return urljoin(_BASE_URL_SNAPSHOT, filename) diff --git a/language_tool_python/exceptions.py b/language_tool_python/exceptions.py index 17565b1..01c163c 100644 --- a/language_tool_python/exceptions.py +++ b/language_tool_python/exceptions.py @@ -1,5 +1,13 @@ """Exceptions used in the language_tool_python library.""" +__all__ = [ + "JavaError", + "LanguageToolError", + "PathError", + "RateLimitError", + "ServerError", +] + class LanguageToolError(Exception): """Exception raised for errors in the LanguageTool library. diff --git a/language_tool_python/language_tag.py b/language_tool_python/language_tag.py index 8bac161..0ee8d1f 100644 --- a/language_tool_python/language_tag.py +++ b/language_tool_python/language_tag.py @@ -5,6 +5,8 @@ from collections.abc import Iterable from functools import total_ordering +__all__ = ["LanguageTag"] + logger = logging.getLogger(__name__) diff --git a/language_tool_python/match.py b/language_tool_python/match.py index acc4030..390fe2a 100644 --- a/language_tool_python/match.py +++ b/language_tool_python/match.py @@ -7,22 +7,24 @@ from collections import OrderedDict from collections import OrderedDict as OrderedDictType from functools import total_ordering -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeGuard if TYPE_CHECKING: from collections.abc import Iterator - from .api_types import CheckMatch + from ._internals.api_types import CheckMatch + +__all__ = ["Match", "is_check_match"] logger = logging.getLogger(__name__) -UTF8_4_BYTE_LENGTH = 4 -CONTEXT_PREFIX_SUFFIX_LENGTH = 3 -CONTEXT_WITH_ADDITIONS_MIN_LENGTH = 6 -MatchValue = str | int | list[str] +_UTF8_4_BYTE_LENGTH = 4 +_CONTEXT_PREFIX_SUFFIX_LENGTH = 3 +_CONTEXT_WITH_ADDITIONS_MIN_LENGTH = 6 +_MatchValue = str | int | list[str] -def get_match_ordered_dict() -> OrderedDictType[str, type]: +def _get_match_ordered_dict() -> OrderedDictType[str, type]: """Return an ordered dictionary with predefined keys and their corresponding types. :return: An OrderedDict where each key is a string representing a specific attribute @@ -58,7 +60,7 @@ def get_match_ordered_dict() -> OrderedDictType[str, type]: ) -def four_byte_char_positions(text: str) -> list[int]: +def _four_byte_char_positions(text: str) -> list[int]: """Identify positions of 4-byte encoded characters in a UTF-8 string. This function scans through the input text and identifies the positions of @@ -74,7 +76,7 @@ def four_byte_char_positions(text: str) -> list[int]: positions: list[int] = [] char_index = 0 for char in text: - if len(char.encode("utf-8")) == UTF8_4_BYTE_LENGTH: + if len(char.encode("utf-8")) == _UTF8_4_BYTE_LENGTH: positions.append(char_index) # Adding 1 to the index because 4 byte characters are # 2 bytes in length in LanguageTool, instead of 1 byte in Python. @@ -85,6 +87,32 @@ def four_byte_char_positions(text: str) -> list[int]: return positions +def is_check_match(value: object) -> TypeGuard[CheckMatch]: + """Verify that a value is a CheckMatch. + + :param value: The value to check. + :type value: object + :return: TypeGuard indicating whether the value is a CheckMatch. + :rtype: TypeGuard[CheckMatch] + """ + if not isinstance(value, dict): + return False + + return ( + isinstance(value.get("message"), str) + and isinstance(value.get("shortMessage"), str) + and isinstance(value.get("replacements"), list) + and isinstance(value.get("offset"), int) + and isinstance(value.get("length"), int) + and isinstance(value.get("context"), dict) + and isinstance(value.get("sentence"), str) + and isinstance(value.get("type"), dict) + and isinstance(value.get("rule"), dict) + and isinstance(value.get("ignoreForIncompleteSentence"), bool) + and isinstance(value.get("contextForSureMatch"), int) + ) + + @total_ordering class Match: # noqa: PLW1641 # Doesn't implement hash because it's mutable """Represent a language rule violation match. @@ -215,7 +243,7 @@ def __init__(self, attrib: CheckMatch, text: str) -> None: if text != Match.PREVIOUS_MATCHES_TEXT: Match.PREVIOUS_MATCHES_TEXT = text - Match.FOUR_BYTES_POSITIONS = four_byte_char_positions(text) + Match.FOUR_BYTES_POSITIONS = _four_byte_char_positions(text) # Get the positions of 4-byte encoded characters in the text because without # carrying out this step, the offsets of the matches could be incorrect. if Match.FOUR_BYTES_POSITIONS is not None: @@ -223,7 +251,7 @@ def __init__(self, attrib: CheckMatch, text: str) -> None: 1 for pos in Match.FOUR_BYTES_POSITIONS if pos < self.offset ) - def _ordered_items(self) -> list[tuple[str, MatchValue]]: + def _ordered_items(self) -> list[tuple[str, _MatchValue]]: """Return public match attributes in the documented order.""" return [ ("rule_id", self.rule_id), @@ -312,8 +340,8 @@ def get_line_and_column(self, original_text: str) -> tuple[int, int]: :raises ValueError: If the original text does not contain the match context. """ context_without_additions = ( - self.context[CONTEXT_PREFIX_SUFFIX_LENGTH:-CONTEXT_PREFIX_SUFFIX_LENGTH] - if len(self.context) > CONTEXT_WITH_ADDITIONS_MIN_LENGTH + self.context[_CONTEXT_PREFIX_SUFFIX_LENGTH:-_CONTEXT_PREFIX_SUFFIX_LENGTH] + if len(self.context) > _CONTEXT_WITH_ADDITIONS_MIN_LENGTH else self.context ) if context_without_additions not in original_text.replace("\n", " "): @@ -366,7 +394,7 @@ def __lt__(self, other: object) -> bool: return NotImplemented return list(self) < list(other) - def __iter__(self) -> Iterator[MatchValue]: + def __iter__(self) -> Iterator[_MatchValue]: """Return an iterator over the attributes of the match object. This method allows the match object to be iterated over, yielding the values of @@ -377,7 +405,7 @@ def __iter__(self) -> Iterator[MatchValue]: """ return iter(value for _, value in self._ordered_items()) - def __setattr__(self, key: str, value: MatchValue) -> None: + def __setattr__(self, key: str, value: _MatchValue) -> None: """Set an attribute on the instance. This method overrides the default behavior of setting an attribute. It attempts @@ -393,7 +421,7 @@ def __setattr__(self, key: str, value: MatchValue) -> None: try: # Ex: if key is "offset", get_match_ordered_dict()[key] will return int, so # the value will be transformed to int - value = get_match_ordered_dict()[key](value) + value = _get_match_ordered_dict()[key](value) except KeyError: return super().__setattr__(key, value) @@ -413,6 +441,6 @@ class tree for self). This method checks if the attribute name is in the ordered :rtype: None :raises AttributeError: If the attribute does not exist. """ - if name not in get_match_ordered_dict(): + if name not in _get_match_ordered_dict(): err = f"{self.__class__.__name__!r} object has no attribute {name!r}" raise AttributeError(err) diff --git a/language_tool_python/server.py b/language_tool_python/server.py index a9dda64..db8419c 100644 --- a/language_tool_python/server.py +++ b/language_tool_python/server.py @@ -20,10 +20,17 @@ import psutil import requests -from .api_types import ( +from ._internals.api_types import ( is_check_response, is_language_info, ) +from ._internals.utils import ( + FAILSAFE_LANGUAGE, + get_locale_language, + kill_process_force, + parse_url, + version_tuple, +) from .config_file import LanguageToolConfig from .download_lt import LTP_DOWNLOAD_VERSION, LocalLanguageTool from .exceptions import ( @@ -34,19 +41,12 @@ ) from .language_tag import LanguageTag from .match import Match -from .utils import ( - FAILSAFE_LANGUAGE, - correct, - get_locale_language, - kill_process_force, - parse_url, - version_tuple, -) +from .utils import correct -startupinfo: object | None = None +_startupinfo: object | None = None if sys.platform == "win32": - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + _startupinfo = subprocess.STARTUPINFO() + _startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW if TYPE_CHECKING: from collections.abc import Mapping @@ -55,13 +55,15 @@ from .config_file import ConfigValue +__all__ = ["LanguageTool", "LanguageToolPublicAPI"] + logger = logging.getLogger(__name__) -HTTP_STATUS_RATE_LIMIT = 426 +_HTTP_STATUS_RATE_LIMIT = 426 # Keep track of running server PIDs in a global list. This way, # we can ensure they're killed on exit. -RUNNING_SERVER_PROCESSES: list[subprocess.Popen[str]] = [] +_RUNNING_SERVER_PROCESSES: list[subprocess.Popen[str]] = [] def _kill_processes(processes: list[subprocess.Popen[str]]) -> None: @@ -1024,7 +1026,7 @@ def _query_server( e, ) logger.debug("Status code: %s", response.status_code) - if response.status_code == HTTP_STATUS_RATE_LIMIT: + if response.status_code == _HTTP_STATUS_RATE_LIMIT: err = ( "You have exceeded the rate limit for the free " "LanguageTool API. Please try again later." @@ -1110,9 +1112,9 @@ def _start_local_server(self) -> None: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, text=True, - startupinfo=startupinfo, + startupinfo=_startupinfo, ) - RUNNING_SERVER_PROCESSES.append(self._server) + _RUNNING_SERVER_PROCESSES.append(self._server) self._wait_for_server_ready() @@ -1186,7 +1188,7 @@ def _terminate_server(self) -> None: if self._server: logger.info("Terminating LanguageTool server on port %s", self._port) _kill_processes([self._server]) - RUNNING_SERVER_PROCESSES.remove(self._server) + _RUNNING_SERVER_PROCESSES.remove(self._server) if self._server.stdin: self._server.stdin.close() @@ -1236,15 +1238,15 @@ def __init__( @atexit.register -def terminate_server() -> None: +def _terminate_server_at_exit() -> None: """Terminates all running server processes. This function iterates over the list of running server processes and forcefully kills each process by its PID. """ - if RUNNING_SERVER_PROCESSES: + if _RUNNING_SERVER_PROCESSES: logger.info( "Terminating %d LanguageTool server process(es) at exit", - len(RUNNING_SERVER_PROCESSES), + len(_RUNNING_SERVER_PROCESSES), ) - _kill_processes(RUNNING_SERVER_PROCESSES) + _kill_processes(_RUNNING_SERVER_PROCESSES) diff --git a/language_tool_python/utils.py b/language_tool_python/utils.py index 34c9294..40d08ac 100644 --- a/language_tool_python/utils.py +++ b/language_tool_python/utils.py @@ -2,110 +2,13 @@ from __future__ import annotations -import contextlib -import locale -import logging -import math -import os -import urllib.parse from enum import Enum -from pathlib import Path -from typing import TYPE_CHECKING, Protocol, runtime_checkable - -import psutil - -from .exceptions import PathError +from typing import TYPE_CHECKING if TYPE_CHECKING: from .match import Match -logger = logging.getLogger(__name__) - -JAR_NAMES = [ - "languagetool-server.jar", - "LanguageTool.jar", -] -FAILSAFE_LANGUAGE = "en" - -LTP_PATH_ENV_VAR = "LTP_PATH" # LanguageTool download path - -# Directory containing the LanguageTool jar file: -LTP_JAR_DIR_PATH_ENV_VAR = "LTP_JAR_DIR_PATH" - - -def parse_url(url_str: str) -> str: - """Parse the given URL string and ensure it has a scheme. - - If the input URL string does not contain 'http', 'http://' is prepended to it. The - function then parses the URL and returns its canonical form. - - :param url_str: The URL string to be parsed. - :type url_str: str - :return: The parsed URL in its canonical form. - :rtype: str - """ - if "http" not in url_str: - url_str = "http://" + url_str - - return urllib.parse.urlparse(url_str).geturl() - - -def get_env_int(env_var: str, default: int) -> int: - """Read a positive integer from the environment. - - :param env_var: Environment variable name. - :type env_var: str - :param default: Value to use when the environment variable is absent. - :type default: int - :return: Configured integer value, or the default. - :rtype: int - :raises PathError: If the configured value is invalid. - """ - configured = os.environ.get(env_var) - - if configured is None: - return default - - try: - value = int(configured) - except ValueError as e: - err = f"Invalid integer configured by {env_var}: {configured!r}." - raise PathError(err) from e - - if value <= 0: - err = f"Invalid integer configured by {env_var}: {configured!r}." - raise PathError(err) - - return value - - -def get_env_float(env_var: str, default: float) -> float: - """Read a positive float from the environment. - - :param env_var: Environment variable name. - :type env_var: str - :param default: Value to use when the environment variable is absent. - :type default: float - :return: Configured float value, or the default. - :rtype: float - :raises PathError: If the configured value is invalid. - """ - configured = os.environ.get(env_var) - - if configured is None: - return default - - try: - value = float(configured) - except ValueError as e: - err = f"Invalid float configured by {env_var}: {configured!r}." - raise PathError(err) from e - - if not math.isfinite(value) or value <= 0: - err = f"Invalid float configured by {env_var}: {configured!r}." - raise PathError(err) - - return value +__all__ = ["TextStatus", "classify_matches", "correct"] class TextStatus(Enum): @@ -165,101 +68,3 @@ def correct(text: str, matches: list[Match]) -> str: ltext[frompos:topos] = list(repl) correct_offset += len(repl) - len(errors[n]) return "".join(ltext) - - -def get_language_tool_download_path() -> Path: - """Get the download path for LanguageTool. - - This function retrieves the download path for LanguageTool from the environment - variable specified by ``LTP_PATH_ENV_VAR``. If the environment variable is not set, - it defaults to a path in the user's home directory under - ``.cache/language_tool_python``. The function ensures that the directory exists - before returning it. - - :return: The download path for LanguageTool. - :rtype: Path - """ - # Get download path from environment or use default. - path_str = os.environ.get( - LTP_PATH_ENV_VAR, - str(Path.home() / ".cache" / "language_tool_python"), - ) - path = Path(path_str) - path.mkdir(parents=True, exist_ok=True) - return path - - -def get_locale_language() -> str: - """Get the current locale language. - - This function retrieves the current locale language setting of the system. It first - attempts to get the locale using ``locale.getlocale()``. If that fails, it falls - back to using ``locale.getdefaultlocale()``. If both methods fail to provide a valid - language code, it returns a default failsafe language code. - - :return: The language code of the current locale. - :rtype: str - """ - return locale.getlocale()[0] or locale.getdefaultlocale()[0] or FAILSAFE_LANGUAGE - - -def kill_process_force( - *, - pid: int | None = None, - proc: psutil.Process | None = None, -) -> None: - """Terminate a process and all of its child processes forcefully. - - This function attempts to kill a process specified either by its PID or by a - psutil.Process object. If the process has any child processes, they will be killed - first. - - :param pid: The process ID of the process to be killed. Either ``pid`` or ``proc`` - must be provided. - :type pid: int | None - :param proc: A psutil.Process object representing the process to be killed. Either - ``pid`` or ``proc`` must be provided. - :type proc: psutil.Process | None - :raises ValueError: If neither ``pid`` nor ``proc`` is provided. - """ - if not any([pid, proc]): - err = "Must pass either pid or proc" - raise ValueError(err) - try: - proc = psutil.Process(pid) if proc is None else proc - except psutil.NoSuchProcess: - logger.debug("Process %s does not exist, nothing to kill", pid) - return - logger.debug("Killing process %s and its children", proc.pid) - for child in proc.children(recursive=True): - with contextlib.suppress(psutil.NoSuchProcess): - logger.debug("Killing child process %s", child.pid) - child.kill() - with contextlib.suppress(psutil.NoSuchProcess): - proc.kill() - - -@runtime_checkable -class SupportsBool(Protocol): - """Protocol for types that can be converted to a boolean value.""" - - def __bool__(self) -> bool: - """Define the interface for types that can be evaluated in a boolean context.""" - ... - - -def version_tuple(v: str) -> tuple[int, int]: - """Convert a version string into a tuple of integers. - - This function takes a version string in the format 'X.Y' and converts it into a - tuple of integers (X, Y). This can be useful for comparing version numbers. - - :param v: The version string to be converted, expected in the format 'X.Y'. - :type v: str - :return: A tuple of integers representing the version, in the format (X, Y). - :rtype: tuple[int, int] - :raises ValueError: If the version string is not in the expected format or contains - non-integer components. - """ - major, minor = v.split(".") - return int(major), int(minor) diff --git a/tests/test_download.py b/tests/test_download.py index f745aee..d90e749 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -19,9 +19,9 @@ import language_tool_python from language_tool_python import download_lt from language_tool_python.download_lt import ( - LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, - LTP_DOWNLOAD_SHA256_ENV_VAR, - LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, + _LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, + _LTP_DOWNLOAD_SHA256_ENV_VAR, + _LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, LocalLanguageTool, ) from language_tool_python.exceptions import LanguageToolError, PathError @@ -186,7 +186,7 @@ def test_http_get_rejects_oversized_content_length( ) response = MockDownloadResponse(payload) response.headers["Content-Length"] = "2" - monkeypatch.setattr(download_lt, "MAX_DOWNLOAD_BYTES", 1) + monkeypatch.setattr(download_lt, "_MAX_DOWNLOAD_BYTES", 1) with ( patch( @@ -205,11 +205,11 @@ def test_max_download_bytes_uses_env_override( try: with monkeypatch.context() as env: env.setenv( - LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, str(EXPECTED_DOWNLOAD_BYTES_OVERRIDE) + _LTP_MAX_DOWNLOAD_BYTES_ENV_VAR, str(EXPECTED_DOWNLOAD_BYTES_OVERRIDE) ) importlib.reload(download_lt) - assert download_lt.MAX_DOWNLOAD_BYTES == EXPECTED_DOWNLOAD_BYTES_OVERRIDE + assert download_lt._MAX_DOWNLOAD_BYTES == EXPECTED_DOWNLOAD_BYTES_OVERRIDE finally: importlib.reload(download_lt) @@ -223,7 +223,7 @@ def test_http_get_rejects_oversized_stream( ) response = MockDownloadResponse(payload) response.headers = {} - monkeypatch.setattr(download_lt, "MAX_DOWNLOAD_BYTES", len(payload) - 1) + monkeypatch.setattr(download_lt, "_MAX_DOWNLOAD_BYTES", len(payload) - 1) with ( patch( @@ -244,7 +244,7 @@ def test_http_get_rejects_oversized_stream_with_small_content_length( ) response = MockDownloadResponse(payload) response.headers["Content-Length"] = "1" - monkeypatch.setattr(download_lt, "MAX_DOWNLOAD_BYTES", len(payload) - 1) + monkeypatch.setattr(download_lt, "_MAX_DOWNLOAD_BYTES", len(payload) - 1) with ( patch( @@ -280,7 +280,7 @@ def test_latest_snapshot_uses_latest_download_url_and_current_date( """Test that latest remains a snapshot alias installed under the current date.""" monkeypatch.setattr( download_lt, - "BASE_URL_SNAPSHOT", + "_BASE_URL_SNAPSHOT", "https://example.test/snapshots/", ) @@ -303,7 +303,7 @@ def test_release_download_url_uses_new_release_base_from_6_7( """Test that releases 6.7 and newer include the version in the base URL.""" monkeypatch.setattr( download_lt, - "BASE_URL_NEW_RELEASES", + "_BASE_URL_NEW_RELEASES", "https://example.test/releases/LanguageTool-{version}/", ) @@ -323,7 +323,7 @@ def test_release_download_url_keeps_main_release_base_for_6_6( """Test that release 6.6 keeps using the versioned filename.""" monkeypatch.setattr( download_lt, - "BASE_URL_RELEASE", + "_BASE_URL_RELEASE", "https://example.test/download/", ) @@ -341,7 +341,7 @@ def test_release_download_url_keeps_main_release_base_before_6_7( """Test that earlier 6.x releases keep using the versioned filename.""" monkeypatch.setattr( download_lt, - "BASE_URL_RELEASE", + "_BASE_URL_RELEASE", "https://example.test/download/", ) @@ -359,7 +359,7 @@ def test_release_download_url_keeps_archive_base_before_6_0( """Test that older supported releases keep using the archive base URL.""" monkeypatch.setattr( download_lt, - "BASE_URL_ARCHIVE", + "_BASE_URL_ARCHIVE", "https://example.test/archive/", ) @@ -407,8 +407,8 @@ def test_http_get_uses_integrity_manifest_sha256( """Test that bundled integrity.toml checksums are used when no env var is set.""" payload = make_zip_payload({"LanguageTool-4.0/languagetool-server.jar": b"jar"}) local_language_tool = LocalLanguageTool.from_version_name("4.0") - monkeypatch.delenv(LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, raising=False) - monkeypatch.delenv(LTP_DOWNLOAD_SHA256_ENV_VAR, raising=False) + monkeypatch.delenv(_LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, raising=False) + monkeypatch.delenv(_LTP_DOWNLOAD_SHA256_ENV_VAR, raising=False) monkeypatch.delenv("LTP_DOWNLOAD_SHA256_4_0", raising=False) with ( @@ -459,8 +459,8 @@ def test_http_get_bypass_skips_sha256_verification( {"LanguageTool-6.9-SNAPSHOT/languagetool-server.jar": b"jar"}, ) local_language_tool = LocalLanguageTool.from_version_name() - monkeypatch.setenv(LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, "true") - monkeypatch.setenv(LTP_DOWNLOAD_SHA256_ENV_VAR, "0" * 64) + monkeypatch.setenv(_LTP_BYPASS_VERIFIED_DOWNLOADS_ENV_VAR, "true") + monkeypatch.setenv(_LTP_DOWNLOAD_SHA256_ENV_VAR, "0" * 64) out_file = io.BytesIO() with ( @@ -487,7 +487,7 @@ def test_snapshot_download_renames_archive_root_to_requested_date( local_language_tool = LocalLanguageTool.from_version_name(requested_snapshot) monkeypatch.setattr( download_lt, - "confirm_java_compatibility", + "_confirm_java_compatibility", skip_java_compatibility_check, ) @@ -529,7 +529,7 @@ def test_latest_snapshot_download_renames_archive_root_to_current_date( local_language_tool = LocalLanguageTool.from_version_name("latest") monkeypatch.setattr( download_lt, - "confirm_java_compatibility", + "_confirm_java_compatibility", skip_java_compatibility_check, ) diff --git a/tests/test_safe_zip.py b/tests/test_safe_zip.py index a003ccb..9741845 100644 --- a/tests/test_safe_zip.py +++ b/tests/test_safe_zip.py @@ -14,9 +14,9 @@ import pytest -from language_tool_python import safe_zip, utils +from language_tool_python._internals import safe_zip, utils +from language_tool_python._internals.safe_zip import SafeZipExtractor, SafeZipLimits from language_tool_python.exceptions import PathError -from language_tool_python.safe_zip import SafeZipExtractor, SafeZipLimits EXPECTED_MAX_ARCHIVE_BYTES = 11 EXPECTED_MAX_EXTRACTED_BYTES = 22 diff --git a/tests/test_server_local.py b/tests/test_server_local.py index c08b904..3731952 100644 --- a/tests/test_server_local.py +++ b/tests/test_server_local.py @@ -7,8 +7,8 @@ from typing import TYPE_CHECKING import language_tool_python +from language_tool_python._internals.utils import get_language_tool_download_path from language_tool_python.download_lt import LTP_DOWNLOAD_VERSION -from language_tool_python.utils import get_language_tool_download_path if TYPE_CHECKING: import subprocess