forked from jxmorris12/language_tool_python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.py
More file actions
365 lines (305 loc) Β· 12.9 KB
/
Copy pathutils.py
File metadata and controls
365 lines (305 loc) Β· 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
"""Utility functions for the LanguageTool library."""
import contextlib
import locale
import logging
import os
import subprocess
import urllib.parse
from enum import Enum
from pathlib import Path
from shutil import which
from typing import Any, List, Optional, Tuple
import psutil
from packaging import version
from ._deprecated import deprecated
from .config_file import LanguageToolConfig
from .exceptions import JavaError, PathError
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"
# https://mail.python.org/pipermail/python-dev/2011-July/112551.html
startupinfo: Optional[Any] = None
if os.name == "nt":
# Gets STARTUPINFO dynamically to avoid issues on non-Windows platforms
startupinfo_cls = getattr(subprocess, "STARTUPINFO", None)
if startupinfo_cls is not None:
si = startupinfo_cls()
# STARTF_USESHOWWINDOW also dynamically retrieved
si.dwFlags |= getattr(subprocess, "STARTF_USESHOWWINDOW", 0)
startupinfo = si
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()
class TextStatus(Enum):
CORRECT = "correct"
FAULTY = "faulty"
GARBAGE = "garbage"
def classify_matches(matches: List[Match]) -> TextStatus:
"""
Classify the matches (result of a check on a text) into one of three categories:
CORRECT, FAULTY, or GARBAGE.
This function checks the status of the matches and returns a corresponding
``TextStatus`` value.
:param matches: A list of Match objects to be classified.
:type matches: List[Match]
:return: The classification of the matches as a ``TextStatus`` value.
:rtype: TextStatus
"""
if not len(matches):
return TextStatus.CORRECT
matches = [match for match in matches if match.replacements]
if not len(matches):
return TextStatus.GARBAGE
return TextStatus.FAULTY
def correct(text: str, matches: List[Match]) -> str:
"""
Corrects the given text based on the provided matches.
Only the first replacement for each match is applied to the text.
:param text: The original text to be corrected.
:type text: str
:param matches: A list of Match objects that contain the positions and replacements for errors in the text.
:type matches: List[Match]
:return: The corrected text.
:rtype: str
"""
ltext = list(text)
matches = [match for match in matches if match.replacements]
errors = [
ltext[match.offset : match.offset + match.error_length] for match in matches
]
correct_offset = 0
for n, match in enumerate(matches):
frompos, topos = (
correct_offset + match.offset,
correct_offset + match.offset + match.error_length,
)
if ltext[frompos:topos] != errors[n]:
continue
repl = match.replacements[0]
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
@deprecated(
"This function is no longer used internally and will be removed in 4.0.\nReplace its usage by an inline alternative.",
stacklevel=2,
) # type: ignore
def find_existing_language_tool_downloads(download_folder: Path) -> List[Path]:
"""
Find existing LanguageTool downloads in the specified folder.
This function searches for directories in the given download folder
that match the pattern 'LanguageTool*' and returns a list of their paths.
:param download_folder: The folder where LanguageTool downloads are stored.
:type download_folder: Path
:return: A list of paths to the existing LanguageTool download directories.
:rtype: List[Path]
.. deprecated:: 3.3.0
This function is no longer used internally and will be removed in 4.0.
"""
return [path for path in download_folder.glob("LanguageTool*") if path.is_dir()]
@deprecated(
"This function is no longer used internally and will be removed in 4.0.",
stacklevel=2,
) # type: ignore
def _extract_version(path: Path) -> version.Version:
"""
Extract the version number from a LanguageTool directory path.
This function parses the directory name to extract the version information
from LanguageTool installation folders that follow the naming convention
'LanguageTool-X.Y-SNAPSHOT'.
:param path: The path to the LanguageTool directory
:type path: Path
:return: The parsed version object extracted from the directory name
:rtype: version.Version
:raises ValueError: If the directory name doesn't start with 'LanguageTool-'
.. deprecated:: 3.3.0
This function is no longer used internally and will be removed in 4.0.
"""
if not path.name.startswith("LanguageTool-"):
raise ValueError(f"Invalid LanguageTool folder name: {path.name}")
# Handle LanguageTool- prefix
version_str = path.name.removeprefix("LanguageTool-")
# Handle both -SNAPSHOT and -snapshot suffixes
version_str = version_str.removesuffix("-SNAPSHOT").removesuffix("-snapshot")
return version.parse(version_str)
@deprecated(
"This function is no longer used internally and will be removed in 4.0.\nUse instead language_tool_python.download_lt.LocalLanguageTool.get_latest_installed_version.",
stacklevel=2,
) # type: ignore
def get_language_tool_directory() -> Path:
"""
Get the directory path of the LanguageTool installation.
This function checks the download folder for LanguageTool installations,
verifies that the folder exists and is a directory, and returns the path
to the latest version of LanguageTool found in the directory.
:raises NotADirectoryError: If the download folder path is not a valid directory.
:raises FileNotFoundError: If no LanguageTool installation is found in the download folder.
:return: The path to the latest version of LanguageTool found in the directory.
:rtype: Path
.. deprecated:: 3.3.0
This function is no longer used internally and will be removed in 4.0.
"""
download_folder = get_language_tool_download_path()
if not download_folder.is_dir():
err = f"LanguageTool directory path is not a valid directory {download_folder}."
raise NotADirectoryError(err)
language_tool_path_list = find_existing_language_tool_downloads(download_folder) # type: ignore
if not len(language_tool_path_list):
err = f"LanguageTool not found in {download_folder}."
raise FileNotFoundError(err)
# Return the latest version found in the directory.
latest: Path = max(
language_tool_path_list,
key=_extract_version, # type: ignore
)
logger.debug("Using LanguageTool directory: %s", latest)
return latest
@deprecated(
"This function is no longer used internally and will be removed in 4.0.\nUse instead language_tool_python.download_lt.LocalLanguageTool.get_server_cmd.",
stacklevel=2,
) # type: ignore
def get_server_cmd(
port: Optional[int] = None,
config: Optional[LanguageToolConfig] = None,
) -> List[str]:
"""
Generate the command to start the LanguageTool HTTP server.
:param port: Optional; The port number on which the server should run. If not provided, the default port will be used.
:type port: Optional[int]
:param config: Optional; The configuration for the LanguageTool server. If not provided, default configuration will be used.
:type config: Optional[LanguageToolConfig]
:return: A list of command line arguments to start the LanguageTool HTTP server.
:rtype: List[str]
.. deprecated:: 3.3.0
This function is no longer used internally and will be removed in 4.0.
"""
java_path, jar_path = get_jar_info() # type: ignore
cmd = [
str(java_path),
"-cp",
str(jar_path),
"org.languagetool.server.HTTPServer",
]
if port is not None:
cmd += ["-p", str(port)]
if config is not None:
cmd += ["--config", config.path]
logger.debug("LanguageTool server command: %r", cmd)
return cmd
@deprecated(
"This function is no longer used internally and will be removed in 4.0.",
stacklevel=2,
) # type: ignore
def get_jar_info() -> Tuple[Path, Path]:
"""
Retrieve the path to the Java executable and the LanguageTool JAR file.
This function searches for the Java executable in the system's PATH and
locates the LanguageTool JAR file either in a directory specified by an
environment variable or in a default download directory.
:raises JavaError: If the Java executable cannot be found.
:raises PathError: If the LanguageTool JAR file cannot be found in the specified directory.
:return: A tuple containing the path to the Java executable and the path to the LanguageTool JAR file.
:rtype: Tuple[Path, Path]
.. deprecated:: 3.3.0
This function is no longer used internally and will be removed in 4.0.
"""
java_path_str = which("java")
if not java_path_str:
err = "can't find Java"
raise JavaError(err)
java_path = Path(java_path_str)
# Use the env var to the jar directory if it is defined
# otherwise look in the download directory
jar_dir_name = os.environ.get(
LTP_JAR_DIR_PATH_ENV_VAR,
get_language_tool_directory(), # type: ignore
)
jar_path = None
for jar_name in JAR_NAMES:
for jar_path in Path(jar_dir_name).glob(jar_name):
if jar_path.is_file():
logger.debug("Found LanguageTool JAR: %s", jar_path)
break
else:
jar_path = None
if jar_path:
break
else:
err = f"can't find languagetool-standalone in {jar_dir_name!r}"
raise PathError(err)
return java_path, jar_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: Optional[int] = None,
proc: Optional[psutil.Process] = None,
) -> None:
"""
Forcefully kills a process and all its child processes.
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: Optional[int]
:param proc: A psutil.Process object representing the process to be killed. Either ``pid`` or ``proc`` must be provided.
:type proc: Optional[psutil.Process]
: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()