Picarones / tests /core /test_public_api_signatures.py
Claude
feat(ci): Sprint A1 — Hardening CI (B-7, B-8, M-4, M-15, m-7, m-8, m-9)
89d5b21 unverified
Raw
History Blame
5.79 kB
"""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é."
)