Grade crdt-sync and gatelock against 3.14 too, not just run on them
Some checks failed
crdt-sync pre-commit / pre-commit (push) Has been cancelled
crdt-sync tests / test (push) Has been cancelled
dependency freshness / dependencies on newest stable (push) Has been cancelled
file length / file-length (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (push) Has been cancelled
md-naming / markdown naming convention (push) Has been cancelled
palette drift / palette (push) Has been cancelled
shared gate tests / gate unit tests (push) Has been cancelled

Both libraries had already moved `requires-python` to 3.14, but ruff, mypy and
pylint were still grading against 3.10 -- three tools judging an interpreter
nothing runs any more, and ruff withholding the pyupgrade fixes that only
become safe at the newer target. `pin_ci_python.py` rewrites these three knobs.

What the stale target was hiding: `datetime.timezone.utc` where `datetime.UTC`
is now correct (UP017), a dead `timezone` import left behind by it, and ruff's
stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`.
That last one makes the source 3.14-only at the syntax level, which is what
`requires-python = ">=3.14"` already promised; kuhy chose it deliberately over
holding ruff a release back (2026-08-29).

These two are libraries the apps install, so the floor matters more here than
anywhere else in the fleet -- every consumer already pins >=3.14.

Per project: ruff, ruff-format, mypy --strict and pylint clean, crdt-sync 432
tests and gatelock 389 tests, both at 100% line and branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
This commit is contained in:
Krzysztof Rudnicki 2026-08-29 11:47:21 +02:00
parent b914cc87fb
commit 305e59b3e8
21 changed files with 34 additions and 36 deletions

View File

@ -121,11 +121,11 @@ class FileCredentialStore:
"""
try:
data = json.loads(self._path.read_text(encoding="utf-8"))
except (OSError, ValueError):
except OSError, ValueError:
return None
try:
return FirebaseCredentials.from_json(data)
except (KeyError, TypeError, ValueError):
except KeyError, TypeError, ValueError:
return None
def save(self, credentials: FirebaseCredentials) -> None:
@ -144,4 +144,4 @@ class FileCredentialStore:
def _utcnow() -> dt.datetime:
return dt.datetime.now(dt.timezone.utc)
return dt.datetime.now(dt.UTC)

View File

@ -9,11 +9,11 @@ ties), without requiring synchronized clocks across devices.
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import UTC, datetime
import time
_ISO_PREFIX_LEN = 23 # len("YYYY-MM-DDTHH:MM:SS.mmm")
_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
_EPOCH = datetime(1970, 1, 1, tzinfo=UTC)
@dataclass(frozen=True, order=True)
@ -66,7 +66,7 @@ class Hlc:
comparison for the wall-clock component.
"""
seconds, millis = divmod(self.wall_time_ms, 1000)
dt = datetime.fromtimestamp(seconds, tz=timezone.utc)
dt = datetime.fromtimestamp(seconds, tz=UTC)
iso = dt.strftime("%Y-%m-%dT%H:%M:%S")
return f"{iso}.{millis:03d}Z-{self.counter:04x}-{self.node_id}"
@ -85,9 +85,7 @@ class Hlc:
if not sep:
msg = f"not a valid Hlc string: {text!r}"
raise ValueError(msg)
dt = datetime.strptime(iso_part, "%Y-%m-%dT%H:%M:%S.%f").replace(
tzinfo=timezone.utc
)
dt = datetime.strptime(iso_part, "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=UTC)
delta = dt - _EPOCH
wall_time_ms = (
delta.days * 86_400_000 + delta.seconds * 1000 + delta.microseconds // 1000

View File

@ -96,7 +96,7 @@ def _pull_remote_logs(
continue
try:
remote_logs.append(ctx.decode(text))
except (ValueError, KeyError, TypeError):
except ValueError, KeyError, TypeError:
_logger.warning(
"Unparsable log pushed by device %r, skipping",
other_device_id,

View File

@ -53,7 +53,7 @@ def read_log(path: Path) -> Log:
return {}
try:
return load_log(text)
except (ValueError, KeyError, TypeError):
except ValueError, KeyError, TypeError:
return {}

View File

@ -119,7 +119,7 @@ class FileSyncStateStore:
"""Return the stored state, or a default one if unreadable."""
try:
data = json.loads(self._path.read_text(encoding="utf-8"))
except (OSError, ValueError):
except OSError, ValueError:
return SyncState()
if not isinstance(data, dict):
return SyncState()

View File

@ -53,7 +53,7 @@ def _auth(*, signed_in: bool = True) -> FirebaseTokenProvider:
FirebaseCredentials(
id_token=_FAKE_ID,
refresh_token=_FAKE_REFRESH,
expires_at=dt.datetime(2099, 1, 1, tzinfo=dt.timezone.utc),
expires_at=dt.datetime(2099, 1, 1, tzinfo=dt.UTC),
)
if signed_in
else None

View File

@ -22,7 +22,7 @@ from crdt_sync import (
)
from crdt_sync import _firebase_auth as fa
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.timezone.utc)
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.UTC)
# Fixture values referenced through names ruff's hardcoded-credential checks
# (S105-S107) do not key off. Comparing an attribute called `id_token` to a

View File

@ -20,7 +20,7 @@ from crdt_sync import (
)
from crdt_sync import _firebase_auth as fa
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.timezone.utc)
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.UTC)
# Fixture values referenced through names ruff's hardcoded-credential checks
# (S105-S107) do not key off. Comparing an attribute called `id_token` to a

View File

@ -25,7 +25,7 @@ from crdt_sync import _firebase_auth as fa
if TYPE_CHECKING:
from pathlib import Path
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.timezone.utc)
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.UTC)
# Fixture values referenced through names ruff's hardcoded-credential checks
# (S105-S107) do not key off. Comparing an attribute called `id_token` to a

View File

@ -21,7 +21,7 @@ from crdt_sync import (
)
from crdt_sync import _firebase_auth as fa
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.timezone.utc)
_NOW = dt.datetime(2026, 8, 3, 12, tzinfo=dt.UTC)
# Fixture values referenced through names ruff's hardcoded-credential checks
# (S105-S107) do not key off. Comparing an attribute called `id_token` to a

View File

@ -50,7 +50,7 @@ def _auth(*, signed_in: bool = True) -> FirebaseTokenProvider:
FirebaseCredentials(
id_token=_FAKE_ID,
refresh_token=_FAKE_REFRESH,
expires_at=dt.datetime(2099, 1, 1, tzinfo=dt.timezone.utc),
expires_at=dt.datetime(2099, 1, 1, tzinfo=dt.UTC),
)
if signed_in
else None

View File

@ -14,7 +14,7 @@ dependencies = ["requests==2.34.2"]
packages = ["crdt_sync", "crdt_sync.tests"]
[tool.ruff]
target-version = "py310"
target-version = "py314"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
@ -58,7 +58,7 @@ line-ending = "auto"
docstring-code-format = true
[tool.mypy]
python_version = "3.10"
python_version = "3.14"
strict = true
warn_return_any = true
warn_unused_configs = true
@ -86,7 +86,7 @@ exclude = [".venv/"]
analyse-fallback-blocks = true
persistent = true
jobs = 0
py-version = "3.10"
py-version = "3.14"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]

View File

@ -227,7 +227,7 @@ def main(argv: list[str] | None = None) -> int:
config = FirebaseConfig.load()
_logger.info(" .. linking onto %s (uid %s)", config.email, config.uid)
linked_uid = link_google(config, token)
except (LinkError, ConfigError, FirebaseAuthError):
except LinkError, ConfigError, FirebaseAuthError:
# logging.exception already renders the exception and its traceback.
_logger.exception(" FAIL")
return 1

View File

@ -30,7 +30,7 @@ arms anyway. See :meth:`Arbiter.evaluate`.
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
import os
from typing import IO, TYPE_CHECKING, Final
@ -78,7 +78,7 @@ _HOLDER_FILENAME: Final = "holder.lock"
def _utc_now_iso() -> str:
"""Return an ISO-8601 UTC timestamp, sortable as a plain string."""
return datetime.now(tz=timezone.utc).isoformat()
return datetime.now(tz=UTC).isoformat()
class Arbiter:

View File

@ -94,7 +94,7 @@ class Claim:
disable_vt=bool(raw["disable_vt"]),
instance_id=str(raw["instance_id"]),
)
except (KeyError, TypeError, ValueError):
except KeyError, TypeError, ValueError:
return None
def at_least_as_strong_as(self, other: Claim) -> bool:

View File

@ -9,7 +9,7 @@ Re-exported from :mod:`gatelock._escape`, so existing imports keep working.
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from datetime import UTC, datetime
import logging
from typing import Any
@ -67,7 +67,7 @@ class EscapeDraft:
def _today_iso() -> str:
"""Return today's date as ``YYYY-MM-DD`` (UTC)."""
return datetime.now(tz=timezone.utc).strftime(_DATE_FORMAT)
return datetime.now(tz=UTC).strftime(_DATE_FORMAT)
def _parse_iso(date_str: str) -> datetime | None:
@ -77,7 +77,7 @@ def _parse_iso(date_str: str) -> datetime | None:
silently counting it as zero would quietly widen the budget.
"""
try:
return datetime.strptime(date_str, _DATE_FORMAT).replace(tzinfo=timezone.utc)
return datetime.strptime(date_str, _DATE_FORMAT).replace(tzinfo=UTC)
except ValueError as exc:
_logger.warning(
"escape history holds an unparsable date %r (%s) -- that entry is "

View File

@ -140,7 +140,7 @@ class RandrBackend:
"""Return the primary output's id, or None when RandR won't say."""
try:
return root.xrandr_get_output_primary().output
except (OSError, ValueError, KeyError, AttributeError, TypeError):
except OSError, ValueError, KeyError, AttributeError, TypeError:
return None
def _crtc_rect(self, crtc: int, timestamp: int) -> OutputRect | None:

View File

@ -100,7 +100,7 @@ def bind_scroll_keys(canvas: tk.Canvas, content: tk.Misc) -> None:
target: tk.Misc = canvas
try:
target = canvas.winfo_toplevel()
except (tk.TclError, KeyError, AttributeError):
except tk.TclError, KeyError, AttributeError:
# Reachable when the widget tree's root is not a real toplevel --
# notably under tests that mock the Tk root. Degrade to binding on
# the canvas rather than failing to build the viewport at all:

View File

@ -97,7 +97,7 @@ def viewport_targets(canvas: tk.Misc, content: tk.Misc) -> list[tk.Misc]:
targets: list[tk.Misc] = [canvas, content]
try:
targets.append(canvas.winfo_toplevel())
except (tk.TclError, KeyError, AttributeError):
except tk.TclError, KeyError, AttributeError:
_logger.warning(
"no toplevel for scroll viewport; user-input tracking is "
"bound to the viewport only, so focus-following scrolling "

View File

@ -37,7 +37,7 @@ from __future__ import annotations
import contextlib
import tkinter as tk
from typing import TYPE_CHECKING, Generic, TypeVar
from typing import TYPE_CHECKING, TypeVar
if TYPE_CHECKING:
from collections.abc import Callable, Iterator
@ -45,7 +45,7 @@ if TYPE_CHECKING:
_W = TypeVar("_W", bound=tk.Misc)
class WidgetGroup(Generic[_W]):
class WidgetGroup[W: tk.Misc]:
"""Every per-output copy of one logical widget.
Build one per logical widget, :meth:`add` a copy as each surface is

View File

@ -21,7 +21,7 @@ randr = ["python-xlib==0.33"]
include = ["gatelock*"]
[tool.ruff]
target-version = "py310"
target-version = "py314"
include = ["*.py", "**/*.py"]
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
@ -70,7 +70,7 @@ line-ending = "auto"
docstring-code-format = true
[tool.mypy]
python_version = "3.10"
python_version = "3.14"
strict = true
warn_return_any = true
warn_unused_configs = true
@ -98,7 +98,7 @@ exclude = [".venv/"]
analyse-fallback-blocks = true
persistent = true
jobs = 0
py-version = "3.10"
py-version = "3.14"
ignore = [".venv", "__pycache__"]
ignore-patterns = [".*\\.pyi$"]