setiastrosuitepro 1.8.0.post3__py3-none-any.whl → 1.8.1.post2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

@@ -16,16 +16,13 @@ from contextlib import contextmanager
16
16
  import platform as _plat
17
17
  from pathlib import Path as _Path
18
18
 
19
- def _maybe_find_torch_shm_manager(torch_mod) -> str | None:
20
- # Only Linux wheels include/use this helper binary.
21
- if _plat.system() != "Linux":
22
- return None
19
+
20
+ def _rt_dbg(msg: str, status_cb=print):
23
21
  try:
24
- base = _Path(getattr(torch_mod, "__file__", "")).parent
25
- p = base / "bin" / "torch_shm_manager"
26
- return str(p) if p.exists() else None
22
+ status_cb(f"[RT] {msg}")
27
23
  except Exception:
28
- return None
24
+ print(f"[RT] {msg}", flush=True)
25
+
29
26
 
30
27
  # ──────────────────────────────────────────────────────────────────────────────
31
28
  # Paths & runtime selection
@@ -67,14 +64,25 @@ def _runtime_base_dir() -> Path:
67
64
  def _current_tag() -> str:
68
65
  return f"py{sys.version_info.major}{sys.version_info.minor}"
69
66
 
70
- def _discover_existing_runtime_dir() -> Path | None:
67
+ def _discover_existing_runtime_dir(status_cb=print) -> Path | None:
71
68
  """
72
- Return the newest existing runtime dir that already has a venv python,
73
- using the venv interpreter's REAL version instead of just the folder name.
69
+ Prefer an existing runtime that MATCHES the current interpreter minor.
70
+ Only if none exists, fall back to the highest available.
74
71
  """
75
72
  base = _runtime_base_dir()
76
73
  if not base.exists():
77
74
  return None
75
+
76
+ cur_tag = _current_tag() # e.g. py311, py312
77
+ cur_dir = base / cur_tag
78
+ cur_vpy = cur_dir / "venv" / ("Scripts/python.exe" if platform.system() == "Windows" else "bin/python")
79
+
80
+ # 1) If matching-current exists and has a venv python, use it.
81
+ if cur_vpy.exists():
82
+ _rt_dbg(f"Found matching runtime for current interpreter: {cur_dir}", status_cb)
83
+ return cur_dir
84
+
85
+ # 2) Otherwise, fall back to "newest existing"
78
86
  candidates: list[tuple[int, int, Path]] = []
79
87
  for p in base.glob("py*"):
80
88
  vpy = p / "venv" / ("Scripts/python.exe" if platform.system() == "Windows" else "bin/python")
@@ -83,22 +91,21 @@ def _discover_existing_runtime_dir() -> Path | None:
83
91
  ver = _venv_pyver(vpy)
84
92
  if ver:
85
93
  candidates.append((ver[0], ver[1], p))
94
+
86
95
  if not candidates:
87
96
  return None
88
- candidates.sort() # pick the highest Python (major, minor)
89
- return candidates[-1][2]
90
97
 
91
- def _user_runtime_dir() -> Path:
92
- """
93
- Use an existing runtime if we find one; otherwise select a directory for the
94
- current interpreter version (py310/py311/py312...).
95
- """
96
- existing = _discover_existing_runtime_dir()
97
- return existing or (_runtime_base_dir() / _current_tag())
98
+ candidates.sort()
99
+ chosen = candidates[-1][2]
100
+ _rt_dbg(f"No matching runtime; using newest existing: {chosen}", status_cb)
101
+ return chosen
102
+
103
+ def _user_runtime_dir(status_cb=print) -> Path:
104
+ existing = _discover_existing_runtime_dir(status_cb=status_cb)
105
+ chosen = existing or (_runtime_base_dir() / _current_tag())
106
+ _rt_dbg(f"_user_runtime_dir() -> {chosen}", status_cb)
107
+ return chosen
98
108
 
99
- # ──────────────────────────────────────────────────────────────────────────────
100
- # Shadowing & sanity checks
101
- # ──────────────────────────────────────────────────────────────────────────────
102
109
 
103
110
  # ──────────────────────────────────────────────────────────────────────────────
104
111
  # Shadowing & sanity checks
@@ -172,23 +179,6 @@ def _purge_bad_torch_from_sysmodules(status_cb=print) -> None:
172
179
  except Exception:
173
180
  pass
174
181
 
