Use !input instead of !placeholder (#43820)

* Use !input instead of !placeholder

* Update input name

* Lint

* Move tests around
This commit is contained in:
Paulus Schoutsen 2020-12-01 18:21:36 +01:00 committed by GitHub
parent 7d23ff6511
commit 1c9c99571e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 148 additions and 139 deletions

View File

@ -26,7 +26,7 @@
"files.trimTrailingWhitespace": true,
"terminal.integrated.shell.linux": "/bin/bash",
"yaml.customTags": [
"!placeholder scalar",
"!input scalar",
"!secret scalar",
"!include_dir_named scalar",
"!include_dir_list scalar",

View File

@ -31,18 +31,18 @@ max_exceeded: silent
trigger:
platform: state
entity_id: !placeholder motion_entity
entity_id: !input motion_entity
from: "off"
to: "on"
action:
- service: homeassistant.turn_on
target: !placeholder light_target
target: !input light_target
- wait_for_trigger:
platform: state
entity_id: !placeholder motion_entity
entity_id: !input motion_entity
from: "on"
to: "off"
- delay: !placeholder no_motion_wait
- delay: !input no_motion_wait
- service: homeassistant.turn_off
target: !placeholder light_target
target: !input light_target

View File

@ -18,10 +18,10 @@ blueprint:
trigger:
platform: state
entity_id: !placeholder person_entity
entity_id: !input person_entity
variables:
zone_entity: !placeholder zone_entity
zone_entity: !input zone_entity
zone_state: "{{ states[zone_entity].name }}"
condition:
@ -29,6 +29,6 @@ condition:
value_template: "{{ trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}"
action:
- service: !placeholder notify_service
- service: !input notify_service
data:
message: "{{ trigger.to_state.name }} has left {{ zone_state }}"

View File

@ -7,7 +7,7 @@ from .errors import ( # noqa
FailedToLoad,
InvalidBlueprint,
InvalidBlueprintInputs,
MissingPlaceholder,
MissingInput,
)
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa
from .schemas import is_blueprint_instance_config # noqa

View File

@ -66,17 +66,17 @@ class InvalidBlueprintInputs(BlueprintException):
)
class MissingPlaceholder(BlueprintWithNameException):
"""When we miss a placeholder."""
class MissingInput(BlueprintWithNameException):
"""When we miss an input."""
def __init__(
self, domain: str, blueprint_name: str, placeholder_names: Iterable[str]
self, domain: str, blueprint_name: str, input_names: Iterable[str]
) -> None:
"""Initialize blueprint exception."""
super().__init__(
domain,
blueprint_name,
f"Missing placeholder {', '.join(sorted(placeholder_names))}",
f"Missing input {', '.join(sorted(input_names))}",
)

View File

@ -19,7 +19,6 @@ from homeassistant.const import (
)
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import placeholder
from homeassistant.util import yaml
from .const import (
@ -38,7 +37,7 @@ from .errors import (
FileAlreadyExists,
InvalidBlueprint,
InvalidBlueprintInputs,
MissingPlaceholder,
MissingInput,
)
from .schemas import BLUEPRINT_INSTANCE_FIELDS, BLUEPRINT_SCHEMA
@ -59,8 +58,6 @@ class Blueprint:
except vol.Invalid as err:
raise InvalidBlueprint(expected_domain, path, data, err) from err
self.placeholders = placeholder.extract_placeholders(data)
# In future, we will treat this as "incorrect" and allow to recover from this
data_domain = data[CONF_BLUEPRINT][CONF_DOMAIN]
if expected_domain is not None and data_domain != expected_domain:
@ -73,7 +70,7 @@ class Blueprint:
self.domain = data_domain
missing = self.placeholders - set(data[CONF_BLUEPRINT][CONF_INPUT])
missing = yaml.extract_inputs(data) - set(data[CONF_BLUEPRINT][CONF_INPUT])
if missing:
raise InvalidBlueprint(
@ -143,7 +140,7 @@ class BlueprintInputs:
@property
def inputs_with_default(self):
"""Return the inputs and fallback to defaults."""
no_input = self.blueprint.placeholders - set(self.inputs)
no_input = set(self.blueprint.inputs) - set(self.inputs)
inputs_with_default = dict(self.inputs)
@ -156,12 +153,10 @@ class BlueprintInputs:
def validate(self) -> None:
"""Validate the inputs."""
missing = self.blueprint.placeholders - set(self.inputs_with_default)
missing = set(self.blueprint.inputs) - set(self.inputs_with_default)
if missing:
raise MissingPlaceholder(
self.blueprint.domain, self.blueprint.name, missing
)
raise MissingInput(self.blueprint.domain, self.blueprint.name, missing)
# In future we can see if entities are correct domain, areas exist etc
# using the new selector helper.
@ -169,9 +164,7 @@ class BlueprintInputs:
@callback
def async_substitute(self) -> dict:
"""Get the blueprint value with the inputs substituted."""
processed = placeholder.substitute(
self.blueprint.data, self.inputs_with_default
)
processed = yaml.substitute(self.blueprint.data, self.inputs_with_default)
combined = {**processed, **self.config_with_inputs}
# From config_with_inputs
combined.pop(CONF_USE_BLUEPRINT)

View File

@ -1,17 +1,21 @@
"""YAML utility functions."""
from .const import _SECRET_NAMESPACE, SECRET_YAML
from .dumper import dump, save_yaml
from .input import UndefinedSubstitution, extract_inputs, substitute
from .loader import clear_secret_cache, load_yaml, parse_yaml, secret_yaml
from .objects import Placeholder
from .objects import Input
__all__ = [
"SECRET_YAML",
"_SECRET_NAMESPACE",
"Placeholder",
"Input",
"dump",
"save_yaml",
"clear_secret_cache",
"load_yaml",
"secret_yaml",
"parse_yaml",
"UndefinedSubstitution",
"extract_inputs",
"substitute",
]

View File

@ -3,7 +3,7 @@ from collections import OrderedDict
import yaml
from .objects import NodeListClass, Placeholder
from .objects import Input, NodeListClass
# mypy: allow-untyped-calls, no-warn-return-any
@ -62,6 +62,6 @@ yaml.SafeDumper.add_representer(
)
yaml.SafeDumper.add_representer(
Placeholder,
lambda dumper, value: dumper.represent_scalar("!placeholder", value.name),
Input,
lambda dumper, value: dumper.represent_scalar("!input", value.name),
)

View File

@ -1,45 +1,46 @@
"""Placeholder helpers."""
"""Deal with YAML input."""
from typing import Any, Dict, Set
from homeassistant.util.yaml import Placeholder
from .objects import Input
class UndefinedSubstitution(Exception):
"""Error raised when we find a substitution that is not defined."""
def __init__(self, placeholder: str) -> None:
def __init__(self, input_name: str) -> None:
"""Initialize the undefined substitution exception."""
super().__init__(f"No substitution found for placeholder {placeholder}")
self.placeholder = placeholder
super().__init__(f"No substitution found for input {input_name}")
self.input = input
def extract_placeholders(obj: Any) -> Set[str]:
"""Extract placeholders from a structure."""
def extract_inputs(obj: Any) -> Set[str]:
"""Extract input from a structure."""
found: Set[str] = set()
_extract_placeholders(obj, found)
_extract_inputs(obj, found)
return found
def _extract_placeholders(obj: Any, found: Set[str]) -> None:
"""Extract placeholders from a structure."""
if isinstance(obj, Placeholder):
def _extract_inputs(obj: Any, found: Set[str]) -> None:
"""Extract input from a structure."""
if isinstance(obj, Input):
found.add(obj.name)
return
if isinstance(obj, list):
for val in obj:
_extract_placeholders(val, found)
_extract_inputs(val, found)
return
if isinstance(obj, dict):
for val in obj.values():
_extract_placeholders(val, found)
_extract_inputs(val, found)
return
def substitute(obj: Any, substitutions: Dict[str, Any]) -> Any:
"""Substitute values."""
if isinstance(obj, Placeholder):
if isinstance(obj, Input):
if obj.name not in substitutions:
raise UndefinedSubstitution(obj.name)
return substitutions[obj.name]

View File

@ -11,7 +11,7 @@ import yaml
from homeassistant.exceptions import HomeAssistantError
from .const import _SECRET_NAMESPACE, SECRET_YAML
from .objects import NodeListClass, NodeStrClass, Placeholder
from .objects import Input, NodeListClass, NodeStrClass
try:
import keyring
@ -331,4 +331,4 @@ yaml.SafeLoader.add_constructor("!include_dir_named", _include_dir_named_yaml)
yaml.SafeLoader.add_constructor(
"!include_dir_merge_named", _include_dir_merge_named_yaml
)
yaml.SafeLoader.add_constructor("!placeholder", Placeholder.from_node)
yaml.SafeLoader.add_constructor("!input", Input.from_node)

View File

@ -13,12 +13,12 @@ class NodeStrClass(str):
@dataclass(frozen=True)
class Placeholder:
"""A placeholder that should be substituted."""
class Input:
"""Input that should be substituted."""
name: str
@classmethod
def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Placeholder":
def from_node(cls, loader: yaml.Loader, node: yaml.nodes.Node) -> "Input":
"""Create a new placeholder from a node."""
return cls(node.value)

View File

@ -57,9 +57,9 @@ def test_extract_blueprint_from_community_topic(community_post):
)
assert imported_blueprint is not None
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == {
"service_to_call",
"trigger_event",
assert imported_blueprint.blueprint.inputs == {
"service_to_call": None,
"trigger_event": None,
}
@ -103,9 +103,9 @@ async def test_fetch_blueprint_from_community_url(hass, aioclient_mock, communit
)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == {
"service_to_call",
"trigger_event",
assert imported_blueprint.blueprint.inputs == {
"service_to_call": None,
"trigger_event": None,
}
assert imported_blueprint.suggested_filename == "balloob/test-topic"
assert (
@ -133,9 +133,9 @@ async def test_fetch_blueprint_from_github_url(hass, aioclient_mock, url):
imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == {
"service_to_call",
"trigger_event",
assert imported_blueprint.blueprint.inputs == {
"service_to_call": None,
"trigger_event": None,
}
assert imported_blueprint.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url
@ -152,9 +152,14 @@ async def test_fetch_blueprint_from_github_gist_url(hass, aioclient_mock):
imported_blueprint = await importer.fetch_blueprint_from_url(hass, url)
assert isinstance(imported_blueprint, importer.ImportedBlueprint)
assert imported_blueprint.blueprint.domain == "automation"
assert imported_blueprint.blueprint.placeholders == {
"motion_entity",
"light_entity",
assert imported_blueprint.blueprint.inputs == {
"motion_entity": {
"name": "Motion Sensor",
"selector": {
"entity": {"domain": "binary_sensor", "device_class": "motion"}
},
},
"light_entity": {"name": "Light", "selector": {"entity": {"domain": "light"}}},
}
assert imported_blueprint.suggested_filename == "balloob/motion_light"
assert imported_blueprint.blueprint.metadata["source_url"] == url

View File

@ -4,7 +4,7 @@ import logging
import pytest
from homeassistant.components.blueprint import errors, models
from homeassistant.util.yaml import Placeholder
from homeassistant.util.yaml import Input
from tests.async_mock import patch
@ -18,18 +18,16 @@ def blueprint_1():
"name": "Hello",
"domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
"input": {
"test-placeholder": {"name": "Name", "description": "Description"}
},
"input": {"test-input": {"name": "Name", "description": "Description"}},
},
"example": Placeholder("test-placeholder"),
"example": Input("test-input"),
}
)
@pytest.fixture
def blueprint_2():
"""Blueprint fixture with default placeholder."""
"""Blueprint fixture with default inputs."""
return models.Blueprint(
{
"blueprint": {
@ -37,12 +35,12 @@ def blueprint_2():
"domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
"input": {
"test-placeholder": {"name": "Name", "description": "Description"},
"test-placeholder-default": {"default": "test"},
"test-input": {"name": "Name", "description": "Description"},
"test-input-default": {"default": "test"},
},
},
"example": Placeholder("test-placeholder"),
"example-default": Placeholder("test-placeholder-default"),
"example": Input("test-input"),
"example-default": Input("test-input-default"),
}
)
@ -72,7 +70,7 @@ def test_blueprint_model_init():
"domain": "automation",
"input": {"something": None},
},
"trigger": {"platform": Placeholder("non-existing")},
"trigger": {"platform": Input("non-existing")},
}
)
@ -83,11 +81,13 @@ def test_blueprint_properties(blueprint_1):
"name": "Hello",
"domain": "automation",
"source_url": "https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml",
"input": {"test-placeholder": {"name": "Name", "description": "Description"}},
"input": {"test-input": {"name": "Name", "description": "Description"}},
}
assert blueprint_1.domain == "automation"
assert blueprint_1.name == "Hello"
assert blueprint_1.placeholders == {"test-placeholder"}
assert blueprint_1.inputs == {
"test-input": {"name": "Name", "description": "Description"}
}
def test_blueprint_update_metadata():
@ -140,13 +140,13 @@ def test_blueprint_inputs(blueprint_2):
{
"use_blueprint": {
"path": "bla",
"input": {"test-placeholder": 1, "test-placeholder-default": 12},
"input": {"test-input": 1, "test-input-default": 12},
},
"example-default": {"overridden": "via-config"},
},
)
inputs.validate()
assert inputs.inputs == {"test-placeholder": 1, "test-placeholder-default": 12}
assert inputs.inputs == {"test-input": 1, "test-input-default": 12}
assert inputs.async_substitute() == {
"example": 1,
"example-default": {"overridden": "via-config"},
@ -159,7 +159,7 @@ def test_blueprint_inputs_validation(blueprint_1):
blueprint_1,
{"use_blueprint": {"path": "bla", "input": {"non-existing-placeholder": 1}}},
)
with pytest.raises(errors.MissingPlaceholder):
with pytest.raises(errors.MissingInput):
inputs.validate()
@ -167,13 +167,13 @@ def test_blueprint_inputs_default(blueprint_2):
"""Test blueprint inputs."""
inputs = models.BlueprintInputs(
blueprint_2,
{"use_blueprint": {"path": "bla", "input": {"test-placeholder": 1}}},
{"use_blueprint": {"path": "bla", "input": {"test-input": 1}}},
)
inputs.validate()
assert inputs.inputs == {"test-placeholder": 1}
assert inputs.inputs == {"test-input": 1}
assert inputs.inputs_with_default == {
"test-placeholder": 1,
"test-placeholder-default": "test",
"test-input": 1,
"test-input-default": "test",
}
assert inputs.async_substitute() == {"example": 1, "example-default": "test"}
@ -185,18 +185,18 @@ def test_blueprint_inputs_override_default(blueprint_2):
{
"use_blueprint": {
"path": "bla",
"input": {"test-placeholder": 1, "test-placeholder-default": "custom"},
"input": {"test-input": 1, "test-input-default": "custom"},
}
},
)
inputs.validate()
assert inputs.inputs == {
"test-placeholder": 1,
"test-placeholder-default": "custom",
"test-input": 1,
"test-input-default": "custom",
}
assert inputs.inputs_with_default == {
"test-placeholder": 1,
"test-placeholder-default": "custom",
"test-input": 1,
"test-input-default": "custom",
}
assert inputs.async_substitute() == {"example": 1, "example-default": "custom"}
@ -238,7 +238,7 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1):
with pytest.raises(errors.InvalidBlueprintInputs):
await domain_bps.async_inputs_from_config({"not-referencing": "use_blueprint"})
with pytest.raises(errors.MissingPlaceholder), patch.object(
with pytest.raises(errors.MissingInput), patch.object(
domain_bps, "async_get_blueprint", return_value=blueprint_1
):
await domain_bps.async_inputs_from_config(
@ -247,10 +247,10 @@ async def test_domain_blueprints_inputs_from_config(domain_bps, blueprint_1):
with patch.object(domain_bps, "async_get_blueprint", return_value=blueprint_1):
inputs = await domain_bps.async_inputs_from_config(
{"use_blueprint": {"path": "bla.yaml", "input": {"test-placeholder": None}}}
{"use_blueprint": {"path": "bla.yaml", "input": {"test-input": None}}}
)
assert inputs.blueprint is blueprint_1
assert inputs.inputs == {"test-placeholder": None}
assert inputs.inputs == {"test-input": None}
async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1):

View File

@ -124,7 +124,7 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client):
assert msg["success"]
assert write_mock.mock_calls
assert write_mock.call_args[0] == (
"blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !placeholder 'trigger_event'\naction:\n service: !placeholder 'service_to_call'\n",
"blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n service_to_call:\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n",
)

