Story 1.1 (TDD, Suite gruen 19/19): - logic/model.py: frozen Dataclasses (NormField, RawForecast, NormForecast, Requirement, Item, ChosenItem, Gap, Outfit, Recommendation) + Enums RequirementKey/Category/Priority (AD-14/21/22). - const.py: normatives flaches Options-Schema + default_options()-Deepcopy, TO_REDACT, Timeouts, LLM-URLs (AD-5/6/23). - manifest.json/hacs.json (min HA 2025.3), Layering-Guard per AST (AD-1). - luna-pro-Story-Review: 4/6 Findings uebernommen, 2 verworfen (Evidenz im Ledger). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.5 KiB
Python
62 lines
2.5 KiB
Python
"""Story 1.1 — architectural layering guard (AD-1).
|
|
|
|
The pure core under ``logic/`` must never import Home Assistant (nor ``dt_util``).
|
|
This is a static AST scan of import statements, so it holds even though the test
|
|
environment has Home Assistant installed, and it does not false-positive on the
|
|
strings "import homeassistant" appearing inside docstrings or data.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
import pathlib
|
|
|
|
LOGIC_DIR = (
|
|
pathlib.Path(__file__).parent.parent
|
|
/ "custom_components"
|
|
/ "what_to_wear"
|
|
/ "logic"
|
|
)
|
|
|
|
|
|
def _forbidden_imports(source: str) -> list[str]:
|
|
"""Return the offending imported module names in ``source`` (AST-based)."""
|
|
offenders: list[str] = []
|
|
tree = ast.parse(source)
|
|
for node in ast.walk(tree):
|
|
names: list[str] = []
|
|
if isinstance(node, ast.Import):
|
|
names = [alias.name for alias in node.names]
|
|
elif isinstance(node, ast.ImportFrom):
|
|
# module is None for "from . import x"; treat as empty
|
|
names = [node.module or ""]
|
|
names += [alias.name for alias in node.names]
|
|
for name in names:
|
|
root = name.split(".")[0]
|
|
if root == "homeassistant" or name == "dt_util" or "dt_util" in name:
|
|
offenders.append(name)
|
|
return offenders
|
|
|
|
|
|
def test_logic_has_no_homeassistant_imports() -> None:
|
|
py_files = list(LOGIC_DIR.rglob("*.py"))
|
|
assert py_files, "logic/ must contain Python modules"
|
|
offenders: dict[str, list[str]] = {}
|
|
for path in py_files:
|
|
found = _forbidden_imports(path.read_text(encoding="utf-8"))
|
|
if found:
|
|
offenders[str(path.relative_to(LOGIC_DIR.parent))] = found
|
|
assert not offenders, f"logic/ must not import homeassistant/dt_util: {offenders}"
|
|
|
|
|
|
def test_guard_catches_violations_and_ignores_strings() -> None:
|
|
# Real imports are caught.
|
|
assert _forbidden_imports("import homeassistant\n")
|
|
assert _forbidden_imports("from homeassistant.core import HomeAssistant\n")
|
|
assert _forbidden_imports("from homeassistant.util import dt as dt_util\n")
|
|
assert _forbidden_imports("import homeassistant.util.dt as dt_util\n")
|
|
# Stdlib imports are fine.
|
|
assert not _forbidden_imports("import datetime\n")
|
|
assert not _forbidden_imports("from zoneinfo import ZoneInfo\n")
|
|
# A docstring merely *mentioning* the words is NOT a violation (no false positive).
|
|
assert not _forbidden_imports('"""We must never import homeassistant here."""\n')
|
|
assert not _forbidden_imports('X = "from homeassistant.core import HomeAssistant"\n')
|