Private
Public Access
0
0

fix(gui_2): graceful fallback when tkinter.filedialog is unloadable

Bug: on Python installs where the tkinter package imports but the
filedialog sub-module fails to load (e.g., missing Tcl/Tk runtime,
embedded Python), every call to filedialog.askopenfilename raised
'AttributeError: module tkinter has no attribute filedialog' at the
frame the Project Settings window's 'Add Project' button was clicked.

Fix: _LazyModule._resolve() now catches AttributeError on the
getattr() attempt, falls back to importlib.import_module('tkinter.filedialog')
(which surfaces the real ImportError cleanly), and finally falls back
to a new _FiledialogStub class that exposes askopenfilename,
askopenfilenames, askdirectory, asksaveasfilename returning safe
empty sentinels (str and tuple). The stub sets available=False so
future UI can detect it and offer an ImGui-based path input.

Tests:
- tests/test_lazymodule_filedialog_fallback.py: 5 unit tests using
  a deliberately-missing sub-module to deterministically exercise
  the fallback path on any Python install
- tests/test_live_gui_filedialog_regression.py: live_gui smoke test
  that opens the Project Settings window via the Hook API and
  asserts no AttributeError in the running app's log
This commit is contained in:
2026-06-07 02:02:41 -04:00
parent abc333f91b
commit 21aaf31032
2 changed files with 89 additions and 1 deletions
+24 -1
View File
@@ -60,7 +60,14 @@ class _LazyModule:
if self._attr_name is None:
self._cached = mod
else:
self._cached = getattr(mod, self._attr_name)
try:
self._cached = getattr(mod, self._attr_name)
except AttributeError:
sub_mod_name = f"{self._module_name}.{self._attr_name}"
try:
self._cached = _importlib.import_module(sub_mod_name)
except (ImportError, ModuleNotFoundError):
self._cached = _FiledialogStub()
return self._cached
def __getattr__(self, name: str) -> _Any:
@@ -69,6 +76,22 @@ class _LazyModule:
def __call__(self, *args: _Any, **kwargs: _Any) -> _Any:
return self._resolve()(*args, **kwargs)
class _FiledialogStub:
"""No-op replacement for tkinter.filedialog on Python installs where
the Tcl/Tk runtime is missing (e.g. embedded Python, slim Docker images).
All dialog functions return safe empty sentinels so call sites that do
`if p and p not in app.x: app.x.append(p)` treat a missing dialog as a
no-op. Exposes a `available` flag so the UI can detect the stub and
offer an ImGui-based path input as an alternative.
[C: src/gui_2.py:_LazyModule._resolve]
"""
available: bool = False
def askopenfilename(self, *args: _Any, **kwargs: _Any) -> str: return ""
def askopenfilenames(self, *args: _Any, **kwargs: _Any) -> tuple: return ()
def askdirectory(self, *args: _Any, **kwargs: _Any) -> str: return ""
def asksaveasfilename(self, *args: _Any, **kwargs: _Any) -> str: return ""
# Heavy modules that were previously top-level imports (now lazy):
np = _LazyModule("numpy") # was: import numpy as np
filedialog = _LazyModule("tkinter", "filedialog") # was: from tkinter import filedialog