From 65c97a39b3bf1791e3c850cba05bcd99ad100573 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Thu, 2 Jul 2026 11:11:07 +0200 Subject: [PATCH] =?UTF-8?q?build:=20specs=20GUI=20V6=20+=20CLI=20torch-fre?= =?UTF-8?q?e=20=E2=80=94=20retrait=20optimum,=20excludes=20explicites=20(P?= =?UTF-8?q?0-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suppression des 5 hiddenimports optimum* dans les deux specs V6/CLI. Ajout de EXCLUDED_TORCH_STACK + excludes=EXCLUDED_TORCH_STACK dans Analysis() pour éviter que PyInstaller embarque torch (~+2 Go) via optimum à l'analyse statique. Spec GUI V5 legacy inchangée (garde optimum légitimement). Test anti-dérive ajouté (5 cas). Correctif import pytest inutilisé (version.py). Co-Authored-By: Claude Fable 5 --- anonymisation_cli_onefile.spec | 17 ++++-- anonymisation_gui_v6_onefile.spec | 14 +++-- tests/unit/test_build_specs_torch_free.py | 68 +++++++++++++++++++++++ tests/unit/test_gui_v6_version.py | 2 - 4 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 tests/unit/test_build_specs_torch_free.py 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