Skip to content

Commit 5b982ba

Browse files
authored
Merge pull request jxmorris12#108 from mdevolde/master
Correction of tests, integration of new versions
2 parents f6df6b4 + e3e11ce commit 5b982ba

File tree

9 files changed

+237
-176
lines changed

9 files changed

+237
-176
lines changed

language_tool_python/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LanguageTool API for Python."""
2+
13
from .language_tag import LanguageTag
24
from .match import Match
35
from .server import LanguageTool, LanguageToolPublicAPI

language_tool_python/config_file.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Module for configuring LanguageTool's local server."""
2+
13
from typing import Any, Dict
24

35
import atexit

language_tool_python/download_lt.py

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LanguageTool download module."""
2+
13
import logging
24
import os
35
import re
@@ -7,6 +9,7 @@
79
import tqdm
810
from typing import IO, Dict, Optional, Tuple
911
import zipfile
12+
from datetime import datetime
1013

1114
from shutil import which
1215
from urllib.parse import urljoin
@@ -24,10 +27,13 @@
2427

2528

2629
# Get download host from environment or default.
27-
BASE_URL = os.environ.get('LTP_DOWNLOAD_HOST', 'https://www.languagetool.org/download/')
28-
FILENAME = 'LanguageTool-{version}.zip'
30+
BASE_URL_SNAPSHOT = os.environ.get('LTP_DOWNLOAD_HOST_SNAPSHOT', 'https://internal1.languagetool.org/snapshots/')
31+
FILENAME_SNAPSHOT = 'LanguageTool-{version}-snapshot.zip'
32+
BASE_URL_RELEASE = os.environ.get('LTP_DOWNLOAD_HOST_RELEASE', 'https://www.languagetool.org/download/')
33+
FILENAME_RELEASE = 'LanguageTool-{version}.zip'
2934

30-
LTP_DOWNLOAD_VERSION = '6.6'
35+
LTP_DOWNLOAD_VERSION = 'latest'
36+
LT_SNAPSHOT_CURRENT_VERSION = '6.7-SNAPSHOT'
3137

3238
JAVA_VERSION_REGEX = re.compile(
3339
r'^(?:java|openjdk) version "(?P<major1>\d+)(|\.(?P<major2>\d+)\.[^"]+)"',
@@ -63,16 +69,16 @@ def parse_java_version(version_text: str) -> Tuple[int, int]:
6369
return (major1, major2)
6470

6571

66-
def confirm_java_compatibility() -> bool:
72+
def confirm_java_compatibility(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) -> None:
6773
"""
6874
Confirms if the installed Java version is compatible with language-tool-python.
69-
This function checks if Java is installed and verifies that the major version is at least 8.
75+
This function checks if Java is installed and verifies that the major version is at least 8 or 17 (depending on the LanguageTool version).
7076
It raises an error if Java is not installed or if the version is incompatible.
7177
78+
:param language_tool_version: The version of LanguageTool to check compatibility for.
79+
:type language_tool_version: Optional[str]
7280
:raises ModuleNotFoundError: If no Java installation is detected.
73-
:raises SystemError: If the detected Java version is less than 8.
74-
:return: True if the Java version is compatible.
75-
:rtype: bool
81+
:raises SystemError: If the detected Java version is less than the required version.
7682
"""
7783

7884
java_path = which('java')
@@ -87,17 +93,24 @@ def confirm_java_compatibility() -> bool:
8793
universal_newlines=True)
8894

8995
major_version, minor_version = parse_java_version(output)
96+
version_date_cutoff = datetime.strptime('2025-03-27', '%Y-%m-%d')
97+
is_old_version = (
98+
language_tool_version != 'latest' and (
99+
(re.match(r'^\d+\.\d+$', language_tool_version) and language_tool_version < '6.6') or
100+
(re.match(r'^\d{8}$', language_tool_version) and datetime.strptime(language_tool_version, '%Y%m%d') < version_date_cutoff)
101+
)
102+
)
103+
90104
# Some installs of java show the version number like `14.0.1`
91105
# and others show `1.14.0.1`
92-
# (with a leading 1). We want to support both,
93-
# as long as the major version is >= 8.
106+
# (with a leading 1). We want to support both.
94107
# (See softwareengineering.stackexchange.com/questions/175075/why-is-java-version-1-x-referred-to-as-java-x)
95-
if major_version == 1 and minor_version >= 8:
96-
return True
97-
elif major_version >= 8:
98-
return True
108+
if is_old_version:
109+
if (major_version == 1 and minor_version < 8) or (major_version != 1 and major_version < 8):
110+
raise SystemError(f'Detected java {major_version}.{minor_version}. LanguageTool requires Java >= 8 for version {language_tool_version}.')
99111
else:
100-
raise SystemError(f'Detected java {major_version}.{minor_version}. LanguageTool requires Java >= 8.')
112+
if (major_version == 1 and minor_version < 17) or (major_version != 1 and major_version < 17):
113+
raise SystemError(f'Detected java {major_version}.{minor_version}. LanguageTool requires Java >= 17 for version {language_tool_version}.')
101114

102115

103116
def get_common_prefix(z: zipfile.ZipFile) -> Optional[str]:
@@ -133,7 +146,7 @@ def http_get(url: str, out_file: IO[bytes], proxies: Optional[Dict[str, str]] =
133146
total = int(content_length) if content_length is not None else None
134147
if req.status_code == 404:
135148
raise PathError(f'Could not find at URL {url}. The given version may not exist or is no longer available.')
136-
version = re.search(r'(\d+\.\d+)', url).group(1)
149+
version = url.split('/')[-1].split('-')[1].replace('-snapshot', '').replace('.zip', '')
137150
progress = tqdm.tqdm(unit="B", unit_scale=True, total=total,
138151
desc=f'Downloading LanguageTool {version}')
139152
for chunk in req.iter_content(chunk_size=1024):
@@ -143,18 +156,18 @@ def http_get(url: str, out_file: IO[bytes], proxies: Optional[Dict[str, str]] =
143156
progress.close()
144157

145158

146-
def unzip_file(temp_file: str, directory_to_extract_to: str) -> None:
159+
def unzip_file(temp_file_name: str, directory_to_extract_to: str) -> None:
147160
"""
148161
Unzips a zip file to a specified directory.
149162
150-
:param temp_file: A temporary file object representing the zip file to be extracted.
151-
:type temp_file: str
163+
:param temp_file_name: A temporary file object representing the zip file to be extracted.
164+
:type temp_file_name: str
152165
:param directory_to_extract_to: The directory where the contents of the zip file will be extracted.
153166
:type directory_to_extract_to: str
154167
"""
155-
156-
logger.info(f'Unzipping {temp_file.name} to {directory_to_extract_to}.')
157-
with zipfile.ZipFile(temp_file.name, 'r') as zip_ref:
168+
169+
logger.info(f'Unzipping {temp_file_name} to {directory_to_extract_to}.')
170+
with zipfile.ZipFile(temp_file_name, 'r') as zip_ref:
158171
zip_ref.extractall(directory_to_extract_to)
159172

160173

@@ -173,7 +186,7 @@ def download_zip(url: str, directory: str) -> None:
173186
# Close the file so we can extract it.
174187
downloaded_file.close()
175188
# Extract zip file to path.
176-
unzip_file(downloaded_file, directory)
189+
unzip_file(downloaded_file.name, directory)
177190
# Remove the temporary file.
178191
os.remove(downloaded_file.name)
179192
# Tell the user the download path.
@@ -192,9 +205,10 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
192205
LTP_DOWNLOAD_VERSION is used.
193206
:type language_tool_version: Optional[str]
194207
:raises AssertionError: If the download folder is not a directory.
208+
:raises ValueError: If the specified version format is invalid.
195209
"""
196210

