fix: plain-int check pre-coercion everywhere; purity guard scans alias leaf names at any level

This commit is contained in:
Joshua King
2026-09-14 10:51:21 -04:00
parent 6398bd525a
commit deb409175a
2 changed files with 52 additions and 12 deletions
+3 -1
View File
@@ -113,7 +113,9 @@ def _validate_weekday_set(weekday_set) -> frozenset[int]:
"""
if isinstance(weekday_set, (list, tuple)):
for entry in weekday_set:
if isinstance(entry, bool) or not isinstance(entry, int):
# _is_plain_int: bool and int subclasses (potentially
# unhashable) are rejected before set() coercion.
if not _is_plain_int(entry):
raise RuleValidationError(
f"weekday_set entries must be plain integers, got "
f"{entry!r}"
+49 -11
View File
@@ -297,25 +297,26 @@ def test_recurrence_module_imports_only_stdlib() -> None:
elif isinstance(node, ast.ImportFrom):
module = (node.module or "").lower()
level = node.level # 1+ means relative import
# 'from . import db' has module=None and the name in the
# aliases: check BOTH the module and every imported name.
imported_names = [alias.name for alias in node.names]
# Forbidden regardless of relative level: the leaf module
# names ('from . import db', 'from .db import x') AND any
# absolute custom_components.nestquest DB/store import.
forbidden_targets = {
"db", "schema", "dao_children", "dao_rules",
"dao_presence", "dao_instances", "migrations", "store",
}
if level:
for name in imported_names:
assert name not in forbidden_targets, (
f"recurrence.py must not import {name!r} from a "
"relative import"
)
imported_names = [alias.name for alias in node.names]
for name in imported_names:
leaf = name.split(".")[-1]
assert leaf not in forbidden_targets, (
f"recurrence.py must not import {name!r}"
)
if module:
full = f".{module}" if level else module
assert module not in forbidden_targets, (
f"recurrence.py must not import from .{module}"
f"recurrence.py must not import from {full}"
)
assert "db" not in module.split("."), (
f"recurrence.py must not import from {module}"
f"recurrence.py must not import from {full}"
)
assert "homeassistant" not in module, (
f"recurrence.py must not import from {module}"
@@ -325,6 +326,43 @@ def test_recurrence_module_imports_only_stdlib() -> None:
)
def test_purity_guard_blocks_absolute_db_import(tmp_path) -> None:
"""Absolute custom_components.nestquest DB imports (level=0) are
caught by the same alias-leaf scan the guard uses."""
import ast as ast_module
forbidden_targets = {
"db", "schema", "dao_children", "dao_rules", "dao_presence",
"dao_instances", "migrations", "store",
}
# The guard's own logic, exercised against probe statements:
from custom_components.nestquest import recurrence as recurrence_mod
probe_lines = (
"from custom_components.nestquest import db",
"from custom_components.nestquest.db import NestQuestDatabase",
"import custom_components.nestquest.dao_rules",
)
for line in probe_lines:
node = ast_module.parse(line).body[0]
caught = False
for item in ast_module.walk(node):
if isinstance(item, ast_module.ImportFrom):
module = (item.module or "").lower()
if module:
leaf = module.split(".")[-1]
if leaf in forbidden_targets:
caught = True
for alias in item.names:
if alias.name.split(".")[-1] in forbidden_targets:
caught = True
elif isinstance(item, ast_module.Import):
for alias in item.names:
leaf = alias.name.split(".")[-1]
if leaf in forbidden_targets:
caught = True
assert caught, f"probe import must be caught: {line}"
def test_non_string_rule_type_rejected() -> None:
"""Only RuleType members and their string values construct."""
for bad in (123, True, None, 3.5, b"daily"):