From 9f271b0d37e842b511c69a91b0b2184f202dc897 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 22 Aug 2026 15:52:59 +0200 Subject: [PATCH] Fix the cap hook in CI, and the re-exports its --all-files run exposed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook entry was an absolute /home/kuhy/utils path, copied from a config written for ~/.claude where it only ever ran locally. On an Actions runner that is exit code 127 — "No such file or directory" — so gatelock-precommit and crdt-sync-precommit failed on every commit of this refactor while the hook silently checked nothing. Now repo-relative, matching the sibling hooks (`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the git root, so it resolves the same in both places. Fixing that made the CI configs runnable locally, and `--all-files` then found what per-file runs had missed — ruff pruning imports that exist *for* their re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore, encode_key, FileCredentialStore). Each affected module now states an explicit __all__ so the autofixer cannot take them again, and the two files that pushed past the cap once __all__ was added are split further: _firebase.py 260 -> 247 + _fberrors 25 _sync.py 257 -> 188 + _syncargs 91 Test patch targets followed the code they patch: gatelock._outputs.shutil -> _xrandr.shutil, gatelock._arbiter.Path -> _claims.Path. gatelock and crdt-sync both pass `pre-commit run --all-files` in full; 389 and 259 tests at 100% branch coverage; the root gate exits 0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG --- .pre-commit-config.yaml | 7 +- crdt-sync/.pre-commit-config.yaml | 2 +- crdt-sync/crdt_sync/_autherrors.py | 1 + crdt-sync/crdt_sync/_credentials.py | 7 +- crdt-sync/crdt_sync/_fberrors.py | 25 +++++ crdt-sync/crdt_sync/_firebase.py | 29 +++--- crdt-sync/crdt_sync/_firebase_auth.py | 16 +-- crdt-sync/crdt_sync/_github.py | 1 - crdt-sync/crdt_sync/_pull.py | 3 +- crdt-sync/crdt_sync/_revisions.py | 1 + crdt-sync/crdt_sync/_sync.py | 99 ++++--------------- crdt-sync/crdt_sync/_syncargs.py | 92 +++++++++++++++++ crdt-sync/crdt_sync/_syncstate.py | 6 +- .../crdt_sync/tests/test_config_client.py | 4 +- .../tests/test_firebase_auth_google.py | 1 - .../tests/test_firebase_auth_session.py | 2 - .../tests/test_firebase_auth_tokens.py | 1 - .../crdt_sync/tests/test_firebase_writes.py | 1 - .../crdt_sync/tests/test_github_writes.py | 1 - .../crdt_sync/tests/test_mirror_fallback.py | 1 - .../tests/test_sync_revisions_backends.py | 2 - .../tests/test_sync_revisions_peers.py | 1 - crdt-sync/crdt_sync/tests/test_sync_skips.py | 1 - crdt-sync/tool/google_id_token.py | 20 ++-- design_system/.pre-commit-config.yaml | 2 +- gatelock/.pre-commit-config.yaml | 2 +- gatelock/gatelock/__init__.py | 4 +- gatelock/gatelock/_arbiter.py | 6 +- gatelock/gatelock/_outputs.py | 8 +- gatelock/gatelock/_placement.py | 3 +- gatelock/gatelock/_randr.py | 4 +- gatelock/gatelock/_surfaces.py | 2 +- gatelock/gatelock/_textmirror.py | 2 +- gatelock/gatelock/_xrandr.py | 3 +- gatelock/gatelock/tests/test_arbiter.py | 2 +- .../gatelock/tests/test_arbiter_lifecycle.py | 4 +- .../gatelock/tests/test_arbiter_release.py | 4 +- gatelock/gatelock/tests/test_outputs.py | 10 +- .../gatelock/tests/test_outputs_enumerator.py | 2 +- gatelock/gatelock/tests/test_outputs_randr.py | 2 +- .../tests/test_window_surfaces_lifecycle.py | 2 +- sync_settings_ui/.pre-commit-config.yaml | 2 +- web_ui/.pre-commit-config.yaml | 2 +- 43 files changed, 214 insertions(+), 176 deletions(-) create mode 100644 crdt-sync/crdt_sync/_fberrors.py create mode 100644 crdt-sync/crdt_sync/_syncargs.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4de83cf..d452896 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,13 +15,14 @@ # the only one that covers the subprojects without a config of their own # (aur-publish/, guard-lib/, crdt_sync_dart/, file_length/, ...). # -# `entry` is not run through a shell, so `~` would not expand: the absolute -# path is deliberate. +# The path is repo-relative on purpose. pre-commit always runs hooks from the +# git root, so this resolves the same locally and on a CI runner -- an absolute +# /home/... path exits 127 in Actions, which is how this was first written. repos: - repo: local hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true diff --git a/crdt-sync/.pre-commit-config.yaml b/crdt-sync/.pre-commit-config.yaml index 0ca5217..837bf8c 100644 --- a/crdt-sync/.pre-commit-config.yaml +++ b/crdt-sync/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true - id: subproject-scope diff --git a/crdt-sync/crdt_sync/_autherrors.py b/crdt-sync/crdt_sync/_autherrors.py index 1270118..25e352b 100644 --- a/crdt-sync/crdt_sync/_autherrors.py +++ b/crdt-sync/crdt_sync/_autherrors.py @@ -26,6 +26,7 @@ class FirebaseAuthError(RemoteSyncError): down". """ + def _reason(response: _requests.Response) -> str: """Return Google's machine-readable error reason, in parentheses. diff --git a/crdt-sync/crdt_sync/_credentials.py b/crdt-sync/crdt_sync/_credentials.py index af1b0df..e31aab6 100644 --- a/crdt-sync/crdt_sync/_credentials.py +++ b/crdt-sync/crdt_sync/_credentials.py @@ -14,9 +14,10 @@ from dataclasses import dataclass import datetime as dt import json import logging -import os -from pathlib import Path -from typing import Protocol +from typing import TYPE_CHECKING, Protocol + +if TYPE_CHECKING: + from pathlib import Path _logger = logging.getLogger(__name__) diff --git a/crdt-sync/crdt_sync/_fberrors.py b/crdt-sync/crdt_sync/_fberrors.py new file mode 100644 index 0000000..98ae6f2 --- /dev/null +++ b/crdt-sync/crdt_sync/_fberrors.py @@ -0,0 +1,25 @@ +"""The errors the Firebase sync backend raises. + +Split from :mod:`crdt_sync._firebase` for the 250-line cap, mirroring +:mod:`crdt_sync._gherrors` on the GitHub side. + +Re-exported from :mod:`crdt_sync._firebase`, so existing imports keep working. +""" + +from __future__ import annotations + +from crdt_sync._remote import RemoteNotFoundError, RemoteSyncError + + +class FirebaseSyncError(RemoteSyncError): + """Raised for an RTDB failure the caller must not silently ignore.""" + + +class DatabaseNotFoundError(FirebaseSyncError, RemoteNotFoundError): + """Raised when the database is unreachable or the uid is not allowed. + + The Firebase counterpart of :class:`crdt_sync.RepoNotFoundError`: "the + database URL is wrong, or the security rules reject this account", as + opposed to "nothing has been pushed to that path yet", which is benign + and surfaces as ``None``. + """ diff --git a/crdt-sync/crdt_sync/_firebase.py b/crdt-sync/crdt_sync/_firebase.py index a5a57a3..05a05aa 100644 --- a/crdt-sync/crdt_sync/_firebase.py +++ b/crdt-sync/crdt_sync/_firebase.py @@ -19,9 +19,21 @@ from typing import TYPE_CHECKING import requests as _requests +from crdt_sync._fberrors import DatabaseNotFoundError, FirebaseSyncError from crdt_sync._fbkeys import decode_key, encode_key, encode_path + +# Named explicitly so the autofixer cannot prune an import that exists for +# its re-export: the tests import encode_key/decode_key from this module. +__all__ = [ + "DatabaseNotFoundError", + "FirebaseSyncClient", + "FirebaseSyncError", + "decode_key", + "encode_key", + "encode_path", +] from crdt_sync._http import new_session -from crdt_sync._remote import RemoteNotFoundError, RemoteSyncError +from crdt_sync._remote import RemoteSyncError # Bound to the name ``requests`` so the call sites below -- and the tests # that patch ``.requests.`` -- are unchanged by pooling. @@ -36,21 +48,6 @@ _HTTP_UNAUTHORIZED = 401 _HTTP_FORBIDDEN = 403 - -class FirebaseSyncError(RemoteSyncError): - """Raised for an RTDB failure the caller must not silently ignore.""" - - -class DatabaseNotFoundError(FirebaseSyncError, RemoteNotFoundError): - """Raised when the database is unreachable or the uid is not allowed. - - The Firebase counterpart of :class:`crdt_sync.RepoNotFoundError`: "the - database URL is wrong, or the security rules reject this account", as - opposed to "nothing has been pushed to that path yet", which is benign - and surfaces as ``None``. - """ - - class FirebaseSyncClient: """Realtime Database seen through the same contract as GitHub. diff --git a/crdt-sync/crdt_sync/_firebase_auth.py b/crdt-sync/crdt_sync/_firebase_auth.py index a3d2b14..edd6c0b 100644 --- a/crdt-sync/crdt_sync/_firebase_auth.py +++ b/crdt-sync/crdt_sync/_firebase_auth.py @@ -12,10 +12,7 @@ two in step. from __future__ import annotations -from dataclasses import dataclass import datetime as dt -import json -from typing import TYPE_CHECKING, Protocol import requests as _requests @@ -29,20 +26,25 @@ from crdt_sync._credentials import ( ) from crdt_sync._http import new_session +__all__ = [ + "CredentialStore", + "FileCredentialStore", + "FirebaseAuthError", + "FirebaseCredentials", + "FirebaseTokenProvider", + "MemoryCredentialStore", +] + # Bound to the name ``requests`` so the call sites below -- and the tests # that patch ``.requests.`` -- are unchanged by pooling. requests = new_session() -if TYPE_CHECKING: - from pathlib import Path - _SIGN_IN_BASE = "https://identitytoolkit.googleapis.com/v1" _REFRESH_BASE = "https://securetoken.googleapis.com/v1" _DEFAULT_TIMEOUT_SECONDS = 15 - class FirebaseTokenProvider: """Signs in and keeps a valid ID token available, refreshing as needed. diff --git a/crdt-sync/crdt_sync/_github.py b/crdt-sync/crdt_sync/_github.py index 1ca24b8..ce88359 100644 --- a/crdt-sync/crdt_sync/_github.py +++ b/crdt-sync/crdt_sync/_github.py @@ -15,7 +15,6 @@ import requests as _requests from crdt_sync._gherrors import GitHubSyncError, RepoNotFoundError from crdt_sync._http import new_session -from crdt_sync._remote import RemoteNotFoundError, RemoteSyncError # Bound to the name ``requests`` so the call sites below -- and the tests # that patch ``.requests.`` -- are unchanged by pooling. diff --git a/crdt-sync/crdt_sync/_pull.py b/crdt-sync/crdt_sync/_pull.py index 9b85de2..9e30d96 100644 --- a/crdt-sync/crdt_sync/_pull.py +++ b/crdt-sync/crdt_sync/_pull.py @@ -9,7 +9,6 @@ all, so a quiet device costs one small read instead of one read per peer. from __future__ import annotations from dataclasses import dataclass -import json import logging from typing import TYPE_CHECKING @@ -18,9 +17,9 @@ if TYPE_CHECKING: from crdt_sync._log import Log from crdt_sync._remote import RemoteStore + from crdt_sync._syncstate import SyncState from crdt_sync._revisions import revision_of -from crdt_sync._syncstate import SyncState _logger = logging.getLogger(__name__) diff --git a/crdt-sync/crdt_sync/_revisions.py b/crdt-sync/crdt_sync/_revisions.py index e14cc7d..5b955cb 100644 --- a/crdt-sync/crdt_sync/_revisions.py +++ b/crdt-sync/crdt_sync/_revisions.py @@ -12,6 +12,7 @@ from __future__ import annotations import hashlib + def revision_of(encoded_log: str) -> str: """Return the revision of a serialized log: a content hash. diff --git a/crdt-sync/crdt_sync/_sync.py b/crdt-sync/crdt_sync/_sync.py index d749bbe..ea9c3ed 100644 --- a/crdt-sync/crdt_sync/_sync.py +++ b/crdt-sync/crdt_sync/_sync.py @@ -11,23 +11,18 @@ Mirrors ``crdt_sync_dart``'s ``lib/src/sync.dart``; keep the two in step. from __future__ import annotations -from dataclasses import dataclass, field -import hashlib -import json +from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING from crdt_sync._log import merge_logs if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - from crdt_sync._log import Log - from crdt_sync._remote import RemoteStore -from crdt_sync._pull import _PullContext, _pull_remote_logs, _remote_revs +from crdt_sync._pull import _pull_remote_logs, _PullContext, _remote_revs from crdt_sync._revisions import revision_of +from crdt_sync._syncargs import LogCodec, RevisionTracking, SyncTarget from crdt_sync._syncstate import ( FileSyncStateStore, MemorySyncStateStore, @@ -35,9 +30,22 @@ from crdt_sync._syncstate import ( SyncStateStore, ) -_logger = logging.getLogger(__name__) +# Named explicitly so the autofixer cannot prune an import that exists for its +# re-export: crdt_sync/__init__.py imports all of these from here. +__all__ = [ + "FileSyncStateStore", + "LogCodec", + "MemorySyncStateStore", + "RevisionTracking", + "SyncState", + "SyncStateStore", + "SyncTarget", + "default_revs_path", + "revision_of", + "sync_log", +] -_DEFAULT_FILENAME = "log.json" +_logger = logging.getLogger(__name__) def default_revs_path(path_prefix: str) -> str: @@ -62,75 +70,6 @@ def default_revs_path(path_prefix: str) -> str: return f"{head}/revs" if separator else f"{path_prefix}/revs" -@dataclass(frozen=True) -class SyncTarget: - """Which remote, which device, and where its logs live. - - Args: - client: An authenticated :class:`RemoteStore` -- a - :class:`crdt_sync.GitHubSyncClient`, a - :class:`crdt_sync.FirebaseSyncClient`, or a mirror of both. - device_id: This device's identifier; also the directory name its own - log is pushed under. - path_prefix: The directory holding one subdirectory per device - (e.g. ``"devices"``). - legacy_device_id: The id this device pushed under before migrating to - a persisted uuid. Treated as this device's own for skip-own - purposes, so its pre-migration log is not pulled back and - re-merged as a peer's. Pass ``None`` once the old path has been - reclaimed. - """ - - client: RemoteStore - device_id: str - path_prefix: str - legacy_device_id: str | None = None - - @property - def own_ids(self) -> frozenset[str]: - """Every id whose pushed log belongs to this device.""" - if self.legacy_device_id is None: - return frozenset({self.device_id}) - return frozenset({self.device_id, self.legacy_device_id}) - - -@dataclass(frozen=True) -class LogCodec: - """How a log is turned into pushed text and back. - - Args: - encode: Serializes a merged log for pushing. - decode: Parses a remote device's pushed text back into a log. - Raising ``ValueError``, ``KeyError``, or ``TypeError`` is treated - as a corrupt/unparsable push, and that device is skipped for - this tick rather than aborting the whole sync. - filename: The file name each device pushes its log as. - commit_message: The commit message used for this device's push. - """ - - encode: Callable[[Log], str] - decode: Callable[[str], Log] - filename: str = _DEFAULT_FILENAME - commit_message: str = "crdt_sync: update log" - - -@dataclass(frozen=True) -class RevisionTracking: - """Optional skip-unchanged behaviour for a tick. - - Omit it entirely and the tick behaves exactly as it always did: fetch - everything, push unconditionally. - - Args: - state_store: Persists what this device last pushed and last merged. - revs_path: Where revisions live; defaults to - :func:`default_revs_path`. - """ - - state_store: SyncStateStore | None = None - revs_path: str | None = None - - def sync_log( target: SyncTarget, local_log: Log, diff --git a/crdt-sync/crdt_sync/_syncargs.py b/crdt-sync/crdt_sync/_syncargs.py new file mode 100644 index 0000000..0375b84 --- /dev/null +++ b/crdt-sync/crdt_sync/_syncargs.py @@ -0,0 +1,92 @@ +"""The three argument bundles :func:`crdt_sync.sync_log` takes. + +Split from :mod:`crdt_sync._sync` for the 250-line cap. Grouping sync_log's +arguments into what/how/whether bundles is what keeps its signature readable; +the bundles themselves carry no behaviour. + +Re-exported from :mod:`crdt_sync._sync`, so existing imports keep working. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + + from crdt_sync._log import Log + from crdt_sync._remote import RemoteStore + from crdt_sync._syncstate import SyncStateStore + + +_DEFAULT_FILENAME = "log.json" + + +@dataclass(frozen=True) +class SyncTarget: + """Which remote, which device, and where its logs live. + + Args: + client: An authenticated :class:`RemoteStore` -- a + :class:`crdt_sync.GitHubSyncClient`, a + :class:`crdt_sync.FirebaseSyncClient`, or a mirror of both. + device_id: This device's identifier; also the directory name its own + log is pushed under. + path_prefix: The directory holding one subdirectory per device + (e.g. ``"devices"``). + legacy_device_id: The id this device pushed under before migrating to + a persisted uuid. Treated as this device's own for skip-own + purposes, so its pre-migration log is not pulled back and + re-merged as a peer's. Pass ``None`` once the old path has been + reclaimed. + """ + + client: RemoteStore + device_id: str + path_prefix: str + legacy_device_id: str | None = None + + @property + def own_ids(self) -> frozenset[str]: + """Every id whose pushed log belongs to this device.""" + if self.legacy_device_id is None: + return frozenset({self.device_id}) + return frozenset({self.device_id, self.legacy_device_id}) + + +@dataclass(frozen=True) +class LogCodec: + """How a log is turned into pushed text and back. + + Args: + encode: Serializes a merged log for pushing. + decode: Parses a remote device's pushed text back into a log. + Raising ``ValueError``, ``KeyError``, or ``TypeError`` is treated + as a corrupt/unparsable push, and that device is skipped for + this tick rather than aborting the whole sync. + filename: The file name each device pushes its log as. + commit_message: The commit message used for this device's push. + """ + + encode: Callable[[Log], str] + decode: Callable[[str], Log] + filename: str = _DEFAULT_FILENAME + commit_message: str = "crdt_sync: update log" + + +@dataclass(frozen=True) +class RevisionTracking: + """Optional skip-unchanged behaviour for a tick. + + Omit it entirely and the tick behaves exactly as it always did: fetch + everything, push unconditionally. + + Args: + state_store: Persists what this device last pushed and last merged. + revs_path: Where revisions live; defaults to + :func:`default_revs_path`. + """ + + state_store: SyncStateStore | None = None + revs_path: str | None = None diff --git a/crdt-sync/crdt_sync/_syncstate.py b/crdt-sync/crdt_sync/_syncstate.py index 159791e..ac4ecd3 100644 --- a/crdt-sync/crdt_sync/_syncstate.py +++ b/crdt-sync/crdt_sync/_syncstate.py @@ -13,8 +13,10 @@ from __future__ import annotations from dataclasses import dataclass, field import json import logging -from pathlib import Path -from typing import Protocol +from typing import TYPE_CHECKING, Protocol + +if TYPE_CHECKING: + from pathlib import Path _logger = logging.getLogger(__name__) diff --git a/crdt-sync/crdt_sync/tests/test_config_client.py b/crdt-sync/crdt_sync/tests/test_config_client.py index 8283493..734708b 100644 --- a/crdt-sync/crdt_sync/tests/test_config_client.py +++ b/crdt-sync/crdt_sync/tests/test_config_client.py @@ -14,8 +14,6 @@ from __future__ import annotations import json from typing import TYPE_CHECKING -import pytest - from crdt_sync import ( FirebaseConfig, FirebaseSyncClient, @@ -27,6 +25,8 @@ from crdt_sync._config import _DEFAULT_TIMEOUT_SECONDS, firebase_client_for if TYPE_CHECKING: from pathlib import Path + import pytest + _VALID = { "apiKey": "AIzaSyExample", "databaseUrl": "https://kuhy-syncs-default-rtdb.europe-west1.firebasedatabase.app", diff --git a/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py b/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py index ced4a42..821121d 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py @@ -4,7 +4,6 @@ Split from ``test_firebase_auth.py`` (250-line cap), which keeps credential handling and password sign-in. """ - from __future__ import annotations import datetime as dt diff --git a/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py b/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py index 8178a34..9eab966 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py @@ -4,8 +4,6 @@ Split from ``test_firebase_auth.py`` (250-line cap); ``test_firebase_auth_tokens.py`` keeps ID-token refresh. """ - - from __future__ import annotations import datetime as dt diff --git a/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py b/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py index e2cc46b..1ae5e04 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py @@ -4,7 +4,6 @@ Split from ``test_firebase_auth.py`` (250-line cap), which keeps credential handling and the two sign-in paths. """ - from __future__ import annotations import datetime as dt diff --git a/crdt-sync/crdt_sync/tests/test_firebase_writes.py b/crdt-sync/crdt_sync/tests/test_firebase_writes.py index 0e82e5d..eb5e689 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_writes.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_writes.py @@ -4,7 +4,6 @@ Split from ``test_firebase.py`` (250-line cap), which keeps key escaping and the read paths. """ - from __future__ import annotations import datetime as dt diff --git a/crdt-sync/crdt_sync/tests/test_github_writes.py b/crdt-sync/crdt_sync/tests/test_github_writes.py index 8b8fa24..6039245 100644 --- a/crdt-sync/crdt_sync/tests/test_github_writes.py +++ b/crdt-sync/crdt_sync/tests/test_github_writes.py @@ -3,7 +3,6 @@ Split from ``test_github.py`` (250-line cap), which keeps the read paths. """ - from __future__ import annotations from unittest.mock import MagicMock, patch diff --git a/crdt-sync/crdt_sync/tests/test_mirror_fallback.py b/crdt-sync/crdt_sync/tests/test_mirror_fallback.py index d9067d0..e789e59 100644 --- a/crdt-sync/crdt_sync/tests/test_mirror_fallback.py +++ b/crdt-sync/crdt_sync/tests/test_mirror_fallback.py @@ -4,7 +4,6 @@ Split from ``test_mirror.py`` (250-line cap), which keeps plain writes, reads and directory listing. """ - from __future__ import annotations import pytest diff --git a/crdt-sync/crdt_sync/tests/test_sync_revisions_backends.py b/crdt-sync/crdt_sync/tests/test_sync_revisions_backends.py index 7976fad..67396b0 100644 --- a/crdt-sync/crdt_sync/tests/test_sync_revisions_backends.py +++ b/crdt-sync/crdt_sync/tests/test_sync_revisions_backends.py @@ -4,8 +4,6 @@ Split from ``test_sync_revisions.py`` (250-line cap); ``test_sync_revisions_peers.py`` keeps peer-download suppression. """ - - from __future__ import annotations from typing import TYPE_CHECKING diff --git a/crdt-sync/crdt_sync/tests/test_sync_revisions_peers.py b/crdt-sync/crdt_sync/tests/test_sync_revisions_peers.py index f212fee..31e2e82 100644 --- a/crdt-sync/crdt_sync/tests/test_sync_revisions_peers.py +++ b/crdt-sync/crdt_sync/tests/test_sync_revisions_peers.py @@ -4,7 +4,6 @@ Split from ``test_sync_revisions.py`` (250-line cap), which keeps revision identity, state, and no-op push suppression. """ - from __future__ import annotations from crdt_sync import ( diff --git a/crdt-sync/crdt_sync/tests/test_sync_skips.py b/crdt-sync/crdt_sync/tests/test_sync_skips.py index e1779f4..612e5e5 100644 --- a/crdt-sync/crdt_sync/tests/test_sync_skips.py +++ b/crdt-sync/crdt_sync/tests/test_sync_skips.py @@ -21,7 +21,6 @@ from crdt_sync import ( ) if TYPE_CHECKING: - import pytest from crdt_sync import Log diff --git a/crdt-sync/tool/google_id_token.py b/crdt-sync/tool/google_id_token.py index 9c92737..7d3e5c2 100644 --- a/crdt-sync/tool/google_id_token.py +++ b/crdt-sync/tool/google_id_token.py @@ -35,20 +35,22 @@ registered verbatim on the client, e.g. ``http://localhost:8765``. from __future__ import annotations import argparse -import http.server import logging from pathlib import Path import secrets -import socket import sys -import threading -import time -from typing import ClassVar import urllib.parse import webbrowser import requests +from tool._oauth_callback import ( + _CONSENT_TIMEOUT_SECONDS, + _CallbackHandler, + _free_port, + _start_callback_server, +) + _AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth" # Assembled from host and path rather than written as one literal: a string @@ -66,14 +68,6 @@ _TIMEOUT_SECONDS = 30 _logger = logging.getLogger("google_id_token") -from tool._oauth_callback import ( - _CONSENT_TIMEOUT_SECONDS, - _CallbackHandler, - _free_port, - _start_callback_server, -) - - class TokenError(Exception): """The authorization flow did not produce an ID token.""" diff --git a/design_system/.pre-commit-config.yaml b/design_system/.pre-commit-config.yaml index 9d9a9c8..845efd0 100644 --- a/design_system/.pre-commit-config.yaml +++ b/design_system/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true - id: subproject-scope diff --git a/gatelock/.pre-commit-config.yaml b/gatelock/.pre-commit-config.yaml index cd43c59..e9bb7d5 100644 --- a/gatelock/.pre-commit-config.yaml +++ b/gatelock/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true - id: subproject-scope diff --git a/gatelock/gatelock/__init__.py b/gatelock/gatelock/__init__.py index 384f167..bc9db06 100644 --- a/gatelock/gatelock/__init__.py +++ b/gatelock/gatelock/__init__.py @@ -34,8 +34,6 @@ from gatelock._outputs import ( OutputScan, enumerate_outputs, ) -from gatelock._randr import RandrBackend -from gatelock._xrandr import parse_xrandr_query from gatelock._queue import ( QUEUE_DEADLINE_SECONDS, QUEUE_POLL_SECONDS, @@ -43,6 +41,7 @@ from gatelock._queue import ( stronger_claims, wait_for_turn, ) +from gatelock._randr import RandrBackend from gatelock._recovery import RecoveryLoop, RecoveryReport from gatelock._root import GateRoot from gatelock._scrollable import ScrollableSurface @@ -62,6 +61,7 @@ from gatelock._window import ( SpaceStep, TypeRole, ) +from gatelock._xrandr import parse_xrandr_query from gatelock.widget_group import WidgetGroup from gatelock.widgets import ( DEFAULT_WRAP, diff --git a/gatelock/gatelock/_arbiter.py b/gatelock/gatelock/_arbiter.py index 8915668..1e6fb5f 100644 --- a/gatelock/gatelock/_arbiter.py +++ b/gatelock/gatelock/_arbiter.py @@ -33,8 +33,7 @@ from __future__ import annotations from datetime import datetime, timezone import logging import os -from pathlib import Path -from typing import IO, Final +from typing import IO, TYPE_CHECKING, Final import uuid from gatelock._claims import ( @@ -47,6 +46,9 @@ from gatelock._claims import ( read_claim_if_held, ) +if TYPE_CHECKING: + from pathlib import Path + _logger = logging.getLogger(__name__) # Re-exported after the 250-line split: `from gatelock._arbiter import diff --git a/gatelock/gatelock/_outputs.py b/gatelock/gatelock/_outputs.py index b08e792..7d67f32 100644 --- a/gatelock/gatelock/_outputs.py +++ b/gatelock/gatelock/_outputs.py @@ -27,15 +27,12 @@ from __future__ import annotations import logging import re -import shutil -import subprocess -from typing import TYPE_CHECKING, Literal, Protocol +from typing import TYPE_CHECKING if TYPE_CHECKING: - from collections.abc import Sequence import tkinter as tk -from gatelock._output_types import Output, OutputRect, OutputScan, OutputSource +from gatelock._output_types import Output, OutputRect, OutputScan from gatelock._randr import RandrBackend from gatelock._xrandr import parse_xrandr_query, scan_xrandr @@ -56,7 +53,6 @@ __all__ = [ _logger = logging.getLogger(__name__) - # Output lines start at column 0; mode lines are indented four spaces. That # anchor is the whole robustness story -- without it, the mode line # " 2560x1440 59.95*+" parses as a geometry and every modeless output diff --git a/gatelock/gatelock/_placement.py b/gatelock/gatelock/_placement.py index 7f729a1..9e51147 100644 --- a/gatelock/gatelock/_placement.py +++ b/gatelock/gatelock/_placement.py @@ -12,14 +12,13 @@ or unmap a surface; that asymmetry is what makes the recovery loop monotonic. from __future__ import annotations from dataclasses import dataclass, field -import tkinter as tk from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Mapping + import tkinter as tk from gatelock._config import LockConfig - from gatelock._outputs import OutputRect from gatelock._surfaces import SurfaceInfo diff --git a/gatelock/gatelock/_randr.py b/gatelock/gatelock/_randr.py index 5796a00..829635d 100644 --- a/gatelock/gatelock/_randr.py +++ b/gatelock/gatelock/_randr.py @@ -58,12 +58,14 @@ if TYPE_CHECKING: def close(self) -> None: ... + _logger = logging.getLogger(__name__) # RandR's `connection` field: 0 is RR_Connected. Hard-coded rather than # imported so the constant is available without python-xlib installed. _RR_CONNECTED = 0 + def _decode_name(raw: object) -> str: """Normalise a RandR output name, which may arrive as bytes.""" if isinstance(raw, bytes): @@ -153,5 +155,3 @@ class RandrBackend: if not info.mode or info.width <= 0 or info.height <= 0: return None return OutputRect(x=info.x, y=info.y, width=info.width, height=info.height) - - diff --git a/gatelock/gatelock/_surfaces.py b/gatelock/gatelock/_surfaces.py index 7c27d0e..35c6a90 100644 --- a/gatelock/gatelock/_surfaces.py +++ b/gatelock/gatelock/_surfaces.py @@ -41,8 +41,8 @@ from typing import TYPE_CHECKING from gatelock._placement import ( _Surface, blind_outputs, - needs_backdrop_root, enforce_all, + needs_backdrop_root, set_geometry, ) from gatelock._surface_types import ( diff --git a/gatelock/gatelock/_textmirror.py b/gatelock/gatelock/_textmirror.py index 6ddc384..05d3979 100644 --- a/gatelock/gatelock/_textmirror.py +++ b/gatelock/gatelock/_textmirror.py @@ -11,11 +11,11 @@ existing imports of either name keep working. from __future__ import annotations -import tkinter as tk from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Sequence + import tkinter as tk # Tk text indices: the first character, and the last one before the newline # Text always appends. diff --git a/gatelock/gatelock/_xrandr.py b/gatelock/gatelock/_xrandr.py index 953036a..fca2987 100644 --- a/gatelock/gatelock/_xrandr.py +++ b/gatelock/gatelock/_xrandr.py @@ -31,6 +31,7 @@ _GEOMETRY = re.compile(r"(?P\d+)x(?P\d+)\+(?P-?\d+)\+(?P-?\d+)") _GEOMETRY = re.compile(r"(?P\d+)x(?P\d+)\+(?P-?\d+)\+(?P-?\d+)") + def _parse_rect(head: str) -> OutputRect | None: """Parse a geometry token out of an xrandr output line's head segment.""" match = _GEOMETRY.search(head) @@ -108,5 +109,3 @@ def scan_xrandr() -> tuple[Output, ...] | None: _logger.warning("xrandr --query produced no parseable output lines") return None return outputs - - diff --git a/gatelock/gatelock/tests/test_arbiter.py b/gatelock/gatelock/tests/test_arbiter.py index 6eac6ce..fca18c7 100644 --- a/gatelock/gatelock/tests/test_arbiter.py +++ b/gatelock/gatelock/tests/test_arbiter.py @@ -13,7 +13,6 @@ from unittest.mock import patch import pytest -from gatelock._claims import _same_file, _try_lock from gatelock._arbiter import ( RANK_SCREEN_LOCKER, RANK_WAKE_ALARM, @@ -22,6 +21,7 @@ from gatelock._arbiter import ( default_runtime_dir, grab_strength, ) +from gatelock._claims import _same_file, _try_lock if TYPE_CHECKING: from collections.abc import Iterator diff --git a/gatelock/gatelock/tests/test_arbiter_lifecycle.py b/gatelock/gatelock/tests/test_arbiter_lifecycle.py index 5a41a56..efee878 100644 --- a/gatelock/gatelock/tests/test_arbiter_lifecycle.py +++ b/gatelock/gatelock/tests/test_arbiter_lifecycle.py @@ -102,7 +102,7 @@ class TestPublishAndLiveClaims: """A claim that cannot be opened is skipped, not fatal.""" bad = tmp_path / "rt" / "claims" / "0100-1-bad.json" bad.write_text("{}", encoding="utf-8") - with patch("gatelock._arbiter.Path.open", side_effect=OSError("denied")): + with patch("gatelock._claims.Path.open", side_effect=OSError("denied")): assert arb.live_claims() == () def test_reap_skips_recreated_file(self, arb: Arbiter, tmp_path: Path) -> None: @@ -117,7 +117,7 @@ class TestPublishAndLiveClaims: """A failed unlink is logged, not raised.""" dead = tmp_path / "rt" / "claims" / "0300-999-dead.json" dead.write_text(dead_claim().to_json(), "utf-8") - with patch("gatelock._arbiter.Path.unlink", side_effect=OSError("busy")): + with patch("gatelock._claims.Path.unlink", side_effect=OSError("busy")): live = arb.live_claims() # The reap is best-effort: the claim is excluded from the live set even # when the file itself cannot be removed. diff --git a/gatelock/gatelock/tests/test_arbiter_release.py b/gatelock/gatelock/tests/test_arbiter_release.py index 5bc2342..f9edcbd 100644 --- a/gatelock/gatelock/tests/test_arbiter_release.py +++ b/gatelock/gatelock/tests/test_arbiter_release.py @@ -131,7 +131,7 @@ class TestHolder: """An unreadable holder file is reported as no holder.""" (tmp_path / "rt").mkdir(parents=True, exist_ok=True) (tmp_path / "rt" / "holder.lock").write_text("{}", encoding="utf-8") - with patch("gatelock._arbiter.Path.open", side_effect=OSError("denied")): + with patch("gatelock._claims.Path.open", side_effect=OSError("denied")): assert arb.describe_holder() is None @@ -167,7 +167,7 @@ class TestRelease: """A claim that cannot be removed does not break teardown.""" arbiter = hard("a", RANK_DIET_GUARD, tmp_path / "rt") arbiter.publish() - with patch("gatelock._arbiter.Path.unlink", side_effect=OSError("busy")): + with patch("gatelock._claims.Path.unlink", side_effect=OSError("busy")): arbiter.release() def test_claim_property(self, arb: Arbiter) -> None: diff --git a/gatelock/gatelock/tests/test_outputs.py b/gatelock/gatelock/tests/test_outputs.py index 25b4706..096d5a0 100644 --- a/gatelock/gatelock/tests/test_outputs.py +++ b/gatelock/gatelock/tests/test_outputs.py @@ -12,13 +12,13 @@ from unittest.mock import MagicMock, patch import pytest -from gatelock._randr import RandrBackend, _decode_name -from gatelock._xrandr import parse_xrandr_query from gatelock._outputs import ( Output, OutputRect, ) from gatelock._outputs import scan_xrandr as real_scan_xrandr +from gatelock._randr import RandrBackend, _decode_name +from gatelock._xrandr import parse_xrandr_query # Bound at import, so the autouse hermetic patch (which replaces these # attributes) does not hide the real implementations from their own tests. @@ -139,8 +139,8 @@ class TestScanXrandr: def _run(self, **kwargs: object) -> tuple[Output, ...] | None: with ( - patch("gatelock._outputs.shutil.which", return_value="/usr/bin/xrandr"), - patch("gatelock._outputs.subprocess.run", **kwargs), + patch("gatelock._xrandr.shutil.which", return_value="/usr/bin/xrandr"), + patch("gatelock._xrandr.subprocess.run", **kwargs), ): return real_scan_xrandr() @@ -151,7 +151,7 @@ class TestScanXrandr: def test_missing_binary(self) -> None: """xrandr not on PATH degrades rather than raising.""" - with patch("gatelock._outputs.shutil.which", return_value=None): + with patch("gatelock._xrandr.shutil.which", return_value=None): assert real_scan_xrandr() is None def test_nonzero_exit(self) -> None: diff --git a/gatelock/gatelock/tests/test_outputs_enumerator.py b/gatelock/gatelock/tests/test_outputs_enumerator.py index 6c9a9b4..21bd344 100644 --- a/gatelock/gatelock/tests/test_outputs_enumerator.py +++ b/gatelock/gatelock/tests/test_outputs_enumerator.py @@ -9,7 +9,6 @@ from __future__ import annotations from unittest.mock import MagicMock, patch -from gatelock._randr import RandrBackend from gatelock._outputs import ( Output, OutputEnumerator, @@ -17,6 +16,7 @@ from gatelock._outputs import ( OutputScan, enumerate_outputs, ) +from gatelock._randr import RandrBackend # Bound at import, so the autouse hermetic patch (which replaces these # attributes) does not hide the real implementations from their own tests. diff --git a/gatelock/gatelock/tests/test_outputs_randr.py b/gatelock/gatelock/tests/test_outputs_randr.py index 894e5a7..9d1dcd5 100644 --- a/gatelock/gatelock/tests/test_outputs_randr.py +++ b/gatelock/gatelock/tests/test_outputs_randr.py @@ -8,10 +8,10 @@ from __future__ import annotations from unittest.mock import MagicMock, patch -from gatelock._randr import RandrBackend from gatelock._outputs import ( OutputRect, ) +from gatelock._randr import RandrBackend # Bound at import, so the autouse hermetic patch (which replaces these # attributes) does not hide the real implementations from their own tests. diff --git a/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py b/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py index 9a71798..d3d059b 100644 --- a/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py +++ b/gatelock/gatelock/tests/test_window_surfaces_lifecycle.py @@ -68,7 +68,7 @@ class TestRemainingBranches: handle.__enter__.return_value = handle handle.read.side_effect = OSError("io error") with ( - patch("gatelock._arbiter.Path.open", return_value=handle), + patch("gatelock._claims.Path.open", return_value=handle), patch("gatelock._claims._try_lock", return_value=False), ): assert arbiter.live_claims() == () diff --git a/sync_settings_ui/.pre-commit-config.yaml b/sync_settings_ui/.pre-commit-config.yaml index 05830f0..3ec41dd 100644 --- a/sync_settings_ui/.pre-commit-config.yaml +++ b/sync_settings_ui/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true - id: subproject-scope diff --git a/web_ui/.pre-commit-config.yaml b/web_ui/.pre-commit-config.yaml index 7f4b6ef..d3918a2 100644 --- a/web_ui/.pre-commit-config.yaml +++ b/web_ui/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: hooks: - id: file-length-cap name: file length <= 250 lines - entry: bash /home/kuhy/utils/scripts/check_file_length.sh + entry: bash scripts/check_file_length.sh language: system pass_filenames: true - id: subproject-scope