Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
import sys
from types import ModuleType
from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar # noqa pylint: disable=unused-import
from typing import Optional, Set, TYPE_CHECKING, Callable, Any, TypeVar, List # noqa pylint: disable=unused-import

from homeassistant.const import PLATFORM_FORMAT

Expand All @@ -34,8 +34,9 @@


DATA_KEY = 'components'
PATH_CUSTOM_COMPONENTS = 'custom_components'
PACKAGE_COMPONENTS = 'homeassistant.components'
PACKAGE_CUSTOM_COMPONENTS = 'custom_components'
PACKAGE_BUILTIN = 'homeassistant.components'
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]


class LoaderError(Exception):
Expand Down Expand Up @@ -76,23 +77,43 @@ def get_platform(hass, # type: HomeAssistant
domain: str, platform_name: str) -> Optional[ModuleType]:
"""Try to load specified platform.

Example invocation: get_platform(hass, 'light', 'hue')

Async friendly.
"""
platform = _load_file(hass, PLATFORM_FORMAT.format(
domain=domain, platform=platform_name))
# If the platform has a component, we will limit the platform loading path
# to be the same source (custom/built-in).
component = get_component(hass, platform_name)

# Until we have moved all platforms under their component/own folder, it
# can be that the component is None.
if component is not None:
base_paths = [component.__name__.rsplit('.', 1)[0]]
else:
base_paths = LOOKUP_PATHS

platform = _load_file(
hass, PLATFORM_FORMAT.format(domain=domain, platform=platform_name),
base_paths)

if platform is not None:
return platform

# Legacy platform check: light/hue.py
platform = _load_file(hass, PLATFORM_FORMAT.format(
domain=platform_name, platform=domain))
platform = _load_file(
hass, PLATFORM_FORMAT.format(domain=platform_name, platform=domain),
base_paths)

if platform is None:
_LOGGER.error("Unable to find platform %s", platform_name)
if component is None:
extra = ""
else:
extra = " Search path was limited to path of component: {}".format(
base_paths[0])
_LOGGER.error("Unable to find platform %s.%s", platform_name, extra)
return None

if platform.__name__.startswith(PATH_CUSTOM_COMPONENTS):
if platform.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS):
_LOGGER.warning(
"Integrations need to be in their own folder. Change %s/%s.py to "
"%s/%s.py. This will stop working soon.",
Expand All @@ -107,7 +128,7 @@ def get_component(hass, # type: HomeAssistant

Async friendly.
"""
comp = _load_file(hass, comp_or_platform)
comp = _load_file(hass, comp_or_platform, LOOKUP_PATHS)

if comp is None:
_LOGGER.error("Unable to find component %s", comp_or_platform)
Expand All @@ -116,7 +137,8 @@ def get_component(hass, # type: HomeAssistant


def _load_file(hass, # type: HomeAssistant
comp_or_platform: str) -> Optional[ModuleType]:
comp_or_platform: str,
base_paths: List[str]) -> Optional[ModuleType]:
"""Try to load specified file.

Looks in config dir first, then built-in components.
Expand All @@ -138,11 +160,8 @@ def _load_file(hass, # type: HomeAssistant
sys.path.insert(0, hass.config.config_dir)
cache = hass.data[DATA_KEY] = {}

# First check custom, then built-in
potential_paths = ['custom_components.{}'.format(comp_or_platform),
'homeassistant.components.{}'.format(comp_or_platform)]

for index, path in enumerate(potential_paths):
for index, path in enumerate('{}.{}'.format(base, comp_or_platform)
for base in base_paths):
try:
module = importlib.import_module(path)

Expand Down
1 change: 1 addition & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ def __init__(self, domain=None, dependencies=None, setup=None,
platform_schema_base=None, async_setup=None,
async_setup_entry=None, async_unload_entry=None):
"""Initialize the mock module."""
self.__name__ = 'homeassistant.components.{}'.format(domain)
self.DOMAIN = domain
self.DEPENDENCIES = dependencies or []
self.REQUIREMENTS = requirements or []
Expand Down
7 changes: 7 additions & 0 deletions tests/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,10 @@ async def test_get_platform(hass, caplog):
legacy_platform = loader.get_platform(hass, 'switch', 'test')
assert legacy_platform.__name__ == 'custom_components.switch.test'
assert 'Integrations need to be in their own folder.' in caplog.text


async def test_get_platform_enforces_component_path(hass, caplog):
"""Test that existence of a component limits lookup path of platforms."""
assert loader.get_platform(hass, 'comp_path_test', 'hue') is None
assert ('Search path was limited to path of component: '
'homeassistant.components') in caplog.text
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Custom platform for a built-in component, should not be allowed."""