Speed up the frame helper (#112562)

This commit is contained in:
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 contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
import functools import functools
import linecache
import logging import logging
import sys import sys
from traceback import FrameSummary, extract_stack from types import FrameType
from typing import Any, TypeVar, cast from typing import TYPE_CHECKING, Any, TypeVar, cast
from homeassistant.core import HomeAssistant, async_get_hass from homeassistant.core import HomeAssistant, async_get_hass
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import async_suggest_report_issue 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__) _LOGGER = logging.getLogger(__name__)
# Keep track of integrations already reported to prevent flooding # Keep track of integrations already reported to prevent flooding
@ -28,10 +34,25 @@ class IntegrationFrame:
"""Integration frame container.""" """Integration frame container."""
custom_integration: bool custom_integration: bool
frame: FrameSummary
integration: str integration: str
module: str | None module: str | None
relative_filename: str 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: 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) 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: def get_integration_frame(exclude_integrations: set | None = None) -> IntegrationFrame:
"""Return the frame, integration and integration path of the current stack frame.""" """Return the frame, integration and integration path of the current stack frame."""
found_frame = None found_frame = None
if not exclude_integrations: if not exclude_integrations:
exclude_integrations = set() 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/"): for path in ("custom_components/", "homeassistant/components/"):
try: try:
index = frame.filename.index(path) index = filename.index(path)
start = index + len(path) start = index + len(path)
end = frame.filename.index("/", start) end = filename.index("/", start)
integration = frame.filename[start:end] integration = filename[start:end]
if integration not in exclude_integrations: if integration not in exclude_integrations:
found_frame = frame found_frame = frame
@ -77,6 +107,8 @@ def get_integration_frame(exclude_integrations: set | None = None) -> Integratio
if found_frame is not None: if found_frame is not None:
break break
frame = frame.f_back
if found_frame is None: if found_frame is None:
raise MissingIntegrationFrame 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(): for module, module_obj in dict(sys.modules).items():
if not hasattr(module_obj, "__file__"): if not hasattr(module_obj, "__file__"):
continue continue
if module_obj.__file__ == found_frame.filename: if module_obj.__file__ == found_frame.f_code.co_filename:
found_module = module found_module = module
break break
return IntegrationFrame( return IntegrationFrame(
custom_integration=path == "custom_components/", custom_integration=path == "custom_components/",
frame=found_frame,
integration=integration, integration=integration,
module=found_module, 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. Async friendly.
""" """
found_frame = integration_frame.frame
# Keep track of integrations already reported to prevent flooding # 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: if key in _REPORTED_INTEGRATIONS:
return return
_REPORTED_INTEGRATIONS.add(key) _REPORTED_INTEGRATIONS.add(key)
@ -160,8 +191,8 @@ def _report_integration(
integration_frame.integration, integration_frame.integration,
what, what,
integration_frame.relative_filename, integration_frame.relative_filename,
found_frame.lineno, integration_frame.line_number,
(found_frame.line or "?").strip(), integration_frame.line,
report_issue, report_issue,
) )

View file

@ -9,7 +9,6 @@ import functools
import logging import logging
import sys import sys
import threading import threading
from traceback import extract_stack
from typing import Any, ParamSpec, TypeVar, TypeVarTuple from typing import Any, ParamSpec, TypeVar, TypeVarTuple
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -116,14 +115,6 @@ def check_loop(
The default advisory message is 'Use `await hass.async_add_executor_job()' The default advisory message is 'Use `await hass.async_add_executor_job()'
Set `advise_msg` to an alternate message if the solution differs. 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: try:
get_running_loop() get_running_loop()
in_loop = True in_loop = True
@ -133,17 +124,31 @@ def check_loop(
if not in_loop: if not in_loop:
return 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 found_frame = None
stack = extract_stack() if func.__name__ == "sleep":
#
if ( # Avoid extracting the stack unless we need to since it
func.__name__ == "sleep" # will have to access the linecache which can do blocking
and len(stack) >= 3 # I/O and we are trying to avoid blocking calls.
and stack[-3].filename.endswith("pydevd.py") #
): # frame[1] is us
# Don't report `time.sleep` injected by the debugger (pydevd.py) # frame[2] is protected_loop_func
# stack[-1] is us, stack[-2] is protected_loop_func, stack[-3] is the offender # 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 return
try: try:
@ -167,7 +172,6 @@ def check_loop(
module=integration_frame.module, module=integration_frame.module,
) )
found_frame = integration_frame.frame
_LOGGER.warning( _LOGGER.warning(
( (
"Detected blocking call to %s inside the event loop by %sintegration '%s' " "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 "", "custom " if integration_frame.custom_integration else "",
integration_frame.integration, integration_frame.integration,
integration_frame.relative_filename, integration_frame.relative_filename,
found_frame.lineno, integration_frame.line_number,
(found_frame.line or "?").strip(), integration_frame.line,
report_issue, report_issue,
) )
@ -186,8 +190,8 @@ def check_loop(
raise RuntimeError( raise RuntimeError(
"Blocking calls must be done in the executor or a separate thread;" "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" {advise_msg or 'Use `await hass.async_add_executor_job()`'}; at"
f" {integration_frame.relative_filename}, line {found_frame.lineno}:" f" {integration_frame.relative_filename}, line {integration_frame.line_number}:"
f" {(found_frame.line or '?').strip()}" f" {integration_frame.line}"
) )

View file

@ -15,7 +15,7 @@ import os
import pathlib import pathlib
import threading import threading
import time import time
from types import ModuleType from types import FrameType, ModuleType
from typing import Any, NoReturn, TypeVar from typing import Any, NoReturn, TypeVar
from unittest.mock import AsyncMock, Mock, patch from unittest.mock import AsyncMock, Mock, patch
@ -1596,3 +1596,20 @@ def help_test_all(module: ModuleType) -> None:
assert set(module.__all__) == { assert set(module.__all__) == {
itm for itm in module.__dir__() if not itm.startswith("_") 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.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import extract_stack_to_frame
DOMAIN = "zeroconf" DOMAIN = "zeroconf"
@ -50,8 +52,11 @@ async def test_multiple_zeroconf_instances_gives_shared(
line="self.light.is_on", line="self.light.is_on",
) )
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline", return_value=correct_frame.line
return_value=[ ), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/dev/homeassistant/core.py", filename="/home/dev/homeassistant/core.py",
lineno="23", lineno="23",
@ -68,7 +73,8 @@ async def test_multiple_zeroconf_instances_gives_shared(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
assert zeroconf.Zeroconf() == zeroconf_instance assert zeroconf.Zeroconf() == zeroconf_instance

View file

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

View file

@ -20,7 +20,12 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant
import homeassistant.helpers.aiohttp_client as client import homeassistant.helpers.aiohttp_client as client
from homeassistant.util.color import RGBColor 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 from tests.test_util.aiohttp import AiohttpClientMocker
@ -166,8 +171,12 @@ async def test_warning_close_session_integration(
) -> None: ) -> None:
"""Test log warning message when closing the session from integration context.""" """Test log warning message when closing the session from integration context."""
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -183,7 +192,8 @@ async def test_warning_close_session_integration(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
session = client.async_get_clientsession(hass) session = client.async_get_clientsession(hass)
await session.close() await session.close()
@ -202,8 +212,12 @@ async def test_warning_close_session_custom(
"""Test log warning message when closing the session from custom context.""" """Test log warning message when closing the session from custom context."""
mock_integration(hass, MockModule("hue"), built_in=False) mock_integration(hass, MockModule("hue"), built_in=False)
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -219,7 +233,8 @@ async def test_warning_close_session_custom(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
session = client.async_get_clientsession(hass) session = client.async_get_clientsession(hass)
await session.close() await session.close()

View file

@ -20,7 +20,7 @@ from homeassistant.helpers.deprecation import (
) )
from homeassistant.helpers.frame import MissingIntegrationFrame 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: class MockBaseClassDeprecatedProperty:
@ -178,8 +178,12 @@ def test_deprecated_function_called_from_built_in_integration(
pass pass
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -195,7 +199,8 @@ def test_deprecated_function_called_from_built_in_integration(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
mock_deprecated_function() mock_deprecated_function()
assert ( assert (
@ -230,8 +235,12 @@ def test_deprecated_function_called_from_custom_integration(
pass pass
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -247,7 +256,8 @@ def test_deprecated_function_called_from_custom_integration(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
mock_deprecated_function() mock_deprecated_function()
assert ( assert (
@ -327,8 +337,12 @@ def test_check_if_deprecated_constant(
# mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame # mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame
with patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch( with patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.close()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -344,7 +358,8 @@ def test_check_if_deprecated_constant(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
value = check_if_deprecated_constant("TEST_CONSTANT", module_globals) value = check_if_deprecated_constant("TEST_CONSTANT", module_globals)
assert value == _get_value(deprecated_constant) assert value == _get_value(deprecated_constant)
@ -397,7 +412,8 @@ def test_check_if_deprecated_constant_integration_not_found(
} }
with patch( 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) value = check_if_deprecated_constant("TEST_CONSTANT", module_globals)
assert value == _get_value(deprecated_constant) assert value == _get_value(deprecated_constant)

View file

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

View file

@ -7,7 +7,7 @@ import pytest
from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE, HomeAssistant
import homeassistant.helpers.httpx_client as client 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: async def test_get_async_client_with_ssl(hass: HomeAssistant) -> None:
@ -104,8 +104,12 @@ async def test_warning_close_session_integration(
) -> None: ) -> None:
"""Test log warning message when closing the session from integration context.""" """Test log warning message when closing the session from integration context."""
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.aclose()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -121,7 +125,8 @@ async def test_warning_close_session_integration(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
httpx_session = client.get_async_client(hass) httpx_session = client.get_async_client(hass)
await httpx_session.aclose() await httpx_session.aclose()
@ -141,8 +146,12 @@ async def test_warning_close_session_custom(
"""Test log warning message when closing the session from custom context.""" """Test log warning message when closing the session from custom context."""
mock_integration(hass, MockModule("hue"), built_in=False) mock_integration(hass, MockModule("hue"), built_in=False)
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline",
return_value=[ return_value="await session.aclose()",
), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -158,7 +167,8 @@ async def test_warning_close_session_custom(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
httpx_session = client.get_async_client(hass) httpx_session = client.get_async_client(hass)
await httpx_session.aclose() await httpx_session.aclose()

View file

@ -1122,7 +1122,7 @@ async def test_hass_components_use_reported(
) )
integration_frame = frame.IntegrationFrame( integration_frame = frame.IntegrationFrame(
custom_integration=True, custom_integration=True,
frame=mock_integration_frame, _frame=mock_integration_frame,
integration="test_integration_frame", integration="test_integration_frame",
module="custom_components.test_integration_frame", module="custom_components.test_integration_frame",
relative_filename="custom_components/test_integration_frame/__init__.py", 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.core import HomeAssistant
from homeassistant.util import async_ as hasync from homeassistant.util import async_ as hasync
from tests.common import extract_stack_to_frame
@patch("concurrent.futures.Future") @patch("concurrent.futures.Future")
@patch("threading.get_ident") @patch("threading.get_ident")
@ -49,8 +51,11 @@ async def test_check_loop_async() -> None:
async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> 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.""" """Test check_loop detects and raises when called from event loop from integration context."""
with pytest.raises(RuntimeError), patch( with pytest.raises(RuntimeError), patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
return_value=[ ), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -66,7 +71,8 @@ async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) ->
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
hasync.check_loop(banned_function) hasync.check_loop(banned_function)
assert ( assert (
@ -82,8 +88,11 @@ async def test_check_loop_async_integration_non_strict(
) -> None: ) -> None:
"""Test check_loop detects when called from event loop from integration context.""" """Test check_loop detects when called from event loop from integration context."""
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
return_value=[ ), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -99,7 +108,8 @@ async def test_check_loop_async_integration_non_strict(
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
hasync.check_loop(banned_function, strict=False) hasync.check_loop(banned_function, strict=False)
assert ( assert (
@ -113,8 +123,11 @@ async def test_check_loop_async_integration_non_strict(
async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None: async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None:
"""Test check_loop detects when called from event loop with custom component context.""" """Test check_loop detects when called from event loop with custom component context."""
with pytest.raises(RuntimeError), patch( with pytest.raises(RuntimeError), patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline", return_value="self.light.is_on"
return_value=[ ), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -130,7 +143,8 @@ async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
hasync.check_loop(banned_function) hasync.check_loop(banned_function)
assert ( assert (
@ -161,24 +175,16 @@ async def test_protect_loop_debugger_sleep(caplog: pytest.LogCaptureFixture) ->
block_async_io.enable() block_async_io.enable()
with patch( with patch(
"homeassistant.util.async_.extract_stack", "homeassistant.helpers.frame.get_current_frame",
return_value=[ return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/.venv/blah/pydevd.py", filename="/home/paulus/homeassistant/.venv/blah/pydevd.py",
lineno="23", lineno="23",
line="do_something()", 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()",
),
],
): ):
time.sleep(0) time.sleep(0)
assert "Detected blocking call inside the event loop" not in caplog.text 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 import homeassistant.util.yaml as yaml
from homeassistant.util.yaml import loader as yaml_loader 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"]) @pytest.fixture(params=["enable_c_loader", "disable_c_loader"])
@ -611,8 +611,11 @@ def mock_integration_frame() -> Generator[Mock, None, None]:
line="self.light.is_on", line="self.light.is_on",
) )
with patch( with patch(
"homeassistant.helpers.frame.extract_stack", "homeassistant.helpers.frame.linecache.getline", return_value=correct_frame.line
return_value=[ ), patch(
"homeassistant.helpers.frame.get_current_frame",
return_value=extract_stack_to_frame(
[
Mock( Mock(
filename="/home/paulus/homeassistant/core.py", filename="/home/paulus/homeassistant/core.py",
lineno="23", lineno="23",
@ -624,7 +627,8 @@ def mock_integration_frame() -> Generator[Mock, None, None]:
lineno="2", lineno="2",
line="something()", line="something()",
), ),
], ]
),
): ):
yield correct_frame yield correct_frame