diff --git a/homeassistant/core.py b/homeassistant/core.py index 7aa823dc0420fa4ae0f4327ea66e4f07b1682972..108248c9e83ace2018028380bf761d7955c9fdde 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -96,6 +96,7 @@ from .helpers.deprecation import ( dir_with_deprecated_constants, ) from .helpers.json import json_bytes, json_fragment +from .helpers.typing import UNDEFINED, UndefinedType from .util import dt as dt_util, location from .util.async_ import ( cancelling, @@ -131,8 +132,6 @@ FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60 CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30 -# Internal; not helpers.typing.UNDEFINED due to circular dependency -_UNDEF: dict[Any, Any] = {} _SENTINEL = object() _DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any]) type CALLBACK_TYPE = Callable[[], None] @@ -3035,11 +3034,10 @@ class Config: unit_system: str | None = None, location_name: str | None = None, time_zone: str | None = None, - # pylint: disable=dangerous-default-value # _UNDEFs not modified - external_url: str | dict[Any, Any] | None = _UNDEF, - internal_url: str | dict[Any, Any] | None = _UNDEF, + external_url: str | UndefinedType | None = UNDEFINED, + internal_url: str | UndefinedType | None = UNDEFINED, currency: str | None = None, - country: str | dict[Any, Any] | None = _UNDEF, + country: str | UndefinedType | None = UNDEFINED, language: str | None = None, ) -> None: """Update the configuration from a dictionary.""" @@ -3059,14 +3057,14 @@ class Config: self.location_name = location_name if time_zone is not None: await self.async_set_time_zone(time_zone) - if external_url is not _UNDEF: - self.external_url = cast(str | None, external_url) - if internal_url is not _UNDEF: - self.internal_url = cast(str | None, internal_url) + if external_url is not UNDEFINED: + self.external_url = external_url + if internal_url is not UNDEFINED: + self.internal_url = internal_url if currency is not None: self.currency = currency - if country is not _UNDEF: - self.country = cast(str | None, country) + if country is not UNDEFINED: + self.country = country if language is not None: self.language = language @@ -3112,8 +3110,8 @@ class Config: unit_system=data.get("unit_system_v2"), location_name=data.get("location_name"), time_zone=data.get("time_zone"), - external_url=data.get("external_url", _UNDEF), - internal_url=data.get("internal_url", _UNDEF), + external_url=data.get("external_url", UNDEFINED), + internal_url=data.get("internal_url", UNDEFINED), currency=data.get("currency"), country=data.get("country"), language=data.get("language"), diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 82ff136332bb01794d7ea4134eb555c84d196cdb..65e8f4ef97e72a3168a74600d7b2613388816008 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -242,6 +242,26 @@ class DeprecatedAlias(NamedTuple): breaks_in_ha_version: str | None +class DeferredDeprecatedAlias: + """Deprecated alias with deferred evaluation of the value.""" + + def __init__( + self, + value_fn: Callable[[], Any], + replacement: str, + breaks_in_ha_version: str | None, + ) -> None: + """Initialize.""" + self.breaks_in_ha_version = breaks_in_ha_version + self.replacement = replacement + self._value_fn = value_fn + + @functools.cached_property + def value(self) -> Any: + """Return the value.""" + return self._value_fn() + + _PREFIX_DEPRECATED = "_DEPRECATED_" @@ -266,7 +286,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}" ) breaks_in_ha_version = deprecated_const.breaks_in_ha_version - elif isinstance(deprecated_const, DeprecatedAlias): + elif isinstance(deprecated_const, (DeprecatedAlias, DeferredDeprecatedAlias)): description = "alias" value = deprecated_const.value replacement = deprecated_const.replacement @@ -274,8 +294,10 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A if value is None or replacement is None: msg = ( - f"Value of {_PREFIX_DEPRECATED}{name} is an instance of {type(deprecated_const)} " - "but an instance of DeprecatedConstant or DeprecatedConstantEnum is required" + f"Value of {_PREFIX_DEPRECATED}{name} is an instance of " + f"{type(deprecated_const)} but an instance of DeprecatedAlias, " + "DeferredDeprecatedAlias, DeprecatedConstant or DeprecatedConstantEnum " + "is required" ) logging.getLogger(module_name).debug(msg) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 13c54862b8ddf529d65947b1ea6c29038816bc1e..3cdd9ec92507930f7f074d29da79e137dcf2f327 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -5,10 +5,8 @@ from enum import Enum from functools import partial from typing import Any, Never -import homeassistant.core - from .deprecation import ( - DeprecatedAlias, + DeferredDeprecatedAlias, all_with_deprecated_constants, check_if_deprecated_constant, dir_with_deprecated_constants, @@ -35,23 +33,27 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # noqa: SLF001 +def _deprecated_typing_helper(attr: str) -> DeferredDeprecatedAlias: + """Help to make a DeferredDeprecatedAlias.""" + + def value_fn() -> Any: + # pylint: disable-next=import-outside-toplevel + import homeassistant.core + + return getattr(homeassistant.core, attr) + + return DeferredDeprecatedAlias(value_fn, f"homeassistant.core.{attr}", "2025.5") + + # The following types should not used and # are not present in the core code base. # They are kept in order not to break custom integrations # that may rely on them. # Deprecated as of 2024.5 use types from homeassistant.core instead. -_DEPRECATED_ContextType = DeprecatedAlias( - homeassistant.core.Context, "homeassistant.core.Context", "2025.5" -) -_DEPRECATED_EventType = DeprecatedAlias( - homeassistant.core.Event, "homeassistant.core.Event", "2025.5" -) -_DEPRECATED_HomeAssistantType = DeprecatedAlias( - homeassistant.core.HomeAssistant, "homeassistant.core.HomeAssistant", "2025.5" -) -_DEPRECATED_ServiceCallType = DeprecatedAlias( - homeassistant.core.ServiceCall, "homeassistant.core.ServiceCall", "2025.5" -) +_DEPRECATED_ContextType = _deprecated_typing_helper("Context") +_DEPRECATED_EventType = _deprecated_typing_helper("Event") +_DEPRECATED_HomeAssistantType = _deprecated_typing_helper("HomeAssistant") +_DEPRECATED_ServiceCallType = _deprecated_typing_helper("ServiceCall") # These can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 542f9d4f009daa19c85281df6d31fffed76cc771..9afad61042052bd680c0d96416e377682f97c20b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -40,6 +40,7 @@ from .generated.ssdp import SSDP from .generated.usb import USB from .generated.zeroconf import HOMEKIT, ZEROCONF from .helpers.json import json_bytes, json_fragment +from .helpers.typing import UNDEFINED from .util.hass_dict import HassKey from .util.json import JSON_DECODE_EXCEPTIONS, json_loads @@ -129,9 +130,6 @@ IMPORT_EVENT_LOOP_WARNING = ( "experience issues with Home Assistant" ) -_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency - - MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer") @@ -1322,7 +1320,7 @@ def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integratio Raises IntegrationNotLoaded if the integration is not loaded. """ cache = hass.data[DATA_INTEGRATIONS] - int_or_fut = cache.get(domain, _UNDEF) + int_or_fut = cache.get(domain, UNDEFINED) # Integration is never subclassed, so we can check for type if type(int_or_fut) is Integration: return int_or_fut @@ -1332,7 +1330,7 @@ def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integratio async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration: """Get integration.""" cache = hass.data[DATA_INTEGRATIONS] - if type(int_or_fut := cache.get(domain, _UNDEF)) is Integration: + if type(int_or_fut := cache.get(domain, UNDEFINED)) is Integration: return int_or_fut integrations_or_excs = await async_get_integrations(hass, [domain]) int_or_exc = integrations_or_excs[domain] @@ -1350,11 +1348,11 @@ async def async_get_integrations( needed: dict[str, asyncio.Future[None]] = {} in_progress: dict[str, asyncio.Future[None]] = {} for domain in domains: - int_or_fut = cache.get(domain, _UNDEF) + int_or_fut = cache.get(domain, UNDEFINED) # Integration is never subclassed, so we can check for type if type(int_or_fut) is Integration: results[domain] = int_or_fut - elif int_or_fut is not _UNDEF: + elif int_or_fut is not UNDEFINED: in_progress[domain] = cast(asyncio.Future[None], int_or_fut) elif "." in domain: results[domain] = ValueError(f"Invalid domain {domain}") @@ -1364,10 +1362,10 @@ async def async_get_integrations( if in_progress: await asyncio.wait(in_progress.values()) for domain in in_progress: - # When we have waited and it's _UNDEF, it doesn't exist + # When we have waited and it's UNDEFINED, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if (int_or_fut := cache.get(domain, _UNDEF)) is _UNDEF: + if (int_or_fut := cache.get(domain, UNDEFINED)) is UNDEFINED: results[domain] = IntegrationNotFound(domain) else: results[domain] = cast(Integration, int_or_fut) diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index fed48c5735b43c1735cc4fe2d34e378ed1e923e0..b48e70eff82da74ec49c8d160707901444233882 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -483,14 +483,19 @@ def test_check_if_deprecated_constant_integration_not_found( def test_test_check_if_deprecated_constant_invalid( caplog: pytest.LogCaptureFixture, ) -> None: - """Test check_if_deprecated_constant will raise an attribute error and create an log entry on an invalid deprecation type.""" + """Test check_if_deprecated_constant error handling. + + Test check_if_deprecated_constant raises an attribute error and creates a log entry + on an invalid deprecation type. + """ module_name = "homeassistant.components.hue.light" module_globals = {"__name__": module_name, "_DEPRECATED_TEST_CONSTANT": 1} name = "TEST_CONSTANT" excepted_msg = ( - f"Value of _DEPRECATED_{name} is an instance of <class 'int'> " - "but an instance of DeprecatedConstant or DeprecatedConstantEnum is required" + f"Value of _DEPRECATED_{name} is an instance of <class 'int'> but an instance " + "of DeprecatedAlias, DeferredDeprecatedAlias, DeprecatedConstant or " + "DeprecatedConstantEnum is required" ) with pytest.raises(AttributeError, match=excepted_msg):