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 <marhje52@gmail.com>
This commit is contained in:
Erik Montnemery 2024-03-06 13:56:47 +01:00 committed by GitHub
parent 780428fde6
commit 807c3ca76b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 115 additions and 5 deletions

View file

@ -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_COMPONENTS = "components"
DATA_INTEGRATIONS = "integrations" DATA_INTEGRATIONS = "integrations"
DATA_MISSING_PLATFORMS = "missing_platforms" DATA_MISSING_PLATFORMS = "missing_platforms"
@ -643,6 +656,7 @@ class Integration:
return integration return integration
_LOGGER.warning(CUSTOM_WARNING, integration.domain) _LOGGER.warning(CUSTOM_WARNING, integration.domain)
if integration.version is None: if integration.version is None:
_LOGGER.error( _LOGGER.error(
( (
@ -679,6 +693,21 @@ class Integration:
integration.version, integration.version,
) )
return None 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 integration
return None return None
@ -1210,6 +1239,20 @@ class Integration:
return f"<Integration {self.domain}: {self.pkg_path}>" return f"<Integration {self.domain}: {self.pkg_path}>"
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( def _resolve_integrations_from_root(
hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str] hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str]
) -> dict[str, Integration]: ) -> dict[str, Integration]:
@ -1565,6 +1608,7 @@ def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool:
def async_get_issue_tracker( def async_get_issue_tracker(
hass: HomeAssistant | None, hass: HomeAssistant | None,
*, *,
integration: Integration | None = None,
integration_domain: str | None = None, integration_domain: str | None = None,
module: str | None = None, module: str | None = None,
) -> str | None: ) -> str | None:
@ -1572,19 +1616,23 @@ def async_get_issue_tracker(
issue_tracker = ( issue_tracker = (
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" "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 # If we know nothing about the entity, suggest opening an issue on HA core
return issue_tracker return issue_tracker
if hass and integration_domain: if not integration and (hass and integration_domain):
with suppress(IntegrationNotLoaded): with suppress(IntegrationNotLoaded):
integration = async_get_loaded_integration(hass, integration_domain) integration = async_get_loaded_integration(hass, integration_domain)
if not integration.is_built_in:
if integration and not integration.is_built_in:
return integration.issue_tracker return integration.issue_tracker
if module and "custom_components" in module: if module and "custom_components" in module:
return None return None
if integration:
integration_domain = integration.domain
if integration_domain: if integration_domain:
issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22" issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22"
return issue_tracker return issue_tracker
@ -1594,15 +1642,21 @@ def async_get_issue_tracker(
def async_suggest_report_issue( def async_suggest_report_issue(
hass: HomeAssistant | None, hass: HomeAssistant | None,
*, *,
integration: Integration | None = None,
integration_domain: str | None = None, integration_domain: str | None = None,
module: str | None = None, module: str | None = None,
) -> str: ) -> str:
"""Generate a blurb asking the user to file a bug report.""" """Generate a blurb asking the user to file a bug report."""
issue_tracker = async_get_issue_tracker( 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 not issue_tracker:
if integration:
integration_domain = integration.domain
if not integration_domain: if not integration_domain:
return "report it to the custom integration author" return "report it to the custom integration author"
return ( return (

View file

@ -6,6 +6,7 @@ import threading
from typing import Any from typing import Any
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
from awesomeversion import AwesomeVersion
import pytest import pytest
from homeassistant import loader from homeassistant import loader
@ -167,6 +168,57 @@ async def test_custom_integration_version_not_valid(
) in caplog.text ) 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: async def test_get_integration(hass: HomeAssistant) -> None:
"""Test resolving integration.""" """Test resolving integration."""
with pytest.raises(loader.IntegrationNotLoaded): with pytest.raises(loader.IntegrationNotLoaded):

View file

@ -0,0 +1,4 @@
{
"domain": "test_blocked_version",
"version": "1.0.0"
}