mirror of
https://github.com/esphome/esphome.git
synced 2026-06-02 11:08:06 +08:00
[lvgl] Memoize and lazily build container_schema (#16567)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from esphome import config_validation as cv
|
from esphome import config_validation as cv
|
||||||
from esphome.automation import Trigger, validate_automation
|
from esphome.automation import Trigger, validate_automation
|
||||||
@@ -534,7 +535,16 @@ def strip_defaults(schema: cv.Schema):
|
|||||||
return cv.Schema({cv.Optional(k): v for k, v in schema.schema.items()})
|
return cv.Schema({cv.Optional(k): v for k, v in schema.schema.items()})
|
||||||
|
|
||||||
|
|
||||||
def container_schema(widget_type: WidgetType, extras=None):
|
# Keyed by (id(widget_type), id(extras)); strong refs in the value keep both
|
||||||
|
# alive so id() can't be recycled.
|
||||||
|
_CONTAINER_SCHEMA_CACHE: dict[
|
||||||
|
tuple[int, int], tuple[Any, Any, Callable[[Any], Any]]
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def container_schema(
|
||||||
|
widget_type: WidgetType, extras: Any = None
|
||||||
|
) -> Callable[[Any], Any]:
|
||||||
"""
|
"""
|
||||||
Create a schema for a container widget of a given type. All obj properties are available, plus
|
Create a schema for a container widget of a given type. All obj properties are available, plus
|
||||||
the extras passed in, plus any defined for the specific widget being specified.
|
the extras passed in, plus any defined for the specific widget being specified.
|
||||||
@@ -542,19 +552,31 @@ def container_schema(widget_type: WidgetType, extras=None):
|
|||||||
:param extras: Additional options to be made available, e.g. layout properties for children
|
:param extras: Additional options to be made available, e.g. layout properties for children
|
||||||
:return: The schema for this type of widget.
|
:return: The schema for this type of widget.
|
||||||
"""
|
"""
|
||||||
|
cache_key = (id(widget_type), id(extras))
|
||||||
|
cached = _CONTAINER_SCHEMA_CACHE.get(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
cached_widget_type, cached_extras, cached_validator = cached
|
||||||
|
if cached_widget_type is widget_type and cached_extras is extras:
|
||||||
|
return cached_validator
|
||||||
|
|
||||||
|
cached_schema: cv.Schema | None = None
|
||||||
|
|
||||||
|
def get_schema() -> cv.Schema:
|
||||||
|
nonlocal cached_schema
|
||||||
|
if cached_schema is None:
|
||||||
schema = obj_schema(widget_type).extend(
|
schema = obj_schema(widget_type).extend(
|
||||||
{cv.GenerateID(): cv.declare_id(widget_type.w_type)}
|
{cv.GenerateID(): cv.declare_id(widget_type.w_type)}
|
||||||
)
|
)
|
||||||
if extras:
|
if extras:
|
||||||
schema = schema.extend(extras)
|
schema = schema.extend(extras)
|
||||||
# Delayed evaluation for recursion
|
cached_schema = schema.extend(widget_type.schema)
|
||||||
|
return cached_schema
|
||||||
|
|
||||||
schema = schema.extend(widget_type.schema)
|
def validator(value: Any) -> Any:
|
||||||
|
|
||||||
def validator(value):
|
|
||||||
value = value or {}
|
value = value or {}
|
||||||
return append_layout_schema(schema, value)(value)
|
return append_layout_schema(get_schema(), value)(value)
|
||||||
|
|
||||||
|
_CONTAINER_SCHEMA_CACHE[cache_key] = (widget_type, extras, validator)
|
||||||
return validator
|
return validator
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Tests for container_schema() memoization and lazy build."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from esphome import config_validation as cv
|
||||||
|
import esphome.components.lvgl # noqa: F401
|
||||||
|
from esphome.components.lvgl import schemas as lvgl_schemas
|
||||||
|
from esphome.components.lvgl.schemas import WIDGET_TYPES, container_schema
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clear_container_schema_cache() -> Generator[None]:
|
||||||
|
cache = getattr(lvgl_schemas, "_CONTAINER_SCHEMA_CACHE", None)
|
||||||
|
if cache is not None:
|
||||||
|
cache.clear()
|
||||||
|
yield
|
||||||
|
if cache is not None:
|
||||||
|
cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _widget_type(name: str = "obj"):
|
||||||
|
wt = WIDGET_TYPES.get(name)
|
||||||
|
assert wt is not None, f"widget type {name!r} not registered"
|
||||||
|
return wt
|
||||||
|
|
||||||
|
|
||||||
|
def test_same_args_return_same_validator() -> None:
|
||||||
|
wt = _widget_type("obj")
|
||||||
|
assert container_schema(wt) is container_schema(wt)
|
||||||
|
|
||||||
|
|
||||||
|
def test_extras_none_vs_truthy_get_different_validators() -> None:
|
||||||
|
wt = _widget_type("obj")
|
||||||
|
no_extras = container_schema(wt)
|
||||||
|
extras = {cv.Optional("custom_extra"): cv.string}
|
||||||
|
assert no_extras is not container_schema(wt, extras)
|
||||||
|
|
||||||
|
|
||||||
|
def test_different_widget_types_get_different_validators() -> None:
|
||||||
|
assert container_schema(_widget_type("obj")) is not container_schema(
|
||||||
|
_widget_type("label")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_build_is_deferred_until_first_validation() -> None:
|
||||||
|
wt = _widget_type("obj")
|
||||||
|
with patch.object(
|
||||||
|
lvgl_schemas, "obj_schema", wraps=lvgl_schemas.obj_schema
|
||||||
|
) as obj_schema_mock:
|
||||||
|
validator = container_schema(wt)
|
||||||
|
assert obj_schema_mock.call_count == 0
|
||||||
|
validator({})
|
||||||
|
assert obj_schema_mock.call_count == 1
|
||||||
|
validator({})
|
||||||
|
assert obj_schema_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cached_validator_produces_equivalent_output() -> None:
|
||||||
|
wt = _widget_type("obj")
|
||||||
|
cached = container_schema(wt)
|
||||||
|
cached_result = cached({})
|
||||||
|
lvgl_schemas._CONTAINER_SCHEMA_CACHE.clear()
|
||||||
|
reference = container_schema(wt)
|
||||||
|
assert cached is not reference
|
||||||
|
assert cached_result == reference({})
|
||||||
|
|
||||||
|
|
||||||
|
def test_id_recycling_is_caught_by_identity_guard() -> None:
|
||||||
|
wt = _widget_type("obj")
|
||||||
|
real_extras = {cv.Optional("a"): cv.int_}
|
||||||
|
validator_a = container_schema(wt, real_extras)
|
||||||
|
|
||||||
|
cache_key = (id(wt), id(real_extras))
|
||||||
|
cached_entry = lvgl_schemas._CONTAINER_SCHEMA_CACHE[cache_key]
|
||||||
|
sentinel = {cv.Optional("a"): cv.int_}
|
||||||
|
lvgl_schemas._CONTAINER_SCHEMA_CACHE[cache_key] = (
|
||||||
|
cached_entry[0],
|
||||||
|
sentinel,
|
||||||
|
cached_entry[2],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert container_schema(wt, real_extras) is not validator_a
|
||||||
Reference in New Issue
Block a user