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é."
    )