diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index e94f734069a..6e6b44cd4b8 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -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.""" diff --git a/homeassistant/components/onvif/config_flow.py b/homeassistant/components/onvif/config_flow.py index 34f322b9f75..830f74b94e8 100644 --- a/homeassistant/components/onvif/config_flow.py +++ b/homeassistant/components/onvif/config_flow.py @@ -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() diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index 24bf89b24a6..45395bd282a 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -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 diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 971fd7d5726..f533a62e753 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -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: diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 6959dc3d3ce..e3f1d110ac0 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -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(