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:
parent
4e121fcbe8
commit
dbd3147c9b
18 changed files with 381 additions and 188 deletions
|
@ -182,7 +182,7 @@ class AmbientStation:
|
|||
# already been done):
|
||||
if not self._entry_setup_complete:
|
||||
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
|
||||
),
|
||||
eager_start=True,
|
||||
|
|
|
@ -28,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -
|
|||
|
||||
async def _async_finish_startup(_: HomeAssistant) -> None:
|
||||
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)
|
||||
return True
|
||||
|
|
|
@ -248,15 +248,9 @@ class RuntimeEntryData:
|
|||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
platforms: set[Platform],
|
||||
late: bool,
|
||||
) -> None:
|
||||
async with self.platform_load_lock:
|
||||
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)
|
||||
self.loaded_platforms |= needed
|
||||
|
||||
|
@ -266,7 +260,6 @@ class RuntimeEntryData:
|
|||
entry: ConfigEntry,
|
||||
infos: list[EntityInfo],
|
||||
mac: str,
|
||||
late: bool = False,
|
||||
) -> None:
|
||||
"""Distribute an update of static infos to 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)
|
||||
|
||||
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
|
||||
# them to the listeners for each specific EntityInfo type
|
||||
|
|
|
@ -491,7 +491,7 @@ class ESPHomeManager:
|
|||
|
||||
entry_data.async_update_device_state()
|
||||
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)
|
||||
|
||||
|
|
|
@ -379,9 +379,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
new_config: list[ConfigType] = config_yaml.get(DOMAIN, [])
|
||||
platforms_used = platforms_from_config(new_config)
|
||||
new_platforms = platforms_used - mqtt_data.platforms_loaded
|
||||
await async_forward_entry_setup_and_setup_discovery(
|
||||
hass, entry, new_platforms, late=True
|
||||
)
|
||||
await async_forward_entry_setup_and_setup_discovery(hass, entry, new_platforms)
|
||||
# Check the schema before continuing reload
|
||||
await async_check_config_schema(hass, config_yaml)
|
||||
|
||||
|
|
|
@ -211,7 +211,7 @@ async def async_start( # noqa: C901
|
|||
async with platform_setup_lock.setdefault(component, asyncio.Lock()):
|
||||
if component not in mqtt_data.platforms_loaded:
|
||||
await async_forward_entry_setup_and_setup_discovery(
|
||||
hass, config_entry, {component}, late=True
|
||||
hass, config_entry, {component}
|
||||
)
|
||||
_async_add_component(discovery_payload)
|
||||
|
||||
|
|
|
@ -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)))
|
||||
if new_entity_platforms := (new_platforms - {"tag", "device_automation"}):
|
||||
if late:
|
||||
coro = hass.config_entries.async_late_forward_entry_setups
|
||||
else:
|
||||
coro = hass.config_entries.async_forward_entry_setups
|
||||
tasks.append(create_eager_task(coro(config_entry, new_entity_platforms)))
|
||||
tasks.append(
|
||||
create_eager_task(
|
||||
hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, new_entity_platforms
|
||||
)
|
||||
)
|
||||
)
|
||||
if not tasks:
|
||||
return
|
||||
await asyncio.gather(*tasks)
|
||||
|
|
|
@ -200,9 +200,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
|||
self.hass.config_entries.async_update_entry(self.entry, data=data)
|
||||
|
||||
# Resume platform setup
|
||||
await self.hass.config_entries.async_late_forward_entry_setups(
|
||||
self.entry, platforms
|
||||
)
|
||||
await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -330,7 +330,7 @@ class DriverEvents:
|
|||
"""Set up platform if needed."""
|
||||
if platform not in self.platform_setup_tasks:
|
||||
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]
|
||||
)
|
||||
)
|
||||
|
|
|
@ -1170,18 +1170,13 @@ class FlowCancelledError(Exception):
|
|||
"""Error to indicate that a flow has been cancelled."""
|
||||
|
||||
|
||||
def _report_non_locked_platform_forwards(entry: ConfigEntry) -> None:
|
||||
"""Report non awaited and non-locked platform forwards."""
|
||||
def _report_non_awaited_platform_forwards(entry: ConfigEntry, what: str) -> None:
|
||||
"""Report non awaited platform forwards."""
|
||||
report(
|
||||
f"calls async_forward_entry_setup after the entry for "
|
||||
f"integration, {entry.domain} with title: {entry.title} "
|
||||
f"and entry_id: {entry.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. "
|
||||
f"calls {what} for integration {entry.domain} with "
|
||||
f"title: {entry.title} and entry_id: {entry.entry_id}, "
|
||||
f"during setup without awaiting {what}, which can cause "
|
||||
"the setup lock to be released before the setup is done. "
|
||||
"This will stop working in Home Assistant 2025.1",
|
||||
error_if_integration=False,
|
||||
error_if_core=False,
|
||||
|
@ -2041,9 +2036,6 @@ class ConfigEntries:
|
|||
before the entry is set up. This ensures that the config entry cannot
|
||||
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
|
||||
it can load multiple platforms at once and does not require a separate
|
||||
import executor job for each platform.
|
||||
|
@ -2052,14 +2044,32 @@ class ConfigEntries:
|
|||
if not integration.platforms_are_loaded(platforms):
|
||||
with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_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(
|
||||
*(
|
||||
create_eager_task(
|
||||
self._async_forward_entry_setup(
|
||||
entry, platform, False, non_locked_platform_forwards
|
||||
),
|
||||
self._async_forward_entry_setup(entry, platform, False),
|
||||
name=(
|
||||
f"config entry forward setup {entry.title} "
|
||||
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(
|
||||
self, entry: ConfigEntry, domain: Platform | str
|
||||
) -> bool:
|
||||
|
@ -2103,13 +2094,7 @@ class ConfigEntries:
|
|||
Instead, await async_forward_entry_setups as it can load
|
||||
multiple platforms at once and is more efficient since it
|
||||
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(
|
||||
"calls async_forward_entry_setup for "
|
||||
f"integration, {entry.domain} with title: {entry.title} "
|
||||
|
@ -2119,16 +2104,27 @@ class ConfigEntries:
|
|||
error_if_core=False,
|
||||
error_if_integration=False,
|
||||
)
|
||||
return await self._async_forward_entry_setup(
|
||||
entry, domain, True, non_locked_platform_forwards
|
||||
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 {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(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
domain: Platform | str,
|
||||
preload_platform: bool,
|
||||
non_locked_platform_forwards: bool,
|
||||
) -> bool:
|
||||
"""Forward the setup of an entry to a different component."""
|
||||
# Setup Component if not set up yet
|
||||
|
@ -2152,12 +2148,6 @@ class ConfigEntries:
|
|||
|
||||
integration = loader.async_get_loaded_integration(self.hass, domain)
|
||||
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
|
||||
|
||||
async def async_unload_platforms(
|
||||
|
@ -2221,7 +2211,7 @@ class ConfigEntries:
|
|||
# The component was not loaded.
|
||||
if entry.domain not in self.hass.config.components:
|
||||
return False
|
||||
return entry.state == ConfigEntryState.LOADED
|
||||
return entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@callback
|
||||
|
|
|
@ -53,7 +53,7 @@ async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry:
|
|||
domain="assist_pipeline", state=ConfigEntryState.LOADED
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
@ -161,7 +161,7 @@ async def test_select_entity_changing_pipelines(
|
|||
|
||||
# Reload config entry to test selected option persists
|
||||
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")
|
||||
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
|
||||
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")
|
||||
assert state is not None
|
||||
|
|
|
@ -278,7 +278,7 @@ async def setup_platform(
|
|||
await hass.async_block_till_done()
|
||||
|
||||
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
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -186,7 +186,7 @@ async def setup_bridge(hass: HomeAssistant, mock_bridge_v1):
|
|||
config_entry.mock_state(hass, ConfigEntryState.LOADED)
|
||||
mock_bridge_v1.config_entry = config_entry
|
||||
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
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
|
@ -75,7 +75,7 @@ async def test_enable_sensor(
|
|||
|
||||
assert await async_setup_component(hass, hue.DOMAIN, {}) is True
|
||||
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"]
|
||||
)
|
||||
|
||||
|
@ -95,7 +95,7 @@ async def test_enable_sensor(
|
|||
|
||||
# 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_late_forward_entry_setups(
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
mock_config_entry_v2, ["sensor"]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -104,7 +104,7 @@ async def test_restoring_location(
|
|||
|
||||
# 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_late_forward_entry_setups(
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
config_entry, ["device_tracker"]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
|
|
@ -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}}
|
||||
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()
|
||||
return config_entry
|
||||
|
||||
|
|
|
@ -13,6 +13,12 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [
|
|||
"tests.helpers.test_event",
|
||||
"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
|
||||
# and should not be removed.
|
||||
|
|
|
@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
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
|
||||
|
||||
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:
|
||||
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"])
|
||||
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"):
|
||||
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_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
|
||||
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()
|
||||
with patch.object(integration, "async_get_platforms"):
|
||||
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(
|
||||
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(
|
||||
async def test_config_entry_unloaded_during_platform_setups(
|
||||
hass: HomeAssistant,
|
||||
manager: config_entries.ConfigEntries,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
@ -5636,12 +5551,12 @@ async def test_config_entry_unloaded_during_platform_setup(
|
|||
) -> bool:
|
||||
"""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
|
||||
def _late_setup():
|
||||
nonlocal 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)
|
||||
|
@ -5695,3 +5610,294 @@ async def test_config_entry_unloaded_during_platform_setup(
|
|||
"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_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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue