Spaces:
Sleeping
Sleeping
File size: 5,788 Bytes
89d5b21 | 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 143 144 145 | """Garde-fou contractuel sur les signatures de l'API publique de ``picarones``.
Sprint A1 (item m-9 de l'audit institutional-readiness-2026-05).
Le module ``tests/core/test_public_api.py`` vérifie déjà *quels* symboles
sont exportés. Ce module-ci verrouille en plus les **valeurs par défaut**
des paramètres des fonctions publiques. Sans ce verrou, un PR peut
silencieusement changer un défaut documenté (ex : ``corpus_lang="fr"``
qui devient ``corpus_lang="en"``) et casser la rétrocompatibilité de
tous les consommateurs externes — y compris des notebooks de chercheurs
pinés sur une version mineure.
Convention : pour ajouter un nouveau paramètre par défaut, mettre à jour
ce fichier ET la documentation publique (CHANGELOG + ``docs/api-stable.md``).
"""
from __future__ import annotations
import inspect
from typing import Any
import pytest
import picarones
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _signature_defaults(callable_obj: Any) -> dict[str, Any]:
"""Retourne ``{nom_param: default_value}`` pour les paramètres avec défaut.
Les paramètres sans défaut (positionnels obligatoires) sont omis.
"""
sig = inspect.signature(callable_obj)
return {
name: param.default
for name, param in sig.parameters.items()
if param.default is not inspect.Parameter.empty
}
# ---------------------------------------------------------------------------
# load_corpus_from_directory
# ---------------------------------------------------------------------------
def test_load_corpus_from_directory_defaults() -> None:
"""``load_corpus_from_directory`` est l'entrée canonique pour charger un
corpus depuis un dossier. Ses défauts sont contractuels."""
defaults = _signature_defaults(picarones.load_corpus_from_directory)
# Ces clés DOIVENT exister. Si l'une est supprimée, c'est un breaking
# change qui mérite un tag majeur.
assert "name" in defaults, (
"load_corpus_from_directory(name=…) doit avoir un défaut "
"(actuellement on accepte None pour déduire du nom de dossier)."
)
# Le défaut historique de ``name`` est ``None`` (déduction depuis le
# nom du dossier). Tout changement vers une chaîne fixe casserait les
# appelants qui s'appuient sur cette déduction.
assert defaults["name"] is None
# ---------------------------------------------------------------------------
# Symboles publics : pas d'arguments positionnels uniquement non-typés
# ---------------------------------------------------------------------------
def _is_public_callable(name: str) -> bool:
"""Filtre les symboles publics de ``picarones`` qui sont appelables."""
if name.startswith("_"):
return False
obj = getattr(picarones, name, None)
return callable(obj) and not isinstance(obj, type(picarones))
@pytest.mark.parametrize("symbol", [s for s in picarones.__all__ if _is_public_callable(s)])
def test_public_callable_has_typed_signature(symbol: str) -> None:
"""Toute fonction publique doit avoir des annotations de type.
Ce garde-fou prépare le passage en strict mypy (Sprint A1, M-4).
Les classes (Corpus, Document, etc.) sont exclues — leur ``__init__``
est testé séparément si nécessaire, mais beaucoup sont des dataclasses
déjà annotées par construction.
"""
obj = getattr(picarones, symbol)
if isinstance(obj, type):
# Les classes sont validées via mypy strict sur core/, pas ici.
return
sig = inspect.signature(obj)
for param_name, param in sig.parameters.items():
if param_name in ("self", "cls"):
continue
assert param.annotation is not inspect.Parameter.empty, (
f"Paramètre `{param_name}` de `picarones.{symbol}` non annoté. "
f"L'API publique exige un typage explicite (Sprint A1)."
)
# ---------------------------------------------------------------------------
# compute_at_junction (registre typé Sprint 34)
# ---------------------------------------------------------------------------
def test_compute_at_junction_defaults() -> None:
"""``compute_at_junction`` est l'API consommée par les pipelines composées
(Sprint 63+). Ses défauts contractuels :
- ``metric_name`` n'a PAS de défaut (on doit toujours préciser la métrique).
"""
defaults = _signature_defaults(picarones.compute_at_junction)
assert "metric_name" not in defaults, (
"compute_at_junction doit exiger metric_name explicite. "
"Un défaut introduirait de l'ambiguïté sur la métrique calculée."
)
# ---------------------------------------------------------------------------
# select_metrics (registre typé Sprint 34)
# ---------------------------------------------------------------------------
def test_select_metrics_signature() -> None:
"""``select_metrics(input_type, output_type)`` est purement positionnel
sur ses deux types — pas de défauts implicites."""
defaults = _signature_defaults(picarones.select_metrics)
assert "input_type" not in defaults
assert "output_type" not in defaults
# ---------------------------------------------------------------------------
# Méta-test : tout symbole de __all__ existe vraiment
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("symbol", picarones.__all__)
def test_all_symbols_resolve(symbol: str) -> None:
"""Chaque entrée de ``__all__`` doit pouvoir être résolue."""
assert hasattr(picarones, symbol), (
f"`picarones.{symbol}` est dans __all__ mais n'est pas exporté."
)
|