diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index aded84427a5..d0b04e53e67 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_late_forward_entry_setups( + self._hass.config_entries.async_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 2a59b10588f..bc6ae29ee8e 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_late_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) async_at_started(hass, _async_finish_startup) return True diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index c45a6dcf253..494669ae839 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -248,16 +248,10 @@ class RuntimeEntryData: hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform], - late: bool, ) -> None: async with self.platform_load_lock: if needed := platforms - self.loaded_platforms: - if late: - await hass.config_entries.async_late_forward_entry_setups( - entry, needed - ) - else: - await hass.config_entries.async_forward_entry_setups(entry, needed) + await hass.config_entries.async_forward_entry_setups(entry, needed) self.loaded_platforms |= needed async def async_update_static_infos( @@ -266,7 +260,6 @@ class RuntimeEntryData: entry: ConfigEntry, infos: list[EntityInfo], mac: str, - late: bool = False, ) -> None: """Distribute an update of static infos to all platforms.""" # First, load all platforms @@ -296,7 +289,7 @@ class RuntimeEntryData: ): ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id) - await self._ensure_platforms_loaded(hass, entry, needed_platforms, late) + await self._ensure_platforms_loaded(hass, entry, needed_platforms) # Make a dict of the EntityInfo by type and send # them to the listeners for each specific EntityInfo type diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 09a751eb72e..f191c36c574 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, late=True + hass, entry, entity_infos, device_info.mac_address ) _setup_services(hass, entry_data, services) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 687e1b14247..ea520e88366 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -379,9 +379,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: new_config: list[ConfigType] = config_yaml.get(DOMAIN, []) platforms_used = platforms_from_config(new_config) new_platforms = platforms_used - mqtt_data.platforms_loaded - await async_forward_entry_setup_and_setup_discovery( - hass, entry, new_platforms, late=True - ) + await async_forward_entry_setup_and_setup_discovery(hass, entry, new_platforms) # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 2ee7dffc18f..0d93af26a57 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}, late=True + hass, config_entry, {component} ) _async_add_component(discovery_payload) diff --git a/homeassistant/components/mqtt/util.py b/homeassistant/components/mqtt/util.py index 747a2c43f76..256bad71ba6 100644 --- a/homeassistant/components/mqtt/util.py +++ b/homeassistant/components/mqtt/util.py @@ -72,11 +72,13 @@ async def async_forward_entry_setup_and_setup_discovery( tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry))) if new_entity_platforms := (new_platforms - {"tag", "device_automation"}): - if late: - coro = hass.config_entries.async_late_forward_entry_setups - else: - coro = hass.config_entries.async_forward_entry_setups - tasks.append(create_eager_task(coro(config_entry, new_entity_platforms))) + tasks.append( + create_eager_task( + hass.config_entries.async_forward_entry_setups( + config_entry, new_entity_platforms + ) + ) + ) if not tasks: return await asyncio.gather(*tasks) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 3415f1b22db..5bb05d48d62 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -200,9 +200,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice]( self.hass.config_entries.async_update_entry(self.entry, data=data) # Resume platform setup - await self.hass.config_entries.async_late_forward_entry_setups( - self.entry, platforms - ) + await self.hass.config_entries.async_forward_entry_setups(self.entry, platforms) return True diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 4b0cc4ac7a9..2b10f415bb7 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -330,7 +330,7 @@ class DriverEvents: """Set up platform if needed.""" if platform not in self.platform_setup_tasks: self.platform_setup_tasks[platform] = self.hass.async_create_task( - self.hass.config_entries.async_late_forward_entry_setups( + self.hass.config_entries.async_forward_entry_setups( self.config_entry, [platform] ) ) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 1ca6e99f262..fdcf4ad7604 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1170,18 +1170,13 @@ class FlowCancelledError(Exception): """Error to indicate that a flow has been cancelled.""" -def _report_non_locked_platform_forwards(entry: ConfigEntry) -> None: - """Report non awaited and non-locked platform forwards.""" +def _report_non_awaited_platform_forwards(entry: ConfigEntry, what: str) -> None: + """Report non awaited platform forwards.""" report( - f"calls async_forward_entry_setup after the entry for " - f"integration, {entry.domain} with title: {entry.title} " - f"and entry_id: {entry.entry_id}, has been set up, " - "without holding the setup lock that prevents the config " - "entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. " + f"calls {what} for integration {entry.domain} with " + f"title: {entry.title} and entry_id: {entry.entry_id}, " + f"during setup without awaiting {what}, which can cause " + "the setup lock to be released before the setup is done. " "This will stop working in Home Assistant 2025.1", error_if_integration=False, error_if_core=False, @@ -2041,9 +2036,6 @@ class ConfigEntries: before the entry is set up. This ensures that the config entry cannot be unloaded before all platforms are loaded. - If platforms must be loaded late (after the config entry is setup), - use async_late_forward_entry_setup instead. - This method is more efficient than async_forward_entry_setup as it can load multiple platforms at once and does not require a separate import executor job for each platform. @@ -2052,14 +2044,32 @@ class ConfigEntries: if not integration.platforms_are_loaded(platforms): with async_pause_setup(self.hass, SetupPhases.WAIT_IMPORT_PLATFORMS): await integration.async_get_platforms(platforms) - if non_locked_platform_forwards := not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) + + if not entry.setup_lock.locked(): + async with entry.setup_lock: + if entry.state is not ConfigEntryState.LOADED: + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id" + f" {entry.entry_id} cannot forward setup for {platforms} because it" + f" is not loaded in the {entry.state} state" + ) + await self._async_forward_entry_setups_locked(entry, platforms) + else: + await self._async_forward_entry_setups_locked(entry, platforms) + # If the lock was held when we stated, and it was released during + # the platform setup, it means they did not await the setup call. + if not entry.setup_lock.locked(): + _report_non_awaited_platform_forwards( + entry, "async_forward_entry_setups" + ) + + async def _async_forward_entry_setups_locked( + self, entry: ConfigEntry, platforms: Iterable[Platform | str] + ) -> None: await asyncio.gather( *( create_eager_task( - self._async_forward_entry_setup( - entry, platform, False, non_locked_platform_forwards - ), + self._async_forward_entry_setup(entry, platform, False), name=( f"config entry forward setup {entry.title} " f"{entry.domain} {entry.entry_id} {platform}" @@ -2070,25 +2080,6 @@ class ConfigEntries: ) ) - async def async_late_forward_entry_setups( - self, entry: ConfigEntry, platforms: Iterable[Platform | str] - ) -> None: - """Forward the setup of an entry to platforms after setup. - - If platforms must be loaded late (after the config entry is setup), - use this method instead of async_forward_entry_setups as it holds - the setup lock until the platforms are loaded to ensure that the - config entry cannot be unloaded while platforms are loaded. - """ - async with entry.setup_lock: - if entry.state is not ConfigEntryState.LOADED: - raise OperationNotAllowed( - f"The config entry {entry.title} ({entry.domain}) with entry_id" - f" {entry.entry_id} cannot forward setup for {platforms} " - f"because it is not loaded in the {entry.state} state" - ) - await self.async_forward_entry_setups(entry, platforms) - async def async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str ) -> bool: @@ -2103,32 +2094,37 @@ class ConfigEntries: Instead, await async_forward_entry_setups as it can load multiple platforms at once and is more efficient since it does not require a separate import executor job for each platform. - - If platforms must be loaded late (after the config entry is setup), - use async_late_forward_entry_setup instead. """ - if non_locked_platform_forwards := not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) - else: - report( - "calls async_forward_entry_setup for " - f"integration, {entry.domain} with title: {entry.title} " - 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 + 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, ) + if not entry.setup_lock.locked(): + async with entry.setup_lock: + if entry.state is not ConfigEntryState.LOADED: + raise OperationNotAllowed( + f"The config entry {entry.title} ({entry.domain}) with entry_id" + f" {entry.entry_id} cannot forward setup for {domain} because it" + f" is not loaded in the {entry.state} state" + ) + return await self._async_forward_entry_setup(entry, domain, True) + result = await self._async_forward_entry_setup(entry, domain, True) + # If the lock was held when we stated, and it was released during + # the platform setup, it means they did not await the setup call. + if not entry.setup_lock.locked(): + _report_non_awaited_platform_forwards(entry, "async_forward_entry_setup") + return result async def _async_forward_entry_setup( self, entry: ConfigEntry, domain: Platform | str, preload_platform: bool, - non_locked_platform_forwards: bool, ) -> bool: """Forward the setup of an entry to a different component.""" # Setup Component if not set up yet @@ -2152,12 +2148,6 @@ class ConfigEntries: integration = loader.async_get_loaded_integration(self.hass, domain) await entry.async_setup(self.hass, integration=integration) - - # Check again after setup to make sure the lock - # is still there because it could have been released - # unless we already reported it. - if not non_locked_platform_forwards and not entry.setup_lock.locked(): - _report_non_locked_platform_forwards(entry) return True async def async_unload_platforms( @@ -2221,7 +2211,7 @@ class ConfigEntries: # The component was not loaded. if entry.domain not in self.hass.config.components: return False - return entry.state == ConfigEntryState.LOADED + return entry.state is ConfigEntryState.LOADED @callback diff --git a/tests/components/assist_pipeline/test_select.py b/tests/components/assist_pipeline/test_select.py index 35f1e015d5d..9fb02e228d8 100644 --- a/tests/components/assist_pipeline/test_select.py +++ b/tests/components/assist_pipeline/test_select.py @@ -53,7 +53,7 @@ async def init_select(hass: HomeAssistant, init_components) -> ConfigEntry: domain="assist_pipeline", state=ConfigEntryState.LOADED ) config_entry.add_to_hass(hass) - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) return config_entry @@ -161,7 +161,7 @@ async def test_select_entity_changing_pipelines( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_prefix_pipeline") assert state is not None @@ -209,7 +209,7 @@ async def test_select_entity_changing_vad_sensitivity( # Reload config entry to test selected option persists assert await hass.config_entries.async_forward_entry_unload(config_entry, "select") - await hass.config_entries.async_late_forward_entry_setups(config_entry, ["select"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["select"]) state = hass.states.get("select.assist_pipeline_test_vad_sensitivity") assert state is not None diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index e824e8cb149..fca950d6b7a 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -278,7 +278,7 @@ async def setup_platform( await hass.async_block_till_done() config_entry.mock_state(hass, ConfigEntryState.LOADED) - await hass.config_entries.async_late_forward_entry_setups(config_entry, platforms) + await hass.config_entries.async_forward_entry_setups(config_entry, platforms) # and make sure it completes before going further await hass.async_block_till_done() diff --git a/tests/components/hue/test_light_v1.py b/tests/components/hue/test_light_v1.py index 3172e834954..21b35e6d5e8 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_late_forward_entry_setups(config_entry, ["light"]) + await hass.config_entries.async_forward_entry_setups(config_entry, ["light"]) # To flush out the service call to update the group await hass.async_block_till_done() diff --git a/tests/components/hue/test_sensor_v2.py b/tests/components/hue/test_sensor_v2.py index ae02c775191..beb86de505b 100644 --- a/tests/components/hue/test_sensor_v2.py +++ b/tests/components/hue/test_sensor_v2.py @@ -75,7 +75,7 @@ async def test_enable_sensor( assert await async_setup_component(hass, hue.DOMAIN, {}) is True await hass.async_block_till_done() - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( mock_config_entry_v2, ["sensor"] ) @@ -95,7 +95,7 @@ async def test_enable_sensor( # reload platform and check if entity is correctly there await hass.config_entries.async_forward_entry_unload(mock_config_entry_v2, "sensor") - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( mock_config_entry_v2, ["sensor"] ) await hass.async_block_till_done() diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 52abe75f966..e3e2ce3227a 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -104,7 +104,7 @@ async def test_restoring_location( # mobile app doesn't support unloading, so we just reload device tracker await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker") - await hass.config_entries.async_late_forward_entry_setups( + await hass.config_entries.async_forward_entry_setups( config_entry, ["device_tracker"] ) await hass.async_block_till_done() diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index abe7657021c..baef9d9fa82 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -71,7 +71,7 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): hass.data[DOMAIN] = {DATA_BROKERS: {config_entry.entry_id: broker}} config_entry.mock_state(hass, ConfigEntryState.LOADED) - await hass.config_entries.async_late_forward_entry_setups(config_entry, [platform]) + await hass.config_entries.async_forward_entry_setups(config_entry, [platform]) await hass.async_block_till_done() return config_entry diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 7be10571222..c8388207af4 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_setups", + ), ( # This test explicitly throws an uncaught exception # and should not be removed. diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index d410cb4568a..b23b247b7a3 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -35,6 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_set_domains_to_be_loaded, async_setup_component +from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from .common import ( @@ -971,7 +972,7 @@ async def test_forward_entry_sets_up_component(hass: HomeAssistant) -> None: ) with patch.object(integration, "async_get_platforms") as mock_async_get_platforms: - await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) + await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"]) mock_async_get_platforms.assert_called_once_with(["forwarded"]) assert len(mock_original_setup_entry.mock_calls) == 0 @@ -1001,7 +1002,7 @@ async def test_forward_entry_does_not_setup_entry_if_setup_fails( ) with patch.object(integration, "async_get_platforms"): - await hass.config_entries.async_late_forward_entry_setups(entry, ["forwarded"]) + await hass.config_entries.async_forward_entry_setups(entry, ["forwarded"]) assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 @@ -1028,23 +1029,7 @@ async def test_async_forward_entry_setup_deprecated( ), ) - with patch.object(integration, "async_get_platforms"): - await hass.config_entries.async_forward_entry_setup(entry, "forwarded") - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 0 entry_id = entry.entry_id - assert ( - "Detected code that calls async_forward_entry_setup after the entry " - "for integration, original with title: Mock Title and entry_id: " - f"{entry_id}, has been set up, without holding the setup lock that " - "prevents the config entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. This will stop working in Home Assistant " - "2025.1. Please report this issue." - ) in caplog.text - caplog.clear() with patch.object(integration, "async_get_platforms"): async with entry.setup_lock: @@ -5553,77 +5538,7 @@ async def test_raise_wrong_exception_in_forwarded_platform( ) -async def test_non_awaited_async_forward_entry_setups( - hass: HomeAssistant, - manager: config_entries.ConfigEntries, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test async_forward_entry_setups not being awaited.""" - - async def mock_setup_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock setting up entry.""" - # Call async_forward_entry_setups without awaiting it - # This is not allowed and will raise a warning - hass.async_create_task( - hass.config_entries.async_forward_entry_setups(entry, ["light"]) - ) - return True - - async def mock_unload_entry( - hass: HomeAssistant, entry: config_entries.ConfigEntry - ) -> bool: - """Mock unloading an entry.""" - result = await hass.config_entries.async_unload_platforms(entry, ["light"]) - assert result - return result - - mock_remove_entry = AsyncMock(return_value=None) - - async def mock_setup_entry_platform( - hass: HomeAssistant, - entry: config_entries.ConfigEntry, - async_add_entities: AddEntitiesCallback, - ) -> None: - """Mock setting up platform.""" - await asyncio.sleep(0) - - mock_integration( - hass, - MockModule( - "test", - async_setup_entry=mock_setup_entry, - async_unload_entry=mock_unload_entry, - async_remove_entry=mock_remove_entry, - ), - ) - mock_platform( - hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) - ) - mock_platform(hass, "test.config_flow", None) - - entry = MockConfigEntry(domain="test", entry_id="test2") - entry.add_to_manager(manager) - - # Setup entry - await manager.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert ( - "Detected code that calls async_forward_entry_setup after the " - "entry for integration, test with title: Mock Title and entry_id:" - " test2, has been set up, without holding the setup lock that " - "prevents the config entry from being set up multiple times. " - "Instead await hass.config_entries.async_forward_entry_setup " - "during setup of the config entry or call " - "hass.config_entries.async_late_forward_entry_setups " - "in a tracked task. This will stop working in Home Assistant" - " 2025.1. Please report this issue." - ) in caplog.text - - -async def test_config_entry_unloaded_during_platform_setup( +async def test_config_entry_unloaded_during_platform_setups( hass: HomeAssistant, manager: config_entries.ConfigEntries, caplog: pytest.LogCaptureFixture, @@ -5636,12 +5551,12 @@ async def test_config_entry_unloaded_during_platform_setup( ) -> bool: """Mock setting up entry.""" - # Call async_late_forward_entry_setups in a non-tracked task + # Call async_forward_entry_setups in a non-tracked task # so we can unload the config entry during the setup def _late_setup(): nonlocal task task = asyncio.create_task( - hass.config_entries.async_late_forward_entry_setups(entry, ["light"]) + hass.config_entries.async_forward_entry_setups(entry, ["light"]) ) hass.loop.call_soon(_late_setup) @@ -5695,3 +5610,294 @@ async def test_config_entry_unloaded_during_platform_setup( "entry_id test2 cannot forward setup for ['light'] because it is " "not loaded in the ConfigEntryState.NOT_LOADED state" ) in caplog.text + + +async def test_non_awaited_async_forward_entry_setups( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setups not being awaited.""" + forward_event = asyncio.Event() + task: asyncio.Task | None = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + # Call async_forward_entry_setups without awaiting it + # This is not allowed and will raise a warning + nonlocal task + task = create_eager_task( + hass.config_entries.async_forward_entry_setups(entry, ["light"]) + ) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await forward_event.wait() + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + forward_event.set() + await hass.async_block_till_done() + await task + + assert ( + "Detected code that calls async_forward_entry_setups for integration " + "test with title: Mock Title and entry_id: test2, during setup without " + "awaiting async_forward_entry_setups, which can cause the setup lock " + "to be released before the setup is done. This will stop working in " + "Home Assistant 2025.1. Please report this issue." + ) in caplog.text + + +async def test_non_awaited_async_forward_entry_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + forward_event = asyncio.Event() + task: asyncio.Task | None = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + # Call async_forward_entry_setup without awaiting it + # This is not allowed and will raise a warning + nonlocal task + task = create_eager_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await forward_event.wait() + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + forward_event.set() + await hass.async_block_till_done() + await task + + assert ( + "Detected code that calls async_forward_entry_setup for integration " + "test with title: Mock Title and entry_id: test2, during setup without " + "awaiting async_forward_entry_setup, which can cause the setup lock " + "to be released before the setup is done. This will stop working in " + "Home Assistant 2025.1. Please report this issue." + ) in caplog.text + + +async def test_config_entry_unloaded_during_platform_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + task = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + + # Call async_forward_entry_setup in a non-tracked task + # so we can unload the config entry during the setup + def _late_setup(): + nonlocal task + task = asyncio.create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + + hass.loop.call_soon(_late_setup) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + await manager.async_unload(entry.entry_id) + await hass.async_block_till_done() + del task + + assert ( + "OperationNotAllowed: The config entry Mock Title (test) with " + "entry_id test2 cannot forward setup for light because it is " + "not loaded in the ConfigEntryState.NOT_LOADED state" + ) in caplog.text + + +async def test_config_entry_late_platform_setup( + hass: HomeAssistant, + manager: config_entries.ConfigEntries, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test async_forward_entry_setup not being awaited.""" + task = None + + async def mock_setup_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock setting up entry.""" + + # Call async_forward_entry_setup in a non-tracked task + # so we can unload the config entry during the setup + def _late_setup(): + nonlocal task + task = asyncio.create_task( + hass.config_entries.async_forward_entry_setup(entry, "light") + ) + + hass.loop.call_soon(_late_setup) + return True + + async def mock_unload_entry( + hass: HomeAssistant, entry: config_entries.ConfigEntry + ) -> bool: + """Mock unloading an entry.""" + result = await hass.config_entries.async_unload_platforms(entry, ["light"]) + assert result + return result + + mock_remove_entry = AsyncMock(return_value=None) + + async def mock_setup_entry_platform( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Mock setting up platform.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + mock_integration( + hass, + MockModule( + "test", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + async_remove_entry=mock_remove_entry, + ), + ) + mock_platform( + hass, "test.light", MockPlatform(async_setup_entry=mock_setup_entry_platform) + ) + mock_platform(hass, "test.config_flow", None) + + entry = MockConfigEntry(domain="test", entry_id="test2") + entry.add_to_manager(manager) + + # Setup entry + await manager.async_setup(entry.entry_id) + await hass.async_block_till_done() + await task + await hass.async_block_till_done() + + assert ( + "OperationNotAllowed: The config entry Mock Title (test) with " + "entry_id test2 cannot forward setup for light because it is " + "not loaded in the ConfigEntryState.NOT_LOADED state" + ) not in caplog.text