feat(cli): option --engines, diagnostic honnête des moteurs du build CLI
Axe CLI (intégration dans d'autres programmes) : contrat stable, codes retour fiables. `--engines` liste les moteurs réellement disponibles dans CET exécutable CLI (`[OUI]/[NON] Label (requis/optionnel) — raison`) et sort 0, sans traiter. `input` devient optionnel pour ce mode (sinon code 2). Le fail-closed CamemBERT (code 3) et le best-effort EDS/GLiNER (jamais déclarés actifs si le chargement échoue) restent inchangés. Ne présume pas du build GUI. 2 tests (--engines → code 0 + moteurs listés ; absence d'input → code 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -103,12 +103,29 @@ def _mandatory_model_path() -> Path:
|
||||
return _APP_DIR / "models" / "camembert-bio-deid" / "onnx" / "model.onnx"
|
||||
|
||||
|
||||
def _print_engines() -> int:
|
||||
"""Affiche les moteurs réellement disponibles dans cet exécutable.
|
||||
|
||||
Diagnostic « honnête » : ne déclare jamais disponible un moteur dont les
|
||||
dépendances (ou le modèle, pour CamemBERT) ne chargent pas. Sortie 0.
|
||||
"""
|
||||
from engine_capabilities import capabilities_map
|
||||
|
||||
caps = capabilities_map()
|
||||
print("Moteurs d'anonymisation — disponibilité dans cet exécutable :")
|
||||
for cap in caps.values():
|
||||
mark = "OUI" if cap.available else "NON"
|
||||
flag = "requis" if cap.required else "optionnel"
|
||||
print(f" [{mark}] {cap.label} ({flag}) — {cap.reason}")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="Anonymisation-CLI",
|
||||
description="Anonymise un fichier (ou dossier) sans GUI.",
|
||||
)
|
||||
ap.add_argument("input", help="Fichier unique existant (ou dossier parcouru récursivement)")
|
||||
ap.add_argument("input", nargs="?", default=None, help="Fichier unique existant (ou dossier parcouru récursivement)")
|
||||
ap.add_argument(
|
||||
"output", nargs="?", default=None,
|
||||
help="Dossier de sortie (forme positionnelle). Créé si absent.",
|
||||
@@ -117,9 +134,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
ap.add_argument("--limit", type=int, default=0, help="Nombre max de documents (0 = tous ; utile pour un dossier)")
|
||||
ap.add_argument("--no-ner", action="store_true", help="Mode regex seul : désactive EDS-Pseudo + CamemBERT (aucun modèle obligatoire)")
|
||||
ap.add_argument("--gliner", action="store_true", help="Active aussi GLiNER (optionnel, vote croisé)")
|
||||
ap.add_argument("--engines", action="store_true", help="Liste les moteurs réellement disponibles dans cet exécutable et quitte (diagnostic honnête, code 0)")
|
||||
ap.add_argument("--config", default=None, help="Chemin config dictionnaires.yml (défaut: runtime)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# --- Diagnostic moteurs : honnêteté sur ce que le build embarque réellement ---
|
||||
if args.engines:
|
||||
return _print_engines()
|
||||
|
||||
if args.input is None:
|
||||
ap.error("argument 'input' requis (sauf avec --engines)")
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
@@ -179,6 +204,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
eds_mgr.load()
|
||||
log.info("CLI: EDS-Pseudo chargé (optionnel) ✓")
|
||||
except Exception as e: # noqa: BLE001
|
||||
eds_mgr = None
|
||||
log.warning("CLI: EDS-Pseudo (optionnel) INDISPONIBLE: %s — traitement poursuivi sans.", e)
|
||||
|
||||
# OPTIONNEL : GLiNER (sur demande).
|
||||
@@ -189,6 +215,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
gliner_mgr.load()
|
||||
log.info("CLI: GLiNER chargé (optionnel) ✓")
|
||||
except Exception as e: # noqa: BLE001
|
||||
gliner_mgr = None
|
||||
log.warning("CLI: GLiNER (optionnel) INDISPONIBLE: %s — traitement poursuivi sans.", e)
|
||||
else:
|
||||
log.warning("CLI: --no-ner -> MODE REGEX SEUL assumé (aucun modèle NER). "
|
||||
|
||||
40
tests/unit/test_cli_engines_diagnostic.py
Normal file
40
tests/unit/test_cli_engines_diagnostic.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Le CLI expose `--engines` : diagnostic honnête des moteurs embarqués (code 0).
|
||||
|
||||
On charge le module CLI par chemin (il n'est pas packagé) et on vérifie que
|
||||
`--engines` liste les moteurs et sort 0, sans exiger d'argument `input`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_CLI_PATH = Path(__file__).resolve().parents[2] / "scripts" / "anonymize_cli.py"
|
||||
|
||||
|
||||
def _load_cli():
|
||||
spec = importlib.util.spec_from_file_location("anonymize_cli_undertest", _CLI_PATH)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def test_engines_flag_lists_engines_and_exits_zero(capsys):
|
||||
cli = _load_cli()
|
||||
rc = cli.main(["--engines"])
|
||||
assert rc == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "Moteurs d'anonymisation" in out
|
||||
# les 3 moteurs connus apparaissent dans le diagnostic
|
||||
assert "CamemBERT-bio" in out
|
||||
assert "EDS-Pseudo" in out
|
||||
assert "GLiNER" in out
|
||||
|
||||
|
||||
def test_no_input_without_engines_errors(capsys):
|
||||
cli = _load_cli()
|
||||
with pytest.raises(SystemExit) as exc: # argparse error => exit 2
|
||||
cli.main([])
|
||||
assert exc.value.code == 2
|
||||
Reference in New Issue
Block a user