Add OptionsFlow helper for a mutable copy of the config entry options (#129718)

* Add OptionsFlow helper for a mutable copy of the config entry options

* Add tests

* Improve coverage

* error_if_core=False

* Adjust report

* Avoid mutli-line ternary
pull/129779/head
epenet 2024-11-03 20:37:58 +01:00 committed by GitHub
parent d671d48869
commit 89eb395e2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 76 additions and 22 deletions

View File

@ -220,7 +220,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> MQTTOptionsFlowHandler:
"""Get the options flow for this handler."""
return MQTTOptionsFlowHandler(config_entry)
return MQTTOptionsFlowHandler()
async def _async_install_addon(self) -> None:
"""Install the Mosquitto Mqtt broker add-on."""
@ -543,11 +543,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
class MQTTOptionsFlowHandler(OptionsFlow):
"""Handle MQTT options."""
def __init__(self, config_entry: ConfigEntry) -> None:
def __init__(self) -> None:
"""Initialize MQTT options flow."""
self.config_entry = config_entry
self.broker_config: dict[str, str | int] = {}
self.options = config_entry.options
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
"""Manage the MQTT options."""

View File

@ -109,7 +109,7 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN):
config_entry: ConfigEntry,
) -> OnvifOptionsFlowHandler:
"""Get the options flow for this handler."""
return OnvifOptionsFlowHandler(config_entry)
return OnvifOptionsFlowHandler()
def __init__(self) -> None:
"""Initialize the ONVIF config flow."""
@ -389,11 +389,6 @@ class OnvifFlowHandler(ConfigFlow, domain=DOMAIN):
class OnvifOptionsFlowHandler(OptionsFlow):
"""Handle ONVIF options."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize ONVIF options flow."""
self.config_entry = config_entry
self.options = dict(config_entry.options)
async def async_step_init(self, user_input: None = None) -> ConfigFlowResult:
"""Manage the ONVIF options."""
return await self.async_step_onvif_devices()

View File

@ -170,8 +170,6 @@ class OptionsFlowHandler(OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self.options = config_entry.options
self.host = config_entry.data[CONF_HOST]
self.key = config_entry.data[CONF_CLIENT_SECRET]
@ -188,7 +186,8 @@ class OptionsFlowHandler(OptionsFlow):
if not sources_list:
errors["base"] = "cannot_retrieve"
sources = [s for s in self.options.get(CONF_SOURCES, []) if s in sources_list]
option_sources = self.config_entry.options.get(CONF_SOURCES, [])
sources = [s for s in option_sources if s in sources_list]
if not sources:
sources = sources_list

View File

@ -3053,6 +3053,7 @@ class OptionsFlowManager(
class OptionsFlow(ConfigEntryBaseFlow):
"""Base class for config options flows."""
_options: dict[str, Any]
handler: str
_config_entry: ConfigEntry
@ -3119,6 +3120,28 @@ class OptionsFlow(ConfigEntryBaseFlow):
)
self._config_entry = value
@property
def options(self) -> dict[str, Any]:
"""Return a mutable copy of the config entry options.
Please note that this is not available inside `__init__` method, and
can only be referenced after initialisation.
"""
if not hasattr(self, "_options"):
self._options = deepcopy(dict(self.config_entry.options))
return self._options
@options.setter
def options(self, value: dict[str, Any]) -> None:
"""Set the options value."""
report(
"sets option flow options explicitly, which is deprecated "
"and will stop working in 2025.12",
error_if_integration=False,
error_if_core=True,
)
self._options = value
class OptionsFlowWithConfigEntry(OptionsFlow):
"""Base class for options flows with config entry and options."""
@ -3127,11 +3150,12 @@ class OptionsFlowWithConfigEntry(OptionsFlow):
"""Initialize options flow."""
self._config_entry = config_entry
self._options = deepcopy(dict(config_entry.options))
@property
def options(self) -> dict[str, Any]:
"""Return a mutable copy of the config entry options."""
return self._options
report(
"inherits from OptionsFlowWithConfigEntry, which is deprecated "
"and will stop working in 2025.12",
error_if_integration=False,
error_if_core=False,
)
class EntityRegistryDisabledHandler:

View File

@ -4812,6 +4812,7 @@ async def test_reauth_reconfigure_missing_entry(
@pytest.mark.usefixtures("mock_integration_frame")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
@pytest.mark.parametrize(
"source", [config_entries.SOURCE_REAUTH, config_entries.SOURCE_RECONFIGURE]
)
@ -5039,15 +5040,21 @@ async def test_async_wait_component_startup(hass: HomeAssistant) -> None:
assert "test" in hass.config.components
async def test_options_flow_options_not_mutated() -> None:
@pytest.mark.usefixtures("mock_integration_frame")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_options_flow_with_config_entry(caplog: pytest.LogCaptureFixture) -> None:
"""Test that OptionsFlowWithConfigEntry doesn't mutate entry options."""
entry = MockConfigEntry(
domain="test",
domain="hue",
data={"first": True},
options={"sub_dict": {"1": "one"}, "sub_list": ["one"]},
)
options_flow = config_entries.OptionsFlowWithConfigEntry(entry)
assert (
"Detected that integration 'hue' inherits from OptionsFlowWithConfigEntry,"
" which is deprecated and will stop working in 2025.12" in caplog.text
)
options_flow._options["sub_dict"]["2"] = "two"
options_flow._options["sub_list"].append("two")
@ -5059,6 +5066,31 @@ async def test_options_flow_options_not_mutated() -> None:
assert entry.options == {"sub_dict": {"1": "one"}, "sub_list": ["one"]}
@pytest.mark.usefixtures("mock_integration_frame")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_options_flow_options_not_mutated(hass: HomeAssistant) -> None:
"""Test that OptionsFlow doesn't mutate entry options."""
entry = MockConfigEntry(
domain="test",
data={"first": True},
options={"sub_dict": {"1": "one"}, "sub_list": ["one"]},
)
entry.add_to_hass(hass)
options_flow = config_entries.OptionsFlow()
options_flow.handler = entry.entry_id
options_flow.hass = hass
options_flow.options["sub_dict"]["2"] = "two"
options_flow._options["sub_list"].append("two")
assert options_flow._options == {
"sub_dict": {"1": "one", "2": "two"},
"sub_list": ["one", "two"],
}
assert entry.options == {"sub_dict": {"1": "one"}, "sub_list": ["one"]}
async def test_initializing_flows_canceled_on_shutdown(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
@ -7405,7 +7437,6 @@ async def test_options_flow_config_entry(
@pytest.mark.usefixtures("mock_integration_frame")
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
async def test_options_flow_deprecated_config_entry_setter(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
@ -7433,7 +7464,10 @@ async def test_options_flow_deprecated_config_entry_setter(
def __init__(self, entry) -> None:
"""Test initialisation."""
self.config_entry = entry
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
self.config_entry = entry
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
self.options = entry.options
async def async_step_init(self, user_input=None):
"""Test user step."""
@ -7462,6 +7496,10 @@ async def test_options_flow_deprecated_config_entry_setter(
"Detected that integration 'hue' sets option flow config_entry explicitly, "
"which is deprecated and will stop working in 2025.12" in caplog.text
)
assert (
"Detected that integration 'hue' sets option flow options explicitly, "
"which is deprecated and will stop working in 2025.12" in caplog.text
)
async def test_add_description_placeholder_automatically(