Install discovery requirements if used (#29795)

* Install discovery requirements if used

* Update loader.py

* Fix types
This commit is contained in:
Paulus Schoutsen 2019-12-10 09:24:49 +01:00 committed by Pascal Vizeli
parent 0ed6a434f8
commit 27244e29c4
6 changed files with 108 additions and 25 deletions

View file

@ -7,6 +7,7 @@ from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@ -169,10 +170,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_ssdp(self, discovery_info):
"""Handle a discovered deCONZ bridge."""
# Import it here, because only now do we know ssdp integration loaded.
# pylint: disable=import-outside-toplevel
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
return self.async_abort(reason="not_deconz_bridge")

View file

@ -8,6 +8,7 @@ import async_timeout
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@ -134,9 +135,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not.
"""
# pylint: disable=import-outside-toplevel
from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME
if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
return self.async_abort(reason="not_hue_bridge")

View file

@ -195,22 +195,45 @@ class Integration:
hass: "HomeAssistant",
pkg_path: str,
file_path: pathlib.Path,
manifest: Dict,
manifest: Dict[str, Any],
):
"""Initialize an integration."""
self.hass = hass
self.pkg_path = pkg_path
self.file_path = file_path
self.name: str = manifest["name"]
self.domain: str = manifest["domain"]
self.dependencies: List[str] = manifest["dependencies"]
self.after_dependencies: Optional[List[str]] = manifest.get(
"after_dependencies"
)
self.requirements: List[str] = manifest["requirements"]
self.config_flow: bool = manifest.get("config_flow", False)
self.manifest = manifest
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
@property
def name(self) -> str:
"""Return name."""
return cast(str, self.manifest["name"])
@property
def domain(self) -> str:
"""Return domain."""
return cast(str, self.manifest["domain"])
@property
def dependencies(self) -> List[str]:
"""Return dependencies."""
return cast(List[str], self.manifest.get("dependencies", []))
@property
def after_dependencies(self) -> List[str]:
"""Return after_dependencies."""
return cast(List[str], self.manifest.get("after_dependencies", []))
@property
def requirements(self) -> List[str]:
"""Return requirements."""
return cast(List[str], self.manifest.get("requirements", []))
@property
def config_flow(self) -> bool:
"""Return config_flow."""
return cast(bool, self.manifest.get("config_flow", False))
@property
def is_built_in(self) -> bool:
"""Test if package is a built-in integration."""

View file

@ -3,7 +3,7 @@ import asyncio
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Set
from typing import Any, Dict, List, Optional, Set, Iterable
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -15,6 +15,10 @@ DATA_PKG_CACHE = "pkg_cache"
CONSTRAINT_FILE = "package_constraints.txt"
PROGRESS_FILE = ".pip_progress"
_LOGGER = logging.getLogger(__name__)
DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = {
"ssdp": ("ssdp",),
"zeroconf": ("zeroconf", "homekit"),
}
class RequirementsNotFound(HomeAssistantError):
@ -30,7 +34,7 @@ class RequirementsNotFound(HomeAssistantError):
async def async_get_integration_with_requirements(
hass: HomeAssistant, domain: str, done: Set[str] = None
) -> Integration:
"""Get an integration with installed requirements.
"""Get an integration with all requirements installed, including the dependencies.
This can raise IntegrationNotFound if manifest or integration
is invalid, RequirementNotFound if there was some type of
@ -53,10 +57,18 @@ async def async_get_integration_with_requirements(
deps_to_check = [
dep
for dep in integration.dependencies + (integration.after_dependencies or [])
for dep in integration.dependencies + integration.after_dependencies
if dep not in done
]
for check_domain, to_check in DISCOVERY_INTEGRATIONS.items():
if (
check_domain not in done
and check_domain not in deps_to_check
and any(check in integration.manifest for check in to_check)
):
deps_to_check.append(check_domain)
if deps_to_check:
await asyncio.gather(
*[

View file

@ -3,6 +3,8 @@ import pathlib
import re
from typing import Dict, Set
from homeassistant.requirements import DISCOVERY_INTEGRATIONS
from .model import Integration
@ -49,7 +51,6 @@ ALLOWED_USED_COMPONENTS = {
"system_log",
"person",
# Discovery
"ssdp",
"discovery",
# Other
"mjpeg", # base class, has no reqs or component to load.
@ -93,6 +94,13 @@ def validate_dependencies(integration: Integration):
referenced -= set(integration.manifest["dependencies"])
referenced -= set(integration.manifest.get("after_dependencies", []))
# Discovery requirements are ok if referenced in manifest
for check_domain, to_check in DISCOVERY_INTEGRATIONS.items():
if check_domain in referenced and any(
check in integration.manifest for check in to_check
):
referenced.remove(check_domain)
if referenced:
for domain in sorted(referenced):
if (

View file

@ -2,10 +2,9 @@
import os
from pathlib import Path
from unittest.mock import call, patch
import pytest
from pytest import raises
from homeassistant import setup
from homeassistant import setup, loader
from homeassistant.requirements import (
CONSTRAINT_FILE,
PROGRESS_FILE,
@ -15,7 +14,12 @@ from homeassistant.requirements import (
async_process_requirements,
)
from tests.common import MockModule, get_test_home_assistant, mock_integration
from tests.common import (
MockModule,
get_test_home_assistant,
mock_coro,
mock_integration,
)
def env_without_wheel_links():
@ -104,7 +108,7 @@ async def test_install_missing_package(hass):
with patch(
"homeassistant.util.package.install_package", return_value=False
) as mock_inst:
with raises(RequirementsNotFound):
with pytest.raises(RequirementsNotFound):
await async_process_requirements(hass, "test_component", ["hello==1.0.0"])
assert len(mock_inst.mock_calls) == 1
@ -222,3 +226,44 @@ async def test_progress_lock(hass):
_install(hass, "hello", kwargs)
assert not progress_path.exists()
async def test_discovery_requirements_ssdp(hass):
"""Test that we load discovery requirements."""
hass.config.skip_pip = False
ssdp = await loader.async_get_integration(hass, "ssdp")
mock_integration(
hass, MockModule("ssdp_comp", partial_manifest={"ssdp": [{"st": "roku:ecp"}]})
)
with patch(
"homeassistant.requirements.async_process_requirements",
side_effect=lambda _, _2, _3: mock_coro(),
) as mock_process:
await async_get_integration_with_requirements(hass, "ssdp_comp")
assert len(mock_process.mock_calls) == 1
assert mock_process.mock_calls[0][1][2] == ssdp.requirements
@pytest.mark.parametrize(
"partial_manifest",
[{"zeroconf": ["_googlecast._tcp.local."]}, {"homekit": {"models": ["LIFX"]}}],
)
async def test_discovery_requirements_zeroconf(hass, partial_manifest):
"""Test that we load discovery requirements."""
hass.config.skip_pip = False
zeroconf = await loader.async_get_integration(hass, "zeroconf")
mock_integration(
hass, MockModule("comp", partial_manifest=partial_manifest),
)
with patch(
"homeassistant.requirements.async_process_requirements",
side_effect=lambda _, _2, _3: mock_coro(),
) as mock_process:
await async_get_integration_with_requirements(hass, "comp")
assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http
assert mock_process.mock_calls[0][1][2] == zeroconf.requirements