mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
ci: mypy ratchet -- keep type-clean modules clean (71 modules, 536 -> 442 errors) (#661)
* ci: mypy ratchet -- keep type-clean modules clean mypy-clean.txt lists the 71 modules under src/ that type-check clean; scripts/check_types.py runs mypy (--follow-imports=silent) on exactly those files and fails on any error or a missing/unsorted/duplicate entry. A new "Type check (mypy ratchet)" CI job runs it with mypy 1.20.2 and pinned stubs; the manual pre-commit mypy hook now runs the same script (a local hook, so mypy sees the installed requirements like CI does). 35 modules were made clean with annotation-only fixes: hints, typing.cast, TYPE_CHECKING imports, implicit-Optional defaults made explicit, and annotations widened (never guards removed) where mypy called a defensive isinstance check unreachable. No runtime behaviour change. mypy.ini: numpy and orjson are treated as Any (follow_imports=skip, also for stubs). numpy 2.3+ stubs use 3.12 `type` statements that mypy won't parse at python_version 3.10, and orjson is optional, so seeing its stubs made the result depend on whether it was installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: annotate check_types.py's list-form mypy subprocess Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,16 @@ import time
|
||||
from datetime import datetime
|
||||
from types import MappingProxyType
|
||||
from src.common.espn_dates import ESPN_MAX_LIMIT
|
||||
from typing import Any, Dict, Mapping, Optional
|
||||
from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, cast
|
||||
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.util.retry import Retry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# What Session() puts in .headers; the stubs only promise a MutableMapping.
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
|
||||
#: The User-Agent core sends to ESPN and other data APIs. It names the client
|
||||
#: and links to it: around 2026-08-04 ESPN began 403ing bare custom tokens
|
||||
@@ -84,7 +88,7 @@ class APIHelper:
|
||||
self.session.headers.update({**DEFAULT_HTTP_HEADERS, 'Connection': 'keep-alive'})
|
||||
|
||||
# Rate limiting
|
||||
self._last_request_time = 0 # wall clock, reported by get_request_stats()
|
||||
self._last_request_time: float = 0 # wall clock, reported by get_request_stats()
|
||||
# The interval is measured on time.monotonic(): a wall-clock step
|
||||
# back (NTP correcting a Pi with no RTC) made time_since_last
|
||||
# negative and the "remaining interval" sleep as long as the step.
|
||||
@@ -112,14 +116,14 @@ class APIHelper:
|
||||
cached = self._get_from_cache(cache_key, cache_ttl)
|
||||
if cached is not None:
|
||||
self.logger.debug(f"Using cached response for {cache_key}")
|
||||
return cached
|
||||
return cast(Dict[Any, Any], cached)
|
||||
|
||||
# Rate limiting
|
||||
self._enforce_rate_limit()
|
||||
|
||||
try:
|
||||
# Prepare request
|
||||
request_headers = self.session.headers.copy()
|
||||
request_headers = cast('CaseInsensitiveDict[Any]', self.session.headers).copy()
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
|
||||
@@ -133,7 +137,7 @@ class APIHelper:
|
||||
response.raise_for_status()
|
||||
|
||||
# Parse JSON response
|
||||
data = response.json()
|
||||
data: Dict[Any, Any] = response.json()
|
||||
|
||||
# Cache response if cache key provided
|
||||
if cache_key and self.cache_manager:
|
||||
@@ -247,7 +251,7 @@ class APIHelper:
|
||||
self._enforce_rate_limit()
|
||||
|
||||
try:
|
||||
request_headers = self.session.headers.copy()
|
||||
request_headers = cast('CaseInsensitiveDict[Any]', self.session.headers).copy()
|
||||
if headers:
|
||||
request_headers.update(headers)
|
||||
|
||||
@@ -260,7 +264,7 @@ class APIHelper:
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
return cast(Optional[Dict[Any, Any]], response.json())
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"POST request failed for {url}: {e}")
|
||||
|
||||
@@ -37,7 +37,7 @@ import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import date, timedelta
|
||||
from functools import partial
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Tuple, cast
|
||||
|
||||
try:
|
||||
from src.common.json_body import response_json
|
||||
@@ -159,7 +159,7 @@ def espn_date_chunks(start: date, end: date) -> List[str]:
|
||||
return chunks
|
||||
|
||||
|
||||
def merge_scoreboard_payloads(payloads: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
def merge_scoreboard_payloads(payloads: List[Any]) -> Dict[str, Any]:
|
||||
"""Fold chunk responses into one scoreboard payload.
|
||||
|
||||
Events are de-duplicated by id and keep first-seen order. Non-event keys
|
||||
@@ -202,7 +202,7 @@ def _fetch_one_chunk(
|
||||
timeout=timeout,
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response_json(response)
|
||||
return cast(Optional[Dict[str, Any]], response_json(response))
|
||||
except Exception as exc: # noqa: BLE001 - see docstring
|
||||
if logger:
|
||||
logger.warning("ESPN chunk %s failed, skipping it: %s", chunk, exc)
|
||||
@@ -379,4 +379,4 @@ def fetch_espn_scoreboard(
|
||||
if data is not None:
|
||||
return data
|
||||
response.raise_for_status()
|
||||
return response_json(response)
|
||||
return cast(Dict[str, Any], response_json(response))
|
||||
|
||||
@@ -93,7 +93,7 @@ import tempfile
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -483,7 +483,13 @@ class FrameTimingRecorder:
|
||||
raise
|
||||
|
||||
|
||||
def watchdog_settings() -> Dict[str, float]:
|
||||
class _WatchdogSettings(TypedDict, total=False):
|
||||
"""The StallWatchdog keyword arguments watchdog_settings() may set."""
|
||||
threshold: float
|
||||
poll: float
|
||||
|
||||
|
||||
def watchdog_settings() -> _WatchdogSettings:
|
||||
"""StallWatchdog arguments from ``LEDMATRIX_STALL_WATCHDOG_MS``, if set.
|
||||
|
||||
The poll comes down with the threshold, or a stall shorter than one poll
|
||||
|
||||
@@ -154,7 +154,7 @@ class LogoHelper:
|
||||
return None
|
||||
|
||||
# Load image
|
||||
logo = Image.open(logo_path)
|
||||
logo: Image.Image = Image.open(logo_path)
|
||||
if logo.mode != 'RGBA':
|
||||
logo = logo.convert('RGBA')
|
||||
|
||||
@@ -359,7 +359,7 @@ class LogoHelper:
|
||||
self._download_failures.clear()
|
||||
self.logger.debug("Logo cache cleared")
|
||||
|
||||
def get_cache_stats(self) -> Dict[str, int]:
|
||||
def get_cache_stats(self) -> Dict[str, float]:
|
||||
"""
|
||||
Get cache statistics.
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from collections import deque
|
||||
from typing import Any, Callable, Deque, List, Optional
|
||||
from typing import Any, Callable, Deque, List, Optional, cast
|
||||
|
||||
from src.common.frame_timing import binding_releases_gil
|
||||
|
||||
@@ -103,8 +103,8 @@ def _held(lock: Any) -> bool:
|
||||
"""Is ``lock`` held? RLocks report this thread's ownership; plain locks, anyone's."""
|
||||
is_owned = getattr(lock, "_is_owned", None)
|
||||
if is_owned is not None:
|
||||
return is_owned()
|
||||
return lock.locked()
|
||||
return cast(bool, is_owned())
|
||||
return cast(bool, lock.locked())
|
||||
|
||||
|
||||
class RenderGate:
|
||||
|
||||
@@ -49,7 +49,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional, cast
|
||||
|
||||
from src.matrix_support import DEFAULT_REFRESH_LIMIT_HZ
|
||||
|
||||
@@ -132,14 +132,14 @@ def crisp_ladder(
|
||||
refresh_hz: float = DEFAULT_REFRESH_HZ,
|
||||
max_frame_hold: int = MAX_FRAME_HOLD,
|
||||
max_pixels_per_frame: int = MAX_PIXELS_PER_FRAME,
|
||||
):
|
||||
) -> List[CrispSpeed]:
|
||||
"""Every whole-pixel speed this panel can show, slowest first.
|
||||
|
||||
Duplicates are collapsed keeping the gentlest option: 100 px/s is reachable
|
||||
as 1px every refresh or 2px every 2nd refresh, and the former moves in
|
||||
smaller increments, so that is the one worth offering.
|
||||
"""
|
||||
best = {}
|
||||
best: Dict[float, CrispSpeed] = {}
|
||||
for hold in range(1, max_frame_hold + 1):
|
||||
for ppf in range(1, max_pixels_per_frame + 1):
|
||||
pps = refresh_hz / hold * ppf
|
||||
@@ -434,7 +434,8 @@ def configure(
|
||||
# they start scrolling. configure() only reports what is needed.
|
||||
|
||||
if choice:
|
||||
requested = settings.requested_pixels_per_second
|
||||
# Set whenever there is a crisp choice (see the replace() above).
|
||||
requested = cast(float, settings.requested_pixels_per_second)
|
||||
if abs(requested - applied) > 0.05:
|
||||
log.info(
|
||||
"Scroll configured: %s (asked for %.1f px/s from %s; "
|
||||
|
||||
@@ -144,7 +144,8 @@ def resolve_font_color(config: Optional[Dict[str, Any]],
|
||||
if len(matches) > 1:
|
||||
configured = []
|
||||
for element in matches:
|
||||
colour = element_color(config, element, None, mode)
|
||||
# None as the default makes it come back when unconfigured.
|
||||
colour = element_color(config, element, None, mode) # type: ignore[arg-type]
|
||||
if colour is not None and colour not in configured:
|
||||
configured.append(colour)
|
||||
if len(configured) == 1:
|
||||
|
||||
Reference in New Issue
Block a user