2020-05-13 07:58:33 +00:00
|
|
|
"""Provide frame helper for finding the current frame context."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
import asyncio
|
2021-09-29 14:32:11 +00:00
|
|
|
from collections.abc import Callable
|
2020-07-06 22:58:53 +00:00
|
|
|
import functools
|
|
|
|
import logging
|
2020-05-13 07:58:33 +00:00
|
|
|
from traceback import FrameSummary, extract_stack
|
2021-09-29 14:32:11 +00:00
|
|
|
from typing import Any, TypeVar, cast
|
2020-05-13 07:58:33 +00:00
|
|
|
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2021-12-06 23:26:31 +00:00
|
|
|
# Keep track of integrations already reported to prevent flooding
|
|
|
|
_REPORTED_INTEGRATIONS: set[str] = set()
|
|
|
|
|
2022-03-17 18:09:55 +00:00
|
|
|
_CallableT = TypeVar("_CallableT", bound=Callable)
|
2020-07-06 22:58:53 +00:00
|
|
|
|
2020-05-13 07:58:33 +00:00
|
|
|
|
2020-08-12 14:08:33 +00:00
|
|
|
def get_integration_frame(
|
2021-03-17 17:34:19 +00:00
|
|
|
exclude_integrations: set | None = None,
|
|
|
|
) -> tuple[FrameSummary, str, str]:
|
2020-05-13 07:58:33 +00:00
|
|
|
"""Return the frame, integration and integration path of the current stack frame."""
|
|
|
|
found_frame = None
|
2020-08-12 14:08:33 +00:00
|
|
|
if not exclude_integrations:
|
|
|
|
exclude_integrations = set()
|
2020-05-13 07:58:33 +00:00
|
|
|
|
|
|
|
for frame in reversed(extract_stack()):
|
|
|
|
for path in ("custom_components/", "homeassistant/components/"):
|
|
|
|
try:
|
|
|
|
index = frame.filename.index(path)
|
2020-08-12 14:08:33 +00:00
|
|
|
start = index + len(path)
|
|
|
|
end = frame.filename.index("/", start)
|
|
|
|
integration = frame.filename[start:end]
|
|
|
|
if integration not in exclude_integrations:
|
|
|
|
found_frame = frame
|
|
|
|
|
2020-05-13 07:58:33 +00:00
|
|
|
break
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
|
|
|
|
if found_frame is not None:
|
|
|
|
break
|
|
|
|
|
|
|
|
if found_frame is None:
|
|
|
|
raise MissingIntegrationFrame
|
|
|
|
|
|
|
|
return found_frame, integration, path
|
|
|
|
|
|
|
|
|
|
|
|
class MissingIntegrationFrame(HomeAssistantError):
|
|
|
|
"""Raised when no integration is found in the frame."""
|
2020-07-06 22:58:53 +00:00
|
|
|
|
|
|
|
|
2021-11-20 10:53:04 +00:00
|
|
|
def report(
|
2021-11-23 12:35:53 +00:00
|
|
|
what: str,
|
|
|
|
exclude_integrations: set | None = None,
|
|
|
|
error_if_core: bool = True,
|
|
|
|
level: int = logging.WARNING,
|
2021-11-20 10:53:04 +00:00
|
|
|
) -> None:
|
2020-07-06 22:58:53 +00:00
|
|
|
"""Report incorrect usage.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
try:
|
2021-11-20 10:53:04 +00:00
|
|
|
integration_frame = get_integration_frame(
|
|
|
|
exclude_integrations=exclude_integrations
|
|
|
|
)
|
2020-08-28 11:50:32 +00:00
|
|
|
except MissingIntegrationFrame as err:
|
2021-11-20 10:53:04 +00:00
|
|
|
msg = f"Detected code that {what}. Please report this issue."
|
|
|
|
if error_if_core:
|
|
|
|
raise RuntimeError(msg) from err
|
|
|
|
_LOGGER.warning(msg, stack_info=True)
|
|
|
|
return
|
2020-07-06 22:58:53 +00:00
|
|
|
|
2021-11-23 12:35:53 +00:00
|
|
|
report_integration(what, integration_frame, level)
|
2020-08-12 14:08:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
def report_integration(
|
2021-11-23 12:35:53 +00:00
|
|
|
what: str,
|
|
|
|
integration_frame: tuple[FrameSummary, str, str],
|
|
|
|
level: int = logging.WARNING,
|
2020-08-12 14:08:33 +00:00
|
|
|
) -> None:
|
|
|
|
"""Report incorrect usage in an integration.
|
|
|
|
|
|
|
|
Async friendly.
|
|
|
|
"""
|
|
|
|
found_frame, integration, path = integration_frame
|
|
|
|
|
2021-12-06 23:26:31 +00:00
|
|
|
# Keep track of integrations already reported to prevent flooding
|
|
|
|
key = f"{found_frame.filename}:{found_frame.lineno}"
|
|
|
|
if key in _REPORTED_INTEGRATIONS:
|
|
|
|
return
|
|
|
|
_REPORTED_INTEGRATIONS.add(key)
|
|
|
|
|
2020-07-06 22:58:53 +00:00
|
|
|
index = found_frame.filename.index(path)
|
|
|
|
if path == "custom_components/":
|
2022-07-18 20:10:22 +00:00
|
|
|
extra = " to the custom integration author"
|
2020-07-06 22:58:53 +00:00
|
|
|
else:
|
|
|
|
extra = ""
|
|
|
|
|
2021-11-23 12:35:53 +00:00
|
|
|
_LOGGER.log(
|
|
|
|
level,
|
2022-12-27 10:18:56 +00:00
|
|
|
(
|
|
|
|
"Detected integration that %s. "
|
|
|
|
"Please report issue%s for %s using this method at %s, line %s: %s"
|
|
|
|
),
|
2020-07-06 22:58:53 +00:00
|
|
|
what,
|
|
|
|
extra,
|
|
|
|
integration,
|
|
|
|
found_frame.filename[index:],
|
|
|
|
found_frame.lineno,
|
2022-04-26 14:41:52 +00:00
|
|
|
(found_frame.line or "?").strip(),
|
2020-07-06 22:58:53 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2022-03-17 18:09:55 +00:00
|
|
|
def warn_use(func: _CallableT, what: str) -> _CallableT:
|
2020-07-06 22:58:53 +00:00
|
|
|
"""Mock a function to warn when it was about to be used."""
|
|
|
|
if asyncio.iscoroutinefunction(func):
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
async def report_use(*args: Any, **kwargs: Any) -> None:
|
|
|
|
report(what)
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
@functools.wraps(func)
|
|
|
|
def report_use(*args: Any, **kwargs: Any) -> None:
|
|
|
|
report(what)
|
|
|
|
|
2022-03-17 18:09:55 +00:00
|
|
|
return cast(_CallableT, report_use)
|