[core] Load component aliases from a generated registry (#18335)

This commit is contained in:
J. Nick Koston
2026-08-13 13:31:19 -05:00
committed by GitHub
parent 7420d23867
commit 945c2458b3
5 changed files with 122 additions and 37 deletions
+24 -37
View File
@@ -269,10 +269,9 @@ def _lookup_module(domain: str, exception: bool) -> ComponentManifest | None:
# If `domain` is the legacy name of a renamed component, redirect to the
# canonical module so the rest of the loader (and every caller of
# `get_component(legacy)`) transparently sees the new component.
alias_map = _get_alias_map()
if domain in alias_map:
canonical = alias_map[domain]
manif = _lookup_module(canonical, exception)
alias_meta = get_alias_metadata().get(domain)
if alias_meta is not None:
manif = _lookup_module(alias_meta.canonical, exception)
if manif is not None:
_COMPONENT_CACHE[domain] = manif
return manif
@@ -329,8 +328,10 @@ def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> Non
# ---------------------------------------------------------------------------
#
# A component can declare ``ALIASES = ["legacy_name"]`` (and optionally
# ``ALIAS_REMOVAL_VERSION = "YYYY.M.0"``) in its ``__init__.py``. Two
# integrations are then wired up automatically:
# ``ALIAS_REMOVAL_VERSION = "YYYY.M.0"``) in its ``__init__.py``, then run
# ``script/build_alias_registry.py`` to regenerate
# ``esphome/component_aliases.py`` (CI and a unit test fail if the registry
# is stale). Two integrations are then wired up automatically:
#
# 1. **Python imports** — a ``sys.meta_path`` finder (``_AliasFinder``)
# intercepts ``esphome.components.<legacy>``/``...<legacy>.<sub>``
@@ -344,13 +345,13 @@ def _replace_component_manifest(domain: str, manifest: ComponentManifest) -> Non
# dependency checks, schema validation and codegen all see only the
# canonical name.
#
# Both lookups are populated by ``_build_alias_map``, which **AST-parses**
# every component's ``__init__.py`` rather than importing it. That keeps the
# cost low: scanning ~400 components on disk takes ~5 ms instead of the
# multi-second cost of executing every component's import side-effects.
# Both lookups read the checked-in registry in ``esphome.component_aliases``
# (generated by ``script/build_alias_registry.py``, verified in CI), so no
# component-directory scan happens at runtime. ``_build_alias_map`` below is
# the generator's scan implementation; it **AST-parses** each component's
# ``__init__.py`` rather than importing it.
_ALIAS_MAP_CACHE: dict[str, str] | None = None
_ALIAS_META_CACHE: dict[str, "AliasMeta"] | None = None
@@ -367,31 +368,17 @@ class AliasMeta:
removal_version: str | None
def _ensure_alias_caches() -> None:
"""Populate both alias caches from a single directory scan.
``_build_alias_map`` returns both maps together, so building them in one
shot avoids scanning every component's ``__init__.py`` twice when a run
needs both the canonical map (loader) and the metadata map (config
pre-pass).
"""
global _ALIAS_MAP_CACHE, _ALIAS_META_CACHE
if _ALIAS_MAP_CACHE is None or _ALIAS_META_CACHE is None:
_ALIAS_MAP_CACHE, _ALIAS_META_CACHE = _build_alias_map()
def _get_alias_map() -> dict[str, str]:
"""Return the legacy-name → canonical-name map, building it lazily."""
_ensure_alias_caches()
return _ALIAS_MAP_CACHE
def get_alias_metadata() -> dict[str, AliasMeta]:
"""Return the legacy-name → :class:`AliasMeta` map (cached).
"""Return the legacy-name → :class:`AliasMeta` map, built lazily from
the generated registry."""
global _ALIAS_META_CACHE # noqa: PLW0603
if _ALIAS_META_CACHE is None:
from esphome.component_aliases import COMPONENT_ALIASES
Used by the YAML pre-pass to format a per-alias deprecation warning.
"""
_ensure_alias_caches()
_ALIAS_META_CACHE = {
alias: AliasMeta(canonical=canonical, removal_version=removal_version)
for alias, (canonical, removal_version) in COMPONENT_ALIASES.items()
}
return _ALIAS_META_CACHE
@@ -537,11 +524,11 @@ class _AliasFinder(importlib.abc.MetaPathFinder):
# least three parts, so ``parts[2]`` (the domain) always exists.
parts = fullname.split(".")
domain = parts[2]
alias_map = _get_alias_map()
if domain not in alias_map:
alias_meta = get_alias_metadata().get(domain)
if alias_meta is None:
return None
parts[2] = alias_map[domain]
parts[2] = alias_meta.canonical
canonical_fullname = ".".join(parts)
try:
canonical_module = importlib.import_module(canonical_fullname)