From 2b422cbd991d26125986a3f5caa40d2edda60198 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 22 May 2026 19:20:39 -0500 Subject: [PATCH] [lvgl] Build widget update action schemas lazily (#16569) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/widgets/__init__.py | 36 +++++++++++-- .../lvgl/test_update_action_lazy.py | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/lvgl/test_update_action_lazy.py diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index ab1c61ff88..400f7c709b 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,4 +1,6 @@ +from collections.abc import Callable import sys +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.automation import register_action @@ -15,6 +17,7 @@ from esphome.const import ( from esphome.core import ID, EsphomeError, TimePeriod from esphome.coroutine import FakeAwaitable from esphome.cpp_generator import MockObj +from esphome.schema_extractors import EnableSchemaExtraction from esphome.types import Expression from ..defines import ( @@ -73,6 +76,34 @@ from ..types import ( EVENT_LAMB = "event_lamb__" +def _build_update_schema(widget_type: "WidgetType") -> Schema: + # Local import: ..schemas imports WidgetType from this module. + from ..schemas import base_update_schema + + return base_update_schema(widget_type, widget_type.parts).extend( + widget_type.modify_schema + ) + + +def _update_action_schema( + widget_type: "WidgetType", +) -> Schema | Callable[[Any], Any]: + # Eager when extracting so build_language_schema.py sees the mapping; + # lazy otherwise to skip ~200 ms of import-time voluptuous work. + if EnableSchemaExtraction: + return _build_update_schema(widget_type) + + cached: Schema | None = None + + def validator(value: Any) -> Any: + nonlocal cached + if cached is None: + cached = _build_update_schema(widget_type) + return cached(value) + + return validator + + class WidgetType: """ Describes a type of Widget, e.g. "bar" or "line" @@ -113,18 +144,17 @@ class WidgetType: # Local import to avoid circular import from ..automation import update_to_code - from ..schemas import WIDGET_TYPES, base_update_schema + from ..schemas import WIDGET_TYPES if not is_mock: if self.name in WIDGET_TYPES: raise EsphomeError(f"Duplicate definition of widget type '{self.name}'") WIDGET_TYPES[self.name] = self - # Register the update action automatically, adding widget-specific properties register_action( f"lvgl.{self.name}.update", ObjUpdateAction, - base_update_schema(self, self.parts).extend(self.modify_schema), + _update_action_schema(self), synchronous=True, )(update_to_code) diff --git a/tests/component_tests/lvgl/test_update_action_lazy.py b/tests/component_tests/lvgl/test_update_action_lazy.py new file mode 100644 index 0000000000..7fcdc149cf --- /dev/null +++ b/tests/component_tests/lvgl/test_update_action_lazy.py @@ -0,0 +1,53 @@ +"""Tests for lvgl..update lazy schema build.""" + +from __future__ import annotations + +from unittest.mock import patch + +from esphome.automation import ACTION_REGISTRY +import esphome.components.lvgl # noqa: F401 +from esphome.components.lvgl.schemas import WIDGET_TYPES +from esphome.components.lvgl.widgets import _update_action_schema +from esphome.config_validation import Schema + + +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_registry_entry_uses_lazy_validator() -> None: + entry = ACTION_REGISTRY["lvgl.label.update"] + assert callable(entry.raw_schema) + assert not isinstance(entry.raw_schema, Schema) + + +def test_lazy_validator_defers_build_until_first_call() -> None: + wt = _widget_type("label") + with patch( + "esphome.components.lvgl.widgets._build_update_schema", + wraps=lambda w: Schema({}), + ) as build_mock: + validator = _update_action_schema(wt) + assert build_mock.call_count == 0 + validator({}) + assert build_mock.call_count == 1 + validator({}) + assert build_mock.call_count == 1 + + +def test_eager_build_when_schema_extraction_enabled() -> None: + wt = _widget_type("label") + with patch("esphome.components.lvgl.widgets.EnableSchemaExtraction", True): + result = _update_action_schema(wt) + assert isinstance(result, Schema) + + +def test_lazy_and_eager_produce_equivalent_validation() -> None: + wt = _widget_type("label") + with patch("esphome.components.lvgl.widgets.EnableSchemaExtraction", True): + eager = _update_action_schema(wt) + lazy = _update_action_schema(wt) + sample = {"id": "label_id"} + assert lazy(sample) == eager(sample)