Skip to content

Commit 63ccc20

Browse files
authored
refactor: makes internal utilities private (jxmorris12#190)
1 parent 399378d commit 63ccc20

16 files changed

Lines changed: 408 additions & 337 deletions

language_tool_python/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pathlib import Path
1414
from typing import TYPE_CHECKING, TypedDict, cast
1515

16-
from ._compat import toml_loads
16+
from ._internals.compat import toml_loads
1717
from .exceptions import LanguageToolError
1818
from .server import LanguageTool
1919

language_tool_python/_internals/__init__.py

Whitespace-only changes.

language_tool_python/api_types.py renamed to language_tool_python/_internals/api_types.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"LanguageInfo",
1818
"MatchType",
1919
"Replacement",
20+
"ReplacementOptional",
2021
"Rule",
22+
"RuleOptional",
2123
"WarningInfo",
2224
"is_check_response",
2325
"is_language_info",
@@ -50,11 +52,11 @@ def is_language_info(value: object) -> TypeGuard[LanguageInfo]:
5052
)
5153

5254

53-
class _ReplacementOptional(TypedDict, total=False):
55+
class ReplacementOptional(TypedDict, total=False):
5456
shortDescription: str
5557

5658

57-
class Replacement(_ReplacementOptional):
59+
class Replacement(ReplacementOptional):
5860
"""A suggested replacement returned by LanguageTool."""
5961

6062
value: str
@@ -75,12 +77,12 @@ class Category(TypedDict):
7577
name: str
7678

7779

78-
class _RuleOptional(TypedDict, total=False):
80+
class RuleOptional(TypedDict, total=False):
7981
sourceFile: str
8082
subId: str
8183

8284

83-
class Rule(_RuleOptional):
85+
class Rule(RuleOptional):
8486
"""LanguageTool rule metadata for a match."""
8587

8688
id: str

language_tool_python/safe_zip.py renamed to language_tool_python/_internals/safe_zip.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
from pathlib import Path, PurePosixPath
1313
from typing import TYPE_CHECKING
1414

15-
from .exceptions import PathError
16-
from .utils import get_env_float, get_env_int
15+
from language_tool_python._internals.utils import get_env_float, get_env_int
16+
from language_tool_python.exceptions import PathError
1717

1818
if TYPE_CHECKING:
1919
import zipfile
2020

21+
__all__ = ["SafeZipExtractor", "SafeZipLimits"]
22+
2123
logger = logging.getLogger(__name__)
2224

