Speed up the frame helper (#112562)

pull/112575/head
J. Nick Koston 2024-03-06 20:54:09 -10:00 committed by GitHub
parent 3ccbb2c87a
commit 1fb9cfe37e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 460 additions and 338 deletions

View File

@ -6,15 +6,21 @@ from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass
import functools
import linecache
import logging
import sys
from traceback import FrameSummary, extract_stack
from typing import Any, TypeVar, cast
from types import FrameType
from typing import TYPE_CHECKING, Any, TypeVar, cast
from homeassistant.core import HomeAssistant, async_get_hass
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import async_suggest_report_issue
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
_LOGGER = logging.getLogger(__name__)
# Keep track of integrations already reported to prevent flooding
@ -28,10 +34,25 @@ class IntegrationFrame:
"""Integration frame container."""
custom_integration: bool
frame: FrameSummary
integration: str
module: str | None
relative_filename: str
_frame: FrameType
@cached_property
def line_number(self) -> int:
"""Return the line number of the frame."""
return self._frame.f_lineno
@cached_property
def filename(self) -> str:
"""Return the filename of the frame."""
return self._frame.f_code.co_filename
@cached_property
def line(self) -> str:
"""Return the line of the frame."""
return (linecache.getline(self.filename, self.line_number) or "?").strip()
def get_integration_logger(fallback_name: str) -> logging.Logger:
@ -54,19 +75,28 @@ def get_integration_logger(fallback_name: str) -> logging.Logger:
return logging.getLogger(logger_name)
def get_current_frame(depth: int = 0) -> FrameType:
"""Return the current frame."""
# Add one to depth since get_current_frame is included
return sys._getframe(depth + 1) # pylint: disable=protected-access
def get_integration_frame(exclude_integrations: set | None = None) -> IntegrationFrame:
"""Return the frame, integration and integration path of the current stack frame."""
found_frame = None
if not exclude_integrations:
exclude_integrations = set()
for frame in reversed(extract_stack()):
frame: FrameType | None = get_current_frame()
while frame is not None:
filename = frame.f_code.co_filename
for path in ("custom_components/", "homeassistant/components/"):
try:
index = frame.filename.index(path)
index = filename.index(path)
start = index + len(path)
end = frame.filename.index("/", start)
integration = frame.filename[start:end]
end = filename.index("/", start)
integration = filename[start:end]
if integration not in exclude_integrations:
found_frame = frame
@ -77,6 +107,8 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio
if found_frame is not None:
break
frame = frame.f_back
if found_frame is None:
raise MissingIntegrationFrame
@ -84,16 +116,16 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio
for module, module_obj in dict(sys.modules).items():
if not hasattr(module_obj, "__file__"):
continue
if module_obj.__file__ == found_frame.filename:
if module_obj.__file__ == found_frame.f_code.co_filename:
found_module = module
break
return IntegrationFrame(
custom_integration=path == "custom_components/",
frame=found_frame,
integration=integration,
module=found_module,
relative_filename=found_frame.filename[index:],
relative_filename=found_frame.f_code.co_filename[index:],
_frame=found_frame,
)
@ -137,9 +169,8 @@ def _report_integration(
Async friendly.
"""
found_frame = integration_frame.frame
# Keep track of integrations already reported to prevent flooding
key = f"{found_frame.filename}:{found_frame.lineno}"
key = f"{integration_frame.filename}:{integration_frame.line_number}"
if key in _REPORTED_INTEGRATIONS:
return
_REPORTED_INTEGRATIONS.add(key)
@ -160,8 +191,8 @@ def _report_integration(
integration_frame.integration,
what,
integration_frame.relative_filename,
found_frame.lineno,
(found_frame.line or "?").strip(),
integration_frame.line_number,
integration_frame.line,
report_issue,
)

View File

@ -9,7 +9,6 @@ import functools
import logging
import sys
import threading
from traceback import extract_stack
from typing import Any, ParamSpec, TypeVar, TypeVarTuple
from homeassistant.exceptions import HomeAssistantError
@ -116,14 +115,6 @@ def check_loop(
The default advisory message is 'Use `await hass.async_add_executor_job()'
Set `advise_msg` to an alternate message if the solution differs.
"""
# pylint: disable=import-outside-toplevel
from homeassistant.core import HomeAssistant, async_get_hass
from homeassistant.helpers.frame import (
MissingIntegrationFrame,
get_integration_frame,
)
from homeassistant.loader import async_suggest_report_issue
try:
get_running_loop()
in_loop = True
@ -133,18 +124,32 @@ def check_loop(
if not in_loop:
return
# Import only after we know we are running in the event loop
# so threads do not have to pay the late import cost.
# pylint: disable=import-outside-toplevel
from homeassistant.core import HomeAssistant, async_get_hass
from homeassistant.helpers.frame import (
MissingIntegrationFrame,
get_current_frame,
get_integration_frame,
)
from homeassistant.loader import async_suggest_report_issue
found_frame = None
stack = extract_stack()
if (
func.__name__ == "sleep"
and len(stack) >= 3
and stack[-3].filename.endswith("pydevd.py")
):
# Don't report `time.sleep` injected by the debugger (pydevd.py)
# stack[-1] is us, stack[-2] is protected_loop_func, stack[-3] is the offender
return
if func.__name__ == "sleep":
#
# Avoid extracting the stack unless we need to since it
# will have to access the linecache which can do blocking
# I/O and we are trying to avoid blocking calls.
#
# frame[1] is us
# frame[2] is protected_loop_func
# frame[3] is the offender
with suppress(ValueError):
offender_frame = get_current_frame(3)
if offender_frame.f_code.co_filename.endswith("pydevd.py"):
return
try:
integration_frame = get_integration_frame()
@ -167,7 +172,6 @@ def check_loop(
module=integration_frame.module,
)
found_frame = integration_frame.frame
_LOGGER.warning(
(
"Detected blocking call to %s inside the event loop by %sintegration '%s' "
@ -177,8 +181,8 @@ def check_loop(
"custom " if integration_frame.custom_integration else "",
integration_frame.integration,
integration_frame.relative_filename,
found_frame.lineno,
(found_frame.line or "?").strip(),
integration_frame.line_number,
integration_frame.line,
report_issue,
)
@ -186,8 +190,8 @@ def check_loop(
raise RuntimeError(
"Blocking calls must be done in the executor or a separate thread;"
f" {advise_msg or 'Use `await hass.async_add_executor_job()`'}; at"
f" {integration_frame.relative_filename}, line {found_frame.lineno}:"
f" {(found_frame.line or '?').strip()}"
f" {integration_frame.relative_filename}, line {integration_frame.line_number}:"
f" {integration_frame.line}"
)

View File

@ -15,7 +15,7 @@ import os
import pathlib
import threading
import time
from types import ModuleType
from types import FrameType, ModuleType
from typing import Any, NoReturn, TypeVar
from unittest.mock import AsyncMock, Mock, patch
@ -1596,3 +1596,20 @@ def help_test_all(module: ModuleType) -> None:
assert set(module.__all__) == {
itm for itm in module.__dir__() if not itm.startswith("_")
}
def extract_stack_to_frame(extract_stack: list[Mock]) -> FrameType:
"""Convert an extract stack to a frame list."""
stack = list(extract_stack)
for frame in stack:
frame.f_back = None
frame.f_code.co_filename = frame.filename
frame.f_lineno = int(frame.lineno)
top_frame = stack.pop()
current_frame = top_frame
while stack and (next_frame := stack.pop()):
current_frame.f_back = next_frame
current_frame = next_frame
return top_frame

View File

@ -9,6 +9,8 @@ from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_ca
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import extract_stack_to_frame
DOMAIN = "zeroconf"
@ -50,25 +52,29 @@ async def test_multiple_zeroconf_instances_gives_shared(
line="self.light.is_on",
)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/dev/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/dev/homeassistant/components/zeroconf/usage.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/dev/mdns/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value=correct_frame.line
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/dev/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/dev/homeassistant/components/zeroconf/usage.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/dev/mdns/lights.py",
lineno="2",
line="something()",
),
]
),
):
assert zeroconf.Zeroconf() == zeroconf_instance

View File

@ -96,6 +96,7 @@ from .common import ( # noqa: E402, isort:skip
init_recorder_component,
mock_storage,
patch_yaml_files,
extract_stack_to_frame,
)
from .test_util.aiohttp import ( # noqa: E402, isort:skip
AiohttpClientMocker,
@ -1588,20 +1589,24 @@ def mock_integration_frame() -> Generator[Mock, None, None]:
line="self.light.is_on",
)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value=correct_frame.line
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
yield correct_frame

View File

@ -20,7 +20,12 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant
import homeassistant.helpers.aiohttp_client as client
from homeassistant.util.color import RGBColor
from tests.common import MockConfigEntry, MockModule, mock_integration
from tests.common import (
MockConfigEntry,
MockModule,
extract_stack_to_frame,
mock_integration,
)
from tests.test_util.aiohttp import AiohttpClientMocker
@ -166,24 +171,29 @@ async def test_warning_close_session_integration(
) -> None:
"""Test log warning message when closing the session from integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
session = client.async_get_clientsession(hass)
await session.close()
@ -202,24 +212,29 @@ async def test_warning_close_session_custom(
"""Test log warning message when closing the session from custom context."""
mock_integration(hass, MockModule("hue"), built_in=False)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
session = client.async_get_clientsession(hass)
await session.close()

View File

@ -20,7 +20,7 @@ from homeassistant.helpers.deprecation import (
)
from homeassistant.helpers.frame import MissingIntegrationFrame
from tests.common import MockModule, mock_integration
from tests.common import MockModule, extract_stack_to_frame, mock_integration
class MockBaseClassDeprecatedProperty:
@ -178,24 +178,29 @@ def test_deprecated_function_called_from_built_in_integration(
pass
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
mock_deprecated_function()
assert (
@ -230,24 +235,29 @@ def test_deprecated_function_called_from_custom_integration(
pass
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
mock_deprecated_function()
assert (
@ -327,24 +337,29 @@ def test_check_if_deprecated_constant(
# mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame
with patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename=filename,
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename=filename,
lineno="23",
line="await session.close()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
value = check_if_deprecated_constant("TEST_CONSTANT", module_globals)
assert value == _get_value(deprecated_constant)
@ -397,7 +412,8 @@ def test_check_if_deprecated_constant_integration_not_found(
}
with patch(
"homeassistant.helpers.frame.extract_stack", side_effect=MissingIntegrationFrame
"homeassistant.helpers.frame.get_current_frame",
side_effect=MissingIntegrationFrame,
):
value = check_if_deprecated_constant("TEST_CONSTANT", module_globals)
assert value == _get_value(deprecated_constant)

View File

@ -7,6 +7,8 @@ import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers import frame
from tests.common import extract_stack_to_frame
async def test_extract_frame_integration(
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
@ -15,7 +17,7 @@ async def test_extract_frame_integration(
integration_frame = frame.get_integration_frame()
assert integration_frame == frame.IntegrationFrame(
custom_integration=False,
frame=mock_integration_frame,
_frame=mock_integration_frame,
integration="hue",
module=None,
relative_filename="homeassistant/components/hue/light.py",
@ -40,7 +42,7 @@ async def test_extract_frame_resolve_module(
assert integration_frame == frame.IntegrationFrame(
custom_integration=True,
frame=ANY,
_frame=ANY,
integration="test_integration_frame",
module="custom_components.test_integration_frame",
relative_filename="custom_components/test_integration_frame/__init__.py",
@ -68,25 +70,27 @@ async def test_extract_frame_integration_with_excluded_integration(
line="self.light.is_on",
)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/dev/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/dev/homeassistant/components/zeroconf/usage.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/dev/mdns/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/dev/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/dev/homeassistant/components/zeroconf/usage.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/dev/mdns/lights.py",
lineno="2",
line="something()",
),
]
),
):
integration_frame = frame.get_integration_frame(
exclude_integrations={"zeroconf"}
@ -94,7 +98,7 @@ async def test_extract_frame_integration_with_excluded_integration(
assert integration_frame == frame.IntegrationFrame(
custom_integration=False,
frame=correct_frame,
_frame=correct_frame,
integration="mdns",
module=None,
relative_filename="homeassistant/components/mdns/light.py",
@ -104,19 +108,21 @@ async def test_extract_frame_integration_with_excluded_integration(
async def test_extract_frame_no_integration(caplog: pytest.LogCaptureFixture) -> None:
"""Test extracting the current frame without integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
), pytest.raises(frame.MissingIntegrationFrame):
frame.get_integration_frame()
@ -126,19 +132,21 @@ async def test_get_integration_logger_no_integration(
) -> None:
"""Test getting fallback logger without integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
logger = frame.get_integration_logger(__name__)

View File

@ -7,7 +7,7 @@ import pytest
from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant
import homeassistant.helpers.httpx_client as client
from tests.common import MockModule, mock_integration
from tests.common import MockModule, extract_stack_to_frame, mock_integration
async def test_get_async_client_with_ssl(hass: HomeAssistant) -> None:
@ -104,24 +104,29 @@ async def test_warning_close_session_integration(
) -> None:
"""Test log warning message when closing the session from integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.aclose()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.aclose()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="await session.aclose()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
httpx_session = client.get_async_client(hass)
await httpx_session.aclose()
@ -141,24 +146,29 @@ async def test_warning_close_session_custom(
"""Test log warning message when closing the session from custom context."""
mock_integration(hass, MockModule("hue"), built_in=False)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.aclose()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline",
return_value="await session.aclose()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="await session.aclose()",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
httpx_session = client.get_async_client(hass)
await httpx_session.aclose()

View File

@ -1122,7 +1122,7 @@ async def test_hass_components_use_reported(
)
integration_frame = frame.IntegrationFrame(
custom_integration=True,
frame=mock_integration_frame,
_frame=mock_integration_frame,
integration="test_integration_frame",
module="custom_components.test_integration_frame",
relative_filename="custom_components/test_integration_frame/__init__.py",

View File

@ -10,6 +10,8 @@ from homeassistant import block_async_io
from homeassistant.core import HomeAssistant
from homeassistant.util import async_ as hasync
from tests.common import extract_stack_to_frame
@patch("concurrent.futures.Future")
@patch("threading.get_ident")
@ -49,24 +51,28 @@ async def test_check_loop_async() -> None:
async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> None:
"""Test check_loop detects and raises when called from event loop from integration context."""
with pytest.raises(RuntimeError), patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
hasync.check_loop(banned_function)
assert (
@ -82,24 +88,28 @@ async def test_check_loop_async_integration_non_strict(
) -> None:
"""Test check_loop detects when called from event loop from integration context."""
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
hasync.check_loop(banned_function, strict=False)
assert (
@ -113,24 +123,28 @@ async def test_check_loop_async_integration_non_strict(
async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None:
"""Test check_loop detects when called from event loop with custom component context."""
with pytest.raises(RuntimeError), patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/config/custom_components/hue/light.py",
lineno="23",
line="self.light.is_on",
),
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
hasync.check_loop(banned_function)
assert (
@ -161,24 +175,16 @@ async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) ->
block_async_io.enable()
with patch(
"homeassistant.util.async_.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/.venv/blah/pydevd.py",
lineno="23",
line="do_something()",
),
Mock(
filename="/home/paulus/homeassistant/util/async.py",
lineno="123",
line="protected_loop_func",
),
Mock(
filename="/home/paulus/homeassistant/util/async.py",
lineno="123",
line="check_loop()",
),
],
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/.venv/blah/pydevd.py",
lineno="23",
line="do_something()",
),
]
),
):
time.sleep(0)
assert "Detected blocking call inside the event loop" not in caplog.text

View File

@ -18,7 +18,7 @@ from homeassistant.exceptions import HomeAssistantError
import homeassistant.util.yaml as yaml
from homeassistant.util.yaml import loader as yaml_loader
from tests.common import get_test_config_dir, patch_yaml_files
from tests.common import extract_stack_to_frame, get_test_config_dir, patch_yaml_files
@pytest.fixture(params=["enable_c_loader", "disable_c_loader"])
@ -611,20 +611,24 @@ def mock_integration_frame() -> Generator[Mock, None, None]:
line="self.light.is_on",
)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
],
"homeassistant.helpers.frame.linecache.getline", return_value=correct_frame.line
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock(
filename="/home/paulus/homeassistant/core.py",
lineno="23",
line="do_something()",
),
correct_frame,
Mock(
filename="/home/paulus/aiohue/lights.py",
lineno="2",
line="something()",
),
]
),
):
yield correct_frame