diff --git a/homeassistant/backports/enum.py b/homeassistant/backports/enum.py index 3c09d8e7f57..8b823f47e22 100644 --- a/homeassistant/backports/enum.py +++ b/homeassistant/backports/enum.py @@ -9,8 +9,21 @@ import it. from __future__ import annotations -from enum import StrEnum +from enum import StrEnum as _StrEnum +from functools import partial -__all__ = [ - "StrEnum", -] +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + +# StrEnum deprecated as of 2024.5 use enum.StrEnum instead. +_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5") + +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/backports/functools.py b/homeassistant/backports/functools.py index 96c9888bd80..bad4236f9c8 100644 --- a/homeassistant/backports/functools.py +++ b/homeassistant/backports/functools.py @@ -9,8 +9,22 @@ import it. from __future__ import annotations -from functools import cached_property +from functools import cached_property as _cached_property, partial -__all__ = [ - "cached_property", -] +from homeassistant.helpers.deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + +# cached_property deprecated as of 2024.5 use functools.cached_property instead. +_DEPRECATED_cached_property = DeprecatedAlias( + _cached_property, "functools.cached_property", "2025.5" +) + +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index fec87262374..6c7a11cf854 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -7,12 +7,12 @@ import dataclasses from dataclasses import dataclass, field from typing import Literal, TypedDict, cast -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.util.ulid import ulid_now from .registry import BaseRegistry from .storage import Store -from .typing import UNDEFINED, EventType, UndefinedType +from .typing import UNDEFINED, UndefinedType DATA_REGISTRY = "category_registry" EVENT_CATEGORY_REGISTRY_UPDATED = "category_registry_updated" @@ -28,7 +28,7 @@ class EventCategoryRegistryUpdatedData(TypedDict): category_id: str -EventCategoryRegistryUpdated = EventType[EventCategoryRegistryUpdatedData] +EventCategoryRegistryUpdated = Event[EventCategoryRegistryUpdatedData] @dataclass(slots=True, kw_only=True, frozen=True) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 6e70bbc7635..93520866142 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -243,6 +243,14 @@ class DeprecatedConstantEnum(NamedTuple): breaks_in_ha_version: str | None +class DeprecatedAlias(NamedTuple): + """Deprecated alias.""" + + value: Any + replacement: str + breaks_in_ha_version: str | None + + _PREFIX_DEPRECATED = "_DEPRECATED_" @@ -254,6 +262,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A """ module_name = module_globals.get("__name__") value = replacement = None + description = "constant" if (deprecated_const := module_globals.get(_PREFIX_DEPRECATED + name)) is None: raise AttributeError(f"Module {module_name!r} has no attribute {name!r}") if isinstance(deprecated_const, DeprecatedConstant): @@ -266,6 +275,11 @@ 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): + description = "alias" + value = deprecated_const.value + replacement = deprecated_const.replacement + breaks_in_ha_version = deprecated_const.breaks_in_ha_version if value is None or replacement is None: msg = ( @@ -284,7 +298,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A name, module_name or __name__, replacement, - "constant", + description, "used", breaks_in_ha_version, log_when_no_integration_is_found=False, diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0f372689809..8b1b4addcdb 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -2,15 +2,22 @@ from collections.abc import Mapping from enum import Enum +from functools import partial from typing import Any, TypeVar import homeassistant.core +from .deprecation import ( + DeprecatedAlias, + all_with_deprecated_constants, + check_if_deprecated_constant, + dir_with_deprecated_constants, +) + _DataT = TypeVar("_DataT") GPSType = tuple[float, float] ConfigType = dict[str, Any] -ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] ServiceDataType = dict[str, Any] StateType = str | int | float | None @@ -33,7 +40,23 @@ UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access # are not present in the core code base. # They are kept in order not to break custom integrations # that may rely on them. -# In due time they will be removed. -EventType = homeassistant.core.Event -HomeAssistantType = homeassistant.core.HomeAssistant -ServiceCallType = homeassistant.core.ServiceCall +# 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" +) + +# These can be removed if no deprecated constant are in this module anymore +__getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial( + dir_with_deprecated_constants, module_globals_keys=[*globals().keys()] +) +__all__ = all_with_deprecated_constants(globals()) diff --git a/tests/common.py b/tests/common.py index 59b93fc7288..3472da6d1ef 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1642,6 +1642,40 @@ def import_and_test_deprecated_constant( assert constant_name in module.__all__ +def import_and_test_deprecated_alias( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Import and test deprecated alias replaced by a value. + + - Import deprecated alias + - Assert value is the same as the replacement + - Assert a warning is logged + - Assert the deprecated alias is included in the modules.__dir__() + - Assert the deprecated alias is included in the modules.__all__() + """ + replacement_name = f"{replacement.__module__}.{replacement.__name__}" + value = import_deprecated_constant(module, alias_name) + assert value == replacement + assert ( + module.__name__, + logging.WARNING, + ( + f"{alias_name} was used from test_constant_deprecation," + f" this is a deprecated alias which will be removed in HA Core {breaks_in_ha_version}. " + f"Use {replacement_name} instead, please report " + "it to the author of the 'test_constant_deprecation' custom integration" + ), + ) in caplog.record_tuples + + # verify deprecated alias is included in dir() + assert alias_name in dir(module) + assert alias_name in module.__all__ + + def help_test_all(module: ModuleType) -> None: """Test module.__all__ is correctly set.""" assert set(module.__all__) == { diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index b53a6d5ec1d..fed48c5735b 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -10,6 +10,7 @@ import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers.deprecation import ( + DeprecatedAlias, DeprecatedConstant, DeprecatedConstantEnum, check_if_deprecated_constant, @@ -283,38 +284,59 @@ class TestDeprecatedConstantEnum(StrEnum): TEST = "value" -def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: - if isinstance(obj, tuple): - if len(obj) == 2: - return obj[0].value - - return obj[0] - +def _get_value( + obj: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple[Any, ...], +) -> Any: if isinstance(obj, DeprecatedConstant): return obj.value if isinstance(obj, DeprecatedConstantEnum): return obj.enum.value + if isinstance(obj, DeprecatedAlias): + return obj.value + + if len(obj) == 2: + return obj[0].value + + return obj[0] + @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -330,10 +352,14 @@ def _get_value(obj: DeprecatedConstant | DeprecatedConstantEnum | tuple) -> Any: ) def test_check_if_deprecated_constant( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, extra_extra_msg: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -378,28 +404,42 @@ def test_check_if_deprecated_constant( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT was used from hue, this is a deprecated constant{extra_msg}{extra_extra_msg}", + f"TEST_CONSTANT was used from hue, this is a deprecated {description}{extra_msg}{extra_extra_msg}", ) in caplog.record_tuples @pytest.mark.parametrize( - ("deprecated_constant", "extra_msg"), + ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", + "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", + "constant", + ), + ( + DeprecatedAlias(1, "new_alias", None), + ". Use new_alias instead", + "alias", + ), + ( + DeprecatedAlias(1, "new_alias", "2099.1"), + " which will be removed in HA Core 2099.1. Use new_alias instead", + "alias", ), ], ) @@ -412,9 +452,13 @@ def test_check_if_deprecated_constant( ) def test_check_if_deprecated_constant_integration_not_found( caplog: pytest.LogCaptureFixture, - deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | tuple, + deprecated_constant: DeprecatedConstant + | DeprecatedConstantEnum + | DeprecatedAlias + | tuple, extra_msg: str, module_name: str, + description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { @@ -432,7 +476,7 @@ def test_check_if_deprecated_constant_integration_not_found( assert ( module_name, logging.WARNING, - f"TEST_CONSTANT is a deprecated constant{extra_msg}", + f"TEST_CONSTANT is a deprecated {description}{extra_msg}", ) not in caplog.record_tuples diff --git a/tests/helpers/test_typing.py b/tests/helpers/test_typing.py new file mode 100644 index 00000000000..5b50a8864de --- /dev/null +++ b/tests/helpers/test_typing.py @@ -0,0 +1,37 @@ +"""Test typing helper module.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from homeassistant.core import Context, Event, HomeAssistant, ServiceCall +from homeassistant.helpers import typing as ha_typing + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("alias_name", "replacement", "breaks_in_ha_version"), + [ + ("ContextType", Context, "2025.5"), + ("EventType", Event, "2025.5"), + ("HomeAssistantType", HomeAssistant, "2025.5"), + ("ServiceCallType", ServiceCall, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + alias_name: str, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + import_and_test_deprecated_alias( + caplog, + ha_typing, + alias_name, + replacement, + breaks_in_ha_version, + ) diff --git a/tests/test_backports.py b/tests/test_backports.py new file mode 100644 index 00000000000..09c11da37cb --- /dev/null +++ b/tests/test_backports.py @@ -0,0 +1,41 @@ +"""Test backports package.""" + +from __future__ import annotations + +from enum import StrEnum +from functools import cached_property +from types import ModuleType +from typing import Any + +import pytest + +from homeassistant.backports import ( + enum as backports_enum, + functools as backports_functools, +) + +from tests.common import import_and_test_deprecated_alias + + +@pytest.mark.parametrize( + ("module", "replacement", "breaks_in_ha_version"), + [ + (backports_enum, StrEnum, "2025.5"), + (backports_functools, cached_property, "2025.5"), + ], +) +def test_deprecated_aliases( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + replacement: Any, + breaks_in_ha_version: str, +) -> None: + """Test deprecated aliases.""" + alias_name = replacement.__name__ + import_and_test_deprecated_alias( + caplog, + module, + alias_name, + replacement, + breaks_in_ha_version, + )