Picarones / tests /docs /test_specification_module_paths.py
Claude
test(docs): 4 garde-fous contre les regressions du drift documentaire
44c1e95 unverified
Raw
History Blame Contribute Delete
4.92 kB
"""Garde-fou : tout chemin de module Python mentionné dans
``docs/reference/specification.md`` doit être importable.
Le drift le plus dommageable identifié dans l'audit de mai 2026
était l'ensemble des chemins de modules listés dans la section
« Architecture » qui n'existaient plus dans le code (paquets
``picarones.engines``, ``picarones.measurements``,
``picarones.core``, etc. supprimés à la release 0.9.0).
Ce test extrait tous les jetons qui ressemblent à un chemin de
module dans le doc — ``picarones.<...>`` ou ``picarones/<...>``
ou des constructions équivalentes — et vérifie qu'ils s'importent.
Les paths qui ne sont pas des modules importables (sous-paquets
sans ``__init__.py``, fichiers internes, helpers) sont
explicitement whitelistés ci-dessous.
Portée
------
Spécifique à ``docs/reference/specification.md``. Les autres docs
ont leur propre garde-fou (cf. ``test_doc_paths.py`` pour les
liens cassés, ``test_views_md_consistency.py`` pour les vues).
"""
from __future__ import annotations
import importlib
import re
from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[2]
_SPEC = _REPO_ROOT / "docs" / "reference" / "specification.md"
#: Chemins extraits du doc qui sont mentionnés au passé comme
#: historiques (paquets retirés à la release 0.9.0) et qui ne
#: doivent **pas** être importables. Cette whitelist trace la
#: mémoire institutionnelle ; le test ``test_whitelist_paths_
#: actually_dont_resolve`` la garde épurée.
_HISTORICAL_REMOVED: frozenset[str] = frozenset({
"picarones.core",
"picarones.measurements",
"picarones.engines",
"picarones.modules",
"picarones.report",
"picarones.llm",
"picarones.pipelines",
"picarones.cli",
"picarones.web",
"picarones.extras",
})
#: Pattern qui capture les jetons ``picarones.<identifier>(.identifier)*``
#: en évitant les fins de phrase (point suivi d'espace ou fin de ligne).
_MODULE_PATTERN = re.compile(
r"\bpicarones(?:\.[a-zA-Z_][a-zA-Z0-9_]*)+(?=[`\s,)\]\.]|$)",
re.MULTILINE,
)
def _extract_module_paths(text: str) -> set[str]:
"""Extrait les chemins de modules ``picarones.<...>`` du texte."""
# On retire les blocs de code YAML/console qui peuvent contenir
# des exemples illustratifs sans contrainte d'importabilité.
# Mais on garde les blocs Python qui DOIVENT être valides.
paths: set[str] = set()
for match in _MODULE_PATTERN.finditer(text):
path = match.group(0).rstrip(".")
# Retire les trailing class names (CapitalizedIdentifier) — on
# ne teste que les modules, pas les attributs.
parts = path.split(".")
# Si le dernier segment commence par une majuscule, c'est
# vraisemblablement une classe ou un constant → strip.
while parts and parts[-1][:1].isupper():
parts.pop()
if len(parts) >= 2:
paths.add(".".join(parts))
return paths
def _module_importable(path: str) -> bool:
"""``True`` si ``path`` est un module Python qui s'importe."""
try:
importlib.import_module(path)
return True
except (ImportError, ModuleNotFoundError):
return False
def test_specification_module_paths_resolve() -> None:
"""Tout chemin ``picarones.<...>`` mentionné dans specification.md
doit être importable ou whitelisté.
Si ce test échoue : soit corriger le chemin dans le doc, soit
ajouter le chemin à ``_WHITELIST`` avec une justification dans
le commentaire correspondant.
"""
text = _SPEC.read_text(encoding="utf-8")
paths = _extract_module_paths(text)
broken: list[str] = []
for path in sorted(paths):
if path in _HISTORICAL_REMOVED:
continue
if not _module_importable(path):
broken.append(path)
assert not broken, (
f"Chemins de modules cassés dans {_SPEC.relative_to(_REPO_ROOT)} : "
+ ", ".join(broken)
+ " — soit corriger le doc, soit ajouter à _HISTORICAL_REMOVED "
"avec un commentaire justifiant la mémoire historique."
)
def test_historical_paths_actually_dont_resolve() -> None:
"""Garde-fou méta : un path marqué « historiquement supprimé »
qui devient à nouveau importable signale qu'il a été réintroduit
(volontairement ou par accident).
Si ce test échoue : soit le paquet est légitimement de retour
(retirer l'entrée de ``_HISTORICAL_REMOVED``), soit il a été
réintroduit par erreur (audit).
"""
unexpectedly_alive: list[str] = []
for path in _HISTORICAL_REMOVED:
if _module_importable(path):
unexpectedly_alive.append(path)
assert not unexpectedly_alive, (
"Paquets historiquement supprimés mais qui s'importent à nouveau : "
+ ", ".join(unexpectedly_alive)
+ " — vérifier que c'est une réintroduction volontaire."
)