View File

@ -7,7 +7,7 @@
"username": "balloob",
"avatar_template": "/user_avatar/community.home-assistant.io/balloob/{size}/21956_2.png",
"created_at": "2020-10-16T12:20:12.688Z",
"cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !placeholder trigger_event\naction:\n service: !placeholder service_to_call\n\u003c/code\u003e\u003c/pre\u003e",
"cooked": "\u003cp\u003ehere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003cbr\u003e\nhere a test topic.\u003c/p\u003e\n\u003ch1\u003eBlock without syntax\u003c/h1\u003e\n\u003cpre\u003e\u003ccode class=\"lang-auto\"\u003eblueprint:\n domain: automation\n name: Example Blueprint from post\n input:\n trigger_event:\n service_to_call:\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n\u003c/code\u003e\u003c/pre\u003e",
"post_number": 1,
"post_type": 1,
"updated_at": "2020-10-20T08:24:14.189Z",

View File

@ -15,7 +15,7 @@
"raw_url": "https://gist.githubusercontent.com/balloob/e717ce85dd0d2f1bdcdfc884ea25a344/raw/d3cede19ffed75443934325177cd78d26b1700ad/motion_light.yaml",
"size": 803,
"truncated": false,
"content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !placeholder light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !placeholder motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !placeholder light_entity\n"
"content": "blueprint:\n name: Motion-activated Light\n domain: automation\n input:\n motion_entity:\n name: Motion Sensor\n selector:\n entity:\n domain: binary_sensor\n device_class: motion\n light_entity:\n name: Light\n selector:\n entity:\n domain: light\n\n# If motion is detected within the 120s delay,\n# we restart the script.\nmode: restart\nmax_exceeded: silent\n\ntrigger:\n platform: state\n entity_id: !input motion_entity\n from: \"off\"\n to: \"on\"\n\naction:\n - service: homeassistant.turn_on\n entity_id: !input light_entity\n - wait_for_trigger:\n platform: state\n entity_id: !input motion_entity\n from: \"on\"\n to: \"off\"\n - delay: 120\n - service: homeassistant.turn_off\n entity_id: !input light_entity\n"
}
},
"public": false,

