From 3f2ca16ad74bddbd19d01e37e78a05948811a778 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 21 Mar 2021 18:44:29 -1000 Subject: [PATCH] Index config entries by id (#48199) --- homeassistant/config_entries.py | 31 ++++++------ tests/common.py | 10 ++-- .../components/config/test_config_entries.py | 4 +- tests/components/hue/conftest.py | 15 +++--- tests/components/huisbaasje/test_init.py | 50 +++++++++---------- tests/components/huisbaasje/test_sensor.py | 35 ++++++------- tests/test_config_entries.py | 35 ++++++++++++- 7 files changed, 106 insertions(+), 74 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 157acb545c1..b5491d83800 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -619,7 +619,7 @@ class ConfigEntries: self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.options = OptionsFlowManager(hass) self._hass_config = hass_config - self._entries: list[ConfigEntry] = [] + self._entries: dict[str, ConfigEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) EntityRegistryDisabledHandler(hass).async_setup() @@ -629,7 +629,7 @@ class ConfigEntries: seen: set[str] = set() result = [] - for entry in self._entries: + for entry in self._entries.values(): if entry.domain not in seen: seen.add(entry.domain) result.append(entry.domain) @@ -639,21 +639,22 @@ class ConfigEntries: @callback def async_get_entry(self, entry_id: str) -> ConfigEntry | None: """Return entry with matching entry_id.""" - for entry in self._entries: - if entry_id == entry.entry_id: - return entry - return None + return self._entries.get(entry_id) @callback def async_entries(self, domain: str | None = None) -> list[ConfigEntry]: """Return all entries or entries for a specific domain.""" if domain is None: - return list(self._entries) - return [entry for entry in self._entries if entry.domain == domain] + return list(self._entries.values()) + return [entry for entry in self._entries.values() if entry.domain == domain] async def async_add(self, entry: ConfigEntry) -> None: """Add and setup an entry.""" - self._entries.append(entry) + if entry.entry_id in self._entries: + raise HomeAssistantError( + f"An entry with the id {entry.entry_id} already exists." + ) + self._entries[entry.entry_id] = entry await self.async_setup(entry.entry_id) self._async_schedule_save() @@ -671,7 +672,7 @@ class ConfigEntries: await entry.async_remove(self.hass) - self._entries.remove(entry) + del self._entries[entry.entry_id] self._async_schedule_save() dev_reg, ent_reg = await asyncio.gather( @@ -707,11 +708,11 @@ class ConfigEntries: ) if config is None: - self._entries = [] + self._entries = {} return - self._entries = [ - ConfigEntry( + self._entries = { + entry["entry_id"]: ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], @@ -730,7 +731,7 @@ class ConfigEntries: disabled_by=entry.get("disabled_by"), ) for entry in config["entries"] - ] + } async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -920,7 +921,7 @@ class ConfigEntries: @callback def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: """Return data to save.""" - return {"entries": [entry.as_dict() for entry in self._entries]} + return {"entries": [entry.as_dict() for entry in self._entries.values()]} async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]: diff --git a/tests/common.py b/tests/common.py index 5f8626afb4e..984b35716f0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -18,7 +18,6 @@ from time import monotonic import types from typing import Any, Awaitable, Collection from unittest.mock import AsyncMock, Mock, patch -import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 @@ -61,6 +60,7 @@ from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM +import homeassistant.util.uuid as uuid_util import homeassistant.util.yaml.loader as yaml_loader _LOGGER = logging.getLogger(__name__) @@ -276,7 +276,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.skip_pip = True hass.config_entries = config_entries.ConfigEntries(hass, {}) - hass.config_entries._entries = [] + hass.config_entries._entries = {} hass.config_entries._store._async_ensure_stop_listener = lambda: None # Load the registries @@ -737,7 +737,7 @@ class MockConfigEntry(config_entries.ConfigEntry): ): """Initialize a mock config entry.""" kwargs = { - "entry_id": entry_id or uuid.uuid4().hex, + "entry_id": entry_id or uuid_util.random_uuid_hex(), "domain": domain, "data": data or {}, "system_options": system_options, @@ -756,11 +756,11 @@ class MockConfigEntry(config_entries.ConfigEntry): def add_to_hass(self, hass): """Test helper to add entry to hass.""" - hass.config_entries._entries.append(self) + hass.config_entries._entries[self.entry_id] = self def add_to_manager(self, manager): """Test helper to add entry to entry manager.""" - manager._entries.append(self) + manager._entries[self.entry_id] = self def patch_yaml_files(files_dict, endswith=True): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 6bb1f1885eb..d6cc474fa8b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -569,7 +569,7 @@ async def test_options_flow(hass, client): source="bla", connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) - entry = hass.config_entries._entries[0] + entry = hass.config_entries.async_entries()[0] with patch.dict(HANDLERS, {"test": TestFlow}): url = "/api/config/config_entries/options/flow" @@ -618,7 +618,7 @@ async def test_two_step_options_flow(hass, client): source="bla", connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) - entry = hass.config_entries._entries[0] + entry = hass.config_entries.async_entries()[0] with patch.dict(HANDLERS, {"test": TestFlow}): url = "/api/config/config_entries/options/flow" diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index db45b9fcd4d..3fc55692cc4 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -12,6 +12,7 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 @@ -111,13 +112,11 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): if hostname is None: hostname = "mock-host" hass.config.components.add(hue.DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": hostname}, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, + config_entry = MockConfigEntry( + domain=hue.DOMAIN, + title="Mock Title", + data={"host": hostname}, + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, ) mock_bridge.config_entry = config_entry @@ -125,7 +124,7 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") # simulate a full setup by manually adding the bridge config entry - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) # and make sure it completes before going further await hass.async_block_till_done() diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 3de6af83e46..2d68cdf8a11 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -9,12 +9,12 @@ from homeassistant.config_entries import ( ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_ERROR, - ConfigEntry, ) from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.components.huisbaasje.test_data import MOCK_CURRENT_MEASUREMENTS @@ -36,20 +36,20 @@ async def test_setup_entry(hass: HomeAssistant): return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) - config_entry = ConfigEntry( - 1, - huisbaasje.DOMAIN, - "userId", - { + config_entry = MockConfigEntry( + version=1, + domain=huisbaasje.DOMAIN, + title="userId", + data={ CONF_ID: "userId", CONF_USERNAME: "username", CONF_PASSWORD: "password", }, - "test", - CONN_CLASS_CLOUD_POLL, + source="test", + connection_class=CONN_CLASS_CLOUD_POLL, system_options={}, ) - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) assert config_entry.state == ENTRY_STATE_NOT_LOADED assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -77,20 +77,20 @@ async def test_setup_entry_error(hass: HomeAssistant): "huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException ) as mock_authenticate: hass.config.components.add(huisbaasje.DOMAIN) - config_entry = ConfigEntry( - 1, - huisbaasje.DOMAIN, - "userId", - { + config_entry = MockConfigEntry( + version=1, + domain=huisbaasje.DOMAIN, + title="userId", + data={ CONF_ID: "userId", CONF_USERNAME: "username", CONF_PASSWORD: "password", }, - "test", - CONN_CLASS_CLOUD_POLL, + source="test", + connection_class=CONN_CLASS_CLOUD_POLL, system_options={}, ) - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) assert config_entry.state == ENTRY_STATE_NOT_LOADED await hass.config_entries.async_setup(config_entry.entry_id) @@ -119,20 +119,20 @@ async def test_unload_entry(hass: HomeAssistant): return_value=MOCK_CURRENT_MEASUREMENTS, ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) - config_entry = ConfigEntry( - 1, - huisbaasje.DOMAIN, - "userId", - { + config_entry = MockConfigEntry( + version=1, + domain=huisbaasje.DOMAIN, + title="userId", + data={ CONF_ID: "userId", CONF_USERNAME: "username", CONF_PASSWORD: "password", }, - "test", - CONN_CLASS_CLOUD_POLL, + source="test", + connection_class=CONN_CLASS_CLOUD_POLL, system_options={}, ) - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) # Load config entry assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index d1ffe565c84..cfb17cd5f2d 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -2,10 +2,11 @@ from unittest.mock import patch from homeassistant.components import huisbaasje -from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigEntry +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from tests.common import MockConfigEntry from tests.components.huisbaasje.test_data import ( MOCK_CURRENT_MEASUREMENTS, MOCK_LIMITED_CURRENT_MEASUREMENTS, @@ -24,20 +25,20 @@ async def test_setup_entry(hass: HomeAssistant): ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) - config_entry = ConfigEntry( - 1, - huisbaasje.DOMAIN, - "userId", - { + config_entry = MockConfigEntry( + version=1, + domain=huisbaasje.DOMAIN, + title="userId", + data={ CONF_ID: "userId", CONF_USERNAME: "username", CONF_PASSWORD: "password", }, - "test", - CONN_CLASS_CLOUD_POLL, + source="test", + connection_class=CONN_CLASS_CLOUD_POLL, system_options={}, ) - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -81,20 +82,20 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): ) as mock_current_measurements: hass.config.components.add(huisbaasje.DOMAIN) - config_entry = ConfigEntry( - 1, - huisbaasje.DOMAIN, - "userId", - { + config_entry = MockConfigEntry( + version=1, + domain=huisbaasje.DOMAIN, + title="userId", + data={ CONF_ID: "userId", CONF_USERNAME: "username", CONF_PASSWORD: "password", }, - "test", - CONN_CLASS_CLOUD_POLL, + source="test", + connection_class=CONN_CLASS_CLOUD_POLL, system_options={}, ) - hass.config_entries._entries.append(config_entry) + config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9abb68e97c9..09ab60e4300 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -7,7 +7,7 @@ import pytest from homeassistant import config_entries, data_entry_flow, loader from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -44,7 +44,7 @@ def mock_handlers(): def manager(hass): """Fixture of a loaded config manager.""" manager = config_entries.ConfigEntries(hass, {}) - manager._entries = [] + manager._entries = {} manager._store._async_ensure_stop_listener = lambda: None hass.config_entries = manager return manager @@ -1383,6 +1383,37 @@ async def test_unique_id_existing_entry(hass, manager): assert len(async_remove_entry.mock_calls) == 1 +async def test_entry_id_existing_entry(hass, manager): + """Test that we throw when the entry id collides.""" + collide_entry_id = "collide" + hass.config.components.add("comp") + MockConfigEntry( + entry_id=collide_entry_id, + domain="comp", + state=config_entries.ENTRY_STATE_LOADED, + unique_id="mock-unique-id", + ).add_to_hass(hass) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + return self.async_create_entry(title="mock-title", data={"via": "flow"}) + + with pytest.raises(HomeAssistantError), patch.dict( + config_entries.HANDLERS, {"comp": TestFlow} + ), patch( + "homeassistant.config_entries.uuid_util.random_uuid_hex", + return_value=collide_entry_id, + ): + await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + + async def test_unique_id_update_existing_entry_without_reload(hass, manager): """Test that we update an entry if there already is an entry with unique ID.""" hass.config.components.add("comp")