[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).
This commit is contained in:
J. Nick Koston
2026-05-16 15:28:12 -07:00
parent 3cee06e930
commit fabc1b584f
3 changed files with 44 additions and 24 deletions
+21
View File
@@ -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(
+12 -14
View File
@@ -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
+11 -10
View File
@@ -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