mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Made dependency loading more robust
This commit is contained in:
parent
ce1a5de607
commit
5fe73cf33e
@ -13,12 +13,10 @@ import os
|
|||||||
import configparser
|
import configparser
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from itertools import chain
|
|
||||||
|
|
||||||
import homeassistant
|
import homeassistant
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.components as core_components
|
import homeassistant.components as core_components
|
||||||
import homeassistant.components.group as group
|
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=too-many-branches, too-many-statements
|
# pylint: disable=too-many-branches, too-many-statements
|
||||||
@ -33,112 +31,22 @@ def from_config_dict(config, hass=None):
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
loader.prepare(hass)
|
||||||
|
|
||||||
# Make a copy because we are mutating it.
|
# Make a copy because we are mutating it.
|
||||||
# Convert it to defaultdict so components can always have config dict
|
# Convert it to defaultdict so components can always have config dict
|
||||||
config = defaultdict(dict, config)
|
config = defaultdict(dict, config)
|
||||||
|
|
||||||
# List of loaded components
|
# Filter out the common config section [homeassistant]
|
||||||
components = {}
|
components = (key for key in config.keys() if key != homeassistant.DOMAIN)
|
||||||
|
|
||||||
# List of components to validate
|
|
||||||
to_validate = []
|
|
||||||
|
|
||||||
# List of validated components
|
|
||||||
validated = []
|
|
||||||
|
|
||||||
# List of components we are going to load
|
|
||||||
to_load = [key for key in config.keys() if key != homeassistant.DOMAIN]
|
|
||||||
|
|
||||||
loader.prepare(hass)
|
|
||||||
|
|
||||||
# Load required components
|
|
||||||
while to_load:
|
|
||||||
domain = to_load.pop()
|
|
||||||
|
|
||||||
component = loader.get_component(domain)
|
|
||||||
|
|
||||||
# if None it does not exist, error already thrown by get_component
|
|
||||||
if component is not None:
|
|
||||||
components[domain] = component
|
|
||||||
|
|
||||||
# Special treatment for GROUP, we want to load it as late as
|
|
||||||
# possible. We do this by loading it if all other to be loaded
|
|
||||||
# modules depend on it.
|
|
||||||
if component.DOMAIN == group.DOMAIN:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Components with no dependencies are valid
|
|
||||||
elif not component.DEPENDENCIES:
|
|
||||||
validated.append(domain)
|
|
||||||
|
|
||||||
# If dependencies we'll validate it later
|
|
||||||
else:
|
|
||||||
to_validate.append(domain)
|
|
||||||
|
|
||||||
# Make sure to load all dependencies that are not being loaded
|
|
||||||
for dependency in component.DEPENDENCIES:
|
|
||||||
if dependency not in chain(components.keys(), to_load):
|
|
||||||
to_load.append(dependency)
|
|
||||||
|
|
||||||
# Validate dependencies
|
|
||||||
group_added = False
|
|
||||||
|
|
||||||
while to_validate:
|
|
||||||
newly_validated = []
|
|
||||||
|
|
||||||
for domain in to_validate:
|
|
||||||
if all(domain in validated for domain
|
|
||||||
in components[domain].DEPENDENCIES):
|
|
||||||
|
|
||||||
newly_validated.append(domain)
|
|
||||||
|
|
||||||
# We validated new domains this iteration, add them to validated
|
|
||||||
if newly_validated:
|
|
||||||
|
|
||||||
# Add newly validated domains to validated
|
|
||||||
validated.extend(newly_validated)
|
|
||||||
|
|
||||||
# remove domains from to_validate
|
|
||||||
for domain in newly_validated:
|
|
||||||
to_validate.remove(domain)
|
|
||||||
|
|
||||||
newly_validated.clear()
|
|
||||||
|
|
||||||
# Nothing validated this iteration. Add group dependency and try again.
|
|
||||||
elif not group_added:
|
|
||||||
group_added = True
|
|
||||||
validated.append(group.DOMAIN)
|
|
||||||
|
|
||||||
# Group has already been added and we still can't validate all.
|
|
||||||
# Report missing deps as error and skip loading of these domains
|
|
||||||
else:
|
|
||||||
for domain in to_validate:
|
|
||||||
missing_deps = [dep for dep in components[domain].DEPENDENCIES
|
|
||||||
if dep not in validated]
|
|
||||||
|
|
||||||
logger.error(
|
|
||||||
"Could not validate all dependencies for %s: %s",
|
|
||||||
domain, ", ".join(missing_deps))
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
# Make sure we load groups if not in list yet.
|
|
||||||
if not group_added:
|
|
||||||
validated.append(group.DOMAIN)
|
|
||||||
|
|
||||||
if group.DOMAIN not in components:
|
|
||||||
components[group.DOMAIN] = \
|
|
||||||
loader.get_component(group.DOMAIN)
|
|
||||||
|
|
||||||
# Setup the components
|
# Setup the components
|
||||||
if core_components.setup(hass, config):
|
if core_components.setup(hass, config):
|
||||||
logger.info("Home Assistant core initialized")
|
logger.info("Home Assistant core initialized")
|
||||||
|
|
||||||
for domain in validated:
|
for domain in loader.load_order_components(components):
|
||||||
component = components[domain]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if component.setup(hass, config):
|
if loader.get_component(domain).setup(hass, config):
|
||||||
logger.info("component %s initialized", domain)
|
logger.info("component %s initialized", domain)
|
||||||
else:
|
else:
|
||||||
logger.error("component %s failed to initialize", domain)
|
logger.error("component %s failed to initialize", domain)
|
||||||
|
@ -19,6 +19,10 @@ import pkgutil
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.util import OrderedSet
|
||||||
|
|
||||||
|
PREPARED = False
|
||||||
|
|
||||||
# List of available components
|
# List of available components
|
||||||
AVAILABLE_COMPONENTS = []
|
AVAILABLE_COMPONENTS = []
|
||||||
|
|
||||||
@ -30,6 +34,8 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def prepare(hass):
|
def prepare(hass):
|
||||||
""" Prepares the loading of components. """
|
""" Prepares the loading of components. """
|
||||||
|
global PREPARED # pylint: disable=global-statement
|
||||||
|
|
||||||
# Load the built-in components
|
# Load the built-in components
|
||||||
import homeassistant.components as components
|
import homeassistant.components as components
|
||||||
|
|
||||||
@ -62,9 +68,13 @@ def prepare(hass):
|
|||||||
AVAILABLE_COMPONENTS.append(
|
AVAILABLE_COMPONENTS.append(
|
||||||
'custom_components.{}'.format(fil[0:-3]))
|
'custom_components.{}'.format(fil[0:-3]))
|
||||||
|
|
||||||
|
PREPARED = True
|
||||||
|
|
||||||
|
|
||||||
def set_component(comp_name, component):
|
def set_component(comp_name, component):
|
||||||
""" Sets a component in the cache. """
|
""" Sets a component in the cache. """
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
_COMPONENT_CACHE[comp_name] = component
|
_COMPONENT_CACHE[comp_name] = component
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +86,8 @@ def get_component(comp_name):
|
|||||||
if comp_name in _COMPONENT_CACHE:
|
if comp_name in _COMPONENT_CACHE:
|
||||||
return _COMPONENT_CACHE[comp_name]
|
return _COMPONENT_CACHE[comp_name]
|
||||||
|
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
# If we ie. try to load custom_components.switch.wemo but the parent
|
# If we ie. try to load custom_components.switch.wemo but the parent
|
||||||
# custom_components.switch does not exist, importing it will trigger
|
# custom_components.switch does not exist, importing it will trigger
|
||||||
# an exception because it will try to import the parent.
|
# an exception because it will try to import the parent.
|
||||||
@ -125,3 +137,89 @@ def get_component(comp_name):
|
|||||||
_LOGGER.error("Unable to find component %s", comp_name)
|
_LOGGER.error("Unable to find component %s", comp_name)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_order_components(components):
|
||||||
|
"""
|
||||||
|
Takes in a list of components we want to load:
|
||||||
|
- filters out components we cannot load
|
||||||
|
- filters out components that have invalid/circular dependencies
|
||||||
|
- Will ensure that all components that do not directly depend on
|
||||||
|
the group component will be loaded before the group component.
|
||||||
|
- returns an OrderedSet load order.
|
||||||
|
"""
|
||||||
|
_check_prepared()
|
||||||
|
|
||||||
|
group = get_component('group')
|
||||||
|
|
||||||
|
load_order = OrderedSet()
|
||||||
|
|
||||||
|
# Sort the list of modules on if they depend on group component or not.
|
||||||
|
# We do this because the components that do not depend on the group
|
||||||
|
# component usually set up states that the group component requires to be
|
||||||
|
# created before it can group them.
|
||||||
|
# This does not matter in the future if we can setup groups without the
|
||||||
|
# states existing yet.
|
||||||
|
for comp_load_order in sorted((load_order_component(component)
|
||||||
|
for component in components),
|
||||||
|
# Test if group component exists in case
|
||||||
|
# above get_component call had an error.
|
||||||
|
key=lambda order:
|
||||||
|
group and group.DOMAIN in order):
|
||||||
|
load_order.update(comp_load_order)
|
||||||
|
|
||||||
|
return load_order
|
||||||
|
|
||||||
|
|
||||||
|
def load_order_component(comp_name):
|
||||||
|
"""
|
||||||
|
Returns an OrderedSet of components in the correct order of loading.
|
||||||
|
Raises HomeAssistantError if a circular dependency is detected.
|
||||||
|
Returns an empty list if component could not be loaded.
|
||||||
|
"""
|
||||||
|
return _load_order_component(comp_name, OrderedSet(), set())
|
||||||
|
|
||||||
|
|
||||||
|
def _load_order_component(comp_name, load_order, loading):
|
||||||
|
""" Recursive function to get load order of components. """
|
||||||
|
component = get_component(comp_name)
|
||||||
|
|
||||||
|
# if None it does not exist, error already thrown by get_component
|
||||||
|
if component is None:
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
loading.add(comp_name)
|
||||||
|
|
||||||
|
for dependency in component.DEPENDENCIES:
|
||||||
|
# Check not already loaded
|
||||||
|
if dependency not in load_order:
|
||||||
|
# If we are already loading it, we have a circular dependency
|
||||||
|
if dependency in loading:
|
||||||
|
_LOGGER.error('Circular dependency detected: %s -> %s',
|
||||||
|
comp_name, dependency)
|
||||||
|
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
dep_load_order = _load_order_component(
|
||||||
|
dependency, load_order, loading)
|
||||||
|
|
||||||
|
# length == 0 means error loading dependency or children
|
||||||
|
if len(dep_load_order) == 0:
|
||||||
|
_LOGGER.error('Error loading %s dependency: %s',
|
||||||
|
comp_name, dependency)
|
||||||
|
return OrderedSet()
|
||||||
|
|
||||||
|
load_order.update(dep_load_order)
|
||||||
|
|
||||||
|
load_order.add(comp_name)
|
||||||
|
loading.remove(comp_name)
|
||||||
|
|
||||||
|
return load_order
|
||||||
|
|
||||||
|
|
||||||
|
def _check_prepared():
|
||||||
|
""" Issues a warning if loader.prepare() has never been called. """
|
||||||
|
if not PREPARED:
|
||||||
|
_LOGGER.warning((
|
||||||
|
"You did not call loader.prepare() yet. "
|
||||||
|
"Certain functionality might not be working."))
|
||||||
|
@ -4,6 +4,8 @@ homeassistant.util
|
|||||||
|
|
||||||
Helper methods for various modules.
|
Helper methods for various modules.
|
||||||
"""
|
"""
|
||||||
|
import collections
|
||||||
|
from itertools import chain
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import datetime
|
import datetime
|
||||||
@ -176,6 +178,75 @@ class OrderedEnum(enum.Enum):
|
|||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class OrderedSet(collections.MutableSet):
|
||||||
|
""" Ordered set taken from http://code.activestate.com/recipes/576694/ """
|
||||||
|
|
||||||
|
def __init__(self, iterable=None):
|
||||||
|
self.end = end = []
|
||||||
|
end += [None, end, end] # sentinel node for doubly linked list
|
||||||
|
self.map = {} # key --> [key, prev, next]
|
||||||
|
if iterable is not None:
|
||||||
|
self |= iterable
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.map)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in self.map
|
||||||
|
|
||||||
|
def add(self, key):
|
||||||
|
""" Add an element to the set. """
|
||||||
|
if key not in self.map:
|
||||||
|
end = self.end
|
||||||
|
curr = end[1]
|
||||||
|
curr[2] = end[1] = self.map[key] = [key, curr, end]
|
||||||
|
|
||||||
|
def discard(self, key):
|
||||||
|
""" Discard an element from the set. """
|
||||||
|
if key in self.map:
|
||||||
|
key, prev_item, next_item = self.map.pop(key)
|
||||||
|
prev_item[2] = next_item
|
||||||
|
next_item[1] = prev_item
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
end = self.end
|
||||||
|
curr = end[2]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[2]
|
||||||
|
|
||||||
|
def __reversed__(self):
|
||||||
|
end = self.end
|
||||||
|
curr = end[1]
|
||||||
|
while curr is not end:
|
||||||
|
yield curr[0]
|
||||||
|
curr = curr[1]
|
||||||
|
|
||||||
|
def pop(self, last=True): # pylint: disable=arguments-differ
|
||||||
|
""" Pops element of the beginning of the set.
|
||||||
|
Set last=True to pop from the back. """
|
||||||
|
if not self:
|
||||||
|
raise KeyError('set is empty')
|
||||||
|
key = self.end[1][0] if last else self.end[2][0]
|
||||||
|
self.discard(key)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def update(self, *args):
|
||||||
|
""" Add elements from args to the set. """
|
||||||
|
for item in chain(*args):
|
||||||
|
self.add(item)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self:
|
||||||
|
return '%s()' % (self.__class__.__name__,)
|
||||||
|
return '%s(%r)' % (self.__class__.__name__, list(self))
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, OrderedSet):
|
||||||
|
return len(self) == len(other) and list(self) == list(other)
|
||||||
|
return set(self) == set(other)
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config, items, logger):
|
def validate_config(config, items, logger):
|
||||||
"""
|
"""
|
||||||
Validates if all items are available in the configuration.
|
Validates if all items are available in the configuration.
|
||||||
|
@ -28,3 +28,13 @@ def mock_service(hass, domain, service):
|
|||||||
domain, service, lambda call: calls.append(call))
|
domain, service, lambda call: calls.append(call))
|
||||||
|
|
||||||
return calls
|
return calls
|
||||||
|
|
||||||
|
|
||||||
|
class MockModule(object):
|
||||||
|
""" Provides a fake module. """
|
||||||
|
|
||||||
|
def __init__(self, domain, dependencies=[], setup=None):
|
||||||
|
self.DOMAIN = domain
|
||||||
|
self.DEPENDENCIES = dependencies
|
||||||
|
# Setup a mock setup if none given.
|
||||||
|
self.setup = lambda hass, config: False if setup is None else setup
|
||||||
|
@ -6,13 +6,14 @@ Provides tests to verify that we can load components.
|
|||||||
"""
|
"""
|
||||||
# pylint: disable=too-many-public-methods,protected-access
|
# pylint: disable=too-many-public-methods,protected-access
|
||||||
import unittest
|
import unittest
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
import homeassistant as ha
|
import homeassistant as ha
|
||||||
import homeassistant.loader as loader
|
import homeassistant.loader as loader
|
||||||
import homeassistant.components.http as http
|
import homeassistant.components.http as http
|
||||||
|
|
||||||
import mock_toggledevice_platform
|
import mock_toggledevice_platform
|
||||||
from helper import get_test_home_assistant
|
from helper import get_test_home_assistant, MockModule
|
||||||
|
|
||||||
|
|
||||||
class TestLoader(unittest.TestCase):
|
class TestLoader(unittest.TestCase):
|
||||||
@ -37,3 +38,54 @@ class TestLoader(unittest.TestCase):
|
|||||||
self.assertEqual(http, loader.get_component('http'))
|
self.assertEqual(http, loader.get_component('http'))
|
||||||
|
|
||||||
self.assertIsNotNone(loader.get_component('custom_one'))
|
self.assertIsNotNone(loader.get_component('custom_one'))
|
||||||
|
|
||||||
|
def test_load_order_component(self):
|
||||||
|
""" Test if we can get the proper load order of components. """
|
||||||
|
loader.set_component('mod1', MockModule('mod1'))
|
||||||
|
loader.set_component('mod2', MockModule('mod2', ['mod1']))
|
||||||
|
loader.set_component('mod3', MockModule('mod3', ['mod2']))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
['mod1', 'mod2', 'mod3'], loader.load_order_component('mod3'))
|
||||||
|
|
||||||
|
# Create circular dependency
|
||||||
|
loader.set_component('mod1', MockModule('mod1', ['mod3']))
|
||||||
|
|
||||||
|
self.assertEqual([], loader.load_order_component('mod3'))
|
||||||
|
|
||||||
|
# Depend on non-existing component
|
||||||
|
loader.set_component('mod1', MockModule('mod1', ['nonexisting']))
|
||||||
|
|
||||||
|
self.assertEqual([], loader.load_order_component('mod1'))
|
||||||
|
|
||||||
|
# Try to get load order for non-existing component
|
||||||
|
self.assertEqual([], loader.load_order_component('mod1'))
|
||||||
|
|
||||||
|
def test_load_order_components(self):
|
||||||
|
loader.set_component('mod1', MockModule('mod1', ['group']))
|
||||||
|
loader.set_component('mod2', MockModule('mod2', ['mod1', 'sun']))
|
||||||
|
loader.set_component('mod3', MockModule('mod3', ['mod2']))
|
||||||
|
loader.set_component('mod4', MockModule('mod4', ['group']))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
['group', 'mod4', 'mod1', 'sun', 'mod2', 'mod3'],
|
||||||
|
loader.load_order_components(['mod4', 'mod3', 'mod2']))
|
||||||
|
|
||||||
|
loader.set_component('mod1', MockModule('mod1'))
|
||||||
|
loader.set_component('mod2', MockModule('mod2', ['group']))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
['mod1', 'group', 'mod2'],
|
||||||
|
loader.load_order_components(['mod2', 'mod1']))
|
||||||
|
|
||||||
|
# Add a non existing one
|
||||||
|
self.assertEqual(
|
||||||
|
['mod1', 'group', 'mod2'],
|
||||||
|
loader.load_order_components(['mod2', 'nonexisting', 'mod1']))
|
||||||
|
|
||||||
|
# Depend on a non existing one
|
||||||
|
loader.set_component('mod1', MockModule('mod1', ['nonexisting']))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
['group', 'mod2'],
|
||||||
|
loader.load_order_components(['mod2', 'mod1']))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user