Claude
docs(sprint-H.8): cleanup obsolete legacy/shim language in production docstrings
e407ec0 unverified
Raw
History Blame
7.44 kB
"""``MetricRegistry`` β€” Sprint A14-S5.
Container mutable qui associe chaque ``MetricSpec`` Γ  son callable
de calcul. **InstanciΓ© explicitement** par un service au dΓ©marrage
de l'application (cf. ``picarones/app/services/registry_service.py``
au S20) β€” pas de singleton global, pas de side-effect d'import,
pas de dΓ©corateur magique.
DiffΓ©rence avec ``picarones.evaluation.metric_registry``
--------------------------------------------------------
L'autre registre utilise un dict module-level ``_METRIC_REGISTRY``
rempli par un dΓ©corateur ``@register_metric`` appliquΓ© au top-level
d'autres modules. ConsΓ©quence : un ``import picarones`` charge
~50 sous-modules pour amorcer le registre.
Ici, ``MetricRegistry`` est une classe instanciable :
.. code-block:: python
from picarones.domain import ArtifactType
from picarones.domain.evaluation_spec import MetricSpec
from picarones.evaluation.registry import MetricRegistry
reg = MetricRegistry()
reg.register(
MetricSpec(name="cer", input_types=(
ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT,
)),
compute_cer, # callable
)
selected = reg.select(
ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT,
)
Anti-sur-ingΓ©nierie
-------------------
Pas de gestion de versions de mΓ©trique, pas de namespace, pas de
recherche par tag. Si un caller a besoin de ces features, il les
implΓ©mentera quand le besoin sera concret (probablement S15+).
"""
from __future__ import annotations
from typing import Any, Callable
from picarones.domain.artifacts import ArtifactType
from picarones.domain.errors import PicaronesError
from picarones.domain.evaluation_spec import MetricSpec
class MetricRegistrationError(PicaronesError):
"""Tentative d'enregistrement invalide d'une mΓ©trique."""
class MetricNotFoundError(PicaronesError):
"""La mΓ©trique demandΓ©e n'est pas enregistrΓ©e."""
class MetricRegistry:
"""Container mutable de ``MetricSpec`` + callables.
Thread-safe en lecture après initialisation ; la séquence
d'enregistrement attendue est : un seul service, au dΓ©marrage,
enregistre toutes les mΓ©triques en une fois, puis l'instance
est figΓ©e par convention (lecture seule depuis les services
consommateurs).
Pas de mΓ©canisme de freeze technique pour l'instant β€” si un
caller modifie le registre après le bootstrap, c'est de sa
responsabilitΓ©.
"""
def __init__(self) -> None:
self._specs: dict[str, MetricSpec] = {}
self._callables: dict[str, Callable[..., Any]] = {}
# ──────────────────────────────────────────────────────────────────
# Enregistrement
# ──────────────────────────────────────────────────────────────────
def register(self, spec: MetricSpec, func: Callable[..., Any]) -> None:
"""Enregistre une mΓ©trique.
Raises
------
MetricRegistrationError
Si une mΓ©trique du mΓͺme nom est dΓ©jΓ  enregistrΓ©e
(sauf re-enregistrement strict du mΓͺme couple
``(spec, func)``, tolΓ©rΓ© pour les tests qui re-instancient).
"""
if not callable(func):
raise MetricRegistrationError(
f"register({spec.name!r}) : func n'est pas callable."
)
if spec.name in self._specs:
existing_spec = self._specs[spec.name]
existing_func = self._callables[spec.name]
if existing_spec == spec and existing_func is func:
return # idempotent
raise MetricRegistrationError(
f"MΓ©trique {spec.name!r} dΓ©jΓ  enregistrΓ©e avec une "
"autre spec ou un autre callable."
)
self._specs[spec.name] = spec
self._callables[spec.name] = func
# ──────────────────────────────────────────────────────────────────
# Lecture
# ──────────────────────────────────────────────────────────────────
def __contains__(self, name: str) -> bool:
return name in self._specs
def __len__(self) -> int:
return len(self._specs)
def names(self) -> list[str]:
"""Liste des noms enregistrΓ©s (ordre d'enregistrement)."""
return list(self._specs.keys())
def get_spec(self, name: str) -> MetricSpec:
if name not in self._specs:
raise MetricNotFoundError(
f"MΓ©trique {name!r} non enregistrΓ©e. "
f"Disponibles : {sorted(self._specs)}."
)
return self._specs[name]
def get_callable(self, name: str) -> Callable[..., Any]:
if name not in self._callables:
raise MetricNotFoundError(
f"Callable de mΓ©trique {name!r} non enregistrΓ©."
)
return self._callables[name]
def select(
self,
reference_type: ArtifactType,
hypothesis_type: ArtifactType,
) -> list[MetricSpec]:
"""MΓ©triques applicables Γ  une jonction donnΓ©e (signature exacte)."""
target = (reference_type, hypothesis_type)
return [s for s in self._specs.values() if s.input_types == target]
# ──────────────────────────────────────────────────────────────────
# Calcul
# ──────────────────────────────────────────────────────────────────
def compute(
self,
name: str,
reference: Any,
hypothesis: Any,
) -> Any:
"""Calcule la métrique nommée sur la paire (référence, hypothèse).
Aucune capture d'exception : si la métrique lève, l'exception
remonte au caller (qui est typiquement un
``EvaluationViewExecutor`` qui dΓ©cide quoi en faire dans son
``ProjectionReport``).
"""
func = self.get_callable(name)
return func(reference, hypothesis)
def compute_at_junction(
self,
reference: Any,
hypothesis: Any,
reference_type: ArtifactType,
hypothesis_type: ArtifactType,
) -> dict[str, Any]:
"""Calcule **toutes** les mΓ©triques applicables Γ  la jonction.
Retourne ``{metric_name: value}``. Une métrique qui lève
est absente du dict (warning loggΓ© au niveau caller via
l'EvaluationViewExecutor β€” ici on remonte l'exception pour
que les tests dΓ©tectent les bugs).
"""
results: dict[str, Any] = {}
for spec in self.select(reference_type, hypothesis_type):
results[spec.name] = self.compute(spec.name, reference, hypothesis)
return results
__all__ = [
"MetricRegistry",
"MetricRegistrationError",
"MetricNotFoundError",
]