mirror of
https://github.com/home-assistant/core.git
synced 2025-07-09 22:37:11 +00:00
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
This commit is contained in:
parent
3b28bf07d1
commit
ab5b9dbdc9
@ -1,5 +1,7 @@
|
|||||||
"""Config flow for AirNow integration."""
|
"""Config flow for AirNow integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -12,7 +14,6 @@ from homeassistant.config_entries import (
|
|||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlow,
|
||||||
OptionsFlowWithConfigEntry,
|
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -120,12 +121,12 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> OptionsFlow:
|
) -> AirNowOptionsFlowHandler:
|
||||||
"""Return the options flow."""
|
"""Return the options flow."""
|
||||||
return AirNowOptionsFlowHandler(config_entry)
|
return AirNowOptionsFlowHandler()
|
||||||
|
|
||||||
|
|
||||||
class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
class AirNowOptionsFlowHandler(OptionsFlow):
|
||||||
"""Handle an options flow for AirNow."""
|
"""Handle an options flow for AirNow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
@ -136,12 +137,7 @@ class AirNowOptionsFlowHandler(OptionsFlowWithConfigEntry):
|
|||||||
return self.async_create_entry(data=user_input)
|
return self.async_create_entry(data=user_input)
|
||||||
|
|
||||||
options_schema = vol.Schema(
|
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(
|
return self.async_show_form(
|
||||||
|
@ -3055,6 +3055,9 @@ class OptionsFlow(ConfigEntryBaseFlow):
|
|||||||
|
|
||||||
handler: str
|
handler: str
|
||||||
|
|
||||||
|
_config_entry: ConfigEntry
|
||||||
|
"""For compatibility only - to be removed in 2025.12"""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_abort_entries_match(
|
def _async_abort_entries_match(
|
||||||
self, match_dict: dict[str, Any] | None = None
|
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.
|
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(
|
_async_abort_entries_match(
|
||||||
[
|
[
|
||||||
entry
|
entry
|
||||||
for entry in self.hass.config_entries.async_entries(config_entry.domain)
|
for entry in self.hass.config_entries.async_entries(
|
||||||
if entry is not config_entry and entry.source != SOURCE_IGNORE
|
self.config_entry.domain
|
||||||
|
)
|
||||||
|
if entry is not self.config_entry and entry.source != SOURCE_IGNORE
|
||||||
],
|
],
|
||||||
match_dict,
|
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):
|
class OptionsFlowWithConfigEntry(OptionsFlow):
|
||||||
"""Base class for options flows with config entry and options."""
|
"""Base class for options flows with config entry and options."""
|
||||||
@ -3085,11 +3128,6 @@ class OptionsFlowWithConfigEntry(OptionsFlow):
|
|||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
self._options = deepcopy(dict(config_entry.options))
|
self._options = deepcopy(dict(config_entry.options))
|
||||||
|
|
||||||
@property
|
|
||||||
def config_entry(self) -> ConfigEntry:
|
|
||||||
"""Return the config entry."""
|
|
||||||
return self._config_entry
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def options(self) -> dict[str, Any]:
|
def options(self) -> dict[str, Any]:
|
||||||
"""Return a mutable copy of the config entry options."""
|
"""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
|
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(
|
async def test_add_description_placeholder_automatically(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
manager: config_entries.ConfigEntries,
|
manager: config_entries.ConfigEntries,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user