From af6d0e24c004c5a3abdcf04c59e8cf7e00ce228a Mon Sep 17 00:00:00 2001 From: Nikolay Miroshnichenko Date: Sat, 12 Sep 2026 21:38:54 +0200 Subject: [PATCH] ci: run mypy against the installed package, fix what that exposed The lint job failed on its first run with three errors my local run had not shown. Cause: locally mypy had no xknxproject installed, so ignore_missing_imports hid every mismatch against that library's own TypedDicts. CI installs the package and therefore sees them. The two range-walk helpers took dict[str, Any] but are handed a GroupRange TypedDict, which mypy rejects because dict is invariant; they take Mapping now, which is what they actually use. suggest passes the raw parsed dict into build_loaded_from_raw, whose parameter is KNXProject, so the boundary is an explicit cast rather than a widened public signature. CONTRIBUTING now gives the command that matches CI, with the reason, so the next person does not get a clean local run and a red CI. Suite passes, corpus guard reports no drift. --- CONTRIBUTING.md | 11 +++++++++++ nickol_knx_mcp/generate_ets.py | 3 ++- nickol_knx_mcp/project.py | 3 ++- nickol_knx_mcp/suggest.py | 5 +++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 760735f..5caf2ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,6 +59,17 @@ pip install -e . python tests/test_pipeline.py ``` +### Lint and types + +```bash +ruff check nickol_knx_mcp tests tools +uvx --with-editable . mypy # the package must be installed, or mypy misses xknxproject's types +``` + +Run mypy with the package installed. Without it, `ignore_missing_imports` silently hides every +mismatch against xknxproject's own TypedDicts, and CI, which does install it, will disagree. +Ruff is configured to `E9,F` only: real errors, no style rules. + CI runs the smoke test on Python 3.10–3.12 for every push and PR. ### Local corpus guard diff --git a/nickol_knx_mcp/generate_ets.py b/nickol_knx_mcp/generate_ets.py index e954306..652e7c1 100644 --- a/nickol_knx_mcp/generate_ets.py +++ b/nickol_knx_mcp/generate_ets.py @@ -12,6 +12,7 @@ from __future__ import annotations import csv import io +from collections.abc import Mapping from typing import Any from xml.sax.saxutils import escape @@ -28,7 +29,7 @@ def _hierarchy(project: LoadedProject) -> dict[int, dict[str, Any]]: main_names: dict[int, str] = {} mid_names: dict[tuple[int, int], str] = {} - def walk(rng: dict[str, Any]) -> None: + def walk(rng: Mapping[str, Any]) -> None: start = rng.get("address_start") name = rng.get("name", "") if isinstance(start, int): diff --git a/nickol_knx_mcp/project.py b/nickol_knx_mcp/project.py index 3bd3c21..0338b2a 100644 --- a/nickol_knx_mcp/project.py +++ b/nickol_knx_mcp/project.py @@ -9,6 +9,7 @@ from __future__ import annotations import re from dataclasses import dataclass, field +from collections.abc import Mapping from typing import Any, Optional from xknxproject import XKNXProj @@ -293,7 +294,7 @@ def _build_range_name_map(raw: KNXProject) -> dict[str, str]: """ out: dict[str, str] = {} - def walk(rng: dict[str, Any], depth: int) -> None: + def walk(rng: Mapping[str, Any], depth: int) -> None: start = rng.get("address_start") name = rng.get("name", "") if isinstance(start, int): diff --git a/nickol_knx_mcp/suggest.py b/nickol_knx_mcp/suggest.py index 4df41d0..4ede23f 100644 --- a/nickol_knx_mcp/suggest.py +++ b/nickol_knx_mcp/suggest.py @@ -27,7 +27,7 @@ from __future__ import annotations import re from collections import defaultdict -from typing import Any, Optional +from typing import Any, Optional, cast import yaml @@ -583,7 +583,8 @@ def _device_of(project: dict[str, Any], ga: str) -> tuple[str, str]: def _fallback_suggestions(project: dict[str, Any], covered: set[str]) -> list[dict[str, Any]]: - loaded = build_loaded_from_raw(project, project.get("info", {}).get("name") or "project.knxproj") + loaded = build_loaded_from_raw(cast("Any", project), + project.get("info", {}).get("name") or "project.knxproj") res = generate_ha_yaml(loaded) doc = yaml.safe_load(res["yaml"]) or {} knx = doc.get("knx") or {}