From fabc1b584fecc85ff7f497689a980e7a29dbf807 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 16 May 2026 15:28:12 -0700 Subject: [PATCH] [lvgl] Extract lazy_once helper to share the build-once pattern Both container_schema and _lazy_update_schema implement the same "build the voluptuous schema on first call and cache the result" closure. Factor it out into lazy_once() in helpers.py so the pattern only lives in one place. No behavioral change; lvgl test configs still pass and import time is unchanged from the previous commit (~215ms cold on M-series Mac). --- esphome/components/lvgl/helpers.py | 21 +++++++++++++++++ esphome/components/lvgl/schemas.py | 26 ++++++++++----------- esphome/components/lvgl/widgets/__init__.py | 21 +++++++++-------- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index 6f70a1e3bd..4e41eb900a 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -1,10 +1,31 @@ +from collections.abc import Callable import re +from typing import TypeVar from esphome import config_validation as cv from esphome.const import CONF_ARGS, CONF_FORMAT CONF_IF_NAN = "if_nan" +T = TypeVar("T") + + +def lazy_once(build: Callable[[], T]) -> Callable[[], T]: + """Return a no-arg callable that runs ``build`` at most once and caches it. + + Used to defer voluptuous schema construction until first validation. Many + of the lvgl schemas would otherwise be built at module-import time even for + YAMLs that never reach them. + """ + cached: list[T] = [] + + def get() -> T: + if not cached: + cached.append(build()) + return cached[0] + + return get + # noqa f_regex = re.compile( diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 889501624e..ac56a93867 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -40,7 +40,7 @@ from .defines import ( get_remapped_uses, is_press_event, ) -from .helpers import CONF_IF_NAN, validate_printf +from .helpers import CONF_IF_NAN, lazy_once, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, GRID_CELL_SCHEMA, @@ -542,27 +542,25 @@ def container_schema(widget_type: WidgetType, extras=None): :param extras: Additional options to be made available, e.g. layout properties for children :return: The schema for this type of widget. """ + # Schema construction is deferred until first validation. obj_schema and the # part_schema -> base_update_schema chain account for ~225ms of cumulative # work at lvgl module import time across ~9 callers; building lazily keeps # `esphome config` fast for YAMLs that never reach this widget type. - schema_holder: list = [] - def build() -> cv.Schema: - if not schema_holder: - built = obj_schema(widget_type).extend( - {cv.GenerateID(): cv.declare_id(widget_type.w_type)} - ) - if extras: - built = built.extend(extras) - # Delayed evaluation for recursion - built = built.extend(widget_type.schema) - schema_holder.append(built) - return schema_holder[0] + built = obj_schema(widget_type).extend( + {cv.GenerateID(): cv.declare_id(widget_type.w_type)} + ) + if extras: + built = built.extend(extras) + # Delayed evaluation for recursion + return built.extend(widget_type.schema) + + get_schema = lazy_once(build) def validator(value): value = value or {} - return append_layout_schema(build(), value)(value) + return append_layout_schema(get_schema(), value)(value) return validator diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 2df20faf9d..d56aefa769 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -81,18 +81,19 @@ def _lazy_update_schema(widget_type: "WidgetType"): caller (register_action) only needs a validator, so wrap the build in a closure that materialises on the first validation and caches the result. """ - compiled: list = [] + from ..helpers import lazy_once + + def build(): + from ..schemas import base_update_schema + + return base_update_schema(widget_type, widget_type.parts).extend( + widget_type.modify_schema + ) + + get_schema = lazy_once(build) def validator(value): - if not compiled: - from ..schemas import base_update_schema - - compiled.append( - base_update_schema(widget_type, widget_type.parts).extend( - widget_type.modify_schema - ) - ) - return compiled[0](value) + return get_schema()(value) return validator