Deprecate the usage of ContextVar for config_entry in coordinator (#138161)

This commit is contained in:
Michael 2025-07-16 21:45:22 +02:00 committed by GitHub
parent e8fca19335
commit 9d178ad5f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 120 additions and 11 deletions

View File

@ -84,9 +84,19 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
self.update_interval = update_interval self.update_interval = update_interval
self._shutdown_requested = False self._shutdown_requested = False
if config_entry is UNDEFINED: 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() 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: else:
self.config_entry = config_entry self.config_entry = config_entry
self.always_update = always_update self.always_update = always_update

View File

@ -19,7 +19,7 @@ from homeassistant.exceptions import (
ConfigEntryError, ConfigEntryError,
ConfigEntryNotReady, ConfigEntryNotReady,
) )
from homeassistant.helpers import update_coordinator from homeassistant.helpers import frame, update_coordinator
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from tests.common import MockConfigEntry, async_fire_time_changed from tests.common import MockConfigEntry, async_fire_time_changed
@ -165,8 +165,6 @@ async def test_shutdown_on_entry_unload(
) -> None: ) -> None:
"""Test shutdown is requested on entry unload.""" """Test shutdown is requested on entry unload."""
entry = MockConfigEntry() entry = MockConfigEntry()
config_entries.current_entry.set(entry)
calls = 0 calls = 0
async def _refresh() -> int: async def _refresh() -> int:
@ -177,6 +175,7 @@ async def test_shutdown_on_entry_unload(
crd = update_coordinator.DataUpdateCoordinator[int]( crd = update_coordinator.DataUpdateCoordinator[int](
hass, hass,
_LOGGER, _LOGGER,
config_entry=entry,
name="test", name="test",
update_method=_refresh, update_method=_refresh,
update_interval=DEFAULT_UPDATE_INTERVAL, update_interval=DEFAULT_UPDATE_INTERVAL,
@ -206,6 +205,7 @@ async def test_shutdown_on_hass_stop(
crd = update_coordinator.DataUpdateCoordinator[int]( crd = update_coordinator.DataUpdateCoordinator[int](
hass, hass,
_LOGGER, _LOGGER,
config_entry=None,
name="test", name="test",
update_method=_refresh, update_method=_refresh,
update_interval=DEFAULT_UPDATE_INTERVAL, update_interval=DEFAULT_UPDATE_INTERVAL,
@ -843,6 +843,7 @@ async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None:
crd = update_coordinator.TimestampDataUpdateCoordinator[int]( crd = update_coordinator.TimestampDataUpdateCoordinator[int](
hass, hass,
_LOGGER, _LOGGER,
config_entry=None,
name="test", name="test",
update_method=refresh, update_method=refresh,
update_interval=timedelta(seconds=10), 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 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.""" """Test behavior of coordinator.entry."""
entry = MockConfigEntry() 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 # Explicit None is OK
crd = update_coordinator.DataUpdateCoordinator[int]( crd = update_coordinator.DataUpdateCoordinator[int](
hass, _LOGGER, name="test", config_entry=None hass, _LOGGER, name="test", config_entry=None
) )
assert crd.config_entry is None assert crd.config_entry is None
assert (
"Detected that integration 'my_integration' relies on ContextVar"
not in caplog.text
)
# Explicit entry is OK # Explicit entry is OK
caplog.clear()
crd = update_coordinator.DataUpdateCoordinator[int]( crd = update_coordinator.DataUpdateCoordinator[int](
hass, _LOGGER, name="test", config_entry=entry hass, _LOGGER, name="test", config_entry=entry
) )
assert crd.config_entry is 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 # set ContextVar
config_entries.current_entry.set(entry) config_entries.current_entry.set(entry)
# Default with ContextVar should match the ContextVar # Default with ContextVar should match the ContextVar
caplog.clear()
crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test") crd = update_coordinator.DataUpdateCoordinator[int](hass, _LOGGER, name="test")
assert crd.config_entry is 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 # Explicit entry different from ContextVar not recommended, but should work
another_entry = MockConfigEntry() another_entry = MockConfigEntry()
caplog.clear()
crd = update_coordinator.DataUpdateCoordinator[int]( crd = update_coordinator.DataUpdateCoordinator[int](
hass, _LOGGER, name="test", config_entry=another_entry hass, _LOGGER, name="test", config_entry=another_entry
) )
assert crd.config_entry is 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: 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 self._unsub = None
coordinator = update_coordinator.DataUpdateCoordinator[int]( coordinator = update_coordinator.DataUpdateCoordinator[int](
hass, _LOGGER, name="test" hass, _LOGGER, config_entry=None, name="test"
) )
subscriber = Subscriber() subscriber = Subscriber()
subscriber.start_listen(coordinator) subscriber.start_listen(coordinator)

View File

@ -4901,6 +4901,7 @@ async def test_setup_raise_entry_error_from_first_coordinator_update(
hass, hass,
logging.getLogger(__name__), logging.getLogger(__name__),
name="any", name="any",
config_entry=entry,
update_method=_async_update_data, update_method=_async_update_data,
update_interval=timedelta(seconds=1000), update_interval=timedelta(seconds=1000),
) )
@ -4941,6 +4942,7 @@ async def test_setup_not_raise_entry_error_from_future_coordinator_update(
hass, hass,
logging.getLogger(__name__), logging.getLogger(__name__),
name="any", name="any",
config_entry=entry,
update_method=_async_update_data, update_method=_async_update_data,
update_interval=timedelta(seconds=1000), update_interval=timedelta(seconds=1000),
) )
@ -5020,6 +5022,7 @@ async def test_setup_raise_auth_failed_from_first_coordinator_update(
hass, hass,
logging.getLogger(__name__), logging.getLogger(__name__),
name="any", name="any",
config_entry=entry,
update_method=_async_update_data, update_method=_async_update_data,
update_interval=timedelta(seconds=1000), update_interval=timedelta(seconds=1000),
) )
@ -5072,6 +5075,7 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update(
hass, hass,
logging.getLogger(__name__), logging.getLogger(__name__),
name="any", name="any",
config_entry=entry,
update_method=_async_update_data, update_method=_async_update_data,
update_interval=timedelta(seconds=1000), update_interval=timedelta(seconds=1000),
) )