diff --git a/anonymisation_cli_onefile.spec b/anonymisation_cli_onefile.spec index 2e2a4e1..ed91471 100644 --- a/anonymisation_cli_onefile.spec +++ b/anonymisation_cli_onefile.spec @@ -114,11 +114,6 @@ hiddenimports = [ "yaml", "loguru", "regex", - "optimum", - "optimum.onnxruntime", - "optimum.pipelines", - "optimum.modeling_base", - "optimum.exporters.onnx", ] @@ -153,12 +148,24 @@ for _package_name in [ _collect_optional_package(_package_name) +# P0-3 (Plan 3) : exclusion dure de la pile torch. Le core fait un +# `import torch` lazy (try/except no-op) dans _configure_torch_threads que +# l'analyse statique suivrait ; en frozen l'ImportError est attendue et gérée. +EXCLUDED_TORCH_STACK = [ + "torch", + "torchvision", + "optimum", + "doctr", +] + + a = Analysis( [str(project_dir / "scripts" / "anonymize_cli.py")], pathex=[str(project_dir)], binaries=binaries, datas=datas, hiddenimports=hiddenimports, + excludes=EXCLUDED_TORCH_STACK, cipher=block_cipher, noarchive=False, ) diff --git a/anonymisation_gui_v6_onefile.spec b/anonymisation_gui_v6_onefile.spec index 5b93036..a7f39f4 100644 --- a/anonymisation_gui_v6_onefile.spec +++ b/anonymisation_gui_v6_onefile.spec @@ -135,11 +135,16 @@ hiddenimports = [ "yaml", "loguru", "regex", +] + +# P0-3 (Plan 3) : exclusion dure de la pile torch. Le core fait un +# `import torch` lazy (try/except no-op) dans _configure_torch_threads que +# l'analyse statique suivrait ; en frozen l'ImportError est attendue et gérée. +EXCLUDED_TORCH_STACK = [ + "torch", + "torchvision", "optimum", - "optimum.onnxruntime", - "optimum.pipelines", - "optimum.modeling_base", - "optimum.exporters.onnx", + "doctr", ] @@ -148,6 +153,7 @@ a = Analysis( pathex=[str(project_dir)], datas=datas, hiddenimports=hiddenimports, + excludes=EXCLUDED_TORCH_STACK, cipher=block_cipher, noarchive=False, ) diff --git a/tests/unit/test_build_specs_torch_free.py b/tests/unit/test_build_specs_torch_free.py new file mode 100644 index 0000000..db144e3 --- /dev/null +++ b/tests/unit/test_build_specs_torch_free.py @@ -0,0 +1,68 @@ +"""Anti-dérive P0-3 (Plan 3) : les specs frozen GUI V6 et CLI doivent être torch-free. + +On vérifie le TEXTE des specs (pas d'exécution PyInstaller sous Linux) : +- aucun hiddenimport optimum*/torch*/doctr* ; +- excludes explicites présents (torch, torchvision, optimum, doctr) — ceinture + et bretelles : même si le venv de build contient torch, l'analyse l'exclut + (le core fait un `import torch` lazy dans _configure_torch_threads, que + l'analyse statique de PyInstaller suivrait sans excludes). +La spec GUI V5 legacy (anonymisation_onefile.spec) n'est PAS concernée. +""" +import re +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +TORCH_FREE_SPECS = [ + ROOT / "anonymisation_gui_v6_onefile.spec", + ROOT / "anonymisation_cli_onefile.spec", +] +FORBIDDEN_HIDDEN = ("optimum", "torch", "torchvision", "doctr") +REQUIRED_EXCLUDES = ("torch", "torchvision", "optimum", "doctr") + + +def _hiddenimport_strings(text): + """Retourne les chaînes littérales présentes dans la section hiddenimports=[...]. + + On extrait uniquement la portion du texte entre `hiddenimports = [` et le `]` + de fermeture correspondant (premier `]` sur une ligne seule), afin d'éviter + les faux positifs générés par le bloc EXCLUDED_TORCH_STACK lui-même qui + contient légitimement "torch", "optimum", etc. comme valeurs d'exclusion. + """ + # Cherche le bloc hiddenimports = [ ... ] + match = re.search(r"hiddenimports\s*=\s*\[([^\]]*)\]", text, re.DOTALL) + if not match: + return [] + block = match.group(1) + return re.findall(r"[\"']([A-Za-z0-9_.]+)[\"']", block) + + +@pytest.mark.parametrize("spec_path", TORCH_FREE_SPECS, ids=lambda p: p.name) +def test_spec_sans_hiddenimport_torch_optimum_doctr(spec_path): + text = spec_path.read_text(encoding="utf-8") + hits = [ + s for s in _hiddenimport_strings(text) + if s.split(".")[0] in FORBIDDEN_HIDDEN + ] + assert hits == [], f"{spec_path.name} référence encore : {hits}" + + +@pytest.mark.parametrize("spec_path", TORCH_FREE_SPECS, ids=lambda p: p.name) +def test_spec_declare_excludes_torch(spec_path): + text = spec_path.read_text(encoding="utf-8") + assert "excludes=EXCLUDED_TORCH_STACK" in text, ( + f"{spec_path.name} : Analysis() sans excludes=EXCLUDED_TORCH_STACK" + ) + for name in REQUIRED_EXCLUDES: + assert f'"{name}"' in text.split("EXCLUDED_TORCH_STACK")[1].split("]")[0], ( + f"{spec_path.name} : '{name}' absent de EXCLUDED_TORCH_STACK" + ) + + +def test_spec_legacy_v5_garde_optimum(): + text = (ROOT / "anonymisation_onefile.spec").read_text(encoding="utf-8") + assert '"optimum"' in text, ( + "anonymisation_onefile.spec (GUI V5 legacy) doit GARDER optimum — " + "si ce test casse, quelqu'un a modifié la spec legacy par erreur." + ) diff --git a/tests/unit/test_gui_v6_version.py b/tests/unit/test_gui_v6_version.py index 579aa0f..45eab09 100644 --- a/tests/unit/test_gui_v6_version.py +++ b/tests/unit/test_gui_v6_version.py @@ -6,8 +6,6 @@ gui_v6/_build_version.py (non commité). En dev, repli sur la version par défau import sys import types -import pytest - from gui_v6.version import DEFAULT_VERSION, resolve_version