View File

@ -159,9 +159,9 @@ blueprint:
service_to_call:
trigger:
platform: event
event_type: !placeholder trigger_event
event_type: !input trigger_event
action:
service: !placeholder service_to_call
service: !input service_to_call
""",
}
with patch("os.path.isfile", return_value=True), patch_yaml_files(files):

View File

@ -1,29 +0,0 @@
"""Test placeholders."""
import pytest
from homeassistant.helpers import placeholder
from homeassistant.util.yaml import Placeholder
def test_extract_placeholders():
"""Test extracting placeholders from data."""
assert placeholder.extract_placeholders(Placeholder("hello")) == {"hello"}
assert placeholder.extract_placeholders(
{"info": [1, Placeholder("hello"), 2, Placeholder("world")]}
) == {"hello", "world"}
def test_substitute():
"""Test we can substitute."""
assert placeholder.substitute(Placeholder("hello"), {"hello": 5}) == 5
with pytest.raises(placeholder.UndefinedSubstitution):
placeholder.substitute(Placeholder("hello"), {})
assert (
placeholder.substitute(
{"info": [1, Placeholder("hello"), 2, Placeholder("world")]},
{"hello": 5, "world": 10},
)
== {"info": [1, 5, 2, 10]}
)

View File

@ -4,5 +4,5 @@ blueprint:
input:
trigger:
action:
trigger: !placeholder trigger
action: !placeholder action
trigger: !input trigger
action: !input action

View File

@ -6,6 +6,6 @@ blueprint:
service_to_call:
trigger:
platform: event
event_type: !placeholder trigger_event
event_type: !input trigger_event
action:
service: !placeholder service_to_call
service: !input service_to_call

View File

@ -0,0 +1 @@
"""Tests for YAML util."""

View File

@ -463,18 +463,18 @@ def test_duplicate_key(caplog):
assert "contains duplicate key" in caplog.text
def test_placeholder_class():
"""Test placeholder class."""
placeholder = yaml_loader.Placeholder("hello")
placeholder2 = yaml_loader.Placeholder("hello")
def test_input_class():
"""Test input class."""
input = yaml_loader.Input("hello")
input2 = yaml_loader.Input("hello")
assert placeholder.name == "hello"
assert placeholder == placeholder2
assert input.name == "hello"
assert input == input2
assert len({placeholder, placeholder2}) == 1
assert len({input, input2}) == 1
def test_placeholder():
"""Test loading placeholders."""
data = {"hello": yaml.Placeholder("test_name")}
def test_input():
"""Test loading inputs."""
data = {"hello": yaml.Input("test_name")}
assert yaml.parse_yaml(yaml.dump(data)) == data

View File

@ -0,0 +1,34 @@
"""Test inputs."""
import pytest
from homeassistant.util.yaml import (
Input,
UndefinedSubstitution,
extract_inputs,
substitute,
)
def test_extract_inputs():
"""Test extracting inputs from data."""
assert extract_inputs(Input("hello")) == {"hello"}
assert extract_inputs({"info": [1, Input("hello"), 2, Input("world")]}) == {
"hello",
"world",
}
def test_substitute():
"""Test we can substitute."""
assert substitute(Input("hello"), {"hello": 5}) == 5
with pytest.raises(UndefinedSubstitution):
substitute(Input("hello"), {})
assert (
substitute(
{"info": [1, Input("hello"), 2, Input("world")]},
{"hello": 5, "world": 10},
)
== {"info": [1, 5, 2, 10]}
)