Ensure config entries are not unloaded while their platforms are setting up (#118767)

* Report non-awaited/non-locked config entry platform forwards

Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.

In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.

If config platform forwards are happening during setup, they should be awaited

If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup

* Report non-awaited/non-locked config entry platform forwards

Its currently possible for config entries to be reloaded while their platforms
are being forwarded if platform forwards are not awaited or done after the
config entry is setup since the lock will not be held in this case.

In https://developers.home-assistant.io/blog/2022/07/08/config_entry_forwards
we advised to await platform forwards to ensure this does not happen, however
for sleeping devices and late discovered devices, platform forwards may happen
later.

If config platform forwards are happening during setup, they should be awaited

If config entry platform forwards are not happening during setup, instead
async_late_forward_entry_setups should be used which will hold the lock to
prevent the config entry from being unloaded while its platforms are being
setup

* run with error on to find them

* cert_exp, hold lock

* cert_exp, hold lock

* shelly async_late_forward_entry_setups

* compact

* compact

* found another

* patch up mobileapp

* patch up hue tests

* patch up smartthings

* fix mqtt

* fix esphome

* zwave_js

* mqtt

* rework

* fixes

* fix mocking

* fix mocking

* do not call async_forward_entry_setup directly

* docstrings

* docstrings

* docstrings

* add comments

* doc strings

* fixed all in core, turn off strict

* coverage

* coverage

* missing

* coverage
This commit is contained in:
J. Nick Koston 2024-06-04 20:34:39 -05:00 committed by GitHub
parent 67b3be8432
commit ed0568c655
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 457 additions and 111 deletions

View file

@ -1556,6 +1556,7 @@ omit =
homeassistant/components/verisure/sensor.py homeassistant/components/verisure/sensor.py
homeassistant/components/verisure/switch.py homeassistant/components/verisure/switch.py
homeassistant/components/versasense/* homeassistant/components/versasense/*
homeassistant/components/vesync/__init__.py
homeassistant/components/vesync/fan.py homeassistant/components/vesync/fan.py
homeassistant/components/vesync/light.py homeassistant/components/vesync/light.py
homeassistant/components/vesync/sensor.py homeassistant/components/vesync/sensor.py

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_forward_entry_setups( self._hass.config_entries.async_late_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_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_late_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

@ -244,15 +244,29 @@ class RuntimeEntryData:
callback_(static_info) callback_(static_info)
async def _ensure_platforms_loaded( async def _ensure_platforms_loaded(
self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform] self,
hass: HomeAssistant,
entry: ConfigEntry,
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
async def async_update_static_infos( async def async_update_static_infos(
self, hass: HomeAssistant, entry: ConfigEntry, infos: list[EntityInfo], mac: str self,
hass: HomeAssistant,
entry: ConfigEntry,
infos: list[EntityInfo],
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
@ -282,7 +296,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) await self._ensure_platforms_loaded(hass, entry, needed_platforms, late)
# 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 hass, entry, entity_infos, device_info.mac_address, late=True
) )
_setup_services(hass, entry_data, services) _setup_services(hass, entry_data, services)

View file

@ -191,15 +191,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
create_knx_exposure(hass, knx_module.xknx, expose_config) create_knx_exposure(hass, knx_module.xknx, expose_config)
) )
# always forward sensor for system entities (telegram counter, etc.) # always forward sensor for system entities (telegram counter, etc.)
await hass.config_entries.async_forward_entry_setup(entry, Platform.SENSOR) platforms = {platform for platform in SUPPORTED_PLATFORMS if platform in config}
await hass.config_entries.async_forward_entry_setups( platforms.add(Platform.SENSOR)
entry, await hass.config_entries.async_forward_entry_setups(entry, platforms)
[
platform
for platform in SUPPORTED_PLATFORMS
if platform in config and platform is not Platform.SENSOR
],
)
# set up notify service for backwards compatibility - remove 2024.11 # set up notify service for backwards compatibility - remove 2024.11
if NotifySchema.PLATFORM in config: if NotifySchema.PLATFORM in config:

View file

@ -379,7 +379,9 @@ 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(hass, entry, new_platforms) await async_forward_entry_setup_and_setup_discovery(
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} hass, config_entry, {component}, late=True
) )
_async_add_component(discovery_payload) _async_add_component(discovery_payload)

View file

@ -47,7 +47,10 @@ def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]:
async def async_forward_entry_setup_and_setup_discovery( async def async_forward_entry_setup_and_setup_discovery(
hass: HomeAssistant, config_entry: ConfigEntry, platforms: set[Platform | str] hass: HomeAssistant,
config_entry: ConfigEntry,
platforms: set[Platform | str],
late: bool = False,
) -> None: ) -> None:
"""Forward the config entry setup to the platforms and set up discovery.""" """Forward the config entry setup to the platforms and set up discovery."""
mqtt_data = hass.data[DATA_MQTT] mqtt_data = hass.data[DATA_MQTT]
@ -69,13 +72,11 @@ 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"}):
tasks.append( if late:
create_eager_task( coro = hass.config_entries.async_late_forward_entry_setups
hass.config_entries.async_forward_entry_setups( else:
config_entry, new_entity_platforms coro = hass.config_entries.async_forward_entry_setups
) 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

@ -205,8 +205,8 @@ class MinutPointClient:
config_entries_key = f"{platform}.{DOMAIN}" config_entries_key = f"{platform}.{DOMAIN}"
async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]:
await self._hass.config_entries.async_forward_entry_setup( await self._hass.config_entries.async_forward_entry_setups(
self._config_entry, platform self._config_entry, [platform]
) )
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key)

View file

@ -200,7 +200,9 @@ 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_forward_entry_setups(self.entry, platforms) await self.hass.config_entries.async_late_forward_entry_setups(
self.entry, platforms
)
return True return True

View file

@ -180,8 +180,8 @@ class TelldusLiveClient:
) )
async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]:
if component not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: if component not in self._hass.data[CONFIG_ENTRY_IS_SETUP]:
await self._hass.config_entries.async_forward_entry_setup( await self._hass.config_entries.async_forward_entry_setups(
self._config_entry, component self._config_entry, [component]
) )
self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component)
device_ids = [] device_ids = []

View file

@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
device_dict = await async_process_devices(hass, manager) device_dict = await async_process_devices(hass, manager)
forward_setup = hass.config_entries.async_forward_entry_setup forward_setups = hass.config_entries.async_forward_entry_setups
hass.data[DOMAIN] = {} hass.data[DOMAIN] = {}
hass.data[DOMAIN][VS_MANAGER] = manager hass.data[DOMAIN][VS_MANAGER] = manager
@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return return
if new_switches and not switches: if new_switches and not switches:
switches.extend(new_switches) switches.extend(new_switches)
hass.async_create_task(forward_setup(config_entry, Platform.SWITCH)) hass.async_create_task(forward_setups(config_entry, [Platform.SWITCH]))
fan_set = set(fan_devs) fan_set = set(fan_devs)
new_fans = list(fan_set.difference(fans)) new_fans = list(fan_set.difference(fans))
@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return return
if new_fans and not fans: if new_fans and not fans:
fans.extend(new_fans) fans.extend(new_fans)
hass.async_create_task(forward_setup(config_entry, Platform.FAN)) hass.async_create_task(forward_setups(config_entry, [Platform.FAN]))
light_set = set(light_devs) light_set = set(light_devs)
new_lights = list(light_set.difference(lights)) new_lights = list(light_set.difference(lights))
@ -117,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return return
if new_lights and not lights: if new_lights and not lights:
lights.extend(new_lights) lights.extend(new_lights)
hass.async_create_task(forward_setup(config_entry, Platform.LIGHT)) hass.async_create_task(forward_setups(config_entry, [Platform.LIGHT]))
sensor_set = set(sensor_devs) sensor_set = set(sensor_devs)
new_sensors = list(sensor_set.difference(sensors)) new_sensors = list(sensor_set.difference(sensors))
@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return return
if new_sensors and not sensors: if new_sensors and not sensors:
sensors.extend(new_sensors) sensors.extend(new_sensors)
hass.async_create_task(forward_setup(config_entry, Platform.SENSOR)) hass.async_create_task(forward_setups(config_entry, [Platform.SENSOR]))
hass.services.async_register( hass.services.async_register(
DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery

View file

@ -324,8 +324,8 @@ 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_forward_entry_setup( self.hass.config_entries.async_late_forward_entry_setups(
self.config_entry, platform self.config_entry, [platform]
) )
) )
await self.platform_setup_tasks[platform] await self.platform_setup_tasks[platform]

View file

@ -1178,6 +1178,24 @@ 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:
"""Report non awaited and non-locked 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. "
"This will stop working in Home Assistant 2025.1",
error_if_integration=False,
error_if_core=False,
)
class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]): class ConfigEntriesFlowManager(data_entry_flow.FlowManager[ConfigFlowResult]):
"""Manage all the config entry flows that are in progress.""" """Manage all the config entry flows that are in progress."""
@ -2024,15 +2042,32 @@ class ConfigEntries:
async def async_forward_entry_setups( async def async_forward_entry_setups(
self, entry: ConfigEntry, platforms: Iterable[Platform | str] self, entry: ConfigEntry, platforms: Iterable[Platform | str]
) -> None: ) -> None:
"""Forward the setup of an entry to platforms.""" """Forward the setup of an entry to platforms.
This method should be awaited before async_setup_entry is finished
in each integration. This is to ensure that all platforms are loaded
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.
"""
integration = await loader.async_get_integration(self.hass, entry.domain) integration = await loader.async_get_integration(self.hass, entry.domain)
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)
await asyncio.gather( await asyncio.gather(
*( *(
create_eager_task( create_eager_task(
self._async_forward_entry_setup(entry, platform, False), self._async_forward_entry_setup(
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}"
@ -2043,6 +2078,25 @@ 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:
@ -2051,11 +2105,38 @@ class ConfigEntries:
By default an entry is setup with the component it belongs to. If that By default an entry is setup with the component it belongs to. If that
component also has related platforms, the component will have to component also has related platforms, the component will have to
forward the entry to be setup by that component. forward the entry to be setup by that component.
This method is deprecated and will stop working in Home Assistant 2025.6.
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.
""" """
return await self._async_forward_entry_setup(entry, domain, True) 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} "
f"and entry_id: {entry.entry_id}, which is deprecated and "
"will stop working in Home Assistant 2025.6, "
"await async_forward_entry_setups instead",
error_if_core=False,
error_if_integration=False,
)
return await self._async_forward_entry_setup(
entry, domain, True, non_locked_platform_forwards
)
async def _async_forward_entry_setup( async def _async_forward_entry_setup(
self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool self,
entry: ConfigEntry,
domain: Platform | str,
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
@ -2079,6 +2160,12 @@ 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(
@ -2104,7 +2191,11 @@ class ConfigEntries:
async def async_forward_entry_unload( async def async_forward_entry_unload(
self, entry: ConfigEntry, domain: Platform | str self, entry: ConfigEntry, domain: Platform | str
) -> bool: ) -> bool:
"""Forward the unloading of an entry to a different component.""" """Forward the unloading of an entry to a different component.
Its is preferred to call async_unload_platforms instead
of directly calling this method.
"""
# It was never loaded. # It was never loaded.
if domain not in self.hass.config.components: if domain not in self.hass.config.components:
return True return True

View file

@ -155,8 +155,8 @@ async def setup_lock_platform_test_entity(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, ALARM_CONTROL_PANEL_DOMAIN config_entry, [ALARM_CONTROL_PANEL_DOMAIN]
) )
return True return True

View file

@ -15,7 +15,7 @@ from homeassistant.components.assist_pipeline.select import (
VadSensitivitySelect, VadSensitivitySelect,
) )
from homeassistant.components.assist_pipeline.vad import VadSensitivity from homeassistant.components.assist_pipeline.vad import VadSensitivity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@ -49,9 +49,11 @@ class SelectPlatform(MockPlatform):
async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry: async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry:
"""Initialize select entity.""" """Initialize select entity."""
mock_platform(hass, "assist_pipeline.select", SelectPlatform()) mock_platform(hass, "assist_pipeline.select", SelectPlatform())
config_entry = MockConfigEntry(domain="assist_pipeline") config_entry = MockConfigEntry(
domain="assist_pipeline", state=ConfigEntryState.LOADED
)
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"])
return config_entry return config_entry
@ -123,13 +125,14 @@ async def test_select_entity_registering_device(
async def test_select_entity_changing_pipelines( async def test_select_entity_changing_pipelines(
hass: HomeAssistant, hass: HomeAssistant,
init_select: ConfigEntry, init_select: MockConfigEntry,
pipeline_1: Pipeline, pipeline_1: Pipeline,
pipeline_2: Pipeline, pipeline_2: Pipeline,
pipeline_storage: PipelineStorageCollection, pipeline_storage: PipelineStorageCollection,
) -> None: ) -> None:
"""Test entity tracking pipeline changes.""" """Test entity tracking pipeline changes."""
config_entry = init_select # nicer naming config_entry = init_select # nicer naming
config_entry.mock_state(hass, ConfigEntryState.LOADED)
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
@ -158,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")
assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") await hass.config_entries.async_late_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
@ -179,10 +182,11 @@ async def test_select_entity_changing_pipelines(
async def test_select_entity_changing_vad_sensitivity( async def test_select_entity_changing_vad_sensitivity(
hass: HomeAssistant, hass: HomeAssistant,
init_select: ConfigEntry, init_select: MockConfigEntry,
) -> None: ) -> None:
"""Test entity tracking pipeline changes.""" """Test entity tracking pipeline changes."""
config_entry = init_select # nicer naming config_entry = init_select # nicer naming
config_entry.mock_state(hass, ConfigEntryState.LOADED)
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
@ -205,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")
assert await hass.config_entries.async_forward_entry_setup(config_entry, "select") await hass.config_entries.async_late_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

@ -63,8 +63,8 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, binary_sensor.DOMAIN config_entry, [binary_sensor.DOMAIN]
) )
return True return True
@ -143,8 +143,8 @@ async def test_entity_category_config_raises_error(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, binary_sensor.DOMAIN config_entry, [binary_sensor.DOMAIN]
) )
return True return True

View file

@ -139,7 +139,7 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -120,7 +120,7 @@ def mock_setup_integration(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(

View file

@ -50,7 +50,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(

View file

@ -141,6 +141,8 @@ async def test_gateway_setup(
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
) -> None: ) -> None:
"""Successful setup.""" """Successful setup."""
# Patching async_forward_entry_setup* is not advisable, and should be refactored
# in the future.
with patch( with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups",
return_value=True, return_value=True,
@ -190,8 +192,10 @@ async def test_gateway_device_configuration_url_when_addon(
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
) -> None: ) -> None:
"""Successful setup.""" """Successful setup."""
# Patching async_forward_entry_setup* is not advisable, and should be refactored
# in the future.
with patch( with patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups",
return_value=True, return_value=True,
): ):
config_entry = await setup_deconz_integration( config_entry = await setup_deconz_integration(

View file

@ -53,9 +53,11 @@ async def test_setup_without_password(hass: HomeAssistant) -> None:
} }
entry = MockConfigEntry(domain=DOMAIN, data=config) entry = MockConfigEntry(domain=DOMAIN, data=config)
entry.add_to_hass(hass) entry.add_to_hass(hass)
# Patching async_forward_entry_setup* is not advisable, and should be refactored
# in the future.
with ( with (
patch( patch(
"homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups",
return_value=True, return_value=True,
), ),
patch("homeassistant.core.EventBus.async_listen_once"), patch("homeassistant.core.EventBus.async_listen_once"),

View file

@ -85,9 +85,8 @@ async def test_update_entity(
"homeassistant.components.esphome.update.DomainData.get_entry_data", "homeassistant.components.esphome.update.DomainData.get_entry_data",
return_value=Mock(available=True, device_info=mock_device_info), return_value=Mock(available=True, device_info=mock_device_info),
): ):
assert await hass.config_entries.async_forward_entry_setup( assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
mock_config_entry, "update" await hass.async_block_till_done()
)
state = hass.states.get("update.none_firmware") state = hass.states.get("update.none_firmware")
assert state is not None assert state is not None
@ -275,9 +274,8 @@ async def test_update_entity_dashboard_not_available_startup(
), ),
): ):
await async_get_dashboard(hass).async_refresh() await async_get_dashboard(hass).async_refresh()
assert await hass.config_entries.async_forward_entry_setup( assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
mock_config_entry, "update" await hass.async_block_till_done()
)
# We have a dashboard but it is not available # We have a dashboard but it is not available
state = hass.states.get("update.none_firmware") state = hass.states.get("update.none_firmware")
@ -362,9 +360,8 @@ async def test_update_entity_not_present_without_dashboard(
"homeassistant.components.esphome.update.DomainData.get_entry_data", "homeassistant.components.esphome.update.DomainData.get_entry_data",
return_value=Mock(available=True, device_info=mock_device_info), return_value=Mock(available=True, device_info=mock_device_info),
): ):
assert await hass.config_entries.async_forward_entry_setup( assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
mock_config_entry, "update" await hass.async_block_till_done()
)
state = hass.states.get("update.none_firmware") state = hass.states.get("update.none_firmware")
assert state is None assert state is None

View file

@ -254,7 +254,7 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -88,6 +88,7 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None:
home = Mock() home = Mock()
hap = HomematicipHAP(hass, entry) hap = HomematicipHAP(hass, entry)
with patch.object(hap, "get_hap", return_value=home): with patch.object(hap, "get_hap", return_value=home):
async with entry.setup_lock:
assert await hap.async_setup() assert await hap.async_setup()
assert hap.home is home assert hap.home is home
@ -96,13 +97,16 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None:
async def test_hap_setup_connection_error() -> None: async def test_hap_setup_connection_error() -> None:
"""Test a failed accesspoint setup.""" """Test a failed accesspoint setup."""
hass = Mock() hass = Mock()
entry = Mock() entry = MockConfigEntry(
entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} domain=HMIPC_DOMAIN,
data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"},
)
hap = HomematicipHAP(hass, entry) hap = HomematicipHAP(hass, entry)
with ( with (
patch.object(hap, "get_hap", side_effect=HmipcConnectionError), patch.object(hap, "get_hap", side_effect=HmipcConnectionError),
pytest.raises(ConfigEntryNotReady), pytest.raises(ConfigEntryNotReady),
): ):
async with entry.setup_lock:
assert not await hap.async_setup() assert not await hap.async_setup()
assert not hass.async_run_hass_job.mock_calls assert not hass.async_run_hass_job.mock_calls
@ -132,6 +136,7 @@ async def test_hap_create(
hap = HomematicipHAP(hass, hmip_config_entry) hap = HomematicipHAP(hass, hmip_config_entry)
assert hap assert hap
with patch.object(hap, "async_connect"): with patch.object(hap, "async_connect"):
async with hmip_config_entry.setup_lock:
assert await hap.async_setup() assert await hap.async_setup()

View file

@ -15,6 +15,7 @@ import pytest
from homeassistant.components import hue from homeassistant.components import hue
from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base
from homeassistant.components.hue.v2.device import async_setup_devices from homeassistant.components.hue.v2.device import async_setup_devices
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
@ -275,8 +276,8 @@ async def setup_platform(
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()
for platform in platforms: config_entry.mock_state(hass, ConfigEntryState.LOADED)
await hass.config_entries.async_forward_entry_setup(config_entry, platform) await hass.config_entries.async_late_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

@ -34,6 +34,7 @@ async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None:
patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward,
): ):
hue_bridge = bridge.HueBridge(hass, config_entry) hue_bridge = bridge.HueBridge(hass, config_entry)
async with config_entry.setup_lock:
assert await hue_bridge.async_initialize_bridge() is True assert await hue_bridge.async_initialize_bridge() is True
assert hue_bridge.api is mock_api_v1 assert hue_bridge.api is mock_api_v1
@ -125,6 +126,7 @@ async def test_reset_unloads_entry_if_setup(hass: HomeAssistant, mock_api_v1) ->
patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward,
): ):
hue_bridge = bridge.HueBridge(hass, config_entry) hue_bridge = bridge.HueBridge(hass, config_entry)
async with config_entry.setup_lock:
assert await hue_bridge.async_initialize_bridge() is True assert await hue_bridge.async_initialize_bridge() is True
await asyncio.sleep(0) await asyncio.sleep(0)
@ -151,6 +153,7 @@ async def test_handle_unauthorized(hass: HomeAssistant, mock_api_v1) -> None:
with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1):
hue_bridge = bridge.HueBridge(hass, config_entry) hue_bridge = bridge.HueBridge(hass, config_entry)
async with config_entry.setup_lock:
assert await hue_bridge.async_initialize_bridge() is True assert await hue_bridge.async_initialize_bridge() is True
with patch.object(bridge, "create_config_flow") as mock_create: with patch.object(bridge, "create_config_flow") as mock_create:

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_forward_entry_setup(config_entry, "light") await hass.config_entries.async_late_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,9 @@ 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_forward_entry_setup(mock_config_entry_v2, "sensor") await hass.config_entries.async_late_forward_entry_setups(
mock_config_entry_v2, ["sensor"]
)
entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity" entity_id = "sensor.wall_switch_with_2_controls_zigbee_connectivity"
entity_entry = entity_registry.async_get(entity_id) entity_entry = entity_registry.async_get(entity_id)
@ -93,7 +95,9 @@ 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_forward_entry_setup(mock_config_entry_v2, "sensor") await hass.config_entries.async_late_forward_entry_setups(
mock_config_entry_v2, ["sensor"]
)
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(entity_id) state = hass.states.get(entity_id)

View file

@ -147,14 +147,16 @@ async def mock_image_config_entry_fixture(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, image.DOMAIN) await hass.config_entries.async_forward_entry_setups(
config_entry, [image.DOMAIN]
)
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Unload test config entry.""" """Unload test config entry."""
await hass.config_entries.async_forward_entry_unload(config_entry, image.DOMAIN) await hass.config_entries.async_unload_platforms(config_entry, [image.DOMAIN])
return True return True
mock_integration( mock_integration(

View file

@ -67,8 +67,8 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, Platform.LAWN_MOWER config_entry, [Platform.LAWN_MOWER]
) )
return True return True

View file

@ -98,7 +98,9 @@ async def setup_lock_platform_test_entity(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, LOCK_DOMAIN) await hass.config_entries.async_forward_entry_setups(
config_entry, [LOCK_DOMAIN]
)
return True return True
MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") MockPlatform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -104,7 +104,9 @@ 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_forward_entry_setup(config_entry, "device_tracker") await hass.config_entries.async_late_forward_entry_setups(
config_entry, ["device_tracker"]
)
await hass.async_block_till_done() await hass.async_block_till_done()
state_2 = hass.states.get("device_tracker.test_1_2") state_2 = hass.states.get("device_tracker.test_1_2")

View file

@ -56,7 +56,7 @@ async def help_async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True

View file

@ -874,7 +874,7 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -2399,7 +2399,9 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, SENSOR_DOMAIN) await hass.config_entries.async_forward_entry_setups(
config_entry, [SENSOR_DOMAIN]
)
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -39,7 +39,7 @@ from homeassistant.components.smartthings.const import (
STORAGE_VERSION, STORAGE_VERSION,
) )
from homeassistant.config import async_process_ha_core_config from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import ( from homeassistant.const import (
CONF_ACCESS_TOKEN, CONF_ACCESS_TOKEN,
CONF_CLIENT_ID, CONF_CLIENT_ID,
@ -70,7 +70,8 @@ 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}}
await hass.config_entries.async_forward_entry_setup(config_entry, platform) config_entry.mock_state(hass, ConfigEntryState.LOADED)
await hass.config_entries.async_late_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

@ -187,7 +187,7 @@ async def mock_config_entry_setup(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(

View file

@ -91,7 +91,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(

View file

@ -226,7 +226,7 @@ async def mock_config_entry_setup(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, TTS_DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [TTS_DOMAIN])
return True return True
async def async_unload_entry_init( async def async_unload_entry_init(

View file

@ -782,7 +782,7 @@ async def test_name(hass: HomeAssistant) -> None:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
@ -890,7 +890,7 @@ async def test_deprecated_supported_features_ints_with_service_call(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow") mock_platform(hass, f"{TEST_DOMAIN}.config_flow")

View file

@ -71,7 +71,7 @@ async def help_async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) await hass.config_entries.async_forward_entry_setups(config_entry, [DOMAIN])
return True return True

View file

@ -152,8 +152,8 @@ def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]:
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, Platform.VALVE config_entry, [Platform.VALVE]
) )
return True return True

View file

@ -117,8 +117,8 @@ async def mock_config_entry_setup(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: ConfigEntry
) -> bool: ) -> bool:
"""Set up test config entry.""" """Set up test config entry."""
await hass.config_entries.async_forward_entry_setup( await hass.config_entries.async_forward_entry_setups(
config_entry, wake_word.DOMAIN config_entry, [wake_word.DOMAIN]
) )
return True return True

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_setup",
),
( (
"test_homeassistant_bridge", "test_homeassistant_bridge",
"test_homeassistant_bridge_fan_setup", "test_homeassistant_bridge_fan_setup",

View file

@ -957,7 +957,9 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None:
async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None:
"""Test we setup the component entry is forwarded to.""" """Test we setup the component entry is forwarded to."""
entry = MockConfigEntry(domain="original") entry = MockConfigEntry(
domain="original", state=config_entries.ConfigEntryState.LOADED
)
mock_original_setup_entry = AsyncMock(return_value=True) mock_original_setup_entry = AsyncMock(return_value=True)
integration = mock_integration( integration = mock_integration(
@ -969,10 +971,10 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None:
hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry) hass, MockModule("forwarded", async_setup_entry=mock_forwarded_setup_entry)
) )
with patch.object(integration, "async_get_platform") as mock_async_get_platform: with patch.object(integration, "async_get_platforms") as mock_async_get_platforms:
await hass.config_entries.async_forward_entry_setup(entry, "forwarded") await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"])
mock_async_get_platform.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
assert len(mock_forwarded_setup_entry.mock_calls) == 1 assert len(mock_forwarded_setup_entry.mock_calls) == 1
@ -981,7 +983,14 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails(
hass: HomeAssistant, hass: HomeAssistant,
) -> None: ) -> None:
"""Test we do not set up entry if component setup fails.""" """Test we do not set up entry if component setup fails."""
entry = MockConfigEntry(domain="original") entry = MockConfigEntry(
domain="original", state=config_entries.ConfigEntryState.LOADED
)
mock_original_setup_entry = AsyncMock(return_value=True)
integration = mock_integration(
hass, MockModule("original", async_setup_entry=mock_original_setup_entry)
)
mock_setup = AsyncMock(return_value=False) mock_setup = AsyncMock(return_value=False)
mock_setup_entry = AsyncMock() mock_setup_entry = AsyncMock()
@ -992,9 +1001,62 @@ 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"])
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 0
async def test_async_forward_entry_setup_deprecated(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test async_forward_entry_setup is deprecated."""
entry = MockConfigEntry(
domain="original", state=config_entries.ConfigEntryState.LOADED
)
mock_original_setup_entry = AsyncMock(return_value=True)
integration = mock_integration(
hass, MockModule("original", async_setup_entry=mock_original_setup_entry)
)
mock_setup = AsyncMock(return_value=False)
mock_setup_entry = AsyncMock()
mock_integration(
hass,
MockModule(
"forwarded", async_setup=mock_setup, async_setup_entry=mock_setup_entry
),
)
with patch.object(integration, "async_get_platforms"):
await hass.config_entries.async_forward_entry_setup(entry, "forwarded") await hass.config_entries.async_forward_entry_setup(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
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:
await hass.config_entries.async_forward_entry_setup(entry, "forwarded")
assert (
"Detected code that calls async_forward_entry_setup for integration, "
f"original with title: Mock Title and entry_id: {entry_id}, "
"which is deprecated and will stop working in Home Assistant 2025.6, "
"await async_forward_entry_setups instead. Please report this issue."
) in caplog.text
async def test_discovery_notification( async def test_discovery_notification(
@ -5483,3 +5545,147 @@ async def test_raise_wrong_exception_in_forwarded_platform(
f"Instead raise {exc_type_name} before calling async_forward_entry_setups" f"Instead raise {exc_type_name} before calling async_forward_entry_setups"
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."""
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,
manager: config_entries.ConfigEntries,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test async_forward_entry_setups not being awaited."""
task = None
async def mock_setup_entry(
hass: HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Mock setting up entry."""
# Call async_late_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.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