Spaces:
Sleeping
Sleeping
File size: 5,054 Bytes
ee86836 | 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 133 134 135 136 137 138 139 140 141 142 | """Garde-fou contre les modules sans consommateur en production.
Chaque module dans ``picarones/measurements/`` doit être importé par
au moins un fichier de production (hors lui-même, hors ``tests/``).
Sinon le module est *test-only* — sa couverture de test est haute mais
il n'est branché à rien dans le pipeline réel.
Snapshot v1.0.0 (2026-05-02) : **12 modules** dans ``measurements/``
n'ont aucun consommateur direct hors tests :
- ``alto_metrics``, ``baseline_comparison``, ``builtin_metrics``,
``cost_projection``, ``equivalence_profile``, ``layout``,
``marginal_cost``, ``ner_backends``, ``rare_tokens``,
``reading_order``, ``taxonomy_cooccurrence``,
``taxonomy_intra_doc``.
Trois actions possibles, par module :
1. **Câbler** dans le runner ou un renderer (le module devient un
produit, pas une expérience).
2. **Déplacer** vers ``picarones/extras/`` si c'est expérimental
et non livré dans le pipeline standard.
3. **Retirer** si c'est mort (le travail reste dans l'historique git).
Test ratchet :
- Tout module ``measurements/X.py`` qui devient test-only sans entrer
dans la baseline → échec (régression).
- Tout module de la baseline qui gagne un consommateur → échec
jusqu'à ce que la baseline soit mise à jour pour verrouiller le gain.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
PICARONES_DIR = REPO_ROOT / "picarones"
MEASUREMENTS_DIR = PICARONES_DIR / "measurements"
#: Snapshot v1.0.0. Modules de ``picarones/measurements/`` sans
#: consommateur en production. À résorber par paliers.
TEST_ONLY_BASELINE: frozenset[str] = frozenset({
"alto_metrics",
"baseline_comparison",
"builtin_metrics",
"cost_projection",
"equivalence_profile",
"layout",
"marginal_cost",
"ner_backends",
"rare_tokens",
"reading_order",
"taxonomy_cooccurrence",
"taxonomy_intra_doc",
})
def _measurements_modules() -> list[str]:
return sorted(
p.stem
for p in MEASUREMENTS_DIR.glob("*.py")
if p.stem != "__init__"
)
def _has_production_consumer(module_name: str) -> bool:
"""True si ``module_name`` est importé par un fichier de production.
"Production" = sous ``picarones/``, hors le module lui-même.
On accepte les imports absolus (``from picarones.measurements.X``
et ``import picarones.measurements.X``) ainsi que les imports
relatifs depuis le package ``measurements`` (``from .X``).
"""
own_file = MEASUREMENTS_DIR / f"{module_name}.py"
absolute_pattern = re.compile(
rf"\bfrom\s+picarones\.measurements\.{re.escape(module_name)}\b"
rf"|\bimport\s+picarones\.measurements\.{re.escape(module_name)}\b"
)
relative_pattern = re.compile(
rf"\bfrom\s+\.\s*{re.escape(module_name)}\b"
rf"|\bfrom\s+\.measurements\.{re.escape(module_name)}\b"
)
for path in PICARONES_DIR.rglob("*.py"):
if path == own_file:
continue
try:
text = path.read_text(encoding="utf-8")
except OSError:
continue
if absolute_pattern.search(text):
return True
# Imports relatifs : ne sont valides que depuis l'arbre measurements.
try:
path.relative_to(MEASUREMENTS_DIR)
except ValueError:
continue
if relative_pattern.search(text):
return True
return False
def _test_only_modules() -> frozenset[str]:
return frozenset(
m for m in _measurements_modules()
if not _has_production_consumer(m)
)
def test_no_new_test_only_modules() -> None:
"""Aucun module ne doit devenir test-only sans entrer dans la baseline."""
current = _test_only_modules()
new = current - TEST_ONLY_BASELINE
assert not new, (
f"\n{len(new)} module(s) de measurements/ sans consommateur en "
f"production : {sorted(new)}.\n\n"
"Choisis l'une des trois options :\n"
" 1. Câble le module dans le runner ou un renderer.\n"
" 2. Déplace-le sous picarones/extras/ s'il est expérimental.\n"
" 3. Retire-le si c'est mort.\n\n"
"En dernier recours, ajoute son nom à TEST_ONLY_BASELINE dans "
"tests/architecture/test_module_coverage.py — c'est admettre "
"consciemment qu'il vit hors du pipeline standard."
)
def test_baseline_modules_still_orphaned() -> None:
"""Si un module de la baseline a gagné un consommateur, lock le gain.
Force à mettre à jour la baseline pour verrouiller chaque câblage,
sinon une régression future re-deviendrait test-only sans alerte.
"""
current = _test_only_modules()
fixed = TEST_ONLY_BASELINE - current
assert not fixed, (
f"\nExcellent : {len(fixed)} module(s) ont gagné un consommateur en "
f"production : {sorted(fixed)}.\n\n"
"Retire ces noms de TEST_ONLY_BASELINE dans "
"tests/architecture/test_module_coverage.py pour verrouiller le gain."
)
|