197-
confirm_java_compatibility()
211+
confirm_java_compatibility(language_tool_version)
198212

199213
download_folder = get_language_tool_download_path()
200214

@@ -211,11 +225,23 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
211225

212226
if language_tool_version:
213227
version = language_tool_version
214-
filename = FILENAME.format(version=version)
215-
language_tool_download_url = urljoin(BASE_URL, filename)
228+
if re.match(r'^\d+\.\d+$', version):
229+
filename = FILENAME_RELEASE.format(version=version)
230+
language_tool_download_url = urljoin(BASE_URL_RELEASE, filename)
231+
elif version == "latest":
232+
filename = FILENAME_SNAPSHOT.format(version=version)
233+
language_tool_download_url = urljoin(BASE_URL_SNAPSHOT, filename)
234+
else:
235+
raise ValueError(
236+
f"You can only download a specific version of LanguageTool if it is "
237+
f"formatted like 'x.y' (e.g. '5.4'). The version you provided is {version}."
238+
f"You can also use 'latest' to download the latest snapshot of LanguageTool."
239+
)
216240
dirname, _ = os.path.splitext(filename)
241+
dirname = dirname.replace('latest', LT_SNAPSHOT_CURRENT_VERSION)
242+
if version == "latest":
243+
dirname = f"LanguageTool-{LT_SNAPSHOT_CURRENT_VERSION}"
217244
extract_path = os.path.join(download_folder, dirname)
218245

