""" Provides methods for loading Home Assistant components. This module has quite some complex parts. I have tried to add as much documentation as possible to keep it understandable. Components can be accessed via hass.components.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 functools as ft import importlib import logging 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 from homeassistant.util import OrderedSet # Typing imports # pylint: disable=using-constant-test,unused-import if False: from homeassistant.core import HomeAssistant # NOQA PREPARED = False DEPENDENCY_BLACKLIST = set(('config',)) # 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'): """Prepare the loading of components. This method needs to run in an executor. """ 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") if os.path.isdir(custom_path): # Ensure we can load custom components using Pythons import sys.path.insert(0, hass.config.config_dir) # 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). # 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): if fil == '__pycache__': continue elif os.path.isdir(os.path.join(custom_path, fil)): AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) else: # For files we will strip out .py extension AVAILABLE_COMPONENTS.append( 'custom_components.{}'.format(fil[0:-3])) PREPARED = True def set_component(comp_name: str, component: ModuleType) -> None: """Set a component in the cache. Async friendly. """ _check_prepared() _COMPONENT_CACHE[comp_name] = component def get_platform(domain: str, platform: str) -> Optional[ModuleType]: """Try to load specified platform. Async friendly. """ return get_component(PLATFORM_FORMAT.format(domain, platform)) def get_component(comp_name) -> Optional[ModuleType]: """Try to load specified component. Looks in config dir first, then built-in components. Only returns it if also found to be valid. Async friendly. """ if comp_name in _COMPONENT_CACHE: return _COMPONENT_CACHE[comp_name] _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. # We do not want to silent the ImportErrors as they provide valuable # information to track down when debugging Home Assistant. # 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: 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) _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) _LOGGER.error("Unable to find component %s", comp_name) return None class Components: """Helper to load components.""" def __init__(self, hass): """Initialize the Components class.""" self._hass = hass def __getattr__(self, comp_name): """Fetch a component.""" component = get_component(comp_name) if component is None: raise ImportError('Unable to load {}'.format(comp_name)) wrapped = ModuleWrapper(self._hass, component) setattr(self, comp_name, wrapped) return wrapped class Helpers: """Helper to load helpers.""" def __init__(self, hass): """Initialize the Helpers class.""" self._hass = hass def __getattr__(self, helper_name): """Fetch a helper.""" helper = importlib.import_module( 'homeassistant.helpers.{}'.format(helper_name)) wrapped = ModuleWrapper(self._hass, helper) setattr(self, helper_name, wrapped) return wrapped class ModuleWrapper: """Class to wrap a Python module and auto fill in hass argument.""" def __init__(self, hass, module): """Initialize the module wrapper.""" self._hass = hass self._module = module def __getattr__(self, attr): """Fetch an attribute.""" value = getattr(self._module, attr) if hasattr(value, '__bind_hass'): value = ft.partial(value, self._hass) setattr(self, attr, value) return value def bind_hass(func): """Decorator to indicate that first argument is hass.""" # pylint: disable=protected-access func.__bind_hass = True return func def load_order_component(comp_name: str) -> OrderedSet: """Return 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. Async friendly. """ return _load_order_component(comp_name, OrderedSet(), set()) def _load_order_component(comp_name: str, load_order: OrderedSet, loading: Set) -> OrderedSet: """Recursive function to get load order of components. Async friendly. """ 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 getattr(component, 'DEPENDENCIES', []): # Check not already loaded if dependency in load_order: continue # 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 not dep_load_order: _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() -> None: """Issue a warning if loader.prepare() has never been called. Async friendly. """ if not PREPARED: _LOGGER.warning(( "You did not call loader.prepare() yet. " "Certain functionality might not be working."))