lazy module??
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Regression test for: AttributeError: module 'tkinter' has no attribute 'filedialog'
|
||||
|
||||
On some Python installs (e.g., embedded distributions, or installs where
|
||||
the Tcl/Tk runtime is missing), the `tkinter` package imports cleanly but
|
||||
the `tkinter.filedialog` sub-module fails to load. The original `_LazyModule`
|
||||
in src/gui_2.py used `getattr(tkinter, 'filedialog')` which raises a
|
||||
confusing AttributeError at the call site. With 14 call sites in
|
||||
render_projects_panel, render_workspace_settings_hub, render_fonts_panel,
|
||||
and render_gemini_cli_settings, this AttributeError spammed the GUI's
|
||||
stderr at 60fps whenever the Project Settings window was open.
|
||||
|
||||
The fix must make `_LazyModule` fall back to a stub that mimics
|
||||
`tkinter.filedialog`'s public API (askopenfilename, askdirectory,
|
||||
asksaveasfilename, askopenfilenames) so the GUI does not crash.
|
||||
|
||||
This test uses a deliberately-missing sub-module to exercise the fallback
|
||||
path, making it deterministic across Python installs.
|
||||
"""
|
||||
import pytest
|
||||
import importlib
|
||||
|
||||
from src.gui_2 import _LazyModule
|
||||
|
||||
|
||||
def test_lazymodule_falls_back_to_stub_on_attribute_error() -> None:
|
||||
"""
|
||||
Resolution must NOT raise AttributeError when the sub-module is
|
||||
missing. Instead, _resolve() must return a stub that exposes the
|
||||
public filedialog API. Before the fix, this test fails with
|
||||
AttributeError: module 'os' has no attribute 'this_submodule_does_not_exist'.
|
||||
"""
|
||||
bad = _LazyModule("os", "this_submodule_does_not_exist")
|
||||
resolved = bad._resolve()
|
||||
assert resolved is not None
|
||||
assert hasattr(resolved, "askopenfilename")
|
||||
assert hasattr(resolved, "askdirectory")
|
||||
assert hasattr(resolved, "asksaveasfilename")
|
||||
assert hasattr(resolved, "askopenfilenames")
|
||||
|
||||
|
||||
def test_lazymodule_stub_returns_empty_strings() -> None:
|
||||
"""
|
||||
The stub functions must return safe empty values:
|
||||
- askopenfilename, askdirectory, asksaveasfilename: empty string ""
|
||||
- askopenfilenames: empty tuple ()
|
||||
This ensures downstream code that does `if p and p not in app.x:`
|
||||
or `if paths:` treats the missing-dialog as a no-op.
|
||||
"""
|
||||
bad = _LazyModule("os", "this_submodule_does_not_exist")
|
||||
resolved = bad._resolve()
|
||||
assert resolved.askopenfilename() == ""
|
||||
assert resolved.askdirectory() == ""
|
||||
assert resolved.asksaveasfilename() == ""
|
||||
assert resolved.askopenfilenames() == ()
|
||||
|
||||
|
||||
def test_lazymodule_stub_ignores_kwargs() -> None:
|
||||
"""
|
||||
The stub must accept the same kwargs the real tkinter.filedialog
|
||||
accepts (title, filetypes, defaultextension, initialdir) and return
|
||||
the empty sentinel. This prevents TypeError if a call site passes
|
||||
kwargs that the stub does not know about.
|
||||
"""
|
||||
bad = _LazyModule("os", "this_submodule_does_not_exist")
|
||||
resolved = bad._resolve()
|
||||
assert resolved.askopenfilename(title="x", filetypes=[("All", "*.*")]) == ""
|
||||
assert resolved.askdirectory(title="y", initialdir="/") == ""
|
||||
assert resolved.asksaveasfilename(title="z", defaultextension=".toml", filetypes=[("TOML", "*.toml")]) == ""
|
||||
assert resolved.askopenfilenames(filetypes=[("Image", "*.png")]) == ()
|
||||
|
||||
|
||||
def test_lazymodule_real_filedialog_resolves_when_tkinter_works() -> None:
|
||||
"""
|
||||
On a working tkinter install (with Tcl/Tk runtime), the
|
||||
`_LazyModule("tkinter", "filedialog")` instance must resolve to the
|
||||
real tkinter.filedialog module. This is the smoke test: if tkinter
|
||||
is healthy, the lazy import works as before.
|
||||
"""
|
||||
import tkinter as tk_root
|
||||
try:
|
||||
import tkinter.filedialog as real_filedialog
|
||||
except (ImportError, AttributeError, tk_root.TclError):
|
||||
pytest.skip("tkinter.filedialog not available in this Python install")
|
||||
lazy = _LazyModule("tkinter", "filedialog")
|
||||
resolved = lazy._resolve()
|
||||
assert resolved is real_filedialog
|
||||
|
||||
|
||||
def test_lazymodule_real_filedialog_does_not_raise_attribute_error() -> None:
|
||||
"""
|
||||
On a working tkinter install, calling .askopenfilename() through the
|
||||
lazy module must not raise AttributeError. (Tests the call path
|
||||
used by 14 call sites in render_projects_panel etc.)
|
||||
"""
|
||||
lazy = _LazyModule("tkinter", "filedialog")
|
||||
resolved = lazy._resolve()
|
||||
assert hasattr(resolved, "askopenfilename")
|
||||
assert hasattr(resolved, "askdirectory")
|
||||
assert hasattr(resolved, "asksaveasfilename")
|
||||
assert hasattr(resolved, "askopenfilenames")
|
||||
Reference in New Issue
Block a user