From 42d15b6a917c0be81aa753a50039a22ff89744ba Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 29 Aug 2026 11:35:06 +0200 Subject: [PATCH] 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. The fixes ruff then had to make are what the stale target was hiding: 18 files on `datetime.timezone.utc` instead of `datetime.UTC` (UP017), plus ruff's stable formatter applying PEP 758 -- `except (A, B):` becomes `except A, B:`. That second one makes the source 3.14-only at the syntax level, which is exactly 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) Claude-Session: https://claude.ai/code/session_01SfnJmxxsZ534AKk16rC2Gr --- app/pubspec.yaml | 2 +- diet_guard/_budget.py | 6 +++--- diet_guard/_budget_history.py | 6 +++--- diet_guard/_foodbank.py | 2 +- diet_guard/_foodbank_manual.py | 6 +++--- diet_guard/_gatelock_mealflow.py | 2 +- diet_guard/_gatelock_nutrition.py | 2 +- diet_guard/_kuchnia_config.py | 2 +- diet_guard/_kuchnia_credential_store.py | 2 +- diet_guard/_meal_schedule_store.py | 8 ++++---- diet_guard/_prune_peers.py | 2 +- diet_guard/_state.py | 6 +++--- diet_guard/_sync.py | 4 ++-- diet_guard/_sync_banks.py | 6 +++--- diet_guard/_sync_kuchnia.py | 6 +++--- diet_guard/_sync_refresh.py | 2 +- diet_guard/_test_guard.py | 2 +- diet_guard/sync_merge/_banks.py | 4 ++-- diet_guard/sync_merge/_budget.py | 6 +++--- diet_guard/sync_merge/_clock.py | 4 ++-- diet_guard/sync_merge/_kuchnia.py | 4 ++-- diet_guard/sync_merge/_schedule.py | 4 ++-- diet_guard/tests/test_budget_history_io.py | 8 ++++---- diet_guard/tests/test_cli_gate.py | 4 ++-- diet_guard/tests/test_gate.py | 4 ++-- diet_guard/tests/test_meal_schedule_store.py | 4 ++-- diet_guard/tests/test_slots.py | 4 ++-- pyproject.toml | 6 +++--- scripts/probe_kuchnia.py | 4 ++-- scripts/probe_kuchnia_auth.py | 2 +- scripts/probe_kuchnia_details.py | 4 ++-- 31 files changed, 64 insertions(+), 64 deletions(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index d25f150..8d4169a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,7 @@ name: diet_guard_app description: "Companion phone app for diet_guard: log meals on the go." publish_to: 'none' -version: 1.0.74+1 +version: 1.0.75+1 environment: sdk: ^3.12.2 diff --git a/diet_guard/_budget.py b/diet_guard/_budget.py index a1c1cbf..ab8ae0b 100644 --- a/diet_guard/_budget.py +++ b/diet_guard/_budget.py @@ -19,7 +19,7 @@ keeps up to date on every edit. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging @@ -49,7 +49,7 @@ def _now_local() -> datetime: imported: ``_state`` imports :func:`daily_budget` from this module, so importing back from ``_state`` here would be circular. """ - return datetime.now(tz=timezone.utc).astimezone() + return datetime.now(tz=UTC).astimezone() # A medically sane lower bound. Even an aggressive deficit must not compute @@ -207,7 +207,7 @@ def read_raw_record() -> dict[str, object] | None: try: with BUDGET_FILE.open() as handle: record = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: return None if not isinstance(record, dict): return None diff --git a/diet_guard/_budget_history.py b/diet_guard/_budget_history.py index feb2a22..07005f2 100644 --- a/diet_guard/_budget_history.py +++ b/diet_guard/_budget_history.py @@ -28,7 +28,7 @@ propagating -- it relays them untouched. No coordinated release is needed. from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging @@ -200,7 +200,7 @@ def read_raw_history() -> dict[str, object] | None: try: with BUDGET_HISTORY_FILE.open() as handle: document = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: return None if not isinstance(document, dict): return None @@ -229,5 +229,5 @@ def load_entries() -> tuple[BudgetEntry, ...]: def record_budget_change(kcal: int, *, when: datetime | None = None) -> None: """Record a budget edit, effective from the day it was made.""" - moment = when if when is not None else datetime.now(tz=timezone.utc).astimezone() + moment = when if when is not None else datetime.now(tz=UTC).astimezone() write_raw_history(history_to_json(upsert(load_entries(), kcal=kcal, when=moment))) diff --git a/diet_guard/_foodbank.py b/diet_guard/_foodbank.py index e9beaae..166fa34 100644 --- a/diet_guard/_foodbank.py +++ b/diet_guard/_foodbank.py @@ -56,7 +56,7 @@ def _read_bank() -> dict[str, BankRecord]: try: with FOOD_BANK_FILE.open() as handle: data = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _quarantine_corrupt_bank() return {} if not isinstance(data, dict): diff --git a/diet_guard/_foodbank_manual.py b/diet_guard/_foodbank_manual.py index ce78f3c..c871e9a 100644 --- a/diet_guard/_foodbank_manual.py +++ b/diet_guard/_foodbank_manual.py @@ -19,7 +19,7 @@ next meal. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging @@ -46,7 +46,7 @@ def read_manual_bank() -> dict[str, ManualRecord]: try: with MANUAL_BANK_FILE.open() as handle: bank = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _logger.warning("Manual food bank %s is unreadable", MANUAL_BANK_FILE) return {} if not isinstance(bank, dict): @@ -77,7 +77,7 @@ def add_manual_entry(name: str, record: ManualRecord) -> None: bank = read_manual_bank() stamped = dict(record) stamped["t"] = ( - datetime.now(tz=timezone.utc) + datetime.now(tz=UTC) .astimezone() .isoformat( timespec="seconds", diff --git a/diet_guard/_gatelock_mealflow.py b/diet_guard/_gatelock_mealflow.py index b0acbe1..1c0e530 100644 --- a/diet_guard/_gatelock_mealflow.py +++ b/diet_guard/_gatelock_mealflow.py @@ -195,7 +195,7 @@ class _GateMealFlow(_PullFlows): consumed = today_total_kcal() try: budget = daily_budget() - except (BudgetError, OSError): + except BudgetError, OSError: return f"{consumed:g} kcal today" return ( f"{consumed:g} / {budget:g} kcal · {round(budget - consumed, 1):g} left" diff --git a/diet_guard/_gatelock_nutrition.py b/diet_guard/_gatelock_nutrition.py index db96d22..e2fe2ba 100644 --- a/diet_guard/_gatelock_nutrition.py +++ b/diet_guard/_gatelock_nutrition.py @@ -97,7 +97,7 @@ class _GateNutrition(_GateCore): this_kcal = nutrition.kcal if nutrition is not None else 0.0 try: budget = daily_budget() - except (BudgetError, OSError): + except BudgetError, OSError: tail = f" · this item {this_kcal:g} kcal" if this_kcal else "" self._vars.projection.set(f"Consumed {consumed:g} kcal today{tail}") return diff --git a/diet_guard/_kuchnia_config.py b/diet_guard/_kuchnia_config.py index 920a866..4fae5c3 100644 --- a/diet_guard/_kuchnia_config.py +++ b/diet_guard/_kuchnia_config.py @@ -98,7 +98,7 @@ def load_session_cookie() -> str | None: return None try: cached = json.loads(KUCHNIA_SESSION_FILE.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _logger.warning("Catering session cache %s is unreadable", KUCHNIA_SESSION_FILE) return None value = cached.get(SESSION_COOKIE) if isinstance(cached, dict) else None diff --git a/diet_guard/_kuchnia_credential_store.py b/diet_guard/_kuchnia_credential_store.py index d69760c..e1e338f 100644 --- a/diet_guard/_kuchnia_credential_store.py +++ b/diet_guard/_kuchnia_credential_store.py @@ -51,7 +51,7 @@ def read_synced_credential() -> tuple[str, str, str] | None: return None try: raw = json.loads(KUCHNIA_SYNCED_CREDENTIAL_FILE.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _logger.warning( "Synced catering credential %s is unreadable", KUCHNIA_SYNCED_CREDENTIAL_FILE, diff --git a/diet_guard/_meal_schedule_store.py b/diet_guard/_meal_schedule_store.py index c0e76f3..5ec59e2 100644 --- a/diet_guard/_meal_schedule_store.py +++ b/diet_guard/_meal_schedule_store.py @@ -29,7 +29,7 @@ KEEP IN SYNC WITH ``app/lib/services/meal_schedule_service.dart``. from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging @@ -187,7 +187,7 @@ def read_raw_history() -> dict[str, object] | None: try: with MEAL_SCHEDULE_FILE.open() as handle: document = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _logger.debug("unreadable meal schedule; using the default") return None if not isinstance(document, dict): @@ -215,7 +215,7 @@ def current_schedule() -> MealSchedule: through; the slot arithmetic itself stays a pure function of its arguments. """ - today = datetime.now(tz=timezone.utc).astimezone().date().isoformat() + today = datetime.now(tz=UTC).astimezone().date().isoformat() return schedule_for_day(load_entries(), today) @@ -227,6 +227,6 @@ def record_schedule_change( Seeds the default at the epoch first, so past days keep the four-meal schedule they were actually judged against. """ - moment = when if when is not None else datetime.now(tz=timezone.utc).astimezone() + moment = when if when is not None else datetime.now(tz=UTC).astimezone() entries = seed_default(load_entries()) write_raw_history(history_to_json(upsert(entries, schedule, moment))) diff --git a/diet_guard/_prune_peers.py b/diet_guard/_prune_peers.py index 42c9218..b2e82b4 100644 --- a/diet_guard/_prune_peers.py +++ b/diet_guard/_prune_peers.py @@ -95,7 +95,7 @@ def _record_ids(client: RemoteStore, device_id: str) -> set[str]: return set() try: parsed = json.loads(text) - except (ValueError, TypeError): + except ValueError, TypeError: return set() return set(parsed) if isinstance(parsed, dict) else set() diff --git a/diet_guard/_state.py b/diet_guard/_state.py index 4998497..0b5e91d 100644 --- a/diet_guard/_state.py +++ b/diet_guard/_state.py @@ -10,7 +10,7 @@ instead of silently losing the data it just wrote. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging import os @@ -36,7 +36,7 @@ DayLog = dict[str, list[dict[str, object]]] def now_local() -> datetime: """Return the current time as a timezone-aware local datetime.""" - return datetime.now(tz=timezone.utc).astimezone() + return datetime.now(tz=UTC).astimezone() def _today() -> str: @@ -76,7 +76,7 @@ def _read_raw_log() -> DayLog: try: with FOOD_LOG_FILE.open() as handle: data = json.load(handle) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: _logger.warning("Cannot read food log %s", FOOD_LOG_FILE) return {} if not isinstance(data, dict): diff --git a/diet_guard/_sync.py b/diet_guard/_sync.py index 496f3e0..b0d9dd5 100644 --- a/diet_guard/_sync.py +++ b/diet_guard/_sync.py @@ -79,7 +79,7 @@ def _remote_revisions(client: RemoteStore) -> dict[str, str]: return {} try: return get_string_map(_REVS_DIR) - except (GitHubSyncError, RemoteSyncError): + except GitHubSyncError, RemoteSyncError: # A revision map that cannot be read is not worth failing a sync # over; without it every peer is simply fetched, as before. return {} @@ -122,7 +122,7 @@ def _pull_remote_logs( continue try: remote_logs.append(parse_remote_log(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: # Deliberately not recorded as seen: a corrupt push must be # retried next tick, not remembered as merged. _logger.warning("Unparsable log pushed by device %r, skipping", device_id) diff --git a/diet_guard/_sync_banks.py b/diet_guard/_sync_banks.py index dfab7ae..c17066b 100644 --- a/diet_guard/_sync_banks.py +++ b/diet_guard/_sync_banks.py @@ -96,7 +96,7 @@ def _sync_food_bank(client: GitHubSyncClient, device_ids: Sequence[str]) -> None continue try: merged = merge_logs(merged, parse_remote_food_bank(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: _logger.warning( "Unparsable food bank pushed by device %r, skipping", device_id, @@ -159,7 +159,7 @@ def _sync_manual_bank(client: GitHubSyncClient, device_ids: Sequence[str]) -> No continue try: merged = merge_logs(merged, parse_remote_manual_bank(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: _logger.warning( "Unparsable curated food bank pushed by device %r, skipping", device_id, @@ -201,7 +201,7 @@ def _sync_budget(client: GitHubSyncClient, device_ids: Sequence[str]) -> None: continue try: merged = merge_logs(merged, parse_remote_budget(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: _logger.warning( "Unparsable budget pushed by device %r, skipping", device_id, diff --git a/diet_guard/_sync_kuchnia.py b/diet_guard/_sync_kuchnia.py index af5ba54..6c30b0c 100644 --- a/diet_guard/_sync_kuchnia.py +++ b/diet_guard/_sync_kuchnia.py @@ -15,7 +15,7 @@ menu with the PC switched off; see :mod:`diet_guard.sync_merge._kuchnia`. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging from typing import TYPE_CHECKING @@ -92,7 +92,7 @@ def _bootstrap_credential_log() -> Log: username, password = _kuchnia_config.read_credentials() except KuchniaError: return {} - edited = datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc) + edited = datetime.fromtimestamp(path.stat().st_mtime, tz=UTC) return credential_to_log( username, password, edited.astimezone().isoformat(timespec="seconds") ) @@ -121,7 +121,7 @@ def _sync_kuchnia_credential( continue try: merged = merge_logs(merged, parse_remote_credential(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: _logger.warning( "Unparsable catering credential pushed by device %r, skipping", device_id, diff --git a/diet_guard/_sync_refresh.py b/diet_guard/_sync_refresh.py index 63f2748..0c489c6 100644 --- a/diet_guard/_sync_refresh.py +++ b/diet_guard/_sync_refresh.py @@ -121,7 +121,7 @@ def _merge_peer_logs( continue try: merged = merge_logs(merged, parse_remote_log(text)) - except (TypeError, KeyError, ValueError, json.JSONDecodeError): + except TypeError, KeyError, ValueError, json.JSONDecodeError: # A corrupt push must be retried next tick, not remembered as # merged -- so deliberately not recorded in ``seen``. _logger.warning("Unparsable log pushed by device %r, skipping", device_id) diff --git a/diet_guard/_test_guard.py b/diet_guard/_test_guard.py index 32f975b..5152da8 100644 --- a/diet_guard/_test_guard.py +++ b/diet_guard/_test_guard.py @@ -37,7 +37,7 @@ def _resolved(path: pathlib.Path) -> pathlib.Path: """Resolve ``path`` as far as the filesystem allows, never raising.""" try: return path.expanduser().resolve() - except (OSError, RuntimeError, ValueError): + except OSError, RuntimeError, ValueError: return path diff --git a/diet_guard/sync_merge/_banks.py b/diet_guard/sync_merge/_banks.py index b617243..054b8dc 100644 --- a/diet_guard/sync_merge/_banks.py +++ b/diet_guard/sync_merge/_banks.py @@ -11,7 +11,7 @@ stamp -- those foods were never eaten, so they are not derivable from any log. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json from typing import TYPE_CHECKING @@ -113,7 +113,7 @@ def log_to_manual_bank(log: Log) -> dict[str, dict[str, object]]: continue stored = dict(body) if hlc is not None: - winning = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=timezone.utc) + winning = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=UTC) stored["t"] = winning.astimezone().isoformat(timespec="seconds") bank[name] = stored return bank diff --git a/diet_guard/sync_merge/_budget.py b/diet_guard/sync_merge/_budget.py index 20c16d8..fc9316b 100644 --- a/diet_guard/sync_merge/_budget.py +++ b/diet_guard/sync_merge/_budget.py @@ -14,7 +14,7 @@ brand-new sync payload, so every device pushing it already speaks this format. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json from typing import TYPE_CHECKING @@ -139,7 +139,7 @@ def log_to_history(log: Log) -> tuple[BudgetEntry, ...]: continue if isinstance(value, bool) or not isinstance(value, int): continue - edited = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=timezone.utc) + edited = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=UTC) entries.append( BudgetEntry( effective_from=name[len(_HISTORY_FIELD_PREFIX) :], @@ -163,7 +163,7 @@ def log_to_budget(log: Log) -> dict[str, object] | None: value, hlc = record.fields.get("value", ({}, None)) result: dict[str, object] = dict(value) if isinstance(value, dict) else {} if hlc is not None: - winning_time = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=timezone.utc) + winning_time = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=UTC) result["t"] = winning_time.astimezone().isoformat(timespec="seconds") weight, _ = record.fields.get(_WEIGHT_FIELD, (None, None)) if isinstance(weight, (int, float)) and not isinstance(weight, bool): diff --git a/diet_guard/sync_merge/_clock.py b/diet_guard/sync_merge/_clock.py index a38fce6..72945be 100644 --- a/diet_guard/sync_merge/_clock.py +++ b/diet_guard/sync_merge/_clock.py @@ -10,9 +10,9 @@ would silently change merge outcomes rather than raising. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime -_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) +_EPOCH = datetime(1970, 1, 1, tzinfo=UTC) def _wall_time_ms(stamp: str) -> int: diff --git a/diet_guard/sync_merge/_kuchnia.py b/diet_guard/sync_merge/_kuchnia.py index 8140eb2..eb7ada9 100644 --- a/diet_guard/sync_merge/_kuchnia.py +++ b/diet_guard/sync_merge/_kuchnia.py @@ -28,7 +28,7 @@ KEEP IN SYNC WITH ``app/lib/services/sync_merge_kuchnia.dart``. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json from typing import TYPE_CHECKING @@ -125,7 +125,7 @@ def log_to_credential(log: Log) -> tuple[str, str, str] | None: return None # `password_hlc` cannot be None here: a field's value and its clock arrive # as one tuple, so reaching this line at all means the field was present. - stamp = datetime.fromtimestamp(password_hlc.wall_time_ms / 1000, tz=timezone.utc) + stamp = datetime.fromtimestamp(password_hlc.wall_time_ms / 1000, tz=UTC) return username, password, stamp.astimezone().isoformat(timespec="seconds") diff --git a/diet_guard/sync_merge/_schedule.py b/diet_guard/sync_merge/_schedule.py index 98512bb..2396f9e 100644 --- a/diet_guard/sync_merge/_schedule.py +++ b/diet_guard/sync_merge/_schedule.py @@ -18,7 +18,7 @@ malformed one is skipped exactly as a non-int ``hist:`` value is. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING from crdt_sync import Hlc @@ -97,7 +97,7 @@ def log_to_schedule_history(log: Log) -> tuple[ScheduleEntry, ...]: isinstance(first, int) and isinstance(last, int) and isinstance(count, int) ) or any(isinstance(part, bool) for part in (first, last, count)): continue - edited = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=timezone.utc) + edited = datetime.fromtimestamp(hlc.wall_time_ms / 1000, tz=UTC) entries.append( ScheduleEntry( effective_from=name[len(SCHEDULE_FIELD_PREFIX) :], diff --git a/diet_guard/tests/test_budget_history_io.py b/diet_guard/tests/test_budget_history_io.py index 7dffe6e..67b4367 100644 --- a/diet_guard/tests/test_budget_history_io.py +++ b/diet_guard/tests/test_budget_history_io.py @@ -6,7 +6,7 @@ fixture, so every read/write here is isolated from real user data. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json from diet_guard import _budget, _budget_derived, _budget_history @@ -64,7 +64,7 @@ class TestFileIO: def test_record_budget_change_defaults_to_now(self) -> None: record_budget_change(1900) entries = load_entries() - today = datetime.now(tz=timezone.utc).astimezone().date().isoformat() + today = datetime.now(tz=UTC).astimezone().date().isoformat() assert entries[-1].effective_from == today assert entries[-1].kcal == 1900 @@ -121,7 +121,7 @@ class TestWriteBudgetIntegration: assert schedule.for_day("2026-06-01") == 2200 assert ( schedule.for_day( - datetime.now(tz=timezone.utc).astimezone().date().isoformat(), + datetime.now(tz=UTC).astimezone().date().isoformat(), ) == 2000 ) @@ -140,7 +140,7 @@ class TestWriteBudgetIntegration: _budget.write_budget(2000) entries = load_entries() assert [e.effective_from for e in entries] == [ - datetime.now(tz=timezone.utc).astimezone().date().isoformat(), + datetime.now(tz=UTC).astimezone().date().isoformat(), ] def test_the_history_file_is_plain_readable_json(self) -> None: diff --git a/diet_guard/tests/test_cli_gate.py b/diet_guard/tests/test_cli_gate.py index 6c95ee3..01420a4 100644 --- a/diet_guard/tests/test_cli_gate.py +++ b/diet_guard/tests/test_cli_gate.py @@ -4,7 +4,7 @@ alongside its source module (see _cli_gate.py's module docstring). from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import MagicMock, patch from diet_guard import _cli_gate @@ -214,7 +214,7 @@ class TestGateReadsFreshState: def test_written_slot_clears_the_gate_immediately(self) -> None: """At 09:00 only the 08:00 slot is due; logging it flips the decision.""" - now = datetime(2026, 1, 1, 9, 0, tzinfo=timezone.utc) + now = datetime(2026, 1, 1, 9, 0, tzinfo=UTC) assert gate_is_due(now) is True log_meal("oatmeal", self._nutrition(), slot=8) assert gate_is_due(now) is False diff --git a/diet_guard/tests/test_gate.py b/diet_guard/tests/test_gate.py index f20eed5..a4ba52e 100644 --- a/diet_guard/tests/test_gate.py +++ b/diet_guard/tests/test_gate.py @@ -6,7 +6,7 @@ here the logged set is mocked and ``now`` is injected to drive each decision. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import patch from diet_guard._gate import due_slots, gate_is_due, gate_message @@ -14,7 +14,7 @@ from diet_guard._gate import due_slots, gate_is_due, gate_message def _at(hour: int) -> datetime: """Return a fixed local datetime at ``hour``.""" - return datetime(2026, 1, 1, hour, 0, tzinfo=timezone.utc) + return datetime(2026, 1, 1, hour, 0, tzinfo=UTC) def _logged(slots: set[int]) -> object: diff --git a/diet_guard/tests/test_meal_schedule_store.py b/diet_guard/tests/test_meal_schedule_store.py index 64e0c1b..0f38091 100644 --- a/diet_guard/tests/test_meal_schedule_store.py +++ b/diet_guard/tests/test_meal_schedule_store.py @@ -7,7 +7,7 @@ here. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING from unittest.mock import patch @@ -190,7 +190,7 @@ class TestPersistence: def test_record_defaults_to_now(self) -> None: """Omitting ``when`` stamps the edit with the current time.""" store.record_schedule_change(MealSchedule(7, 19, 3)) - today = datetime.now(tz=timezone.utc).astimezone().date().isoformat() + today = datetime.now(tz=UTC).astimezone().date().isoformat() assert store.schedule_for_day(store.load_entries(), today) == MealSchedule( 7, 19, 3 ) diff --git a/diet_guard/tests/test_slots.py b/diet_guard/tests/test_slots.py index 5a2bcc5..6fda04c 100644 --- a/diet_guard/tests/test_slots.py +++ b/diet_guard/tests/test_slots.py @@ -8,7 +8,7 @@ still pin the behaviour these functions had when those hours were hardcoded. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from diet_guard._meal_schedule import DEFAULT_SCHEDULE, MealSchedule from diet_guard._slots import ( @@ -24,7 +24,7 @@ from diet_guard._slots import ( def _at(hour: int) -> datetime: """Return a fixed local datetime at ``hour`` (date is irrelevant here).""" - return datetime(2026, 1, 1, hour, 0, tzinfo=timezone.utc) + return datetime(2026, 1, 1, hour, 0, tzinfo=UTC) class TestDaySlots: diff --git a/pyproject.toml b/pyproject.toml index 60582fe..88efcbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ mcp = ["mcp==2.1.1"] include = ["diet_guard*"] [tool.ruff] -target-version = "py310" +target-version = "py314" include = ["*.py", "**/*.py"] exclude = [".git", ".venv", "__pycache__", "build", "dist", ".eggs"] @@ -66,7 +66,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 @@ -100,7 +100,7 @@ init-hook = "import sys; sys.path.insert(0, '.')" analyse-fallback-blocks = true persistent = true jobs = 0 -py-version = "3.10" +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 diff --git a/scripts/probe_kuchnia.py b/scripts/probe_kuchnia.py index a5e9822..0bcfc0a 100755 --- a/scripts/probe_kuchnia.py +++ b/scripts/probe_kuchnia.py @@ -30,7 +30,7 @@ Usage: from __future__ import annotations import argparse -from datetime import datetime, timezone +from datetime import UTC, datetime import json from pathlib import Path import sys @@ -192,7 +192,7 @@ def main() -> int: out.write_text( json.dumps( { - "captured_at": datetime.now(tz=timezone.utc).isoformat(), + "captured_at": datetime.now(tz=UTC).isoformat(), "login_base": base, "xsrf_cookie_present": xsrf is not None, "steps": captured, diff --git a/scripts/probe_kuchnia_auth.py b/scripts/probe_kuchnia_auth.py index 2ab06fa..af9cf37 100755 --- a/scripts/probe_kuchnia_auth.py +++ b/scripts/probe_kuchnia_auth.py @@ -64,7 +64,7 @@ def load_session_cookie() -> str | None: return None try: cached = json.loads(SESSION_FILE.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): + except OSError, json.JSONDecodeError: return None value = cached.get(SESSION_COOKIE) if isinstance(cached, dict) else None return value if isinstance(value, str) else None diff --git a/scripts/probe_kuchnia_details.py b/scripts/probe_kuchnia_details.py index 5434cdd..bbdc307 100755 --- a/scripts/probe_kuchnia_details.py +++ b/scripts/probe_kuchnia_details.py @@ -26,7 +26,7 @@ Usage: from __future__ import annotations import argparse -from datetime import datetime, timezone +from datetime import UTC, datetime import json from pathlib import Path import sys @@ -69,7 +69,7 @@ def pick_deliveries(capture: dict[str, Any]) -> list[dict[str, Any]]: """ order = capture["steps"]["order"]["body"] deliveries = sorted(order["deliveries"], key=lambda item: str(item["date"])) - today = datetime.now(tz=timezone.utc).astimezone().date().isoformat() + today = datetime.now(tz=UTC).astimezone().date().isoformat() past = [item for item in deliveries if str(item["date"]) < today] current = [item for item in deliveries if str(item["date"]) == today] future = [item for item in deliveries if str(item["date"]) > today]