diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index bd85391f98f..6b566797017 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -84,9 +84,19 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): self.update_interval = update_interval self._shutdown_requested = False if config_entry is UNDEFINED: + # late import to avoid circular imports + from . import frame # noqa: PLC0415 + + # It is not planned to enforce this for custom integrations. + # see https://github.com/home-assistant/core/pull/138161#discussion_r1958184241 + frame.report_usage( + "relies on ContextVar, but should pass the config entry explicitly.", + core_behavior=frame.ReportBehavior.ERROR, + custom_integration_behavior=frame.ReportBehavior.LOG, + breaks_in_ha_version="2026.8", + ) + self.config_entry = config_entries.current_entry.get() - # This should be deprecated once all core integrations are updated - # to pass in the config entry explicitly. else: self.config_entry = config_entry self.always_update = always_update diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 5fd9f9e39fd..b4216a3fc6d 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -19,7 +19,7 @@ from homeassistant.exceptions import ( ConfigEntryError, ConfigEntryNotReady, ) -from homeassistant.helpers import update_coordinator +from homeassistant.helpers import frame, update_coordinator from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, async_fire_time_changed @@ -165,8 +165,6 @@ async def test_shutdown_on_entry_unload( ) -> None: """Test shutdown is requested on entry unload.""" entry = MockConfigEntry() - config_entries.current_entry.set(entry) - calls = 0 async def _refresh() -> int: @@ -177,6 +175,7 @@ async def test_shutdown_on_entry_unload( crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, + config_entry=entry, name="test", update_method=_refresh, update_interval=DEFAULT_UPDATE_INTERVAL, @@ -206,6 +205,7 @@ async def test_shutdown_on_hass_stop( crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, + config_entry=None, name="test", update_method=_refresh, update_interval=DEFAULT_UPDATE_INTERVAL, @@ -843,6 +843,7 @@ async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None: crd = update_coordinator.TimestampDataUpdateCoordinator[int]( hass, _LOGGER, + config_entry=None, name="test", update_method=refresh, update_interval=timedelta(seconds=10), @@ -865,39 +866,133 @@ async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None: assert len(last_update_success_times) == 1 -async def test_config_entry(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + "integration_frame_path", ["homeassistant/components/my_integration"] +) +@pytest.mark.usefixtures("mock_integration_frame") +async def test_config_entry( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: """Test behavior of coordinator.entry.""" entry = MockConfigEntry() - # Default without context should be None - crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") - assert crd.config_entry is None - # Explicit None is OK crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=None ) assert crd.config_entry is None + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) # Explicit entry is OK + caplog.clear() crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=entry ) assert crd.config_entry is entry + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) + + # Explicit entry different from ContextVar not recommended, but should work + another_entry = MockConfigEntry() + caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( + hass, _LOGGER, name="test", config_entry=another_entry + ) + assert crd.config_entry is another_entry + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) + + # Default without context should log a warning + caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") + assert crd.config_entry is None + assert ( + "Detected that integration 'my_integration' relies on ContextVar, " + "but should pass the config entry explicitly." + ) in caplog.text + + # Default with context should log a warning + caplog.clear() + frame._REPORTED_INTEGRATIONS.clear() + config_entries.current_entry.set(entry) + crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") + assert ( + "Detected that integration 'my_integration' relies on ContextVar, " + "but should pass the config entry explicitly." + ) in caplog.text + assert crd.config_entry is entry + + +@pytest.mark.parametrize("integration_frame_path", ["custom_components/my_integration"]) +@pytest.mark.usefixtures("hass", "mock_integration_frame") +async def test_config_entry_custom_integration( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test behavior of coordinator.entry for custom integrations.""" + entry = MockConfigEntry(domain="custom_integration") + + # Default without context should be None + crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") + assert crd.config_entry is None + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) + + # Explicit None is OK + caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( + hass, _LOGGER, name="test", config_entry=None + ) + assert crd.config_entry is None + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) + + # Explicit entry is OK + caplog.clear() + crd = update_coordinator.DataUpdateCoordinator[int]( + hass, _LOGGER, name="test", config_entry=entry + ) + assert crd.config_entry is entry + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) # set ContextVar config_entries.current_entry.set(entry) # Default with ContextVar should match the ContextVar + caplog.clear() crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") assert crd.config_entry is entry + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) # Explicit entry different from ContextVar not recommended, but should work another_entry = MockConfigEntry() + caplog.clear() crd = update_coordinator.DataUpdateCoordinator[int]( hass, _LOGGER, name="test", config_entry=another_entry ) assert crd.config_entry is another_entry + assert ( + "Detected that integration 'my_integration' relies on ContextVar" + not in caplog.text + ) async def test_listener_unsubscribe_releases_coordinator(hass: HomeAssistant) -> None: @@ -920,7 +1015,7 @@ async def test_listener_unsubscribe_releases_coordinator(hass: HomeAssistant) -> self._unsub = None coordinator = update_coordinator.DataUpdateCoordinator[int]( - hass, _LOGGER, name="test" + hass, _LOGGER, config_entry=None, name="test" ) subscriber = Subscriber() subscriber.start_listen(coordinator) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index dc893e4c5fd..7fb632e18b5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -4901,6 +4901,7 @@ async def test_setup_raise_entry_error_from_first_coordinator_update( hass, logging.getLogger(__name__), name="any", + config_entry=entry, update_method=_async_update_data, update_interval=timedelta(seconds=1000), ) @@ -4941,6 +4942,7 @@ async def test_setup_not_raise_entry_error_from_future_coordinator_update( hass, logging.getLogger(__name__), name="any", + config_entry=entry, update_method=_async_update_data, update_interval=timedelta(seconds=1000), ) @@ -5020,6 +5022,7 @@ async def test_setup_raise_auth_failed_from_first_coordinator_update( hass, logging.getLogger(__name__), name="any", + config_entry=entry, update_method=_async_update_data, update_interval=timedelta(seconds=1000), ) @@ -5072,6 +5075,7 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update( hass, logging.getLogger(__name__), name="any", + config_entry=entry, update_method=_async_update_data, update_interval=timedelta(seconds=1000), )