mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[core] Load component aliases from a generated registry (#18335)
This commit is contained in:
@@ -179,6 +179,7 @@ jobs:
|
|||||||
. venv/bin/activate
|
. venv/bin/activate
|
||||||
script/ci-custom.py
|
script/ci-custom.py
|
||||||
script/build_codeowners.py --check
|
script/build_codeowners.py --check
|
||||||
|
script/build_alias_registry.py --check
|
||||||
script/build_language_schema.py --check
|
script/build_language_schema.py --check
|
||||||
script/generate-esp32-boards.py --check
|
script/generate-esp32-boards.py --check
|
||||||
script/generate-rp2-boards.py --check
|
script/generate-rp2-boards.py --check
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Component alias registry.
|
||||||
|
|
||||||
|
Generated by script/build_alias_registry.py - do not edit manually.
|
||||||
|
See the component-alias section of esphome/loader.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# alias -> (canonical component, removal version or None)
|
||||||
|
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
||||||
|
"rp2040": ("rp2", "2027.7.0"),
|
||||||
|
}
|
||||||
+24
-37
@@ -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
|
# 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
|
# canonical module so the rest of the loader (and every caller of
|
||||||
# `get_component(legacy)`) transparently sees the new component.
|
# `get_component(legacy)`) transparently sees the new component.
|
||||||
alias_map = _get_alias_map()
|
alias_meta = get_alias_metadata().get(domain)
|
||||||
if domain in alias_map:
|
if alias_meta is not None:
|
||||||
canonical = alias_map[domain]
|
manif = _lookup_module(alias_meta.canonical, exception)
|
||||||
manif = _lookup_module(canonical, exception)
|
|
||||||
if manif is not None:
|
if manif is not None:
|
||||||
_COMPONENT_CACHE[domain] = manif
|
_COMPONENT_CACHE[domain] = manif
|
||||||
return 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
|
# A component can declare ``ALIASES = ["legacy_name"]`` (and optionally
|
||||||
# ``ALIAS_REMOVAL_VERSION = "YYYY.M.0"``) in its ``__init__.py``. Two
|
# ``ALIAS_REMOVAL_VERSION = "YYYY.M.0"``) in its ``__init__.py``, then run
|
||||||
# integrations are then wired up automatically:
|
# ``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``)
|
# 1. **Python imports** — a ``sys.meta_path`` finder (``_AliasFinder``)
|
||||||
# intercepts ``esphome.components.<legacy>``/``...<legacy>.<sub>``
|
# 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
|
# dependency checks, schema validation and codegen all see only the
|
||||||
# canonical name.
|
# canonical name.
|
||||||
#
|
#
|
||||||
# Both lookups are populated by ``_build_alias_map``, which **AST-parses**
|
# Both lookups read the checked-in registry in ``esphome.component_aliases``
|
||||||
# every component's ``__init__.py`` rather than importing it. That keeps the
|
# (generated by ``script/build_alias_registry.py``, verified in CI), so no
|
||||||
# cost low: scanning ~400 components on disk takes ~5 ms instead of the
|
# component-directory scan happens at runtime. ``_build_alias_map`` below is
|
||||||
# multi-second cost of executing every component's import side-effects.
|
# 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
|
_ALIAS_META_CACHE: dict[str, "AliasMeta"] | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -367,31 +368,17 @@ class AliasMeta:
|
|||||||
removal_version: str | None
|
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]:
|
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.
|
_ALIAS_META_CACHE = {
|
||||||
"""
|
alias: AliasMeta(canonical=canonical, removal_version=removal_version)
|
||||||
_ensure_alias_caches()
|
for alias, (canonical, removal_version) in COMPONENT_ALIASES.items()
|
||||||
|
}
|
||||||
return _ALIAS_META_CACHE
|
return _ALIAS_META_CACHE
|
||||||
|
|
||||||
|
|
||||||
@@ -537,11 +524,11 @@ class _AliasFinder(importlib.abc.MetaPathFinder):
|
|||||||
# least three parts, so ``parts[2]`` (the domain) always exists.
|
# least three parts, so ``parts[2]`` (the domain) always exists.
|
||||||
parts = fullname.split(".")
|
parts = fullname.split(".")
|
||||||
domain = parts[2]
|
domain = parts[2]
|
||||||
alias_map = _get_alias_map()
|
alias_meta = get_alias_metadata().get(domain)
|
||||||
if domain not in alias_map:
|
if alias_meta is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
parts[2] = alias_map[domain]
|
parts[2] = alias_meta.canonical
|
||||||
canonical_fullname = ".".join(parts)
|
canonical_fullname = ".".join(parts)
|
||||||
try:
|
try:
|
||||||
canonical_module = importlib.import_module(canonical_fullname)
|
canonical_module = importlib.import_module(canonical_fullname)
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate esphome/component_aliases.py from component ALIASES declarations.
|
||||||
|
|
||||||
|
Run without arguments to regenerate the registry; ``--check`` (run in CI)
|
||||||
|
verifies it is up to date.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# The root directory of the repo
|
||||||
|
root = Path(__file__).parent.parent
|
||||||
|
# Make the repo's esphome package win over any installed copy
|
||||||
|
sys.path.insert(0, str(root))
|
||||||
|
|
||||||
|
from esphome.helpers import write_file_if_changed # noqa: E402
|
||||||
|
from esphome.loader import _build_alias_map # noqa: E402
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--check",
|
||||||
|
help="Check if the alias registry is up to date.",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
registry_file = root / "esphome" / "component_aliases.py"
|
||||||
|
|
||||||
|
HEADER = '''"""Component alias registry.
|
||||||
|
|
||||||
|
Generated by script/build_alias_registry.py - do not edit manually.
|
||||||
|
See the component-alias section of esphome/loader.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# alias -> (canonical component, removal version or None)
|
||||||
|
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
||||||
|
'''
|
||||||
|
|
||||||
|
# _build_alias_map scans the real component tree and already rejects
|
||||||
|
# duplicate and shadowing aliases with an EsphomeError.
|
||||||
|
_, alias_meta = _build_alias_map()
|
||||||
|
|
||||||
|
lines = [HEADER]
|
||||||
|
for alias, meta in sorted(alias_meta.items()):
|
||||||
|
removal = f'"{meta.removal_version}"' if meta.removal_version else "None"
|
||||||
|
lines.append(f' "{alias}": ("{meta.canonical}", {removal}),\n')
|
||||||
|
lines.append("}\n")
|
||||||
|
content = "".join(lines)
|
||||||
|
|
||||||
|
if args.check:
|
||||||
|
if registry_file.read_text(encoding="utf-8") != content:
|
||||||
|
print("Component alias registry is not up to date.")
|
||||||
|
print("Please run `script/build_alias_registry.py`")
|
||||||
|
sys.exit(1)
|
||||||
|
print("Component alias registry is up to date")
|
||||||
|
else:
|
||||||
|
write_file_if_changed(registry_file, content)
|
||||||
|
print(f"Wrote {registry_file}")
|
||||||
@@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from esphome.component_aliases import COMPONENT_ALIASES
|
||||||
from esphome.loader import (
|
from esphome.loader import (
|
||||||
AliasMeta,
|
AliasMeta,
|
||||||
ComponentManifest,
|
ComponentManifest,
|
||||||
@@ -481,6 +482,33 @@ def test_real_alias_map_includes_rp2040() -> None:
|
|||||||
assert meta["rp2040"].removal_version == "2027.7.0"
|
assert meta["rp2040"].removal_version == "2027.7.0"
|
||||||
|
|
||||||
|
|
||||||
|
def test_alias_registry_matches_component_tree() -> None:
|
||||||
|
"""The checked-in registry must match a live scan of the component tree."""
|
||||||
|
_, meta_map = _build_alias_map()
|
||||||
|
expected = {
|
||||||
|
alias: (meta.canonical, meta.removal_version)
|
||||||
|
for alias, meta in meta_map.items()
|
||||||
|
}
|
||||||
|
assert expected == COMPONENT_ALIASES, (
|
||||||
|
"esphome/component_aliases.py is out of date; "
|
||||||
|
"run script/build_alias_registry.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_alias_map_built_from_registry() -> None:
|
||||||
|
"""The runtime alias map comes from the generated registry, not a scan."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"esphome.component_aliases.COMPONENT_ALIASES",
|
||||||
|
{"legacy": ("modern", "2099.1.0")},
|
||||||
|
),
|
||||||
|
patch("esphome.loader._ALIAS_META_CACHE", None),
|
||||||
|
):
|
||||||
|
assert get_alias_metadata() == {
|
||||||
|
"legacy": AliasMeta(canonical="modern", removal_version="2099.1.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_component_resolves_alias() -> None:
|
def test_get_component_resolves_alias() -> None:
|
||||||
"""``get_component('rp2040')`` should return the rp2 manifest — every
|
"""``get_component('rp2040')`` should return the rp2 manifest — every
|
||||||
caller of the loader (dep checker, schema validator, codegen) hits
|
caller of the loader (dep checker, schema validator, codegen) hits
|
||||||
|
|||||||
Reference in New Issue
Block a user