Apply PR #144 review round 2: settings-based service kwargs, required settings, shim removal, hardened AST scan

P2: services._generation_kwargs now reads runtime.settings.horizon_days
instead of re-implementing the bool/non-int/<1 fallback against
runtime.options; drop the now-unused CONF_HORIZON_DAYS/DEFAULT_HORIZON_DAYS
imports. Remove NestQuestRuntimeData.options (kept only for that read; the
admin-id path reads entry.options directly) and the matching test assertion.

P3: _run_horizon_materialization's settings param is now REQUIRED (drop the
'| None = None' default + the NestQuestSettings() substitution branch);
update the test_timezones callers to pass NestQuestSettings() explicitly.

P3: __post_init__ normalises notify_target via _resolve_optional_str (strip)
so a hand-built NestQuestSettings(notify_target=' x ') behaves like the
options flow; keep the docstring accurate.

P3: delete the custom_components/nestquest/settings.py shim (settings is a
new module with no legacy import path) and import NestQuestSettings from
.core.settings directly in __init__.py, in alphabetical position.

P3: harden the AST scan for importlib.import_module('homeassistant...')
(string-based import) and call keyword names like hass= (ast.keyword);
add negative-control cases for each; explain in the failure message why
.config/.options/.data are banned (HA config-entry attributes with no
legitimate core use).

P3: drive from_options and from_options_resilient from one _FIELDS table of
(field, key, default, validator) to avoid three-way drift.

uv run pytest -q: 1573 passed.
This commit is contained in:
2026-09-21 15:42:26 -04:00
parent 33102f8806
commit d75ddc19bd
7 changed files with 221 additions and 176 deletions
+10 -12
View File
@@ -24,7 +24,7 @@ from .const import (
LOGGER,
PLATFORMS,
)
from .settings import NestQuestSettings
from .core.settings import NestQuestSettings
from .frontend import async_register_frontend
from .coordinator import NestQuestCoordinator
from .db import NestQuestDatabase, make_database
@@ -237,7 +237,6 @@ class NestQuestRuntimeData:
"""Runtime data stored on a NestQuest config entry."""
entry_id: str
options: dict[str, Any]
settings: NestQuestSettings
database: NestQuestDatabase
remove_update_listener: Callable[[], Any]
@@ -270,7 +269,7 @@ async def _async_owner_user_ids(hass: HomeAssistant) -> list[str]:
async def _run_horizon_materialization(
hass: HomeAssistant,
database: NestQuestDatabase,
settings: NestQuestSettings | None = None,
settings: NestQuestSettings,
) -> None:
"""Materialize the rolling horizon ``[today, today + horizon_days]``.
@@ -281,14 +280,12 @@ async def _run_horizon_materialization(
This is the ONE generation path: the startup backfill, the daily rollover
listener and the ``regenerate`` service all funnel through it, so there is
no duplicated materialization logic. ``settings`` carries the entry's
validated ``horizon_days``; when omitted a default
:class:`NestQuestSettings` is used (its horizon is the configured
default). The window itself comes from :meth:`settings.horizon_window`
so the helper is real and shared (not recomputed inline here).
no duplicated materialization logic. ``settings`` (REQUIRED — there is no
silent default; every caller already passes the entry's validated
:class:`NestQuestSettings`) carries the configured ``horizon_days``. The
window itself comes from :meth:`settings.horizon_window` so the helper is
real and shared (not recomputed inline here).
"""
if settings is None:
settings = NestQuestSettings()
time_zone = ZoneInfo(hass.config.time_zone)
today = datetime.datetime.now(time_zone).date()
start, end = settings.horizon_window(today)
@@ -329,7 +326,9 @@ def _find_live_runtime_data(hass: HomeAssistant) -> NestQuestRuntimeData | None:
their own; they resolve whichever config entry's runtime data is
currently live at call time, so none of them can hold a stale handle
to a closed connection after another entry unloads. The record also
carries that entry's options (e.g. the configured horizon).
carries that entry's validated ``settings`` (e.g. the configured
horizon) so service handlers read horizon days off it rather than
re-reading HA config.
"""
for runtime_data in hass.data.get(DOMAIN, {}).values():
database = getattr(runtime_data, "database", None)
@@ -505,7 +504,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
runtime_data = NestQuestRuntimeData(
entry_id=entry.entry_id,
options=dict(entry.options),
settings=settings,
database=database,
remove_update_listener=remove_update_listener,