Allow importing typing helper in core files (#119377)
* Allow importing typing helper in core files * Really fix the circular import * Update test
This commit is contained in:
parent
572700a326
commit
904b89df80
5 changed files with 69 additions and 44 deletions
|
@ -96,6 +96,7 @@ from .helpers.deprecation import (
|
||||||
dir_with_deprecated_constants,
|
dir_with_deprecated_constants,
|
||||||
)
|
)
|
||||||
from .helpers.json import json_bytes, json_fragment
|
from .helpers.json import json_bytes, json_fragment
|
||||||
|
from .helpers.typing import UNDEFINED, UndefinedType
|
||||||
from .util import dt as dt_util, location
|
from .util import dt as dt_util, location
|
||||||
from .util.async_ import (
|
from .util.async_ import (
|
||||||
cancelling,
|
cancelling,
|
||||||
|
@ -131,8 +132,6 @@ FINAL_WRITE_STAGE_SHUTDOWN_TIMEOUT = 60
|
||||||
CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30
|
CLOSE_STAGE_SHUTDOWN_TIMEOUT = 30
|
||||||
|
|
||||||
|
|
||||||
# Internal; not helpers.typing.UNDEFINED due to circular dependency
|
|
||||||
_UNDEF: dict[Any, Any] = {}
|
|
||||||
_SENTINEL = object()
|
_SENTINEL = object()
|
||||||
_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any])
|
_DataT = TypeVar("_DataT", bound=Mapping[str, Any], default=Mapping[str, Any])
|
||||||
type CALLBACK_TYPE = Callable[[], None]
|
type CALLBACK_TYPE = Callable[[], None]
|
||||||
|
@ -3035,11 +3034,10 @@ class Config:
|
||||||
unit_system: str | None = None,
|
unit_system: str | None = None,
|
||||||
location_name: str | None = None,
|
location_name: str | None = None,
|
||||||
time_zone: str | None = None,
|
time_zone: str | None = None,
|
||||||
# pylint: disable=dangerous-default-value # _UNDEFs not modified
|
external_url: str | UndefinedType | None = UNDEFINED,
|
||||||
external_url: str | dict[Any, Any] | None = _UNDEF,
|
internal_url: str | UndefinedType | None = UNDEFINED,
|
||||||
internal_url: str | dict[Any, Any] | None = _UNDEF,
|
|
||||||
currency: str | None = None,
|
currency: str | None = None,
|
||||||
country: str | dict[Any, Any] | None = _UNDEF,
|
country: str | UndefinedType | None = UNDEFINED,
|
||||||
language: str | None = None,
|
language: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the configuration from a dictionary."""
|
"""Update the configuration from a dictionary."""
|
||||||
|
@ -3059,14 +3057,14 @@ class Config:
|
||||||
self.location_name = location_name
|
self.location_name = location_name
|
||||||
if time_zone is not None:
|
if time_zone is not None:
|
||||||
await self.async_set_time_zone(time_zone)
|
await self.async_set_time_zone(time_zone)
|
||||||
if external_url is not _UNDEF:
|
if external_url is not UNDEFINED:
|
||||||
self.external_url = cast(str | None, external_url)
|
self.external_url = external_url
|
||||||
if internal_url is not _UNDEF:
|
if internal_url is not UNDEFINED:
|
||||||
self.internal_url = cast(str | None, internal_url)
|
self.internal_url = internal_url
|
||||||
if currency is not None:
|
if currency is not None:
|
||||||
self.currency = currency
|
self.currency = currency
|
||||||
if country is not _UNDEF:
|
if country is not UNDEFINED:
|
||||||
self.country = cast(str | None, country)
|
self.country = country
|
||||||
if language is not None:
|
if language is not None:
|
||||||
self.language = language
|
self.language = language
|
||||||
|
|
||||||
|
@ -3112,8 +3110,8 @@ class Config:
|
||||||
unit_system=data.get("unit_system_v2"),
|
unit_system=data.get("unit_system_v2"),
|
||||||
location_name=data.get("location_name"),
|
location_name=data.get("location_name"),
|
||||||
time_zone=data.get("time_zone"),
|
time_zone=data.get("time_zone"),
|
||||||
external_url=data.get("external_url", _UNDEF),
|
external_url=data.get("external_url", UNDEFINED),
|
||||||
internal_url=data.get("internal_url", _UNDEF),
|
internal_url=data.get("internal_url", UNDEFINED),
|
||||||
currency=data.get("currency"),
|
currency=data.get("currency"),
|
||||||
country=data.get("country"),
|
country=data.get("country"),
|
||||||
language=data.get("language"),
|
language=data.get("language"),
|
||||||
|
|
|
@ -242,6 +242,26 @@ class DeprecatedAlias(NamedTuple):
|
||||||
breaks_in_ha_version: str | None
|
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_"
|
_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}"
|
f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}"
|
||||||
)
|
)
|
||||||
breaks_in_ha_version = deprecated_const.breaks_in_ha_version
|
breaks_in_ha_version = deprecated_const.breaks_in_ha_version
|
||||||
elif isinstance(deprecated_const, DeprecatedAlias):
|
elif isinstance(deprecated_const, (DeprecatedAlias, DeferredDeprecatedAlias)):
|
||||||
description = "alias"
|
description = "alias"
|
||||||
value = deprecated_const.value
|
value = deprecated_const.value
|
||||||
replacement = deprecated_const.replacement
|
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:
|
if value is None or replacement is None:
|
||||||
msg = (
|
msg = (
|
||||||
f"Value of {_PREFIX_DEPRECATED}{name} is an instance of {type(deprecated_const)} "
|
f"Value of {_PREFIX_DEPRECATED}{name} is an instance of "
|
||||||
"but an instance of DeprecatedConstant or DeprecatedConstantEnum is required"
|
f"{type(deprecated_const)} but an instance of DeprecatedAlias, "
|
||||||
|
"DeferredDeprecatedAlias, DeprecatedConstant or DeprecatedConstantEnum "
|
||||||
|
"is required"
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.getLogger(module_name).debug(msg)
|
logging.getLogger(module_name).debug(msg)
|
||||||
|
|
|
@ -5,10 +5,8 @@ from enum import Enum
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Any, Never
|
from typing import Any, Never
|
||||||
|
|
||||||
import homeassistant.core
|
|
||||||
|
|
||||||
from .deprecation import (
|
from .deprecation import (
|
||||||
DeprecatedAlias,
|
DeferredDeprecatedAlias,
|
||||||
all_with_deprecated_constants,
|
all_with_deprecated_constants,
|
||||||
check_if_deprecated_constant,
|
check_if_deprecated_constant,
|
||||||
dir_with_deprecated_constants,
|
dir_with_deprecated_constants,
|
||||||
|
@ -35,23 +33,27 @@ class UndefinedType(Enum):
|
||||||
UNDEFINED = UndefinedType._singleton # noqa: SLF001
|
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
|
# The following types should not used and
|
||||||
# are not present in the core code base.
|
# are not present in the core code base.
|
||||||
# They are kept in order not to break custom integrations
|
# They are kept in order not to break custom integrations
|
||||||
# that may rely on them.
|
# that may rely on them.
|
||||||
# Deprecated as of 2024.5 use types from homeassistant.core instead.
|
# Deprecated as of 2024.5 use types from homeassistant.core instead.
|
||||||
_DEPRECATED_ContextType = DeprecatedAlias(
|
_DEPRECATED_ContextType = _deprecated_typing_helper("Context")
|
||||||
homeassistant.core.Context, "homeassistant.core.Context", "2025.5"
|
_DEPRECATED_EventType = _deprecated_typing_helper("Event")
|
||||||
)
|
_DEPRECATED_HomeAssistantType = _deprecated_typing_helper("HomeAssistant")
|
||||||
_DEPRECATED_EventType = DeprecatedAlias(
|
_DEPRECATED_ServiceCallType = _deprecated_typing_helper("ServiceCall")
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# These can be removed if no deprecated constant are in this module anymore
|
# These can be removed if no deprecated constant are in this module anymore
|
||||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||||
|
|
|
@ -40,6 +40,7 @@ from .generated.ssdp import SSDP
|
||||||
from .generated.usb import USB
|
from .generated.usb import USB
|
||||||
from .generated.zeroconf import HOMEKIT, ZEROCONF
|
from .generated.zeroconf import HOMEKIT, ZEROCONF
|
||||||
from .helpers.json import json_bytes, json_fragment
|
from .helpers.json import json_bytes, json_fragment
|
||||||
|
from .helpers.typing import UNDEFINED
|
||||||
from .util.hass_dict import HassKey
|
from .util.hass_dict import HassKey
|
||||||
from .util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
from .util.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||||
|
|
||||||
|
@ -129,9 +130,6 @@ IMPORT_EVENT_LOOP_WARNING = (
|
||||||
"experience issues with Home Assistant"
|
"experience issues with Home Assistant"
|
||||||
)
|
)
|
||||||
|
|
||||||
_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency
|
|
||||||
|
|
||||||
|
|
||||||
MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer")
|
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.
|
Raises IntegrationNotLoaded if the integration is not loaded.
|
||||||
"""
|
"""
|
||||||
cache = hass.data[DATA_INTEGRATIONS]
|
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
|
# Integration is never subclassed, so we can check for type
|
||||||
if type(int_or_fut) is Integration:
|
if type(int_or_fut) is Integration:
|
||||||
return int_or_fut
|
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:
|
async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration:
|
||||||
"""Get integration."""
|
"""Get integration."""
|
||||||
cache = hass.data[DATA_INTEGRATIONS]
|
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
|
return int_or_fut
|
||||||
integrations_or_excs = await async_get_integrations(hass, [domain])
|
integrations_or_excs = await async_get_integrations(hass, [domain])
|
||||||
int_or_exc = integrations_or_excs[domain]
|
int_or_exc = integrations_or_excs[domain]
|
||||||
|
@ -1350,11 +1348,11 @@ async def async_get_integrations(
|
||||||
needed: dict[str, asyncio.Future[None]] = {}
|
needed: dict[str, asyncio.Future[None]] = {}
|
||||||
in_progress: dict[str, asyncio.Future[None]] = {}
|
in_progress: dict[str, asyncio.Future[None]] = {}
|
||||||
for domain in domains:
|
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
|
# Integration is never subclassed, so we can check for type
|
||||||
if type(int_or_fut) is Integration:
|
if type(int_or_fut) is Integration:
|
||||||
results[domain] = int_or_fut
|
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)
|
in_progress[domain] = cast(asyncio.Future[None], int_or_fut)
|
||||||
elif "." in domain:
|
elif "." in domain:
|
||||||
results[domain] = ValueError(f"Invalid domain {domain}")
|
results[domain] = ValueError(f"Invalid domain {domain}")
|
||||||
|
@ -1364,10 +1362,10 @@ async def async_get_integrations(
|
||||||
if in_progress:
|
if in_progress:
|
||||||
await asyncio.wait(in_progress.values())
|
await asyncio.wait(in_progress.values())
|
||||||
for domain in in_progress:
|
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
|
# 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.
|
# 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)
|
results[domain] = IntegrationNotFound(domain)
|
||||||
else:
|
else:
|
||||||
results[domain] = cast(Integration, int_or_fut)
|
results[domain] = cast(Integration, int_or_fut)
|
||||||
|
|
|
@ -483,14 +483,19 @@ def test_check_if_deprecated_constant_integration_not_found(
|
||||||
def test_test_check_if_deprecated_constant_invalid(
|
def test_test_check_if_deprecated_constant_invalid(
|
||||||
caplog: pytest.LogCaptureFixture,
|
caplog: pytest.LogCaptureFixture,
|
||||||
) -> None:
|
) -> 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_name = "homeassistant.components.hue.light"
|
||||||
module_globals = {"__name__": module_name, "_DEPRECATED_TEST_CONSTANT": 1}
|
module_globals = {"__name__": module_name, "_DEPRECATED_TEST_CONSTANT": 1}
|
||||||
name = "TEST_CONSTANT"
|
name = "TEST_CONSTANT"
|
||||||
|
|
||||||
excepted_msg = (
|
excepted_msg = (
|
||||||
f"Value of _DEPRECATED_{name} is an instance of <class 'int'> "
|
f"Value of _DEPRECATED_{name} is an instance of <class 'int'> but an instance "
|
||||||
"but an instance of DeprecatedConstant or DeprecatedConstantEnum is required"
|
"of DeprecatedAlias, DeferredDeprecatedAlias, DeprecatedConstant or "
|
||||||
|
"DeprecatedConstantEnum is required"
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(AttributeError, match=excepted_msg):
|
with pytest.raises(AttributeError, match=excepted_msg):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue