mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
Prevent partial custom component overlays (#21070)
* Prevent partial custom component overlays * Fix tests
This commit is contained in:
parent
73099caede
commit
2435456248
@ -15,7 +15,7 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from types import ModuleType
|
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
|
from homeassistant.const import PLATFORM_FORMAT
|
||||||
|
|
||||||
@ -34,8 +34,9 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
DATA_KEY = 'components'
|
DATA_KEY = 'components'
|
||||||
PATH_CUSTOM_COMPONENTS = 'custom_components'
|
PACKAGE_CUSTOM_COMPONENTS = 'custom_components'
|
||||||
PACKAGE_COMPONENTS = 'homeassistant.components'
|
PACKAGE_BUILTIN = 'homeassistant.components'
|
||||||
|
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
|
||||||
|
|
||||||
|
|
||||||
class LoaderError(Exception):
|
class LoaderError(Exception):
|
||||||
@ -76,23 +77,43 @@ def get_platform(hass, # type: HomeAssistant
|
|||||||
domain: str, platform_name: str) -> Optional[ModuleType]:
|
domain: str, platform_name: str) -> Optional[ModuleType]:
|
||||||
"""Try to load specified platform.
|
"""Try to load specified platform.
|
||||||
|
|
||||||
|
Example invocation: get_platform(hass, 'light', 'hue')
|
||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
platform = _load_file(hass, PLATFORM_FORMAT.format(
|
# If the platform has a component, we will limit the platform loading path
|
||||||
domain=domain, platform=platform_name))
|
# 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:
|
if platform is not None:
|
||||||
return platform
|
return platform
|
||||||
|
|
||||||
# Legacy platform check: light/hue.py
|
# Legacy platform check: light/hue.py
|
||||||
platform = _load_file(hass, PLATFORM_FORMAT.format(
|
platform = _load_file(
|
||||||
domain=platform_name, platform=domain))
|
hass, PLATFORM_FORMAT.format(domain=platform_name, platform=domain),
|
||||||
|
base_paths)
|
||||||
|
|
||||||
if platform is None:
|
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
|
return None
|
||||||
|
|
||||||
if platform.__name__.startswith(PATH_CUSTOM_COMPONENTS):
|
if platform.__name__.startswith(PACKAGE_CUSTOM_COMPONENTS):
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Integrations need to be in their own folder. Change %s/%s.py to "
|
"Integrations need to be in their own folder. Change %s/%s.py to "
|
||||||
"%s/%s.py. This will stop working soon.",
|
"%s/%s.py. This will stop working soon.",
|
||||||
@ -107,7 +128,7 @@ def get_component(hass, # type: HomeAssistant
|
|||||||
|
|
||||||
Async friendly.
|
Async friendly.
|
||||||
"""
|
"""
|
||||||
comp = _load_file(hass, comp_or_platform)
|
comp = _load_file(hass, comp_or_platform, LOOKUP_PATHS)
|
||||||
|
|
||||||
if comp is None:
|
if comp is None:
|
||||||
_LOGGER.error("Unable to find component %s", comp_or_platform)
|
_LOGGER.error("Unable to find component %s", comp_or_platform)
|
||||||
@ -116,7 +137,8 @@ def get_component(hass, # type: HomeAssistant
|
|||||||
|
|
||||||
|
|
||||||
def _load_file(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.
|
"""Try to load specified file.
|
||||||
|
|
||||||
Looks in config dir first, then built-in components.
|
Looks in config dir first, then built-in components.
|
||||||
@ -138,11 +160,8 @@ def _load_file(hass, # type: HomeAssistant
|
|||||||
sys.path.insert(0, hass.config.config_dir)
|
sys.path.insert(0, hass.config.config_dir)
|
||||||
cache = hass.data[DATA_KEY] = {}
|
cache = hass.data[DATA_KEY] = {}
|
||||||
|
|
||||||
# First check custom, then built-in
|
for index, path in enumerate('{}.{}'.format(base, comp_or_platform)
|
||||||
potential_paths = ['custom_components.{}'.format(comp_or_platform),
|
for base in base_paths):
|
||||||
'homeassistant.components.{}'.format(comp_or_platform)]
|
|
||||||
|
|
||||||
for index, path in enumerate(potential_paths):
|
|
||||||
try:
|
try:
|
||||||
module = importlib.import_module(path)
|
module = importlib.import_module(path)
|
||||||
|
|
||||||
|
@ -454,6 +454,7 @@ class MockModule:
|
|||||||
async_setup_entry=None, async_unload_entry=None,
|
async_setup_entry=None, async_unload_entry=None,
|
||||||
async_migrate_entry=None):
|
async_migrate_entry=None):
|
||||||
"""Initialize the mock module."""
|
"""Initialize the mock module."""
|
||||||
|
self.__name__ = 'homeassistant.components.{}'.format(domain)
|
||||||
self.DOMAIN = domain
|
self.DOMAIN = domain
|
||||||
self.DEPENDENCIES = dependencies or []
|
self.DEPENDENCIES = dependencies or []
|
||||||
self.REQUIREMENTS = requirements or []
|
self.REQUIREMENTS = requirements or []
|
||||||
|
@ -135,3 +135,10 @@ async def test_get_platform(hass, caplog):
|
|||||||
legacy_platform = loader.get_platform(hass, 'switch', 'test')
|
legacy_platform = loader.get_platform(hass, 'switch', 'test')
|
||||||
assert legacy_platform.__name__ == 'custom_components.switch.test'
|
assert legacy_platform.__name__ == 'custom_components.switch.test'
|
||||||
assert 'Integrations need to be in their own folder.' in caplog.text
|
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
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
"""Custom platform for a built-in component, should not be allowed."""
|
Loading…
x
Reference in New Issue
Block a user