mirror of
https://github.com/esphome/esphome.git
synced 2026-10-15 10:42:52 +00:00
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""CodSpeed benchmarks for importing entity components.
|
|
|
|
Every ``esphome config`` or ``esphome compile`` imports the Python module of
|
|
each component the YAML uses. A component that imports its optional
|
|
integrations (mqtt, web_server, zigbee) at module scope drags their whole
|
|
import chains in for every user, so the cost of a fresh import is what these
|
|
benchmarks pin down.
|
|
|
|
The core of esphome is imported once up front and stays loaded; each run
|
|
drops only the component modules from ``sys.modules`` and imports the
|
|
component again, so the import machinery and the module bodies of the
|
|
component and everything it pulls in run every time, as they do in a new
|
|
process, while a change to the core moves no number here. The modules and
|
|
registry entries the session had before a case are put back afterwards, so
|
|
later tests keep the objects they already hold.
|
|
|
|
Skipped when ``pytest-codspeed`` isn't installed so the regular unit-test
|
|
suite keeps working unchanged.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator, Iterable
|
|
import importlib
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("pytest_codspeed")
|
|
|
|
# Load the core before the first case so no case pays for it.
|
|
from esphome import automation, pins # noqa: E402
|
|
import esphome.codegen # noqa: E402, F401
|
|
import esphome.config_validation # noqa: E402, F401
|
|
|
|
COMPONENTS = ("binary_sensor", "sensor", "logger")
|
|
COMPONENT_PACKAGE = "esphome.components"
|
|
COMPONENT_PREFIX = COMPONENT_PACKAGE + "."
|
|
# Core registries that component module bodies write to when they are imported
|
|
REGISTRIES = (
|
|
automation.ACTION_REGISTRY,
|
|
automation.CONDITION_REGISTRY,
|
|
pins.PIN_SCHEMA_REGISTRY,
|
|
)
|
|
|
|
|
|
def _component_modules() -> dict[str, object]:
|
|
# The package itself goes too: ``from esphome.components import x`` is
|
|
# answered from its attributes, so a stale package would hide the import.
|
|
return {
|
|
n: m
|
|
for n, m in sys.modules.items()
|
|
if n == COMPONENT_PACKAGE or n.startswith(COMPONENT_PREFIX)
|
|
}
|
|
|
|
|
|
def _forget_component_modules() -> None:
|
|
for name in _component_modules():
|
|
del sys.modules[name]
|
|
|
|
|
|
def _import_component(name: str) -> object:
|
|
_forget_component_modules()
|
|
return importlib.import_module(COMPONENT_PREFIX + name)
|
|
|
|
|
|
_MISSING = object()
|
|
|
|
|
|
def _parent_bindings(names: Iterable[str]) -> list[tuple[object, str, object]]:
|
|
# An import binds the module to its parent package; a package may hold something else
|
|
# under that name (substitutions keeps a Jinja() instance as ``jinja``), so the value
|
|
# the parent has now is what goes back, not the module.
|
|
bindings = []
|
|
for name in names:
|
|
parent, _, child = name.rpartition(".")
|
|
bindings.append(
|
|
(sys.modules[parent], child, getattr(sys.modules[parent], child, _MISSING))
|
|
)
|
|
return bindings
|
|
|
|
|
|
@pytest.fixture
|
|
def restore_component_modules() -> Generator[None]:
|
|
saved = _component_modules()
|
|
saved_bindings = _parent_bindings(saved)
|
|
saved_registries = [dict(registry) for registry in REGISTRIES]
|
|
yield
|
|
_forget_component_modules()
|
|
sys.modules.update(saved)
|
|
for parent, child, value in saved_bindings:
|
|
if value is _MISSING:
|
|
delattr(parent, child)
|
|
else:
|
|
setattr(parent, child, value)
|
|
# The re-imported bodies registered their actions, conditions and pin schemas again
|
|
for registry, entries in zip(REGISTRIES, saved_registries, strict=True):
|
|
registry.clear()
|
|
registry.update(entries)
|
|
|
|
|
|
@pytest.mark.usefixtures("restore_component_modules")
|
|
@pytest.mark.parametrize("component", COMPONENTS)
|
|
def test_import_component(benchmark, component: str) -> None:
|
|
benchmark(_import_component, component)
|