fix: plain-int check pre-coercion everywhere; purity guard scans alias leaf names at any level
This commit is contained in:
@@ -113,7 +113,9 @@ def _validate_weekday_set(weekday_set) -> frozenset[int]:
|
|||||||
"""
|
"""
|
||||||
if isinstance(weekday_set, (list, tuple)):
|
if isinstance(weekday_set, (list, tuple)):
|
||||||
for entry in weekday_set:
|
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(
|
raise RuleValidationError(
|
||||||
f"weekday_set entries must be plain integers, got "
|
f"weekday_set entries must be plain integers, got "
|
||||||
f"{entry!r}"
|
f"{entry!r}"
|
||||||
|
|||||||
@@ -297,25 +297,26 @@ def test_recurrence_module_imports_only_stdlib() -> None:
|
|||||||
elif isinstance(node, ast.ImportFrom):
|
elif isinstance(node, ast.ImportFrom):
|
||||||
module = (node.module or "").lower()
|
module = (node.module or "").lower()
|
||||||
level = node.level # 1+ means relative import
|
level = node.level # 1+ means relative import
|
||||||
# 'from . import db' has module=None and the name in the
|
# Forbidden regardless of relative level: the leaf module
|
||||||
# aliases: check BOTH the module and every imported name.
|
# names ('from . import db', 'from .db import x') AND any
|
||||||
imported_names = [alias.name for alias in node.names]
|
# absolute custom_components.nestquest DB/store import.
|
||||||
forbidden_targets = {
|
forbidden_targets = {
|
||||||
"db", "schema", "dao_children", "dao_rules",
|
"db", "schema", "dao_children", "dao_rules",
|
||||||
"dao_presence", "dao_instances", "migrations", "store",
|
"dao_presence", "dao_instances", "migrations", "store",
|
||||||
}
|
}
|
||||||
if level:
|
imported_names = [alias.name for alias in node.names]
|
||||||
for name in imported_names:
|
for name in imported_names:
|
||||||
assert name not in forbidden_targets, (
|
leaf = name.split(".")[-1]
|
||||||
f"recurrence.py must not import {name!r} from a "
|
assert leaf not in forbidden_targets, (
|
||||||
"relative import"
|
f"recurrence.py must not import {name!r}"
|
||||||
)
|
)
|
||||||
if module:
|
if module:
|
||||||
|
full = f".{module}" if level else module
|
||||||
assert module not in forbidden_targets, (
|
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("."), (
|
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, (
|
assert "homeassistant" not in module, (
|
||||||
f"recurrence.py must not import from {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:
|
def test_non_string_rule_type_rejected() -> None:
|
||||||
"""Only RuleType members and their string values construct."""
|
"""Only RuleType members and their string values construct."""
|
||||||
for bad in (123, True, None, 3.5, b"daily"):
|
for bad in (123, True, None, 3.5, b"daily"):
|
||||||
|
|||||||
Reference in New Issue
Block a user