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
pull/129637/head
epenet 2024-11-01 12:54:35 +01:00 committed by GitHub
parent 3b28bf07d1
commit ab5b9dbdc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 211 additions and 21 deletions

View File

@ -1,5 +1,7 @@
"""Config flow for AirNow integration."""
from __future__ import annotations
import logging
from typing import Any
@ -12,7 +14,6 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback
@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
@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(
@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
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(

View File

@ -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
@ -3063,19 +3066,59 @@ class OptionsFlow(ConfigEntryBaseFlow):
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."""
@ -3085,11 +3128,6 @@ class OptionsFlowWithConfigEntry(OptionsFlow):
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."""

View File

@ -7308,6 +7308,162 @@ async def test_context_no_leak(hass: HomeAssistant) -> None:
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,