219-
if extract_path in old_path_list:
220-
return
221-
download_zip(language_tool_download_url, download_folder)
246+
if extract_path not in old_path_list:
247+
download_zip(language_tool_download_url, download_folder)

language_tool_python/language_tag.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LanguageTool language tag normalization module."""
2+
13
import re
24
from typing import Iterable, Any
35
from functools import total_ordering

language_tool_python/match.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LanguageTool API Match object representation and utility module."""
2+
13
import unicodedata
24
from collections import OrderedDict
35
from typing import Any, Dict, Tuple, Iterator, OrderedDict as OrderedDictType, List, Optional

language_tool_python/server.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""LanguageTool server management module."""
2+
13
from typing import Dict, List, Optional, Any, Set
24

35
import atexit
@@ -21,7 +23,7 @@
2123
parse_url, get_locale_language,
2224
get_language_tool_directory, get_server_cmd,
2325
FAILSAFE_LANGUAGE, startupinfo,
24-
LanguageToolError, ServerError, PathError,
26+
LanguageToolError, ServerError, PathError, RateLimitError,
2527
kill_process_force
2628
)
2729

@@ -483,6 +485,11 @@ def _query_server(
483485
)
484486
print(response)
485487
print(response.content)
488+
if response.status_code == 426:
489+
raise RateLimitError(
490+
'You have exceeded the rate limit for the free '
491+
'LanguageTool API. Please try again later.'
492+
)
486493
raise LanguageToolError(response.content.decode())
487494
except (IOError, http.client.HTTPException) as e:
488495
if self._remote is False:

language_tool_python/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Utility functions for the LanguageTool library."""
2+
13
from typing import List, Tuple, Optional
24
from shutil import which
35

@@ -71,6 +73,15 @@ class PathError(LanguageToolError):
7173
pass
7274

7375

76+
class RateLimitError(LanguageToolError):
77+
"""
78+
Exception raised for errors related to rate limiting in the LanguageTool server.
79+
This exception is a subclass of `LanguageToolError` and is used to indicate
80+
issues such as exceeding the allowed number of requests to the public API without a key.
81+
"""
82+
pass
83+
84+
7485
def parse_url(url_str: str) -> str:
7586
"""
7687
Parse the given URL string and ensure it has a scheme.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "language_tool_python"
3-
version = "2.9.2"
3+
version = "2.9.3"
44
requires-python = ">=3.9"
55
description = "Checks grammar using LanguageTool."
66
readme = { file = "README.md", content-type = "text/markdown" }

0 commit comments

Comments
 (0)