From 0cb575d15f275f91cf53cd7b8b1238bcdd22994e Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 12 Sep 2026 19:41:13 +0200 Subject: [PATCH] gatelock 0.8.1: public recovery/detector handles; declarative types import from _config leetcode-guard's study mode has to stop the re-assertion loop and the output-change watcher before it releases the grab, and could only reach them as LockWindow._recovery / _detector -- attributes 0.8.0 folded into the arming bundle, which broke that documented private dependency. They are public read-only properties now, next to `surfaces`. _window no longer re-exports the LockConfig family; it was the one thing keeping it over the 250-line cap, and every importer now takes those names from gatelock._config (or the package) directly. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA --- gatelock/gatelock/__init__.py | 11 ++---- gatelock/gatelock/_config.py | 3 -- gatelock/gatelock/_fitcheck.py | 2 +- gatelock/gatelock/_scrollable.py | 2 +- gatelock/gatelock/_surfaces.py | 2 +- gatelock/gatelock/_widget_fills.py | 2 +- gatelock/gatelock/_window.py | 39 +++++++++---------- gatelock/gatelock/tests/conftest.py | 3 +- .../tests/test_recovery_reassertion.py | 2 +- gatelock/gatelock/tests/test_scrollable.py | 2 +- .../tests/test_scrollable_into_view.py | 2 +- gatelock/gatelock/tests/test_surfaces.py | 3 +- .../gatelock/tests/test_surfaces_focus.py | 2 +- gatelock/gatelock/tests/test_widgets.py | 2 +- gatelock/gatelock/tests/test_window.py | 3 +- gatelock/gatelock/tests/test_window_grab.py | 3 +- .../gatelock/tests/test_window_surfaces.py | 3 +- .../tests/test_window_surfaces_lifecycle.py | 2 +- .../tests/test_window_surfaces_preempt.py | 3 +- gatelock/gatelock/widgets.py | 2 +- gatelock/pyproject.toml | 2 +- 21 files changed, 40 insertions(+), 55 deletions(-) diff --git a/gatelock/gatelock/__init__.py b/gatelock/gatelock/__init__.py index d8f2d77..4790af7 100644 --- a/gatelock/gatelock/__init__.py +++ b/gatelock/gatelock/__init__.py @@ -15,7 +15,7 @@ from gatelock._arbiter import ( Arbiter, ) from gatelock._claims import ArbiterVerdict, Claim, default_runtime_dir, grab_strength -from gatelock._config import GrabPolicy +from gatelock._config import GrabPolicy, LockConfig, SpaceStep, TypeRole from gatelock._detect import OutputChangeDetector from gatelock._escape import ( EscapeDraft, @@ -25,6 +25,7 @@ from gatelock._escape import ( ) from gatelock._fitcheck import FitResult, measure_fit, report_fit from gatelock._guards import assert_not_under_pytest, wait_for_x_server +from gatelock._hooks import LockWindowHooks from gatelock._keyboard import bind_activate, bind_cancel, escape_text_tab_trap from gatelock._outputs import ( Output, @@ -56,13 +57,7 @@ from gatelock._surfaces import ( ) from gatelock._theme import LockPalette, LockSpacing, LockTypography from gatelock._vt import disable_vt_switching, restore_vt_switching -from gatelock._window import ( - LockConfig, - LockWindow, - LockWindowHooks, - SpaceStep, - TypeRole, -) +from gatelock._window import LockWindow from gatelock._xrandr import parse_xrandr_query from gatelock.widget_group import WidgetGroup from gatelock.widgets import ( diff --git a/gatelock/gatelock/_config.py b/gatelock/gatelock/_config.py index 82ae4c3..2739087 100644 --- a/gatelock/gatelock/_config.py +++ b/gatelock/gatelock/_config.py @@ -6,9 +6,6 @@ lifecycle) and the *declarative* surface an embedding app actually fills in can each be read in one piece. :class:`LockConfig` carries no behaviour beyond resolving its own presets and the design-system scales, and nothing here imports the window machinery -- the dependency runs one way only. - -Imported from :mod:`gatelock._window` as well, so ``from gatelock._window -import LockConfig`` keeps working for the modules and tests that already do. """ from __future__ import annotations diff --git a/gatelock/gatelock/_fitcheck.py b/gatelock/gatelock/_fitcheck.py index 531d67a..8a11a84 100644 --- a/gatelock/gatelock/_fitcheck.py +++ b/gatelock/gatelock/_fitcheck.py @@ -29,8 +29,8 @@ import tkinter as tk from typing import TYPE_CHECKING from gatelock import _density +from gatelock._config import LockConfig from gatelock._scrollable import ScrollableSurface -from gatelock._window import LockConfig if TYPE_CHECKING: from collections.abc import Callable diff --git a/gatelock/gatelock/_scrollable.py b/gatelock/gatelock/_scrollable.py index 896e894..2ef0aef 100644 --- a/gatelock/gatelock/_scrollable.py +++ b/gatelock/gatelock/_scrollable.py @@ -32,7 +32,7 @@ from gatelock._scrollkeys import bind_scroll_keys, track_focus from gatelock._userintent import UserIntentTracker, viewport_targets if TYPE_CHECKING: - from gatelock._window import LockConfig + from gatelock._config import LockConfig # Extra px kept visible around a widget scrolled into view, so a focused # field never sits flush against the viewport edge. diff --git a/gatelock/gatelock/_surfaces.py b/gatelock/gatelock/_surfaces.py index 073b5ea..d7ac6c7 100644 --- a/gatelock/gatelock/_surfaces.py +++ b/gatelock/gatelock/_surfaces.py @@ -68,8 +68,8 @@ __all__ = [ ] if TYPE_CHECKING: + from gatelock._config import LockConfig from gatelock._outputs import OutputScan - from gatelock._window import LockConfig _logger = logging.getLogger(__name__) diff --git a/gatelock/gatelock/_widget_fills.py b/gatelock/gatelock/_widget_fills.py index 011324b..448e4f7 100644 --- a/gatelock/gatelock/_widget_fills.py +++ b/gatelock/gatelock/_widget_fills.py @@ -13,7 +13,7 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: - from gatelock._window import LockConfig + from gatelock._config import LockConfig ButtonVariant = Literal["primary", "secondary", "danger"] """Emphasis role for :func:`gatelock.widgets.make_button`, not a colour.""" diff --git a/gatelock/gatelock/_window.py b/gatelock/gatelock/_window.py index edfaa41..7f81d31 100644 --- a/gatelock/gatelock/_window.py +++ b/gatelock/gatelock/_window.py @@ -33,15 +33,7 @@ import tkinter as tk from typing import TYPE_CHECKING from gatelock import _arming, _preempt -from gatelock._config import ( - GrabKind, - LockConfig, - LockMode, - SpaceStep, - TypeRole, -) from gatelock._detect import OutputChangeDetector -from gatelock._hooks import LockWindowHooks from gatelock._outputs import OutputEnumerator from gatelock._recovery import RecoveryCollaborators, RecoveryLoop from gatelock._surfaces import SurfaceSet @@ -51,22 +43,11 @@ if TYPE_CHECKING: from types import FrameType from gatelock._arbiter import Arbiter + from gatelock._config import LockConfig + from gatelock._hooks import LockWindowHooks _logger = logging.getLogger(__name__) -# Re-exported so `from gatelock._window import LockConfig` -- which several -# sibling modules and the test suite already do -- keeps resolving after the -# declarative half moved to :mod:`gatelock._config`. -__all__ = [ - "GrabKind", - "LockConfig", - "LockMode", - "LockWindow", - "LockWindowHooks", - "SpaceStep", - "TypeRole", -] - # Periodic no-op so a grabbed, event-starved loop keeps handing control back # to Python, letting SIGTERM/SIGINT be serviced promptly. _KEEPALIVE_MS = 250 @@ -133,6 +114,22 @@ class LockWindow: """The live surface set, for apps that need to fan work out over it.""" return self._arming.surfaces + @property + def recovery(self) -> RecoveryLoop: + """The re-assertion loop, for an app that suspends the lock. + + Releasing the grab while this runs is pointless: it re-takes the grab + within a tick. An app that hands the screen to something else + (leetcode-guard's study mode) stops this and :attr:`detector` first, + and starts both again when it re-arms. + """ + return self._arming.recovery + + @property + def detector(self) -> OutputChangeDetector: + """The output-change watcher; see :attr:`recovery` for when to stop it.""" + return self._arming.detector + # -- window mechanics ----------------------------------------------------- def setup(self) -> None: diff --git a/gatelock/gatelock/tests/conftest.py b/gatelock/gatelock/tests/conftest.py index 524cc30..b097e7d 100644 --- a/gatelock/gatelock/tests/conftest.py +++ b/gatelock/gatelock/tests/conftest.py @@ -17,10 +17,11 @@ from unittest.mock import MagicMock, patch import pytest +from gatelock._config import LockConfig from gatelock._outputs import Output, OutputRect, OutputScan from gatelock._recovery import RecoveryCollaborators, RecoveryLoop from gatelock._surfaces import SurfaceSet -from gatelock._window import LockConfig, LockWindow +from gatelock._window import LockWindow if TYPE_CHECKING: from collections.abc import Generator diff --git a/gatelock/gatelock/tests/test_recovery_reassertion.py b/gatelock/gatelock/tests/test_recovery_reassertion.py index 720ece9..4666bff 100644 --- a/gatelock/gatelock/tests/test_recovery_reassertion.py +++ b/gatelock/gatelock/tests/test_recovery_reassertion.py @@ -17,7 +17,7 @@ import tkinter as tk from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch -from gatelock._window import LockConfig +from gatelock._config import LockConfig from gatelock.tests.conftest import BOTH, DEAD_PRIMARY, build_loop if TYPE_CHECKING: diff --git a/gatelock/gatelock/tests/test_scrollable.py b/gatelock/gatelock/tests/test_scrollable.py index 0d55088..be5cc75 100644 --- a/gatelock/gatelock/tests/test_scrollable.py +++ b/gatelock/gatelock/tests/test_scrollable.py @@ -20,8 +20,8 @@ import tkinter as tk import pytest +from gatelock._config import LockConfig from gatelock._scrollable import ScrollableSurface -from gatelock._window import LockConfig pytestmark = pytest.mark.skipif( not os.environ.get("DISPLAY"), diff --git a/gatelock/gatelock/tests/test_scrollable_into_view.py b/gatelock/gatelock/tests/test_scrollable_into_view.py index 0a5a115..6885a24 100644 --- a/gatelock/gatelock/tests/test_scrollable_into_view.py +++ b/gatelock/gatelock/tests/test_scrollable_into_view.py @@ -13,10 +13,10 @@ import tkinter as tk import pytest +from gatelock._config import LockConfig from gatelock._scrollable import ScrollableSurface from gatelock._scrollkeys import bind_scroll_keys from gatelock._userintent import viewport_targets -from gatelock._window import LockConfig pytestmark = pytest.mark.skipif( not os.environ.get("DISPLAY"), diff --git a/gatelock/gatelock/tests/test_surfaces.py b/gatelock/gatelock/tests/test_surfaces.py index 066458a..5f09fc8 100644 --- a/gatelock/gatelock/tests/test_surfaces.py +++ b/gatelock/gatelock/tests/test_surfaces.py @@ -7,13 +7,12 @@ from unittest.mock import MagicMock import pytest -from gatelock._config import GrabPolicy +from gatelock._config import GrabPolicy, LockConfig from gatelock._outputs import Output, OutputRect, OutputScan from gatelock._surfaces import ( SurfaceSet, needs_backdrop_root, ) -from gatelock._window import LockConfig DP0 = Output("DP-0", connected=True, rect=OutputRect(0, 0, 3840, 2160), primary=True) HDMI = Output("HDMI-0", connected=True, rect=OutputRect(3840, 0, 2560, 1440)) diff --git a/gatelock/gatelock/tests/test_surfaces_focus.py b/gatelock/gatelock/tests/test_surfaces_focus.py index 5a6bcd4..3116aaa 100644 --- a/gatelock/gatelock/tests/test_surfaces_focus.py +++ b/gatelock/gatelock/tests/test_surfaces_focus.py @@ -10,6 +10,7 @@ from unittest.mock import MagicMock import pytest +from gatelock._config import LockConfig from gatelock._outputs import Output, OutputRect, OutputScan from gatelock._surfaces import ( SurfaceInfo, @@ -17,7 +18,6 @@ from gatelock._surfaces import ( TextMirror, mirror_text_widgets, ) -from gatelock._window import LockConfig DP0 = Output("DP-0", connected=True, rect=OutputRect(0, 0, 3840, 2160), primary=True) HDMI = Output("HDMI-0", connected=True, rect=OutputRect(3840, 0, 2560, 1440)) diff --git a/gatelock/gatelock/tests/test_widgets.py b/gatelock/gatelock/tests/test_widgets.py index 99f8df3..4b3e4ce 100644 --- a/gatelock/gatelock/tests/test_widgets.py +++ b/gatelock/gatelock/tests/test_widgets.py @@ -16,7 +16,7 @@ from typing import TYPE_CHECKING import pytest -from gatelock._window import LockConfig +from gatelock._config import LockConfig from gatelock.widgets import ( DEFAULT_WRAP, ButtonStyle, diff --git a/gatelock/gatelock/tests/test_window.py b/gatelock/gatelock/tests/test_window.py index b845b6d..10b53be 100644 --- a/gatelock/gatelock/tests/test_window.py +++ b/gatelock/gatelock/tests/test_window.py @@ -6,9 +6,8 @@ from __future__ import annotations import tkinter as tk from unittest.mock import MagicMock, patch -from gatelock._config import GrabPolicy +from gatelock._config import GrabPolicy, LockConfig from gatelock._theme import LockPalette -from gatelock._window import LockConfig from gatelock.tests.conftest import make_window diff --git a/gatelock/gatelock/tests/test_window_grab.py b/gatelock/gatelock/tests/test_window_grab.py index 7434165..c0c1c3c 100644 --- a/gatelock/gatelock/tests/test_window_grab.py +++ b/gatelock/gatelock/tests/test_window_grab.py @@ -9,8 +9,7 @@ from __future__ import annotations import tkinter as tk from unittest.mock import MagicMock, patch -from gatelock._config import GrabPolicy -from gatelock._window import LockConfig +from gatelock._config import GrabPolicy, LockConfig from gatelock.tests.conftest import make_window diff --git a/gatelock/gatelock/tests/test_window_surfaces.py b/gatelock/gatelock/tests/test_window_surfaces.py index 2ac157e..80ee144 100644 --- a/gatelock/gatelock/tests/test_window_surfaces.py +++ b/gatelock/gatelock/tests/test_window_surfaces.py @@ -10,10 +10,9 @@ from gatelock._arbiter import ( RANK_SCREEN_LOCKER, ) from gatelock._claims import Claim -from gatelock._config import GrabPolicy +from gatelock._config import GrabPolicy, LockConfig from gatelock._detect import _RandrEventSource from gatelock._outputs import Output, OutputRect -from gatelock._window import LockConfig from gatelock.tests.conftest import make_window # Captured before the autouse hermetic fixture replaces it. diff --git a/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py b/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py index 569f1d9..0b1325d 100644 --- a/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py +++ b/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py @@ -12,9 +12,9 @@ from unittest.mock import MagicMock, patch from gatelock._arbiter import ( RANK_DIET_GUARD, ) +from gatelock._config import LockConfig from gatelock._detect import _RandrEventSource from gatelock._outputs import Output, OutputRect -from gatelock._window import LockConfig from gatelock.tests.conftest import make_window # Captured before the autouse hermetic fixture replaces it. diff --git a/gatelock/gatelock/tests/test_window_surfaces_preempt.py b/gatelock/gatelock/tests/test_window_surfaces_preempt.py index 76c0fed..e50e2e9 100644 --- a/gatelock/gatelock/tests/test_window_surfaces_preempt.py +++ b/gatelock/gatelock/tests/test_window_surfaces_preempt.py @@ -17,10 +17,9 @@ from gatelock._arbiter import ( RANK_WAKE_ALARM, ) from gatelock._claims import Claim -from gatelock._config import GrabPolicy +from gatelock._config import GrabPolicy, LockConfig from gatelock._detect import _RandrEventSource from gatelock._outputs import Output, OutputRect -from gatelock._window import LockConfig from gatelock.tests.conftest import make_window # Captured before the autouse hermetic fixture replaces it. diff --git a/gatelock/gatelock/widgets.py b/gatelock/gatelock/widgets.py index 7badeae..e0fb26e 100644 --- a/gatelock/gatelock/widgets.py +++ b/gatelock/gatelock/widgets.py @@ -32,7 +32,7 @@ from gatelock._widget_fills import ButtonVariant, _button_fills, _lighten if TYPE_CHECKING: from collections.abc import Callable - from gatelock._window import LockConfig, TypeRole + from gatelock._config import LockConfig, TypeRole @dataclass(frozen=True, slots=True) diff --git a/gatelock/pyproject.toml b/gatelock/pyproject.toml index 87acf55..94071a4 100644 --- a/gatelock/pyproject.toml +++ b/gatelock/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "gatelock" -version = "0.8.0" +version = "0.8.1" description = "Shared per-output lock-window + HMAC log-integrity backend for blocking-overlay apps" requires-python = ">=3.14" dependencies = [] # pure stdlib — tkinter is bundled with Python