Skip to content

Commit

Permalink
Add custom integration block list (#112481)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
2 people authored and frenck committed Mar 6, 2024
1 parent 3b63719 commit 8b2f403
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 5 deletions.
65 changes: 60 additions & 5 deletions homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@

_LOGGER = logging.getLogger(__name__)


@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"
Expand Down Expand Up @@ -599,6 +613,7 @@ def resolve_from_root(
return integration

_LOGGER.warning(CUSTOM_WARNING, integration.domain)

if integration.version is None:
_LOGGER.error(
(
Expand Down Expand Up @@ -635,6 +650,21 @@ def resolve_from_root(
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
Expand Down Expand Up @@ -1032,6 +1062,20 @@ def __repr__(self) -> str:
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(
hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str]
) -> dict[str, Integration]:
Expand Down Expand Up @@ -1387,26 +1431,31 @@ 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:
"""Return a URL for an integration's 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
Expand All @@ -1416,15 +1465,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 (
Expand Down
52 changes: 52 additions & 0 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any
from unittest.mock import MagicMock, Mock, patch

from awesomeversion import AwesomeVersion
import pytest

from homeassistant import loader
Expand Down Expand Up @@ -163,6 +164,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"domain": "test_blocked_version",
"version": "1.0.0"
}

0 comments on commit 8b2f403

Please sign in to comment.