Remove async_late_forward_entry_setups and instead implicitly hold the lock (#119088)

* Refactor config entry forwards to implictly obtain the lock instead of explictly

This is a bit of a tradeoff to not need async_late_forward_entry_setups

The downside is we can no longer detect non-awaited plastform setups
as we will always implicitly obtain the lock instead of explictly.

Note, this PR is incomplete and is only for discussion purposes
at this point

* preen

* cover

* cover

* restore check for non-awaited platform setup

* cleanup

* fix missing word

* make non-awaited test safer
This commit is contained in:
J. Nick Koston 2024-06-12 20:06:11 -05:00 committed by GitHub
parent 4e121fcbe8
commit dbd3147c9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 381 additions and 188 deletions

View file

@ -182,7 +182,7 @@ class AmbientStation:
# already been done): # already been done):
if not self._entry_setup_complete: if not self._entry_setup_complete:
self._hass.async_create_task( self._hass.async_create_task(
self._hass.config_entries.async_late_forward_entry_setups( self._hass.config_entries.async_forward_entry_setups(
self._entry, PLATFORMS self._entry, PLATFORMS
), ),
eager_start=True, eager_start=True,

View file

@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -
async def _async_finish_startup(_: HomeAssistant) -> None: async def _async_finish_startup(_: HomeAssistant) -> None:
await coordinator.async_refresh() await coordinator.async_refresh()
await hass.config_entries.async_late_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async_at_started(hass, _async_finish_startup) async_at_started(hass, _async_finish_startup)
return True return True

View file

@ -248,15 +248,9 @@ class RuntimeEntryData:
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: ConfigEntry,
platforms: set[Platform], platforms: set[Platform],
late: bool,
) -> None: ) -> None:
async with self.platform_load_lock: async with self.platform_load_lock:
if needed := platforms - self.loaded_platforms: if needed := platforms - self.loaded_platforms:
if late:
await hass.config_entries.async_late_forward_entry_setups(
entry, needed
)
else:
await hass.config_entries.async_forward_entry_setups(entry, needed) await hass.config_entries.async_forward_entry_setups(entry, needed)
self.loaded_platforms |= needed self.loaded_platforms |= needed
@ -266,7 +260,6 @@ class RuntimeEntryData:
entry: ConfigEntry, entry: ConfigEntry,
infos: list[EntityInfo], infos: list[EntityInfo],
mac: str, mac: str,
late: bool = False,
) -> None: ) -> None:
"""Distribute an update of static infos to all platforms.""" """Distribute an update of static infos to all platforms."""
# First, load all platforms # First, load all platforms
@ -296,7 +289,7 @@ class RuntimeEntryData:
): ):
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id) ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
await self._ensure_platforms_loaded(hass, entry, needed_platforms, late) await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Make a dict of the EntityInfo by type and send # Make a dict of the EntityInfo by type and send
# them to the listeners for each specific EntityInfo type # them to the listeners for each specific EntityInfo type

View file

@ -491,7 +491,7 @@ class ESPHomeManager:
entry_data.async_update_device_state() entry_data.async_update_device_state()
await entry_data.async_update_static_infos( await entry_data.async_update_static_infos(
hass, entry, entity_infos, device_info.mac_address, late=True hass, entry, entity_infos, device_info.mac_address
) )
_setup_services(hass, entry_data, services) _setup_services(hass, entry_data, services)

View file

@ -379,9 +379,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
new_config: list[ConfigType] = config_yaml.get(DOMAIN, []) new_config: list[ConfigType] = config_yaml.get(DOMAIN, [])
platforms_used = platforms_from_config(new_config) platforms_used = platforms_from_config(new_config)
new_platforms = platforms_used - mqtt_data.platforms_loaded new_platforms = platforms_used - mqtt_data.platforms_loaded
await async_forward_entry_setup_and_setup_discovery( await async_forward_entry_setup_and_setup_discovery(hass, entry, new_platforms)
hass, entry, new_platforms, late=True
)
# Check the schema before continuing reload # Check the schema before continuing reload
await async_check_config_schema(hass, config_yaml) await async_check_config_schema(hass, config_yaml)

