fix(lint): improved type checking and code maintainability

This commit is contained in:
matt 2025-10-31 10:11:00 -07:00
parent 83fe527979
commit 40023e93b8
62 changed files with 187 additions and 197 deletions

View file

@ -11,9 +11,9 @@ def _load_applier():
root = Path(__file__).resolve().parents[2]
mod_path = root / 'code' / 'tagging' / 'bracket_policy_applier.py'
spec = importlib.util.spec_from_file_location('bracket_policy_applier', str(mod_path))
mod = importlib.util.module_from_spec(spec) # type: ignore[arg-type]
mod = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(mod) # type: ignore[assignment]
spec.loader.exec_module(mod)
return mod

View file

@ -30,8 +30,8 @@ def test_card_index_color_identity_list_handles_edge_cases(tmp_path, monkeypatch
csv_path = write_csv(tmp_path)
monkeypatch.setenv("CARD_INDEX_EXTRA_CSV", str(csv_path))
# Force rebuild
card_index._CARD_INDEX.clear() # type: ignore
card_index._CARD_INDEX_MTIME = None # type: ignore
card_index._CARD_INDEX.clear()
card_index._CARD_INDEX_MTIME = None
card_index.maybe_build_index()
pool = card_index.get_tag_pool("Blink")

View file

@ -8,7 +8,7 @@ from urllib.parse import parse_qs, urlparse
import pytest
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
from code.web.services.commander_catalog_loader import clear_commander_catalog_cache

View file

@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
from code.web.services import telemetry
from code.web.services.commander_catalog_loader import clear_commander_catalog_cache

View file

@ -7,7 +7,7 @@ from types import SimpleNamespace
import pytest
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
from code.web.routes import commanders
from code.web.services import commander_catalog_loader
from code.web.services.commander_catalog_loader import clear_commander_catalog_cache, load_commander_catalog

View file

@ -24,7 +24,7 @@ def load_app_with_env(**env: str) -> types.ModuleType:
os.environ.pop(key, None)
for k, v in env.items():
os.environ[k] = v
import code.web.app as app_module # type: ignore
import code.web.app as app_module
importlib.reload(app_module)
return app_module

View file

@ -50,7 +50,7 @@ def _load_catalog() -> Dict[str, Any]:
def test_deterministic_build_under_seed():
# Import build after setting seed env
os.environ['EDITORIAL_SEED'] = '999'
from scripts.build_theme_catalog import build_catalog # type: ignore
from scripts.build_theme_catalog import build_catalog
first = build_catalog(limit=0, verbose=False)
second = build_catalog(limit=0, verbose=False)
# Drop volatile metadata_info/timestamp fields before comparison
@ -106,7 +106,7 @@ def test_metadata_info_block_coverage():
def test_synergy_commanders_exclusion_of_examples():
import yaml # type: ignore
import yaml
pattern = re.compile(r" - Synergy \(.*\)$")
violations: List[str] = []
for p in CATALOG_DIR.glob('*.yml'):
@ -128,7 +128,7 @@ def test_synergy_commanders_exclusion_of_examples():
def test_mapping_trigger_specialization_guard():
import yaml # type: ignore
import yaml
assert MAPPING.exists(), "description_mapping.yml missing"
mapping_yaml = yaml.safe_load(MAPPING.read_text(encoding='utf-8')) or []
triggers: Set[str] = set()

View file

@ -20,7 +20,7 @@ def load_app_with_env(**env: str) -> types.ModuleType:
os.environ.pop(key, None)
for k, v in env.items():
os.environ[k] = v
import code.web.app as app_module # type: ignore
import code.web.app as app_module
importlib.reload(app_module)
return app_module

View file

@ -14,7 +14,7 @@ class DummyBuilder(ReportingMixin):
self.card_library = card_library
self.color_identity = colors
self.output_lines: List[str] = []
self.output_func = self.output_lines.append # type: ignore[assignment]
self.output_func = self.output_lines.append
self._full_cards_df = None
self._combined_cards_df = None
self.include_exclude_diagnostics = None

View file

@ -20,7 +20,7 @@ def _stub_modal_matrix(builder: DeckBuilder) -> None:
"Forest": {"G": 1},
}
builder._compute_color_source_matrix = MethodType(fake_matrix, builder) # type: ignore[attr-defined]
builder._compute_color_source_matrix = MethodType(fake_matrix, builder)
def test_modal_dfc_swaps_basic_when_enabled():

