mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
Grade against 3.14 too, not just run on it
`requires-python` and the CI pin already said 3.14, but ruff, mypy and pylint were still grading against 3.10/3.11 -- 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), `socket.timeout` where `TimeoutError` is (UP041), and ruff's stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`. That 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). ruff, ruff-format, mypy --strict and pylint all clean at the new target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
This commit is contained in:
parent
d14ece9129
commit
c1dbdcda22
@ -25,7 +25,7 @@ dependencies = [
|
||||
include = ["wake_alarm*"]
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
target-version = "py314"
|
||||
include = ["*.py", "**/*.py"]
|
||||
exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"]
|
||||
|
||||
@ -74,7 +74,7 @@ line-ending = "auto"
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
python_version = "3.14"
|
||||
strict = true
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
@ -108,7 +108,7 @@ init-hook = "import sys; sys.path.insert(0, '.')"
|
||||
analyse-fallback-blocks = true
|
||||
persistent = true
|
||||
jobs = 0
|
||||
py-version = "3.11"
|
||||
py-version = "3.14"
|
||||
# "tests" is a basename match: test suites intentionally use patterns
|
||||
# (protected-access, magic-value comparisons) that don't apply to source
|
||||
# code, matching testsAndMisc's pylint scope (tests are linted by ruff, not
|
||||
|
||||
@ -7,7 +7,7 @@ Started by ``morning-routine.service``, never directly (AGENTS.md). Gate:
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
@ -206,7 +206,7 @@ class WakeAlarm(AlarmTimersMixin, AlarmFlashMixin):
|
||||
self._active = False
|
||||
self.dismissed = True
|
||||
self._stop_beep.set()
|
||||
now_iso = datetime.now(tz=timezone.utc).isoformat()
|
||||
now_iso = datetime.now(tz=UTC).isoformat()
|
||||
save_wake_state(dismissed_at=now_iso, skip_workout=earned_skip)
|
||||
show_dismissal(
|
||||
self._view.containers,
|
||||
@ -236,7 +236,7 @@ def main() -> None:
|
||||
|
||||
_logger.warning(
|
||||
"ALARM TRIGGERED at %s (demo=%s, trigger_now=%s)",
|
||||
datetime.now(tz=timezone.utc).isoformat(timespec="seconds"),
|
||||
datetime.now(tz=UTC).isoformat(timespec="seconds"),
|
||||
args.demo,
|
||||
args.trigger_now,
|
||||
)
|
||||
|
||||
@ -31,7 +31,7 @@ def _ddcutil_power_on() -> None:
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("ddcutil setvcp failed", exc_info=True)
|
||||
return
|
||||
if result.returncode != 0:
|
||||
|
||||
@ -34,7 +34,7 @@ record cannot simply be edited to say ``dismissed``.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
@ -54,7 +54,7 @@ STATE_FINISHED = "finished"
|
||||
|
||||
def _now() -> datetime:
|
||||
"""Return the current instant, aware and local. The seam tests patch."""
|
||||
return datetime.now(tz=timezone.utc).astimezone()
|
||||
return datetime.now(tz=UTC).astimezone()
|
||||
|
||||
|
||||
def _write(entry: dict[str, object]) -> bool:
|
||||
@ -136,7 +136,7 @@ def load_session() -> dict[str, object] | None:
|
||||
try:
|
||||
with ALARM_SESSION_FILE.open() as handle:
|
||||
entry = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
_logger.warning("Cannot read the alarm session record")
|
||||
return None
|
||||
if not isinstance(entry, dict):
|
||||
|
||||
@ -104,7 +104,7 @@ def _try_player(binary: str, wav: Path) -> bool:
|
||||
timeout=_TONE_TIMEOUT_SECONDS,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("%s failed playing %s", binary, wav.name, exc_info=True)
|
||||
return False
|
||||
if result.returncode != 0:
|
||||
@ -158,7 +158,7 @@ def _play_tone(frequency: int) -> None:
|
||||
timeout=_TONE_TIMEOUT_SECONDS,
|
||||
check=False,
|
||||
)
|
||||
except (FileNotFoundError, OSError, subprocess.TimeoutExpired):
|
||||
except FileNotFoundError, OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning(
|
||||
"All tone players failed at %d Hz; falling back to soft beep",
|
||||
frequency,
|
||||
|
||||
@ -27,7 +27,7 @@ def _set_max_brightness() -> None:
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("xrandr --query failed; skipping max-brightness", exc_info=True)
|
||||
return
|
||||
for line in result.stdout.splitlines():
|
||||
@ -40,7 +40,7 @@ def _set_max_brightness() -> None:
|
||||
capture_output=True,
|
||||
timeout=5,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning(
|
||||
"Failed to set brightness on %s",
|
||||
output,
|
||||
|
||||
@ -107,7 +107,7 @@ def _load_config() -> dict[str, object] | None:
|
||||
try:
|
||||
with CURTAINS_CONFIG_FILE.open() as handle:
|
||||
config = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
_logger.warning(
|
||||
"Cannot read %s; falling back to the no-op curtain driver",
|
||||
CURTAINS_CONFIG_FILE,
|
||||
@ -157,7 +157,7 @@ def open_curtains() -> bool:
|
||||
"""
|
||||
try:
|
||||
return get_driver().open()
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
except OSError, RuntimeError, ValueError:
|
||||
_logger.warning(
|
||||
"Curtain driver failed to open; the alarm continues",
|
||||
exc_info=True,
|
||||
|
||||
@ -12,7 +12,7 @@ time gate, a ``WantedBy=`` would fire the alarm at login.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
import logging
|
||||
|
||||
from wake_alarm._constants import ALARM_DAYS
|
||||
@ -30,7 +30,7 @@ def is_alarm_day(*, now: datetime | None = None) -> bool:
|
||||
Returns:
|
||||
True when today's weekday is in ``ALARM_DAYS``.
|
||||
"""
|
||||
moment = now if now is not None else datetime.now(tz=timezone.utc).astimezone()
|
||||
moment = now if now is not None else datetime.now(tz=UTC).astimezone()
|
||||
return moment.weekday() in ALARM_DAYS
|
||||
|
||||
|
||||
|
||||
@ -98,7 +98,7 @@ def kill_stale_inhibitors() -> int:
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("Could not check for a stale power-key inhibitor")
|
||||
return 0
|
||||
killed = 0
|
||||
@ -107,7 +107,7 @@ def kill_stale_inhibitors() -> int:
|
||||
continue
|
||||
try:
|
||||
os.kill(int(pid), signal.SIGTERM)
|
||||
except (OSError, ValueError):
|
||||
except OSError, ValueError:
|
||||
continue
|
||||
killed += 1
|
||||
if killed:
|
||||
|
||||
@ -59,7 +59,7 @@ class DedupeFilter(logging.Filter):
|
||||
"""
|
||||
try:
|
||||
raw = json.loads(self._state_file.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
except OSError, ValueError:
|
||||
return "", 0
|
||||
if not isinstance(raw, dict):
|
||||
return "", 0
|
||||
|
||||
@ -47,7 +47,7 @@ def _pactl_text(pactl: str, *args: str, timeout: float = 5.0) -> str | None:
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("pactl %s failed", " ".join(args), exc_info=True)
|
||||
return None
|
||||
return result.stdout.decode(errors="replace")
|
||||
@ -79,7 +79,7 @@ def _current_default_sink(pactl: str) -> str | None:
|
||||
timeout=3,
|
||||
check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
except OSError, subprocess.TimeoutExpired:
|
||||
_logger.warning("pactl get-default-sink failed", exc_info=True)
|
||||
return None
|
||||
name = result.stdout.decode(errors="replace").strip()
|
||||
|
||||
@ -63,7 +63,7 @@ def _load_config() -> dict[str, str] | None:
|
||||
TAPO_CONFIG_FILE,
|
||||
)
|
||||
return None
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
_logger.warning(
|
||||
"Tapo config %s is unreadable or malformed; skipping plug control",
|
||||
TAPO_CONFIG_FILE,
|
||||
@ -94,12 +94,12 @@ async def _connect(config: dict[str, str]) -> Device | None:
|
||||
config["host"],
|
||||
credentials=Credentials(config["email"], config["password"]),
|
||||
)
|
||||
except (KasaException, OSError, asyncio.TimeoutError):
|
||||
except TimeoutError, KasaException, OSError:
|
||||
_logger.warning("Tapo plug discovery failed", exc_info=True)
|
||||
return None
|
||||
try:
|
||||
await dev.update()
|
||||
except (KasaException, OSError, asyncio.TimeoutError):
|
||||
except TimeoutError, KasaException, OSError:
|
||||
_logger.warning("Tapo plug update failed", exc_info=True)
|
||||
with contextlib.suppress(KasaException, OSError):
|
||||
await dev.disconnect()
|
||||
@ -120,7 +120,7 @@ async def _set_state(*, on: bool) -> None:
|
||||
await dev.turn_on()
|
||||
else:
|
||||
await dev.turn_off()
|
||||
except (KasaException, OSError, asyncio.TimeoutError):
|
||||
except TimeoutError, KasaException, OSError:
|
||||
_logger.warning("Tapo plug toggle failed", exc_info=True)
|
||||
finally:
|
||||
with contextlib.suppress(KasaException, OSError):
|
||||
@ -141,7 +141,7 @@ def _run(*, on: bool) -> None:
|
||||
|
||||
try:
|
||||
asyncio.run(_runner())
|
||||
except (asyncio.TimeoutError, OSError, RuntimeError):
|
||||
except TimeoutError, OSError, RuntimeError:
|
||||
_logger.warning("Tapo plug control timed out or failed", exc_info=True)
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
@ -18,7 +18,7 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
def _today_str() -> str:
|
||||
"""Return today's date as YYYY-MM-DD in UTC."""
|
||||
return datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
return datetime.now(tz=UTC).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def save_wake_state(
|
||||
@ -73,7 +73,7 @@ def load_wake_state() -> dict[str, object] | None:
|
||||
try:
|
||||
with WAKE_STATE_FILE.open() as f:
|
||||
state = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
_logger.warning("Cannot read wake state file")
|
||||
return None
|
||||
|
||||
@ -121,7 +121,7 @@ def was_workout_logged_today() -> bool:
|
||||
try:
|
||||
with WORKOUT_LOG_FILE.open() as f:
|
||||
log = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
_logger.warning("Cannot read workout log file %s", WORKOUT_LOG_FILE)
|
||||
return False
|
||||
if not isinstance(log, dict):
|
||||
|
||||
@ -94,7 +94,7 @@ def _read_cache() -> tuple[int, int, float] | None:
|
||||
try:
|
||||
with ALARM_CACHE_FILE.open() as handle:
|
||||
cached = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(cached, dict):
|
||||
return None
|
||||
|
||||
@ -61,7 +61,7 @@ def _read_token_config() -> str | None:
|
||||
try:
|
||||
with SYNC_CONFIG_FILE.open() as handle:
|
||||
config = json.load(handle)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
except OSError, json.JSONDecodeError:
|
||||
return None
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
|
||||
@ -21,7 +21,7 @@ must be built in the same timezone the user reads off their phone.
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
@ -53,7 +53,7 @@ def _now() -> datetime:
|
||||
Returns:
|
||||
The current local time, with tzinfo set. Also the seam tests patch.
|
||||
"""
|
||||
return datetime.now(tz=timezone.utc).astimezone()
|
||||
return datetime.now(tz=UTC).astimezone()
|
||||
|
||||
|
||||
def _fire_window(alarm: Alarm, now: datetime) -> tuple[datetime, datetime]:
|
||||
|
||||
@ -7,7 +7,7 @@ write the *production* record, which the next real trigger tick would act on.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -61,7 +61,7 @@ class TestShouldRefire:
|
||||
clear; unbounded, this would restart the alarm every minute forever.
|
||||
"""
|
||||
start_session("wake-20260827-0700")
|
||||
much_later = datetime.now(tz=timezone.utc).astimezone() + timedelta(days=1)
|
||||
much_later = datetime.now(tz=UTC).astimezone() + timedelta(days=1)
|
||||
with caplog.at_level("INFO"):
|
||||
assert should_refire(now=much_later) is False
|
||||
assert "Ignoring an unfinished alarm session" in caplog.text
|
||||
@ -78,7 +78,7 @@ class TestShouldRefire:
|
||||
exact scenario this record exists to close.
|
||||
"""
|
||||
start_session("wake-20260827-0700")
|
||||
corrected = datetime.now(tz=timezone.utc).astimezone() - timedelta(minutes=5)
|
||||
corrected = datetime.now(tz=UTC).astimezone() - timedelta(minutes=5)
|
||||
with caplog.at_level("INFO"):
|
||||
assert should_refire(now=corrected) is True
|
||||
assert "in the future" in caplog.text
|
||||
@ -88,7 +88,7 @@ class TestShouldRefire:
|
||||
) -> None:
|
||||
"""Bounded, so a broken RTC cannot create an endless re-fire."""
|
||||
start_session("wake-20260827-0700")
|
||||
way_earlier = datetime.now(tz=timezone.utc).astimezone() - timedelta(hours=6)
|
||||
way_earlier = datetime.now(tz=UTC).astimezone() - timedelta(hours=6)
|
||||
with caplog.at_level("WARNING"):
|
||||
assert should_refire(now=way_earlier) is False
|
||||
assert "beyond the" in caplog.text
|
||||
@ -128,7 +128,7 @@ class TestShouldRefire:
|
||||
def test_age_is_reported_for_a_live_session(self) -> None:
|
||||
"""The age helper is what makes the bound observable."""
|
||||
start_session("wake-20260827-0700")
|
||||
later = datetime.now(tz=timezone.utc).astimezone() + timedelta(seconds=90)
|
||||
later = datetime.now(tz=UTC).astimezone() + timedelta(seconds=90)
|
||||
age = unfinished_session_age(now=later)
|
||||
assert age is not None
|
||||
assert 89 <= age <= 92
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
@ -55,7 +55,7 @@ class TestNow:
|
||||
now = _now()
|
||||
assert now.tzinfo is not None
|
||||
# Local, not UTC: the wall-clock hour must match the system clock's.
|
||||
assert now.hour == datetime.now(tz=timezone.utc).astimezone().hour
|
||||
assert now.hour == datetime.now(tz=UTC).astimezone().hour
|
||||
|
||||
|
||||
class TestFireWindow:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user