mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 04:37:06 +00:00
Improve after_dependencies handling (#36898)
This commit is contained in:
parent
93272e3083
commit
5642027ffb
@ -1,6 +1,7 @@
|
||||
"""Provide methods to bootstrap a Home Assistant instance."""
|
||||
import asyncio
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
@ -20,7 +21,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import DATA_SETUP, DATA_SETUP_STARTED, async_setup_component
|
||||
from homeassistant.setup import (
|
||||
DATA_SETUP,
|
||||
DATA_SETUP_STARTED,
|
||||
async_set_domains_to_be_loaded,
|
||||
async_setup_component,
|
||||
)
|
||||
from homeassistant.util.logging import async_activate_log_queue_handler
|
||||
from homeassistant.util.package import async_get_user_site, is_virtual_env
|
||||
from homeassistant.util.yaml import clear_secret_cache
|
||||
@ -36,10 +42,16 @@ LOG_SLOW_STARTUP_INTERVAL = 60
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"ptvsd"}
|
||||
CORE_INTEGRATIONS = ("homeassistant", "persistent_notification")
|
||||
LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
LOGGING_INTEGRATIONS = {
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Error logging
|
||||
"system_log",
|
||||
"sentry",
|
||||
# To record data
|
||||
"recorder",
|
||||
}
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
# To make sure we forward data to other instances
|
||||
"mqtt_eventstream",
|
||||
# To provide account link implementations
|
||||
@ -330,77 +342,130 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]:
|
||||
return domains
|
||||
|
||||
|
||||
async def _async_log_pending_setups(
|
||||
domains: Set[str], setup_started: Dict[str, datetime]
|
||||
) -> None:
|
||||
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
|
||||
while True:
|
||||
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
|
||||
remaining = [domain for domain in domains if domain in setup_started]
|
||||
|
||||
if remaining:
|
||||
_LOGGER.info(
|
||||
"Waiting on integrations to complete setup: %s", ", ".join(remaining),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_multi_components(
|
||||
hass: core.HomeAssistant,
|
||||
domains: Set[str],
|
||||
config: Dict[str, Any],
|
||||
setup_started: Dict[str, datetime],
|
||||
) -> None:
|
||||
"""Set up multiple domains. Log on failure."""
|
||||
futures = {
|
||||
domain: hass.async_create_task(async_setup_component(hass, domain, config))
|
||||
for domain in domains
|
||||
}
|
||||
log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started))
|
||||
await asyncio.wait(futures.values())
|
||||
log_task.cancel()
|
||||
errors = [domain for domain in domains if futures[domain].exception()]
|
||||
for domain in errors:
|
||||
exception = futures[domain].exception()
|
||||
assert exception is not None
|
||||
_LOGGER.error(
|
||||
"Error setting up integration %s - received exception",
|
||||
domain,
|
||||
exc_info=(type(exception), exception, exception.__traceback__),
|
||||
)
|
||||
|
||||
|
||||
async def _async_set_up_integrations(
|
||||
hass: core.HomeAssistant, config: Dict[str, Any]
|
||||
) -> None:
|
||||
"""Set up all the integrations."""
|
||||
|
||||
setup_started = hass.data[DATA_SETUP_STARTED] = {}
|
||||
domains_to_setup = _get_domains(hass, config)
|
||||
|
||||
async def async_setup_multi_components(domains: Set[str]) -> None:
|
||||
"""Set up multiple domains. Log on failure."""
|
||||
# Resolve all dependencies so we know all integrations
|
||||
# that will have to be loaded and start rightaway
|
||||
integration_cache: Dict[str, loader.Integration] = {}
|
||||
to_resolve = domains_to_setup
|
||||
while to_resolve:
|
||||
old_to_resolve = to_resolve
|
||||
to_resolve = set()
|
||||
|
||||
async def _async_log_pending_setups() -> None:
|
||||
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
|
||||
while True:
|
||||
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
|
||||
remaining = [domain for domain in domains if domain in setup_started]
|
||||
|
||||
if remaining:
|
||||
_LOGGER.info(
|
||||
"Waiting on integrations to complete setup: %s",
|
||||
", ".join(remaining),
|
||||
)
|
||||
|
||||
futures = {
|
||||
domain: hass.async_create_task(async_setup_component(hass, domain, config))
|
||||
for domain in domains
|
||||
}
|
||||
log_task = asyncio.create_task(_async_log_pending_setups())
|
||||
await asyncio.wait(futures.values())
|
||||
log_task.cancel()
|
||||
errors = [domain for domain in domains if futures[domain].exception()]
|
||||
for domain in errors:
|
||||
exception = futures[domain].exception()
|
||||
assert exception is not None
|
||||
_LOGGER.error(
|
||||
"Error setting up integration %s - received exception",
|
||||
domain,
|
||||
exc_info=(type(exception), exception, exception.__traceback__),
|
||||
integrations_to_process = [
|
||||
int_or_exc
|
||||
for int_or_exc in await asyncio.gather(
|
||||
*(
|
||||
loader.async_get_integration(hass, domain)
|
||||
for domain in old_to_resolve
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
if isinstance(int_or_exc, loader.Integration)
|
||||
]
|
||||
resolve_dependencies_tasks = [
|
||||
itg.resolve_dependencies()
|
||||
for itg in integrations_to_process
|
||||
if not itg.all_dependencies_resolved
|
||||
]
|
||||
|
||||
domains = _get_domains(hass, config)
|
||||
if resolve_dependencies_tasks:
|
||||
await asyncio.gather(*resolve_dependencies_tasks)
|
||||
|
||||
for itg in integrations_to_process:
|
||||
integration_cache[itg.domain] = itg
|
||||
|
||||
for dep in itg.all_dependencies:
|
||||
if dep in domains_to_setup:
|
||||
continue
|
||||
|
||||
domains_to_setup.add(dep)
|
||||
to_resolve.add(dep)
|
||||
|
||||
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
|
||||
|
||||
logging_domains = domains_to_setup & LOGGING_INTEGRATIONS
|
||||
|
||||
# Load logging as soon as possible
|
||||
if logging_domains:
|
||||
_LOGGER.info("Setting up logging: %s", logging_domains)
|
||||
await async_setup_multi_components(hass, logging_domains, config, setup_started)
|
||||
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
debuggers = domains & DEBUGGER_INTEGRATIONS
|
||||
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
|
||||
|
||||
if debuggers:
|
||||
_LOGGER.debug("Starting up debuggers %s", debuggers)
|
||||
await async_setup_multi_components(debuggers)
|
||||
domains -= DEBUGGER_INTEGRATIONS
|
||||
_LOGGER.debug("Setting up debuggers: %s", debuggers)
|
||||
await async_setup_multi_components(hass, debuggers, config, setup_started)
|
||||
|
||||
# Resolve all dependencies of all components so we can find the logging
|
||||
# and integrations that need faster initialization.
|
||||
resolved_domains_task = asyncio.gather(
|
||||
*(loader.async_component_dependencies(hass, domain) for domain in domains),
|
||||
return_exceptions=True,
|
||||
)
|
||||
# calculate what components to setup in what stage
|
||||
stage_1_domains = set()
|
||||
|
||||
# Finish resolving domains
|
||||
for dep_domains in await resolved_domains_task:
|
||||
# Result is either a set or an exception. We ignore exceptions
|
||||
# It will be properly handled during setup of the domain.
|
||||
if isinstance(dep_domains, set):
|
||||
domains.update(dep_domains)
|
||||
# Find all dependencies of any dependency of any stage 1 integration that
|
||||
# we plan on loading and promote them to stage 1
|
||||
deps_promotion = STAGE_1_INTEGRATIONS
|
||||
while deps_promotion:
|
||||
old_deps_promotion = deps_promotion
|
||||
deps_promotion = set()
|
||||
|
||||
# setup components
|
||||
logging_domains = domains & LOGGING_INTEGRATIONS
|
||||
stage_1_domains = domains & STAGE_1_INTEGRATIONS
|
||||
stage_2_domains = domains - logging_domains - stage_1_domains
|
||||
for domain in old_deps_promotion:
|
||||
if domain not in domains_to_setup or domain in stage_1_domains:
|
||||
continue
|
||||
|
||||
if logging_domains:
|
||||
_LOGGER.info("Setting up %s", logging_domains)
|
||||
stage_1_domains.add(domain)
|
||||
|
||||
await async_setup_multi_components(logging_domains)
|
||||
dep_itg = integration_cache.get(domain)
|
||||
|
||||
if dep_itg is None:
|
||||
continue
|
||||
|
||||
deps_promotion.update(dep_itg.all_dependencies)
|
||||
|
||||
stage_2_domains = domains_to_setup - logging_domains - debuggers - stage_1_domains
|
||||
|
||||
# Kick off loading the registries. They don't need to be awaited.
|
||||
asyncio.gather(
|
||||
@ -409,49 +474,17 @@ async def _async_set_up_integrations(
|
||||
hass.helpers.area_registry.async_get_registry(),
|
||||
)
|
||||
|
||||
# Start setup
|
||||
if stage_1_domains:
|
||||
_LOGGER.info("Setting up %s", stage_1_domains)
|
||||
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||
await async_setup_multi_components(hass, stage_1_domains, config, setup_started)
|
||||
|
||||
await async_setup_multi_components(stage_1_domains)
|
||||
# Enables after dependencies
|
||||
async_set_domains_to_be_loaded(hass, stage_1_domains | stage_2_domains)
|
||||
|
||||
# Load all integrations
|
||||
after_dependencies: Dict[str, Set[str]] = {}
|
||||
|
||||
for int_or_exc in await asyncio.gather(
|
||||
*(loader.async_get_integration(hass, domain) for domain in stage_2_domains),
|
||||
return_exceptions=True,
|
||||
):
|
||||
# Exceptions are handled in async_setup_component.
|
||||
if isinstance(int_or_exc, loader.Integration) and int_or_exc.after_dependencies:
|
||||
after_dependencies[int_or_exc.domain] = set(int_or_exc.after_dependencies)
|
||||
|
||||
last_load = None
|
||||
while stage_2_domains:
|
||||
domains_to_load = set()
|
||||
|
||||
for domain in stage_2_domains:
|
||||
after_deps = after_dependencies.get(domain)
|
||||
# Load if integration has no after_dependencies or they are
|
||||
# all loaded
|
||||
if not after_deps or not after_deps - hass.config.components:
|
||||
domains_to_load.add(domain)
|
||||
|
||||
if not domains_to_load or domains_to_load == last_load:
|
||||
break
|
||||
|
||||
_LOGGER.debug("Setting up %s", domains_to_load)
|
||||
|
||||
await async_setup_multi_components(domains_to_load)
|
||||
|
||||
last_load = domains_to_load
|
||||
stage_2_domains -= domains_to_load
|
||||
|
||||
# These are stage 2 domains that never have their after_dependencies
|
||||
# satisfied.
|
||||
if stage_2_domains:
|
||||
_LOGGER.debug("Final set up: %s", stage_2_domains)
|
||||
|
||||
await async_setup_multi_components(stage_2_domains)
|
||||
_LOGGER.info("Setting up stage 2: %s", stage_2_domains)
|
||||
await async_setup_multi_components(hass, stage_2_domains, config, setup_started)
|
||||
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
|
@ -3,7 +3,6 @@
|
||||
"name": "Auth",
|
||||
"documentation": "https://www.home-assistant.io/integrations/auth",
|
||||
"dependencies": ["http"],
|
||||
"after_dependencies": ["onboarding"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
@ -203,6 +203,14 @@ class Integration:
|
||||
self.file_path = file_path
|
||||
self.manifest = manifest
|
||||
manifest["is_built_in"] = self.is_built_in
|
||||
|
||||
if self.dependencies:
|
||||
self._all_dependencies_resolved: Optional[bool] = None
|
||||
self._all_dependencies: Optional[Set[str]] = None
|
||||
else:
|
||||
self._all_dependencies_resolved = True
|
||||
self._all_dependencies = set()
|
||||
|
||||
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
|
||||
|
||||
@property
|
||||
@ -255,6 +263,49 @@ class Integration:
|
||||
"""Test if package is a built-in integration."""
|
||||
return self.pkg_path.startswith(PACKAGE_BUILTIN)
|
||||
|
||||
@property
|
||||
def all_dependencies(self) -> Set[str]:
|
||||
"""Return all dependencies including sub-dependencies."""
|
||||
if self._all_dependencies is None:
|
||||
raise RuntimeError("Dependencies not resolved!")
|
||||
|
||||
return self._all_dependencies
|
||||
|
||||
@property
|
||||
def all_dependencies_resolved(self) -> bool:
|
||||
"""Return if all dependencies have been resolved."""
|
||||
return self._all_dependencies_resolved is not None
|
||||
|
||||
async def resolve_dependencies(self) -> bool:
|
||||
"""Resolve all dependencies."""
|
||||
if self._all_dependencies_resolved is not None:
|
||||
return self._all_dependencies_resolved
|
||||
|
||||
try:
|
||||
dependencies = await _async_component_dependencies(
|
||||
self.hass, self.domain, self, set(), set()
|
||||
)
|
||||
dependencies.discard(self.domain)
|
||||
self._all_dependencies = dependencies
|
||||
self._all_dependencies_resolved = True
|
||||
except IntegrationNotFound as err:
|
||||
_LOGGER.error(
|
||||
"Unable to resolve dependencies for %s: we are unable to resolve (sub)dependency %s",
|
||||
self.domain,
|
||||
err.domain,
|
||||
)
|
||||
self._all_dependencies_resolved = False
|
||||
except CircularDependency as err:
|
||||
_LOGGER.error(
|
||||
"Unable to resolve dependencies for %s: it contains a circular dependency: %s -> %s",
|
||||
self.domain,
|
||||
err.from_domain,
|
||||
err.to_domain,
|
||||
)
|
||||
self._all_dependencies_resolved = False
|
||||
|
||||
return self._all_dependencies_resolved
|
||||
|
||||
def get_component(self) -> ModuleType:
|
||||
"""Return the component."""
|
||||
cache = self.hass.data.setdefault(DATA_COMPONENTS, {})
|
||||
@ -488,23 +539,18 @@ def bind_hass(func: CALLABLE_T) -> CALLABLE_T:
|
||||
return func
|
||||
|
||||
|
||||
async def async_component_dependencies(hass: "HomeAssistant", domain: str) -> Set[str]:
|
||||
"""Return all dependencies and subdependencies of components.
|
||||
|
||||
Raises CircularDependency if a circular dependency is found.
|
||||
"""
|
||||
return await _async_component_dependencies(hass, domain, set(), set())
|
||||
|
||||
|
||||
async def _async_component_dependencies(
|
||||
hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set[str]
|
||||
hass: "HomeAssistant",
|
||||
start_domain: str,
|
||||
integration: Integration,
|
||||
loaded: Set[str],
|
||||
loading: Set[str],
|
||||
) -> Set[str]:
|
||||
"""Recursive function to get component dependencies.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
integration = await async_get_integration(hass, domain)
|
||||
|
||||
domain = integration.domain
|
||||
loading.add(domain)
|
||||
|
||||
for dependency_domain in integration.dependencies:
|
||||
@ -516,11 +562,19 @@ async def _async_component_dependencies(
|
||||
if dependency_domain in loading:
|
||||
raise CircularDependency(domain, dependency_domain)
|
||||
|
||||
dep_loaded = await _async_component_dependencies(
|
||||
hass, dependency_domain, loaded, loading
|
||||
)
|
||||
loaded.add(dependency_domain)
|
||||
|
||||
loaded.update(dep_loaded)
|
||||
dep_integration = await async_get_integration(hass, dependency_domain)
|
||||
|
||||
if start_domain in dep_integration.after_dependencies:
|
||||
raise CircularDependency(start_domain, dependency_domain)
|
||||
|
||||
if dep_integration.dependencies:
|
||||
dep_loaded = await _async_component_dependencies(
|
||||
hass, start_domain, dep_integration, loaded, loading
|
||||
)
|
||||
|
||||
loaded.update(dep_loaded)
|
||||
|
||||
loaded.add(domain)
|
||||
loading.remove(domain)
|
||||
|
@ -3,7 +3,7 @@ import asyncio
|
||||
import logging.handlers
|
||||
from timeit import default_timer as timer
|
||||
from types import ModuleType
|
||||
from typing import Awaitable, Callable, List, Optional
|
||||
from typing import Awaitable, Callable, Optional, Set
|
||||
|
||||
from homeassistant import config as conf_util, core, loader, requirements
|
||||
from homeassistant.config import async_notify_setup_error
|
||||
@ -16,6 +16,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_COMPONENT = "component"
|
||||
|
||||
DATA_SETUP_DONE = "setup_done"
|
||||
DATA_SETUP_STARTED = "setup_started"
|
||||
DATA_SETUP = "setup_tasks"
|
||||
DATA_DEPS_REQS = "deps_reqs_processed"
|
||||
@ -26,6 +27,15 @@ SLOW_SETUP_WARNING = 10
|
||||
SLOW_SETUP_MAX_WAIT = 1800
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: Set[str]) -> None:
|
||||
"""Set domains that are going to be loaded from the config.
|
||||
|
||||
This will allow us to properly handle after_dependencies.
|
||||
"""
|
||||
hass.data[DATA_SETUP_DONE] = {domain: asyncio.Event() for domain in domains}
|
||||
|
||||
|
||||
def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool:
|
||||
"""Set up a component and all its dependencies."""
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
@ -52,26 +62,43 @@ async def async_setup_component(
|
||||
_async_setup_component(hass, domain, config)
|
||||
)
|
||||
|
||||
return await task # type: ignore
|
||||
try:
|
||||
return await task # type: ignore
|
||||
finally:
|
||||
if domain in hass.data.get(DATA_SETUP_DONE, {}):
|
||||
hass.data[DATA_SETUP_DONE].pop(domain).set()
|
||||
|
||||
|
||||
async def _async_process_dependencies(
|
||||
hass: core.HomeAssistant, config: ConfigType, name: str, dependencies: List[str]
|
||||
hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration
|
||||
) -> bool:
|
||||
"""Ensure all dependencies are set up."""
|
||||
tasks = [async_setup_component(hass, dep, config) for dep in dependencies]
|
||||
tasks = {
|
||||
dep: hass.loop.create_task(async_setup_component(hass, dep, config))
|
||||
for dep in integration.dependencies
|
||||
}
|
||||
|
||||
to_be_loaded = hass.data.get(DATA_SETUP_DONE, {})
|
||||
for dep in integration.after_dependencies:
|
||||
if dep in to_be_loaded and dep not in hass.config.components:
|
||||
tasks[dep] = hass.loop.create_task(to_be_loaded[dep].wait())
|
||||
|
||||
if not tasks:
|
||||
return True
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
_LOGGER.debug("Dependency %s will wait for %s", integration.domain, list(tasks))
|
||||
results = await asyncio.gather(*tasks.values())
|
||||
|
||||
failed = [dependencies[idx] for idx, res in enumerate(results) if not res]
|
||||
failed = [
|
||||
domain
|
||||
for idx, domain in enumerate(integration.dependencies)
|
||||
if not results[idx]
|
||||
]
|
||||
|
||||
if failed:
|
||||
_LOGGER.error(
|
||||
"Unable to set up dependencies of %s. Setup failed for dependencies: %s",
|
||||
name,
|
||||
integration.domain,
|
||||
", ".join(failed),
|
||||
)
|
||||
|
||||
@ -99,22 +126,7 @@ async def _async_setup_component(
|
||||
return False
|
||||
|
||||
# Validate all dependencies exist and there are no circular dependencies
|
||||
try:
|
||||
await loader.async_component_dependencies(hass, domain)
|
||||
except loader.IntegrationNotFound as err:
|
||||
_LOGGER.error(
|
||||
"Not setting up %s because we are unable to resolve (sub)dependency %s",
|
||||
domain,
|
||||
err.domain,
|
||||
)
|
||||
return False
|
||||
except loader.CircularDependency as err:
|
||||
_LOGGER.error(
|
||||
"Not setting up %s because it contains a circular dependency: %s -> %s",
|
||||
domain,
|
||||
err.from_domain,
|
||||
err.to_domain,
|
||||
)
|
||||
if not await integration.resolve_dependencies():
|
||||
return False
|
||||
|
||||
# Process requirements as soon as possible, so we can import the component
|
||||
@ -301,9 +313,7 @@ async def async_process_deps_reqs(
|
||||
elif integration.domain in processed:
|
||||
return
|
||||
|
||||
if integration.dependencies and not await _async_process_dependencies(
|
||||
hass, config, integration.domain, integration.dependencies
|
||||
):
|
||||
if not await _async_process_dependencies(hass, config, integration):
|
||||
raise HomeAssistantError("Could not set up all dependencies.")
|
||||
|
||||
if not hass.config.skip_pip and integration.requirements:
|
||||
|
@ -103,6 +103,7 @@ ALLOWED_USED_COMPONENTS = {
|
||||
"input_number",
|
||||
"input_select",
|
||||
"input_text",
|
||||
"onboarding",
|
||||
"persistent_notification",
|
||||
"person",
|
||||
"script",
|
||||
@ -253,7 +254,14 @@ def validate(integrations: Dict[str, Integration], config):
|
||||
continue
|
||||
|
||||
# check that all referenced dependencies exist
|
||||
after_deps = integration.manifest.get("after_dependencies", [])
|
||||
for dep in integration.manifest.get("dependencies", []):
|
||||
if dep in after_deps:
|
||||
integration.add_error(
|
||||
"dependencies",
|
||||
f"Dependency {dep} is both in dependencies and after_dependencies",
|
||||
)
|
||||
|
||||
if dep not in integrations:
|
||||
integration.add_error(
|
||||
"dependencies", f"Dependency {dep} does not exist"
|
||||
|
@ -991,6 +991,8 @@ def mock_integration(hass, module):
|
||||
hass.data.setdefault(loader.DATA_INTEGRATIONS, {})[module.DOMAIN] = integration
|
||||
hass.data.setdefault(loader.DATA_COMPONENTS, {})[module.DOMAIN] = module
|
||||
|
||||
return integration
|
||||
|
||||
|
||||
def mock_entity_platform(hass, platform_path, module):
|
||||
"""Mock a entity platform.
|
||||
|
@ -7,7 +7,7 @@ from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import bootstrap
|
||||
from homeassistant import bootstrap, core
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.util.dt as dt_util
|
||||
@ -16,9 +16,11 @@ from tests.async_mock import patch
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockModule,
|
||||
MockPlatform,
|
||||
flush_store,
|
||||
get_test_config_dir,
|
||||
mock_coro,
|
||||
mock_entity_platform,
|
||||
mock_integration,
|
||||
)
|
||||
|
||||
@ -81,7 +83,7 @@ async def test_core_failure_loads_safe_mode(hass, caplog):
|
||||
assert "group" not in hass.config.components
|
||||
|
||||
|
||||
async def test_setting_up_config(hass, caplog):
|
||||
async def test_setting_up_config(hass):
|
||||
"""Test we set up domains in config."""
|
||||
await bootstrap._async_set_up_integrations(
|
||||
hass, {"group hello": {}, "homeassistant": {}}
|
||||
@ -90,9 +92,8 @@ async def test_setting_up_config(hass, caplog):
|
||||
assert "group" in hass.config.components
|
||||
|
||||
|
||||
async def test_setup_after_deps_all_present(hass, caplog):
|
||||
async def test_setup_after_deps_all_present(hass):
|
||||
"""Test after_dependencies when all present."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
order = []
|
||||
|
||||
def gen_domain_setup(domain):
|
||||
@ -122,19 +123,115 @@ async def test_setup_after_deps_all_present(hass, caplog):
|
||||
),
|
||||
)
|
||||
|
||||
await bootstrap._async_set_up_integrations(
|
||||
hass, {"root": {}, "first_dep": {}, "second_dep": {}}
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.logger.async_setup", gen_domain_setup("logger")
|
||||
):
|
||||
await bootstrap._async_set_up_integrations(
|
||||
hass, {"root": {}, "first_dep": {}, "second_dep": {}, "logger": {}}
|
||||
)
|
||||
|
||||
assert "root" in hass.config.components
|
||||
assert "first_dep" in hass.config.components
|
||||
assert "second_dep" in hass.config.components
|
||||
assert order == ["root", "first_dep", "second_dep"]
|
||||
assert order == ["logger", "root", "first_dep", "second_dep"]
|
||||
|
||||
|
||||
async def test_setup_after_deps_not_trigger_load(hass, caplog):
|
||||
async def test_setup_after_deps_in_stage_1_ignored(hass):
|
||||
"""Test after_dependencies are ignored in stage 1."""
|
||||
# This test relies on this
|
||||
assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS
|
||||
order = []
|
||||
|
||||
def gen_domain_setup(domain):
|
||||
async def async_setup(hass, config):
|
||||
order.append(domain)
|
||||
return True
|
||||
|
||||
return async_setup
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
domain="normal_integration",
|
||||
async_setup=gen_domain_setup("normal_integration"),
|
||||
partial_manifest={"after_dependencies": ["an_after_dep"]},
|
||||
),
|
||||
)
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
domain="an_after_dep", async_setup=gen_domain_setup("an_after_dep"),
|
||||
),
|
||||
)
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
domain="cloud",
|
||||
async_setup=gen_domain_setup("cloud"),
|
||||
partial_manifest={"after_dependencies": ["normal_integration"]},
|
||||
),
|
||||
)
|
||||
|
||||
await bootstrap._async_set_up_integrations(
|
||||
hass, {"cloud": {}, "normal_integration": {}, "an_after_dep": {}}
|
||||
)
|
||||
|
||||
assert "normal_integration" in hass.config.components
|
||||
assert "cloud" in hass.config.components
|
||||
assert order == ["cloud", "an_after_dep", "normal_integration"]
|
||||
|
||||
|
||||
async def test_setup_after_deps_via_platform(hass):
|
||||
"""Test after_dependencies set up via platform."""
|
||||
order = []
|
||||
after_dep_event = asyncio.Event()
|
||||
|
||||
def gen_domain_setup(domain):
|
||||
async def async_setup(hass, config):
|
||||
if domain == "after_dep_of_platform_int":
|
||||
await after_dep_event.wait()
|
||||
|
||||
order.append(domain)
|
||||
return True
|
||||
|
||||
return async_setup
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
domain="after_dep_of_platform_int",
|
||||
async_setup=gen_domain_setup("after_dep_of_platform_int"),
|
||||
),
|
||||
)
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(
|
||||
domain="platform_int",
|
||||
async_setup=gen_domain_setup("platform_int"),
|
||||
partial_manifest={"after_dependencies": ["after_dep_of_platform_int"]},
|
||||
),
|
||||
)
|
||||
mock_entity_platform(hass, "light.platform_int", MockPlatform())
|
||||
|
||||
@core.callback
|
||||
def continue_loading(_):
|
||||
"""When light component loaded, continue other loading."""
|
||||
after_dep_event.set()
|
||||
|
||||
hass.bus.async_listen_once("component_loaded", continue_loading)
|
||||
|
||||
await bootstrap._async_set_up_integrations(
|
||||
hass, {"light": {"platform": "platform_int"}, "after_dep_of_platform_int": {}}
|
||||
)
|
||||
|
||||
assert "light" in hass.config.components
|
||||
assert "after_dep_of_platform_int" in hass.config.components
|
||||
assert "platform_int" in hass.config.components
|
||||
assert order == ["after_dep_of_platform_int", "platform_int"]
|
||||
|
||||
|
||||
async def test_setup_after_deps_not_trigger_load(hass):
|
||||
"""Test after_dependencies does not trigger loading it."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
order = []
|
||||
|
||||
def gen_domain_setup(domain):
|
||||
@ -169,12 +266,10 @@ async def test_setup_after_deps_not_trigger_load(hass, caplog):
|
||||
assert "root" in hass.config.components
|
||||
assert "first_dep" not in hass.config.components
|
||||
assert "second_dep" in hass.config.components
|
||||
assert order == ["root", "second_dep"]
|
||||
|
||||
|
||||
async def test_setup_after_deps_not_present(hass, caplog):
|
||||
async def test_setup_after_deps_not_present(hass):
|
||||
"""Test after_dependencies when referenced integration doesn't exist."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
order = []
|
||||
|
||||
def gen_domain_setup(domain):
|
||||
|
@ -13,27 +13,43 @@ async def test_component_dependencies(hass):
|
||||
"""Test if we can get the proper load order of components."""
|
||||
mock_integration(hass, MockModule("mod1"))
|
||||
mock_integration(hass, MockModule("mod2", ["mod1"]))
|
||||
mock_integration(hass, MockModule("mod3", ["mod2"]))
|
||||
mod_3 = mock_integration(hass, MockModule("mod3", ["mod2"]))
|
||||
|
||||
assert {"mod1", "mod2", "mod3"} == await loader.async_component_dependencies(
|
||||
hass, "mod3"
|
||||
assert {"mod1", "mod2", "mod3"} == await loader._async_component_dependencies(
|
||||
hass, "mod_3", mod_3, set(), set()
|
||||
)
|
||||
|
||||
# Create circular dependency
|
||||
mock_integration(hass, MockModule("mod1", ["mod3"]))
|
||||
|
||||
with pytest.raises(loader.CircularDependency):
|
||||
print(await loader.async_component_dependencies(hass, "mod3"))
|
||||
print(
|
||||
await loader._async_component_dependencies(
|
||||
hass, "mod_3", mod_3, set(), set()
|
||||
)
|
||||
)
|
||||
|
||||
# Depend on non-existing component
|
||||
mock_integration(hass, MockModule("mod1", ["nonexisting"]))
|
||||
mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"]))
|
||||
|
||||
with pytest.raises(loader.IntegrationNotFound):
|
||||
print(await loader.async_component_dependencies(hass, "mod1"))
|
||||
print(
|
||||
await loader._async_component_dependencies(
|
||||
hass, "mod_1", mod_1, set(), set()
|
||||
)
|
||||
)
|
||||
|
||||
# Try to get dependencies for non-existing component
|
||||
with pytest.raises(loader.IntegrationNotFound):
|
||||
print(await loader.async_component_dependencies(hass, "nonexisting"))
|
||||
# Having an after dependency 2 deps down that is circular
|
||||
mod_1 = mock_integration(
|
||||
hass, MockModule("mod1", partial_manifest={"after_dependencies": ["mod_3"]})
|
||||
)
|
||||
|
||||
with pytest.raises(loader.CircularDependency):
|
||||
print(
|
||||
await loader._async_component_dependencies(
|
||||
hass, "mod_3", mod_3, set(), set()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_component_loader(hass):
|
||||
|
@ -480,12 +480,6 @@ class TestSetup:
|
||||
assert call_order == [1, 1, 2]
|
||||
|
||||
|
||||
async def test_component_cannot_depend_config(hass):
|
||||
"""Test config is not allowed to be a dependency."""
|
||||
result = await setup._async_process_dependencies(hass, None, "test", ["config"])
|
||||
assert not result
|
||||
|
||||
|
||||
async def test_component_warn_slow_setup(hass):
|
||||
"""Warn we log when a component setup takes a long time."""
|
||||
mock_integration(hass, MockModule("test_component1"))
|
||||
|
Loading…
x
Reference in New Issue
Block a user