mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25: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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user