From 890edb360e97c8f844d37f0d28a3486f0d18f505 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Tue, 16 Jun 2026 17:38:56 +0200 Subject: [PATCH] =?UTF-8?q?feat(cli):=20option=20--engines,=20diagnostic?= =?UTF-8?q?=20honn=C3=AAte=20des=20moteurs=20du=20build=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/anonymize_cli.py | 29 +++++++++++++++- tests/unit/test_cli_engines_diagnostic.py | 40 +++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_cli_engines_diagnostic.py diff --git a/scripts/anonymize_cli.py b/scripts/anonymize_cli.py index efd000f..631447f 100644 --- a/scripts/anonymize_cli.py +++ b/scripts/anonymize_cli.py @@ -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). " diff --git a/tests/unit/test_cli_engines_diagnostic.py b/tests/unit/test_cli_engines_diagnostic.py new file mode 100644 index 0000000..10e1cde --- /dev/null +++ b/tests/unit/test_cli_engines_diagnostic.py @@ -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