mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 06:50:41 -04:00
Implement a classlevel ReadOnlyDict instead of a dict per instance for allowed_context_keys in Selector classes (#183761)
This commit is contained in:
@@ -14,6 +14,7 @@ from homeassistant.const import CONF_MODE, CONF_UNIT_OF_MEASUREMENT, Platform
|
||||
from homeassistant.core import split_entity_id, valid_entity_id
|
||||
from homeassistant.generated.countries import COUNTRIES
|
||||
from homeassistant.util import decorator
|
||||
from homeassistant.util.read_only_dict import ReadOnlyDict
|
||||
from homeassistant.util.yaml import dumper
|
||||
|
||||
from . import config_validation as cv
|
||||
@@ -66,12 +67,11 @@ class Selector[_T: Mapping[str, Any]]:
|
||||
# context for filtering for example. The selector defines
|
||||
# which context keys it supports and what selector types
|
||||
# are allowed for each key.
|
||||
allowed_context_keys: dict[str, set[str]]
|
||||
allowed_context_keys: Mapping[str, frozenset[str]] = ReadOnlyDict({})
|
||||
|
||||
def __init__(self, config: Mapping[str, Any] | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
self.config = self.CONFIG_SCHEMA(config)
|
||||
self.allowed_context_keys = {}
|
||||
|
||||
@override
|
||||
def __eq__(self, other: object) -> bool:
|
||||
@@ -441,6 +441,13 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
|
||||
|
||||
selector_type = "attribute"
|
||||
|
||||
allowed_context_keys = ReadOnlyDict(
|
||||
{
|
||||
# Filters the available attributes based on the selected entity
|
||||
"filter_entity": frozenset({"entity"})
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = make_selector_config_schema(
|
||||
{
|
||||
probatio.Required("entity_id"): cv.entity_id,
|
||||
@@ -453,10 +460,6 @@ class AttributeSelector(Selector[AttributeSelectorConfig]):
|
||||
def __init__(self, config: AttributeSelectorConfig) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
self.allowed_context_keys = {
|
||||
# Filters the available attributes based on the selected entity
|
||||
"filter_entity": {"entity"}
|
||||
}
|
||||
|
||||
def __call__(self, data: Any) -> str:
|
||||
"""Validate the passed selection."""
|
||||
@@ -1374,6 +1377,13 @@ class MediaSelector(Selector[MediaSelectorConfig]):
|
||||
|
||||
selector_type = "media"
|
||||
|
||||
allowed_context_keys = ReadOnlyDict(
|
||||
{
|
||||
# Filters the available media based on the selected entity
|
||||
"filter_entity": frozenset({EntitySelector.selector_type})
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = probatio.All(
|
||||
make_selector_config_schema(
|
||||
{
|
||||
@@ -1400,10 +1410,6 @@ class MediaSelector(Selector[MediaSelectorConfig]):
|
||||
def __init__(self, config: MediaSelectorConfig | None = None) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
self.allowed_context_keys = {
|
||||
# Filters the available media based on the selected entity
|
||||
"filter_entity": {EntitySelector.selector_type}
|
||||
}
|
||||
|
||||
def __call__(self, data: Any) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
"""Validate the passed selection."""
|
||||
@@ -2071,6 +2077,17 @@ class StateSelector(Selector[StateSelectorConfig]):
|
||||
|
||||
selector_type = "state"
|
||||
|
||||
allowed_context_keys = ReadOnlyDict(
|
||||
{
|
||||
# Filters the available states based on the selected entity
|
||||
"filter_entity": frozenset({EntitySelector.selector_type}),
|
||||
# Filters the available states based on the selected target
|
||||
"filter_target": frozenset({"target"}),
|
||||
# Only show the attribute values of a specific attribute
|
||||
"filter_attribute": frozenset({AttributeSelector.selector_type}),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = make_selector_config_schema(
|
||||
{
|
||||
probatio.Optional("entity_id"): cv.entity_id,
|
||||
@@ -2083,14 +2100,6 @@ class StateSelector(Selector[StateSelectorConfig]):
|
||||
def __init__(self, config: StateSelectorConfig) -> None:
|
||||
"""Instantiate a selector."""
|
||||
super().__init__(config)
|
||||
self.allowed_context_keys = {
|
||||
# Filters the available states based on the selected entity
|
||||
"filter_entity": {EntitySelector.selector_type},
|
||||
# Filters the available states based on the selected target
|
||||
"filter_target": {"target"},
|
||||
# Only show the attribute values of a specific attribute
|
||||
"filter_attribute": {AttributeSelector.selector_type},
|
||||
}
|
||||
|
||||
def __call__(self, data: Any) -> str | list[str]:
|
||||
"""Validate the passed selection."""
|
||||
|
||||
@@ -44,28 +44,37 @@ def test_invalid_base_schema(schema) -> None:
|
||||
selector.validate_selector(schema)
|
||||
|
||||
|
||||
def test_allowed_context_keys_not_shared_between_instances() -> None:
|
||||
"""Test allowed_context_keys is isolated between selector instances."""
|
||||
@pytest.mark.parametrize(
|
||||
"selector_class",
|
||||
[
|
||||
pytest.param(selector.Selector, id="base"),
|
||||
pytest.param(selector.AttributeSelector, id="attribute"),
|
||||
pytest.param(selector.MediaSelector, id="media"),
|
||||
pytest.param(selector.StateSelector, id="state"),
|
||||
],
|
||||
)
|
||||
def test_allowed_context_keys_read_only(
|
||||
selector_class: type[selector.Selector],
|
||||
) -> None:
|
||||
"""Test allowed_context_keys cannot be modified."""
|
||||
with pytest.raises(RuntimeError, match="Cannot modify ReadOnlyDict"):
|
||||
selector_class.allowed_context_keys["some_key"] = frozenset()
|
||||
|
||||
class TestSelectorConfig(selector.BaseSelectorConfig, total=False):
|
||||
"""Test selector config class."""
|
||||
|
||||
class TestSelector(selector.Selector):
|
||||
"""Test selector used to verify instance isolation."""
|
||||
|
||||
CONFIG_SCHEMA = selector.make_selector_config_schema({})
|
||||
|
||||
selector_type = "test"
|
||||
|
||||
def __call__(self, data: Any) -> Any:
|
||||
"""Validate the passed selection."""
|
||||
return data
|
||||
|
||||
test_selector = TestSelector(TestSelectorConfig())
|
||||
other_selector = TestSelector(TestSelectorConfig())
|
||||
test_selector.allowed_context_keys["some_key"] = set()
|
||||
assert test_selector.allowed_context_keys
|
||||
assert not other_selector.allowed_context_keys
|
||||
@pytest.mark.parametrize(
|
||||
"selector_class",
|
||||
[
|
||||
pytest.param(selector.AttributeSelector, id="attribute"),
|
||||
pytest.param(selector.MediaSelector, id="media"),
|
||||
pytest.param(selector.StateSelector, id="state"),
|
||||
],
|
||||
)
|
||||
def test_allowed_context_keys_values_immutable(
|
||||
selector_class: type[selector.Selector],
|
||||
) -> None:
|
||||
"""Test allowed_context_keys values cannot be modified."""
|
||||
for allowed_types in selector_class.allowed_context_keys.values():
|
||||
assert isinstance(allowed_types, frozenset)
|
||||
|
||||
|
||||
def _test_selector(
|
||||
|
||||
Reference in New Issue
Block a user