diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 1ab01c62aab..ebf23653941 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -3,7 +3,17 @@ homeassistant.loader ~~~~~~~~~~~~~~~~~~~~ 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 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 os import sys import pkgutil import importlib @@ -30,22 +40,27 @@ def prepare(hass): pkgutil.iter_modules(components.__path__, 'homeassistant.components.')) # Look for available custom components + custom_path = hass.get_config_path("custom_components") - # Ensure we can load custom components from the config dir - sys.path.append(hass.config_dir) + if os.path.isdir(custom_path): + # Ensure we can load custom components using Pythons import + sys.path.insert(0, hass.config_dir) - try: - # pylint: disable=import-error - import custom_components + # 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). - AVAILABLE_COMPONENTS.extend( - item[1] for item in - pkgutil.iter_modules( - custom_components.__path__, 'custom_components.')) + # 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 os.path.isdir(os.path.join(custom_path, fil)): + AVAILABLE_COMPONENTS.append('custom_components.{}'.format(fil)) - except ImportError: - # No folder custom_components exist in the config directory - pass + else: + AVAILABLE_COMPONENTS.append( + 'custom_components.{}'.format(fil[0:-3])) def get_component(comp_name): @@ -61,8 +76,10 @@ def get_component(comp_name): # 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 config dir, then built-in + # First check custom, then built-in potential_paths = ['custom_components.{}'.format(comp_name), 'homeassistant.components.{}'.format(comp_name)] @@ -76,17 +93,30 @@ def get_component(comp_name): continue try: - _COMPONENT_CACHE[comp_name] = importlib.import_module(path) + 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) - return _COMPONENT_CACHE[comp_name] + _COMPONENT_CACHE[comp_name] = module + + return module + except ImportError: _LOGGER.exception( ("Error loading %s. Make sure all " "dependencies are installed"), path) - # We did find components but were unable to load them - _LOGGER.error("Unable to load component %s", comp_name) + _LOGGER.error("Unable to find component %s", comp_name) return None