""" Test conftest to provide headless-safe tkinter/ttk stubs so GUI tests can run in CI without a real Tcl/Tk installation. This file is intentionally minimal: it implements only the methods and attributes used by the project test-suite. """ from types import SimpleNamespace import builtins import tkinter as tk from tkinter import ttk class DummyVar: def __init__(self, value=None): self._value = value self._trace_callbacks = [] def get(self): return self._value def set(self, v): self._value = v # invoke trace callbacks registered via trace_add ('write' mode) try: for cb in list(self._trace_callbacks): try: cb() except Exception: pass except Exception: pass def trace_add(self, mode, callback): # minimal shim: accept registration and return a token try: if callable(callback): self._trace_callbacks.append(callback) except Exception: pass return f"trace-{len(self._trace_callbacks)}" def trace_remove(self, mode, token): # best-effort removal by index token format try: if token and token.startswith("trace-"): idx = int(token.split("-", 1)[1]) - 1 if 0 <= idx < len(self._trace_callbacks): self._trace_callbacks.pop(idx) except Exception: pass class DummyWidget: def __init__(self, master=None, **kwargs): self.master = master self.children = {} self._config = dict(kwargs) # Option database placeholder used by some GUI code (kept as method _options) # expose a tk interpreter object for tkinter internals try: self.tk = getattr(master, "tk") except Exception: # Provide a tiny interpreter facade used by some widgets/PIL backends class DummyInterp: def __init__(self): self._commands = {} def createcommand(self, name, func): self._commands[name] = func return name def deletecommand(self, name): self._commands.pop(name, None) def call(self, *args, **kwargs): return None self.tk = DummyInterp() # default identifiers used by tkinter internals self._last_child_ids = {} parent_w = getattr(master, "_w", ".") self._w = parent_w + f".{id(self)}" # geometry managers def pack(self, *a, **k): return None def grid(self, *a, **k): return None def place(self, *a, **k): return None def pack_forget(self): return None def grid_remove(self): # mirror tkinter widget api used by some code paths return None def pack_propagate(self, flag=None): # emulate tkinter.Frame.pack_propagate; tests only set False to disable # geometry propagation. We store it for introspection if needed. if not hasattr(self, "_pack_propagate"): self._pack_propagate = True if flag is not None: self._pack_propagate = bool(flag) return None def config(self, **kwargs): self._config.update(kwargs) # tkinter widgets often expose 'configure' as an alias def configure(self, **kwargs): return self.config(**kwargs) def bind(self, *a, **k): return None def destroy(self): return None def grab_set(self): return None def grab_release(self): return None def after(self, ms, func=None, *args): # return a fake id return f"after-{id(self)}-{ms}" def after_idle(self, func, *args): # matplotlib backend expects after_idle to schedule an idle call. # For headless tests call immediately and return an id-like token. try: func(*args) except Exception: # ignore exceptions raised by idle work in tests pass return f"after-idle-{id(self)}" def after_cancel(self, id_): return None def see(self, *a, **k): return None def focus(self, *a, **k): return None def focus_set(self): return None def selection_set(self, *a, **k): return None def __setitem__(self, key, value): # allow widget[key] = child assignment used by some windows self.children[key] = value def __getitem__(self, key): return self.children.get(key) def selection(self): return [] # window-like helpers expected on Toplevel/Tk def title(self, *a, **k): return None def geometry(self, *a, **k): return None def transient(self, master): return None def resizable(self, a, b): return None def protocol(self, *a, **k): return None def minsize(self, *a, **k): return None def rowconfigure(self, index, **kwargs): if not hasattr(self, "_row_config"): self._row_config = {} self._row_config[index] = kwargs return None def columnconfigure(self, index, **kwargs): if not hasattr(self, "_col_config"): self._col_config = {} self._col_config[index] = kwargs return None # aliases some code expects def grid_rowconfigure(self, index, **kwargs): return self.rowconfigure(index, **kwargs) def grid_columnconfigure(self, index, **kwargs): return self.columnconfigure(index, **kwargs) def set(self, *a, **k): # scrollbar.set compatibility return None def create_image(self, *a, **k): return f"img-{id(self)}" def create_rectangle(self, *a, **k): return f"rect-{id(self)}" def create_line(self, *a, **k): return f"line-{id(self)}" def create_oval(self, *a, **k): return f"oval-{id(self)}" def delete(self, *a, **k): # generic delete for canvas or widgets return None def itemconfig(self, *a, **k): return None def move(self, *a, **k): return None def create_text(self, *a, **k): return f"text-{id(self)}" def create_window(self, *a, **k): return f"window-{id(self)}" def cget(self, key): return self._config.get(key) def winfo_exists(self): return True def winfo_width(self): return getattr(self, "_width", 100) def winfo_height(self): return getattr(self, "_height", 100) def winfo_children(self): return list(self.children.values()) def pack_info(self): return {} def grid_info(self): return {} def winfo_toplevel(self): # walk up to the top-level node = self while getattr(node, "master", None) is not None: node = node.master return node def winfo_rootx(self): return 0 def winfo_rooty(self): return 0 def winfo_x(self): return 0 def winfo_y(self): return 0 def wait_window(self, widget=None): # non-blocking stub for tests return None def _options(self, options: dict): """Convert an options dict into the flattened list expected by tk interpreter calls (e.g. ['-title', 'MyTitle', '-message', '...']). Tests only need that this is callable and returns a sequence. """ res = [] for k, v in (options or {}).items(): # tkinter option names are prefixed with '-' in the interpreter res.append(f"-{k}") res.append(v) return res # Minimal text buffer API used by scrolledtext.ScrolledText in the GUI def insert(self, index, text, tag=None): if not hasattr(self, "_text"): self._text = "" # simple append semantics for tests self._text = (self._text or "") + str(text) def delete(self, start, end=None): # clear buffer entirely for simplicity self._text = "" def get(self, start, end=None): return getattr(self, "_text", "") def tag_config(self, *a, **k): return None def index(self, idx): # return a dummy index return "1.0" def yview(self, *a, **k): # report full-range view (user at bottom) to allow auto-scroll logic return (0.0, 1.0) class DummyTreeview(DummyWidget): def __init__(self, master=None, **kwargs): super().__init__(master, **kwargs) self._items = {} def delete(self, *items): for it in items: if it in self._items: del self._items[it] def insert(self, parent, pos, iid=None, values=None): if iid is None: # generate a deterministic id iid = f"item-{len(self._items)+1}" self._items[iid] = values return iid def get_children(self): return list(self._items.keys()) def heading(self, *a, **k): return None def column(self, *a, **k): return None def configure(self, **kwargs): # accept yscroll or other widget bindings self._config.update(kwargs) return None def yview(self, *a, **k): # Treeview-specific yview may be used by some code; keep full-range return (0.0, 1.0) def item(self, iid, **kwargs): # emulate getting/setting an item if kwargs: # set values if provided if "values" in kwargs: self._items[iid] = kwargs["values"] return None return {"values": self._items.get(iid)} def set(self, item, column=None, value=None): # behave like setting a cell value if item in self._items: vals = list(self._items[item]) if self._items[item] is not None else [] # try to set by index try: idx = int(column) # expand if needed while len(vals) <= idx: vals.append(None) vals[idx] = value self._items[item] = tuple(vals) except Exception: # fallback: ignore pass def selection(self): # return empty selection by default return [] class DummyTk(DummyWidget): def __init__(self, *a, **k): super().__init__(None) # tk attribute used by tkinter internals: provide a fake interpreter class _Interp: def __init__(self): self._commands = {} def createcommand(self, name, func): self._commands[name] = func return name def deletecommand(self, name): self._commands.pop(name, None) def call(self, *args, **kwargs): return None self.tk = _Interp() self._last_child_ids = {} self._w = "." def after(self, ms, func=None, *args): # For logger tests which schedule via after return f"after-{ms}" def title(self, *a, **k): return None def geometry(self, *a, **k): return None def protocol(self, *a, **k): return None def withdraw(self): return None def wait_window(self, widget=None): return None def focus_set(self): return None def winfo_x(self): return 0 self._w = "." # emulate option database expected by some GUI code self._options = {} def winfo_y(self): return 0 # Apply the lightweight stubs to tkinter and ttk used names tk.Tk = DummyTk tk.Toplevel = DummyWidget tk.StringVar = DummyVar tk.BooleanVar = DummyVar tk.DoubleVar = DummyVar tk.IntVar = DummyVar ttk.Frame = DummyWidget ttk.LabelFrame = DummyWidget ttk.Scrollbar = DummyWidget ttk.Button = DummyWidget ttk.Label = DummyWidget ttk.Scale = DummyWidget ttk.Combobox = DummyWidget ttk.Checkbutton = DummyWidget ttk.Treeview = DummyTreeview # Replace tk.Canvas as matplotlib backend sometimes creates it tk.Canvas = DummyWidget # Ensure scrolledtext.ScrolledText uses our DummyWidget so get/insert/delete work try: import tkinter.scrolledtext as scrolledtext scrolledtext.ScrolledText = DummyWidget except Exception: pass class DummyMenu(DummyWidget): def add_cascade(self, label=None, menu=None, **kwargs): return None def add_command(self, label=None, command=None, **kwargs): return None def add_separator(self): return None tk.Menu = DummyMenu # Add set to scrollbar widget def _scrollbar_set(*a, **k): return None ttk.Scrollbar.set = staticmethod(lambda *a, **k: None) # Some modules import names at top-level; also set them on builtins to be safe builtins._DUMMY_TK_INSTALLED_FOR_TESTS = True # PhotoImage stub used by some GUI code class DummyPhotoImage: def __init__(self, *a, **k): self._size = k.get("size", (1, 1)) # Provide a .tk attribute similar to real PhotoImage which holds the tk interpreter try: self.tk = getattr(tk, "_default_root").tk except Exception: self.tk = SimpleNamespace(call=lambda *a, **k: None) def subsample(self, *a, **k): return self def zoom(self, *a, **k): return self tk.PhotoImage = DummyPhotoImage # Minimal ttk.Style stub class DummyStyle: def __init__(self): self._styles = {} def configure(self, style_name, **kwargs): self._styles[style_name] = kwargs def lookup(self, style_name, option, default=None): return self._styles.get(style_name, {}).get(option, default) ttk.Style = DummyStyle # If PIL.ImageTk is available, ensure its PhotoImage is replaced by our dummy to avoid # AttributeError when Pillow's PhotoImage expects a real tk interpreter in headless tests. try: from PIL import ImageTk ImageTk.PhotoImage = DummyPhotoImage except Exception: pass # provide a default root used by some libraries _DEFAULT_ROOT = DummyTk() tk._default_root = _DEFAULT_ROOT # ensure Scrollbar is available on tk and ttk tk.Scrollbar = DummyWidget ttk.Scrollbar = DummyWidget # add update stubs used by some widgets/backends def _noop_update(self=None): return None DummyWidget.update = _noop_update DummyWidget.update_idletasks = _noop_update DummyTk.update = _noop_update DummyTk.update_idletasks = _noop_update