Spaces:
Sleeping
Sleeping
File size: 4,512 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 | """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),
)
@pytest.mark.parametrize(
"heading,layer_path,tolerance",
_LAYERS_TO_CHECK,
)
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,
)
|