Skip to content

Commit

Permalink
Add OptionsFlow helpers to get the current config entry (home-assista…
Browse files Browse the repository at this point in the history
…nt#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
  • Loading branch information
epenet authored Nov 1, 2024
1 parent 3b28bf0 commit ab5b9db
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 21 deletions.
16 changes: 6 additions & 10 deletions homeassistant/components/airnow/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Config flow for AirNow integration."""

from __future__ import annotations

import logging
from typing import Any

Expand All @@ -12,7 +14,6 @@
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback
Expand Down Expand Up @@ -120,12 +121,12 @@ async def async_step_user(
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
) -> AirNowOptionsFlowHandler:
"""Return the options flow."""
return AirNowOptionsFlowHandler(config_entry)
return AirNowOptionsFlowHandler()


class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
class AirNowOptionsFlowHandler(OptionsFlow):
"""Handle an options flow for AirNow."""

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

options_schema = vol.Schema(
{
vol.Optional(CONF_RADIUS): vol.All(
int,
vol.Range(min=5),
),
}
{vol.Optional(CONF_RADIUS): vol.All(int, vol.Range(min=5))}
)

return self.async_show_form(
Expand Down
60 changes: 49 additions & 11 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3055,6 +3055,9 @@ class OptionsFlow(ConfigEntryBaseFlow):

handler: str

_config_entry: ConfigEntry
"""For compatibility only - to be removed in 2025.12"""

@callback
def _async_abort_entries_match(
self, match_dict: dict[str, Any] | None = None
Expand All @@ -3063,19 +3066,59 @@ def _async_abort_entries_match(
Requires `already_configured` in strings.json in user visible flows.
"""

config_entry = cast(
ConfigEntry, self.hass.config_entries.async_get_entry(self.handler)
)
_async_abort_entries_match(
[
entry
for entry in self.hass.config_entries.async_entries(config_entry.domain)
if entry is not config_entry and entry.source != SOURCE_IGNORE
for entry in self.hass.config_entries.async_entries(
self.config_entry.domain
)
if entry is not self.config_entry and entry.source != SOURCE_IGNORE
],
match_dict,
)

@property
def _config_entry_id(self) -> str:
"""Return config entry id.
Please note that this is not available inside `__init__` method, and
can only be referenced after initialisation.
"""
# This is the same as handler, but that's an implementation detail
if self.handler is None:
raise ValueError(
"The config entry id is not available during initialisation"
)
return self.handler

@property
def config_entry(self) -> ConfigEntry:
"""Return the config entry linked to the current options flow.
Please note that this is not available inside `__init__` method, and
can only be referenced after initialisation.
"""
# For compatibility only - to be removed in 2025.12
if hasattr(self, "_config_entry"):
return self._config_entry

if self.hass is None:
raise ValueError("The config entry is not available during initialisation")
if entry := self.hass.config_entries.async_get_entry(self._config_entry_id):
return entry
raise UnknownEntry

@config_entry.setter
def config_entry(self, value: ConfigEntry) -> None:
"""Set the config entry value."""
report(
"sets option flow config_entry explicitly, which is deprecated "
"and will stop working in 2025.12",
error_if_integration=False,
error_if_core=True,
)
self._config_entry = value


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

@property
def config_entry(self) -> ConfigEntry:
"""Return the config entry."""
return self._config_entry

@property
def options(self) -> dict[str, Any]:
"""Return a mutable copy of the config entry options."""
Expand Down
156 changes: 156 additions & 0 deletions tests/test_config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7308,6 +7308,162 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
assert config_entries.current_entry.get() is None


async def test_options_flow_config_entry(
hass: HomeAssistant, manager: config_entries.ConfigEntries
) -> None:
"""Test _config_entry_id and config_entry properties in options flow."""
original_entry = MockConfigEntry(domain="test", data={})
original_entry.add_to_hass(hass)

mock_setup_entry = AsyncMock(return_value=True)

mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry))
mock_platform(hass, "test.config_flow", None)

