Add async_update_reload_and_abort to config entry subentries (#149768)

Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
This commit is contained in:
G Johansson
2025-08-13 11:59:37 +02:00
committed by GitHub
parent 5a7f7d90a0
commit 5fc2e6ed53
3 changed files with 336 additions and 6 deletions

View File

@@ -146,7 +146,7 @@ class SubentryFlowHandler(ConfigSubentryFlow):
"""Reconfigure a sensor.""" """Reconfigure a sensor."""
if user_input is not None: if user_input is not None:
title = user_input.pop("name") title = user_input.pop("name")
return self.async_update_and_abort( return self.async_update_reload_and_abort(
self._get_entry(), self._get_entry(),
self._get_reconfigure_subentry(), self._get_reconfigure_subentry(),
data=user_input, data=user_input,

View File

@@ -3385,6 +3385,34 @@ class ConfigSubentryFlow(
return result return result
@callback
def _async_update(
self,
entry: ConfigEntry,
subentry: ConfigSubentry,
*,
unique_id: str | None | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED,
) -> bool:
"""Update config subentry and return result.
Internal to be used by update_and_abort and update_reload_and_abort methods only.
"""
if data_updates is not UNDEFINED:
if data is not UNDEFINED:
raise ValueError("Cannot set both data and data_updates")
data = subentry.data | data_updates
return self.hass.config_entries.async_update_subentry(
entry=entry,
subentry=subentry,
unique_id=unique_id,
title=title,
data=data,
)
@callback @callback
def async_update_and_abort( def async_update_and_abort(
self, self,
@@ -3404,19 +3432,52 @@ class ConfigSubentryFlow(
:param title: replace the title of the subentry :param title: replace the title of the subentry
:param unique_id: replace the unique_id of the subentry :param unique_id: replace the unique_id of the subentry
""" """
if data_updates is not UNDEFINED: self._async_update(
if data is not UNDEFINED:
raise ValueError("Cannot set both data and data_updates")
data = subentry.data | data_updates
self.hass.config_entries.async_update_subentry(
entry=entry, entry=entry,
subentry=subentry, subentry=subentry,
unique_id=unique_id, unique_id=unique_id,
title=title, title=title,
data=data, data=data,
data_updates=data_updates,
) )
return self.async_abort(reason="reconfigure_successful") return self.async_abort(reason="reconfigure_successful")
@callback
def async_update_reload_and_abort(
self,
entry: ConfigEntry,
subentry: ConfigSubentry,
*,
unique_id: str | None | UndefinedType = UNDEFINED,
title: str | UndefinedType = UNDEFINED,
data: Mapping[str, Any] | UndefinedType = UNDEFINED,
data_updates: Mapping[str, Any] | UndefinedType = UNDEFINED,
reload_even_if_entry_is_unchanged: bool = True,
) -> SubentryFlowResult:
"""Update config subentry, reload config entry and finish subentry flow.
:param data: replace the subentry data with new data
:param data_updates: add items from data_updates to subentry data - existing
keys are overridden
:param title: replace the title of the subentry
:param unique_id: replace the unique_id of the subentry
:param reload_even_if_entry_is_unchanged: set this to `False` if the entry
should not be reloaded if it is unchanged
"""
result = self._async_update(
entry=entry,
subentry=subentry,
unique_id=unique_id,
title=title,
data=data,
data_updates=data_updates,
)
if reload_even_if_entry_is_unchanged or result:
if entry.update_listeners:
raise ValueError("Cannot update and reload entry with update listeners")
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="reconfigure_successful")
@property @property
def _entry_id(self) -> str: def _entry_id(self) -> str:
"""Return config entry id.""" """Return config entry id."""

View File

@@ -6519,6 +6519,275 @@ async def test_update_subentry_and_abort(
assert result["reason"] == "reconfigure_successful" assert result["reason"] == "reconfigure_successful"
@pytest.mark.parametrize(
(
"kwargs",
"expected_title",
"expected_unique_id",
"expected_data",
"raises",
"reload", # True is default
"setup_call_count",
"expected_result",
),
[
(
{
"unique_id": "5678",
"title": "Updated title",
"data": {"vendor": "data2"},
},
"Updated title",
"5678",
{"vendor": "data2"},
does_not_raise(),
True,
2,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{
"unique_id": "1234",
"title": "Test",
"data": {"vendor": "data"},
},
"Test",
"1234",
{"vendor": "data"},
does_not_raise(),
True,
2,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{
"unique_id": "1234",
"title": "Test",
"data": {"vendor": "data"},
},
"Test",
"1234",
{"vendor": "data"},
does_not_raise(),
False,
1,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{},
"Test",
"1234",
{"vendor": "data"},
does_not_raise(),
True,
2,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{
"data": {"buyer": "me"},
},
"Test",
"1234",
{"buyer": "me"},
does_not_raise(),
True,
2,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{"data_updates": {"buyer": "me"}},
"Test",
"1234",
{"vendor": "data", "buyer": "me"},
does_not_raise(),
True,
2,
{
"type": FlowResultType.ABORT,
"reason": "reconfigure_successful",
"description_placeholders": None,
},
),
(
{
"unique_id": "5678",
"title": "Updated title",
"data": {"vendor": "data2"},
"data_updates": {"buyer": "me"},
},
"Test",
"1234",
{"vendor": "data"},
pytest.raises(ValueError),
True,
1,
{},
),
],
ids=[
"changed_entry_default",
"unchanged_entry_default",
"unchanged_entry_no_reload",
"no_kwargs",
"replace_data",
"update_data",
"update_and_data_raises",
],
)
async def test_update_subentry_reload_and_abort(
hass: HomeAssistant,
expected_title: str,
expected_unique_id: str,
expected_data: dict[str, Any],
kwargs: dict[str, Any],
raises: AbstractContextManager,
reload: bool,
setup_call_count: int,
expected_result: dict[str, Any],
) -> None:
"""Test updating an entry and reloading."""
subentry_id = "blabla"
entry = MockConfigEntry(
domain="comp",
unique_id="entry_unique_id",
title="entry_title",
data={},
subentries_data=[
config_entries.ConfigSubentryData(
data={"vendor": "data"},
subentry_id=subentry_id,
subentry_type="test",
unique_id="1234",
title="Test",
)
],
)
entry.add_to_hass(hass)
subentry = entry.subentries[subentry_id]
setup_entry = AsyncMock(return_value=True)
comp = MockModule(
"comp",
async_setup_entry=setup_entry,
async_unload_entry=AsyncMock(return_value=True),
)
mock_integration(hass, comp)
mock_platform(hass, "comp.config_flow", None)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
class TestFlow(config_entries.ConfigFlow):
class SubentryFlowHandler(config_entries.ConfigSubentryFlow):
async def async_step_reconfigure(self, user_input=None):
return self.async_update_reload_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
**kwargs,
reload_even_if_entry_is_unchanged=reload,
)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: config_entries.ConfigEntry
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with mock_config_flow("comp", TestFlow), raises:
result = await entry.start_subentry_reconfigure_flow(hass, subentry_id)
await hass.async_block_till_done()
subentry = entry.subentries[subentry_id]
assert subentry.title == expected_title
assert subentry.unique_id == expected_unique_id
assert subentry.data == expected_data
assert setup_entry.call_count == setup_call_count
for k, v in expected_result.items():
assert result[k] == v
async def test_update_subentry_reload_with_listener(hass: HomeAssistant) -> None:
"""Test updating an entry and reloading fails with update listener."""
subentry_id = "blabla"
entry = MockConfigEntry(
domain="comp",
unique_id="entry_unique_id",
title="entry_title",
data={},
subentries_data=[
config_entries.ConfigSubentryData(
data={"vendor": "data"},
subentry_id=subentry_id,
subentry_type="test",
unique_id="1234",
title="Test",
)
],
)
entry.add_to_hass(hass)
entry.add_update_listener(AsyncMock())
setup_entry = AsyncMock(return_value=True)
comp = MockModule(
"comp",
async_setup_entry=setup_entry,
async_unload_entry=AsyncMock(return_value=True),
)
mock_integration(hass, comp)
mock_platform(hass, "comp.config_flow", None)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
class TestFlow(config_entries.ConfigFlow):
class SubentryFlowHandler(config_entries.ConfigSubentryFlow):
async def async_step_reconfigure(self, user_input=None):
return self.async_update_reload_and_abort(
self._get_entry(),
self._get_reconfigure_subentry(),
data={},
reload_even_if_entry_is_unchanged=True,
)
@classmethod
@callback
def async_get_supported_subentry_types(
cls, config_entry: config_entries.ConfigEntry
) -> dict[str, type[config_entries.ConfigSubentryFlow]]:
return {"test": TestFlow.SubentryFlowHandler}
with (
mock_config_flow("comp", TestFlow),
pytest.raises(
ValueError, match="Cannot update and reload entry with update listeners"
),
):
await entry.start_subentry_reconfigure_flow(hass, subentry_id)
async def test_reconfigure_subentry_create_subentry(hass: HomeAssistant) -> None: async def test_reconfigure_subentry_create_subentry(hass: HomeAssistant) -> None:
"""Test it's not allowed to create a subentry from a subentry reconfigure flow.""" """Test it's not allowed to create a subentry from a subentry reconfigure flow."""
subentry_id = "blabla" subentry_id = "blabla"