1
+ """LanguageTool download module."""
2
+
1
3
import logging
2
4
import os
3
5
import re
7
9
import tqdm
8
10
from typing import IO , Dict , Optional , Tuple
9
11
import zipfile
12
+ from datetime import datetime
10
13
11
14
from shutil import which
12
15
from urllib .parse import urljoin
24
27
25
28
26
29
# 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'
29
34
30
- LTP_DOWNLOAD_VERSION = '6.6'
35
+ LTP_DOWNLOAD_VERSION = 'latest'
36
+ LT_SNAPSHOT_CURRENT_VERSION = '6.7-SNAPSHOT'
31
37
32
38
JAVA_VERSION_REGEX = re .compile (
33
39
r'^(?:java|openjdk) version "(?P<major1>\d+)(|\.(?P<major2>\d+)\.[^"]+)"' ,
@@ -63,16 +69,16 @@ def parse_java_version(version_text: str) -> Tuple[int, int]:
63
69
return (major1 , major2 )
64
70
65
71
66
- def confirm_java_compatibility () -> bool :
72
+ def confirm_java_compatibility (language_tool_version : Optional [ str ] = LTP_DOWNLOAD_VERSION ) -> None :
67
73
"""
68
74
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) .
70
76
It raises an error if Java is not installed or if the version is incompatible.
71
77
78
+ :param language_tool_version: The version of LanguageTool to check compatibility for.
79
+ :type language_tool_version: Optional[str]
72
80
: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.
76
82
"""
77
83
78
84
java_path = which ('java' )
@@ -87,17 +93,24 @@ def confirm_java_compatibility() -> bool:
87
93
universal_newlines = True )
88
94
89
95
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
+
90
104
# Some installs of java show the version number like `14.0.1`
91
105
# 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.
94
107
# (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 } .' )
99
111
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 } .' )
101
114
102
115
103
116
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]] =
133
146
total = int (content_length ) if content_length is not None else None
134
147
if req .status_code == 404 :
135
148
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' , '' )
137
150
progress = tqdm .tqdm (unit = "B" , unit_scale = True , total = total ,
138
151
desc = f'Downloading LanguageTool { version } ' )
139
152
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]] =
143
156
progress .close ()
144
157
145
158
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 :
147
160
"""
148
161
Unzips a zip file to a specified directory.
149
162
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
152
165
:param directory_to_extract_to: The directory where the contents of the zip file will be extracted.
153
166
:type directory_to_extract_to: str
154
167
"""
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 :
158
171
zip_ref .extractall (directory_to_extract_to )
159
172
160
173
@@ -173,7 +186,7 @@ def download_zip(url: str, directory: str) -> None:
173
186
# Close the file so we can extract it.
174
187
downloaded_file .close ()
175
188
# Extract zip file to path.
176
- unzip_file (downloaded_file , directory )
189
+ unzip_file (downloaded_file . name , directory )
177
190
# Remove the temporary file.
178
191
os .remove (downloaded_file .name )
179
192
# Tell the user the download path.
@@ -192,9 +205,10 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
192
205
LTP_DOWNLOAD_VERSION is used.
193
206
:type language_tool_version: Optional[str]
194
207
:raises AssertionError: If the download folder is not a directory.
208
+ :raises ValueError: If the specified version format is invalid.
195
209
"""
196
210
197
- confirm_java_compatibility ()
211
+ confirm_java_compatibility (language_tool_version )
198
212
199
213
download_folder = get_language_tool_download_path ()
200
214
@@ -211,11 +225,23 @@ def download_lt(language_tool_version: Optional[str] = LTP_DOWNLOAD_VERSION) ->
211
225
212
226
if language_tool_version :
213
227
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
+ )
216
240
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 } "
217
244
extract_path = os .path .join (download_folder , dirname )
218
245
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 )
0 commit comments