Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
- Added `premium_key` property to `language_tool_python.server.LanguageTool` to attach a premium API key for LanguageTool API requests.
- Added `premium_username` property to `language_tool_python.server.LanguageTool` to attach a username for premium LanguageTool API requests.
- Added `language_tool_python.match.is_check_match` type guard function to verify that a value is a `CheckMatch` (type from `language_tool_python._internals`).
- Added `language_tool_python.match.four_byte_char_positions` public function returning the positions of 4-byte encoded characters in a string (previously private).
- Added `language_tool_python.config_file.ConfigValue` public type alias representing all accepted value types for `LanguageToolConfig`.

### Changed
Expand All @@ -31,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/),
- Added `typing_extensions` as a dependency for Python < 3.13 (fallback for the stdlib `warnings.deprecated`).

### Fixed
- **Breaking:** Fixed a bug in `language_tool_python.match.Match` where a shared class-level cache of 4-byte character positions could be overwritten by a concurrent `.check()` call from another thread on a different text, producing incorrect match offsets. `language_tool_python.match.Match.__init__` now takes a `four_byte_positions: list[int]` argument instead of `text: str`. Removed the `Match.PREVIOUS_MATCHES_TEXT` and `Match.FOUR_BYTES_POSITIONS` class attributes.
- Corrected a bug in `language_tool_python.config_file.LanguageToolConfig` where directory paths were incorrectly rejected by the path validator.
- Fixed a bug in `LanguageTool._start_server_on_free_port` where `_url` was not updated when retrying on a different port, causing all subsequent server requests to target the wrong (original) port.
- Fixed a bug in `LanguageTool._query_server` where `RateLimitError` was only raised when the rate-limit response body was invalid JSON, a valid JSON body with status 426 was silently returned as data instead (for now, the body from LanguageTool for rate-limiting responses is "Upgrade Required", which is not valid JSON, but this may change in the future).
Expand Down
50 changes: 28 additions & 22 deletions src/language_tool_python/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from ._internals.api_types import CheckMatch

__all__ = ["Match", "is_check_match"]
__all__ = ["Match", "four_byte_char_positions", "is_check_match"]

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,7 +61,7 @@ def _get_match_ordered_dict() -> OrderedDictType[str, type]:
)


def _four_byte_char_positions(text: str) -> list[int]:
def four_byte_char_positions(text: str) -> list[int]:
"""Identify positions of 4-byte encoded characters in a UTF-8 string.

This function scans through the input text and identifies the positions of
Expand Down Expand Up @@ -123,9 +123,10 @@ class Match: # noqa: PLW1641 # Doesn't implement hash because it's mutable
and ``text``), ``replacements`` (items with ``value``), ``length``, and
``message``.
:type attrib: CheckMatch
:param text: The original text in which the error occurred (the whole text,
not just the context).
:type text: str
:param four_byte_positions: The positions of 4-byte encoded characters in the
original text in which the error occurred (the whole text, not just the
context), as returned by :func:`four_byte_char_positions`.
:type four_byte_positions: list[int]

Example of a match object received from the LanguageTool API :

Expand Down Expand Up @@ -176,13 +177,6 @@ class Match: # noqa: PLW1641 # Doesn't implement hash because it's mutable

"""

PREVIOUS_MATCHES_TEXT: str | None = None
"""The text of the previous match object."""

FOUR_BYTES_POSITIONS: list[int] | None = None
"""The positions of 4-byte encoded characters in the text, registered by the
previous match object (kept for optimization purposes if the text is the same)."""

rule_id: str
"""The ID of the rule that was violated."""

Expand Down Expand Up @@ -213,12 +207,24 @@ class Match: # noqa: PLW1641 # Doesn't implement hash because it's mutable
sentence: str
"""The sentence that contains the rule violation."""

def __init__(self, attrib: CheckMatch, text: str) -> None:
def __init__(
self,
attrib: CheckMatch,
four_byte_positions: list[int],
) -> None:
"""Initialize a Match object with the given attributes.

The method processes and normalizes the attributes before storing them on the
object. This method adjusts the positions of 4-byte encoded characters in the
text to ensure the offsets of the matches are correct.

:param attrib: The raw LanguageTool API match.
:type attrib: CheckMatch
:param four_byte_positions: The positions of 4-byte encoded characters in the
original text (the whole text, not just the context), as returned by
:func:`four_byte_char_positions`. Callers processing multiple matches for
the same text should compute this once and reuse it across all matches.
:type four_byte_positions: list[int]
"""
# Process rule.
custom_match: dict[str, str | int | list[str]] = {}
Expand All @@ -242,15 +248,15 @@ def __init__(self, attrib: CheckMatch, text: str) -> None:
for k, v in custom_match.items():
setattr(self, k, v)

if text != Match.PREVIOUS_MATCHES_TEXT:
Match.PREVIOUS_MATCHES_TEXT = text
Match.FOUR_BYTES_POSITIONS = _four_byte_char_positions(text)
# Get the positions of 4-byte encoded characters in the text because without
# carrying out this step, the offsets of the matches could be incorrect.
if Match.FOUR_BYTES_POSITIONS is not None:
self.offset -= sum(
1 for pos in Match.FOUR_BYTES_POSITIONS if pos < self.offset
)
# Adjust the offset for 4-byte encoded characters because without carrying out
# this step, the offsets of the matches could be incorrect.
offset = self.offset
adjustment = 0
for pos in four_byte_positions:
if pos >= offset:
break
adjustment += 1
self.offset = offset - adjustment

def _ordered_items(self) -> list[tuple[str, _MatchValue]]:
"""Return public match attributes in the documented order."""
Expand Down
7 changes: 5 additions & 2 deletions src/language_tool_python/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
ServerError,
)
from .language_tag import LanguageTag
from .match import Match
from .match import Match, four_byte_char_positions
from .utils import correct

_startupinfo: object | None = None
Expand Down Expand Up @@ -732,7 +732,10 @@ def check(self, text: str) -> list[Match]:
err = f"Invalid response received from the LanguageTool server: {response}"
raise ServerError(err)
matches = response["matches"]
return [Match(match, text) for match in matches]
if not matches:
return []
positions = four_byte_char_positions(text)
return [Match(match, positions) for match in matches]
Comment thread
mdevolde marked this conversation as resolved.

def check_matching_regions(
self,
Expand Down
Loading