screen-locker/screen_locker/_shutdown_base.py
Krzysztof kuhy Rudnicki ec0946f64e 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 -- 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 exactly these three knobs.

What the stale target was hiding: 118 sites on `datetime.timezone.utc` instead
of `datetime.UTC` (UP017), two classes still written with `Generic[...]` rather
than PEP 695 syntax, 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).

The PEP 695 fix needed a hand: ruff rewrote `class WidgetGroup(Generic[_W])`
to `class WidgetGroup[W: tk.Misc]` but left the body annotated with the
module-level `_W`, a *different* type variable -- mypy caught it as "a function
returning TypeVar should receive at least one argument containing the same
TypeVar". Renamed the body to the new parameter, gave `FrameGroup.__init__`
the concrete `list[tk.Frame]` it always wrapped, and deleted the two now-dead
module-level TypeVars.

ruff, ruff-format, mypy --strict and both pylint passes clean at the new target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr
2026-08-29 11:42:47 +02:00

115 lines
3.9 KiB
Python

"""Daily shutdown-time base reset for the screen locker.
On each new calendar day the shutdown config is reset to base hours (21:00
by default) so that the day's workout bonuses always layer on top of a known
floor rather than accumulating indefinitely across days.
The sick-day state file is cleared on reset so the sick-restore path cannot
overwrite the fresh base when it runs later in the same startup.
"""
from __future__ import annotations
from datetime import UTC, datetime
import json
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from pathlib import Path
_logger = logging.getLogger(__name__)
_DEFAULT_BASE_HOUR = 21
def get_base_hours(state_file: Path) -> tuple[int, int]:
"""Return ``(base_mon_wed_hour, base_thu_sun_hour)`` from *state_file*.
Falls back to ``(21, 21)`` if the file is missing or corrupt.
"""
if not state_file.exists():
return (_DEFAULT_BASE_HOUR, _DEFAULT_BASE_HOUR)
try:
with state_file.open() as f:
state: dict[str, Any] = json.load(f)
return (
int(state.get("base_mon_wed_hour", _DEFAULT_BASE_HOUR)),
int(state.get("base_thu_sun_hour", _DEFAULT_BASE_HOUR)),
)
except (OSError, json.JSONDecodeError, ValueError) as exc:
_logger.warning(
"Could not read shutdown base hours from %s: %s — defaulting both "
"to %02d:00",
state_file,
exc,
_DEFAULT_BASE_HOUR,
)
return (_DEFAULT_BASE_HOUR, _DEFAULT_BASE_HOUR)
def reset_to_base_if_new_day(
state_file: Path,
mixin: object,
sick_day_state_file: Path | None = None,
) -> bool:
"""Reset the shutdown config to base hours if a new calendar day has begun.
Writes base hours via *mixin*._write_shutdown_config (with restore=True so
the script allows moving the time earlier), updates ``last_reset_date`` in
*state_file*, and removes *sick_day_state_file* if it exists so the
sick-restore path does not fight with the fresh base on the same startup.
Returns True if a reset was performed, False if today was already reset.
"""
today = datetime.now(tz=UTC).strftime("%Y-%m-%d")
if state_file.exists():
try:
with state_file.open() as f:
state: dict[str, Any] = json.load(f)
if state.get("last_reset_date") == today:
return False
except (OSError, json.JSONDecodeError) as exc:
_logger.warning(
"Could not read last_reset_date from %s: %s — cannot tell if "
"today was already reset, so resetting to base hours again",
state_file,
exc,
)
base_mw, base_ts = get_base_hours(state_file)
# Preserve the morning-end hour from the live config.
config = mixin._read_shutdown_config()
morning_end = config[2] if config else 5
ok: bool = mixin._write_shutdown_config(base_mw, base_ts, morning_end, restore=True)
if not ok:
_logger.warning("Daily base reset: failed to write shutdown config.")
return False
# Clear stale sick-day state so it does not override the base reset.
if sick_day_state_file is not None and sick_day_state_file.exists():
try:
sick_day_state_file.unlink()
_logger.info("Daily base reset: cleared stale sick-day state.")
except OSError as exc:
_logger.warning(
"Daily base reset: could not remove sick-day state: %s", exc
)
new_state: dict[str, Any] = {
"base_mon_wed_hour": base_mw,
"base_thu_sun_hour": base_ts,
"last_reset_date": today,
}
try:
with state_file.open("w") as f:
json.dump(new_state, f, indent=2)
except OSError as exc:
_logger.warning("Daily base reset: failed to write state file: %s", exc)
_logger.info("Daily base reset: Mon-Wed=%d, Thu-Sun=%d.", base_mw, base_ts)
return True