Skip to content
Draft
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: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- os: ubuntu-26.04
python-version: "3.14"
- os: ubuntu-26.04
python-version: "3.15.0-beta.2"
python-version: "3.15.0-beta.3"
- os: macos-26
python-version: "3.14"
- os: windows-2025
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ cython_debug/
# Ruff stuff:
.ruff_cache/

# Pytest tmp_path base directory (project-relative to avoid Windows temp permission issues)
.pytest_tmp/

# PyPI configuration file
.pypirc

Expand Down
15 changes: 12 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ changelog = "https://github.com/jxmorris12/language_tool_python/blob/master/CHAN
[dependency-groups]
tests = [
"pytest",
"pytest-benchmark",
"pytest-cov",
"hypothesis",
]

docs = [
Expand Down Expand Up @@ -141,9 +143,10 @@ ignore = [
]

[tool.ruff.lint.per-file-ignores]
"tests/*.py" = [
"S101", # Need to use assert statements in tests
"SLF001" # Need to use private members of the library for testing
"tests/**/*.py" = [
"S101", # Need to use assert statements in tests
"SLF001", # Need to use private members of the library for testing
"RUF001", # LanguageTool output contains typographic quotes (‘’“”)
]
"src/language_tool_python/__main__.py" = ["T201"] # Allow usage of print in the CLI entry point

Expand All @@ -170,3 +173,9 @@ warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
warn_unused_ignores = true

[[tool.mypy.overrides]]
module = ["tests.property.*"]
# hypothesis decorators contain Any expressions, so we need to disable the following checks for tests using hypothesis
disallow_any_decorated = false
disallow_any_expr = false
7 changes: 6 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
[pytest]
addopts = -vra --cov=src --cov-report=html --cov-report=xml
addopts = -vra --cov=src --cov-report=html --cov-report=xml --basetemp=.pytest_tmp
testpaths = tests
markers =
unit: fast, isolated tests with no external dependencies
integration: tests that require a live LanguageTool server or network
property: property-based tests using Hypothesis
perf: performance benchmark tests using pytest-benchmark

[coverage:run]
source = src
4 changes: 2 additions & 2 deletions src/language_tool_python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def _read_project_version(pyproject: Path) -> str:
__version__ = version("language_tool_python")
# If the package is not installed in the environment,
# read the version from pyproject.toml
except PackageNotFoundError:
except PackageNotFoundError: # pragma: no cover
project_root = Path(__file__).resolve().parent.parent
pyproject = project_root / "pyproject.toml"
__version__ = _read_project_version(pyproject)
Expand Down Expand Up @@ -258,7 +258,7 @@ def __call__(
cli_args.disable_categories.update(rule_values)
elif self.dest == "enable_categories":
cli_args.enable_categories.update(rule_values)
else:
else: # pragma: no cover
err = f"unexpected rules destination: {self.dest}"
raise ValueError(err)

Expand Down
4 changes: 2 additions & 2 deletions src/language_tool_python/_internals/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
if sys.version_info >= (3, 11):
from tomllib import loads as toml_loads
else:
from tomli import loads as toml_loads
from tomli import loads as toml_loads # pragma: no cover

if sys.version_info >= (3, 13):
from warnings import deprecated
else:
from typing_extensions import deprecated
from typing_extensions import deprecated # pragma: no cover

__all__ = ["deprecated", "toml_loads"]
2 changes: 1 addition & 1 deletion src/language_tool_python/config_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def _path_validator(v: PathLike[str] | str) -> None:
if not p.exists():
err = f"path does not exist: {p}"
raise PathError(err)
if not p.is_file() and not p.is_dir():
if not p.is_file() and not p.is_dir(): # pragma: no cover
err = f"path is not a file/directory: {p}"
raise PathError(err)

Expand Down
2 changes: 1 addition & 1 deletion src/language_tool_python/download_lt.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def download(self) -> None:

:raises NotImplementedError: Always, unless implemented by a subclass.
"""
raise NotImplementedError
raise NotImplementedError # pragma: no cover

def _get_remote_zip(
self,
Expand Down
1 change: 1 addition & 0 deletions tests/benchmarks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Benchmark tests for the language_tool_python library."""
17 changes: 17 additions & 0 deletions tests/benchmarks/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Configuration for the benchmark test suite."""

from __future__ import annotations

from pathlib import Path

import pytest


def pytest_collection_modifyitems(
items: list[pytest.Item],
) -> None:
"""Apply the 'perf' marker to all tests collected from this directory."""
benchmarks_dir = Path(__file__).parent
for item in items:
if item.path.is_relative_to(benchmarks_dir):
item.add_marker(pytest.mark.perf)
81 changes: 81 additions & 0 deletions tests/benchmarks/test_bench_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Benchmark tests for LanguageTool grammar checking performance.

Run with: pytest tests/benchmarks/ -v
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

import language_tool_python

if TYPE_CHECKING:
from collections.abc import Generator

from pytest_benchmark.fixture import BenchmarkFixture

_SHORT_TEXT = "This is a sentence with some erors in it. "
_MEDIUM_TEXT = (_SHORT_TEXT * 20).strip()
_LONG_TEXT = (_SHORT_TEXT * 100).strip()


@pytest.fixture(scope="module")
def tool() -> Generator[language_tool_python.LanguageTool, None, None]:
"""Provide a LanguageTool instance shared across benchmarks in this module."""
with language_tool_python.LanguageTool("en-US") as t:
yield t


@pytest.fixture(scope="module")
def cached_tool() -> Generator[language_tool_python.LanguageTool, None, None]:
"""Provide a pipeline-caching LanguageTool instance for cache benchmarks."""
with language_tool_python.LanguageTool(
"en-US",
config={"cacheSize": 1000, "pipelineCaching": True},
) as t:
yield t


def test_bench_check_short_text(
benchmark: BenchmarkFixture,
tool: language_tool_python.LanguageTool,
) -> None:
"""Benchmark grammar checking on a short sentence (~38 characters)."""
benchmark(tool.check, _SHORT_TEXT)


def test_bench_check_medium_text(
benchmark: BenchmarkFixture,
tool: language_tool_python.LanguageTool,
) -> None:
"""Benchmark grammar checking on medium-length text (~840 characters)."""
benchmark(tool.check, _MEDIUM_TEXT)


def test_bench_check_long_text(
benchmark: BenchmarkFixture,
tool: language_tool_python.LanguageTool,
) -> None:
"""Benchmark grammar checking on long text (~4200 characters)."""
benchmark(tool.check, _LONG_TEXT)


def test_bench_correct_short_text(
benchmark: BenchmarkFixture,
tool: language_tool_python.LanguageTool,
) -> None:
"""Benchmark automatic text correction on a short sentence."""
benchmark(tool.correct, _SHORT_TEXT)


def test_bench_check_with_pipeline_cache(
benchmark: BenchmarkFixture,
cached_tool: language_tool_python.LanguageTool,
) -> None:
"""Benchmark grammar checking with pipeline caching enabled.

Compare with test_bench_check_short_text to measure cache speedup.
"""
benchmark(cached_tool.check, _SHORT_TEXT)
1 change: 1 addition & 0 deletions tests/integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Integration tests for the language_tool_python library."""
17 changes: 17 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Configuration for the integration test suite."""

from __future__ import annotations

from pathlib import Path

import pytest


def pytest_collection_modifyitems(
items: list[pytest.Item],
) -> None:
"""Apply the 'integration' marker to all tests collected from this directory."""
integration_dir = Path(__file__).parent
for item in items:
if item.path.is_relative_to(integration_dir):
item.add_marker(pytest.mark.integration)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Tests for the public API functionality."""
"""Integration tests for the public API functionality."""

import pytest

Expand Down Expand Up @@ -34,7 +34,7 @@ def test_remote_es() -> None:
'INCORRECT_EXPRESSIONS', 'rule_issue_type': 'grammar', 'sentence':
'LanguageTool le ayudará a afrentar algunas dificultades propias de
la escritura.'}), Match({'rule_id': 'PRON_HABER_PARTICIPIO',
'message': 'El v. \u2018haber\u2019 se escribe con hache.',
'message': 'El v. ‘haber’ se escribe con hache.',
'replacements': ['ha'], 'offset_in_context': 43, 'context':
'...ificultades propias de la escritura. Se a hecho un esfuerzo para
detectar errores...', 'offset': 107, 'error_length': 1, 'category':
Expand All @@ -50,8 +50,8 @@ def test_remote_es() -> None:
'misspelling', 'sentence': 'Se a hecho un esfuerzo para detectar
errores tipográficos, ortograficos y incluso gramaticales.'}),
Match({'rule_id': 'Y_E_O_U', 'message': 'Cuando precede a palabras
que comienzan por \u2018i\u2019, la conjunción \u2018y\u2019 se
transforma en \u2018e\u2019.', 'replacements': ['e'],
que comienzan por ‘i’, la conjunción ‘y’ se
transforma en ‘e’.', 'replacements': ['e'],
'offset_in_context': 43, 'context': '...ctar errores tipográficos,
ortograficos y incluso gramaticales. También algunos e...', 'offset':
176, 'error_length': 1, 'category': 'GRAMMAR', 'rule_issue_type':
Expand Down
Loading
Loading