"""Global pytest configuration. CI/autobuild must run unattended and non-interactive. We do **not** globally replace the whole `tkinter` module because some tests exercise real widget layout, but we *do* stub popup-oriented modules (`tkinter.messagebox`, `tkinter.filedialog`) so no test can block on a dialog. """ from __future__ import annotations import sys import types from unittest.mock import MagicMock import pytest try: import tkinter as tk except Exception: # pragma: no cover tk = None # type: ignore[assignment] def _configure_messagebox(msg: MagicMock) -> None: # Confirmation-style prompts: auto-accept. for fn in ( "askyesno", "askokcancel", "askretrycancel", "askyesnocancel", ): getattr(msg, fn).return_value = True # Informational prompts: no-op. for fn in ( "showinfo", "showwarning", "showerror", ): getattr(msg, fn).return_value = None def _install_popup_stubs() -> dict[str, MagicMock]: messagebox = MagicMock(name="tkinter.messagebox") filedialog = MagicMock(name="tkinter.filedialog") _configure_messagebox(messagebox) # Seed sys.modules so `from tkinter import messagebox, filedialog` picks up # these stubs without requiring a real UI. sys.modules["tkinter.messagebox"] = messagebox sys.modules["tkinter.filedialog"] = filedialog # If tkinter is already imported, ensure it points at our stub submodules. tk_mod = sys.modules.get("tkinter") if tk_mod is not None: try: setattr(tk_mod, "messagebox", messagebox) setattr(tk_mod, "filedialog", filedialog) except Exception: pass return { "tkinter.messagebox": messagebox, "tkinter.filedialog": filedialog, } _POPUP_STUBS = _install_popup_stubs() @pytest.fixture(scope="session", autouse=True) def _tk_default_root_session(): """Ensure a default Tkinter root exists for tests that create variables. Some UI logic uses `tkinter.StringVar`/`BooleanVar` without explicitly providing a master, which requires a default root. In headless contexts we prefer a `Tcl()` interpreter fallback. """ if tk is None: yield return existing = getattr(tk, "_default_root", None) if existing is not None: yield return root = None try: root = tk.Tk() try: root.withdraw() except Exception: pass except Exception: try: root = tk.Tcl() try: tk._default_root = root # type: ignore[attr-defined] except Exception: pass except Exception: root = None try: yield finally: try: if root is not None and hasattr(root, "destroy"): root.destroy() except Exception: pass try: tk._default_root = None # type: ignore[attr-defined] except Exception: pass def _stub_module(name: str, *, is_package: bool = False) -> types.ModuleType: m = types.ModuleType(name) if is_package: # Mark as a package so submodule imports can succeed. m.__path__ = [] # type: ignore[attr-defined] return m def _ensure_importable_or_stub(name: str, *, is_package: bool = False) -> None: if name in sys.modules: return try: __import__(name) except Exception: sys.modules[name] = _stub_module(name, is_package=is_package) # Optional UI/OS modules: only stub when unavailable (keeps real deps when present). _ensure_importable_or_stub("Foundation") _ensure_importable_or_stub("AppKit") _ensure_importable_or_stub("PIL", is_package=True) _ensure_importable_or_stub("PIL.Image") _ensure_importable_or_stub("PIL.ImageTk") @pytest.fixture(autouse=True) def _reset_tkinter_stubs_between_tests(): # Avoid test-order coupling by resetting call history between tests. for mod in _POPUP_STUBS.values(): mod.reset_mock() _configure_messagebox(_POPUP_STUBS["tkinter.messagebox"]) yield