2325
CONTROL_CHARACTER_MAX = 32
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import contextlib
2+
import locale
3+
import logging
4+
import math
5+
import os
6+
import urllib.parse
7+
from pathlib import Path
8+
from typing import Protocol, runtime_checkable
9+
10+
import psutil
11+
12+
from language_tool_python.exceptions import PathError
13+
14+
__all__ = [
15+
"SupportsBool",
16+
"get_env_float",
17+
"get_env_int",
18+
"get_language_tool_download_path",
19+
"get_locale_language",
20+
"kill_process_force",
21+
"parse_url",
22+
"version_tuple",
23+
]
24+
25+
logger = logging.getLogger(__name__)
26+
27+
JAR_NAMES = [
28+
"languagetool-server.jar",
29+
"LanguageTool.jar",
30+
]
31+
FAILSAFE_LANGUAGE = "en"
32+
33+
LTP_PATH_ENV_VAR = "LTP_PATH" # LanguageTool download path
34+
35+
# Directory containing the LanguageTool jar file:
36+
LTP_JAR_DIR_PATH_ENV_VAR = "LTP_JAR_DIR_PATH"
37+
38+
39+
def parse_url(url_str: str) -> str:
40+
"""Parse the given URL string and ensure it has a scheme.
41+
42+
If the input URL string does not contain 'http', 'http://' is prepended to it. The
43+
function then parses the URL and returns its canonical form.
44+
45+
:param url_str: The URL string to be parsed.
46+
:type url_str: str
47+
:return: The parsed URL in its canonical form.
48+
:rtype: str
49+
"""
50+
if "http" not in url_str:
51+
url_str = "http://" + url_str
52+
53+
return urllib.parse.urlparse(url_str).geturl()
54+
55+
56+
def get_env_int(env_var: str, default: int) -> int:
57+
"""Read a positive integer from the environment.
58+
59+
:param env_var: Environment variable name.
60+
:type env_var: str
61+
:param default: Value to use when the environment variable is absent.
62+
:type default: int
63+
:return: Configured integer value, or the default.
64+
:rtype: int
65+
:raises PathError: If the configured value is invalid.
66+
"""
67+
configured = os.environ.get(env_var)
68+
69+
if configured is None:
70+
return default
71+
72+
try:
73+
value = int(configured)
74+
except ValueError as e:
75+
err = f"Invalid integer configured by {env_var}: {configured!r}."
76+
raise PathError(err) from e
77+
78+
if value <= 0:
79+
err = f"Invalid integer configured by {env_var}: {configured!r}."
80+
raise PathError(err)
81+
82+
return value
83+
84+
85+
def get_env_float(env_var: str, default: float) -> float:
86+
"""Read a positive float from the environment.
87+
88+
:param env_var: Environment variable name.
89+
:type env_var: str
90+
:param default: Value to use when the environment variable is absent.
91+
:type default: float
92+
:return: Configured float value, or the default.
93+
:rtype: float
94+
:raises PathError: If the configured value is invalid.
95+
"""
96+
configured = os.environ.get(env_var)
97+
98+
if configured is None:
99+
return default
100+
101+
try:
102+
value = float(configured)
103+
except ValueError as e:
104+
err = f"Invalid float configured by {env_var}: {configured!r}."
105+
raise PathError(err) from e
106+
107+
if not math.isfinite(value) or value <= 0:
108+
err = f"Invalid float configured by {env_var}: {configured!r}."
109+
raise PathError(err)
110+
111+
return value
112+
113+
114+
def get_language_tool_download_path() -> Path:
115+
"""Get the download path for LanguageTool.
116+
117+
This function retrieves the download path for LanguageTool from the environment
118+
variable specified by ``LTP_PATH_ENV_VAR``. If the environment variable is not set,
119+
it defaults to a path in the user's home directory under
120+
``.cache/language_tool_python``. The function ensures that the directory exists
121+
before returning it.
122+
123+
:return: The download path for LanguageTool.
124+
:rtype: Path
125+
"""
126+
# Get download path from environment or use default.
127+
path_str = os.environ.get(
128+
LTP_PATH_ENV_VAR,
129+
str(Path.home() / ".cache" / "language_tool_python"),
130+
)
131+
path = Path(path_str)
132+
path.mkdir(parents=True, exist_ok=True)
133+
return path
134+
135+
136+
def get_locale_language() -> str:
137+
"""Get the current locale language.
138+
139+
This function retrieves the current locale language setting of the system. It first
140+
attempts to get the locale using ``locale.getlocale()``. If that fails, it falls
141+
back to using ``locale.getdefaultlocale()``. If both methods fail to provide a valid
142+
language code, it returns a default failsafe language code.
143+
144+
:return: The language code of the current locale.
145+
:rtype: str
146+
"""
147+
return locale.getlocale()[0] or locale.getdefaultlocale()[0] or FAILSAFE_LANGUAGE
148+
149+
150+
def kill_process_force(
151+
*,
152+
pid: int | None = None,
153+
proc: psutil.Process | None = None,
154+
) -> None:
155+
"""Terminate a process and all of its child processes forcefully.
156+
157+
This function attempts to kill a process specified either by its PID or by a
158+
psutil.Process object. If the process has any child processes, they will be killed
159+
first.
160+
161+
:param pid: The process ID of the process to be killed. Either ``pid`` or ``proc``
162+
must be provided.
163+
:type pid: int | None
164+
:param proc: A psutil.Process object representing the process to be killed. Either
165+
``pid`` or ``proc`` must be provided.
166+
:type proc: psutil.Process | None
167+
:raises ValueError: If neither ``pid`` nor ``proc`` is provided.
168+
"""
169+
if not any([pid, proc]):
170+
err = "Must pass either pid or proc"
171+
raise ValueError(err)
172+
try:
173+
proc = psutil.Process(pid) if proc is None else proc
174+
except psutil.NoSuchProcess:
175+
logger.debug("Process %s does not exist, nothing to kill", pid)
176+
return
177+
logger.debug("Killing process %s and its children", proc.pid)
178+
for child in proc.children(recursive=True):
179+
with contextlib.suppress(psutil.NoSuchProcess):
180+
logger.debug("Killing child process %s", child.pid)
181+
child.kill()
182+
with contextlib.suppress(psutil.NoSuchProcess):
183+
proc.kill()
184+
185+
186+
@runtime_checkable
187+
class SupportsBool(Protocol):
188+
"""Protocol for types that can be converted to a boolean value."""
189+
190+
def __bool__(self) -> bool:
191+
"""Define the interface for types that can be evaluated in a boolean context."""
192+
...
193+
194+
195+
def version_tuple(v: str) -> tuple[int, int]:
196+
"""Convert a version string into a tuple of integers.
197+
198+
This function takes a version string in the format 'X.Y' and converts it into a
199+
tuple of integers (X, Y). This can be useful for comparing version numbers.
200+
201+
:param v: The version string to be converted, expected in the format 'X.Y'.
202+
:type v: str
203+
:return: A tuple of integers representing the version, in the format (X, Y).
204+
:rtype: tuple[int, int]
205+
:raises ValueError: If the version string is not in the expected format or contains
206+
non-integer components.
207+
"""
208+
major, minor = v.split(".")
209+
return int(major), int(minor)