View file

@ -211,7 +211,7 @@ async def async_start( # noqa: C901
async with platform_setup_lock.setdefault(component, asyncio.Lock()): async with platform_setup_lock.setdefault(component, asyncio.Lock()):
if component not in mqtt_data.platforms_loaded: if component not in mqtt_data.platforms_loaded:
await async_forward_entry_setup_and_setup_discovery( await async_forward_entry_setup_and_setup_discovery(
hass, config_entry, {component}, late=True hass, config_entry, {component}
) )
_async_add_component(discovery_payload) _async_add_component(discovery_payload)

View file

@ -72,11 +72,13 @@ async def async_forward_entry_setup_and_setup_discovery(
tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry)))
if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): if new_entity_platforms := (new_platforms - {"tag", "device_automation"}):
if late: tasks.append(
coro = hass.config_entries.async_late_forward_entry_setups create_eager_task(
else: hass.config_entries.async_forward_entry_setups(
coro = hass.config_entries.async_forward_entry_setups config_entry, new_entity_platforms
tasks.append(create_eager_task(coro(config_entry, new_entity_platforms))) )
)
)
if not tasks: if not tasks:
return return
await asyncio.gather(*tasks) await asyncio.gather(*tasks)

View file

@ -200,9 +200,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
self.hass.config_entries.async_update_entry(self.entry, data=data) self.hass.config_entries.async_update_entry(self.entry, data=data)
# Resume platform setup # Resume platform setup
await self.hass.config_entries.async_late_forward_entry_setups( await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms)
self.entry, platforms
)
return True return True

View file

@ -330,7 +330,7 @@ class DriverEvents:
"""Set up platform if needed.""" """Set up platform if needed."""
if platform not in self.platform_setup_tasks: if platform not in self.platform_setup_tasks:
self.platform_setup_tasks[platform] = self.hass.async_create_task( self.platform_setup_tasks[platform] = self.hass.async_create_task(
self.hass.config_entries.async_late_forward_entry_setups( self.hass.config_entries.async_forward_entry_setups(
self.config_entry, [platform] self.config_entry, [platform]
) )
) )

View file

