Spaces:
Running
Running
| """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)``." | |
| ) | |