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.pypull/129637/head
parent
3b28bf07d1
commit
ab5b9dbdc9
|
@ -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(
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue