Declare les trois contrats de Zonza

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Ralph Mayola 2026-08-23 17:47:02 +02:00
parent 332afede0e
commit 54479d9af9
2 changed files with 86 additions and 0 deletions

59
contrats.py Normal file
View File

@ -0,0 +1,59 @@
"""Les trois frontières de Zonza.
Chaque contrat est délibérément minuscule : ce sont les seuls points par lesquels
le noyau parle au monde extérieur. Tout ce qui dépend d'un système d'exploitation,
d'une bibliothèque d'affichage ou d'un moteur de transcription vit DERRIÈRE l'un
d'eux — jamais dans `core.py`, `controller.py` ni `audio_level.py`.
"""
from typing import Protocol
class Transcripteur(Protocol):
"""Transforme un signal audio en texte."""
def warmup(self) -> None:
"""Charge le modèle une fois, pour que la première dictée ne traîne pas."""
def transcrire(self, audio) -> str:
"""Rend le texte d'un tableau numpy float32 mono."""
class Overlay(Protocol):
"""La fenêtre flottante affichée pendant la dictée."""
def show(self) -> None:
"""Affiche la fenêtre et démarre l'animation. Thread principal."""
def hide(self) -> None:
"""Masque la fenêtre et arrête l'animation. Thread principal."""
def set_level(self, level: float) -> None:
"""Pousse un niveau audio dans [0, 1]. Appelable depuis un thread de fond."""
def set_status(self, text: str) -> None:
"""Texte affiché dans la bulle. Appelable depuis un thread de fond."""
def rearmer_garde(self, duree_s: float) -> None:
"""Remplace la durée de vie maximale de la fenêtre. Thread principal."""
class Systeme(Protocol):
"""Les appels qui diffèrent d'un système d'exploitation à l'autre."""
def jouer_son(self, nom: str) -> None:
"""Joue « start », « done » ou « error » sans bloquer."""
def modificateurs_enfonces(self) -> bool:
"""True si une touche modificatrice est physiquement enfoncée."""
def raccourci_defaut(self) -> str:
"""Raccourci global au format pynput, propre à la plateforme."""
def chemin_log(self) -> str:
"""Chemin absolu du journal."""
def dossier_donnees(self) -> str:
"""Dossier des données de l'application (verrou d'instance, réglages)."""
def acquerir_verrou_instance(self, chemin: str):
"""Verrou exclusif non bloquant ; rend le fichier, ou None s'il est pris."""

27
tests/test_contrats.py Normal file
View File

@ -0,0 +1,27 @@
"""Une suite unique, rejouée contre TOUTES les implémentations d'un contrat.
Le jour une implémentation oublie une méthode, c'est ici que ça rougit — y
compris pour les plateformes absentes de la machine de développement.
"""
import inspect
import pytest
from contrats import Overlay, Systeme, Transcripteur
@pytest.mark.parametrize("contrat, methodes", [
(Transcripteur, ["warmup", "transcrire"]),
(Overlay, ["show", "hide", "set_level", "set_status", "rearmer_garde"]),
(Systeme, ["jouer_son", "modificateurs_enfonces", "raccourci_defaut",
"chemin_log", "dossier_donnees", "acquerir_verrou_instance"]),
])
def test_le_contrat_declare_exactement_ses_methodes(contrat, methodes):
declarees = [n for n, _ in inspect.getmembers(contrat, inspect.isfunction)
if not n.startswith("_")]
assert sorted(declarees) == sorted(methodes)
def test_les_contrats_sont_documentes():
for contrat in (Transcripteur, Overlay, Systeme):
assert contrat.__doc__, f"{contrat.__name__} doit dire à quoi il sert"