From ed3f9455c585840513c789fb5135b2205350e63c Mon Sep 17 00:00:00 2001 From: alexeybe1kin <210597588+alexeybe1kin@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:16:56 +0300 Subject: [PATCH] Lint in CI with the shared Conker policy Adds Pi's ruff.toml policy, runs pinned ruff in CI, and fixes 13 findings (UTC alias, comparison order, getattr with a constant, import order). Collector exception comments keep their rationale as plain comments. Refs Conker-AI/conker#52 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 6 ++++++ ruff.toml | 34 ++++++++++++++++++++++++++++++++++ systemgate/backups.py | 4 ++-- systemgate/main.py | 6 +++--- systemgate/runtime.py | 18 +++++++++--------- tests/test_endpoints.py | 1 - 6 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 ruff.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28b89bc..bc3de06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,12 @@ jobs: pip install --quiet -r requirements.txt pip install --quiet pytest httpx + - name: Lint + # Same pinned ruff and policy as Pi (ruff.toml). + run: | + pip install --quiet ruff==0.16.6 + python -m ruff check . + - name: The module contract requires these files to exist run: | missing="" diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..9aa6ac6 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,34 @@ +# Lint settings for SystemGate; the same policy as Pi's, shared across Conker modules. +# +# Explicit rather than default, because the defaults move between ruff releases +# and a lint set that changes under you produces diffs nobody asked for. Pinned +# in requirements-dev.txt for the same reason. + +line-length = 100 +target-version = "py311" + +[lint] +select = [ + "E", "W", # pycodestyle + "F", # pyflakes + "I", # import order + "UP", # modern syntax for the target version + "B", # bugbear + "SIM", # obvious simplifications + "ISC", # implicit string concatenation, which hides missing commas + "RUF", +] +ignore = [ + # Health checks and status endpoints catch broadly on purpose. This service + # reports `unavailable` with a reason when a dependency misbehaves; a check + # that raised instead would take down the very endpoint the owner uses to + # find out what is wrong. See the truthful-status rule in CLAUDE.md. + "BLE001", + # Line length is left to review until this module adopts `ruff format`. + "E501", +] + +[lint.per-file-ignores] +# Test doubles subclass stdlib handlers whose method names are fixed by the +# stdlib, and hold shared state as class attributes on purpose. +"tests/*" = ["N802", "RUF012", "F811"] # F811: pytest fixtures are imported, then requested by name diff --git a/systemgate/backups.py b/systemgate/backups.py index 865013d..caa11b9 100644 --- a/systemgate/backups.py +++ b/systemgate/backups.py @@ -114,7 +114,7 @@ def verify_snapshot(directory: Path) -> dict: except (KeyError, TypeError, AttributeError, ValueError, OverflowError): _invalid("Snapshot creation time is invalid; verify the manifest and system clock.") files = manifest.get("files") - if not isinstance(files, dict) or not REQUIRED <= files.keys(): + if not isinstance(files, dict) or not files.keys() >= REQUIRED: _invalid("Required files are missing from the manifest; create a complete backup.") actual = _files(directory) if files.keys() != actual.keys(): @@ -126,7 +126,7 @@ def verify_snapshot(directory: Path) -> dict: for image in images.values())): _invalid("Image identities are invalid; obtain an intact version manifest.") stores = manifest.get("stores") - if (not isinstance(stores, dict) or not STORES <= stores.keys() + if (not isinstance(stores, dict) or not stores.keys() >= STORES or any(name not in STORES and not re.fullmatch(r"extra-[a-z]+-[0-9]+", name) for name in stores) or {name + ".tar" for name in stores} != {n for n in files if n.endswith(".tar")}): diff --git a/systemgate/main.py b/systemgate/main.py index 1898ecf..56db06f 100644 --- a/systemgate/main.py +++ b/systemgate/main.py @@ -6,7 +6,7 @@ import subprocess import time from contextlib import asynccontextmanager -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -115,7 +115,7 @@ def health(request: Request): scanning, and so one dashboard renders every module with no special cases. """ settings = request.app.state.settings - checked_at = datetime.now(timezone.utc) + checked_at = datetime.now(UTC) checks = { "procfs": _probe(psutil.virtual_memory), "docker": _probe(lambda: _docker_client().ping()), @@ -139,7 +139,7 @@ def health(request: Request): def vitals(): temps = {} try: - sensors = getattr(psutil, "sensors_temperatures") + sensors = psutil.sensors_temperatures temps = {name: [entry._asdict() for entry in values] for name, values in sensors(fahrenheit=False).items()} except (AttributeError, OSError): temps = {} diff --git a/systemgate/runtime.py b/systemgate/runtime.py index bca7e67..bf07192 100644 --- a/systemgate/runtime.py +++ b/systemgate/runtime.py @@ -4,7 +4,7 @@ import math import socket import time -from datetime import datetime, timezone +from datetime import UTC, datetime MAX_ROWS = 200 MAX_SCAN = 2000 @@ -92,9 +92,9 @@ def add(section, row): ): break process_ids[pid] = (created, identity) - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("processes", "process_unavailable") - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("processes", "collection_failed", True) try: @@ -119,7 +119,7 @@ def add(section, row): created = float(psutil.Process(conn.pid).create_time()) if created == process_ids[conn.pid][0]: process_id = process_ids[conn.pid][1] - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("ports", "process_link_unavailable") key = f"{protocol}:{address}:{port}:{process_id or 'unknown'}" if not add( @@ -138,7 +138,7 @@ def add(section, row): }, ): break - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("ports", "listener_collection_failed", True) client = None @@ -207,22 +207,22 @@ def add(section, row): }, ): break - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("containers", "container_unavailable") error("ports", "container_bindings_unavailable") - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("containers", "collection_failed", True) error("ports", "container_bindings_unavailable", True) finally: if client is not None: try: client.close() - except Exception: # noqa: BLE001 - Collector failures must remain isolated and opaque. + except Exception: # Collector failures must remain isolated and opaque. error("containers", "client_close_failed") finished = clock() return { "mode": "observed", - "sampledAt": datetime.fromtimestamp(started, timezone.utc).isoformat(), + "sampledAt": datetime.fromtimestamp(started, UTC).isoformat(), "ageSeconds": max(0, finished - started), "collectionSeconds": max(0, finished - started), "source": { diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 8befdb1..4ef358c 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -4,7 +4,6 @@ from unittest.mock import Mock import pytest - from fastapi.testclient import TestClient