175
- def _torch_stack_sanity_check(status_cb=print) -> None:
176
- """
177
- Ensure torch imports sanely AND torchvision/torchaudio are importable.
178
- (Satellite engine requires torchvision; we install torchaudio too for safety.)
179
- """
180
- _torch_sanity_check(status_cb=status_cb)
181
-
182
- try:
183
- import torchvision # noqa
184
- except Exception as e:
185
- raise RuntimeError(f"torchvision import failed: {e}") from e
186
-
187
- try:
188
- import torchaudio # noqa
189
- except Exception as e:
190
- raise RuntimeError(f"torchaudio import failed: {e}") from e
191
-
192
182
 
193
183
  def _torch_sanity_check(status_cb=print):
194
184
  try:
@@ -631,129 +621,517 @@ def _install_torch(venv_python: Path, prefer_cuda: bool, prefer_xpu: bool, prefe
631
621
  # Public entry points
632
622
  # ──────────────────────────────────────────────────────────────────────────────
633
623
 
634
- def import_torch(prefer_cuda: bool = True, prefer_xpu: bool = False, prefer_dml: bool = False, status_cb=print):
624
+ def _venv_import_probe(venv_python: Path, modname: str) -> tuple[bool, str]:
635
625
  """
636
- Ensure a per-user venv exists with torch installed; return the imported module.
637
- Hardened against shadow imports, broken wheels, concurrent installs, and partial markers.
626
+ Try importing a module INSIDE the runtime venv python.
627
+ Returns (ok, output_or_error_tail).
638
628
  """
639
- # Before any attempt, demote shadowing paths (CWD / random folders)
640
- _ban_shadow_torch_paths(status_cb=status_cb)
641
- _purge_bad_torch_from_sysmodules(status_cb=status_cb)
629
+ code = (
630
+ "import importlib, sys\n"
631
+ f"m=importlib.import_module('{modname}')\n"
632
+ "print('OK', getattr(m,'__version__',None), getattr(m,'__file__',None))\n"
633
+ )
634
+ r = subprocess.run([str(venv_python), "-c", code],
635
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
636
+ out = (r.stdout or "").strip()
637
+ if r.returncode == 0 and out.startswith("OK"):
638
+ return True, out
639
+ return False, out[-4000:] if out else "no output"
642
640
 
643
- add_runtime_to_sys_path(status_cb=lambda *_: None)
644
641
 
645
- # Fast path: if torch already importable and sane, use it
642
+ def _write_torch_marker(marker: Path, status_cb=print) -> None:
643
+ """
644
+ Create torch_installed.json based on runtime venv imports.
645
+ Safe to call repeatedly.
646
+ """
647
+ rt = marker.parent
648
+ vp = _venv_paths(rt)["python"]
649
+
650
+ ok_t, out_t = _venv_import_probe(vp, "torch")
651
+ ok_v, out_v = _venv_import_probe(vp, "torchvision")
652
+ ok_a, out_a = _venv_import_probe(vp, "torchaudio")
653
+
654
+ payload = {
655
+ "installed": bool(ok_t),
656
+ "when": int(time.time()),
657
+ "python": None,
658
+ "torch": None,
659
+ "torchvision": None,
660
+ "torchaudio": None,
661
+ "torch_file": None,
662
+ "torchvision_file": None,
663
+ "torchaudio_file": None,
664
+ "probe": {
665
+ "torch": out_t,
666
+ "torchvision": out_v,
667
+ "torchaudio": out_a,
668
+ }
669
+ }
670
+
671
+ # get venv python version
646
672
  try:
647
- import torch # noqa
648
- _torch_stack_sanity_check(status_cb=status_cb)
649
- return torch
673
+ r = subprocess.run([str(vp), "-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
674
+ stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)
675
+ if r.returncode == 0:
676
+ payload["python"] = (r.stdout or "").strip()
650
677
  except Exception:
651
678
  pass
652
679
 
653
- rt = _user_runtime_dir()
654
- vp = _ensure_venv(rt, status_cb=status_cb)
655
- site = _site_packages(vp)
656
- marker = rt / "torch_installed.json"
680
+ # parse "OK ver file" lines
681
+ def _parse_ok(s: str):
682
+ # format: "OK <ver> <file>"
683
+ try:
684
+ parts = s.split(" ", 2)
685
+ ver = parts[1] if len(parts) > 1 else None
686
+ f = parts[2] if len(parts) > 2 else None
687
+ return ver, f
688
+ except Exception:
689
+ return None, None
690
+
691
+ if ok_t:
692
+ payload["torch"], payload["torch_file"] = _parse_ok(out_t)
693
+ if ok_v:
694
+ payload["torchvision"], payload["torchvision_file"] = _parse_ok(out_v)
695
+ if ok_a:
696
+ payload["torchaudio"], payload["torchaudio_file"] = _parse_ok(out_a)
657
697
 
658
698
  try:
659
- _ensure_numpy(vp, status_cb=status_cb)
660
- except Exception:
661
- # Non-fatal; we'll try again if torch complains at runtime
662
- pass
699
+ marker.write_text(json.dumps(payload, indent=2), encoding="utf-8")
700
+ status_cb(f"[RT] Wrote marker: {marker}")
701
+ except Exception as e:
702
+ status_cb(f"[RT] Failed to write marker {marker}: {e!r}")
703
+
704
+ def _venv_has_torch_stack(
705
+ vp: Path,
706
+ status_cb=print,
707
+ *,
708
+ require_torchaudio: bool = True
709
+ ) -> tuple[bool, dict]:
710
+ """
711
+ Definitive check: can the RUNTIME VENV import torch/torchvision/(torchaudio)?
712
+ This does NOT use the frozen app interpreter to decide installation state.
713
+ """
714
+ ok_t, out_t = _venv_import_probe(vp, "torch")
715
+ ok_v, out_v = _venv_import_probe(vp, "torchvision")
716
+ ok_a, out_a = _venv_import_probe(vp, "torchaudio")
717
+
718
+ info = {
719
+ "torch": (ok_t, out_t),
720
+ "torchvision": (ok_v, out_v),
721
+ "torchaudio": (ok_a, out_a),
722
+ }
723
+
724
+ ok_all = (ok_t and ok_v and ok_a) if require_torchaudio else (ok_t and ok_v)
725
+ return ok_all, info
726
+
727
+ def _marker_says_ready(
728
+ marker: Path,
729
+ site: Path,
730
+ venv_ver: tuple[int, int] | None,
731
+ *,
732
+ require_torchaudio: bool = True,
733
+ max_age_days: int = 180,
734
+ ) -> bool:
735
+ """
736
+ Advisory fast-path gate ONLY.
737
+
738
+ Returns True if the marker looks sane enough that an in-process import attempt
739
+ is worth trying *without* doing the expensive subprocess venv probes.
740
+
741
+ IMPORTANT:
742
+ - This must NOT be used to decide whether to install/uninstall anything.
743
+ - If this returns True and the in-process import fails, we fall back to the
744
+ definitive venv probe (_venv_has_torch_stack).
745
+ """
746
+ try:
747
+ if not marker.exists():
748
+ return False
749
+
750
+ raw = marker.read_text(encoding="utf-8", errors="replace")
751
+ data = json.loads(raw) if raw else {}
752
+ if not isinstance(data, dict):
753
+ return False
754
+
755
+ if not data.get("installed", False):
756
+ return False
757
+
758
+ # Age gate (advisory only).
759
+ when = data.get("when")
760
+ if isinstance(when, (int, float)):
761
+ age_s = max(0.0, time.time() - float(when))
762
+ if age_s > (max_age_days * 86400):
763
+ return False
764
+
765
+ # Marker python version should match the RUNTIME VENV python (not the app interpreter).
766
+ py = data.get("python")
767
+ if not (isinstance(py, str) and py.strip()):
768
+ return False
663
769
 
664
- # If no marker, perform install under a lock
665
- if not marker.exists():
666
770
  try:
667
- with _install_lock(rt):
668
- # Re-check inside lock in case another process finished
669
- if not marker.exists():
670
- _install_torch(vp, prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, prefer_dml=prefer_dml, status_cb=status_cb)
671
- except Exception as e:
672
- if _is_access_denied(e):
673
- raise OSError(_access_denied_msg(rt)) from e
674
- raise
771
+ maj_s, min_s = py.strip().split(".", 1)
772
+ marker_ver = (int(maj_s), int(min_s))
773
+ except Exception:
774
+ return False
675
775
 
676
- # Ensure the venv site is first on sys.path, then demote shadowers again
677
- if str(site) not in sys.path:
678
- sys.path.insert(0, str(site))
679
- _demote_shadow_torch_paths(status_cb=status_cb)
776
+ # If we can't determine venv version, treat marker as unreliable for fast-path.
777
+ if venv_ver is None:
778
+ return False
779
+
780
+ if marker_ver != venv_ver:
781
+ return False
782
+
783
+ # Check that recorded files (if present) live under the computed site-packages path.
784
+ site_s = str(site)
785
+ tf = data.get("torch_file")
786
+ tvf = data.get("torchvision_file")
787
+ taf = data.get("torchaudio_file")
788
+
789
+ def _under_site(p: str | None) -> bool:
790
+ if not p or not isinstance(p, str):
791
+ return False
792
+ return site_s in p
793
+
794
+ if not _under_site(tf):
795
+ return False
796
+ if not _under_site(tvf):
797
+ return False
798
+ if require_torchaudio and not _under_site(taf):
799
+ return False
800
+
801
+ return True
802
+ except Exception:
803
+ return False
804
+
805
+
806
+ def _qt_settings():
807
+ """
808
+ Create QSettings without importing PyQt6 at module import time.
809
+ We only import it inside the function so runtime_torch stays usable
810
+ in non-GUI contexts.
811
+ """
812
+ try:
813
+ from PyQt6.QtCore import QSettings
814
+ # Must match what your app sets via QCoreApplication.setOrganizationName / setApplicationName
815
+ return QSettings()
816
+ except Exception:
817
+ return None
818
+
819
+
820
+ def _qcache_get():
821
+ s = _qt_settings()
822
+ if not s:
823
+ return None
824
+ s.beginGroup("runtime_torch")
825
+ data = {
826
+ "tag": s.value("tag", "", str),
827
+ "rt_dir": s.value("rt_dir", "", str),
828
+ "site": s.value("site", "", str),
829
+ "python": s.value("python", "", str),
830
+ "torch": s.value("torch", "", str),
831
+ "torchvision": s.value("torchvision", "", str),
832
+ "torchaudio": s.value("torchaudio", "", str),
833
+ "when": s.value("when", 0, int),
834
+ "require_torchaudio": s.value("require_torchaudio", True, bool),
835
+ }
836
+ s.endGroup()
837
+ return data
838
+
839
+
840
+ def _qcache_set(*, tag: str, rt_dir: Path, site: Path, python_ver: str | None,
841
+ torch_ver: str | None, tv_ver: str | None, ta_ver: str | None,
842
+ require_torchaudio: bool):
843
+ s = _qt_settings()
844
+ if not s:
845
+ return
846
+ s.beginGroup("runtime_torch")
847
+ s.setValue("tag", tag)
848
+ s.setValue("rt_dir", str(rt_dir))
849
+ s.setValue("site", str(site))
850
+ s.setValue("python", python_ver or "")
851
+ s.setValue("torch", torch_ver or "")
852
+ s.setValue("torchvision", tv_ver or "")
853
+ s.setValue("torchaudio", ta_ver or "")
854
+ s.setValue("when", int(time.time()))
855
+ s.setValue("require_torchaudio", bool(require_torchaudio))
856
+ s.endGroup()
857
+ s.sync()
858
+
859
+
860
+ def _qcache_clear():
861
+ s = _qt_settings()
862
+ if not s:
863
+ return
864
+ s.beginGroup("runtime_torch")
865
+ s.remove("") # remove all keys in group
866
+ s.endGroup()
867
+ s.sync()
680
868
 
681
- # Import + sanity. If broken, force a clean repair (all OSes).
682
- def _force_repair():
869
+
870
+ # module-level cache (optional but recommended)
871
+ # module-level cache (optional but recommended)
872
+ _TORCH_CACHED = None
873
+
874
+
875
+ def import_torch(
876
+ prefer_cuda: bool = True,
877
+ prefer_xpu: bool = False,
878
+ prefer_dml: bool = False,
879
+ status_cb=print,
880
+ *,
881
+ require_torchaudio: bool = True,
882
+ ):
883
+ """
884
+ Ensure a per-user venv exists with torch installed; return the imported torch module.
885
+
886
+ ULTRA FAST PATH:
887
+ - Use QSettings cached site-packages (no subprocess at all) and attempt in-process import.
888
+
889
+ FAST PATH:
890
+ - If marker looks valid, compute site-packages (1 subprocess) and try in-process imports.
891
+ - If that works, skip expensive subprocess probes.
892
+
893
+ SLOW PATH:
894
+ - Probe runtime venv via subprocess (torch/torchvision/torchaudio).
895
+ - Install only if missing, then re-probe.
896
+ - Finally import in-process from venv site-packages.
897
+
898
+ NEW RULES:
899
+ - Marker/QSettings are advisory only (fast path gates).
900
+ - If torch/torchvision(/torchaudio) exist in the runtime venv, USE THEM. Do nothing else.
901
+ - Only if missing in the runtime venv should we install.
902
+ - NEVER auto-uninstall user torch/torchvision/torchaudio. No automatic repair.
903
+ """
904
+ global _TORCH_CACHED
905
+ if _TORCH_CACHED is not None:
906
+ return _TORCH_CACHED
907
+
908
+ def _write_qcache_best_effort(rt: Path, site: Path, venv_ver: tuple[int,int] | None):
909
+ """
910
+ Write QSettings cache only after we have proven imports work in-process.
911
+ """
683
912
  try:
684
- status_cb("Detected broken/shadowed Torch import attempting clean repair…")
913
+ import torch as _t # noqa
914
+ import torchvision as _tv # noqa
915
+ _ta = None
916
+ if require_torchaudio:
917
+ import torchaudio as _ta # noqa
918
+
919
+ _qcache_set(
920
+ tag=rt.name, # IMPORTANT: runtime tag, not sys.version_info tag
921
+ rt_dir=rt,
922
+ site=site,
923
+ python_ver=(f"{venv_ver[0]}.{venv_ver[1]}" if venv_ver else ""),
924
+ torch_ver=getattr(_t, "__version__", None),
925
+ tv_ver=getattr(_tv, "__version__", None),
926
+ ta_ver=getattr(_ta, "__version__", None) if _ta else None,
927
+ require_torchaudio=require_torchaudio,
928
+ )
685
929
  except Exception:
686
930
  pass
687
931
 
688
- # remove marker so future launches don't skip install
932
+ _rt_dbg(f"sys.frozen={getattr(sys,'frozen',False)}", status_cb)
933
+ _rt_dbg(f"sys.executable={sys.executable}", status_cb)
934
+ _rt_dbg(f"sys.version={sys.version}", status_cb)
935
+ _rt_dbg(f"current_tag={_current_tag()}", status_cb)
936
+ _rt_dbg(f"SASPRO_RUNTIME_DIR={os.getenv('SASPRO_RUNTIME_DIR')!r}", status_cb)
937
+
938
+ # Remove obvious shadowing paths (repo folders / cwd torch trees)
939
+ _ban_shadow_torch_paths(status_cb=status_cb)
940
+ _purge_bad_torch_from_sysmodules(status_cb=status_cb)
941
+
942
+ # ------------------------------------------------------------
943
+ # Choose runtime + ensure venv exists
944
+ # ------------------------------------------------------------
945
+ rt = _user_runtime_dir(status_cb=status_cb)
946
+ vp = _ensure_venv(rt, status_cb=status_cb)
947
+
948
+ # ------------------------------------------------------------
949
+ # ULTRA FAST PATH (runtime-aware): QSettings cache.
950
+ # Now we can compare the cache tag against the RUNTIME tag, not sys.version_info.
951
+ # This stays correct for "app python != runtime venv python" cases.
952
+ # ------------------------------------------------------------
953
+ try:
954
+ qc = _qcache_get()
955
+ if qc:
956
+ site_s = (qc.get("site") or "").strip()
957
+ rt_s = (qc.get("rt_dir") or "").strip()
958
+ req_ta = bool(qc.get("require_torchaudio", True))
959
+ tag = (qc.get("tag") or "").strip()
960
+
961
+ # Accept cache only if it matches this runtime folder tag
962
+ if (
963
+ tag == rt.name
964
+ and site_s and Path(site_s).exists()
965
+ and rt_s and Path(rt_s).exists()
966
+ and (req_ta == require_torchaudio)
967
+ ):
968
+ status_cb("[RT] QSettings cache hit (runtime tag match); attempting zero-subprocess import.")
969
+
970
+ if site_s not in sys.path:
971
+ sys.path.insert(0, site_s)
972
+
973
+ _demote_shadow_torch_paths(status_cb=status_cb)
974
+ _purge_bad_torch_from_sysmodules(status_cb=status_cb)
975
+
976
+ import torch # noqa
977
+ import torchvision # noqa
978
+ if require_torchaudio:
979
+ import torchaudio # noqa
980
+
981
+ _TORCH_CACHED = torch
982
+
983
+ return torch
984
+
985
+ except Exception as e:
986
+ status_cb(f"[RT] QSettings fast-path failed: {type(e).__name__}: {e}. Continuing…")
689
987
  try:
690
- marker.unlink()
988
+ _qcache_clear()
691
989
  except Exception:
692
990
  pass
693
991
 
694
- subprocess.run([str(vp), "-m", "pip", "uninstall", "-y",
695
- "torch", "torchvision", "torchaudio"], check=False)
696
- subprocess.run([str(vp), "-m", "pip", "cache", "purge"], check=False)
697
- with _install_lock(rt):
698
- _install_torch(
699
- vp,
700
- prefer_cuda=prefer_cuda,
701
- prefer_xpu=prefer_xpu,
702
- prefer_dml=prefer_dml,
703
- status_cb=status_cb,
704
- )
705
- importlib.invalidate_caches()
706
- _demote_shadow_torch_paths(status_cb=status_cb)
992
+ # site-packages path (subprocess but relatively cheap)
993
+ site = _site_packages(vp)
994
+ marker = rt / "torch_installed.json"
995
+ venv_ver = _venv_pyver(vp)
707
996
 
997
+ _rt_dbg(f"venv_ver={venv_ver}", status_cb)
998
+ _rt_dbg(f"rt={rt}", status_cb)
999
+ _rt_dbg(f"venv_python={vp}", status_cb)
1000
+ _rt_dbg(f"marker={marker} exists={marker.exists()}", status_cb)
1001
+ _rt_dbg(f"site={site}", status_cb)
708
1002
 
1003
+ # Best-effort ensure numpy in venv (harmless if already there)
709
1004
  try:
710
1005
  _ensure_numpy(vp, status_cb=status_cb)
711
1006
  except Exception:
712
1007
  pass
713
1008
 
1009
+ # ------------------------------------------------------------
1010
+ # FAST PATH: if marker looks valid, try in-process import NOW.
1011
+ # This avoids the 3 subprocess probes on every launch.
1012
+ # ------------------------------------------------------------
714
1013
  try:
715
- import torch # noqa
716
- _torch_stack_sanity_check(status_cb=status_cb)
717
- # write/update marker only when sane
718
- if not marker.exists():
719
- pyver = f"{sys.version_info.major}.{sys.version_info.minor}"
1014
+ if _marker_says_ready(marker, site, venv_ver, require_torchaudio=require_torchaudio):
1015
+ status_cb("[RT] Marker valid; attempting fast in-process import (skipping venv probe).")
1016
+
1017
+ sp = str(site)
1018
+ if sp not in sys.path:
1019
+ sys.path.insert(0, sp)
1020
+
1021
+ _demote_shadow_torch_paths(status_cb=status_cb)
1022
+ _purge_bad_torch_from_sysmodules(status_cb=status_cb)
1023
+
1024
+ import torch # noqa
1025
+ import torchvision # noqa
1026
+ if require_torchaudio:
1027
+ import torchaudio # noqa
1028
+
1029
+ # refresh marker (best-effort)
720
1030
  try:
721
- import torch, torchvision, torchaudio
722
- marker.write_text(json.dumps({
723
- "installed": True,
724
- "python": pyver,
725
- "when": int(time.time()),
726
- "torch": getattr(torch, "__version__", None),
727
- "torchvision": getattr(torchvision, "__version__", None),
728
- "torchaudio": getattr(torchaudio, "__version__", None),
729
- }), encoding="utf-8")
1031
+ _write_torch_marker(marker, status_cb=status_cb)
730
1032
  except Exception:
731
- marker.write_text(json.dumps({"installed": True, "python": pyver, "when": int(time.time())}), encoding="utf-8")
1033
+ pass
732
1034
 
733
- return torch
1035
+ _TORCH_CACHED = torch
1036
+ _write_qcache_best_effort(rt, site, venv_ver)
1037
+ return torch
1038
+
1039
+ except Exception as e:
1040
+ status_cb(f"[RT] Marker fast-path failed: {type(e).__name__}: {e}. Falling back to full probe…")
1041
+ # if marker fast path fails, your cached site-packages may also be stale
1042
+ try:
1043
+ _qcache_clear()
1044
+ except Exception:
1045
+ pass
1046
+
1047
+ # ------------------------------------------------------------
1048
+ # SLOW PATH: Probe the runtime venv definitively.
1049
+ # If it has torch stack, we're DONE (no installs, no repair).
1050
+ # ------------------------------------------------------------
1051
+ ok_all, info = _venv_has_torch_stack(vp, status_cb=status_cb, require_torchaudio=require_torchaudio)
1052
+ status_cb(
1053
+ "[RT] venv probe: "
1054
+ f"torch={info['torch'][0]} "
1055
+ f"torchvision={info['torchvision'][0]} "
1056
+ f"torchaudio={info['torchaudio'][0]}"
1057
+ )
1058
+
1059
+ if not ok_all:
1060
+ missing = []
1061
+ if not info["torch"][0]:
1062
+ missing.append("torch")
1063
+ if not info["torchvision"][0]:
1064
+ missing.append("torchvision")
1065
+ if require_torchaudio and (not info["torchaudio"][0]):
1066
+ missing.append("torchaudio")
1067
+
1068
+ status_cb(f"[RT] Missing in runtime venv: {missing}. Installing…")
1069
+
1070
+ try:
1071
+ with _install_lock(rt):
1072
+ _install_torch(
1073
+ vp,
1074
+ prefer_cuda=prefer_cuda,
1075
+ prefer_xpu=prefer_xpu,
1076
+ prefer_dml=prefer_dml,
1077
+ status_cb=status_cb,
1078
+ )
1079
+ except Exception as e:
1080
+ if _is_access_denied(e):
1081
+ raise OSError(_access_denied_msg(rt)) from e
1082
+ raise
1083
+
1084
+ # Re-probe after install
1085
+ ok_all, info = _venv_has_torch_stack(vp, status_cb=status_cb, require_torchaudio=require_torchaudio)
1086
+ status_cb(
1087
+ "[RT] venv re-probe: "
1088
+ f"torch={info['torch'][0]} "
1089
+ f"torchvision={info['torchvision'][0]} "
1090
+ f"torchaudio={info['torchaudio'][0]}"
1091
+ )
1092
+ if not ok_all:
1093
+ msg = "\n".join([f"{k}: ok={ok} :: {out}" for k, (ok, out) in info.items()])
1094
+ raise RuntimeError("Torch stack still not importable in runtime venv after install:\n" + msg)
1095
+
1096
+ # Always write/update marker for convenience, but never trust it for decisions.
1097
+ try:
1098
+ _write_torch_marker(marker, status_cb=status_cb)
734
1099
  except Exception:
735
- _force_repair()
736
- _purge_bad_torch_from_sysmodules(status_cb=status_cb)
737
- _ban_shadow_torch_paths(status_cb=status_cb)
738
- import torch # retry
739
- _torch_stack_sanity_check(status_cb=status_cb)
740
- if not marker.exists():
741
- pyver = f"{sys.version_info.major}.{sys.version_info.minor}"
742
- try:
743
- import torch, torchvision, torchaudio
744
- marker.write_text(json.dumps({
745
- "installed": True,
746
- "python": pyver,
747
- "when": int(time.time()),
748
- "torch": getattr(torch, "__version__", None),
749
- "torchvision": getattr(torchvision, "__version__", None),
750
- "torchaudio": getattr(torchaudio, "__version__", None),
751
- }), encoding="utf-8")
752
- except Exception:
753
- marker.write_text(json.dumps({"installed": True, "python": pyver, "when": int(time.time())}), encoding="utf-8")
1100
+ pass
1101
+
1102
+ # ------------------------------------------------------------
1103
+ # Now import torch in-process, but ONLY after putting runtime site first.
1104
+ # ------------------------------------------------------------
1105
+ sp = str(site)
1106
+ if sp not in sys.path:
1107
+ sys.path.insert(0, sp)
754
1108
 
1109
+ _demote_shadow_torch_paths(status_cb=status_cb)
1110
+ _purge_bad_torch_from_sysmodules(status_cb=status_cb)
1111
+
1112
+ try:
1113
+ import torch # noqa
1114
+
1115
+ _TORCH_CACHED = torch
1116
+ _write_qcache_best_effort(rt, site, venv_ver)
755
1117
  return torch
756
1118
 
1119
+ except Exception as e:
1120
+ # prevent repeatedly hitting a bad cached site path on next launch
1121
+ try:
1122
+ _qcache_clear()
1123
+ except Exception:
1124
+ pass
1125
+
1126
+ msg = "\n".join([f"{k}: ok={ok} :: {out}" for k, (ok, out) in info.items()])
1127
+ raise RuntimeError(
1128
+ "Runtime venv probe says torch stack exists, but in-process import failed.\n"
1129
+ "This typically indicates a frozen-stdlib / PyInstaller packaging issue, not a bad torch install.\n\n"
1130
+ f"Original error: {type(e).__name__}: {e}\n\n"
1131
+ "Runtime venv probe:\n" + msg
1132
+ ) from e
1133
+
1134
+
757
1135
  def _find_system_python_cmd() -> list[str]:
758
1136
  import platform as _plat
759
1137
  if _plat.system() == "Darwin":
@@ -814,7 +1192,7 @@ def add_runtime_to_sys_path(status_cb=print) -> None:
814
1192
  """
815
1193
  Warm up sys.path so a fresh launch can see the runtime immediately.
816
1194
  """
817
- rt = _user_runtime_dir()
1195
+ rt = _user_runtime_dir(status_cb=status_cb)
818
1196
  p = _venv_paths(rt)
819
1197
  vpy = p["python"]
820
1198
  if not vpy.exists():
@@ -837,3 +1215,69 @@ def add_runtime_to_sys_path(status_cb=print) -> None:
837
1215
  _demote_shadow_torch_paths(status_cb=status_cb)
838
1216
  except Exception:
839
1217
  return
1218
+
1219
+ def prewarm_torch_cache(
1220
+ status_cb=print,
1221
+ *,
1222
+ require_torchaudio: bool = True,
1223
+ ensure_venv: bool = True,
1224
+ ensure_numpy: bool = False,
1225
+ validate_marker: bool = True,
1226
+ ) -> None:
1227
+ """
1228
+ Build and persist the QSettings cache early (startup), so the first real
1229
+ import_torch() call can be zero-subprocess.
1230
+
1231
+ By default this does NOT import torch (keeps startup lighter).
1232
+ It only computes runtime rt/vpy/site and writes QSettings.
1233
+ """
1234
+ try:
1235
+ _ban_shadow_torch_paths(status_cb=status_cb)
1236
+ _purge_bad_torch_from_sysmodules(status_cb=status_cb)
1237
+
1238
+ rt = _user_runtime_dir(status_cb=status_cb)
1239
+ p = _venv_paths(rt)
1240
+ vp = p["python"]
1241
+
1242
+ if ensure_venv:
1243
+ vp = _ensure_venv(rt, status_cb=status_cb)
1244
+
1245
+ if not vp.exists():
1246
+ return
1247
+
1248
+ if ensure_numpy:
1249
+ try:
1250
+ _ensure_numpy(vp, status_cb=status_cb)
1251
+ except Exception:
1252
+ pass
1253
+
1254
+ site = _site_packages(vp)
1255
+ marker = rt / "torch_installed.json"
1256
+ venv_ver = _venv_pyver(vp)
1257
+
1258
+ # Optionally only cache if marker looks valid (recommended),
1259
+ # otherwise you may cache a site-packages that doesn't actually contain torch yet.
1260
+ if validate_marker:
1261
+ if not _marker_says_ready(marker, site, venv_ver, require_torchaudio=require_torchaudio):
1262
+ status_cb("[RT] prewarm: marker not valid; skipping QSettings cache write.")
1263
+ return
1264
+
1265
+ # IMPORTANT: use runtime tag, not app interpreter tag, for mixed-version scenarios
1266
+ cache_tag = rt.name # e.g. "py312"
1267
+
1268
+ _qcache_set(
1269
+ tag=cache_tag,
1270
+ rt_dir=rt,
1271
+ site=site,
1272
+ python_ver=(f"{venv_ver[0]}.{venv_ver[1]}" if venv_ver else ""),
1273
+ torch_ver=None,
1274
+ tv_ver=None,
1275
+ ta_ver=None,
1276
+ require_torchaudio=require_torchaudio,
1277
+ )
1278
+ status_cb("[RT] prewarm: QSettings cache written.")
1279
+ except Exception as e:
1280
+ try:
1281
+ status_cb(f"[RT] prewarm failed: {type(e).__name__}: {e}")
1282
+ except Exception:
1283
+ pass