diff --git a/homeassistant/helpers/frame.py b/homeassistant/helpers/frame.py index 920c7150f6dbd4..9c17e17a2c64b3 100644 --- a/homeassistant/helpers/frame.py +++ b/homeassistant/helpers/frame.py @@ -86,6 +86,7 @@ def report( exclude_integrations: set | None = None, error_if_core: bool = True, level: int = logging.WARNING, + log_custom_component_only: bool = False, ) -> None: """Report incorrect usage. @@ -99,10 +100,12 @@ def report( msg = f"Detected code that {what}. Please report this issue." if error_if_core: raise RuntimeError(msg) from err - _LOGGER.warning(msg, stack_info=True) + if not log_custom_component_only: + _LOGGER.warning(msg, stack_info=True) return - _report_integration(what, integration_frame, level) + if not log_custom_component_only or integration_frame.custom_integration: + _report_integration(what, integration_frame, level) def _report_integration( diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 8aac185cac0645..1bbd22d407069f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1247,6 +1247,19 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper: if component is None: raise ImportError(f"Unable to load {comp_name}") + # Local import to avoid circular dependencies + from .helpers.frame import report # pylint: disable=import-outside-toplevel + + report( + ( + f"accesses hass.components.{comp_name}." + " This is deprecated and will stop working in Home Assistant 2024.6, it" + f" should be updated to import functions used from {comp_name} directly" + ), + error_if_core=False, + log_custom_component_only=True, + ) + wrapped = ModuleWrapper(self._hass, component) setattr(self, comp_name, wrapped) return wrapped diff --git a/tests/conftest.py b/tests/conftest.py index 18645034c2927f..3be03e1e3ca001 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1579,6 +1579,33 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]: yield mock_bleak_scanner_start +@pytest.fixture +def mock_integration_frame() -> Generator[Mock, None, None]: + """Mock as if we're calling code from inside an integration.""" + correct_frame = Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + 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()", + ), + ], + ): + yield correct_frame + + @pytest.fixture def mock_bluetooth( mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index f1547f36e399c1..5010c459345806 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,6 +1,5 @@ """Test the frame helper.""" -from collections.abc import Generator from unittest.mock import ANY, Mock, patch import pytest @@ -9,33 +8,6 @@ from homeassistant.helpers import frame -@pytest.fixture -def mock_integration_frame() -> Generator[Mock, None, None]: - """Mock as if we're calling code from inside an integration.""" - correct_frame = Mock( - filename="/home/paulus/homeassistant/components/hue/light.py", - lineno="23", - 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()", - ), - ], - ): - yield correct_frame - - async def test_extract_frame_integration( caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock ) -> None: @@ -174,3 +146,8 @@ async def test_report_missing_integration_frame( frame.report(what, error_if_core=False) assert what in caplog.text assert caplog.text.count(what) == 1 + + caplog.clear() + + frame.report(what, error_if_core=False, log_custom_component_only=True) + assert caplog.text == "" diff --git a/tests/test_loader.py b/tests/test_loader.py index 27fe3b94cf2897..d173e3e8aa6333 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,6 +1,6 @@ """Test to verify that we can load components.""" import asyncio -from unittest.mock import patch +from unittest.mock import Mock, patch import pytest @@ -8,6 +8,7 @@ from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import frame from .common import MockModule, async_get_persistent_notifications, mock_integration @@ -287,6 +288,7 @@ async def test_get_integration_custom_component( ) -> None: """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_package") + assert integration.get_component().DOMAIN == "test_package" assert integration.name == "Test Package" @@ -1001,3 +1003,33 @@ async def test_config_folder_not_in_path(hass): # Verify that we are able to load the file with absolute path import tests.testing_config.check_config_not_in_path # noqa: F401 + + +async def test_hass_components_use_reported( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock +) -> None: + """Test that use of hass.components is reported.""" + mock_integration_frame.filename = ( + "/home/paulus/homeassistant/custom_components/demo/light.py" + ) + integration_frame = frame.IntegrationFrame( + custom_integration=True, + frame=mock_integration_frame, + integration="test_integration_frame", + module="custom_components.test_integration_frame", + relative_filename="custom_components/test_integration_frame/__init__.py", + ) + + with patch( + "homeassistant.helpers.frame.get_integration_frame", + return_value=integration_frame, + ), patch( + "homeassistant.components.http.start_http_server_and_save_config", + return_value=None, + ): + hass.components.http.start_http_server_and_save_config(hass, [], None) + + assert ( + "Detected that custom integration 'test_integration_frame'" + " accesses hass.components.http. This is deprecated" + ) in caplog.text