Spaces:
Sleeping
Sleeping
| """Garde-fou informatif : pour chaque couche listée dans | |
| ``specification.md § 2``, vérifier que les modules réellement | |
| présents dans le code correspondent (à ±N près) à ceux annoncés | |
| dans le tableau. | |
| Stratégie | |
| --------- | |
| Ce test est volontairement *informatif* (utilise ``pytest.warns`` | |
| plutôt que ``assert``) parce que : | |
| - On ne veut pas qu'un PR mineur (ajout d'un helper, renommage) | |
| fasse échouer le test doc. | |
| - Le doc utilise des grappes (« registry/, evaluation_engine.py, | |
| projection_engine.py… ») et énumérer chaque fichier serait | |
| contre-productif éditorialement. | |
| - Le vrai garde-fou contre les modules fantômes est | |
| ``test_specification_module_paths.py`` (qui vérifie que tout | |
| chemin mentionné existe). | |
| Ce test détecte le drift inverse : un module *présent dans le | |
| code* mais *absent du tableau de la spec*. Quand l'écart dépasse | |
| une tolérance, le test émet un ``UserWarning`` listant les | |
| modules orphelins — au reviewer de décider si c'est intentionnel | |
| (helper interne) ou si la spec doit être mise à jour. | |
| Pour bloquer ponctuellement : ``pytest -W error::UserWarning | |
| tests/docs/test_specification_layer_inventory.py``. | |
| """ | |
| from __future__ import annotations | |
| import warnings | |
| from pathlib import Path | |
| import pytest | |
| _REPO_ROOT = Path(__file__).resolve().parents[2] | |
| _SPEC = _REPO_ROOT / "docs" / "reference" / "specification.md" | |
| #: Modules réels du code qu'on accepte de ne pas mentionner dans | |
| #: la spec (helpers internes, fichiers privés). | |
| _OK_TO_OMIT_PATTERNS: tuple[str, ...] = ( | |
| "__init__.py", | |
| "__pycache__", | |
| "_version_fallback.py", | |
| ) | |
| def _real_modules(layer_dir: Path) -> set[str]: | |
| """Liste les modules ``.py`` réels dans une couche (non récursif). | |
| Sous-paquets (dossiers) inclus, par leur nom de répertoire. | |
| """ | |
| if not layer_dir.is_dir(): | |
| return set() | |
| modules: set[str] = set() | |
| for entry in layer_dir.iterdir(): | |
| if entry.name.startswith(".") or entry.name in {"__pycache__"}: | |
| continue | |
| if entry.is_file() and entry.suffix == ".py": | |
| if any(entry.name == p for p in _OK_TO_OMIT_PATTERNS): | |
| continue | |
| modules.add(entry.stem) | |
| elif entry.is_dir() and (entry / "__init__.py").exists(): | |
| modules.add(entry.name) | |
| return modules | |
| def _spec_modules_for_layer(layer_section_heading: str) -> set[str]: | |
| """Extrait les noms de modules backquotés ``…`` du tableau qui | |
| suit la section ``layer_section_heading`` dans la spec. | |
| """ | |
| text = _SPEC.read_text(encoding="utf-8") | |
| idx = text.find(layer_section_heading) | |
| if idx < 0: | |
| return set() | |
| # Section finit à la prochaine section ``###`` de même niveau. | |
| end = text.find("\n### ", idx + len(layer_section_heading)) | |
| section = text[idx:end] if end > 0 else text[idx:] | |
| # Tokens backquotés terminant par .py ou nom de dossier suivi de / | |
| import re | |
| found: set[str] = set() | |
| for m in re.finditer(r"`([a-zA-Z_][a-zA-Z0-9_]*)\.py`", section): | |
| found.add(m.group(1)) | |
| for m in re.finditer(r"`([a-zA-Z_][a-zA-Z0-9_]*)/`", section): | |
| found.add(m.group(1)) | |
| return found | |
| #: Couches à comparer : (section heading dans la spec, chemin filesystem, | |
| #: tolérance d'écart pour ne pas warner). | |
| _LAYERS_TO_CHECK: tuple[tuple[str, str, int], ...] = ( | |
| ("### 2.1 `picarones/domain/`", "picarones/domain", 2), | |
| ("### 2.4 `picarones/pipeline/`", "picarones/pipeline", 3), | |
| ) | |
| def test_layer_inventory_drift_warns( | |
| heading: str, | |
| layer_path: str, | |
| tolerance: int, | |
| ) -> None: | |
| """Informatif : signale les modules présents dans le code mais | |
| pas dans la spec, au-delà d'une tolérance. | |
| """ | |
| real = _real_modules(_REPO_ROOT / layer_path) | |
| spec = _spec_modules_for_layer(heading) | |
| missing_from_spec = real - spec | |
| if len(missing_from_spec) > tolerance: | |
| warnings.warn( | |
| ( | |
| f"\n Couche {layer_path!r} :\n" | |
| f" - réels (code) : {sorted(real)}\n" | |
| f" - listés (spec): {sorted(spec)}\n" | |
| f" - non listés (>{tolerance} de tolérance) : " | |
| f"{sorted(missing_from_spec)}\n" | |
| f" Mettre à jour spec.md § {heading.split('`')[0].strip()} " | |
| f"si ces modules sont publiquement intéressants." | |
| ), | |
| UserWarning, | |
| stacklevel=2, | |
| ) | |