Index config entries by id (#48199)

This commit is contained in:
J. Nick Koston 2021-03-21 18:44:29 -10:00 committed by GitHub
parent 6fab4a2c82
commit 3f2ca16ad7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 74 deletions

View File

@ -619,7 +619,7 @@ class ConfigEntries:
self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
self.options = OptionsFlowManager(hass) self.options = OptionsFlowManager(hass)
self._hass_config = hass_config self._hass_config = hass_config
self._entries: list[ConfigEntry] = [] self._entries: dict[str, ConfigEntry] = {}
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
EntityRegistryDisabledHandler(hass).async_setup() EntityRegistryDisabledHandler(hass).async_setup()
@ -629,7 +629,7 @@ class ConfigEntries:
seen: set[str] = set() seen: set[str] = set()
result = [] result = []
for entry in self._entries: for entry in self._entries.values():
if entry.domain not in seen: if entry.domain not in seen:
seen.add(entry.domain) seen.add(entry.domain)
result.append(entry.domain) result.append(entry.domain)
@ -639,21 +639,22 @@ class ConfigEntries:
@callback @callback
def async_get_entry(self, entry_id: str) -> ConfigEntry | None: def async_get_entry(self, entry_id: str) -> ConfigEntry | None:
"""Return entry with matching entry_id.""" """Return entry with matching entry_id."""
for entry in self._entries: return self._entries.get(entry_id)
if entry_id == entry.entry_id:
return entry
return None
@callback @callback
def async_entries(self, domain: str | None = None) -> list[ConfigEntry]: def async_entries(self, domain: str | None = None) -> list[ConfigEntry]:
"""Return all entries or entries for a specific domain.""" """Return all entries or entries for a specific domain."""
if domain is None: if domain is None:
return list(self._entries) return list(self._entries.values())
return [entry for entry in self._entries if entry.domain == domain] return [entry for entry in self._entries.values() if entry.domain == domain]
async def async_add(self, entry: ConfigEntry) -> None: async def async_add(self, entry: ConfigEntry) -> None:
"""Add and setup an entry.""" """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) await self.async_setup(entry.entry_id)
self._async_schedule_save() self._async_schedule_save()
@ -671,7 +672,7 @@ class ConfigEntries:
await entry.async_remove(self.hass) await entry.async_remove(self.hass)
self._entries.remove(entry) del self._entries[entry.entry_id]
self._async_schedule_save() self._async_schedule_save()
dev_reg, ent_reg = await asyncio.gather( dev_reg, ent_reg = await asyncio.gather(
@ -707,11 +708,11 @@ class ConfigEntries:
) )
if config is None: if config is None:
self._entries = [] self._entries = {}
return return
self._entries = [ self._entries = {
ConfigEntry( entry["entry_id"]: ConfigEntry(
version=entry["version"], version=entry["version"],
domain=entry["domain"], domain=entry["domain"],
entry_id=entry["entry_id"], entry_id=entry["entry_id"],
@ -730,7 +731,7 @@ class ConfigEntries:
disabled_by=entry.get("disabled_by"), disabled_by=entry.get("disabled_by"),
) )
for entry in config["entries"] for entry in config["entries"]
] }
async def async_setup(self, entry_id: str) -> bool: async def async_setup(self, entry_id: str) -> bool:
"""Set up a config entry. """Set up a config entry.
@ -920,7 +921,7 @@ class ConfigEntries:
@callback @callback
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]: def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
"""Return data to save.""" """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]: async def _old_conf_migrator(old_config: dict[str, Any]) -> dict[str, Any]:

View File

