mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:27:14 +01:00
Make _FakeTclError subclass the real tkinter.TclError
Found while auditing the `_NO_WIDGETS` exemption added two commits ago: the comment claimed `_gatelock_widgetgroups` binds real `tk` only for its `contextlib.suppress(tk.TclError)` dead-copy guards. That part is true -- it constructs no widgets -- but the consequence was wrong. `_FakeTclError` subclassed `Exception`, not `tkinter.TclError`, so a fake-tk gate test raising it sailed straight through those suppressions. The "a monitor vanished mid-update" paths were therefore only ever exercised against the real error; with the fake they would have raised through the gate. Reproduced directly before fixing: a `_FAKE_TK.TclError` escaped `WidgetGroup.config` both with and without the module patched, so adding the module to the patch set would not have fixed it. `tkinter.TclError` subclasses `Exception`, so inheriting keeps every existing `except _FakeTclError` working and makes both spellings catchable. `test_fake_tclerror_is_catchable_as_the_real_one` pins it and asserts the live copy still gets configured; reverting the base class fails it. Verified: 707 Python tests / 100% branch coverage, pre-commit clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
This commit is contained in:
parent
af461d3157
commit
df5e1272f1
@ -2,7 +2,7 @@ name: diet_guard_app
|
||||
description: "Companion phone app for diet_guard: log meals on the go."
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.21+1
|
||||
version: 1.0.22+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -20,6 +20,7 @@ rather than merely asserting that a widget was constructed.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
from types import SimpleNamespace
|
||||
|
||||
from diet_guard.tests._tk_fakes_canvas import FakeCanvas, FakeScrollbar
|
||||
@ -52,8 +53,18 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class _FakeTclError(Exception):
|
||||
"""Stand-in for ``tkinter.TclError`` (a real, catchable exception)."""
|
||||
class _FakeTclError(tk.TclError):
|
||||
"""Stand-in for ``tkinter.TclError`` (a real, catchable exception).
|
||||
|
||||
Subclasses the *real* ``tkinter.TclError`` rather than ``Exception``.
|
||||
Not every module that catches a dead-widget error is in the fake-tk patch
|
||||
set -- ``_gatelock_widgetgroups`` deliberately is not, since it constructs
|
||||
no widgets -- so its ``contextlib.suppress(tk.TclError)`` refers to the
|
||||
genuine class. With a bare ``Exception`` base the fake escaped that
|
||||
suppression, meaning the "a monitor vanished mid-update" paths were only
|
||||
ever exercised against the real error and would have raised through the
|
||||
gate in a fake-tk test. Inheriting keeps both spellings catchable.
|
||||
"""
|
||||
|
||||
|
||||
class FakeVar:
|
||||
|
||||
@ -18,10 +18,16 @@ from __future__ import annotations
|
||||
import ast
|
||||
import pathlib
|
||||
import tkinter as tk
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from diet_guard import _gatelock_calendar, _gatelock_calendar_ui, _gatelock_ui
|
||||
from diet_guard import (
|
||||
_gatelock_calendar,
|
||||
_gatelock_calendar_ui,
|
||||
_gatelock_ui,
|
||||
_gatelock_widgetgroups,
|
||||
)
|
||||
from diet_guard.tests._gate_fixtures import _GATE_TK_MODULES, fake_tk
|
||||
from diet_guard.tests._tk_fakes import _FAKE_TK, _FAKE_TTK
|
||||
from diet_guard.tests._tk_fakes import _FAKE_TK, _FAKE_TTK, _FakeTclError
|
||||
|
||||
#: Modules that bind ``tk`` but build no part of the gate's widget tree, so
|
||||
#: they never need the shared fake. Each is exempt for a stated reason -- this
|
||||
@ -29,8 +35,9 @@ from diet_guard.tests._tk_fakes import _FAKE_TK, _FAKE_TTK
|
||||
_NO_WIDGETS = frozenset(
|
||||
{
|
||||
# Mirror/adapter layer over widgets built elsewhere; constructs none
|
||||
# of its own, but does bind ``tk`` at runtime for its ``TclError``
|
||||
# suppressions.
|
||||
# of its own. It binds real ``tk`` only to name ``tk.TclError`` in
|
||||
# its dead-copy suppressions, which is why ``_FakeTclError``
|
||||
# subclasses the real one -- see the last test in this file.
|
||||
"diet_guard._gatelock_widgetgroups",
|
||||
# Display-readiness probe. Builds a throwaway ``tk.Tk()`` to test
|
||||
# whether X is reachable -- deliberately NOT part of the gate's widget
|
||||
@ -145,3 +152,24 @@ def test_fake_tk_actually_replaces_real_tkinter() -> None:
|
||||
|
||||
# ...and is restored afterwards, so the fake cannot leak into other tests.
|
||||
assert _gatelock_ui.tk is tk
|
||||
|
||||
|
||||
def test_fake_tclerror_is_catchable_as_the_real_one() -> None:
|
||||
"""The fake ``TclError`` must satisfy ``except tkinter.TclError``.
|
||||
|
||||
Not every module that catches a dead-widget error is in the patch set:
|
||||
``_gatelock_widgetgroups`` is deliberately exempt (it constructs no
|
||||
widgets), so its ``contextlib.suppress(tk.TclError)`` names the genuine
|
||||
class. If the fake did not inherit from it, a fake-tk test raising
|
||||
``_FakeTclError`` would sail straight through that suppression -- the
|
||||
"a monitor vanished mid-update" paths would be untested against the error
|
||||
the tests actually raise, and would raise through the gate instead.
|
||||
"""
|
||||
assert issubclass(_FakeTclError, tk.TclError)
|
||||
|
||||
dead, alive = MagicMock(), MagicMock()
|
||||
dead.configure.side_effect = _FAKE_TK.TclError("bad window path name")
|
||||
# Must not raise: the group has to skip the dead copy and configure the
|
||||
# live one, exactly as it does on a real monitor going dark.
|
||||
_gatelock_widgetgroups.WidgetGroup([dead, alive]).config(fg="#ff0000")
|
||||
alive.configure.assert_called_once_with(fg="#ff0000")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user