Deprecate hass.components and log warning if used inside custom component (#111508)

* Deprecate @bind_hass and log error if used inside custom component

* Log also when accessing `hass.components`

* Log warning only when `hass.components` is used

* Change version

* Process code review
This commit is contained in:
Jan-Philipp Benecke 2024-02-29 12:25:46 +01:00 committed by GitHub
parent af4771a198
commit bc6b4d01c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 83 additions and 31 deletions

View File

@ -86,6 +86,7 @@ def report(
exclude_integrations: set | None = None, exclude_integrations: set | None = None,
error_if_core: bool = True, error_if_core: bool = True,
level: int = logging.WARNING, level: int = logging.WARNING,
log_custom_component_only: bool = False,
) -> None: ) -> None:
"""Report incorrect usage. """Report incorrect usage.
@ -99,10 +100,12 @@ def report(
msg = f"Detected code that {what}. Please report this issue." msg = f"Detected code that {what}. Please report this issue."
if error_if_core: if error_if_core:
raise RuntimeError(msg) from err raise RuntimeError(msg) from err
_LOGGER.warning(msg, stack_info=True) if not log_custom_component_only:
_LOGGER.warning(msg, stack_info=True)
return 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( def _report_integration(

View File

@ -1247,6 +1247,19 @@ class Components:
if component is None: if component is None:
raise ImportError(f"Unable to load {comp_name}") 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) wrapped = ModuleWrapper(self._hass, component)
setattr(self, comp_name, wrapped) setattr(self, comp_name, wrapped)
return wrapped return wrapped

View File

@ -1579,6 +1579,33 @@ def mock_bleak_scanner_start() -> Generator[MagicMock, None, None]:
yield mock_bleak_scanner_start 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 @pytest.fixture
def mock_bluetooth( def mock_bluetooth(
mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None mock_bleak_scanner_start: MagicMock, mock_bluetooth_adapters: None

View File

@ -1,6 +1,5 @@
"""Test the frame helper.""" """Test the frame helper."""
from collections.abc import Generator
from unittest.mock import ANY, Mock, patch from unittest.mock import ANY, Mock, patch
import pytest import pytest
@ -9,33 +8,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import frame 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( async def test_extract_frame_integration(
caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
) -> None: ) -> None:
@ -174,3 +146,8 @@ async def test_report_missing_integration_frame(
frame.report(what, error_if_core=False) frame.report(what, error_if_core=False)
assert what in caplog.text assert what in caplog.text
assert caplog.text.count(what) == 1 assert caplog.text.count(what) == 1
caplog.clear()
frame.report(what, error_if_core=False, log_custom_component_only=True)
assert caplog.text == ""

View File

@ -1,6 +1,6 @@
"""Test to verify that we can load components.""" """Test to verify that we can load components."""
import asyncio import asyncio
from unittest.mock import patch from unittest.mock import Mock, patch
import pytest import pytest
@ -8,6 +8,7 @@ from homeassistant import loader
from homeassistant.components import http, hue from homeassistant.components import http, hue
from homeassistant.components.hue import light as hue_light from homeassistant.components.hue import light as hue_light
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import frame
from .common import MockModule, async_get_persistent_notifications, mock_integration from .common import MockModule, async_get_persistent_notifications, mock_integration
@ -287,6 +288,7 @@ async def test_get_integration_custom_component(
) -> None: ) -> None:
"""Test resolving integration.""" """Test resolving integration."""
integration = await loader.async_get_integration(hass, "test_package") integration = await loader.async_get_integration(hass, "test_package")
assert integration.get_component().DOMAIN == "test_package" assert integration.get_component().DOMAIN == "test_package"
assert integration.name == "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 # Verify that we are able to load the file with absolute path
import tests.testing_config.check_config_not_in_path # noqa: F401 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