Skip to content

Commit 23eae73

Browse files
committed
fix deletion handlers, add unit tests for closing LTP, add examples of with..as and close() to the README, remove backports references
1 parent 3df7ce4 commit 23eae73

8 files changed

Lines changed: 69 additions & 86 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ From the interpreter:
6565
>>> matches = tool.check(text)
6666
>>> len(matches)
6767
2
68+
...
69+
>>> tool.close() # Call `close()` to shut off the server when you're done.
6870
```
6971

7072
Check out some ``Match`` object attributes:
@@ -101,6 +103,28 @@ $ language_tool_python example.txt
101103
example.txt:1:1: THIS_NNS[3]: Did you mean 'these'?
102104
```
103105

106+
## Closing LanguageTool
107+
108+
`language_tool_python` runs a LanguageTool Java server in the background. It will shut the server off when garbage collected, for example when a created `language_tool_python.LanguageTool` object goes out of scope. However, if garbage collection takes awhile, the process might not get deleted right away. If you're seeing lots of processes get spawned and not get deleted, you can explicitly close them:
109+
110+
111+
```python
112+
import language_tool_python
113+
tool = language_tool_python.LanguageToolPublicAPI('de-DE') # starts a process
114+
# do stuff with `tool`
115+
tool.close() # explicitly shut off the LanguageTool
116+
```
117+
118+
You can also use a context manager (`with .. as`) to explicitly control when the server is started and stopped:
119+
120+
```python
121+
import language_tool_python
122+
123+
with language_tool_python.LanguageToolPublicAPI('de-DE') as tool:
124+
# do stuff with `tool`
125+
# no need to call `close() as it will happen at the end of the with statement
126+
```
127+
104128
## Installation
105129

106130
To install via pip:

‎language_tool_python/backports/__init__.py‎

Whitespace-only changes.

‎language_tool_python/backports/subprocess.py‎

Lines changed: 0 additions & 69 deletions
This file was deleted.

‎language_tool_python/download_lt.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
BASE_URL = os.environ.get('LTP_DOWNLOAD_HOST', 'https://www.languagetool.org/download/')
2929
FILENAME = 'LanguageTool-{version}.zip'
3030

31-
LATEST_VERSION = '5.5'
31+
LATEST_VERSION = '5.6'
3232

3333
JAVA_VERSION_REGEX = re.compile(
3434
r'^(?:java|openjdk) version "(?P<major1>\d+)(|\.(?P<major2>\d+)\.[^"]+)"',
@@ -104,7 +104,7 @@ def http_get(url, out_file, proxies=None):
104104
total = int(content_length) if content_length is not None else None
105105
if req.status_code == 403: # Not found on AWS
106106
raise Exception('Could not find at URL {}.'.format(url))
107-
progress = tqdm.tqdm(unit="B", unit_scale=True, total=total, desc='Downloading LanguageTool')
107+
progress = tqdm.tqdm(unit="B", unit_scale=True, total=total, desc=f'Downloading LanguageTool {LATEST_VERSION}')
108108
for chunk in req.iter_content(chunk_size=1024):
109109
if chunk: # filter out keep-alive new chunks
110110
progress.update(len(chunk))

‎language_tool_python/server.py‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict
1+
from typing import Dict, List
22

33
import atexit
44
import http.client
@@ -7,11 +7,10 @@
77
import re
88
import requests
99
import socket
10+
import subprocess
1011
import threading
1112
import urllib.parse
12-
from weakref import WeakValueDictionary
1313

14-
from .backports import subprocess
1514
from .download_lt import download_lt
1615
from .language_tag import LanguageTag
1716
from .match import Match
@@ -26,21 +25,20 @@
2625

2726
# Keep track of running server PIDs in a global list. This way,
2827
# we can ensure they're killed on exit.
29-
RUNNING_SERVER_PROCESSES = []
28+
RUNNING_SERVER_PROCESSES: List[subprocess.Popen] = []
3029

3130
class LanguageTool:
32-
""" Main class used for checking text against different rules.
33-
LanguageTool v2 API documentation: https://languagetool.org/http-api/swagger-ui/#!/default/post_check
31+
"""Main class used for checking text against different rules.
32+
LanguageTool v2 API documentation: https://languagetool.org/http-api/swagger-ui/#!/default/post_check
3433
"""
3534
_HOST = socket.gethostbyname('localhost')
3635
_MIN_PORT = 8081
3736
_MAX_PORT = 8999
3837
_TIMEOUT = 5 * 60
3938
_remote = False
4039
_port = _MIN_PORT
41-
_server = None
42-
_consumer_thread = None
43-
_instances = WeakValueDictionary()
40+
_server: subprocess.Popen = None
41+
_consumer_thread: threading.Thread = None
4442
_PORT_RE = re.compile(r"(?:https?://.*:|port\s+)(\d+)", re.I)
4543

4644
def __init__(self, language=None, motherTongue=None, remote_server=None, newSpellings=None, new_spellings_persist=True):
@@ -68,20 +66,22 @@ def __init__(self, language=None, motherTongue=None, remote_server=None, newSpel
6866
self.disabled_categories = set()
6967
self.enabled_categories = set()
7068
self.enabled_rules_only = False
71-
self._instances[id(self)] = self
72-
69+
7370
def __enter__(self):
7471
return self
7572

7673
def __exit__(self, exc_type, exc_val, exc_tb):
7774
self.close()
7875

76+
def __del__(self):
77+
self.close()
78+
7979
def __repr__(self):
8080
return '{}(language={!r}, motherTongue={!r})'.format(
8181
self.__class__.__name__, self.language, self.motherTongue)
8282

8383
def close(self):
84-
if not self._instances and self._server_is_alive():
84+
if self._server_is_alive():
8585
self._terminate_server()
8686
if not self._new_spellings_persist and self._new_spellings:
8787
self._unregister_spellings()
@@ -311,7 +311,7 @@ def _terminate_server(self):
311311
return LanguageToolError_message
312312

313313
class LanguageToolPublicAPI(LanguageTool):
314-
""" Language tool client of the official API. """
314+
"""Language tool client of the official API."""
315315
def __init__(self, *args, **kwargs):
316316
super().__init__(*args, remote_server='https://languagetool.org/api/', **kwargs)
317317

‎language_tool_python/utils.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import locale
44
import os
55
import re
6+
import subprocess
67
import sys
78
import urllib.parse
89
import urllib.request
910

10-
from .backports import subprocess
1111
from .match import Match
1212
from .which import which
1313

‎setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setup(
99
name='language_tool_python',
10-
version='2.6.2',
10+
version='2.6.3',
1111
description='Checks grammar using LanguageTool.',
1212
long_description_content_type='text/markdown',
1313
long_description=long_description,

‎tests/test_major_functionality.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ def test_langtool_load():
55
matches = lang_tool.check('ain\'t nothin but a thang')
66
assert str(matches) == """[Match({'ruleId': 'UPPERCASE_SENTENCE_START', 'message': 'This sentence does not start with an uppercase letter.', 'replacements': ['Ai'], 'offsetInContext': 0, 'context': "ain't nothin but a thang", 'offset': 0, 'errorLength': 2, 'category': 'CASING', 'ruleIssueType': 'typographical', 'sentence': "ain't nothin but a thang"}), Match({'ruleId': 'MORFOLOGIK_RULE_EN_US', 'message': 'Possible spelling mistake found.', 'replacements': ['nothing', 'no thin'], 'offsetInContext': 6, 'context': "ain't nothin but a thang", 'offset': 6, 'errorLength': 6, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': "ain't nothin but a thang"}), Match({'ruleId': 'MORFOLOGIK_RULE_EN_US', 'message': 'Possible spelling mistake found.', 'replacements': ['than', 'thing', 'hang', 'thank', 'Chang', 'tang', 'thong', 'twang', 'Thant', 'thane', 'Thanh', 'Jhang', 'Shang', 'Zhang'], 'offsetInContext': 19, 'context': "ain't nothin but a thang", 'offset': 19, 'errorLength': 5, 'category': 'TYPOS', 'ruleIssueType': 'misspelling', 'sentence': "ain't nothin but a thang"})]"""
77

8+
9+
def test_process_starts_and_stops():
10+
import language_tool_python
11+
with language_tool_python.LanguageTool("en-US") as lang_tool:
12+
proc: subprocess.Popen = lang_tool._server
13+
# Make sure process is running before killing language tool object.
14+
assert proc.poll() is None, "lang_tool._server not running after creation"
15+
# lang_tool.close() # Explicitly close() object so process stops before garbage collection.
16+
# Make sure process stopped after close() was called.
17+
assert proc.poll() is not None, "lang_tool._server should stop running after deletion"
18+
import time; time.sleep(10)
19+
# if poll is None: # p.subprocess is alive
20+
21+
def test_process_starts_and_closes():
22+
import language_tool_python
23+
lang_tool = language_tool_python.LanguageTool("en-US")
24+
proc: subprocess.Popen = lang_tool._server
25+
# Make sure process is running before killing language tool object.
26+
assert proc.poll() is None, "lang_tool._server not running after creation"
27+
lang_tool.close() # Explicitly close() object so process stops before garbage collection.
28+
del lang_tool
29+
# Make sure process stopped after close() was called.
30+
assert proc.poll() is not None, "lang_tool._server should stop running after deletion"
31+
# if poll is None: # p.subprocess is alive
32+
33+
34+
# TODO(jxm): Add test to confirm that multiple instances connect to the same process instead of starting new ones.
35+
836
def test_langtool_languages():
937
import language_tool_python
1038
lang_tool = language_tool_python.LanguageTool("en-US")

0 commit comments

Comments
 (0)