From 305e59b3e86a623c1e72242fe3a9e5c335dfac6c Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 29 Aug 2026 11:47:21 +0200 Subject: [PATCH] Grade crdt-sync and gatelock against 3.14 too, not just run on them 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) Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr --- crdt-sync/crdt_sync/_credentials.py | 6 +++--- crdt-sync/crdt_sync/_hlc.py | 10 ++++------ crdt-sync/crdt_sync/_pull.py | 2 +- crdt-sync/crdt_sync/_store.py | 2 +- crdt-sync/crdt_sync/_syncstate.py | 2 +- crdt-sync/crdt_sync/tests/test_firebase.py | 2 +- crdt-sync/crdt_sync/tests/test_firebase_auth.py | 2 +- crdt-sync/crdt_sync/tests/test_firebase_auth_google.py | 2 +- .../crdt_sync/tests/test_firebase_auth_session.py | 2 +- crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py | 2 +- crdt-sync/crdt_sync/tests/test_firebase_writes.py | 2 +- crdt-sync/pyproject.toml | 6 +++--- crdt-sync/tool/link_google.py | 2 +- gatelock/gatelock/_arbiter.py | 4 ++-- gatelock/gatelock/_claims.py | 2 +- gatelock/gatelock/_escape_types.py | 6 +++--- gatelock/gatelock/_randr.py | 2 +- gatelock/gatelock/_scrollkeys.py | 2 +- gatelock/gatelock/_userintent.py | 2 +- gatelock/gatelock/widget_group.py | 4 ++-- gatelock/pyproject.toml | 6 +++--- 21 files changed, 34 insertions(+), 36 deletions(-) diff --git a/crdt-sync/crdt_sync/_credentials.py b/crdt-sync/crdt_sync/_credentials.py index e31aab6..33da087 100644 --- a/crdt-sync/crdt_sync/_credentials.py +++ b/crdt-sync/crdt_sync/_credentials.py @@ -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) diff --git a/crdt-sync/crdt_sync/_hlc.py b/crdt-sync/crdt_sync/_hlc.py index 1a6bbe6..3cbe07a 100644 --- a/crdt-sync/crdt_sync/_hlc.py +++ b/crdt-sync/crdt_sync/_hlc.py @@ -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 diff --git a/crdt-sync/crdt_sync/_pull.py b/crdt-sync/crdt_sync/_pull.py index 9e30d96..ef9f40a 100644 --- a/crdt-sync/crdt_sync/_pull.py +++ b/crdt-sync/crdt_sync/_pull.py @@ -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, diff --git a/crdt-sync/crdt_sync/_store.py b/crdt-sync/crdt_sync/_store.py index 10f4572..c0b879d 100644 --- a/crdt-sync/crdt_sync/_store.py +++ b/crdt-sync/crdt_sync/_store.py @@ -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 {} diff --git a/crdt-sync/crdt_sync/_syncstate.py b/crdt-sync/crdt_sync/_syncstate.py index ac4ecd3..087d940 100644 --- a/crdt-sync/crdt_sync/_syncstate.py +++ b/crdt-sync/crdt_sync/_syncstate.py @@ -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() diff --git a/crdt-sync/crdt_sync/tests/test_firebase.py b/crdt-sync/crdt_sync/tests/test_firebase.py index 599fd49..76b51c4 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase.py +++ b/crdt-sync/crdt_sync/tests/test_firebase.py @@ -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 diff --git a/crdt-sync/crdt_sync/tests/test_firebase_auth.py b/crdt-sync/crdt_sync/tests/test_firebase_auth.py index 9ce8e5b..85934a6 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth.py @@ -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 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 821121d..69f93ed 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_google.py @@ -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 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 9eab966..355b638 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_session.py @@ -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 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 1ae5e04..73aa0dd 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_auth_tokens.py @@ -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 diff --git a/crdt-sync/crdt_sync/tests/test_firebase_writes.py b/crdt-sync/crdt_sync/tests/test_firebase_writes.py index eb5e689..7702250 100644 --- a/crdt-sync/crdt_sync/tests/test_firebase_writes.py +++ b/crdt-sync/crdt_sync/tests/test_firebase_writes.py @@ -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 diff --git a/crdt-sync/pyproject.toml b/crdt-sync/pyproject.toml index 84a0db9..98cdf5d 100644 --- a/crdt-sync/pyproject.toml +++ b/crdt-sync/pyproject.toml @@ -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$"] diff --git a/crdt-sync/tool/link_google.py b/crdt-sync/tool/link_google.py index 071f8c4..a0e3091 100644 --- a/crdt-sync/tool/link_google.py +++ b/crdt-sync/tool/link_google.py @@ -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 diff --git a/gatelock/gatelock/_arbiter.py b/gatelock/gatelock/_arbiter.py index 1e6fb5f..5534544 100644 --- a/gatelock/gatelock/_arbiter.py +++ b/gatelock/gatelock/_arbiter.py @@ -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: diff --git a/gatelock/gatelock/_claims.py b/gatelock/gatelock/_claims.py index d568b63..ba63a24 100644 --- a/gatelock/gatelock/_claims.py +++ b/gatelock/gatelock/_claims.py @@ -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: diff --git a/gatelock/gatelock/_escape_types.py b/gatelock/gatelock/_escape_types.py index 51647e9..7d48307 100644 --- a/gatelock/gatelock/_escape_types.py +++ b/gatelock/gatelock/_escape_types.py @@ -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 " diff --git a/gatelock/gatelock/_randr.py b/gatelock/gatelock/_randr.py index 829635d..3347ede 100644 --- a/gatelock/gatelock/_randr.py +++ b/gatelock/gatelock/_randr.py @@ -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: diff --git a/gatelock/gatelock/_scrollkeys.py b/gatelock/gatelock/_scrollkeys.py index 10c6dac..f9f7a46 100644 --- a/gatelock/gatelock/_scrollkeys.py +++ b/gatelock/gatelock/_scrollkeys.py @@ -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: diff --git a/gatelock/gatelock/_userintent.py b/gatelock/gatelock/_userintent.py index 86f9f7c..8143ceb 100644 --- a/gatelock/gatelock/_userintent.py +++ b/gatelock/gatelock/_userintent.py @@ -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 " diff --git a/gatelock/gatelock/widget_group.py b/gatelock/gatelock/widget_group.py index ad26b25..34b241c 100644 --- a/gatelock/gatelock/widget_group.py +++ b/gatelock/gatelock/widget_group.py @@ -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 diff --git a/gatelock/pyproject.toml b/gatelock/pyproject.toml index 5ea648b..bcbc040 100644 --- a/gatelock/pyproject.toml +++ b/gatelock/pyproject.toml @@ -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$"]