From 3f9dbd3e25165121934f57c7a712306489197ed5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 5 Mar 2024 16:18:57 -1000 Subject: [PATCH] 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 --- homeassistant/components/config/__init__.py | 289 ++---------------- homeassistant/components/config/automation.py | 3 +- homeassistant/components/config/const.py | 5 + homeassistant/components/config/scene.py | 3 +- homeassistant/components/config/script.py | 3 +- homeassistant/components/config/view.py | 243 +++++++++++++++ tests/components/config/conftest.py | 4 +- tests/components/config/test_automation.py | 17 +- tests/components/config/test_core.py | 9 +- tests/components/config/test_scene.py | 11 +- tests/components/config/test_script.py | 17 +- 11 files changed, 317 insertions(+), 287 deletions(-) create mode 100644 homeassistant/components/config/const.py create mode 100644 homeassistant/components/config/view.py diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index fbeb5904a1a..77c49c4b412 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -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) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index cf637b0aa23..59d4854533d 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -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 diff --git a/homeassistant/components/config/const.py b/homeassistant/components/config/const.py new file mode 100644 index 00000000000..f48d00a7ca9 --- /dev/null +++ b/homeassistant/components/config/const.py @@ -0,0 +1,5 @@ +"""Constants for config.""" + +ACTION_CREATE_UPDATE = "create_update" +ACTION_DELETE = "delete" +DOMAIN = "config" diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 01bdce0c8bc..dbcce222a4f 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -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 diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index d181ad94286..35596c7a84f 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -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 diff --git a/homeassistant/components/config/view.py b/homeassistant/components/config/view.py new file mode 100644 index 00000000000..cf24074bda9 --- /dev/null +++ b/homeassistant/components/config/view.py @@ -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) diff --git a/tests/components/config/conftest.py b/tests/components/config/conftest.py index e6f1532428e..83c8cd014f3 100644 --- a/tests/components/config/conftest.py +++ b/tests/components/config/conftest.py @@ -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( diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 1a099c05b16..9ba5762e1d0 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -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"}] diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index bd21e5e7d30..e50ec97e1b1 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -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) diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index 9fd596f7f91..2f2ca9b1c37 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -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"] = [ diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 7cf8cf5833e..bebec0aedba 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -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"] = {