Skip to content

Commit ab5b9db

Browse files
authored
Add OptionsFlow helpers to get the current config entry (#129562)
* Add OptionsFlow helpers to get the current config entry * Add tests * Improve * Add ValueError to indicate that the config entry is not available in `__init__` method * Use a property * Update config_entries.py * Update config_entries.py * Update config_entries.py * Add a property setter for compatibility * Add report * Update config_flow.py * Add tests * Update test_config_entries.py
1 parent 3b28bf0 commit ab5b9db

File tree

3 files changed

+211
-21
lines changed

3 files changed

+211
-21
lines changed

homeassistant/components/airnow/config_flow.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Config flow for AirNow integration."""
22

3+
from __future__ import annotations
4+
35
import logging
46
from typing import Any
57

@@ -12,7 +14,6 @@
1214
ConfigFlow,
1315
ConfigFlowResult,
1416
OptionsFlow,
15-
OptionsFlowWithConfigEntry,
1617
)
1718
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
1819
from homeassistant.core import HomeAssistant, callback
@@ -120,12 +121,12 @@ async def async_step_user(
120121
@callback
121122
def async_get_options_flow(
122123
config_entry: ConfigEntry,
123-
) -> OptionsFlow:
124+
) -> AirNowOptionsFlowHandler:
124125
"""Return the options flow."""
125-
return AirNowOptionsFlowHandler(config_entry)
126+
return AirNowOptionsFlowHandler()
126127

127128

128-
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
129+
class AirNowOptionsFlowHandler(OptionsFlow):
129130
"""Handle an options flow for AirNow."""
130131

131132
async def async_step_init(
@@ -136,12 +137,7 @@ async def async_step_init(
136137
return self.async_create_entry(data=user_input)
137138

138139
options_schema = vol.Schema(
139-
{
140-
vol.Optional(CONF_RADIUS): vol.All(
141-
int,
142-
vol.Range(min=5),
143-
),
144-
}
140+
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
145141
)
146142

147143
return self.async_show_form(

homeassistant/config_entries.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3055,6 +3055,9 @@ class OptionsFlow(ConfigEntryBaseFlow):
30553055

30563056
handler: str
30573057

3058+
_config_entry: ConfigEntry
3059+
"""For compatibility only - to be removed in 2025.12"""
3060+
30583061
@callback
30593062
def _async_abort_entries_match(
30603063
self, match_dict: dict[str, Any] | None = None
@@ -3063,19 +3066,59 @@ def _async_abort_entries_match(
30633066
30643067
Requires `already_configured` in strings.json in user visible flows.
30653068
"""
3066-
3067-
config_entry = cast(
3068-
ConfigEntry, self.hass.config_entries.async_get_entry(self.handler)
3069-
)
30703069
_async_abort_entries_match(
30713070
[
30723071
entry
3073-
for entry in self.hass.config_entries.async_entries(config_entry.domain)
3074-
if entry is not config_entry and entry.source != SOURCE_IGNORE
3072+
for entry in self.hass.config_entries.async_entries(
3073+
self.config_entry.domain
3074+
)
3075+
if entry is not self.config_entry and entry.source != SOURCE_IGNORE
30753076
],
30763077
match_dict,
30773078
)
30783079

3080+
@property
3081+
def _config_entry_id(self) -> str:
3082+
"""Return config entry id.
3083+
3084+
Please note that this is not available inside `__init__` method, and
3085+
can only be referenced after initialisation.
3086+
"""
3087+
# This is the same as handler, but that's an implementation detail
3088+
if self.handler is None:
3089+
raise ValueError(
3090+
"The config entry id is not available during initialisation"
3091+
)
3092+
return self.handler
3093+
3094+
@property
3095+
def config_entry(self) -> ConfigEntry:
3096+
"""Return the config entry linked to the current options flow.
3097+
3098+
Please note that this is not available inside `__init__` method, and
3099+
can only be referenced after initialisation.
3100+
"""
3101+
# For compatibility only - to be removed in 2025.12
3102+
if hasattr(self, "_config_entry"):
3103+
return self._config_entry
3104+
3105+
if self.hass is None:
3106+
raise ValueError("The config entry is not available during initialisation")
3107+
if entry := self.hass.config_entries.async_get_entry(self._config_entry_id):
3108+
return entry
3109+
raise UnknownEntry
3110+
3111+
@config_entry.setter
3112+
def config_entry(self, value: ConfigEntry) -> None:
3113+
"""Set the config entry value."""
3114+
report(
3115+
"sets option flow config_entry explicitly, which is deprecated "
3116+
"and will stop working in 2025.12",
3117+
error_if_integration=False,
3118+
error_if_core=True,
3119+
)
3120+
self._config_entry = value
3121+
30793122

30803123
class OptionsFlowWithConfigEntry(OptionsFlow):
30813124
"""Base class for options flows with config entry and options."""
@@ -3085,11 +3128,6 @@ def __init__(self, config_entry: ConfigEntry) -> None:
30853128
self._config_entry = config_entry
30863129
self._options = deepcopy(dict(config_entry.options))
30873130

3088-
@property
3089-
def config_entry(self) -> ConfigEntry:
3090-
"""Return the config entry."""
3091-
return self._config_entry
3092-
30933131
@property
30943132
def options(self) -> dict[str, Any]:
30953133
"""Return a mutable copy of the config entry options."""

tests/test_config_entries.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7308,6 +7308,162 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
73087308
assert config_entries.current_entry.get() is None
73097309

73107310

7311+
async def test_options_flow_config_entry(
7312+
hass: HomeAssistant, manager: config_entries.ConfigEntries
7313+
) -> None:
7314+
"""Test _config_entry_id and config_entry properties in options flow."""
7315+
original_entry = MockConfigEntry(domain="test", data={})
7316+
original_entry.add_to_hass(hass)
7317+
7318+
mock_setup_entry = AsyncMock(return_value=True)
7319+
7320+
mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
7321+
mock_platform(hass, "test.config_flow", None)
7322+
7323+
class TestFlow(config_entries.ConfigFlow):
7324+
"""Test flow."""
7325+
7326+
@staticmethod
7327+
@callback
7328+
def async_get_options_flow(config_entry):
7329+
"""Test options flow."""
7330+
7331+
class _OptionsFlow(config_entries.OptionsFlow):
7332+
"""Test flow."""
7333+
7334+
def __init__(self) -> None:
7335+
"""Test initialisation."""
7336+
try:
7337+
self.init_entry_id = self._config_entry_id
7338+
except ValueError as err:
7339+
self.init_entry_id = err
7340+
try:
7341+
self.init_entry = self.config_entry
7342+
except ValueError as err:
7343+
self.init_entry = err
7344+
7345+
async def async_step_init(self, user_input=None):
7346+
"""Test user step."""
7347+
errors = {}
7348+
if user_input is not None:
7349+
if user_input.get("abort"):
7350+
return self.async_abort(reason="abort")
7351+
7352+
errors["entry_id"] = self._config_entry_id
7353+
try:
7354+
errors["entry"] = self.config_entry
7355+
except config_entries.UnknownEntry as err:
7356+
errors["entry"] = err
7357+
7358+
return self.async_show_form(step_id="init", errors=errors)
7359+
7360+
return _OptionsFlow()
7361+
7362+
with mock_config_flow("test", TestFlow):
7363+
result = await hass.config_entries.options.async_init(original_entry.entry_id)
7364+
7365+
options_flow = hass.config_entries.options._progress.get(result["flow_id"])
7366+
assert isinstance(options_flow, config_entries.OptionsFlow)
7367+
assert options_flow.handler == original_entry.entry_id
7368+
assert isinstance(options_flow.init_entry_id, ValueError)
7369+
assert (
7370+
str(options_flow.init_entry_id)
7371+
== "The config entry id is not available during initialisation"
7372+
)
7373+
assert isinstance(options_flow.init_entry, ValueError)
7374+
assert (
7375+
str(options_flow.init_entry)
7376+
== "The config entry is not available during initialisation"
7377+
)
7378+
7379+
assert result["type"] == FlowResultType.FORM
7380+
assert result["step_id"] == "init"
7381+
assert result["errors"] == {}
7382+
7383+
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
7384+
assert result["type"] == FlowResultType.FORM
7385+
assert result["step_id"] == "init"
7386+
assert result["errors"]["entry_id"] == original_entry.entry_id
7387+
assert result["errors"]["entry"] is original_entry
7388+
7389+
# Bad handler - not linked to a config entry
7390+
options_flow.handler = "123"
7391+
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
7392+
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
7393+
assert result["type"] == FlowResultType.FORM
7394+
assert result["step_id"] == "init"
7395+
assert result["errors"]["entry_id"] == "123"
7396+
assert isinstance(result["errors"]["entry"], config_entries.UnknownEntry)
7397+
# Reset handler
7398+
options_flow.handler = original_entry.entry_id
7399+
7400+
result = await hass.config_entries.options.async_configure(
7401+
result["flow_id"], {"abort": True}
7402+
)
7403+
assert result["type"] == FlowResultType.ABORT
7404+
assert result["reason"] == "abort"
7405+
7406+
7407+
@pytest.mark.usefixtures("mock_integration_frame")
7408+
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
7409+
async def test_options_flow_deprecated_config_entry_setter(
7410+
hass: HomeAssistant,
7411+
manager: config_entries.ConfigEntries,
7412+
caplog: pytest.LogCaptureFixture,
7413+
) -> None:
7414+
"""Test that setting config_entry explicitly still works."""
7415+
original_entry = MockConfigEntry(domain="hue", data={})
7416+
original_entry.add_to_hass(hass)
7417+
7418+
mock_setup_entry = AsyncMock(return_value=True)
7419+
7420+
mock_integration(hass, MockModule("hue", async_setup_entry=mock_setup_entry))
7421+
mock_platform(hass, "hue.config_flow", None)
7422+
7423+
class TestFlow(config_entries.ConfigFlow):
7424+
"""Test flow."""
7425+
7426+
@staticmethod
7427+
@callback
7428+
def async_get_options_flow(config_entry):
7429+
"""Test options flow."""
7430+
7431+
class _OptionsFlow(config_entries.OptionsFlow):
7432+
"""Test flow."""
7433+
7434+
def __init__(self, entry) -> None:
7435+
"""Test initialisation."""
7436+
self.config_entry = entry
7437+
7438+
async def async_step_init(self, user_input=None):
7439+
"""Test user step."""
7440+
errors = {}
7441+
if user_input is not None:
7442+
if user_input.get("abort"):
7443+
return self.async_abort(reason="abort")
7444+
7445+
errors["entry_id"] = self._config_entry_id
7446+
try:
7447+
errors["entry"] = self.config_entry
7448+
except config_entries.UnknownEntry as err:
7449+
errors["entry"] = err
7450+
7451+
return self.async_show_form(step_id="init", errors=errors)
7452+
7453+
return _OptionsFlow(config_entry)
7454+
7455+
with mock_config_flow("hue", TestFlow):
7456+
result = await hass.config_entries.options.async_init(original_entry.entry_id)
7457+
7458+
options_flow = hass.config_entries.options._progress.get(result["flow_id"])
7459+
assert options_flow.config_entry is original_entry
7460+
7461+
assert (
7462+
"Detected that integration 'hue' sets option flow config_entry explicitly, "
7463+
"which is deprecated and will stop working in 2025.12" in caplog.text
7464+
)
7465+
7466+
73117467
async def test_add_description_placeholder_automatically(
73127468
hass: HomeAssistant,
73137469
manager: config_entries.ConfigEntries,

0 commit comments

Comments
 (0)