Spaces:
Sleeping
Sleeping
File size: 4,915 Bytes
44c1e95 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | """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."
)
|