View file

@ -18,7 +18,7 @@ def test_multicopy_clamp_trims_current_stage_additions_only():
# Preseed 95 cards in the library
b.card_library = {"Filler": {"Count": 95, "Role": "Test", "SubRole": "", "AddedBy": "Test"}}
# Set a multi-copy selection that would exceed 100 by 15
b._web_multi_copy = { # type: ignore[attr-defined]
b._web_multi_copy = {
"id": "persistent_petitioners",
"name": "Persistent Petitioners",
"count": 20,

View file

@ -23,7 +23,7 @@ def test_petitioners_clamp_to_100_and_reduce_creature_slots():
"card_advantage": 8, "protection": 4,
}
# Thread multi-copy selection for Petitioners as a creature archetype
b._web_multi_copy = { # type: ignore[attr-defined]
b._web_multi_copy = {
"id": "persistent_petitioners",
"name": "Persistent Petitioners",
"count": 40, # intentionally large to trigger clamp/adjustments

View file

@ -17,7 +17,7 @@ def _minimal_ctx(selection: dict):
b = DeckBuilder(output_func=out, input_func=lambda *_: "", headless=True)
# Thread selection and ensure empty library
b._web_multi_copy = selection # type: ignore[attr-defined]
b._web_multi_copy = selection
b.card_library = {}
ctx = {

View file

@ -1,7 +1,7 @@
import importlib
import pytest
try:
from starlette.testclient import TestClient # type: ignore
from starlette.testclient import TestClient
except Exception: # pragma: no cover - optional dep in CI
TestClient = None # type: ignore

View file

@ -128,7 +128,7 @@ def _make_request(path: str = "/api/partner/suggestions", query_string: str = ""
"client": ("203.0.113.5", 52345),
"server": ("testserver", 80),
}
request = Request(scope, receive=_receive) # type: ignore[arg-type]
request = Request(scope, receive=_receive)
request.state.request_id = "req-telemetry"
return request
@ -197,21 +197,21 @@ def test_load_dataset_refresh_retries_after_prior_failure(tmp_path: Path, monkey
from code.web.services import orchestrator as orchestrator_service
original_default = partner_service.DEFAULT_DATASET_PATH
original_path = partner_service._DATASET_PATH # type: ignore[attr-defined]
original_cache = partner_service._DATASET_CACHE # type: ignore[attr-defined]
original_attempted = partner_service._DATASET_REFRESH_ATTEMPTED # type: ignore[attr-defined]
original_path = partner_service._DATASET_PATH
original_cache = partner_service._DATASET_CACHE
original_attempted = partner_service._DATASET_REFRESH_ATTEMPTED
partner_service.DEFAULT_DATASET_PATH = dataset_path
partner_service._DATASET_PATH = dataset_path # type: ignore[attr-defined]
partner_service._DATASET_CACHE = None # type: ignore[attr-defined]
partner_service._DATASET_REFRESH_ATTEMPTED = True # type: ignore[attr-defined]
partner_service._DATASET_PATH = dataset_path
partner_service._DATASET_CACHE = None
partner_service._DATASET_REFRESH_ATTEMPTED = True
calls = {"count": 0}
payload_path = tmp_path / "seed_dataset.json"
_write_dataset(payload_path)
def seeded_refresh(out_func=None, *, force=False, root=None): # type: ignore[override]
def seeded_refresh(out_func=None, *, force=False, root=None):
calls["count"] += 1
dataset_path.write_text(payload_path.read_text(encoding="utf-8"), encoding="utf-8")
@ -227,9 +227,9 @@ def test_load_dataset_refresh_retries_after_prior_failure(tmp_path: Path, monkey
assert calls["count"] == 1
finally:
partner_service.DEFAULT_DATASET_PATH = original_default
partner_service._DATASET_PATH = original_path # type: ignore[attr-defined]
partner_service._DATASET_CACHE = original_cache # type: ignore[attr-defined]
partner_service._DATASET_REFRESH_ATTEMPTED = original_attempted # type: ignore[attr-defined]
partner_service._DATASET_PATH = original_path
partner_service._DATASET_CACHE = original_cache
partner_service._DATASET_REFRESH_ATTEMPTED = original_attempted
try:
dataset_path.unlink()
except FileNotFoundError:

View file

@ -33,7 +33,7 @@ def _invoke_helper(
) -> list[tuple[list[str], str]]:
calls: list[tuple[list[str], str]] = []
def _fake_run(cmd, check=False, cwd=None): # type: ignore[no-untyped-def]
def _fake_run(cmd, check=False, cwd=None):
calls.append((list(cmd), cwd))
class _Completed:
returncode = 0

View file

@ -10,7 +10,7 @@ fastapi = pytest.importorskip("fastapi")
def load_app_with_env(**env: str) -> types.ModuleType:
for k,v in env.items():
os.environ[k] = v
import code.web.app as app_module # type: ignore
import code.web.app as app_module
importlib.reload(app_module)
return app_module

View file

@ -1,7 +1,7 @@
import json
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
def test_preview_includes_curated_examples_regression():

View file

@ -1,8 +1,8 @@
import os
from code.web.services.theme_preview import get_theme_preview, bust_preview_cache # type: ignore
from code.web.services import preview_cache as pc # type: ignore
from code.web.services.preview_metrics import preview_metrics # type: ignore
from code.web.services.theme_preview import get_theme_preview, bust_preview_cache
from code.web.services import preview_cache as pc
from code.web.services.preview_metrics import preview_metrics
def _prime(slug: str, limit: int = 12, hits: int = 0, *, colors=None):
@ -89,7 +89,7 @@ def test_env_weight_override(monkeypatch):
bust_preview_cache()
# Clear module-level caches for weights
if hasattr(pc, '_EVICT_WEIGHTS_CACHE'):
pc._EVICT_WEIGHTS_CACHE = None # type: ignore
pc._EVICT_WEIGHTS_CACHE = None
# Create two entries: one older with many hits, one fresh with none.
_prime('Blink', limit=6, hits=6, colors=None) # older hot entry
old_key = next(iter(pc.PREVIEW_CACHE.keys()))

View file

@ -1,6 +1,6 @@
import os
from code.web.services.theme_preview import get_theme_preview, bust_preview_cache # type: ignore
from code.web.services import preview_cache as pc # type: ignore
from code.web.services.theme_preview import get_theme_preview, bust_preview_cache
from code.web.services import preview_cache as pc
def test_basic_low_score_eviction(monkeypatch):
@ -17,7 +17,7 @@ def test_basic_low_score_eviction(monkeypatch):
get_theme_preview('Blink', limit=6, colors=c)
# Cache limit 5, inserted 6 distinct -> eviction should have occurred
assert len(pc.PREVIEW_CACHE) <= 5
from code.web.services.preview_metrics import preview_metrics # type: ignore
from code.web.services.preview_metrics import preview_metrics
m = preview_metrics()
assert m['preview_cache_evictions'] >= 1, 'Expected at least one eviction'
assert m['preview_cache_evictions_by_reason'].get('low_score', 0) >= 1

View file

@ -1,5 +1,5 @@
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
def test_minimal_variant_hides_controls_and_headers():

View file

@ -8,7 +8,7 @@ pytestmark = pytest.mark.skip(reason="M4: preview_perf_benchmark module removed
def test_fetch_all_theme_slugs_retries(monkeypatch):
calls = {"count": 0}
def fake_fetch(url): # type: ignore[override]
def fake_fetch(url):
calls["count"] += 1
if calls["count"] == 1:
raise RuntimeError("transient 500")
@ -27,7 +27,7 @@ def test_fetch_all_theme_slugs_retries(monkeypatch):
def test_fetch_all_theme_slugs_page_level_retry(monkeypatch):
calls = {"count": 0}
def fake_fetch_with_retry(url, attempts=3, delay=0.6): # type: ignore[override]
def fake_fetch_with_retry(url, attempts=3, delay=0.6):
calls["count"] += 1
if calls["count"] < 3:
raise RuntimeError("service warming up")

View file

@ -1,5 +1,5 @@
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
def test_preview_fragment_suppress_curated_removes_examples():

View file

@ -3,16 +3,16 @@ from code.web.services import preview_cache as pc
def _force_interval_elapsed():
# Ensure adaptation interval guard passes
if pc._LAST_ADAPT_AT is not None: # type: ignore[attr-defined]
pc._LAST_ADAPT_AT -= (pc._ADAPT_INTERVAL_S + 1) # type: ignore[attr-defined]
if pc._LAST_ADAPT_AT is not None:
pc._LAST_ADAPT_AT -= (pc._ADAPT_INTERVAL_S + 1)
def test_ttl_adapts_down_and_up(capsys):
# Enable adaptation regardless of env
pc._ADAPTATION_ENABLED = True # type: ignore[attr-defined]
pc.TTL_SECONDS = pc._TTL_BASE # type: ignore[attr-defined]
pc._RECENT_HITS.clear() # type: ignore[attr-defined]
pc._LAST_ADAPT_AT = None # type: ignore[attr-defined]
pc._ADAPTATION_ENABLED = True
pc.TTL_SECONDS = pc._TTL_BASE
pc._RECENT_HITS.clear()
pc._LAST_ADAPT_AT = None
# Low hit ratio pattern (~0.1)
for _ in range(72):
@ -23,11 +23,11 @@ def test_ttl_adapts_down_and_up(capsys):
out1 = capsys.readouterr().out
assert "theme_preview_ttl_adapt" in out1, "expected adaptation log for low hit ratio"
ttl_after_down = pc.TTL_SECONDS
assert ttl_after_down <= pc._TTL_BASE # type: ignore[attr-defined]
assert ttl_after_down <= pc._TTL_BASE
# Force interval elapsed & high hit ratio pattern (~0.9)
_force_interval_elapsed()
pc._RECENT_HITS.clear() # type: ignore[attr-defined]
pc._RECENT_HITS.clear()
for _ in range(72):
pc.record_request_hit(True)
for _ in range(8):

View file

@ -19,17 +19,17 @@ def _client_with_flags(window_s: int = 2, limit_random: int = 2, limit_build: in
# Force fresh import so RATE_LIMIT_* constants reflect env
sys.modules.pop('code.web.app', None)
from code.web import app as app_module # type: ignore
from code.web import app as app_module
# Force override constants for deterministic test
try:
app_module.RATE_LIMIT_ENABLED = True # type: ignore[attr-defined]
app_module.RATE_LIMIT_WINDOW_S = window_s # type: ignore[attr-defined]
app_module.RATE_LIMIT_RANDOM = limit_random # type: ignore[attr-defined]
app_module.RATE_LIMIT_BUILD = limit_build # type: ignore[attr-defined]
app_module.RATE_LIMIT_SUGGEST = limit_suggest # type: ignore[attr-defined]
app_module.RATE_LIMIT_ENABLED = True
app_module.RATE_LIMIT_WINDOW_S = window_s
app_module.RATE_LIMIT_RANDOM = limit_random
app_module.RATE_LIMIT_BUILD = limit_build
app_module.RATE_LIMIT_SUGGEST = limit_suggest
# Reset in-memory counters
if hasattr(app_module, '_RL_COUNTS'):
app_module._RL_COUNTS.clear() # type: ignore[attr-defined]
app_module._RL_COUNTS.clear()
except Exception:
pass
return TestClient(app_module.app)

View file

@ -3,8 +3,8 @@ from pathlib import Path
from fastapi.testclient import TestClient
from code.web import app as web_app # type: ignore
from code.web.app import app # type: ignore
from code.web import app as web_app
from code.web.app import app
# Ensure project root on sys.path for absolute imports
ROOT = Path(__file__).resolve().parents[2]

View file

@ -9,17 +9,17 @@ def setup_module(module): # ensure deterministic env weights
def test_rarity_diminishing():
# Monkeypatch internal index
card_index._CARD_INDEX.clear() # type: ignore
card_index._CARD_INDEX.clear()
theme = "Test Theme"
card_index._CARD_INDEX[theme] = [ # type: ignore
card_index._CARD_INDEX[theme] = [
{"name": "Mythic One", "tags": [theme], "color_identity": "G", "mana_cost": "G", "rarity": "mythic"},
{"name": "Mythic Two", "tags": [theme], "color_identity": "G", "mana_cost": "G", "rarity": "mythic"},
]
def no_build():
return None
sampling.maybe_build_index = no_build # type: ignore
sampling.maybe_build_index = no_build
cards = sampling.sample_real_cards_for_theme(theme, 2, None, synergies=[theme], commander=None)
rarity_weights = [r for c in cards for r in c["reasons"] if r.startswith("rarity_weight_calibrated")] # type: ignore
rarity_weights = [r for c in cards for r in c["reasons"] if r.startswith("rarity_weight_calibrated")]
assert len(rarity_weights) >= 2
v1 = float(rarity_weights[0].split(":")[-1])
v2 = float(rarity_weights[1].split(":")[-1])
@ -40,15 +40,15 @@ def test_commander_overlap_monotonic_diminishing():
def test_splash_off_color_penalty_applied():
card_index._CARD_INDEX.clear() # type: ignore
card_index._CARD_INDEX.clear()
theme = "Splash Theme"
# Commander W U B R (4 colors)
commander = {"name": "CommanderTest", "tags": [theme], "color_identity": "WUBR", "mana_cost": "", "rarity": "mythic"}
# Card with single off-color G (W U B R G)
splash_card = {"name": "CardSplash", "tags": [theme], "color_identity": "WUBRG", "mana_cost": "G", "rarity": "rare"}
card_index._CARD_INDEX[theme] = [commander, splash_card] # type: ignore
sampling.maybe_build_index = lambda: None # type: ignore
card_index._CARD_INDEX[theme] = [commander, splash_card]
sampling.maybe_build_index = lambda: None
cards = sampling.sample_real_cards_for_theme(theme, 2, None, synergies=[theme], commander="CommanderTest")
splash = next((c for c in cards if c["name"] == "CardSplash"), None)
assert splash is not None
assert any(r.startswith("splash_off_color_penalty") for r in splash["reasons"]) # type: ignore
assert any(r.startswith("splash_off_color_penalty") for r in splash["reasons"])

View file

@ -1,5 +1,5 @@
import re
from code.web.services.theme_preview import get_theme_preview # type: ignore
from code.web.services.theme_preview import get_theme_preview
# We can't easily execute the JS normalizeCardName in Python, but we can ensure
# server-delivered sample names that include appended synergy annotations are not

View file

@ -10,7 +10,7 @@ fastapi = pytest.importorskip("fastapi") # skip if FastAPI missing
def load_app_with_env(**env: str) -> types.ModuleType:
for k, v in env.items():
os.environ[k] = v
import code.web.app as app_module # type: ignore
import code.web.app as app_module
importlib.reload(app_module)
return app_module

View file

@ -2,7 +2,7 @@ import sys
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from code.web.app import app # type: ignore
from code.web.app import app
# Ensure project root on sys.path for absolute imports
ROOT = Path(__file__).resolve().parents[2]

View file

@ -146,7 +146,7 @@ def test_generate_theme_catalog_basic(tmp_path: Path, fixed_now: datetime) -> No
assert all(row['last_generated_at'] == result.generated_at for row in rows)
assert all(row['version'] == result.version for row in rows)
expected_hash = new_catalog._compute_version_hash([row['theme'] for row in rows]) # type: ignore[attr-defined]
expected_hash = new_catalog._compute_version_hash([row['theme'] for row in rows])
assert result.version == expected_hash

View file

@ -4,7 +4,7 @@ import os
import importlib
from pathlib import Path
from starlette.testclient import TestClient
from code.type_definitions_theme_catalog import ThemeCatalog # type: ignore
from code.type_definitions_theme_catalog import ThemeCatalog
CATALOG_PATH = Path('config/themes/theme_list.json')

View file

@ -8,7 +8,7 @@ def test_theme_list_json_validates_against_pydantic_and_fast_path():
raw = json.loads(p.read_text(encoding='utf-8'))
# Pydantic validation
from code.type_definitions_theme_catalog import ThemeCatalog # type: ignore
from code.type_definitions_theme_catalog import ThemeCatalog
catalog = ThemeCatalog(**raw)
assert isinstance(catalog.themes, list) and len(catalog.themes) > 0
# Basic fields exist on entries

View file

@ -36,7 +36,7 @@ from fastapi.testclient import TestClient
def _get_app(): # local import to avoid heavy import cost if file unused
from code.web.app import app # type: ignore
from code.web.app import app
return app
@ -115,13 +115,13 @@ def test_preview_cache_hit_timing(monkeypatch, client):
r1 = client.get(f"/themes/fragment/preview/{theme_id}?limit=12")
assert r1.status_code == 200
# Monkeypatch theme_preview._now to freeze time so second call counts as hit
import code.web.services.theme_preview as tp # type: ignore
import code.web.services.theme_preview as tp
orig_now = tp._now
monkeypatch.setattr(tp, "_now", lambda: orig_now())
r2 = client.get(f"/themes/fragment/preview/{theme_id}?limit=12")
assert r2.status_code == 200
# Deterministic service-level verification: second direct function call should short-circuit via cache
import code.web.services.theme_preview as tp # type: ignore
import code.web.services.theme_preview as tp
# Snapshot counters
pre_hits = getattr(tp, "_PREVIEW_CACHE_HITS", 0)
first_payload = tp.get_theme_preview(theme_id, limit=12)

View file

@ -16,7 +16,7 @@ def _new_client(prewarm: bool = False) -> TestClient:
# Remove existing module (if any) so lifespan runs again
if 'code.web.app' in list(importlib.sys.modules.keys()):
importlib.sys.modules.pop('code.web.app')
from code.web.app import app # type: ignore
from code.web.app import app
return TestClient(app)

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import pytest
from code.web.services.theme_preview import get_theme_preview # type: ignore
from code.web.services.theme_catalog_loader import load_index, slugify, project_detail # type: ignore
from code.web.services.theme_preview import get_theme_preview
from code.web.services.theme_catalog_loader import load_index, slugify, project_detail
@pytest.mark.parametrize("limit", [8, 12])

View file

@ -1,7 +1,7 @@
import os
import time
import json
from code.web.services.theme_preview import get_theme_preview, preview_metrics, bust_preview_cache # type: ignore
from code.web.services.theme_preview import get_theme_preview, preview_metrics, bust_preview_cache
def test_colors_filter_constraint_green_subset():

View file

@ -47,10 +47,10 @@ class DummySpellBuilder(SpellAdditionMixin):
def rng(self) -> DummyRNG:
return self._rng
def get_theme_context(self) -> ThemeContext: # type: ignore[override]
def get_theme_context(self) -> ThemeContext:
return self._theme_context
def add_card(self, name: str, **kwargs: Any) -> None: # type: ignore[override]
def add_card(self, name: str, **kwargs: Any) -> None:
self.card_library[name] = {"Count": kwargs.get("count", 1)}
self.added_cards.append(name)

View file

@ -20,7 +20,7 @@ def _fresh_client() -> TestClient:
from code.web.services.commander_catalog_loader import clear_commander_catalog_cache
clear_commander_catalog_cache()
from code.web.app import app # type: ignore
from code.web.app import app
client = TestClient(app)
from code.web.services import tasks