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