Install discovery requirements if used (#29795)
* Install discovery requirements if used * Update loader.py * Fix typespull/29824/head
parent
0ed6a434f8
commit
27244e29c4
|
@ -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")
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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(
|
||||
*[
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue