Freeze config entry data (#32615)

* Freeze config entry data

* Fix mutating entry.data

* Fix config entry options tests
This commit is contained in:
Paulus Schoutsen 2020-03-09 14:07:50 -07:00 committed by GitHub
parent 3318e65948
commit d4615fd432
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 71 additions and 45 deletions

View file

@ -52,9 +52,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
# Check if host needs to be updated # Check if host needs to be updated
entry = entries[0] entry = entries[0]
if entry.data[CONF_HOST] != host: if entry.data[CONF_HOST] != host:
entry.data[CONF_HOST] = host hass.config_entries.async_update_entry(
entry.title = format_title(host) entry, title=format_title(host), data={**entry.data, CONF_HOST: host}
hass.config_entries.async_update_entry(entry) )
return True return True

View file

@ -568,7 +568,7 @@ async def async_setup_entry(hass, entry):
# If user didn't have configuration.yaml config, generate defaults # If user didn't have configuration.yaml config, generate defaults
if conf is None: if conf is None:
conf = CONFIG_SCHEMA({DOMAIN: entry.data})[DOMAIN] conf = CONFIG_SCHEMA({DOMAIN: dict(entry.data)})[DOMAIN]
elif any(key in conf for key in entry.data): elif any(key in conf for key in entry.data):
_LOGGER.warning( _LOGGER.warning(
"Data in your configuration entry is going to override your " "Data in your configuration entry is going to override your "

View file

@ -227,7 +227,7 @@ class PlexOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry): def __init__(self, config_entry):
"""Initialize Plex options flow.""" """Initialize Plex options flow."""
self.options = copy.deepcopy(config_entry.options) self.options = copy.deepcopy(dict(config_entry.options))
self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER] self.server_id = config_entry.data[CONF_SERVER_IDENTIFIER]
async def async_step_init(self, user_input=None): async def async_step_init(self, user_input=None):

View file

@ -158,13 +158,14 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self._id.startswith("uuid:"): if self._id.startswith("uuid:"):
self._id = self._id[5:] self._id = self._id[5:]
config_entry = await self.async_set_unique_id(ip_address) await self.async_set_unique_id(ip_address)
if config_entry: self._abort_if_unique_id_configured(
config_entry.data[CONF_ID] = self._id {
config_entry.data[CONF_MANUFACTURER] = self._manufacturer CONF_ID: self._id,
config_entry.data[CONF_MODEL] = self._model CONF_MANUFACTURER: self._manufacturer,
self.hass.config_entries.async_update_entry(config_entry) CONF_MODEL: self._model,
return self.async_abort(reason="already_configured") }
)
self.context["title_placeholders"] = {"model": self._model} self.context["title_placeholders"] = {"model": self._model}
return await self.async_step_confirm() return await self.async_step_confirm()

View file

@ -109,8 +109,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
entry.data[CONF_OAUTH_CLIENT_SECRET], entry.data[CONF_OAUTH_CLIENT_SECRET],
entry.data[CONF_REFRESH_TOKEN], entry.data[CONF_REFRESH_TOKEN],
) )
entry.data[CONF_REFRESH_TOKEN] = token.refresh_token hass.config_entries.async_update_entry(
hass.config_entries.async_update_entry(entry) entry, data={**entry.data, CONF_REFRESH_TOKEN: token.refresh_token}
)
# Get devices and their current status # Get devices and their current status
devices = await api.devices(location_ids=[installed_app.location_id]) devices = await api.devices(location_ids=[installed_app.location_id])
@ -304,8 +305,13 @@ class DeviceBroker:
self._entry.data[CONF_OAUTH_CLIENT_ID], self._entry.data[CONF_OAUTH_CLIENT_ID],
self._entry.data[CONF_OAUTH_CLIENT_SECRET], self._entry.data[CONF_OAUTH_CLIENT_SECRET],
) )
self._entry.data[CONF_REFRESH_TOKEN] = self._token.refresh_token self._hass.config_entries.async_update_entry(
self._hass.config_entries.async_update_entry(self._entry) self._entry,
data={
**self._entry.data,
CONF_REFRESH_TOKEN: self._token.refresh_token,
},
)
_LOGGER.debug( _LOGGER.debug(
"Regenerated refresh token for installed app: %s", "Regenerated refresh token for installed app: %s",
self._installed_app_id, self._installed_app_id,

View file

@ -428,8 +428,9 @@ async def smartapp_update(hass: HomeAssistantType, req, resp, app):
None, None,
) )
if entry: if entry:
entry.data[CONF_REFRESH_TOKEN] = req.refresh_token hass.config_entries.async_update_entry(
hass.config_entries.async_update_entry(entry) entry, data={**entry.data, CONF_REFRESH_TOKEN: req.refresh_token}
)
_LOGGER.debug( _LOGGER.debug(
"Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id "Updated SmartApp '%s' under parent app '%s'", req.installed_app_id, app.app_id

View file

@ -196,7 +196,7 @@ class TransmissionClient:
def add_options(self): def add_options(self):
"""Add options for entry.""" """Add options for entry."""
if not self.config_entry.options: if not self.config_entry.options:
scan_interval = self.config_entry.data.pop( scan_interval = self.config_entry.data.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
) )
options = {CONF_SCAN_INTERVAL: scan_interval} options = {CONF_SCAN_INTERVAL: scan_interval}

View file

@ -151,9 +151,10 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry):
return False return False
# 'register'/save UDN # 'register'/save UDN
config_entry.data["udn"] = device.udn
hass.data[DOMAIN]["devices"][device.udn] = device hass.data[DOMAIN]["devices"][device.udn] = device
hass.config_entries.async_update_entry(entry=config_entry, data=config_entry.data) hass.config_entries.async_update_entry(
entry=config_entry, data={**config_entry.data, "udn": device.udn}
)
# create device registry entry # create device registry entry
device_registry = await dr.async_get_registry(hass) device_registry = await dr.async_get_registry(hass)

View file

@ -2,6 +2,7 @@
import asyncio import asyncio
import functools import functools
import logging import logging
from types import MappingProxyType
from typing import Any, Callable, Dict, List, Optional, Set, Union, cast from typing import Any, Callable, Dict, List, Optional, Set, Union, cast
import uuid import uuid
import weakref import weakref
@ -139,10 +140,10 @@ class ConfigEntry:
self.title = title self.title = title
# Config data # Config data
self.data = data self.data = MappingProxyType(data)
# Entry options # Entry options
self.options = options or {} self.options = MappingProxyType(options or {})
# Entry system options # Entry system options
self.system_options = SystemOptions(**system_options) self.system_options = SystemOptions(**system_options)
@ -396,8 +397,8 @@ class ConfigEntry:
"version": self.version, "version": self.version,
"domain": self.domain, "domain": self.domain,
"title": self.title, "title": self.title,
"data": self.data, "data": dict(self.data),
"options": self.options, "options": dict(self.options),
"system_options": self.system_options.as_dict(), "system_options": self.system_options.as_dict(),
"source": self.source, "source": self.source,
"connection_class": self.connection_class, "connection_class": self.connection_class,
@ -720,6 +721,7 @@ class ConfigEntries:
entry: ConfigEntry, entry: ConfigEntry,
*, *,
unique_id: Union[str, dict, None] = _UNDEF, unique_id: Union[str, dict, None] = _UNDEF,
title: Union[str, dict] = _UNDEF,
data: dict = _UNDEF, data: dict = _UNDEF,
options: dict = _UNDEF, options: dict = _UNDEF,
system_options: dict = _UNDEF, system_options: dict = _UNDEF,
@ -728,11 +730,14 @@ class ConfigEntries:
if unique_id is not _UNDEF: if unique_id is not _UNDEF:
entry.unique_id = cast(Optional[str], unique_id) entry.unique_id = cast(Optional[str], unique_id)
if title is not _UNDEF:
entry.title = cast(str, title)
if data is not _UNDEF: if data is not _UNDEF:
entry.data = data entry.data = MappingProxyType(data)
if options is not _UNDEF: if options is not _UNDEF:
entry.options = options entry.options = MappingProxyType(options)
if system_options is not _UNDEF: if system_options is not _UNDEF:
entry.system_options.update(**system_options) entry.system_options.update(**system_options)
@ -818,7 +823,9 @@ class ConfigFlow(data_entry_flow.FlowHandler):
raise data_entry_flow.UnknownHandler raise data_entry_flow.UnknownHandler
@callback @callback
def _abort_if_unique_id_configured(self, updates: Dict[Any, Any] = None) -> None: def _abort_if_unique_id_configured(
self, updates: Optional[Dict[Any, Any]] = None
) -> None:
"""Abort if the unique ID is already configured.""" """Abort if the unique ID is already configured."""
assert self.hass assert self.hass
if self.unique_id is None: if self.unique_id is None:

View file

@ -38,7 +38,7 @@ async def test_setup_entry(hass):
async def test_setup_entry_fails(hass): async def test_setup_entry_fails(hass):
"""Test successful setup of entry.""" """Test successful setup of entry."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, options=True, version=2 domain=axis.DOMAIN, data={axis.CONF_MAC: "0123"}, version=2
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)

View file

@ -196,7 +196,7 @@ async def test_hap_with_name(hass, mock_connection, hmip_config_entry):
entity_name = f"{home_name} Treppe" entity_name = f"{home_name} Treppe"
device_model = "HmIP-BSL" device_model = "HmIP-BSL"
hmip_config_entry.data["name"] = home_name hmip_config_entry.data = {**hmip_config_entry.data, "name": home_name}
mock_hap = await HomeFactory( mock_hap = await HomeFactory(
hass, mock_connection, hmip_config_entry hass, mock_connection, hmip_config_entry
).async_get_mock_hap(test_devices=["Treppe"]) ).async_get_mock_hap(test_devices=["Treppe"])

View file

@ -73,11 +73,10 @@ async def test_flow_entry_already_exists(hass):
Test when the form should show when user puts existing location Test when the form should show when user puts existing location
in the config gui. Then the form should show with error. in the config gui. Then the form should show with error.
""" """
first_entry = MockConfigEntry(domain="met") first_entry = MockConfigEntry(
first_entry.data["name"] = "home" domain="met",
first_entry.data[CONF_LONGITUDE] = 0 data={"name": "home", CONF_LATITUDE: 0, CONF_LONGITUDE: 0, CONF_ELEVATION: 0},
first_entry.data[CONF_LATITUDE] = 0 )
first_entry.data[CONF_ELEVATION] = 0
first_entry.add_to_hass(hass) first_entry.add_to_hass(hass)
test_data = { test_data = {

View file

@ -33,10 +33,10 @@ async def setup_mikrotik_entry(hass, **kwargs):
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
if "force_dhcp" in kwargs: if "force_dhcp" in kwargs:
config_entry.options["force_dhcp"] = True config_entry.options = {**config_entry.options, "force_dhcp": True}
if "arp_ping" in kwargs: if "arp_ping" in kwargs:
config_entry.options["arp_ping"] = True config_entry.options = {**config_entry.options, "arp_ping": True}
with patch("librouteros.connect"), patch.object( with patch("librouteros.connect"), patch.object(
mikrotik.hub.MikrotikData, "command", new=mock_command mikrotik.hub.MikrotikData, "command", new=mock_command

View file

@ -316,9 +316,10 @@ async def test_ssdp_already_configured(hass, remote):
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
) )
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["data"][CONF_MANUFACTURER] is None entry = result["result"]
assert result["data"][CONF_MODEL] is None assert entry.data[CONF_MANUFACTURER] is None
assert result["data"][CONF_ID] is None assert entry.data[CONF_MODEL] is None
assert entry.data[CONF_ID] is None
# failed as already configured # failed as already configured
result2 = await hass.config_entries.flow.async_init( result2 = await hass.config_entries.flow.async_init(
@ -328,9 +329,9 @@ async def test_ssdp_already_configured(hass, remote):
assert result2["reason"] == "already_configured" assert result2["reason"] == "already_configured"
# check updated device info # check updated device info
assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" assert entry.data[CONF_MANUFACTURER] == "fake_manufacturer"
assert result["data"][CONF_MODEL] == "fake_model" assert entry.data[CONF_MODEL] == "fake_model"
assert result["data"][CONF_ID] == "fake_uuid" assert entry.data[CONF_ID] == "fake_uuid"
async def test_autodetect_websocket(hass, remote): async def test_autodetect_websocket(hass, remote):

View file

@ -423,7 +423,10 @@ async def test_event_handler_dispatches_updated_devices(
data={"codeId": "1"}, data={"codeId": "1"},
) )
request = event_request_factory(device_ids=device_ids, events=[event]) request = event_request_factory(device_ids=device_ids, events=[event])
config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id config_entry.data = {
**config_entry.data,
CONF_INSTALLED_APP_ID: request.installed_app_id,
}
called = False called = False
def signal(ids): def signal(ids):
@ -479,7 +482,10 @@ async def test_event_handler_fires_button_events(
device.device_id, capability="button", attribute="button", value="pushed" device.device_id, capability="button", attribute="button", value="pushed"
) )
request = event_request_factory(events=[event]) request = event_request_factory(events=[event])
config_entry.data[CONF_INSTALLED_APP_ID] = request.installed_app_id config_entry.data = {
**config_entry.data,
CONF_INSTALLED_APP_ID: request.installed_app_id,
}
called = False called = False
def handler(evt): def handler(evt):

View file

@ -456,6 +456,8 @@ async def test_saving_and_loading(hass):
"test", context={"source": config_entries.SOURCE_USER} "test", context={"source": config_entries.SOURCE_USER}
) )
assert len(hass.config_entries.async_entries()) == 2
# To trigger the call_later # To trigger the call_later
async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1))
# To execute the save # To execute the save
@ -465,6 +467,8 @@ async def test_saving_and_loading(hass):
manager = config_entries.ConfigEntries(hass, {}) manager = config_entries.ConfigEntries(hass, {})
await manager.async_initialize() await manager.async_initialize()
assert len(manager.async_entries()) == 2
# Ensure same order # Ensure same order
for orig, loaded in zip( for orig, loaded in zip(
hass.config_entries.async_entries(), manager.async_entries() hass.config_entries.async_entries(), manager.async_entries()