@ -18,7 +18,6 @@ from time import monotonic
import types import types
from typing import Any, Awaitable, Collection from typing import Any, Awaitable, Collection
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
import uuid
from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 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 from homeassistant.util.async_ import run_callback_threadsafe
import homeassistant.util.dt as date_util import homeassistant.util.dt as date_util
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
import homeassistant.util.uuid as uuid_util
import homeassistant.util.yaml.loader as yaml_loader import homeassistant.util.yaml.loader as yaml_loader
_LOGGER = logging.getLogger(__name__) _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.skip_pip = True
hass.config_entries = config_entries.ConfigEntries(hass, {}) 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 hass.config_entries._store._async_ensure_stop_listener = lambda: None
# Load the registries # Load the registries
@ -737,7 +737,7 @@ class MockConfigEntry(config_entries.ConfigEntry):
): ):
"""Initialize a mock config entry.""" """Initialize a mock config entry."""
kwargs = { kwargs = {
"entry_id": entry_id or uuid.uuid4().hex, "entry_id": entry_id or uuid_util.random_uuid_hex(),
"domain": domain, "domain": domain,
"data": data or {}, "data": data or {},
"system_options": system_options, "system_options": system_options,
@ -756,11 +756,11 @@ class MockConfigEntry(config_entries.ConfigEntry):
def add_to_hass(self, hass): def add_to_hass(self, hass):
"""Test helper to add entry to 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): def add_to_manager(self, manager):
"""Test helper to add entry to entry 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): def patch_yaml_files(files_dict, endswith=True):

View File

@ -569,7 +569,7 @@ async def test_options_flow(hass, client):
source="bla", source="bla",
connection_class=core_ce.CONN_CLASS_LOCAL_POLL, connection_class=core_ce.CONN_CLASS_LOCAL_POLL,
).add_to_hass(hass) ).add_to_hass(hass)
entry = hass.config_entries._entries[0] entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}): with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/options/flow" url = "/api/config/config_entries/options/flow"
@ -618,7 +618,7 @@ async def test_two_step_options_flow(hass, client):
source="bla", source="bla",
connection_class=core_ce.CONN_CLASS_LOCAL_POLL, connection_class=core_ce.CONN_CLASS_LOCAL_POLL,
).add_to_hass(hass) ).add_to_hass(hass)
entry = hass.config_entries._entries[0] entry = hass.config_entries.async_entries()[0]
with patch.dict(HANDLERS, {"test": TestFlow}): with patch.dict(HANDLERS, {"test": TestFlow}):
url = "/api/config/config_entries/options/flow" url = "/api/config/config_entries/options/flow"

View File

