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