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:
Chuck
2026-09-28 15:01:35 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent e6e0a16140
commit b8c01c69fb
42 changed files with 372 additions and 104 deletions
+6 -2
View File
@@ -191,7 +191,7 @@ def satisfies_compatible_versions(
return any(parsed)
def declared_min_version(manifest: Dict[str, Any]) -> Optional[str]:
def declared_min_version(manifest: Dict[str, Any]) -> Any:
"""The core version this plugin says it needs, or ``None`` if it doesn't say.
Checked in order of specificity. `ledmatrix_min` is the deprecated spelling
@@ -206,6 +206,10 @@ def declared_min_version(manifest: Dict[str, Any]) -> Optional[str]:
untrustworthy-core branch of :func:`check` calls this for *every* manifest,
so one malformed file would take down the install path rather than just
itself. A shape we do not recognise means "no declared floor".
The value is returned as the manifest holds it -- normally a version
string, but nothing here checks that; callers hand it to
:func:`parse_semver`, which accepts anything.
"""
declared = manifest.get('min_ledmatrix_version')
if not declared:
@@ -222,7 +226,7 @@ def declared_min_version(manifest: Dict[str, Any]) -> Optional[str]:
return None
def is_update_available(installed_version: str, latest_version: str) -> bool:
def is_update_available(installed_version: Any, latest_version: Any) -> bool:
"""Return True when the registry's ``latest_version`` is strictly newer
than the installed version.
+6 -5
View File
@@ -45,7 +45,7 @@ from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Set, Union
from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast
from src.common.path_safety import safe_path_component
@@ -91,7 +91,7 @@ class PluginDirEntry:
"""One candidate directory and its manifest, read once."""
path: Path
status: str
manifest: Optional[Any] = None
manifest: Any = None
error: Optional[BaseException] = None
@property
@@ -103,7 +103,8 @@ class PluginDirEntry:
"""The manifest's ``id`` when the manifest is usable, else None."""
if self.status != ManifestStatus.OK:
return None
return self.manifest['id']
# OK means a dict whose "id" is a non-empty string (_read_entry).
return cast(str, self.manifest['id'])
@property
def manifest_parses(self) -> bool:
@@ -240,7 +241,7 @@ class PluginDirectoryIndex:
# -- lookup -----------------------------------------------------------
def find(self, plugin_id: str, *, prefix: bool, case_insensitive: bool,
def find(self, plugin_id: Any, *, prefix: bool, case_insensitive: bool,
by_manifest: bool = True) -> Optional[Path]:
"""Resolve ``plugin_id`` within this directory (rules in the module doc)."""
plugin_id = _lookup_id(plugin_id)
@@ -270,7 +271,7 @@ def _lookup_id(plugin_id: Any) -> Optional[str]:
plugin_id = safe_path_component(plugin_id)
if plugin_id is None or is_ignored_dir_name(plugin_id):
return None
return plugin_id
return cast(str, plugin_id) # safe_path_component returned a str
def _candidate_names(plugin_id: str, prefix: bool) -> List[str]:
+2 -2
View File
@@ -6,7 +6,7 @@ error isolation, and performance monitoring.
"""
import time
from typing import Any, Optional, Callable
from typing import Any, Dict, Optional, Callable
from threading import Thread
import logging
@@ -62,7 +62,7 @@ class PluginExecutor:
plugin_context = f"plugin {plugin_id}" if plugin_id else "plugin"
# Use threading-based timeout (more reliable than signal-based)
result_container = {'value': None, 'exception': None, 'completed': False}
result_container: Dict[str, Any] = {'value': None, 'exception': None, 'completed': False}
def target():
try:
+1
View File
@@ -277,6 +277,7 @@ class PluginLoader:
Path to plugin directory or None if not found. An id that is not
one plain path segment finds nothing.
"""
plugin_dir: Optional[Path]
# Strategy 1: Use mapping from discovery
if plugin_directories and plugin_id in plugin_directories:
plugin_dir = plugin_directories[plugin_id]
+4 -4
View File
@@ -8,7 +8,7 @@ Provides resource limits and performance monitoring.
import math
import time
import threading
from typing import Dict, Optional, Any, Callable
from typing import Dict, Optional, Any, Callable, cast
from dataclasses import dataclass, field, fields
from src.logging_config import get_logger
@@ -209,7 +209,7 @@ class PluginResourceMonitor:
# (not \"int\") to str"). Coerce here, where there is still a cache
# key to name in the warning.
declared = {f.name: f.type for f in fields(ResourceMetrics)}
usable = {}
usable: Dict[str, Any] = {}
for key, value in cached.items():
if key not in known:
continue
@@ -299,7 +299,7 @@ class PluginResourceMonitor:
if not self.enable_monitoring or self._process is None:
return 0.0
try:
return self._process.memory_info().rss / 1024 / 1024
return cast(float, self._process.memory_info().rss / 1024 / 1024)
except Exception:
return 0.0
@@ -313,7 +313,7 @@ class PluginResourceMonitor:
if not self.enable_monitoring or self._process is None:
return 0.0
try:
return self._process.cpu_percent(interval=None)
return cast(float, self._process.cpu_percent(interval=None))
except Exception:
return 0.0
+2 -2
View File
@@ -7,7 +7,7 @@ Manages saved GitHub repository URLs for easy plugin discovery and installation.
import json
import os
from pathlib import Path
from typing import List, Dict, Optional
from typing import List, Dict, Optional, cast
from src.logging_config import get_logger
from src.plugin_system.repo_urls import normalize_repo_url
@@ -37,7 +37,7 @@ class SavedRepositoriesManager:
if isinstance(data, list):
return data
elif isinstance(data, dict) and 'repositories' in data:
return data['repositories']
return cast(List[Dict[str, str]], data['repositories'])
else:
return []
return []
+5 -4
View File
@@ -9,7 +9,7 @@ Detects and fixes inconsistencies between:
"""
import json
from typing import Dict, Any, List, Set
from typing import Dict, Any, List, Set, cast
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
@@ -237,7 +237,7 @@ class StateReconciliation:
state_manager_state = self._get_state_manager_state()
# Find all unique plugin IDs
all_plugin_ids = set()
all_plugin_ids: Set[str] = set()
all_plugin_ids.update(config_state.keys())
all_plugin_ids.update(disk_state.keys())
all_plugin_ids.update(manager_state.keys())
@@ -380,7 +380,7 @@ class StateReconciliation:
state_manager_state: Dict[str, Dict[str, Any]]
) -> List[Inconsistency]:
"""Check consistency for a single plugin."""
inconsistencies = []
inconsistencies: List[Inconsistency] = []
if plugin_id in CORE_CONFIG_KEYS:
# A plugin whose id is a core setting's key ('display', 'sync',
@@ -496,7 +496,8 @@ class StateReconciliation:
# Bring the state manager in sync with config rather than the reverse,
# so that manual config edits (or the state left behind after an
# uninstall+reinstall cycle) don't silently override the user's intent.
config_enabled = inconsistency.expected_state.get('enabled')
# Always set for this type (see _check_plugin_consistency).
config_enabled = cast(bool, inconsistency.expected_state.get('enabled'))
success = self.state_manager.set_plugin_enabled(inconsistency.plugin_id, config_enabled)
if success:
self.logger.info(
+4 -4
View File
@@ -7,7 +7,7 @@ plugin discovery / manifest / config-default logic lives in exactly one place.
import json
from pathlib import Path
from typing import Any, Dict, Optional, Sequence, Union
from typing import Any, Dict, Optional, Sequence, Union, cast
def find_plugin_dir(plugin_id: str, search_dirs: Sequence[Union[str, Path]]) -> Optional[Path]:
@@ -39,7 +39,7 @@ def load_manifest(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
if not manifest_path.exists():
raise FileNotFoundError(f"No manifest.json in {plugin_dir}")
with open(manifest_path, 'r', encoding='utf-8') as f:
return json.load(f)
return cast(Dict[str, Any], json.load(f))
def merge_config(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
@@ -64,7 +64,7 @@ def load_schema(plugin_dir: Union[str, Path]) -> Optional[Dict[str, Any]]:
if not schema_path.exists():
return None
with open(schema_path, 'r', encoding='utf-8') as f:
return json.load(f)
return cast(Optional[Dict[str, Any]], json.load(f))
def load_config_defaults(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
@@ -124,7 +124,7 @@ def load_harness_spec(plugin_dir: Union[str, Path]) -> Dict[str, Any]:
if not spec_path.exists():
return {}
with open(spec_path, 'r', encoding='utf-8') as f:
spec = json.load(f)
spec: Dict[str, Any] = json.load(f)
# Resolve mock_data path and inline its contents for convenience.
mock_rel = spec.get('mock_data')
+5 -5
View File
@@ -6,7 +6,7 @@ and plugin_manager for use in plugin unit tests.
"""
import warnings
from typing import Dict, Any, Optional
from typing import Dict, Any, List, Optional
from PIL import Image
#: Why draw_image() warns. Kept (rather than removed) so existing plugin test
@@ -32,7 +32,7 @@ class MockDisplayManager:
self.image = Image.new('RGB', (width, height), color=(0, 0, 0))
self.clear_called = False
self.update_called = False
self.draw_calls = []
self.draw_calls: List[Dict[str, Any]] = []
def clear(self):
"""Clear the display."""
@@ -43,7 +43,7 @@ class MockDisplayManager:
"""Update the display."""
self.update_called = True
def draw_text(self, text: str, x: int = None, y: int = None, color: tuple = (255, 255, 255),
def draw_text(self, text: str, x: Optional[int] = None, y: Optional[int] = None, color: tuple = (255, 255, 255),
font=None, small_font: bool = False, centered: bool = False):
"""Draw text on the display.
@@ -166,8 +166,8 @@ class MockConfigManager:
def __init__(self, config: Optional[Dict[str, Any]] = None):
self._config = config or {}
self.load_config_calls = []
self.save_config_calls = []
self.load_config_calls: List[Dict[str, Any]] = []
self.save_config_calls: List[Dict[str, Any]] = []
def load_config(self) -> Dict[str, Any]:
"""Load configuration."""