@ -1170,18 +1170,13 @@ class FlowCancelledError(Exception):
"""Error to indicate that a flow has been cancelled.""" """Error to indicate that a flow has been cancelled."""
def _report_non_locked_platform_forwards(entry: ConfigEntry) -> None: def _report_non_awaited_platform_forwards(entry: ConfigEntry, what: str) -> None:
"""Report non awaited and non-locked platform forwards.""" """Report non awaited platform forwards."""
report( report(
f"calls async_forward_entry_setup after the entry for " f"calls {what} for integration {entry.domain} with "
f"integration, {entry.domain} with title: {entry.title} " f"title: {entry.title} and entry_id: {entry.entry_id}, "
f"and entry_id: {entry.entry_id}, has been set up, " f"during setup without awaiting {what}, which can cause "
"without holding the setup lock that prevents the config " "the setup lock to be released before the setup is done. "
"entry from being set up multiple times. "
"Instead await hass.config_entries.async_forward_entry_setup "
"during setup of the config entry or call "
"hass.config_entries.async_late_forward_entry_setups "
"in a tracked task. "
"This will stop working in Home Assistant 2025.1", "This will stop working in Home Assistant 2025.1",
error_if_integration=False, error_if_integration=False,
error_if_core=False, error_if_core=False,
@ -2041,9 +2036,6 @@ class ConfigEntries:
before the entry is set up. This ensures that the config entry cannot before the entry is set up. This ensures that the config entry cannot
be unloaded before all platforms are loaded. be unloaded before all platforms are loaded.
If platforms must be loaded late (after the config entry is setup),
use async_late_forward_entry_setup instead.
This method is more efficient than async_forward_entry_setup as This method is more efficient than async_forward_entry_setup as
it can load multiple platforms at once and does not require a separate it can load multiple platforms at once and does not require a separate
import executor job for each platform. import executor job for each platform.
@ -2052,14 +2044,32 @@ class ConfigEntries:
if not integration.platforms_are_loaded(platforms): if not integration.platforms_are_loaded(platforms):
with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS):
await integration.async_get_platforms(platforms) await integration.async_get_platforms(platforms)
if non_locked_platform_forwards := not entry.setup_lock.locked():
_report_non_locked_platform_forwards(entry) if not entry.setup_lock.locked():
async with entry.setup_lock:
if entry.state is not ConfigEntryState.LOADED:
raise OperationNotAllowed(
f"The config entry {entry.title} ({entry.domain}) with entry_id"
f" {entry.entry_id} cannot forward setup for {platforms} because it"
f" is not loaded in the {entry.state} state"
)
await self._async_forward_entry_setups_locked(entry, platforms)
else:
await self._async_forward_entry_setups_locked(entry, platforms)
# If the lock was held when we stated, and it was released during
# the platform setup, it means they did not await the setup call.
if not entry.setup_lock.locked():
_report_non_awaited_platform_forwards(
entry, "async_forward_entry_setups"
)
async def _async_forward_entry_setups_locked(
self, entry: ConfigEntry, platforms: Iterable[Platform | str]
) -> None:
await asyncio.gather( await asyncio.gather(
*( *(
create_eager_task( create_eager_task(
self._async_forward_entry_setup( self._async_forward_entry_setup(entry, platform, False),
entry, platform, False, non_locked_platform_forwards
),
name=( name=(
f"config entry forward setup {entry.title} " f"config entry forward setup {entry.title} "
f"{entry.domain} {entry.entry_id} {platform}" f"{entry.domain} {entry.entry_id} {platform}"
@ -2070,25 +2080,6 @@ class ConfigEntries:
) )
) )
async def async_late_forward_entry_setups(
self, entry: ConfigEntry, platforms: Iterable[Platform | str]
) -> None:
"""Forward the setup of an entry to platforms after setup.
If platforms must be loaded late (after the config entry is setup),
use this method instead of async_forward_entry_setups as it holds
the setup lock until the platforms are loaded to ensure that the
config entry cannot be unloaded while platforms are loaded.
"""
async with entry.setup_lock:
if entry.state is not ConfigEntryState.LOADED:
raise OperationNotAllowed(
f"The config entry {entry.title} ({entry.domain}) with entry_id"
f" {entry.entry_id} cannot forward setup for {platforms} "
f"because it is not loaded in the {entry.state} state"
)
await self.async_forward_entry_setups(entry, platforms)
async def async_forward_entry_setup( async def async_forward_entry_setup(
self, entry: ConfigEntry, domain: Platform | str self, entry: ConfigEntry, domain: Platform | str
) -> bool: ) -> bool:
@ -2103,13 +2094,7 @@ class ConfigEntries:
Instead, await async_forward_entry_setups as it can load Instead, await async_forward_entry_setups as it can load
multiple platforms at once and is more efficient since it multiple platforms at once and is more efficient since it
does not require a separate import executor job for each platform. does not require a separate import executor job for each platform.
If platforms must be loaded late (after the config entry is setup),
use async_late_forward_entry_setup instead.
""" """
if non_locked_platform_forwards := not entry.setup_lock.locked():
_report_non_locked_platform_forwards(entry)
else:
report( report(
"calls async_forward_entry_setup for " "calls async_forward_entry_setup for "
f"integration, {entry.domain} with title: {entry.title} " f"integration, {entry.domain} with title: {entry.title} "
@ -2119,16 +2104,27 @@ class ConfigEntries:
error_if_core=False, error_if_core=False,
error_if_integration=False, error_if_integration=False,
) )
return await self._async_forward_entry_setup( if not entry.setup_lock.locked():
entry, domain, True, non_locked_platform_forwards async with entry.setup_lock:
if entry.state is not ConfigEntryState.LOADED:
raise OperationNotAllowed(
f"The config entry {entry.title} ({entry.domain}) with entry_id"
f" {entry.entry_id} cannot forward setup for {domain} because it"
f" is not loaded in the {entry.state} state"
) )
return await self._async_forward_entry_setup(entry, domain, True)
result = await self._async_forward_entry_setup(entry, domain, True)
# If the lock was held when we stated, and it was released during
# the platform setup, it means they did not await the setup call.
if not entry.setup_lock.locked():
_report_non_awaited_platform_forwards(entry, "async_forward_entry_setup")
return result
async def _async_forward_entry_setup( async def _async_forward_entry_setup(
self, self,
entry: ConfigEntry, entry: ConfigEntry,
domain: Platform | str, domain: Platform | str,
preload_platform: bool, preload_platform: bool,
non_locked_platform_forwards: bool,
) -> bool: ) -> bool:
"""Forward the setup of an entry to a different component.""" """Forward the setup of an entry to a different component."""
# Setup Component if not set up yet # Setup Component if not set up yet
@ -2152,12 +2148,6 @@ class ConfigEntries:
integration = loader.async_get_loaded_integration(self.hass, domain) integration = loader.async_get_loaded_integration(self.hass, domain)
await entry.async_setup(self.hass, integration=integration) await entry.async_setup(self.hass, integration=integration)
# Check again after setup to make sure the lock
# is still there because it could have been released
# unless we already reported it.
if not non_locked_platform_forwards and not entry.setup_lock.locked():
_report_non_locked_platform_forwards(entry)
return True return True
async def async_unload_platforms( async def async_unload_platforms(
@ -2221,7 +2211,7 @@ class ConfigEntries:
# The component was not loaded. # The component was not loaded.
if entry.domain not in self.hass.config.components: if entry.domain not in self.hass.config.components:
return False return False
return entry.state == ConfigEntryState.LOADED return entry.state is ConfigEntryState.LOADED
@callback @callback

View file

@ -53,7 +53,7 @@ async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry:
domain="assist_pipeline", state=ConfigEntryState.LOADED domain="assist_pipeline", state=ConfigEntryState.LOADED
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) await hass.config_entries.async_forward_entry_setups(config_entry, ["select"])
return config_entry return config_entry
@ -161,7 +161,7 @@ async def test_select_entity_changing_pipelines(
# Reload config entry to test selected option persists # Reload config entry to test selected option persists
assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") assert await hass.config_entries.async_forward_entry_unload(config_entry, "select")
await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) await hass.config_entries.async_forward_entry_setups(config_entry, ["select"])
state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") state = hass.states.get("select.assist_pipeline_test_prefix_pipeline")
assert state is not None assert state is not None
@ -209,7 +209,7 @@ async def test_select_entity_changing_vad_sensitivity(
# Reload config entry to test selected option persists # Reload config entry to test selected option persists
assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") assert await hass.config_entries.async_forward_entry_unload(config_entry, "select")
await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) await hass.config_entries.async_forward_entry_setups(config_entry, ["select"])
state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") state = hass.states.get("select.assist_pipeline_test_vad_sensitivity")
assert state is not None assert state is not None

View file

@ -278,7 +278,7 @@ async def setup_platform(
await hass.async_block_till_done() await hass.async_block_till_done()
config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.mock_state(hass, ConfigEntryState.LOADED)
await hass.config_entries.async_late_forward_entry_setups(config_entry, platforms) await hass.config_entries.async_forward_entry_setups(config_entry, platforms)
# and make sure it completes before going further # and make sure it completes before going further
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -186,7 +186,7 @@ async def setup_bridge(hass: HomeAssistant, mock_bridge_v1):
config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.mock_state(hass, ConfigEntryState.LOADED)
mock_bridge_v1.config_entry = config_entry mock_bridge_v1.config_entry = config_entry
hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1} hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge_v1}
await hass.config_entries.async_late_forward_entry_setups(config_entry, ["light"]) await hass.config_entries.async_forward_entry_setups(config_entry, ["light"])
# To flush out the service call to update the group # To flush out the service call to update the group
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -75,7 +75,7 @@ async def test_enable_sensor(
assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert await async_setup_component(hass, hue.DOMAIN, {}) is True
await hass.async_block_till_done() await hass.async_block_till_done()
await hass.config_entries.async_late_forward_entry_setups( await hass.config_entries.async_forward_entry_setups(
mock_config_entry_v2, ["sensor"] mock_config_entry_v2, ["sensor"]
) )
@ -95,7 +95,7 @@ async def test_enable_sensor(
# reload platform and check if entity is correctly there # reload platform and check if entity is correctly there
await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor") await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor")
await hass.config_entries.async_late_forward_entry_setups( await hass.config_entries.async_forward_entry_setups(
mock_config_entry_v2, ["sensor"] mock_config_entry_v2, ["sensor"]
) )
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -104,7 +104,7 @@ async def test_restoring_location(
# mobile app doesn't support unloading, so we just reload device tracker # mobile app doesn't support unloading, so we just reload device tracker
await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker")
await hass.config_entries.async_late_forward_entry_setups( await hass.config_entries.async_forward_entry_setups(
config_entry, ["device_tracker"] config_entry, ["device_tracker"]
) )
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -71,7 +71,7 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None):
hass.data[DOMAIN] = {DATA_BROKERS: {config_entry.entry_id: broker}} hass.data[DOMAIN] = {DATA_BROKERS: {config_entry.entry_id: broker}}
config_entry.mock_state(hass, ConfigEntryState.LOADED) config_entry.mock_state(hass, ConfigEntryState.LOADED)
await hass.config_entries.async_late_forward_entry_setups(config_entry, [platform]) await hass.config_entries.async_forward_entry_setups(config_entry, [platform])
await hass.async_block_till_done() await hass.async_block_till_done()
return config_entry return config_entry

View file

@ -13,6 +13,12 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [
"tests.helpers.test_event", "tests.helpers.test_event",
"test_track_point_in_time_repr", "test_track_point_in_time_repr",
), ),
(
# This test explicitly throws an uncaught exception
# and should not be removed.
"tests.test_config_entries",
"test_config_entry_unloaded_during_platform_setups",
),
( (
# This test explicitly throws an uncaught exception # This test explicitly throws an uncaught exception
# and should not be removed. # and should not be removed.

View file

@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component
from homeassistant.util.async_ import create_eager_task
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
from .common import ( from .common import (
@ -971,7 +972,7 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None:
) )
with patch.object(integration, "async_get_platforms") as mock_async_get_platforms: with patch.object(integration, "async_get_platforms") as mock_async_get_platforms:
await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"])
mock_async_get_platforms.assert_called_once_with(["forwarded"]) mock_async_get_platforms.assert_called_once_with(["forwarded"])
assert len(mock_original_setup_entry.mock_calls) == 0 assert len(mock_original_setup_entry.mock_calls) == 0
@ -1001,7 +1002,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(
) )
with patch.object(integration, "async_get_platforms"): with patch.object(integration, "async_get_platforms"):
await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"])
assert len(mock_setup.mock_calls) == 1 assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0
@ -1028,23 +1029,7 @@ async def test_async_forward_entry_setup_deprecated(
), ),
) )
with patch.object(integration, "async_get_platforms"):
await hass.config_entries.async_forward_entry_setup(entry, "forwarded")
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 0
entry_id = entry.entry_id entry_id = entry.entry_id
assert (
"Detected code that calls async_forward_entry_setup after the entry "
"for integration, original with title: Mock Title and entry_id: "
f"{entry_id}, has been set up, without holding the setup lock that "
"prevents the config entry from being set up multiple times. "
"Instead await hass.config_entries.async_forward_entry_setup "
"during setup of the config entry or call "
"hass.config_entries.async_late_forward_entry_setups "
"in a tracked task. This will stop working in Home Assistant "
"2025.1. Please report this issue."
) in caplog.text
caplog.clear() caplog.clear()
with patch.object(integration, "async_get_platforms"): with patch.object(integration, "async_get_platforms"):
async with entry.setup_lock: async with entry.setup_lock:
@ -5553,77 +5538,7 @@ async def test_raise_wrong_exception_in_forwarded_platform(
) )
async def test_non_awaited_async_forward_entry_setups( async def test_config_entry_unloaded_during_platform_setups(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setups not being awaited."""
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_forward_entry_setups without awaiting it
# This is not allowed and will raise a warning
hass.async_create_task(
hass.config_entries.async_forward_entry_setups(entry, ["light"])
)
return True
async def mock_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock unloading an entry."""
result = await hass.config_entries.async_unload_platforms(entry, ["light"])
assert result
return result
mock_remove_entry = AsyncMock(return_value=None)
async def mock_setup_entry_platform(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Mock setting up platform."""
await asyncio.sleep(0)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=mock_setup_entry,
async_unload_entry=mock_unload_entry,
async_remove_entry=mock_remove_entry,
),
)
mock_platform(
hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform)
)
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", entry_id="test2")
entry.add_to_manager(manager)
# Setup entry
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert (
"Detected code that calls async_forward_entry_setup after the "
"entry for integration, test with title: Mock Title and entry_id:"
" test2, has been set up, without holding the setup lock that "
"prevents the config entry from being set up multiple times. "
"Instead await hass.config_entries.async_forward_entry_setup "
"during setup of the config entry or call "
"hass.config_entries.async_late_forward_entry_setups "
"in a tracked task. This will stop working in Home Assistant"
" 2025.1. Please report this issue."
) in caplog.text
async def test_config_entry_unloaded_during_platform_setup(
hass: HomeAssistant, hass: HomeAssistant,
manager: config_entries.ConfigEntries, manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
@ -5636,12 +5551,12 @@ async def test_config_entry_unloaded_during_platform_setup(
) -> bool: ) -> bool:
"""Mock setting up entry.""" """Mock setting up entry."""
# Call async_late_forward_entry_setups in a non-tracked task # Call async_forward_entry_setups in a non-tracked task
# so we can unload the config entry during the setup # so we can unload the config entry during the setup
def _late_setup(): def _late_setup():
nonlocal task nonlocal task
task = asyncio.create_task( task = asyncio.create_task(
hass.config_entries.async_late_forward_entry_setups(entry, ["light"]) hass.config_entries.async_forward_entry_setups(entry, ["light"])
) )
hass.loop.call_soon(_late_setup) hass.loop.call_soon(_late_setup)
@ -5695,3 +5610,294 @@ async def test_config_entry_unloaded_during_platform_setup(
"entry_id test2 cannot forward setup for ['light'] because it is " "entry_id test2 cannot forward setup for ['light'] because it is "
"not loaded in the ConfigEntryState.NOT_LOADED state" "not loaded in the ConfigEntryState.NOT_LOADED state"
) in caplog.text ) in caplog.text
async def test_non_awaited_async_forward_entry_setups(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setups not being awaited."""
forward_event = asyncio.Event()
task: asyncio.Task | None = None
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_forward_entry_setups without awaiting it
# This is not allowed and will raise a warning
nonlocal task
task = create_eager_task(
hass.config_entries.async_forward_entry_setups(entry, ["light"])
)
return True
async def mock_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock unloading an entry."""
result = await hass.config_entries.async_unload_platforms(entry, ["light"])
assert result
return result
mock_remove_entry = AsyncMock(return_value=None)
async def mock_setup_entry_platform(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Mock setting up platform."""
await forward_event.wait()
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=mock_setup_entry,
async_unload_entry=mock_unload_entry,
async_remove_entry=mock_remove_entry,
),
)
mock_platform(
hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform)
)
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", entry_id="test2")
entry.add_to_manager(manager)
# Setup entry
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()
forward_event.set()
await hass.async_block_till_done()
await task
assert (
"Detected code that calls async_forward_entry_setups for integration "
"test with title: Mock Title and entry_id: test2, during setup without "
"awaiting async_forward_entry_setups, which can cause the setup lock "
"to be released before the setup is done. This will stop working in "
"Home Assistant 2025.1. Please report this issue."
) in caplog.text
async def test_non_awaited_async_forward_entry_setup(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setup not being awaited."""
forward_event = asyncio.Event()
task: asyncio.Task | None = None
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_forward_entry_setup without awaiting it
# This is not allowed and will raise a warning
nonlocal task
task = create_eager_task(
hass.config_entries.async_forward_entry_setup(entry, "light")
)
return True
async def mock_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock unloading an entry."""
result = await hass.config_entries.async_unload_platforms(entry, ["light"])
assert result
return result
mock_remove_entry = AsyncMock(return_value=None)
async def mock_setup_entry_platform(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Mock setting up platform."""
await forward_event.wait()
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=mock_setup_entry,
async_unload_entry=mock_unload_entry,
async_remove_entry=mock_remove_entry,
),
)
mock_platform(
hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform)
)
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", entry_id="test2")
entry.add_to_manager(manager)
# Setup entry
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()
forward_event.set()
await hass.async_block_till_done()
await task
assert (
"Detected code that calls async_forward_entry_setup for integration "
"test with title: Mock Title and entry_id: test2, during setup without "
"awaiting async_forward_entry_setup, which can cause the setup lock "
"to be released before the setup is done. This will stop working in "
"Home Assistant 2025.1. Please report this issue."
) in caplog.text
async def test_config_entry_unloaded_during_platform_setup(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setup not being awaited."""
task = None
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_forward_entry_setup in a non-tracked task
# so we can unload the config entry during the setup
def _late_setup():
nonlocal task
task = asyncio.create_task(
hass.config_entries.async_forward_entry_setup(entry, "light")
)
hass.loop.call_soon(_late_setup)
return True
async def mock_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock unloading an entry."""
result = await hass.config_entries.async_unload_platforms(entry, ["light"])
assert result
return result
mock_remove_entry = AsyncMock(return_value=None)
async def mock_setup_entry_platform(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Mock setting up platform."""
await asyncio.sleep(0)
await asyncio.sleep(0)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=mock_setup_entry,
async_unload_entry=mock_unload_entry,
async_remove_entry=mock_remove_entry,
),
)
mock_platform(
hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform)
)
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", entry_id="test2")
entry.add_to_manager(manager)
# Setup entry
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()
await manager.async_unload(entry.entry_id)
await hass.async_block_till_done()
del task
assert (
"OperationNotAllowed: The config entry Mock Title (test) with "
"entry_id test2 cannot forward setup for light because it is "
"not loaded in the ConfigEntryState.NOT_LOADED state"
) in caplog.text
async def test_config_entry_late_platform_setup(
hass: HomeAssistant,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setup not being awaited."""
task = None
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_forward_entry_setup in a non-tracked task
# so we can unload the config entry during the setup
def _late_setup():
nonlocal task
task = asyncio.create_task(
hass.config_entries.async_forward_entry_setup(entry, "light")
)
hass.loop.call_soon(_late_setup)
return True
async def mock_unload_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock unloading an entry."""
result = await hass.config_entries.async_unload_platforms(entry, ["light"])
assert result
return result
mock_remove_entry = AsyncMock(return_value=None)
async def mock_setup_entry_platform(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Mock setting up platform."""
await asyncio.sleep(0)
await asyncio.sleep(0)
mock_integration(
hass,
MockModule(
"test",
async_setup_entry=mock_setup_entry,
async_unload_entry=mock_unload_entry,
async_remove_entry=mock_remove_entry,
),
)
mock_platform(
hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform)
)
mock_platform(hass, "test.config_flow", None)
entry = MockConfigEntry(domain="test", entry_id="test2")
entry.add_to_manager(manager)
# Setup entry
await manager.async_setup(entry.entry_id)
await hass.async_block_till_done()
await task
await hass.async_block_till_done()
assert (
"OperationNotAllowed: The config entry Mock Title (test) with "
"entry_id test2 cannot forward setup for light because it is "
"not loaded in the ConfigEntryState.NOT_LOADED state"
) not in caplog.text