From 4e98d39106c71712441ead6cbf07c66f258bd490 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 6 Oct 2023 19:57:43 +0200 Subject: [PATCH] Use loader.async_suggest_report_issue in async util (#101516) --- homeassistant/util/async_.py | 74 +++++++++++++++++++----------------- tests/util/test_async.py | 30 +++++++-------- 2 files changed, 54 insertions(+), 50 deletions(-) diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index ce1105cff75..bc4cf68bb81 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -5,12 +5,15 @@ from asyncio import Future, Semaphore, gather, get_running_loop from asyncio.events import AbstractEventLoop from collections.abc import Awaitable, Callable import concurrent.futures +from contextlib import suppress import functools import logging import threading from traceback import extract_stack from typing import Any, ParamSpec, TypeVar +from homeassistant.exceptions import HomeAssistantError + _LOGGER = logging.getLogger(__name__) _SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" @@ -82,6 +85,14 @@ 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 @@ -104,54 +115,47 @@ def check_loop( # stack[-1] is us, stack[-2] is protected_loop_func, stack[-3] is the offender return - for frame in reversed(stack): - for path in ("custom_components/", "homeassistant/components/"): - try: - index = frame.filename.index(path) - found_frame = frame - break - except ValueError: - continue + try: + integration_frame = get_integration_frame() + except MissingIntegrationFrame: + # Did not source from integration? Hard error. + if found_frame is None: + raise RuntimeError( # noqa: TRY200 + f"Detected blocking call to {func.__name__} inside the event loop. " + f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " + "This is causing stability issues. Please create a bug report at " + f"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) - if found_frame is not None: - break - - # Did not source from integration? Hard error. - if found_frame is None: - raise RuntimeError( - f"Detected blocking call to {func.__name__} inside the event loop. " - f"{advise_msg or 'Use `await hass.async_add_executor_job()`'}; " - "This is causing stability issues. Please report issue" - ) - - start = index + len(path) - end = found_frame.filename.index("/", start) - - integration = found_frame.filename[start:end] - - if path == "custom_components/": - extra = " to the custom integration author" - else: - extra = "" + hass: HomeAssistant | None = None + with suppress(HomeAssistantError): + hass = async_get_hass() + report_issue = async_suggest_report_issue( + hass, + integration_domain=integration_frame.integration, + module=integration_frame.module, + ) + found_frame = integration_frame.frame _LOGGER.warning( ( - "Detected blocking call to %s inside the event loop. This is causing" - " stability issues. Please report issue%s for %s doing blocking calls at" - " %s, line %s: %s" + "Detected blocking call to %s inside the event loop by %sintegration '%s' " + "at %s, line %s: %s, please %s" ), func.__name__, - extra, - integration, - found_frame.filename[index:], + "custom " if integration_frame.custom_integration else "", + integration_frame.integration, + integration_frame.relative_filename, found_frame.lineno, (found_frame.line or "?").strip(), + report_issue, ) + if strict: 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" {found_frame.filename[index:]}, line {found_frame.lineno}:" + f" {integration_frame.relative_filename}, line {found_frame.lineno}:" f" {(found_frame.line or '?').strip()}" ) diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 7b0cc916ec7..4945e95d2d7 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -48,7 +48,7 @@ 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.util.async_.extract_stack", + "homeassistant.helpers.frame.extract_stack", return_value=[ Mock( filename="/home/paulus/homeassistant/core.py", @@ -69,10 +69,10 @@ async def test_check_loop_async_integration(caplog: pytest.LogCaptureFixture) -> ): hasync.check_loop(banned_function) assert ( - "Detected blocking call to banned_function inside the event loop. This is " - "causing stability issues. Please report issue for hue doing blocking calls at " - "homeassistant/components/hue/light.py, line 23: self.light.is_on" - in caplog.text + "Detected blocking call to banned_function inside the event loop by integration" + " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " + "please create a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) @@ -81,7 +81,7 @@ 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.util.async_.extract_stack", + "homeassistant.helpers.frame.extract_stack", return_value=[ Mock( filename="/home/paulus/homeassistant/core.py", @@ -102,17 +102,17 @@ async def test_check_loop_async_integration_non_strict( ): hasync.check_loop(banned_function, strict=False) assert ( - "Detected blocking call to banned_function inside the event loop. This is " - "causing stability issues. Please report issue for hue doing blocking calls at " - "homeassistant/components/hue/light.py, line 23: self.light.is_on" - in caplog.text + "Detected blocking call to banned_function inside the event loop by integration" + " 'hue' at homeassistant/components/hue/light.py, line 23: self.light.is_on, " + "please create a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" in caplog.text ) 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.util.async_.extract_stack", + "homeassistant.helpers.frame.extract_stack", return_value=[ Mock( filename="/home/paulus/homeassistant/core.py", @@ -133,10 +133,10 @@ async def test_check_loop_async_custom(caplog: pytest.LogCaptureFixture) -> None ): hasync.check_loop(banned_function) assert ( - "Detected blocking call to banned_function inside the event loop. This is" - " causing stability issues. Please report issue to the custom integration" - " author for hue doing blocking calls at custom_components/hue/light.py, line" - " 23: self.light.is_on" + "Detected blocking call to banned_function inside the event loop by custom " + "integration 'hue' at custom_components/hue/light.py, line 23: self.light.is_on" + ", please create a bug report at https://github.com/home-assistant/core/issues?" + "q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+hue%22" ) in caplog.text