Picarones / tests /architecture /test_output_paths_uniformity.py
Claude
refactor: audit S58 — 2 HIGH + 4 MED + 4 LOW bricolages corrigés
c21d686 unverified
Raw
History Blame
3.42 kB
"""Garde-fou : tous les adapters qui écrivent un output passent par
``resolve_output_path``.
L'audit S58 a relevé que S51 (helper de résolution de chemin pour
respecter ``context.workspace_uri``) n'était appliqué qu'à 1 OCR sur
5 + LLM/VLM. Les 4 autres OCR (Pero, Mistral, Google Vision, Azure
DI) écrivaient encore directement dans ``image_path.parent``,
plantant en mode read-only mount — exactement le problème que S51
prétendait régler.
Ce test rejette tout ``image_path.parent / f"{stem}.{name}.txt"``
ou variante équivalente dans les modules d'adapter (OCR/LLM/VLM).
La forme canonique unique est ``resolve_output_path(...)``.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
#: Modules à scanner — tous les adapters qui produisent des fichiers
#: de sortie.
ADAPTER_DIRS: tuple[Path, ...] = (
REPO_ROOT / "picarones" / "adapters" / "ocr",
REPO_ROOT / "picarones" / "adapters" / "llm",
REPO_ROOT / "picarones" / "adapters" / "vlm",
)
#: Module canonique qui définit le helper — exempté du test.
HELPER_MODULE: Path = (
REPO_ROOT / "picarones" / "adapters" / "output_paths.py"
)
#: Modules exemptés avec justification.
#:
#: - ``ocr/precomputed.py`` : adapter qui **lit** un texte pré-calculé
#: placé manuellement à côté de l'image par l'utilisateur. Le
#: ``image_path.parent`` est l'emplacement attendu de l'**input**,
#: pas une sortie produite par l'adapter. La sémantique attendue
#: par les utilisateurs est précisément « cherche à côté de
#: l'image » — déplacer ça vers le workspace casserait l'usage
#: documenté.
EXEMPTED: frozenset[Path] = frozenset({
REPO_ROOT / "picarones" / "adapters" / "ocr" / "precomputed.py",
})
#: Pattern interdit : écriture directe à côté de l'image source.
#: ``image_path.parent / f"…"`` ou ``input_path.parent / f"…"``.
FORBIDDEN_PATTERN: re.Pattern[str] = re.compile(
r"(?:image_path|input_path|img_path)\s*\.\s*parent\s*/\s*f[\"']",
)
def _adapter_files() -> list[Path]:
files: list[Path] = []
for d in ADAPTER_DIRS:
if d.exists():
files.extend(
p for p in d.rglob("*.py")
if p != HELPER_MODULE and p not in EXEMPTED
)
return sorted(files)
def test_adapters_write_via_resolve_output_path() -> None:
"""Aucun adapter ne contourne ``resolve_output_path``."""
offenders: list[tuple[str, int, str]] = []
for f in _adapter_files():
try:
text = f.read_text(encoding="utf-8")
except OSError:
continue
for i, line in enumerate(text.splitlines(), start=1):
if FORBIDDEN_PATTERN.search(line):
rel = f.relative_to(REPO_ROOT).as_posix()
offenders.append((rel, i, line.strip()))
if offenders:
sample = "\n".join(
f" {p}:{n}{s}" for p, n, s in offenders[:10]
)
raise AssertionError(
f"\n{len(offenders)} adapter(s) écrivent à côté de "
"l'image source au lieu de passer par "
"``resolve_output_path``. Cela casse les corpus "
"montés en read-only.\n\n"
f"{sample}\n\n"
"Remplacer par ``resolve_output_path(input_path=...,"
" adapter_name=self.name, suffix=..., context=context)``."
)