mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:07:17 +01:00
Fix the cap hook in CI, and the re-exports its --all-files run exposed
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
This commit is contained in:
parent
966ec60f17
commit
9f271b0d37
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -26,6 +26,7 @@ class FirebaseAuthError(RemoteSyncError):
|
||||
down".
|
||||
"""
|
||||
|
||||
|
||||
def _reason(response: _requests.Response) -> str:
|
||||
"""Return Google's machine-readable error reason, in parentheses.
|
||||
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
25
crdt-sync/crdt_sync/_fberrors.py
Normal file
25
crdt-sync/crdt_sync/_fberrors.py
Normal file
@ -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``.
|
||||
"""
|
||||
@ -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 ``<module>.requests.<verb>`` -- 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.
|
||||
|
||||
|
||||
@ -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 ``<module>.requests.<verb>`` -- 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.
|
||||
|
||||
|
||||
@ -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 ``<module>.requests.<verb>`` -- are unchanged by pooling.
|
||||
|
||||
@ -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__)
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
92
crdt-sync/crdt_sync/_syncargs.py
Normal file
92
crdt-sync/crdt_sync/_syncargs.py
Normal file
@ -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
|
||||
@ -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__)
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -21,7 +21,6 @@ from crdt_sync import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
import pytest
|
||||
|
||||
from crdt_sync import Log
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -31,6 +31,7 @@ _GEOMETRY = re.compile(r"(?P<w>\d+)x(?P<h>\d+)\+(?P<x>-?\d+)\+(?P<y>-?\d+)")
|
||||
|
||||
_GEOMETRY = re.compile(r"(?P<w>\d+)x(?P<h>\d+)\+(?P<x>-?\d+)\+(?P<y>-?\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
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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() == ()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user