ha-core/homeassistant/loader.py

243 lines
8.6 KiB
Python
Raw Normal View History

"""
Provides methods for loading Home Assistant components.
2014-11-15 08:17:18 +01:00
This module has quite some complex parts. I have tried to add as much
documentation as possible to keep it understandable.
Components are loaded by calling get_component('switch') from your code.
If you want to retrieve a platform that is part of a component, you should
call get_component('switch.your_platform'). In both cases the config directory
is checked to see if it contains a user provided version. If not available it
will check the built-in components and platforms.
"""
import importlib
import logging
2016-02-19 06:27:50 +01:00
import os
import pkgutil
import sys
from types import ModuleType
# pylint: disable=unused-import
from typing import Optional, Sequence, Set, Dict # NOQA
from homeassistant.const import PLATFORM_FORMAT
2014-11-29 00:34:42 +01:00
from homeassistant.util import OrderedSet
# Typing imports
# pylint: disable=using-constant-test,unused-import
if False:
from homeassistant.core import HomeAssistant # NOQA
2014-11-29 00:34:42 +01:00
PREPARED = False
# List of available components
AVAILABLE_COMPONENTS = [] # type: List[str]
# Dict of loaded components mapped name => module
_COMPONENT_CACHE = {} # type: Dict[str, ModuleType]
_LOGGER = logging.getLogger(__name__)
def prepare(hass: 'HomeAssistant'):
2016-03-08 00:06:04 +01:00
"""Prepare the loading of components."""
2014-11-29 00:34:42 +01:00
global PREPARED # pylint: disable=global-statement
# Load the built-in components
import homeassistant.components as components
AVAILABLE_COMPONENTS.clear()
AVAILABLE_COMPONENTS.extend(
item[1] for item in
pkgutil.iter_modules(components.__path__, 'homeassistant.components.'))
# Look for available custom components
custom_path = hass.config.path("custom_components")
2014-11-15 08:17:18 +01:00
if os.path.isdir(custom_path):
# Ensure we can load custom components using Pythons import
sys.path.insert(0, hass.config.config_dir)
2014-11-15 08:17:18 +01:00
# We cannot use the same approach as for built-in components because
# custom components might only contain a platform for a component.
# ie custom_components/switch/some_platform.py. Using pkgutil would
# not give us the switch component (and neither should it).
2014-11-15 08:17:18 +01:00
# Assumption: the custom_components dir only contains directories or
# python components. If this assumption is not true, HA won't break,
# just might output more errors.
for fil in os.listdir(custom_path):
2015-08-03 17:05:33 +02:00
if fil == '__pycache__':
continue
elif os.path.isdir(os.path.join(custom_path, fil)):
AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil))
2014-11-15 08:17:18 +01:00
else:
# For files we will strip out .py extension
2014-11-15 08:17:18 +01:00
AVAILABLE_COMPONENTS.append(
'custom_components.{}'.format(fil[0:-3]))
2014-11-29 00:34:42 +01:00
PREPARED = True
def set_component(comp_name: str, component: ModuleType) -> None:
2016-03-08 00:06:04 +01:00
"""Set a component in the cache."""
2014-11-29 00:34:42 +01:00
_check_prepared()
2014-11-25 09:20:36 +01:00
_COMPONENT_CACHE[comp_name] = component
def get_platform(domain: str, platform: str) -> Optional[ModuleType]:
"""Try to load specified platform."""
return get_component(PLATFORM_FORMAT.format(domain, platform))
def get_component(comp_name) -> Optional[ModuleType]:
2016-03-08 00:06:04 +01:00
"""Try to load specified component.
2016-03-08 00:06:04 +01:00
Looks in config dir first, then built-in components.
Only returns it if also found to be valid.
"""
if comp_name in _COMPONENT_CACHE:
return _COMPONENT_CACHE[comp_name]
2014-11-29 00:34:42 +01:00
_check_prepared()
# If we ie. try to load custom_components.switch.wemo but the parent
# custom_components.switch does not exist, importing it will trigger
# an exception because it will try to import the parent.
# Because of this behavior, we will approach loading sub components
# with caution: only load it if we can verify that the parent exists.
2014-11-15 08:17:18 +01:00
# We do not want to silent the ImportErrors as they provide valuable
# information to track down when debugging Home Assistant.
2014-11-15 08:17:18 +01:00
# First check custom, then built-in
potential_paths = ['custom_components.{}'.format(comp_name),
'homeassistant.components.{}'.format(comp_name)]
for path in potential_paths:
# Validate here that root component exists
# If path contains a '.' we are specifying a sub-component
# Using rsplit we get the parent component from sub-component
root_comp = path.rsplit(".", 1)[0] if '.' in comp_name else path
if root_comp not in AVAILABLE_COMPONENTS:
continue
try:
2014-11-15 08:17:18 +01:00
module = importlib.import_module(path)
# In Python 3 you can import files from directories that do not
# contain the file __init__.py. A directory is a valid module if
# it contains a file with the .py extension. In this case Python
# will succeed in importing the directory as a module and call it
# a namespace. We do not care about namespaces.
# This prevents that when only
# custom_components/switch/some_platform.py exists,
# the import custom_components.switch would succeeed.
if module.__spec__.origin == 'namespace':
continue
_LOGGER.info("Loaded %s from %s", comp_name, path)
2014-11-15 08:17:18 +01:00
_COMPONENT_CACHE[comp_name] = module
return module
except ImportError as err:
# This error happens if for example custom_components/switch
# exists and we try to load switch.demo.
if str(err) != "No module named '{}'".format(path):
_LOGGER.exception(
("Error loading %s. Make sure all "
"dependencies are installed"), path)
2014-11-15 08:17:18 +01:00
_LOGGER.error("Unable to find component %s", comp_name)
return None
2014-11-29 00:34:42 +01:00
def load_order_components(components: Sequence[str]) -> OrderedSet:
2016-03-08 00:06:04 +01:00
"""Take in a list of components we want to load.
- filters out components we cannot load
- filters out components that have invalid/circular dependencies
- Will make sure the recorder component is loaded first
- 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.
2014-11-29 00:34:42 +01:00
"""
_check_prepared()
load_order = OrderedSet()
# Sort the list of modules on if they depend on group component or not.
# Components that do not depend on the group usually set up states.
# Components that depend on group usually use states in their setup.
2014-11-29 00:34:42 +01:00
for comp_load_order in sorted((load_order_component(component)
for component in components),
key=lambda order: 'group' in order):
2014-11-29 00:34:42 +01:00
load_order.update(comp_load_order)
# Push some to first place in load order
2015-11-06 22:57:03 +01:00
for comp in ('logger', 'recorder', 'introduction'):
if comp in load_order:
load_order.promote(comp)
2014-11-29 00:34:42 +01:00
return load_order
def load_order_component(comp_name: str) -> OrderedSet:
2016-03-08 00:06:04 +01:00
"""Return an OrderedSet of components in the correct order of loading.
2014-11-29 00:34:42 +01:00
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: str, load_order: OrderedSet,
loading: Set) -> OrderedSet:
2016-03-08 00:06:04 +01:00
"""Recursive function to get load order of components."""
2014-11-29 00:34:42 +01:00
component = get_component(comp_name)
2016-03-08 00:06:04 +01:00
# If None it does not exist, error already thrown by get_component.
2014-11-29 00:34:42 +01:00
if component is None:
return OrderedSet()
loading.add(comp_name)
for dependency in getattr(component, 'DEPENDENCIES', []):
2014-11-29 00:34:42 +01:00
# Check not already loaded
2015-08-03 17:05:33 +02:00
if dependency in load_order:
continue
2014-11-29 00:34:42 +01:00
2016-03-08 00:06:04 +01:00
# If we are already loading it, we have a circular dependency.
2015-08-03 17:05:33 +02:00
if dependency in loading:
_LOGGER.error('Circular dependency detected: %s -> %s',
comp_name, dependency)
return OrderedSet()
2014-11-29 00:34:42 +01:00
2015-08-03 17:05:33 +02:00
dep_load_order = _load_order_component(dependency, load_order, loading)
2014-11-29 00:34:42 +01:00
2015-08-03 17:05:33 +02:00
# 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()
2014-11-29 00:34:42 +01:00
2015-08-03 17:05:33 +02:00
load_order.update(dep_load_order)
2014-11-29 00:34:42 +01:00
load_order.add(comp_name)
loading.remove(comp_name)
return load_order
def _check_prepared() -> None:
2016-03-08 00:06:04 +01:00
"""Issue a warning if loader.prepare() has never been called."""
2014-11-29 00:34:42 +01:00
if not PREPARED:
_LOGGER.warning((
"You did not call loader.prepare() yet. "
"Certain functionality might not be working."))