From 4156436e666641ee19eb1fe92eb9bbde6a9481a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 2 Jun 2026 13:15:46 +0000 Subject: [PATCH] chore(lint): add ruff config and Python lint workflow Closes #101. Adds a ruff configuration to pyproject.toml selecting the rule set that matches the existing in-code `noqa` codes (S, BLE) plus the conventional hygiene families (E, F, W, I, B, UP). Adds a dev extra (`pip install -e .[dev]`) so contributors install ruff and pytest reproducibly. Adds a dedicated `python-lint.yml` workflow that runs `ruff check simplicio_mapper tests/python` on push and pull_request and blocks merge on violations. Applies the surgical fixes ruff surfaced on first run (import sorting, unnecessary `mode` arguments, redundant utf-8 encodings, switch to `collections.abc.Callable` / `Sequence`). The intentional `git` subprocess invocations are covered by an explicit `S603`/`S607` ignore list with a documented rationale, matching the existing `# noqa: S603` annotation on `cli.py`. A separate Python CI job covering the full pytest matrix and the Rust crate is tracked in #97; this PR delivers the lint half so the rule set itself is enforced from day one. https://claude.ai/code/session_01JdmemqddwFnvbceWyuDE8m --- .github/workflows/python-lint.yml | 43 ++++++++++++++++++++++++++ pyproject.toml | 51 +++++++++++++++++++++++++++++++ simplicio_mapper/_native.py | 4 ++- simplicio_mapper/cache.py | 4 +-- simplicio_mapper/cli.py | 12 ++++---- simplicio_mapper/mapper.py | 5 +-- 6 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/python-lint.yml diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml new file mode 100644 index 0000000..5bef3e9 --- /dev/null +++ b/.github/workflows/python-lint.yml @@ -0,0 +1,43 @@ +# Python lint — runs ruff against the Python package and its test suite. +# Mirrors the `npm run lint` Node side and complements scaffold-self-check.yml. + +name: Python Lint + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +concurrency: + group: python-lint-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + ruff: + name: ruff check + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dev tooling + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[dev]" + + - name: Ruff check + run: ruff check simplicio_mapper tests/python diff --git a/pyproject.toml b/pyproject.toml index a9c0047..8d90eeb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,12 @@ dependencies = [ "diskcache>=5.6.3,<6", ] +[project.optional-dependencies] +dev = [ + "ruff>=0.15,<0.17", + "pytest>=8,<9", +] + [project.scripts] simplicio-mapper = "simplicio_mapper.cli:main" llm-project-mapper = "simplicio_mapper.cli:main" @@ -41,6 +47,51 @@ Issues = "https://github.com/wesleysimplicio/simplicio-mapper/issues" [tool.hatch.build.targets.wheel] packages = ["simplicio_mapper"] +[tool.ruff] +line-length = 110 +target-version = "py310" +extend-exclude = [ + ".simplicio", + ".specs", + "dist", + "build", + "rust/target", + "docs-site", + "vscode-extension", +] + +[tool.ruff.lint] +# Rule set is chosen to match the `# noqa` codes already used in the +# codebase (S = bandit-style security checks, BLE = blind exception) +# plus standard hygiene families that do not require sweeping rewrites. +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "I", # import sorting + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "S", # flake8-bandit + "UP", # pyupgrade +] +ignore = [ + "E501", # line length handled by formatter / project style + "S101", # pytest uses assert + "S311", # non-crypto randomness is fine in mapper heuristics + "S603", # subprocess calls only run fixed argv with project-controlled inputs + "S607", # `git` is intentionally resolved from PATH on host + "UP007", # keep `Optional[...]` style optional for stdlib 3.10 compat + "UP045", # keep `Optional[...]` style optional for stdlib 3.10 compat + "B008", # default mutable factories are intentional in CLI signatures +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S", "BLE", "B"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + [tool.hatch.build.targets.sdist] include = [ "simplicio_mapper", diff --git a/simplicio_mapper/_native.py b/simplicio_mapper/_native.py index 60da9b9..879717f 100644 --- a/simplicio_mapper/_native.py +++ b/simplicio_mapper/_native.py @@ -9,7 +9,7 @@ from __future__ import annotations -from typing import Callable +from collections.abc import Callable HAS_NATIVE: bool = False sha256_hex: Callable[[str], str] | None = None @@ -18,6 +18,8 @@ try: from simplicio_mapper_rs import ( # type: ignore[import-not-found] parse_imports as _native_parse_imports, + ) + from simplicio_mapper_rs import ( sha256_hex as _native_sha256_hex, ) except ImportError: diff --git a/simplicio_mapper/cache.py b/simplicio_mapper/cache.py index 3063d10..4fac1a7 100644 --- a/simplicio_mapper/cache.py +++ b/simplicio_mapper/cache.py @@ -24,7 +24,7 @@ class FileProcessingCache: def __init__(self, cache_dir: str | Path) -> None: self._cache = Cache(str(cache_dir)) - def __enter__(self) -> "FileProcessingCache": + def __enter__(self) -> FileProcessingCache: return self def __exit__(self, exc_type: object, exc: object, tb: object) -> None: @@ -38,7 +38,7 @@ def clear(self) -> None: def make_file_key(self, path: str | Path, size_bytes: int, mtime_ns: int) -> str: normalized = Path(path).as_posix() - raw = f"{self.VERSION}:{normalized}:{size_bytes}:{mtime_ns}".encode("utf-8") + raw = f"{self.VERSION}:{normalized}:{size_bytes}:{mtime_ns}".encode() digest = hashlib.blake2b(raw, digest_size=24).hexdigest() return f"file:{digest}" diff --git a/simplicio_mapper/cli.py b/simplicio_mapper/cli.py index 7e4b197..a7f3365 100644 --- a/simplicio_mapper/cli.py +++ b/simplicio_mapper/cli.py @@ -7,14 +7,14 @@ from __future__ import annotations -import json import hashlib +import json import os import re import subprocess import sys import time -from typing import Sequence +from collections.abc import Sequence from . import __version__ from .mapper import export_architecture_docs, write_architecture_docs, write_mapping_artifacts @@ -80,7 +80,7 @@ def _read_json_safe(file: str) -> dict: try: - with open(file, "r", encoding="utf-8") as handle: + with open(file, encoding="utf-8") as handle: return json.load(handle) except (OSError, ValueError): return {} @@ -318,7 +318,7 @@ def _tree_signature(root: str, out: str) -> dict: except OSError: continue rel = os.path.relpath(path, root).replace(os.sep, "/") - digest.update(f"{rel}\0{stat.st_size}\0{stat.st_mtime_ns}\n".encode("utf-8")) + digest.update(f"{rel}\0{stat.st_size}\0{stat.st_mtime_ns}\n".encode()) return {"kind": "tree", "hash": digest.hexdigest()} @@ -542,7 +542,7 @@ def _endpoint_inventory_for(root: str) -> dict: seen_client: set[tuple[str, str, str]] = set() for file in _endpoint_files(root): try: - with open(file, "r", encoding="utf-8", errors="replace") as handle: + with open(file, encoding="utf-8", errors="replace") as handle: text = handle.read() except OSError: continue @@ -839,7 +839,7 @@ def _screen_inventory_for(root: str) -> dict: entries: list[dict] = [] for file in _screen_files(root): try: - with open(file, "r", encoding="utf-8", errors="replace") as handle: + with open(file, encoding="utf-8", errors="replace") as handle: text = handle.read() except OSError: continue diff --git a/simplicio_mapper/mapper.py b/simplicio_mapper/mapper.py index 44856f6..8374b17 100644 --- a/simplicio_mapper/mapper.py +++ b/simplicio_mapper/mapper.py @@ -14,8 +14,9 @@ import re import shutil import subprocess +from collections.abc import Callable from datetime import datetime, timezone -from typing import Any, Callable +from typing import Any import orjson @@ -96,7 +97,7 @@ def _normalize_rel(file: str) -> str: def _read_safe(file: str) -> str: try: - with open(file, "r", encoding="utf-8", errors="replace") as handle: + with open(file, encoding="utf-8", errors="replace") as handle: return handle.read() except OSError: return ""