class TestFlow(config_entries.ConfigFlow):
"""Test flow."""

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Test options flow."""

class _OptionsFlow(config_entries.OptionsFlow):
"""Test flow."""

def __init__(self) -> None:
"""Test initialisation."""
try:
self.init_entry_id = self._config_entry_id
except ValueError as err:
self.init_entry_id = err
try:
self.init_entry = self.config_entry
except ValueError as err:
self.init_entry = err

async def async_step_init(self, user_input=None):
"""Test user step."""
errors = {}
if user_input is not None:
if user_input.get("abort"):
return self.async_abort(reason="abort")

errors["entry_id"] = self._config_entry_id
try:
errors["entry"] = self.config_entry
except config_entries.UnknownEntry as err:
errors["entry"] = err

return self.async_show_form(step_id="init", errors=errors)

return _OptionsFlow()

with mock_config_flow("test", TestFlow):
result = await hass.config_entries.options.async_init(original_entry.entry_id)

options_flow = hass.config_entries.options._progress.get(result["flow_id"])
assert isinstance(options_flow, config_entries.OptionsFlow)
assert options_flow.handler == original_entry.entry_id
assert isinstance(options_flow.init_entry_id, ValueError)
assert (
str(options_flow.init_entry_id)
== "The config entry id is not available during initialisation"
)
assert isinstance(options_flow.init_entry, ValueError)
assert (
str(options_flow.init_entry)
== "The config entry is not available during initialisation"
)

assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
assert result["errors"] == {}

result = await hass.config_entries.options.async_configure(result["flow_id"], {})
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
assert result["errors"]["entry_id"] == original_entry.entry_id
assert result["errors"]["entry"] is original_entry

# Bad handler - not linked to a config entry
options_flow.handler = "123"
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
result = await hass.config_entries.options.async_configure(result["flow_id"], {})
assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "init"
assert result["errors"]["entry_id"] == "123"
assert isinstance(result["errors"]["entry"], config_entries.UnknownEntry)
# Reset handler
options_flow.handler = original_entry.entry_id

result = await hass.config_entries.options.async_configure(
result["flow_id"], {"abort": True}
)
assert result["type"] == FlowResultType.ABORT
assert result["reason"] == "abort"


@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,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that setting config_entry explicitly still works."""
original_entry = MockConfigEntry(domain="hue", data={})
original_entry.add_to_hass(hass)

mock_setup_entry = AsyncMock(return_value=True)

mock_integration(hass, MockModule("hue", async_setup_entry=mock_setup_entry))
mock_platform(hass, "hue.config_flow", None)

class TestFlow(config_entries.ConfigFlow):
"""Test flow."""

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Test options flow."""

class _OptionsFlow(config_entries.OptionsFlow):
"""Test flow."""

def __init__(self, entry) -> None:
"""Test initialisation."""
self.config_entry = entry

async def async_step_init(self, user_input=None):
"""Test user step."""
errors = {}
if user_input is not None:
if user_input.get("abort"):
return self.async_abort(reason="abort")

errors["entry_id"] = self._config_entry_id
try:
errors["entry"] = self.config_entry
except config_entries.UnknownEntry as err:
errors["entry"] = err

return self.async_show_form(step_id="init", errors=errors)

return _OptionsFlow(config_entry)

with mock_config_flow("hue", TestFlow):
result = await hass.config_entries.options.async_init(original_entry.entry_id)

options_flow = hass.config_entries.options._progress.get(result["flow_id"])
assert options_flow.config_entry is original_entry

assert (
"Detected that integration 'hue' sets option flow config_entry explicitly, "
"which is deprecated and will stop working in 2025.12" in caplog.text
)


async def test_add_description_placeholder_automatically(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
Expand Down

0 comments on commit ab5b9db

Please sign in to comment.