mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user