Home Assistant Unofficial Reference 2024.12.1
deprecation.py
Go to the documentation of this file.
1 """Deprecation helpers for Home Assistant."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from contextlib import suppress
7 from enum import Enum, EnumType, _EnumDict
8 import functools
9 import inspect
10 import logging
11 from typing import Any, NamedTuple
12 
13 
14 def deprecated_substitute[_ObjectT: object](
15  substitute_name: str,
16 ) -> Callable[[Callable[[_ObjectT], Any]], Callable[[_ObjectT], Any]]:
17  """Help migrate properties to new names.
18 
19  When a property is added to replace an older property, this decorator can
20  be added to the new property, listing the old property as the substitute.
21  If the old property is defined, its value will be used instead, and a log
22  warning will be issued alerting the user of the impending change.
23  """
24 
25  def decorator(func: Callable[[_ObjectT], Any]) -> Callable[[_ObjectT], Any]:
26  """Decorate function as deprecated."""
27 
28  def func_wrapper(self: _ObjectT) -> Any:
29  """Wrap for the original function."""
30  if hasattr(self, substitute_name):
31  # If this platform is still using the old property, issue
32  # a logger warning once with instructions on how to fix it.
33  warnings = getattr(func, "_deprecated_substitute_warnings", {})
34  module_name = self.__module__
35  if not warnings.get(module_name):
36  logger = logging.getLogger(module_name)
37  logger.warning(
38  (
39  "'%s' is deprecated. Please rename '%s' to "
40  "'%s' in '%s' to ensure future support."
41  ),
42  substitute_name,
43  substitute_name,
44  func.__name__,
45  inspect.getfile(self.__class__),
46  )
47  warnings[module_name] = True
48  setattr(func, "_deprecated_substitute_warnings", warnings)
49 
50  # Return the old property
51  return getattr(self, substitute_name)
52  return func(self)
53 
54  return func_wrapper
55 
56  return decorator
57 
58 
60  config: dict[str, Any], new_name: str, old_name: str, default: Any | None = None
61 ) -> Any | None:
62  """Allow an old config name to be deprecated with a replacement.
63 
64  If the new config isn't found, but the old one is, the old value is used
65  and a warning is issued to the user.
66  """
67  if old_name in config:
68  module = inspect.getmodule(inspect.stack(context=0)[1].frame)
69  if module is not None:
70  module_name = module.__name__
71  else:
72  # If Python is unable to access the sources files, the call stack frame
73  # will be missing information, so let's guard.
74  # https://github.com/home-assistant/core/issues/24982
75  module_name = __name__
76 
77  logger = logging.getLogger(module_name)
78  logger.warning(
79  (
80  "'%s' is deprecated. Please rename '%s' to '%s' in your "
81  "configuration file."
82  ),
83  old_name,
84  old_name,
85  new_name,
86  )
87  return config.get(old_name)
88  return config.get(new_name, default)
89 
90 
91 def deprecated_class[**_P, _R](
92  replacement: str, *, breaks_in_ha_version: str | None = None
93 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
94  """Mark class as deprecated and provide a replacement class to be used instead.
95 
96  If the deprecated function was called from a custom integration, ask the user to
97  report an issue.
98  """
99 
100  def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]:
101  """Decorate class as deprecated."""
102 
103  @functools.wraps(cls)
104  def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R:
105  """Wrap for the original class."""
107  cls, replacement, "class", "instantiated", breaks_in_ha_version
108  )
109  return cls(*args, **kwargs)
110 
111  return deprecated_cls
112 
113  return deprecated_decorator
114 
115 
116 def deprecated_function[**_P, _R](
117  replacement: str, *, breaks_in_ha_version: str | None = None
118 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
119  """Mark function as deprecated and provide a replacement to be used instead.
120 
121  If the deprecated function was called from a custom integration, ask the user to
122  report an issue.
123  """
124 
125  def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]:
126  """Decorate function as deprecated."""
127 
128  @functools.wraps(func)
129  def deprecated_func(*args: _P.args, **kwargs: _P.kwargs) -> _R:
130  """Wrap for the original function."""
132  func, replacement, "function", "called", breaks_in_ha_version
133  )
134  return func(*args, **kwargs)
135 
136  return deprecated_func
137 
138  return deprecated_decorator
139 
140 
142  obj: Any,
143  replacement: str,
144  description: str,
145  verb: str,
146  breaks_in_ha_version: str | None,
147 ) -> None:
149  obj.__name__,
150  obj.__module__,
151  replacement,
152  description,
153  verb,
154  breaks_in_ha_version,
155  log_when_no_integration_is_found=True,
156  )
157 
158 
160  obj_name: str,
161  module_name: str,
162  replacement: str,
163  description: str,
164  verb: str,
165  breaks_in_ha_version: str | None,
166  *,
167  log_when_no_integration_is_found: bool,
168 ) -> None:
169  # Suppress ImportError due to use of deprecated enum in core.py
170  # Can be removed in HA Core 2025.1
171  with suppress(ImportError):
173  obj_name,
174  module_name,
175  replacement,
176  description,
177  verb,
178  breaks_in_ha_version,
179  log_when_no_integration_is_found=log_when_no_integration_is_found,
180  )
181 
182 
184  obj_name: str,
185  module_name: str,
186  replacement: str,
187  description: str,
188  verb: str,
189  breaks_in_ha_version: str | None,
190  *,
191  log_when_no_integration_is_found: bool,
192 ) -> None:
193  # pylint: disable=import-outside-toplevel
194  from homeassistant.core import async_get_hass_or_none
195  from homeassistant.loader import async_suggest_report_issue
196 
197  from .frame import MissingIntegrationFrame, get_integration_frame
198 
199  logger = logging.getLogger(module_name)
200  if breaks_in_ha_version:
201  breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}"
202  else:
203  breaks_in = ""
204  try:
205  integration_frame = get_integration_frame()
206  except MissingIntegrationFrame:
207  if log_when_no_integration_is_found:
208  logger.warning(
209  "%s is a deprecated %s%s. Use %s instead",
210  obj_name,
211  description,
212  breaks_in,
213  replacement,
214  )
215  else:
216  if integration_frame.custom_integration:
217  report_issue = async_suggest_report_issue(
219  integration_domain=integration_frame.integration,
220  module=integration_frame.module,
221  )
222  logger.warning(
223  (
224  "%s was %s from %s, this is a deprecated %s%s. Use %s instead,"
225  " please %s"
226  ),
227  obj_name,
228  verb,
229  integration_frame.integration,
230  description,
231  breaks_in,
232  replacement,
233  report_issue,
234  )
235  else:
236  logger.warning(
237  "%s was %s from %s, this is a deprecated %s%s. Use %s instead",
238  obj_name,
239  verb,
240  integration_frame.integration,
241  description,
242  breaks_in,
243  replacement,
244  )
245 
246 
247 class DeprecatedConstant(NamedTuple):
248  """Deprecated constant."""
249 
250  value: Any
251  replacement: str
252  breaks_in_ha_version: str | None
253 
254 
255 class DeprecatedConstantEnum(NamedTuple):
256  """Deprecated constant."""
257 
258  enum: Enum
259  breaks_in_ha_version: str | None
260 
261 
262 class DeprecatedAlias(NamedTuple):
263  """Deprecated alias."""
264 
265  value: Any
266  replacement: str
267  breaks_in_ha_version: str | None
268 
269 
271  """Deprecated alias with deferred evaluation of the value."""
272 
273  def __init__(
274  self,
275  value_fn: Callable[[], Any],
276  replacement: str,
277  breaks_in_ha_version: str | None,
278  ) -> None:
279  """Initialize."""
280  self.breaks_in_ha_versionbreaks_in_ha_version = breaks_in_ha_version
281  self.replacementreplacement = replacement
282  self._value_fn_value_fn = value_fn
283 
284  @functools.cached_property
285  def value(self) -> Any:
286  """Return the value."""
287  return self._value_fn_value_fn()
288 
289 
290 _PREFIX_DEPRECATED = "_DEPRECATED_"
291 
292 
293 def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> Any:
294  """Check if the not found name is a deprecated constant.
295 
296  If it is, print a deprecation warning and return the value of the constant.
297  Otherwise raise AttributeError.
298  """
299  module_name = module_globals.get("__name__")
300  value = replacement = None
301  description = "constant"
302  if (deprecated_const := module_globals.get(_PREFIX_DEPRECATED + name)) is None:
303  raise AttributeError(f"Module {module_name!r} has no attribute {name!r}")
304  if isinstance(deprecated_const, DeprecatedConstant):
305  value = deprecated_const.value
306  replacement = deprecated_const.replacement
307  breaks_in_ha_version = deprecated_const.breaks_in_ha_version
308  elif isinstance(deprecated_const, DeprecatedConstantEnum):
309  value = deprecated_const.enum.value
310  replacement = (
311  f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}"
312  )
313  breaks_in_ha_version = deprecated_const.breaks_in_ha_version
314  elif isinstance(deprecated_const, (DeprecatedAlias, DeferredDeprecatedAlias)):
315  description = "alias"
316  value = deprecated_const.value
317  replacement = deprecated_const.replacement
318  breaks_in_ha_version = deprecated_const.breaks_in_ha_version
319 
320  if value is None or replacement is None:
321  msg = (
322  f"Value of {_PREFIX_DEPRECATED}{name} is an instance of "
323  f"{type(deprecated_const)} but an instance of DeprecatedAlias, "
324  "DeferredDeprecatedAlias, DeprecatedConstant or DeprecatedConstantEnum "
325  "is required"
326  )
327 
328  logging.getLogger(module_name).debug(msg)
329  # PEP 562 -- Module __getattr__ and __dir__
330  # specifies that __getattr__ should raise AttributeError if the attribute is not
331  # found.
332  # https://peps.python.org/pep-0562/#specification
333  raise AttributeError(msg)
334 
336  name,
337  module_name or __name__,
338  replacement,
339  description,
340  "used",
341  breaks_in_ha_version,
342  log_when_no_integration_is_found=False,
343  )
344  return value
345 
346 
347 def dir_with_deprecated_constants(module_globals_keys: list[str]) -> list[str]:
348  """Return dir() with deprecated constants."""
349  return module_globals_keys + [
350  name.removeprefix(_PREFIX_DEPRECATED)
351  for name in module_globals_keys
352  if name.startswith(_PREFIX_DEPRECATED)
353  ]
354 
355 
356 def all_with_deprecated_constants(module_globals: dict[str, Any]) -> list[str]:
357  """Generate a list for __all___ with deprecated constants."""
358  # Iterate over a copy in case the globals dict is mutated by another thread
359  # while we loop over it.
360  module_globals_keys = list(module_globals)
361  return [itm for itm in module_globals_keys if not itm.startswith("_")] + [
362  name.removeprefix(_PREFIX_DEPRECATED)
363  for name in module_globals_keys
364  if name.startswith(_PREFIX_DEPRECATED)
365  ]
366 
367 
369  """Enum with deprecated members."""
370 
371  def __new__(
372  mcs, # noqa: N804 ruff bug, ruff does not understand this is a metaclass
373  cls: str,
374  bases: tuple[type, ...],
375  classdict: _EnumDict,
376  *,
377  deprecated: dict[str, tuple[str, str]],
378  **kwds: Any,
379  ) -> Any:
380  """Create a new class."""
381  classdict["__deprecated__"] = deprecated
382  return super().__new__(mcs, cls, bases, classdict, **kwds)
383 
384  def __getattribute__(cls, name: str) -> Any:
385  """Warn if accessing a deprecated member."""
386  deprecated = super().__getattribute__("__deprecated__")
387  if name in deprecated:
389  f"{cls.__name__}.{name}",
390  cls.__module__,
391  f"{deprecated[name][0]}",
392  "enum member",
393  "used",
394  deprecated[name][1],
395  log_when_no_integration_is_found=False,
396  )
397  return super().__getattribute__(name)
None __init__(self, Callable[[], Any] value_fn, str replacement, str|None breaks_in_ha_version)
Definition: deprecation.py:278
Any __new__(mcs, str cls, tuple[type,...] bases, _EnumDict classdict, *dict[str, tuple[str, str]] deprecated, **Any kwds)
Definition: deprecation.py:379
HomeAssistant|None async_get_hass_or_none()
Definition: core.py:299
None _print_deprecation_warning(Any obj, str replacement, str description, str verb, str|None breaks_in_ha_version)
Definition: deprecation.py:147
None _print_deprecation_warning_internal(str obj_name, str module_name, str replacement, str description, str verb, str|None breaks_in_ha_version, *bool log_when_no_integration_is_found)
Definition: deprecation.py:168
Any|None get_deprecated(dict[str, Any] config, str new_name, str old_name, Any|None default=None)
Definition: deprecation.py:61
list[str] dir_with_deprecated_constants(list[str] module_globals_keys)
Definition: deprecation.py:347
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356
Any check_if_deprecated_constant(str name, dict[str, Any] module_globals)
Definition: deprecation.py:293
None _print_deprecation_warning_internal_impl(str obj_name, str module_name, str replacement, str description, str verb, str|None breaks_in_ha_version, *bool log_when_no_integration_is_found)
Definition: deprecation.py:192
IntegrationFrame get_integration_frame(set|None exclude_integrations=None)
Definition: frame.py:84
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752