mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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
54 lines
3.1 KiB
Python
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
|