From 807c3ca76b424f71697f085562db00dc4e82c955 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 6 Mar 2024 13:56:47 +0100 Subject: [PATCH] Add custom integration block list (#112481) * Add custom integration block list * Fix typo * Add version condition * Add block reason, simplify blocked versions, add tests * Change logic for OK versions * Add link to custom integration's issue tracker * Add missing file --------- Co-authored-by: Martin Hjelmare --- homeassistant/loader.py | 64 +++++++++++++++++-- tests/test_loader.py | 52 +++++++++++++++ .../test_blocked_version/manifest.json | 4 ++ 3 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 tests/testing_config/custom_components/test_blocked_version/manifest.json diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 06bac608aec..ebea95eaa75 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -82,6 +82,19 @@ BASE_PRELOAD_PLATFORMS = [ ] +@dataclass +class BlockedIntegration: + """Blocked custom integration details.""" + + lowest_good_version: AwesomeVersion | None + reason: str + + +BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { + # Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464 + "start_time": BlockedIntegration(None, "breaks Home Assistant") +} + DATA_COMPONENTS = "components" DATA_INTEGRATIONS = "integrations" DATA_MISSING_PLATFORMS = "missing_platforms" @@ -643,6 +656,7 @@ class Integration: return integration _LOGGER.warning(CUSTOM_WARNING, integration.domain) + if integration.version is None: _LOGGER.error( ( @@ -679,6 +693,21 @@ class Integration: integration.version, ) return None + + if blocked := BLOCKED_CUSTOM_INTEGRATIONS.get(integration.domain): + if _version_blocked(integration.version, blocked): + _LOGGER.error( + ( + "Version %s of custom integration '%s' %s and was blocked " + "from loading, please %s" + ), + integration.version, + integration.domain, + blocked.reason, + async_suggest_report_issue(None, integration=integration), + ) + return None + return integration return None @@ -1210,6 +1239,20 @@ class Integration: return f"" +def _version_blocked( + integration_version: AwesomeVersion, + blocked_integration: BlockedIntegration, +) -> bool: + """Return True if the integration version is blocked.""" + if blocked_integration.lowest_good_version is None: + return True + + if integration_version >= blocked_integration.lowest_good_version: + return False + + return True + + def _resolve_integrations_from_root( hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str] ) -> dict[str, Integration]: @@ -1565,6 +1608,7 @@ def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool: def async_get_issue_tracker( hass: HomeAssistant | None, *, + integration: Integration | None = None, integration_domain: str | None = None, module: str | None = None, ) -> str | None: @@ -1572,19 +1616,23 @@ def async_get_issue_tracker( issue_tracker = ( "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" ) - if not integration_domain and not module: + if not integration and not integration_domain and not module: # If we know nothing about the entity, suggest opening an issue on HA core return issue_tracker - if hass and integration_domain: + if not integration and (hass and integration_domain): with suppress(IntegrationNotLoaded): integration = async_get_loaded_integration(hass, integration_domain) - if not integration.is_built_in: - return integration.issue_tracker + + if integration and not integration.is_built_in: + return integration.issue_tracker if module and "custom_components" in module: return None + if integration: + integration_domain = integration.domain + if integration_domain: issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22" return issue_tracker @@ -1594,15 +1642,21 @@ def async_get_issue_tracker( def async_suggest_report_issue( hass: HomeAssistant | None, *, + integration: Integration | None = None, integration_domain: str | None = None, module: str | None = None, ) -> str: """Generate a blurb asking the user to file a bug report.""" issue_tracker = async_get_issue_tracker( - hass, integration_domain=integration_domain, module=module + hass, + integration=integration, + integration_domain=integration_domain, + module=module, ) if not issue_tracker: + if integration: + integration_domain = integration.domain if not integration_domain: return "report it to the custom integration author" return ( diff --git a/tests/test_loader.py b/tests/test_loader.py index 22379b340d4..6fa4de1da9c 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -6,6 +6,7 @@ import threading from typing import Any from unittest.mock import MagicMock, Mock, patch +from awesomeversion import AwesomeVersion import pytest from homeassistant import loader @@ -167,6 +168,57 @@ async def test_custom_integration_version_not_valid( ) in caplog.text +@pytest.mark.parametrize( + "blocked_versions", + [ + loader.BlockedIntegration(None, "breaks Home Assistant"), + loader.BlockedIntegration(AwesomeVersion("2.0.0"), "breaks Home Assistant"), + ], +) +async def test_custom_integration_version_blocked( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + blocked_versions, +) -> None: + """Test that we log a warning when custom integrations have a blocked version.""" + with patch.dict( + loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions} + ): + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_blocked_version") + + assert ( + "Version 1.0.0 of custom integration 'test_blocked_version' breaks" + " Home Assistant and was blocked from loading, please report it to the" + " author of the 'test_blocked_version' custom integration" + ) in caplog.text + + +@pytest.mark.parametrize( + "blocked_versions", + [ + loader.BlockedIntegration(AwesomeVersion("0.9.9"), "breaks Home Assistant"), + loader.BlockedIntegration(AwesomeVersion("1.0.0"), "breaks Home Assistant"), + ], +) +async def test_custom_integration_version_not_blocked( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + blocked_versions, +) -> None: + """Test that we log a warning when custom integrations have a blocked version.""" + with patch.dict( + loader.BLOCKED_CUSTOM_INTEGRATIONS, {"test_blocked_version": blocked_versions} + ): + await loader.async_get_integration(hass, "test_blocked_version") + + assert ( + "Version 1.0.0 of custom integration 'test_blocked_version'" + ) not in caplog.text + + async def test_get_integration(hass: HomeAssistant) -> None: """Test resolving integration.""" with pytest.raises(loader.IntegrationNotLoaded): diff --git a/tests/testing_config/custom_components/test_blocked_version/manifest.json b/tests/testing_config/custom_components/test_blocked_version/manifest.json new file mode 100644 index 00000000000..8359c4fe510 --- /dev/null +++ b/tests/testing_config/custom_components/test_blocked_version/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_blocked_version", + "version": "1.0.0" +}