diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 04f16ebddd0..3b9c556966a 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -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, ) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 304f2446722..4ec5028252e 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -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}" ) diff --git a/tests/common.py b/tests/common.py index 829368b309d..cc2b4fa2ad6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -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 diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 3aaea1c50ee..81743cb5e93 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -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 diff --git a/tests/conftest.py b/tests/conftest.py index 3be03e1e3ca..5753729213f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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 diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 8488a5f15e3..1a9c2b0212e 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -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() diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 25b37e2073f..f5089ecbaec 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -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) diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index fa495e9dbc9..7881c845a77 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -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__) diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 693c45cc73a..52a6226c307 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -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() diff --git a/tests/test_loader.py b/tests/test_loader.py index 6fa4de1da9c..d73ae161041 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -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", diff --git a/tests/util/test_async.py b/tests/util/test_async.py index ad2c9329fb7..199e21960e4 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -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 diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 93c8ed50498..9fda5be73a8 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -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