""" 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")