From d1b65dc116c3e04360a35244d95db58ee9402b04 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Sat, 31 Oct 2026 05:23:22 +0100 Subject: [PATCH] chore(hygiene): harden CI matrix with Python 3.13, expand PEP 621 metadata, and add contract test suite --- .github/workflows/tests.yml | 5 ++- CHANGELOG.md | 7 ++++ README.md | 2 +- README_de.md | 2 +- pyproject.toml | 13 +++++- tests/test_contract.py | 79 +++++++++++++++++++++++++++++++++++++ 6 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 tests/test_contract.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e66609..5e5a0d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] env: QT_QPA_PLATFORM: offscreen PYTHONIOENCODING: utf-8 @@ -34,6 +34,9 @@ jobs: - run: python -m pip install --upgrade pip - run: pip install -e ".[dev]" - run: pytest + - name: Linter check + if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' + run: ruff check absorber tests # Qt is a typed C++ API behind stubs: a call can pass every test and still # match no documented overload. That happened once, so it is gated now. - name: Type check diff --git a/CHANGELOG.md b/CHANGELOG.md index ed04ac9..f8bc8ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unreleased] +### Geändert + +- CI-Matrix um Python 3.13 erweitert (Ubuntu, Windows, macOS). +- PEP-621-Metadaten in `pyproject.toml` um Python 3.13 Classifier, Projekt-URLs (Repository, Changelog) und Linter-Konfiguration (`[tool.ruff]`) ergänzt. +- Vertragstest (`tests/test_contract.py`) für PEP 621 Metadaten, Versionsparität, Lizenzdateien und Konfigurationsvertrag hinzugefügt; Testzahl von 115 auf 119 gestiegen. +- Linter-Prüfung (`ruff check absorber tests`) in CI-Workflow integriert. + ## [0.1.1] - 2026-10-13 ### Behoben diff --git a/README.md b/README.md index 48d95dd..69370f7 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ pip install -e ".[dev]" QT_QPA_PLATFORM=offscreen pytest ``` -115 tests, no network access, no real clipboard required. The core is deliberately free of +119 tests, no network access, no real clipboard required. The core is deliberately free of GUI imports: | Module | Responsibility | diff --git a/README_de.md b/README_de.md index 6aad303..f10ffb7 100644 --- a/README_de.md +++ b/README_de.md @@ -153,7 +153,7 @@ pip install -e ".[dev]" QT_QPA_PLATFORM=offscreen pytest ``` -115 Tests, ohne Netzzugriff und ohne echte Zwischenablage. Der Kern ist bewusst frei von +119 Tests, ohne Netzzugriff und ohne echte Zwischenablage. Der Kern ist bewusst frei von GUI-Importen: | Modul | Zuständigkeit | diff --git a/pyproject.toml b/pyproject.toml index c79c95f..f6b0683 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,20 +18,23 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Desktop Environment :: File Managers", "Topic :: Utilities", ] dependencies = ["PySide6>=6.6"] [project.optional-dependencies] -dev = ["pytest>=8.0", "pyright>=1.1.400"] +dev = ["pytest>=8.0", "pyright>=1.1.400", "ruff>=0.5.0"] [project.gui-scripts] absorber = "absorber.__main__:main" [project.urls] Homepage = "https://github.com/file-bricks/absorber" +Repository = "https://github.com/file-bricks/absorber" Issues = "https://github.com/file-bricks/absorber/issues" +Changelog = "https://github.com/file-bricks/absorber/blob/main/CHANGELOG.md" [tool.setuptools.packages.find] include = ["absorber*"] @@ -39,3 +42,11 @@ include = ["absorber*"] [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-q" + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "W", "C4", "UP"] +ignore = ["E501"] diff --git a/tests/test_contract.py b/tests/test_contract.py new file mode 100644 index 0000000..2826447 --- /dev/null +++ b/tests/test_contract.py @@ -0,0 +1,79 @@ +"""Contract tests for PEP 621 metadata, version consistency, and repository integrity.""" + +from __future__ import annotations + +from pathlib import Path +import re + +import absorber +from absorber import config + +REPO_ROOT = Path(__file__).resolve().parent.parent +PYPROJECT_PATH = REPO_ROOT / "pyproject.toml" +README_PATH = REPO_ROOT / "README.md" +README_DE_PATH = REPO_ROOT / "README_de.md" +LICENSE_PATH = REPO_ROOT / "LICENSE" +THIRD_PARTY_LICENSES_PATH = REPO_ROOT / "THIRD_PARTY_LICENSES.txt" + + +def _read_pyproject() -> str: + return PYPROJECT_PATH.read_text(encoding="utf-8") + + +def test_pep621_metadata_fields(): + content = _read_pyproject() + assert 'name = "absorber"' in content + assert 'requires-python = ">=3.11"' in content + assert 'license = { text = "MIT" }' in content + assert "[project.urls]" in content + assert 'Homepage = "https://github.com/file-bricks/absorber"' in content + assert 'Repository = "https://github.com/file-bricks/absorber"' in content + assert 'Issues = "https://github.com/file-bricks/absorber/issues"' in content + assert 'Changelog = "https://github.com/file-bricks/absorber/blob/main/CHANGELOG.md"' in content + assert '"Programming Language :: Python :: 3.11"' in content + assert '"Programming Language :: Python :: 3.12"' in content + assert '"Programming Language :: Python :: 3.13"' in content + assert '"License :: OSI Approved :: MIT License"' in content + + +def test_version_parity(): + content = _read_pyproject() + match = re.search(r'version\s*=\s*"([^"]+)"', content) + assert match is not None, "pyproject.toml must define project version" + pyproject_version = match.group(1) + assert absorber.__version__ == pyproject_version + + +def test_essential_files_and_licenses(): + assert LICENSE_PATH.is_file(), "LICENSE must exist" + license_text = LICENSE_PATH.read_text(encoding="utf-8") + assert "MIT License" in license_text + + assert THIRD_PARTY_LICENSES_PATH.is_file(), "THIRD_PARTY_LICENSES.txt must exist" + third_party_text = THIRD_PARTY_LICENSES_PATH.read_text(encoding="utf-8") + assert "PySide6" in third_party_text + assert "LGPL" in third_party_text + + readme_en = README_PATH.read_text(encoding="utf-8") + assert "THIRD_PARTY_LICENSES.txt" in readme_en + assert "LICENSE" in readme_en + + readme_de = README_DE_PATH.read_text(encoding="utf-8") + assert "THIRD_PARTY_LICENSES.txt" in readme_de + assert "LICENSE" in readme_de + + +def test_config_defaults_contract(): + expected_keys = { + "mode", + "target_dir", + "text_formats", + "copy_file_lists", + "path_to_clipboard", + "model_naming", + "model", + "ollama_url", + "virtual_retention_days", + "recent_max", + } + assert expected_keys.issubset(set(config.DEFAULTS.keys()))