Accept None directly in the selector schemas (#151510)

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Artur Pragacz
2025-09-03 08:37:43 +02:00
committed by GitHub
parent 73ab041051
commit 8f16b09751
5 changed files with 62 additions and 63 deletions

View File

@@ -47,14 +47,7 @@ def selector(config: Any) -> Selector:
def validate_selector(config: Any) -> dict:
"""Validate a selector."""
selector_type, selector_class = _get_selector_type_and_class(config)
# Selectors can be empty
if config[selector_type] is None:
config = {selector_type: {}}
return {
selector_type: cast(dict, selector_class.CONFIG_SCHEMA(config[selector_type]))
}
return {selector_type: selector_class.CONFIG_SCHEMA(config[selector_type])}
class Selector[_T: Mapping[str, Any]]:
@@ -66,10 +59,6 @@ class Selector[_T: Mapping[str, Any]]:
def __init__(self, config: Mapping[str, Any] | None = None) -> None:
"""Instantiate a selector."""
# Selectors can be empty
if config is None:
config = {}
self.config = self.CONFIG_SCHEMA(config)
def __eq__(self, other: object) -> bool:
@@ -125,11 +114,25 @@ def _validate_supported_features(supported_features: list[str]) -> int:
return feature_mask
BASE_SELECTOR_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional("read_only"): bool,
}
)
def make_selector_config_schema(schema_dict: dict | None = None) -> vol.Schema:
"""Make selector config schema."""
if schema_dict is None:
schema_dict = {}
def none_to_empty_dict(value: Any) -> Any:
if value is None:
return {}
return value
return vol.Schema(
vol.All(
none_to_empty_dict,
{
vol.Optional("read_only"): bool,
**schema_dict,
},
)
)
class BaseSelectorConfig(TypedDict, total=False):
@@ -224,7 +227,7 @@ class ActionSelector(Selector[ActionSelectorConfig]):
selector_type = "action"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: ActionSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -248,7 +251,7 @@ class AddonSelector(Selector[AddonSelectorConfig]):
selector_type = "addon"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("name"): str,
vol.Optional("slug"): str,
@@ -279,7 +282,7 @@ class AreaSelector(Selector[AreaSelectorConfig]):
selector_type = "area"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("entity"): vol.All(
cv.ensure_list,
@@ -317,7 +320,7 @@ class AssistPipelineSelector(Selector[AssistPipelineSelectorConfig]):
selector_type = "assist_pipeline"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: AssistPipelineSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -342,7 +345,7 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
selector_type = "attribute"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Required("entity_id"): cv.entity_id,
# hide_attributes is used to hide attributes in the frontend.
@@ -371,7 +374,7 @@ class BackupLocationSelector(Selector[BackupLocationSelectorConfig]):
selector_type = "backup_location"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: BackupLocationSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -393,7 +396,7 @@ class BooleanSelector(Selector[BooleanSelectorConfig]):
selector_type = "boolean"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: BooleanSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -415,7 +418,7 @@ class ColorRGBSelector(Selector[ColorRGBSelectorConfig]):
selector_type = "color_rgb"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: ColorRGBSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -450,7 +453,7 @@ class ColorTempSelector(Selector[ColorTempSelectorConfig]):
selector_type = "color_temp"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("unit", default=ColorTempSelectorUnit.MIRED): vol.All(
vol.Coerce(ColorTempSelectorUnit), lambda val: val.value
@@ -497,7 +500,7 @@ class ConditionSelector(Selector[ConditionSelectorConfig]):
selector_type = "condition"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: ConditionSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -520,7 +523,7 @@ class ConfigEntrySelector(Selector[ConfigEntrySelectorConfig]):
selector_type = "config_entry"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("integration"): str,
}
@@ -550,7 +553,7 @@ class ConstantSelector(Selector[ConstantSelectorConfig]):
selector_type = "constant"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("label"): str,
vol.Optional("translation_key"): cv.string,
@@ -580,7 +583,7 @@ class ConversationAgentSelector(Selector[ConversationAgentSelectorConfig]):
selector_type = "conversation_agent"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("language"): str,
}
@@ -609,7 +612,7 @@ class CountrySelector(Selector[CountrySelectorConfig]):
selector_type = "country"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("countries"): [str],
vol.Optional("no_sort", default=False): cv.boolean,
@@ -640,7 +643,7 @@ class DateSelector(Selector[DateSelectorConfig]):
selector_type = "date"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: DateSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -662,7 +665,7 @@ class DateTimeSelector(Selector[DateTimeSelectorConfig]):
selector_type = "datetime"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: DateTimeSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -688,7 +691,7 @@ class DeviceSelector(Selector[DeviceSelectorConfig]):
selector_type = "device"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
**_LEGACY_DEVICE_SELECTOR_CONFIG_SCHEMA_DICT,
# Device has to contain entities matching this selector
@@ -731,7 +734,7 @@ class DurationSelector(Selector[DurationSelectorConfig]):
selector_type = "duration"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
# Enable day field in frontend. A selection with `days` set is allowed
# even if `enable_day` is not set
@@ -772,7 +775,7 @@ class EntitySelector(Selector[EntitySelectorConfig]):
selector_type = "entity"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
**_LEGACY_ENTITY_SELECTOR_CONFIG_SCHEMA_DICT,
vol.Optional("exclude_entities"): [str],
@@ -832,7 +835,7 @@ class FileSelector(Selector[FileSelectorConfig]):
selector_type = "file"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
# https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept
vol.Required("accept"): str,
@@ -867,7 +870,7 @@ class FloorSelector(Selector[FloorSelectorConfig]):
selector_type = "floor"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("entity"): vol.All(
cv.ensure_list,
@@ -907,7 +910,7 @@ class IconSelector(Selector[IconSelectorConfig]):
selector_type = "icon"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{vol.Optional("placeholder"): str}
# Frontend also has a fallbackPath option, this is not used by core
)
@@ -934,7 +937,7 @@ class LabelSelector(Selector[LabelSelectorConfig]):
selector_type = "label"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("multiple", default=False): cv.boolean,
}
@@ -968,7 +971,7 @@ class LanguageSelector(Selector[LanguageSelectorConfig]):
selector_type = "language"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("languages"): [str],
vol.Optional("native_name", default=False): cv.boolean,
@@ -1001,7 +1004,7 @@ class LocationSelector(Selector[LocationSelectorConfig]):
selector_type = "location"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{vol.Optional("radius"): bool, vol.Optional("icon"): str}
)
DATA_SCHEMA = vol.Schema(
@@ -1034,7 +1037,7 @@ class MediaSelector(Selector[MediaSelectorConfig]):
selector_type = "media"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("accept"): [str],
}
@@ -1109,7 +1112,7 @@ class NumberSelector(Selector[NumberSelectorConfig]):
selector_type = "number"
CONFIG_SCHEMA = vol.All(
BASE_SELECTOR_CONFIG_SCHEMA.extend(
make_selector_config_schema(
{
vol.Optional("min"): vol.Coerce(float),
vol.Optional("max"): vol.Coerce(float),
@@ -1169,7 +1172,7 @@ class ObjectSelector(Selector[ObjectSelectorConfig]):
selector_type = "object"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("fields"): {
str: {
@@ -1217,7 +1220,7 @@ class QrCodeSelector(Selector[QrCodeSelectorConfig]):
selector_type = "qr_code"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Required("data"): str,
vol.Optional("scale"): int,
@@ -1279,7 +1282,7 @@ class SelectSelector(Selector[SelectSelectorConfig]):
selector_type = "select"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Required("options"): vol.All(vol.Any([str], [select_option])),
vol.Optional("multiple", default=False): cv.boolean,
@@ -1333,7 +1336,7 @@ class StateSelector(Selector[StateSelectorConfig]):
selector_type = "state"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("entity_id"): cv.entity_id,
vol.Optional("hide_states"): [str],
@@ -1372,7 +1375,7 @@ class StatisticSelector(Selector[StatisticSelectorConfig]):
selector_type = "statistic"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("multiple", default=False): cv.boolean,
}
@@ -1409,7 +1412,7 @@ class TargetSelector(Selector[TargetSelectorConfig]):
selector_type = "target"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("entity"): vol.All(
cv.ensure_list,
@@ -1444,7 +1447,7 @@ class TemplateSelector(Selector[TemplateSelectorConfig]):
selector_type = "template"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: TemplateSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -1491,7 +1494,7 @@ class TextSelector(Selector[TextSelectorConfig]):
selector_type = "text"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("multiline", default=False): bool,
vol.Optional("prefix"): str,
@@ -1530,7 +1533,7 @@ class ThemeSelector(Selector[ThemeSelectorConfig]):
selector_type = "theme"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA.extend(
CONFIG_SCHEMA = make_selector_config_schema(
{
vol.Optional("include_default", default=False): cv.boolean,
}
@@ -1556,7 +1559,7 @@ class TimeSelector(Selector[TimeSelectorConfig]):
selector_type = "time"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: TimeSelectorConfig | None = None) -> None:
"""Instantiate a selector."""
@@ -1578,7 +1581,7 @@ class TriggerSelector(Selector[TriggerSelectorConfig]):
selector_type = "trigger"
CONFIG_SCHEMA = BASE_SELECTOR_CONFIG_SCHEMA
CONFIG_SCHEMA = make_selector_config_schema()
def __init__(self, config: TriggerSelectorConfig | None = None) -> None:
"""Instantiate a selector."""

View File

@@ -190,7 +190,7 @@ _SECTION_SCHEMA = vol.Schema(
_SERVICE_SCHEMA = vol.Schema(
{
vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None),
vol.Optional("target"): TargetSelector.CONFIG_SCHEMA,
vol.Optional("fields"): vol.Schema(
{str: vol.Any(_SECTION_SCHEMA, _FIELD_SCHEMA)}
),

View File

@@ -83,7 +83,7 @@ _FIELD_SCHEMA = vol.Schema(
_TRIGGER_SCHEMA = vol.Schema(
{
vol.Optional("target"): vol.Any(TargetSelector.CONFIG_SCHEMA, None),
vol.Optional("target"): TargetSelector.CONFIG_SCHEMA,
vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}),
},
extra=vol.ALLOW_EXTRA,

View File

@@ -119,9 +119,7 @@ def _service_schema(targeted: bool, custom: bool) -> vol.Schema:
}
if targeted:
schema_dict[vol.Required("target")] = vol.Any(
selector.TargetSelector.CONFIG_SCHEMA, None
)
schema_dict[vol.Required("target")] = selector.TargetSelector.CONFIG_SCHEMA
if custom:
schema_dict |= CUSTOM_INTEGRATION_EXTRA_SCHEMA_DICT

View File

@@ -38,9 +38,7 @@ FIELD_SCHEMA = vol.Schema(
TRIGGER_SCHEMA = vol.Any(
vol.Schema(
{
vol.Optional("target"): vol.Any(
selector.TargetSelector.CONFIG_SCHEMA, None
),
vol.Optional("target"): selector.TargetSelector.CONFIG_SCHEMA,
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
}
),