Files
core/homeassistant/helpers/deprecation.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

397 lines
13 KiB
Python
Raw Permalink Normal View History

"""Deprecation helpers for Home Assistant."""
2021-03-17 18:34:19 +01:00
from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
2025-01-14 10:04:48 +01:00
from enum import EnumType, IntEnum, IntFlag, StrEnum, _EnumDict
import functools
import inspect
import logging
from typing import Any, NamedTuple
def deprecated_substitute[_ObjectT: object](
substitute_name: str,
) -> Callable[[Callable[[_ObjectT], Any]], Callable[[_ObjectT], Any]]:
"""Help migrate properties to new names.
When a property is added to replace an older property, this decorator can
be added to the new property, listing the old property as the substitute.
2018-02-02 23:35:34 +02:00
If the old property is defined, its value will be used instead, and a log
warning will be issued alerting the user of the impending change.
"""
2019-07-31 12:25:30 -07:00
def decorator(func: Callable[[_ObjectT], Any]) -> Callable[[_ObjectT], Any]:
"""Decorate function as deprecated."""
2019-07-31 12:25:30 -07:00
def func_wrapper(self: _ObjectT) -> Any:
"""Wrap for the original function."""
if hasattr(self, substitute_name):
# If this platform is still using the old property, issue
# a logger warning once with instructions on how to fix it.
warnings = getattr(func, "_deprecated_substitute_warnings", {})
module_name = self.__module__
if not warnings.get(module_name):
logger = logging.getLogger(module_name)
logger.warning(
(
"'%s' is deprecated. Please rename '%s' to "
"'%s' in '%s' to ensure future support."
),
substitute_name,
substitute_name,
func.__name__,
inspect.getfile(self.__class__),
)
warnings[module_name] = True
setattr(func, "_deprecated_substitute_warnings", warnings)
# Return the old property
return getattr(self, substitute_name)
return func(self)
2019-07-31 12:25:30 -07:00
return func_wrapper
2019-07-31 12:25:30 -07:00
return decorator
def get_deprecated(
2021-03-17 18:34:19 +01:00
config: dict[str, Any], new_name: str, old_name: str, default: Any | None = None
) -> Any | None:
"""Allow an old config name to be deprecated with a replacement.
If the new config isn't found, but the old one is, the old value is used
and a warning is issued to the user.
"""
if old_name in config:
module = inspect.getmodule(inspect.stack(context=0)[1].frame)
if module is not None:
module_name = module.__name__
else:
# If Python is unable to access the sources files, the call stack frame
# will be missing information, so let's guard.
# https://github.com/home-assistant/core/issues/24982
module_name = __name__
logger = logging.getLogger(module_name)
logger.warning(
(
"'%s' is deprecated. Please rename '%s' to '%s' in your "
"configuration file."
),
old_name,
old_name,
new_name,
)
return config.get(old_name)
return config.get(new_name, default)
def deprecated_class[**_P, _R](
replacement: str, *, breaks_in_ha_version: str | None = None
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
"""Mark class as deprecated and provide a replacement class to be used instead.
If the deprecated function was called from a custom integration, ask the user to
report an issue.
"""
def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]:
"""Decorate class as deprecated."""
@functools.wraps(cls)
def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Wrap for the original class."""
_print_deprecation_warning(
cls, replacement, "class", "instantiated", breaks_in_ha_version
)
return cls(*args, **kwargs)
return deprecated_cls
return deprecated_decorator
def deprecated_function[**_P, _R](
replacement: str, *, breaks_in_ha_version: str | None = None
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
"""Mark function as deprecated and provide a replacement to be used instead.
If the deprecated function was called from a custom integration, ask the user to
report an issue.
"""
def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]:
"""Decorate function as deprecated."""
@functools.wraps(func)
def deprecated_func(*args: _P.args, **kwargs: _P.kwargs) -> _R:
"""Wrap for the original function."""
_print_deprecation_warning(
func, replacement, "function", "called", breaks_in_ha_version
)
return func(*args, **kwargs)
return deprecated_func
return deprecated_decorator
def _print_deprecation_warning(
obj: Any,
replacement: str,
description: str,
verb: str,
breaks_in_ha_version: str | None,
) -> None:
_print_deprecation_warning_internal(
obj.__name__,
obj.__module__,
replacement,
description,
verb,
breaks_in_ha_version,
log_when_no_integration_is_found=True,
)
def _print_deprecation_warning_internal(
obj_name: str,
module_name: str,
replacement: str,
description: str,
verb: str,
breaks_in_ha_version: str | None,
*,
log_when_no_integration_is_found: bool,
) -> None:
# Suppress ImportError due to use of deprecated enum in core.py
# Can be removed in HA Core 2025.1
with suppress(ImportError):
_print_deprecation_warning_internal_impl(
obj_name,
module_name,
replacement,
description,
verb,
breaks_in_ha_version,
log_when_no_integration_is_found=log_when_no_integration_is_found,
)
def _print_deprecation_warning_internal_impl(
obj_name: str,
module_name: str,
replacement: str,
description: str,
verb: str,
breaks_in_ha_version: str | None,
*,
log_when_no_integration_is_found: bool,
) -> None:
2025-06-19 20:39:09 +02:00
from homeassistant.core import async_get_hass_or_none # noqa: PLC0415
from homeassistant.loader import async_suggest_report_issue # noqa: PLC0415
2025-06-19 20:39:09 +02:00
from .frame import MissingIntegrationFrame, get_integration_frame # noqa: PLC0415
logger = logging.getLogger(module_name)
if breaks_in_ha_version:
breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}"
else:
breaks_in = ""
try:
integration_frame = get_integration_frame()
except MissingIntegrationFrame:
if log_when_no_integration_is_found:
logger.warning(
"%s is a deprecated %s%s. Use %s instead",
obj_name,
description,
breaks_in,
replacement,
)
else:
if integration_frame.custom_integration:
report_issue = async_suggest_report_issue(
2024-05-26 01:38:46 -10:00
async_get_hass_or_none(),
integration_domain=integration_frame.integration,
module=integration_frame.module,
)
logger.warning(
(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead,"
" please %s"
),
obj_name,
verb,
integration_frame.integration,
description,
breaks_in,
replacement,
report_issue,
)
else:
logger.warning(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead",
obj_name,
verb,
integration_frame.integration,
description,
breaks_in,
replacement,
)
class DeprecatedConstant[T](NamedTuple):
"""Deprecated constant."""
value: T
replacement: str
breaks_in_ha_version: str | None
class DeprecatedConstantEnum[T: (StrEnum | IntEnum | IntFlag)](NamedTuple):
"""Deprecated constant."""
enum: T
breaks_in_ha_version: str | None
class DeprecatedAlias[T](NamedTuple):
"""Deprecated alias."""
value: T
replacement: str
breaks_in_ha_version: str | None
class DeferredDeprecatedAlias[T]:
"""Deprecated alias with deferred evaluation of the value."""
def __init__(
self,
value_fn: Callable[[], T],
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) -> T:
"""Return the value."""
return self._value_fn()
_PREFIX_DEPRECATED = "_DEPRECATED_"
def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> Any:
"""Check if the not found name is a deprecated constant.
If it is, print a deprecation warning and return the value of the constant.
Otherwise raise AttributeError.
"""
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):
value = deprecated_const.value
replacement = deprecated_const.replacement
breaks_in_ha_version = deprecated_const.breaks_in_ha_version
elif isinstance(deprecated_const, DeprecatedConstantEnum):
2025-01-14 10:04:48 +01:00
value = deprecated_const.enum
replacement = (
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, DeferredDeprecatedAlias)):
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 = (
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)
# PEP 562 -- Module __getattr__ and __dir__
# specifies that __getattr__ should raise AttributeError if the attribute is not
# found.
# https://peps.python.org/pep-0562/#specification
raise AttributeError(msg)
_print_deprecation_warning_internal(
name,
module_name or __name__,
replacement,
description,
"used",
breaks_in_ha_version,
log_when_no_integration_is_found=False,
)
return value
def dir_with_deprecated_constants(module_globals_keys: list[str]) -> list[str]:
"""Return dir() with deprecated constants."""
return module_globals_keys + [
name.removeprefix(_PREFIX_DEPRECATED)
for name in module_globals_keys
if name.startswith(_PREFIX_DEPRECATED)
]
def all_with_deprecated_constants(module_globals: dict[str, Any]) -> list[str]:
"""Generate a list for __all___ with deprecated constants."""
# Iterate over a copy in case the globals dict is mutated by another thread
# while we loop over it.
module_globals_keys = list(module_globals)
return [itm for itm in module_globals_keys if not itm.startswith("_")] + [
name.removeprefix(_PREFIX_DEPRECATED)
for name in module_globals_keys
if name.startswith(_PREFIX_DEPRECATED)
]
class EnumWithDeprecatedMembers(EnumType):
"""Enum with deprecated members."""
def __new__(
mcs,
cls: str,
bases: tuple[type, ...],
classdict: _EnumDict,
*,
deprecated: dict[str, tuple[str, str]],
**kwds: Any,
) -> Any:
"""Create a new class."""
classdict["__deprecated__"] = deprecated
return super().__new__(mcs, cls, bases, classdict, **kwds)
def __getattribute__(cls, name: str) -> Any:
"""Warn if accessing a deprecated member."""
deprecated = super().__getattribute__("__deprecated__")
if name in deprecated:
_print_deprecation_warning_internal(
f"{cls.__name__}.{name}",
cls.__module__,
f"{deprecated[name][0]}",
"enum member",
"used",
deprecated[name][1],
log_when_no_integration_is_found=False,
)
return super().__getattribute__(name)