Fix config modules being imported in the event loop (#112462)

* Fix config modules being imported in the event loop

There was a late import in this integration because of the circular import.

The code has been rearranged to avoid the circular imports

* fixes

* fixes

* fix patching

* make eager

* remove unrelated change from this branch
pull/112472/head
J. Nick Koston 2024-03-05 16:18:57 -10:00 committed by GitHub
parent f03be2fd9e
commit 3f9dbd3e25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 317 additions and 287 deletions

View File

@ -1,48 +1,44 @@
"""Component to configure Home Assistant via an API."""
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from http import HTTPStatus
import importlib
import os
from typing import Any, Generic, TypeVar, cast
from aiohttp import web
import voluptuous as vol
from homeassistant.components import frontend
from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED
from homeassistant.const import EVENT_COMPONENT_LOADED
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import ATTR_COMPONENT
from homeassistant.util.file import write_utf8_file_atomic
from homeassistant.util.yaml import dump, load_yaml
from homeassistant.util.yaml.loader import JSON_TYPE
_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]])
DOMAIN = "config"
from . import (
area_registry,
auth,
auth_provider_homeassistant,
automation,
config_entries,
core,
device_registry,
entity_registry,
floor_registry,
label_registry,
scene,
script,
)
from .const import DOMAIN
SECTIONS = (
"area_registry",
"auth",
"auth_provider_homeassistant",
"automation",
"config_entries",
"core",
"device_registry",
"entity_registry",
"floor_registry",
"label_registry",
"script",
"scene",
area_registry,
auth,
auth_provider_homeassistant,
automation,
config_entries,
core,
device_registry,
entity_registry,
floor_registry,
label_registry,
script,
scene,
)
ACTION_CREATE_UPDATE = "create_update"
ACTION_DELETE = "delete"
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@ -53,231 +49,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, "config", "config", "hass:cog", require_admin=True
)
for panel_name in SECTIONS:
panel = importlib.import_module(f".{panel_name}", __name__)
for panel in SECTIONS:
if panel.async_setup(hass):
key = f"{DOMAIN}.{panel_name}"
name = panel.__name__.split(".")[-1]
key = f"{DOMAIN}.{name}"
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key})
return True
class BaseEditConfigView(HomeAssistantView, Generic[_DataT]):
"""Configure a Group endpoint."""
def __init__(
self,
component: str,
config_type: str,
path: str,
key_schema: Callable[[Any], str],
data_schema: Callable[[dict[str, Any]], Any],
*,
post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None,
data_validator: Callable[
[HomeAssistant, str, dict[str, Any]],
Coroutine[Any, Any, dict[str, Any] | None],
]
| None = None,
) -> None:
"""Initialize a config view."""
self.url = f"/api/config/{component}/{config_type}/{{config_key}}"
self.name = f"api:config:{component}:{config_type}"
self.path = path
self.key_schema = key_schema
self.data_schema = data_schema
self.post_write_hook = post_write_hook
self.data_validator = data_validator
self.mutation_lock = asyncio.Lock()
def _empty_config(self) -> _DataT:
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(
self, hass: HomeAssistant, data: _DataT, config_key: str
) -> dict[str, Any] | None:
"""Get value."""
raise NotImplementedError
def _write_value(
self,
hass: HomeAssistant,
data: _DataT,
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
raise NotImplementedError
def _delete_value(
self, hass: HomeAssistant, data: _DataT, config_key: str
) -> dict[str, Any] | None:
"""Delete value."""
raise NotImplementedError
@require_admin
async def get(self, request: web.Request, config_key: str) -> web.Response:
"""Fetch device specific config."""
hass: HomeAssistant = request.app["hass"]
async with self.mutation_lock:
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
if value is None:
return self.json_message("Resource not found", HTTPStatus.NOT_FOUND)
return self.json(value)
@require_admin
async def post(self, request: web.Request, config_key: str) -> web.Response:
"""Validate config and return results."""
try:
data = await request.json()
except ValueError:
return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST)
try:
self.key_schema(config_key)
except vol.Invalid as err:
return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST)
hass: HomeAssistant = request.app["hass"]
try:
# We just validate, we don't store that data because
# we don't want to store the defaults.
if self.data_validator:
await self.data_validator(hass, config_key, data)
else:
self.data_schema(data)
except (vol.Invalid, HomeAssistantError) as err:
return self.json_message(
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
)
path = hass.config.path(self.path)
async with self.mutation_lock:
current = await self.read_config(hass)
self._write_value(hass, current, config_key, data)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(
self.post_write_hook(ACTION_CREATE_UPDATE, config_key)
)
return self.json({"result": "ok"})
@require_admin
async def delete(self, request: web.Request, config_key: str) -> web.Response:
"""Remove an entry."""
hass: HomeAssistant = request.app["hass"]
async with self.mutation_lock:
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
path = hass.config.path(self.path)
if value is None:
return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST)
self._delete_value(hass, current, config_key)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
return self.json({"result": "ok"})
async def read_config(self, hass: HomeAssistant) -> _DataT:
"""Read the config."""
current = await hass.async_add_executor_job(_read, hass.config.path(self.path))
if not current:
current = self._empty_config()
return cast(_DataT, current)
class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]):
"""Configure a list of entries."""
def _empty_config(self) -> dict[str, Any]:
"""Return an empty config."""
return {}
def _get_value(
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
) -> dict[str, Any] | None:
"""Get value."""
return data.get(config_key)
def _write_value(
self,
hass: HomeAssistant,
data: dict[str, dict[str, Any]],
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
def _delete_value(
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
) -> dict[str, Any]:
"""Delete value."""
return data.pop(config_key)
class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]):
"""Configure key based config entries."""
def _empty_config(self) -> list[Any]:
"""Return an empty config."""
return []
def _get_value(
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
) -> dict[str, Any] | None:
"""Get value."""
return next((val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(
self,
hass: HomeAssistant,
data: list[dict[str, Any]],
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
if (value := self._get_value(hass, data, config_key)) is None:
value = {CONF_ID: config_key}
data.append(value)
value.update(new_value)
def _delete_value(
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
) -> None:
"""Delete value."""
index = next(
idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key
)
data.pop(index)
def _read(path: str) -> JSON_TYPE | None:
"""Read YAML helper."""
if not os.path.isfile(path):
return None
return load_yaml(path)
def _write(path: str, data: dict | list) -> None:
"""Write YAML helper."""
# Do it before opening file. If dump causes error it will now not
# truncate the file.
contents = dump(data)
write_utf8_file_atomic(path, contents)

View File

@ -14,7 +14,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from . import ACTION_DELETE, EditIdBasedConfigView
from .const import ACTION_DELETE
from .view import EditIdBasedConfigView
@callback

View File

@ -0,0 +1,5 @@
"""Constants for config."""
ACTION_CREATE_UPDATE = "create_update"
ACTION_DELETE = "delete"
DOMAIN = "config"

View File

@ -10,7 +10,8 @@ from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from . import ACTION_DELETE, EditIdBasedConfigView
from .const import ACTION_DELETE
from .view import EditIdBasedConfigView
@callback

View File

@ -13,7 +13,8 @@ from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_registry as er
from . import ACTION_DELETE, EditKeyBasedConfigView
from .const import ACTION_DELETE
from .view import EditKeyBasedConfigView
@callback

View File

@ -0,0 +1,243 @@
"""Component to configure Home Assistant via an API."""
from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine
from http import HTTPStatus
import os
from typing import Any, Generic, TypeVar, cast
from aiohttp import web
import voluptuous as vol
from homeassistant.components.http import HomeAssistantView, require_admin
from homeassistant.const import CONF_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.file import write_utf8_file_atomic
from homeassistant.util.yaml import dump, load_yaml
from homeassistant.util.yaml.loader import JSON_TYPE
from .const import ACTION_CREATE_UPDATE, ACTION_DELETE
_DataT = TypeVar("_DataT", dict[str, dict[str, Any]], list[dict[str, Any]])
class BaseEditConfigView(HomeAssistantView, Generic[_DataT]):
"""Configure a Group endpoint."""
def __init__(
self,
component: str,
config_type: str,
path: str,
key_schema: Callable[[Any], str],
data_schema: Callable[[dict[str, Any]], Any],
*,
post_write_hook: Callable[[str, str], Coroutine[Any, Any, None]] | None = None,
data_validator: Callable[
[HomeAssistant, str, dict[str, Any]],
Coroutine[Any, Any, dict[str, Any] | None],
]
| None = None,
) -> None:
"""Initialize a config view."""
self.url = f"/api/config/{component}/{config_type}/{{config_key}}"
self.name = f"api:config:{component}:{config_type}"
self.path = path
self.key_schema = key_schema
self.data_schema = data_schema
self.post_write_hook = post_write_hook
self.data_validator = data_validator
self.mutation_lock = asyncio.Lock()
def _empty_config(self) -> _DataT:
"""Empty config if file not found."""
raise NotImplementedError
def _get_value(
self, hass: HomeAssistant, data: _DataT, config_key: str
) -> dict[str, Any] | None:
"""Get value."""
raise NotImplementedError
def _write_value(
self,
hass: HomeAssistant,
data: _DataT,
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
raise NotImplementedError
def _delete_value(
self, hass: HomeAssistant, data: _DataT, config_key: str
) -> dict[str, Any] | None:
"""Delete value."""
raise NotImplementedError
@require_admin
async def get(self, request: web.Request, config_key: str) -> web.Response:
"""Fetch device specific config."""
hass: HomeAssistant = request.app["hass"]
async with self.mutation_lock:
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
if value is None:
return self.json_message("Resource not found", HTTPStatus.NOT_FOUND)
return self.json(value)
@require_admin
async def post(self, request: web.Request, config_key: str) -> web.Response:
"""Validate config and return results."""
try:
data = await request.json()
except ValueError:
return self.json_message("Invalid JSON specified", HTTPStatus.BAD_REQUEST)
try:
self.key_schema(config_key)
except vol.Invalid as err:
return self.json_message(f"Key malformed: {err}", HTTPStatus.BAD_REQUEST)
hass: HomeAssistant = request.app["hass"]
try:
# We just validate, we don't store that data because
# we don't want to store the defaults.
if self.data_validator:
await self.data_validator(hass, config_key, data)
else:
self.data_schema(data)
except (vol.Invalid, HomeAssistantError) as err:
return self.json_message(
f"Message malformed: {err}", HTTPStatus.BAD_REQUEST
)
path = hass.config.path(self.path)
async with self.mutation_lock:
current = await self.read_config(hass)
self._write_value(hass, current, config_key, data)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(
self.post_write_hook(ACTION_CREATE_UPDATE, config_key)
)
return self.json({"result": "ok"})
@require_admin
async def delete(self, request: web.Request, config_key: str) -> web.Response:
"""Remove an entry."""
hass: HomeAssistant = request.app["hass"]
async with self.mutation_lock:
current = await self.read_config(hass)
value = self._get_value(hass, current, config_key)
path = hass.config.path(self.path)
if value is None:
return self.json_message("Resource not found", HTTPStatus.BAD_REQUEST)
self._delete_value(hass, current, config_key)
await hass.async_add_executor_job(_write, path, current)
if self.post_write_hook is not None:
hass.async_create_task(self.post_write_hook(ACTION_DELETE, config_key))
return self.json({"result": "ok"})
async def read_config(self, hass: HomeAssistant) -> _DataT:
"""Read the config."""
current = await hass.async_add_executor_job(_read, hass.config.path(self.path))
if not current:
current = self._empty_config()
return cast(_DataT, current)
class EditKeyBasedConfigView(BaseEditConfigView[dict[str, dict[str, Any]]]):
"""Configure a list of entries."""
def _empty_config(self) -> dict[str, Any]:
"""Return an empty config."""
return {}
def _get_value(
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
) -> dict[str, Any] | None:
"""Get value."""
return data.get(config_key)
def _write_value(
self,
hass: HomeAssistant,
data: dict[str, dict[str, Any]],
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
data.setdefault(config_key, {}).update(new_value)
def _delete_value(
self, hass: HomeAssistant, data: dict[str, dict[str, Any]], config_key: str
) -> dict[str, Any]:
"""Delete value."""
return data.pop(config_key)
class EditIdBasedConfigView(BaseEditConfigView[list[dict[str, Any]]]):
"""Configure key based config entries."""
def _empty_config(self) -> list[Any]:
"""Return an empty config."""
return []
def _get_value(
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
) -> dict[str, Any] | None:
"""Get value."""
return next((val for val in data if val.get(CONF_ID) == config_key), None)
def _write_value(
self,
hass: HomeAssistant,
data: list[dict[str, Any]],
config_key: str,
new_value: dict[str, Any],
) -> None:
"""Set value."""
if (value := self._get_value(hass, data, config_key)) is None:
value = {CONF_ID: config_key}
data.append(value)
value.update(new_value)
def _delete_value(
self, hass: HomeAssistant, data: list[dict[str, Any]], config_key: str
) -> None:
"""Delete value."""
index = next(
idx for idx, val in enumerate(data) if val.get(CONF_ID) == config_key
)
data.pop(index)
def _read(path: str) -> JSON_TYPE | None:
"""Read YAML helper."""
if not os.path.isfile(path):
return None
return load_yaml(path)
def _write(path: str, data: dict | list) -> None:
"""Write YAML helper."""
# Do it before opening file. If dump causes error it will now not
# truncate the file.
contents = dump(data)
write_utf8_file_atomic(path, contents)

View File

@ -51,11 +51,11 @@ def mock_config_store(data=None):
return result
with patch(
"homeassistant.components.config._read",
"homeassistant.components.config.view._read",
side_effect=mock_read,
autospec=True,
), patch(
"homeassistant.components.config._write",
"homeassistant.components.config.view._write",
side_effect=mock_write,
autospec=True,
), patch(

View File

@ -8,6 +8,7 @@ import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from homeassistant.components.config import automation
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -41,7 +42,7 @@ async def test_get_automation_config(
setup_automation,
) -> None:
"""Test getting automation config."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
client = await hass_client()
@ -64,7 +65,7 @@ async def test_update_automation_config(
setup_automation,
) -> None:
"""Test updating automation config."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
@ -153,7 +154,7 @@ async def test_update_automation_config_with_error(
validation_error: str,
) -> None:
"""Test updating automation config with errors."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
@ -206,7 +207,7 @@ async def test_update_automation_config_with_blueprint_substitution_error(
validation_error: str,
) -> None:
"""Test updating automation config with errors."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
@ -242,7 +243,7 @@ async def test_update_remove_key_automation_config(
setup_automation,
) -> None:
"""Test updating automation config while removing a key."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
@ -281,7 +282,7 @@ async def test_bad_formatted_automations(
setup_automation,
) -> None:
"""Test that we handle automations without ID."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == []
@ -347,7 +348,7 @@ async def test_delete_automation(
assert len(entity_registry.entities) == 2
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
assert await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("automation")) == [
@ -385,7 +386,7 @@ async def test_api_calls_require_admin(
setup_automation,
) -> None:
"""Test cloud APIs endpoints do not work as a normal user."""
with patch.object(config, "SECTIONS", ["automation"]):
with patch.object(config, "SECTIONS", [automation]):
await async_setup_component(hass, "config", {})
hass_config_store["automations.yaml"] = [{"id": "sun"}, {"id": "moon"}]

View File

@ -6,6 +6,7 @@ import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from homeassistant.components.config import core
from homeassistant.components.websocket_api.const import TYPE_RESULT
from homeassistant.const import (
CONF_UNIT_SYSTEM,
@ -23,7 +24,7 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator
@pytest.fixture
async def client(hass, hass_ws_client):
"""Fixture that can interact with the config manager API."""
with patch.object(config, "SECTIONS", ["core"]):
with patch.object(config, "SECTIONS", [core]):
assert await async_setup_component(hass, "config", {})
return await hass_ws_client(hass)
@ -32,7 +33,7 @@ async def test_validate_config_ok(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test checking config."""
with patch.object(config, "SECTIONS", ["core"]):
with patch.object(config, "SECTIONS", [core]):
await async_setup_component(hass, "config", {})
client = await hass_client()
@ -95,7 +96,7 @@ async def test_validate_config_requires_admin(
hass_read_only_access_token: str,
) -> None:
"""Test checking configuration does not work as a normal user."""
with patch.object(config, "SECTIONS", ["core"]):
with patch.object(config, "SECTIONS", [core]):
await async_setup_component(hass, "config", {})
client = await hass_client(hass_read_only_access_token)
@ -180,7 +181,7 @@ async def test_websocket_core_update_not_admin(
) -> None:
"""Test core config fails for non admin."""
hass_admin_user.groups = []
with patch.object(config, "SECTIONS", ["core"]):
with patch.object(config, "SECTIONS", [core]):
await async_setup_component(hass, "config", {})
client = await hass_ws_client(hass)

View File

@ -7,6 +7,7 @@ import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from homeassistant.components.config import scene
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -27,7 +28,7 @@ async def test_create_scene(
setup_scene,
) -> None:
"""Test creating a scene."""
with patch.object(config, "SECTIONS", ["scene"]):
with patch.object(config, "SECTIONS", [scene]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("scene")) == []
@ -74,7 +75,7 @@ async def test_update_scene(
setup_scene,
) -> None:
"""Test updating a scene."""
with patch.object(config, "SECTIONS", ["scene"]):
with patch.object(config, "SECTIONS", [scene]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("scene")) == []
@ -122,7 +123,7 @@ async def test_bad_formatted_scene(
setup_scene,
) -> None:
"""Test that we handle scene without ID."""
with patch.object(config, "SECTIONS", ["scene"]):
with patch.object(config, "SECTIONS", [scene]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("scene")) == []
@ -192,7 +193,7 @@ async def test_delete_scene(
assert len(entity_registry.entities) == 2
with patch.object(config, "SECTIONS", ["scene"]):
with patch.object(config, "SECTIONS", [scene]):
assert await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("scene")) == [
@ -232,7 +233,7 @@ async def test_api_calls_require_admin(
setup_scene,
) -> None:
"""Test scene APIs endpoints do not work as a normal user."""
with patch.object(config, "SECTIONS", ["scene"]):
with patch.object(config, "SECTIONS", [scene]):
await async_setup_component(hass, "config", {})
hass_config_store["scenes.yaml"] = [

View File

@ -8,6 +8,7 @@ import pytest
from homeassistant.bootstrap import async_setup_component
from homeassistant.components import config
from homeassistant.components.config import script
from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
@ -32,7 +33,7 @@ async def test_get_script_config(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
) -> None:
"""Test getting script config."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
client = await hass_client()
@ -55,7 +56,7 @@ async def test_update_script_config(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
) -> None:
"""Test updating script config."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == []
@ -91,7 +92,7 @@ async def test_invalid_object_id(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
) -> None:
"""Test creating a script with an invalid object_id."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == []
@ -156,7 +157,7 @@ async def test_update_script_config_with_error(
validation_error: str,
) -> None:
"""Test updating script config with errors."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == []
@ -207,7 +208,7 @@ async def test_update_script_config_with_blueprint_substitution_error(
validation_error: str,
) -> None:
"""Test updating script config with errors."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == []
@ -240,7 +241,7 @@ async def test_update_remove_key_script_config(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_config_store
) -> None:
"""Test updating script config while removing a key."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == []
@ -287,7 +288,7 @@ async def test_delete_script(
hass_config_store,
) -> None:
"""Test deleting a script."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
assert sorted(hass.states.async_entity_ids("script")) == [
@ -326,7 +327,7 @@ async def test_api_calls_require_admin(
hass_config_store,
) -> None:
"""Test script APIs endpoints do not work as a normal user."""
with patch.object(config, "SECTIONS", ["script"]):
with patch.object(config, "SECTIONS", [script]):
await async_setup_component(hass, "config", {})
hass_config_store["scripts.yaml"] = {