@ -12,6 +12,7 @@ from homeassistant import config_entries
from homeassistant.components import hue from homeassistant.components import hue
from homeassistant.components.hue import sensor_base as hue_sensor_base 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 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: if hostname is None:
hostname = "mock-host" hostname = "mock-host"
hass.config.components.add(hue.DOMAIN) hass.config.components.add(hue.DOMAIN)
config_entry = config_entries.ConfigEntry( config_entry = MockConfigEntry(
1, domain=hue.DOMAIN,
hue.DOMAIN, title="Mock Title",
"Mock Title", data={"host": hostname},
{"host": hostname}, connection_class=config_entries.CONN_CLASS_LOCAL_POLL,
"test",
config_entries.CONN_CLASS_LOCAL_POLL,
system_options={}, system_options={},
) )
mock_bridge.config_entry = config_entry 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, "binary_sensor")
await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor")
# simulate a full setup by manually adding the bridge config entry # 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 # and make sure it completes before going further
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -9,12 +9,12 @@ from homeassistant.config_entries import (
ENTRY_STATE_LOADED, ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED, ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_ERROR,
ConfigEntry,
) )
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.components.huisbaasje.test_data import MOCK_CURRENT_MEASUREMENTS 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, return_value=MOCK_CURRENT_MEASUREMENTS,
) as mock_current_measurements: ) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN) hass.config.components.add(huisbaasje.DOMAIN)
config_entry = ConfigEntry( config_entry = MockConfigEntry(
1, version=1,
huisbaasje.DOMAIN, domain=huisbaasje.DOMAIN,
"userId", title="userId",
{ data={
CONF_ID: "userId", CONF_ID: "userId",
CONF_USERNAME: "username", CONF_USERNAME: "username",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
"test", source="test",
CONN_CLASS_CLOUD_POLL, connection_class=CONN_CLASS_CLOUD_POLL,
system_options={}, system_options={},
) )
hass.config_entries._entries.append(config_entry) config_entry.add_to_hass(hass)
assert config_entry.state == ENTRY_STATE_NOT_LOADED assert config_entry.state == ENTRY_STATE_NOT_LOADED
assert await hass.config_entries.async_setup(config_entry.entry_id) 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 "huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException
) as mock_authenticate: ) as mock_authenticate:
hass.config.components.add(huisbaasje.DOMAIN) hass.config.components.add(huisbaasje.DOMAIN)
config_entry = ConfigEntry( config_entry = MockConfigEntry(
1, version=1,
huisbaasje.DOMAIN, domain=huisbaasje.DOMAIN,
"userId", title="userId",
{ data={
CONF_ID: "userId", CONF_ID: "userId",
CONF_USERNAME: "username", CONF_USERNAME: "username",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
"test", source="test",
CONN_CLASS_CLOUD_POLL, connection_class=CONN_CLASS_CLOUD_POLL,
system_options={}, system_options={},
) )
hass.config_entries._entries.append(config_entry) config_entry.add_to_hass(hass)
assert config_entry.state == ENTRY_STATE_NOT_LOADED assert config_entry.state == ENTRY_STATE_NOT_LOADED
await hass.config_entries.async_setup(config_entry.entry_id) 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, return_value=MOCK_CURRENT_MEASUREMENTS,
) as mock_current_measurements: ) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN) hass.config.components.add(huisbaasje.DOMAIN)
config_entry = ConfigEntry( config_entry = MockConfigEntry(
1, version=1,
huisbaasje.DOMAIN, domain=huisbaasje.DOMAIN,
"userId", title="userId",
{ data={
CONF_ID: "userId", CONF_ID: "userId",
CONF_USERNAME: "username", CONF_USERNAME: "username",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
"test", source="test",
CONN_CLASS_CLOUD_POLL, connection_class=CONN_CLASS_CLOUD_POLL,
system_options={}, system_options={},
) )
hass.config_entries._entries.append(config_entry) config_entry.add_to_hass(hass)
# Load config entry # Load config entry
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)

View File

@ -2,10 +2,11 @@
from unittest.mock import patch from unittest.mock import patch
from homeassistant.components import huisbaasje 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.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.components.huisbaasje.test_data import ( from tests.components.huisbaasje.test_data import (
MOCK_CURRENT_MEASUREMENTS, MOCK_CURRENT_MEASUREMENTS,
MOCK_LIMITED_CURRENT_MEASUREMENTS, MOCK_LIMITED_CURRENT_MEASUREMENTS,
@ -24,20 +25,20 @@ async def test_setup_entry(hass: HomeAssistant):
) as mock_current_measurements: ) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN) hass.config.components.add(huisbaasje.DOMAIN)
config_entry = ConfigEntry( config_entry = MockConfigEntry(
1, version=1,
huisbaasje.DOMAIN, domain=huisbaasje.DOMAIN,
"userId", title="userId",
{ data={
CONF_ID: "userId", CONF_ID: "userId",
CONF_USERNAME: "username", CONF_USERNAME: "username",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
"test", source="test",
CONN_CLASS_CLOUD_POLL, connection_class=CONN_CLASS_CLOUD_POLL,
system_options={}, 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -81,20 +82,20 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant):
) as mock_current_measurements: ) as mock_current_measurements:
hass.config.components.add(huisbaasje.DOMAIN) hass.config.components.add(huisbaasje.DOMAIN)
config_entry = ConfigEntry( config_entry = MockConfigEntry(
1, version=1,
huisbaasje.DOMAIN, domain=huisbaasje.DOMAIN,
"userId", title="userId",
{ data={
CONF_ID: "userId", CONF_ID: "userId",
CONF_USERNAME: "username", CONF_USERNAME: "username",
CONF_PASSWORD: "password", CONF_PASSWORD: "password",
}, },
"test", source="test",
CONN_CLASS_CLOUD_POLL, connection_class=CONN_CLASS_CLOUD_POLL,
system_options={}, 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) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -7,7 +7,7 @@ import pytest
from homeassistant import config_entries, data_entry_flow, loader from homeassistant import config_entries, data_entry_flow, loader
from homeassistant.core import callback 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.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util import dt from homeassistant.util import dt
@ -44,7 +44,7 @@ def mock_handlers():
def manager(hass): def manager(hass):
"""Fixture of a loaded config manager.""" """Fixture of a loaded config manager."""
manager = config_entries.ConfigEntries(hass, {}) manager = config_entries.ConfigEntries(hass, {})
manager._entries = [] manager._entries = {}
manager._store._async_ensure_stop_listener = lambda: None manager._store._async_ensure_stop_listener = lambda: None
hass.config_entries = manager hass.config_entries = manager
return manager return manager
@ -1383,6 +1383,37 @@ async def test_unique_id_existing_entry(hass, manager):
assert len(async_remove_entry.mock_calls) == 1 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): 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.""" """Test that we update an entry if there already is an entry with unique ID."""
hass.config.components.add("comp") hass.config.components.add("comp")