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:
Krzysztof Rudnicki 2026-08-22 15:52:59 +02:00
parent 966ec60f17
commit 9f271b0d37
43 changed files with 214 additions and 176 deletions

View File

@ -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

View File

@ -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

View File

@ -26,6 +26,7 @@ class FirebaseAuthError(RemoteSyncError):
down".
"""
def _reason(response: _requests.Response) -> str:
"""Return Google's machine-readable error reason, in parentheses.

View File

@ -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__)

View 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``.
"""

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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__)

View File

@ -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.

View File

@ -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,

View 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

View File

@ -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__)

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 (

View File

@ -21,7 +21,6 @@ from crdt_sync import (
)
if TYPE_CHECKING:
import pytest
from crdt_sync import Log

View File

@ -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."""

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 (

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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.

View File

@ -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() == ()

View File

@ -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

View File

@ -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