language_tool_python/config_file.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@
1111
from pathlib import Path
1212
from typing import TypeVar, cast
1313

14+
from ._internals.utils import SupportsBool
1415
from .exceptions import PathError
15-
from .utils import SupportsBool
16+
17+
__all__ = [
18+
"ConfigValue",
19+
"LanguageToolConfig",
20+
]
1621

1722
ConfigValue = PathLike[str] | SupportsBool | str | int | float | Iterable[str]
18-
ConfigValueT = TypeVar("ConfigValueT", bound=ConfigValue)
23+
_ConfigValueT = TypeVar("_ConfigValueT", bound=ConfigValue)
1924

2025
logger = logging.getLogger(__name__)
2126

22-
LANGUAGE_KEY_PARTS = 2
23-
LANGUAGE_KEY_WITH_DICT_PATH_PARTS = 3
24-
LANGUAGE_DICT_PATH_SEPARATOR_COUNT = 2
27+
_LANGUAGE_KEY_PARTS = 2
28+
_LANGUAGE_KEY_WITH_DICT_PATH_PARTS = 3
29+
_LANGUAGE_DICT_PATH_SEPARATOR_COUNT = 2
2530

2631

2732
def _reject_line_breaks(field_name: str, value: str) -> None:
@@ -45,7 +50,7 @@ def _reject_line_breaks(field_name: str, value: str) -> None:
4550

4651

4752
@dataclass(frozen=True)
48-
class OptionSpec:
53+
class _OptionSpec:
4954
"""Specification for a configuration option.
5055
5156
This class defines the structure and behavior of a configuration option, including
@@ -67,11 +72,11 @@ class OptionSpec:
6772

6873
def _option_spec(
6974
py_types: type[object] | tuple[type[object], ...],
70-
encoder: Callable[[ConfigValueT], str],
71-
validator: Callable[[ConfigValueT], None] | None = None,
72-
) -> OptionSpec:
75+
encoder: Callable[[_ConfigValueT], str],
76+
validator: Callable[[_ConfigValueT], None] | None = None,
77+
) -> _OptionSpec:
7378
"""Create a schema entry for a runtime-checked configuration option."""
74-
return OptionSpec(
79+
return _OptionSpec(
7580
py_types=py_types,
7681
encoder=cast("Callable[[ConfigValue], str]", encoder),
7782
validator=cast("Callable[[ConfigValue], None] | None", validator),
@@ -152,7 +157,7 @@ def _path_validator(v: PathLike[str] | str) -> None:
152157
raise PathError(err)
153158

154159

155-
CONFIG_SCHEMA: dict[str, OptionSpec] = {
160+
_CONFIG_SCHEMA: dict[str, _OptionSpec] = {
156161
"maxTextLength": _option_spec(int, _int_encoder),
157162
"maxTextHardLength": _option_spec(int, _int_encoder),
158163
"maxCheckTimeMillis": _option_spec(int, _int_encoder),
@@ -199,8 +204,8 @@ def _is_lang_key(key: str) -> bool:
199204
return False
200205

201206
parts = key.split("-")
202-
return (len(parts) == LANGUAGE_KEY_PARTS and len(parts[1]) > 0) or ( # lang-<code>
203-
len(parts) == LANGUAGE_KEY_WITH_DICT_PATH_PARTS
207+
return (len(parts) == _LANGUAGE_KEY_PARTS and len(parts[1]) > 0) or ( # lang-<code>
208+
len(parts) == _LANGUAGE_KEY_WITH_DICT_PATH_PARTS
204209
and len(parts[1]) > 0
205210
and parts[2] == "dictPath" # lang-<code>-dictPath
206211
)
@@ -234,7 +239,7 @@ def _encode_config(config: Mapping[str, ConfigValue]) -> dict[str, str]:
234239
_reject_line_breaks(key, encoded[key])
235240
continue
236241
if (
237-
_is_lang_key(key) and key.count("-") == LANGUAGE_DICT_PATH_SEPARATOR_COUNT
242+
_is_lang_key(key) and key.count("-") == _LANGUAGE_DICT_PATH_SEPARATOR_COUNT
238243
): # lang-<code>-dictPath
239244
logger.debug("Encoding language dictPath %s=%r", key, value)
240245
path_value = cast("PathLike[str] | str", value)
@@ -243,7 +248,7 @@ def _encode_config(config: Mapping[str, ConfigValue]) -> dict[str, str]:
243248
_reject_line_breaks(key, encoded[key])
244249
continue
245250

246-
spec = CONFIG_SCHEMA.get(key)
251+
spec = _CONFIG_SCHEMA.get(key)
247252
if spec is None:
248253
err = f"unexpected key in config: {key}"
249254
raise ValueError(err)

0 commit comments

Comments
 (0)