mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Revert custom component loading logic (#14327)
* Revert custom component loading logic * Lint * Fix tests * Guard for infinite inserts into sys.path
This commit is contained in:
parent
e60d066514
commit
5c95c53c6c
@ -31,12 +31,6 @@ PREPARED = False
|
|||||||
|
|
||||||
DEPENDENCY_BLACKLIST = set(('config',))
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -64,85 +58,63 @@ def get_platform(hass, domain: str, platform: str) -> Optional[ModuleType]:
|
|||||||
return get_component(hass, PLATFORM_FORMAT.format(domain, platform))
|
return get_component(hass, PLATFORM_FORMAT.format(domain, platform))
|
||||||
|
|
||||||
|
|
||||||
def get_component(hass, comp_or_platform):
|
def get_component(hass, comp_or_platform) -> Optional[ModuleType]:
|
||||||
"""Load a module from either custom component or built-in."""
|
"""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.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return hass.data[DATA_KEY][comp_or_platform]
|
return hass.data[DATA_KEY][comp_or_platform]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Try custom component
|
|
||||||
module = _load_module(hass.config.path(PATH_CUSTOM_COMPONENTS),
|
|
||||||
PATH_CUSTOM_COMPONENTS, comp_or_platform)
|
|
||||||
|
|
||||||
if module is None:
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(
|
|
||||||
'{}.{}'.format(PACKAGE_COMPONENTS, comp_or_platform))
|
|
||||||
_LOGGER.debug('Loaded %s (built-in)', comp_or_platform)
|
|
||||||
except ImportError:
|
|
||||||
_LOGGER.warning('Unable to find %s', comp_or_platform)
|
|
||||||
module = None
|
|
||||||
|
|
||||||
cache = hass.data.get(DATA_KEY)
|
cache = hass.data.get(DATA_KEY)
|
||||||
if cache is None:
|
if cache is None:
|
||||||
|
# Only insert if it's not there (happens during tests)
|
||||||
|
if sys.path[0] != hass.config.config_dir:
|
||||||
|
sys.path.insert(0, hass.config.config_dir)
|
||||||
cache = hass.data[DATA_KEY] = {}
|
cache = hass.data[DATA_KEY] = {}
|
||||||
cache[comp_or_platform] = module
|
|
||||||
|
|
||||||
return module
|
# First check custom, then built-in
|
||||||
|
potential_paths = ['custom_components.{}'.format(comp_or_platform),
|
||||||
|
'homeassistant.components.{}'.format(comp_or_platform)]
|
||||||
|
|
||||||
|
for path in potential_paths:
|
||||||
def _find_spec(path, name):
|
|
||||||
for finder in sys.meta_path:
|
|
||||||
try:
|
try:
|
||||||
spec = finder.find_spec(name, path=path)
|
module = importlib.import_module(path)
|
||||||
if spec is not None:
|
|
||||||
return spec
|
# In Python 3 you can import files from directories that do not
|
||||||
except AttributeError:
|
# contain the file __init__.py. A directory is a valid module if
|
||||||
# Not all finders have the find_spec method
|
# it contains a file with the .py extension. In this case Python
|
||||||
pass
|
# 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 succeed.
|
||||||
|
if module.__spec__.origin == 'namespace':
|
||||||
|
continue
|
||||||
|
|
||||||
|
_LOGGER.info("Loaded %s from %s", comp_or_platform, path)
|
||||||
|
|
||||||
|
cache[comp_or_platform] = 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_or_platform)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _load_module(path, base_module, name):
|
|
||||||
"""Load a module based on a folder and a name."""
|
|
||||||
mod_name = "{}.{}".format(base_module, name)
|
|
||||||
spec = _find_spec([path], name)
|
|
||||||
|
|
||||||
# Special handling if loading platforms and the folder is a namespace
|
|
||||||
# (namespace is a folder without __init__.py)
|
|
||||||
if spec is None and '.' in name:
|
|
||||||
mod_parent_name = name.split('.')[0]
|
|
||||||
parent_spec = _find_spec([path], mod_parent_name)
|
|
||||||
if (parent_spec is None or
|
|
||||||
parent_spec.submodule_search_locations is None):
|
|
||||||
return None
|
|
||||||
spec = _find_spec(parent_spec.submodule_search_locations, mod_name)
|
|
||||||
|
|
||||||
# Not found
|
|
||||||
if spec is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# This is a namespace
|
|
||||||
if spec.loader is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
_LOGGER.debug('Loaded %s (%s)', name, base_module)
|
|
||||||
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
# A hack, I know. Don't currently know how to work around it.
|
|
||||||
if not module.__name__.startswith(base_module):
|
|
||||||
module.__name__ = "{}.{}".format(base_module, name)
|
|
||||||
|
|
||||||
if not module.__package__:
|
|
||||||
module.__package__ = base_module
|
|
||||||
elif not module.__package__.startswith(base_module):
|
|
||||||
module.__package__ = "{}.{}".format(base_module, name)
|
|
||||||
|
|
||||||
return module
|
|
||||||
|
|
||||||
|
|
||||||
class Components:
|
class Components:
|
||||||
"""Helper to load components."""
|
"""Helper to load components."""
|
||||||
|
|
||||||
|
@ -35,28 +35,30 @@ class TestNotifyFile(unittest.TestCase):
|
|||||||
assert setup_component(self.hass, notify.DOMAIN, config)
|
assert setup_component(self.hass, notify.DOMAIN, config)
|
||||||
assert not handle_config[notify.DOMAIN]
|
assert not handle_config[notify.DOMAIN]
|
||||||
|
|
||||||
def _test_notify_file(self, timestamp, mock_utcnow, mock_stat):
|
def _test_notify_file(self, timestamp):
|
||||||
"""Test the notify file output."""
|
"""Test the notify file output."""
|
||||||
mock_utcnow.return_value = dt_util.as_utc(dt_util.now())
|
filename = 'mock_file'
|
||||||
mock_stat.return_value.st_size = 0
|
message = 'one, two, testing, testing'
|
||||||
|
with assert_setup_component(1) as handle_config:
|
||||||
|
self.assertTrue(setup_component(self.hass, notify.DOMAIN, {
|
||||||
|
'notify': {
|
||||||
|
'name': 'test',
|
||||||
|
'platform': 'file',
|
||||||
|
'filename': filename,
|
||||||
|
'timestamp': timestamp,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
assert handle_config[notify.DOMAIN]
|
||||||
|
|
||||||
m_open = mock_open()
|
m_open = mock_open()
|
||||||
with patch(
|
with patch(
|
||||||
'homeassistant.components.notify.file.open',
|
'homeassistant.components.notify.file.open',
|
||||||
m_open, create=True
|
m_open, create=True
|
||||||
):
|
), patch('homeassistant.components.notify.file.os.stat') as mock_st, \
|
||||||
filename = 'mock_file'
|
patch('homeassistant.util.dt.utcnow',
|
||||||
message = 'one, two, testing, testing'
|
return_value=dt_util.utcnow()):
|
||||||
with assert_setup_component(1) as handle_config:
|
|
||||||
self.assertTrue(setup_component(self.hass, notify.DOMAIN, {
|
mock_st.return_value.st_size = 0
|
||||||
'notify': {
|
|
||||||
'name': 'test',
|
|
||||||
'platform': 'file',
|
|
||||||
'filename': filename,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
assert handle_config[notify.DOMAIN]
|
|
||||||
title = '{} notifications (Log started: {})\n{}\n'.format(
|
title = '{} notifications (Log started: {})\n{}\n'.format(
|
||||||
ATTR_TITLE_DEFAULT,
|
ATTR_TITLE_DEFAULT,
|
||||||
dt_util.utcnow().isoformat(),
|
dt_util.utcnow().isoformat(),
|
||||||
@ -82,14 +84,10 @@ class TestNotifyFile(unittest.TestCase):
|
|||||||
dt_util.utcnow().isoformat(), message))]
|
dt_util.utcnow().isoformat(), message))]
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch('homeassistant.components.notify.file.os.stat')
|
def test_notify_file(self):
|
||||||
@patch('homeassistant.util.dt.utcnow')
|
|
||||||
def test_notify_file(self, mock_utcnow, mock_stat):
|
|
||||||
"""Test the notify file output without timestamp."""
|
"""Test the notify file output without timestamp."""
|
||||||
self._test_notify_file(False, mock_utcnow, mock_stat)
|
self._test_notify_file(False)
|
||||||
|
|
||||||
@patch('homeassistant.components.notify.file.os.stat')
|
def test_notify_file_timestamp(self):
|
||||||
@patch('homeassistant.util.dt.utcnow')
|
|
||||||
def test_notify_file_timestamp(self, mock_utcnow, mock_stat):
|
|
||||||
"""Test the notify file output with timestamp."""
|
"""Test the notify file output with timestamp."""
|
||||||
self._test_notify_file(True, mock_utcnow, mock_stat)
|
self._test_notify_file(True)
|
||||||
|
@ -120,3 +120,7 @@ async def test_custom_component_name(hass):
|
|||||||
comp = loader.get_component(hass, 'light.test')
|
comp = loader.get_component(hass, 'light.test')
|
||||||
assert comp.__name__ == 'custom_components.light.test'
|
assert comp.__name__ == 'custom_components.light.test'
|
||||||
assert comp.__package__ == 'custom_components.light'
|
assert comp.__package__ == 'custom_components.light'
|
||||||
|
|
||||||
|
# Test custom components is mounted
|
||||||
|
from custom_components.test_package import TEST
|
||||||
|
assert TEST == 5
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
from homeassistant.components.image_processing import ImageProcessingEntity
|
from homeassistant.components.image_processing import ImageProcessingEntity
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_devices_callback,
|
||||||
|
discovery_info=None):
|
||||||
"""Set up the test image_processing platform."""
|
"""Set up the test image_processing platform."""
|
||||||
add_devices([TestImageProcessing('camera.demo_camera', "Test")])
|
async_add_devices_callback([
|
||||||
|
TestImageProcessing('camera.demo_camera', "Test")])
|
||||||
|
|
||||||
|
|
||||||
class TestImageProcessing(ImageProcessingEntity):
|
class TestImageProcessing(ImageProcessingEntity):
|
||||||
|
@ -21,6 +21,7 @@ def init(empty=False):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_devices_callback,
|
||||||
|
discovery_info=None):
|
||||||
"""Return mock devices."""
|
"""Return mock devices."""
|
||||||
add_devices_callback(DEVICES)
|
async_add_devices_callback(DEVICES)
|
||||||
|
@ -21,6 +21,7 @@ def init(empty=False):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
async def async_setup_platform(hass, config, async_add_devices_callback,
|
||||||
|
discovery_info=None):
|
||||||
"""Find and return test switches."""
|
"""Find and return test switches."""
|
||||||
add_devices_callback(DEVICES)
|
async_add_devices_callback(DEVICES)
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
"""Provide a mock package component."""
|
"""Provide a mock package component."""
|
||||||
|
from .const import TEST # noqa
|
||||||
|
|
||||||
|
|
||||||
DOMAIN = 'test_package'
|
DOMAIN = 'test_package'
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
"""Constants for test_package custom component."""
|
||||||
|
TEST = 5
|
Loading…
x
Reference in New Issue
Block a user