Files
houseplan-card/tests_backend/pure_imports.py
T
Codex d9b6766362 test: the sys.modules guard now sees the write, not its spelling (#398)
The guard introduced by #394 matched the literal
sys.modules['custom_components... and therefore never looked at
pure_imports.py, which writes through a variable — the third instance of
the #389 class walked straight past the check created for it.

The guard now inspects the write itself and decides by the key: a whole
literal or the literal head of an f-string is safe unless it starts with
custom_components (that is how tests register homeassistant.*, hp_pure.*
and houseplan.trails); anything else — a variable, a concatenation,
setdefault/update — counts as a violation whenever the file is able to
name the package at all, i.e. contains a custom_components. literal. A
file that never names the package cannot poison it through a variable,
so restoring a snapshot stays legal.

load_pure now removes what it registered. Removing its own name is not
enough: relative imports pull neighbours in, so junction_limits leaves
wall_segment_model and coordinate_canonicalization behind. It removes
the whole custom_components difference accumulated during exec_module,
in a finally, and a repeated call still works.

pure_imports.py is a named exemption of the static guard precisely
because that guard cannot see the cleanup — so the cleanup is proven by
an executable test instead, and the mutant pure-imports-stops-cleaning
reddens it. Both mutants were run by hand.

User-Visible: no
Issue: #398
2026-08-31 03:56:01 +03:00

54 lines
3.1 KiB
Python

"""Загрузка модуля интеграции по пути, без Home Assistant (issue #394).
Зачем не обычный импорт: часть чистых тестов читает модуль под своим именем
(`hp_validation`) либо под каноническим, чтобы работали относительные импорты
внутри него. Родительские пакеты при этом должны существовать — в окружении с
Home Assistant это настоящий пакет, без него пустышки ставит `conftest.py`.
Ставить пустышки здесь, в момент загрузки, было бы ошибкой: именно так они
переживали свой тест и доставались всей сессии. В #389 это стоило пяти часов
красного `dev` — HA получал пустышку вместо интеграции и не мог её поднять.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
PACKAGE_ROOT = REPO / "custom_components"
HOUSEPLAN_ROOT = PACKAGE_ROOT / "houseplan"
def load_pure(name: str, file: Path):
"""Загрузить модуль по пути под указанным именем и убрать за собой.
Имя значимо: относительные импорты внутри модуля резолвятся только тогда,
когда модуль знает, какому пакету принадлежит.
Регистрация в `sys.modules` обязательна на время `exec_module` и вредна
после (#398). Она переживала вызов и доставалась всей сессии — тот же
класс, что #389: загрузчик Home Assistant получал бы модуль, собранный
мимо него, а объекты классов одного файла оказывались бы разными. Снимать
только собственное имя мало: относительные импорты подтягивают соседей
(`junction_limits` тянет `wall_segment_model` и
`coordinate_canonicalization`), поэтому снимается вся разница под
префиксом `custom_components`, появившаяся за время загрузки.
`conftest.py` ставит пустышки родительских пакетов, когда Home Assistant
недоступен, — они принадлежат ему и здесь не трогаются.
"""
before = frozenset(sys.modules)
spec = importlib.util.spec_from_file_location(name, file)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
try:
spec.loader.exec_module(module)
finally:
for key in [
key for key in sys.modules
if key not in before and key.startswith(PACKAGE_ROOT.name)
]:
del sys.modules[key]
return module