diff --git a/.coveragerc b/.coveragerc index 071fdade647dab8b579762dfd6d35452dc1f7604..fefd9205b052a20326c00069808b7f619f1719e5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1556,6 +1556,7 @@ omit = homeassistant/components/verisure/sensor.py homeassistant/components/verisure/switch.py homeassistant/components/versasense/* + homeassistant/components/vesync/__init__.py homeassistant/components/vesync/fan.py homeassistant/components/vesync/light.py homeassistant/components/vesync/sensor.py diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index d0b04e53e67ae2715cb207d6382b56248b906ffe..aded84427a5e9ebada5499f409388594c7f983ea 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -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_forward_entry_setups( + self._hass.config_entries.async_late_forward_entry_setups( self._entry, PLATFORMS ), eager_start=True, diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index bc6ae29ee8e96c1ff2e32a34e8352d425ad4da88..2a59b10588f9414312280c5716df399616e037ad 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -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_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_late_forward_entry_setups(entry, PLATFORMS) async_at_started(hass, _async_finish_startup) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 19e5267e8bc78876ba16d9332a6ca1a890a85de0..c45a6dcf2537493d7c72aa25ddc154e455b26ee5 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -244,15 +244,29 @@ class RuntimeEntryData: callback_(static_info) async def _ensure_platforms_loaded( - self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform] + self, + hass: HomeAssistant, + entry: ConfigEntry, + platforms: set[Platform], + late: bool, ) -> None: async with self.platform_load_lock: if needed := platforms - self.loaded_platforms: - await hass.config_entries.async_forward_entry_setups(entry, needed) + 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 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: """Distribute an update of static infos to 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) - 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 # them to the listeners for each specific EntityInfo type diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index f191c36c5743971b43cba6d66a87a2dde7f47ff7..09a751eb72e71f559cd121daf73476cf4381a436 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -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 + hass, entry, entity_infos, device_info.mac_address, late=True ) _setup_services(hass, entry_data, services) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index da68dc36a6d9b12743d9f20aad8cc46d2fd7caa7..9c64b4e1b3105a95c6fc070bbe1b7b6278a8a721 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -191,15 +191,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: create_knx_exposure(hass, knx_module.xknx, expose_config) ) # always forward sensor for system entities (telegram counter, etc.) - await hass.config_entries.async_forward_entry_setup(entry, Platform.SENSOR) - await hass.config_entries.async_forward_entry_setups( - entry, - [ - platform - for platform in SUPPORTED_PLATFORMS - if platform in config and platform is not Platform.SENSOR - ], - ) + platforms = {platform for platform in SUPPORTED_PLATFORMS if platform in config} + platforms.add(Platform.SENSOR) + await hass.config_entries.async_forward_entry_setups(entry, platforms) # set up notify service for backwards compatibility - remove 2024.11 if NotifySchema.PLATFORM in config: diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ea520e8836657a04ae711db149b6aa3ca9d76eee..687e1b142472553441224b37d3f4f1980c3d0aa7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -379,7 +379,9 @@ 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) + await async_forward_entry_setup_and_setup_discovery( + hass, entry, new_platforms, late=True + ) # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 0d93af26a57303591e57e9652e9adb4842497d12..2ee7dffc18f3cf37ac1705323423247d9a693524 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -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} + hass, config_entry, {component}, late=True ) _async_add_component(discovery_payload) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index eeca23613059ec4f5f54398683163807058afa46..747a2c43f766ddea66943c277c0a5e0cc8e30be4 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -47,7 +47,10 @@ def platforms_from_config(config: list[ConfigType]) -> set[Platform | str]: 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: """Forward the config entry setup to the platforms and set up discovery.""" 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))) if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): - tasks.append( - create_eager_task( - hass.config_entries.async_forward_entry_setups( - config_entry, new_entity_platforms - ) - ) - ) + 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))) if not tasks: return await asyncio.gather(*tasks) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index e15363790841673b3de8a0bee2da5b4d90229b07..138bc8be596af386bae099a6f84a98cef9748cc5 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -205,8 +205,8 @@ class MinutPointClient: config_entries_key = f"{platform}.{DOMAIN}" async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: - await self._hass.config_entries.async_forward_entry_setup( - self._config_entry, platform + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, [platform] ) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(config_entries_key) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index cf6e9cc897f5b91357748cee36fd919cbf87cd96..2fe3f6a994336ed58a66004c3b052e07124b5ad8 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -200,7 +200,9 @@ 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_forward_entry_setups(self.entry, platforms) + await self.hass.config_entries.async_late_forward_entry_setups( + self.entry, platforms + ) return True diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 92e61edec567a2d333405ed4f54371a0bf7beb5a..4f88b47b531f933558109e55a4aeb6cf45d5009e 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -180,8 +180,8 @@ class TelldusLiveClient: ) async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if component not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: - await self._hass.config_entries.async_forward_entry_setup( - self._config_entry, component + await self._hass.config_entries.async_forward_entry_setups( + self._config_entry, [component] ) self._hass.data[CONFIG_ENTRY_IS_SETUP].add(component) device_ids = [] diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index e758636900bdcfab6ff40550aeeb99bdc4228036..7dceb1b3f8faf789ec1cea3249be37aee348bf9a 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -46,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b 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][VS_MANAGER] = manager @@ -97,7 +97,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_switches and not 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) new_fans = list(fan_set.difference(fans)) @@ -107,7 +107,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_fans and not 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) new_lights = list(light_set.difference(lights)) @@ -117,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_lights and not 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) new_sensors = list(sensor_set.difference(sensors)) @@ -127,7 +127,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b return if new_sensors and not 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( DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index efd9ab717adc21187d29ccd861eb3dc2084bd91f..2b6852126424b11264199307fb43a3d6b4bd7f32 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -324,8 +324,8 @@ 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_forward_entry_setup( - self.config_entry, platform + self.hass.config_entries.async_late_forward_entry_setups( + self.config_entry, [platform] ) ) await self.platform_setup_tasks[platform] diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 01363ec8129726e62e9e4de501dab97334cc7fd3..8da9b50ffa937603cf8ac5cbc9a33a67f4a49273 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1178,6 +1178,24 @@ 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.""" + 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]): """Manage all the config entry flows that are in progress.""" @@ -2024,15 +2042,32 @@ class ConfigEntries: async def async_forward_entry_setups( self, entry: ConfigEntry, platforms: Iterable[Platform | str] ) -> 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) 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) await asyncio.gather( *( create_eager_task( - self._async_forward_entry_setup(entry, platform, False), + self._async_forward_entry_setup( + entry, platform, False, non_locked_platform_forwards + ), name=( f"config entry forward setup {entry.title} " 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( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -2051,11 +2105,38 @@ class ConfigEntries: By default an entry is setup with the component it belongs to. If that component also has related platforms, the component will have to 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( - self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool + 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 @@ -2079,6 +2160,12 @@ 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( @@ -2104,7 +2191,11 @@ class ConfigEntries: async def async_forward_entry_unload( self, entry: ConfigEntry, domain: Platform | str ) -> 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. if domain not in self.hass.config.components: return True diff --git a/tests/components/alarm_control_panel/conftest.py b/tests/components/alarm_control_panel/conftest.py index c076dd8ab67980f8854523bdcf2c2913a99a192a..9cb832abca00b743cb7a339c374b96c1f33f8de8 100644 --- a/tests/components/alarm_control_panel/conftest.py +++ b/tests/components/alarm_control_panel/conftest.py @@ -155,8 +155,8 @@ async def setup_lock_platform_test_entity( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, ALARM_CONTROL_PANEL_DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [ALARM_CONTROL_PANEL_DOMAIN] ) return True diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 73c069ddd0424f8e720625c264c4199a941f8989..35f1e015d5dc3352fcdc8c4e6759d8771f7f2186 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -15,7 +15,7 @@ from homeassistant.components.assist_pipeline.select import ( VadSensitivitySelect, ) 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.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo @@ -49,9 +49,11 @@ class SelectPlatform(MockPlatform): async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry: """Initialize select entity.""" 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) - 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 @@ -123,13 +125,14 @@ async def test_select_entity_registering_device( async def test_select_entity_changing_pipelines( hass: HomeAssistant, - init_select: ConfigEntry, + init_select: MockConfigEntry, pipeline_1: Pipeline, pipeline_2: Pipeline, pipeline_storage: PipelineStorageCollection, ) -> None: """Test entity tracking pipeline changes.""" config_entry = init_select # nicer naming + config_entry.mock_state(hass, ConfigEntryState.LOADED) state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") assert state is not None @@ -158,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") - 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") assert state is not None @@ -179,10 +182,11 @@ async def test_select_entity_changing_pipelines( async def test_select_entity_changing_vad_sensitivity( hass: HomeAssistant, - init_select: ConfigEntry, + init_select: MockConfigEntry, ) -> None: """Test entity tracking pipeline changes.""" config_entry = init_select # nicer naming + config_entry.mock_state(hass, ConfigEntryState.LOADED) state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") 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 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") assert state is not None diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 335b9b40d50902195331501056d8750250fab51c..63a921b4c3ee8d7a9e86e7a402b5b81b9f77764b 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -63,8 +63,8 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, binary_sensor.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [binary_sensor.DOMAIN] ) return True @@ -143,8 +143,8 @@ async def test_entity_category_config_raises_error( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, binary_sensor.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [binary_sensor.DOMAIN] ) return True diff --git a/tests/components/button/test_init.py b/tests/components/button/test_init.py index 0641bbe29dc3d3d93810c7ebd6eef57b01588bad..6cb2f1a570007126e5004614b95dde856d076c6d 100644 --- a/tests/components/button/test_init.py +++ b/tests/components/button/test_init.py @@ -139,7 +139,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/calendar/conftest.py b/tests/components/calendar/conftest.py index ba0064cb4e4892c6f489ae5f466dfb286f212b5c..94a2e72e0f475531b2169e827a99fa2e0cb2ae8e 100644 --- a/tests/components/calendar/conftest.py +++ b/tests/components/calendar/conftest.py @@ -120,7 +120,7 @@ def mock_setup_integration( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index 1aaea386320bee701548ef619e85d4684c872750..8e2ec09650c1758ac367c1e3e7285e553df94493 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -50,7 +50,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 5a55fb64090ac03ed1a834181095269d75af17f7..610aea3b01b6df2d5a1a6d274ef1ceded915a021 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -141,6 +141,8 @@ async def test_gateway_setup( device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with patch( "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, @@ -190,8 +192,10 @@ async def test_gateway_device_configuration_url_when_addon( device_registry: dr.DeviceRegistry, ) -> None: """Successful setup.""" + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ): config_entry = await setup_deconz_integration( diff --git a/tests/components/devolo_home_network/test_init.py b/tests/components/devolo_home_network/test_init.py index c4a02f9e3752dbe56fe39298f3dccbd32df9716c..1b8903c568e878d65b9a4c2e1ab947be6e137458 100644 --- a/tests/components/devolo_home_network/test_init.py +++ b/tests/components/devolo_home_network/test_init.py @@ -53,9 +53,11 @@ async def test_setup_without_password(hass: HomeAssistant) -> None: } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) + # Patching async_forward_entry_setup* is not advisable, and should be refactored + # in the future. with ( patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setups", return_value=True, ), patch("homeassistant.core.EventBus.async_listen_once"), diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index b3deb2f33ee64edaf002223cd7fe4df83b833cd2..50ca6104aa4126f749becf74bb18cac031dc863d 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -85,9 +85,8 @@ async def test_update_entity( "homeassistant.components.esphome.update.DomainData.get_entry_data", return_value=Mock(available=True, device_info=mock_device_info), ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("update.none_firmware") 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() - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() # We have a dashboard but it is not available 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", return_value=Mock(available=True, device_info=mock_device_info), ): - assert await hass.config_entries.async_forward_entry_setup( - mock_config_entry, "update" - ) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() state = hass.states.get("update.none_firmware") assert state is None diff --git a/tests/components/event/test_init.py b/tests/components/event/test_init.py index fd3cf0eaf9b221d94ba3dd509bd4b5f40dbe5a10..8e3f1a8a9325c009eda69f91ea630591e7e5388b 100644 --- a/tests/components/event/test_init.py +++ b/tests/components/event/test_init.py @@ -254,7 +254,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 3cb8b7d61e95ab19d813525efd6d21d42da33ec6..2da32b2844d45ebf766a8736eaeb150617e4c1f7 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -88,7 +88,8 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None: home = Mock() hap = HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=home): - assert await hap.async_setup() + async with entry.setup_lock: + assert await hap.async_setup() assert hap.home is home @@ -96,14 +97,17 @@ async def test_hap_setup_works(hass: HomeAssistant) -> None: async def test_hap_setup_connection_error() -> None: """Test a failed accesspoint setup.""" hass = Mock() - entry = Mock() - entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} + entry = MockConfigEntry( + domain=HMIPC_DOMAIN, + data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}, + ) hap = HomematicipHAP(hass, entry) with ( patch.object(hap, "get_hap", side_effect=HmipcConnectionError), pytest.raises(ConfigEntryNotReady), ): - assert not await hap.async_setup() + async with entry.setup_lock: + assert not await hap.async_setup() assert not hass.async_run_hass_job.mock_calls assert not hass.config_entries.flow.async_init.mock_calls @@ -132,7 +136,8 @@ async def test_hap_create( hap = HomematicipHAP(hass, hmip_config_entry) assert hap with patch.object(hap, "async_connect"): - assert await hap.async_setup() + async with hmip_config_entry.setup_lock: + assert await hap.async_setup() async def test_hap_create_exception( diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 39b860fadf275fb27a7261db5057a60e4285cef0..dd27a657e2a6546d84f990214948a0d45e8ee751 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -15,6 +15,7 @@ import pytest from homeassistant.components import hue from homeassistant.components.hue.v1 import sensor_base as hue_sensor_base from homeassistant.components.hue.v2.device import async_setup_devices +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, ServiceCall 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 await hass.async_block_till_done() - for platform in platforms: - 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, platforms) # and make sure it completes before going further await hass.async_block_till_done() diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 5d103e47870c826ecb3cac3f4bb6d91d90c87d2b..42631215035808715264b609b1c8d6d25c6157a3 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -34,7 +34,8 @@ async def test_bridge_setup_v1(hass: HomeAssistant, mock_api_v1) -> None: patch.object(hass.config_entries, "async_forward_entry_setups") as mock_forward, ): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True assert hue_bridge.api is mock_api_v1 assert isinstance(hue_bridge.api, HueBridgeV1) @@ -125,7 +126,8 @@ 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, ): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True await asyncio.sleep(0) @@ -151,7 +153,8 @@ async def test_handle_unauthorized(hass: HomeAssistant, mock_api_v1) -> None: with patch.object(bridge, "HueBridgeV1", return_value=mock_api_v1): hue_bridge = bridge.HueBridge(hass, config_entry) - assert await hue_bridge.async_initialize_bridge() is True + async with config_entry.setup_lock: + assert await hue_bridge.async_initialize_bridge() is True with patch.object(bridge, "create_config_flow") as mock_create: await hue_bridge.handle_unauthorized_error() diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 9a74d9cd99448c390013f6cd5f3444dffa44b838..3172e834954e4f1cc91f6d183e2cc69b251bcd13 100644 --- a/tests/components/hue/test_light_v1.py +++ b/tests/components/hue/test_light_v1.py @@ -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_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 await hass.async_block_till_done() diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index 4c1f8defc95b0d515754fe4f009c3198b16538fc..ae02c775191f823d92470f9a61b50e80dc565130 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -75,7 +75,9 @@ 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_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_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 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() state = hass.states.get(entity_id) diff --git a/tests/components/image/conftest.py b/tests/components/image/conftest.py index 35c9f0a86af4d64dd7a42aa5f5b99ea4ca738417..4592ccf58d5c4392b8538f4b26aafa354ba32c05 100644 --- a/tests/components/image/conftest.py +++ b/tests/components/image/conftest.py @@ -147,14 +147,16 @@ async def mock_image_config_entry_fixture( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 mock_integration( diff --git a/tests/components/lawn_mower/test_init.py b/tests/components/lawn_mower/test_init.py index 87115cb1900777769369d07e9187bc558ac4c8f1..7dc59fb6f91fc6cf9e81653eae58d462918596d0 100644 --- a/tests/components/lawn_mower/test_init.py +++ b/tests/components/lawn_mower/test_init.py @@ -67,8 +67,8 @@ async def test_lawn_mower_setup(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, Platform.LAWN_MOWER + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.LAWN_MOWER] ) return True diff --git a/tests/components/lock/conftest.py b/tests/components/lock/conftest.py index 07399a39e92a105d7227d3a967249bb6f5803a91..9c0240b098afde660b74b7cd2d0e309f91f080a5 100644 --- a/tests/components/lock/conftest.py +++ b/tests/components/lock/conftest.py @@ -98,7 +98,9 @@ async def setup_lock_platform_test_entity( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 MockPlatform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 21d4d80c791ee7030170822a0face4cd6a17b1a5..52abe75f966cc65d0c8a02ec33723709b056d7ee 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -104,7 +104,9 @@ 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_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() state_2 = hass.states.get("device_tracker.test_1_2") diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py index cfafae28b6ef35bf6a067e0286a6120f3b127265..0c559ad779f3cb6555c1d4af4a498760538e607a 100644 --- a/tests/components/notify/test_init.py +++ b/tests/components/notify/test_init.py @@ -56,7 +56,7 @@ async def help_async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 919c79403c4c66c424e0b39aa3beb4fd092e0193..1ca1264c53b29483df13a6b5df38d71c37c5ba3d 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -874,7 +874,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 100b7ec7186ec1768ab7fa3316a0edc56d8d5057..8dc82483a400920302e5525b9c11e8aebb25dbdc 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -2399,7 +2399,9 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index b638b9bbf4fea385ddb43053cd8a1d81d440a852..abe7657021c8f291acacbee48b2834a3669b40da 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -39,7 +39,7 @@ from homeassistant.components.smartthings.const import ( STORAGE_VERSION, ) 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 ( CONF_ACCESS_TOKEN, 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}} - 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() return config_entry diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 165a520c6532ebaeba7602455633ed0c8dd57875..9aa889f27c9915abe678b019cc03e6d172d3da30 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -187,7 +187,7 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4b8e35c9061dae6614b566f76a73c0fd769b119c..44ebc78591319fd765068ffd1f2d02d4ee8b3bdb 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -91,7 +91,7 @@ def mock_setup_integration(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( diff --git a/tests/components/tts/common.py b/tests/components/tts/common.py index 87a9993c72a019c88f1e5f0c8a1fcd49511ecf72..06712deea998d5a3126ce9ab5a507aa5f9d901db 100644 --- a/tests/components/tts/common.py +++ b/tests/components/tts/common.py @@ -226,7 +226,7 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 async def async_unload_entry_init( diff --git a/tests/components/update/test_init.py b/tests/components/update/test_init.py index 02ca605eed4d4e2c2a6d8ebff5d4deecb31755f8..04e2e5c7076979935e3830bf8e40d0afd1276a87 100644 --- a/tests/components/update/test_init.py +++ b/tests/components/update/test_init.py @@ -782,7 +782,7 @@ async def test_name(hass: HomeAssistant) -> None: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 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 ) -> bool: """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 mock_platform(hass, f"{TEST_DOMAIN}.config_flow") diff --git a/tests/components/vacuum/__init__.py b/tests/components/vacuum/__init__.py index 98a02155b651c1f009909d816d3aa711d78bc6cb..0a681730cb27c1cd19037986656be3cd91163325 100644 --- a/tests/components/vacuum/__init__.py +++ b/tests/components/vacuum/__init__.py @@ -71,7 +71,7 @@ async def help_async_setup_entry_init( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """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 diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py index eee215d2e295c6c294d7f6c1c99344b73139a8c5..1f9f141d89f150a6fea90131d62314ce9b5e001c 100644 --- a/tests/components/valve/test_init.py +++ b/tests/components/valve/test_init.py @@ -152,8 +152,8 @@ def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]: hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, Platform.VALVE + await hass.config_entries.async_forward_entry_setups( + config_entry, [Platform.VALVE] ) return True diff --git a/tests/components/wake_word/test_init.py b/tests/components/wake_word/test_init.py index 1e957ad7a2cd5cfc54b052046acf06d286839839..c4793653c9a94de92c3b4b3fcfe7fb9c1eb64e28 100644 --- a/tests/components/wake_word/test_init.py +++ b/tests/components/wake_word/test_init.py @@ -117,8 +117,8 @@ async def mock_config_entry_setup( hass: HomeAssistant, config_entry: ConfigEntry ) -> bool: """Set up test config entry.""" - await hass.config_entries.async_forward_entry_setup( - config_entry, wake_word.DOMAIN + await hass.config_entries.async_forward_entry_setups( + config_entry, [wake_word.DOMAIN] ) return True diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index aaf6cbe3efe159ddd6abd4582aba77a1f5eecd60..7be10571222b2980bbfe85151fb7f6a2e497d24d 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -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_setup", + ), ( "test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup", diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index a88b6ad31c32a5e474e48aeece0be46bde7ee8a6..017bc5bff253eb25beb732ac23dbf35eb8353bdf 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -957,7 +957,9 @@ async def test_as_dict(snapshot: SnapshotAssertion) -> None: async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: """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) 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) ) - with patch.object(integration, "async_get_platform") as mock_async_get_platform: - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + with patch.object(integration, "async_get_platforms") as mock_async_get_platforms: + 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_forwarded_setup_entry.mock_calls) == 1 @@ -981,7 +983,42 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( hass: HomeAssistant, ) -> None: """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_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_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() @@ -992,9 +1029,34 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( ), ) - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") + 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: + 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( @@ -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" 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