Support config flow on custom components (#24946)

* Support populating list of flows from custom components

* Re-allow custom component config flows

* Add tests for custom component retrieval

* Don't crash view if no handler exist

* Use get_custom_components instead fo resolve_from_root

* Switch to using an event instead of lock

* Leave list of integrations as set

* The returned list is not guaranteed to be ordered

Backend uses a set to represent them.
This commit is contained in:
Joakim Plate 2019-07-09 01:19:37 +02:00 committed by GitHub
parent a2237ce5d4
commit 2fbbcafaed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 31 deletions

View File

@ -1,12 +1,11 @@
"""Http views to control the config manager.""" """Http views to control the config manager."""
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.exceptions import Unauthorized from homeassistant.exceptions import Unauthorized
from homeassistant.helpers.data_entry_flow import ( from homeassistant.helpers.data_entry_flow import (
FlowManagerIndexView, FlowManagerResourceView) FlowManagerIndexView, FlowManagerResourceView)
from homeassistant.generated import config_flows from homeassistant.loader import async_get_config_flows
async def async_setup(hass): async def async_setup(hass):
@ -61,7 +60,7 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
'state': entry.state, 'state': entry.state,
'connection_class': entry.connection_class, 'connection_class': entry.connection_class,
'supports_options': hasattr( 'supports_options': hasattr(
config_entries.HANDLERS[entry.domain], config_entries.HANDLERS.get(entry.domain),
'async_get_options_flow'), 'async_get_options_flow'),
} for entry in hass.config_entries.async_entries()]) } for entry in hass.config_entries.async_entries()])
@ -173,7 +172,8 @@ class ConfigManagerAvailableFlowView(HomeAssistantView):
async def get(self, request): async def get(self, request):
"""List available flow handlers.""" """List available flow handlers."""
return self.json(config_flows.FLOWS) hass = request.app['hass']
return self.json(await async_get_config_flows(hass))
class OptionManagerFlowIndexView(FlowManagerIndexView): class OptionManagerFlowIndexView(FlowManagerIndexView):

View File

@ -553,14 +553,6 @@ class ConfigEntries:
_LOGGER.error('Cannot find integration %s', handler_key) _LOGGER.error('Cannot find integration %s', handler_key)
raise data_entry_flow.UnknownHandler raise data_entry_flow.UnknownHandler
# Our config flow list is based on built-in integrations. If overriden,
# we should not load it's config flow.
if not integration.is_built_in:
_LOGGER.error(
'Config flow is not supported for custom integration %s',
handler_key)
raise data_entry_flow.UnknownHandler
# Make sure requirements and dependencies of component are resolved # Make sure requirements and dependencies of component are resolved
await async_process_deps_reqs( await async_process_deps_reqs(
self.hass, self._hass_config, integration) self.hass, self._hass_config, integration)

View File

@ -2,9 +2,9 @@
import logging import logging
from typing import Any, Dict, Iterable, Optional from typing import Any, Dict, Iterable, Optional
from homeassistant.loader import async_get_integration, bind_hass from homeassistant.loader import (
async_get_integration, bind_hass, async_get_config_flows)
from homeassistant.util.json import load_json from homeassistant.util.json import load_json
from homeassistant.generated import config_flows
from .typing import HomeAssistantType from .typing import HomeAssistantType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -106,7 +106,8 @@ async def async_get_component_resources(hass: HomeAssistantType,
translation_cache = hass.data[TRANSLATION_STRING_CACHE][language] translation_cache = hass.data[TRANSLATION_STRING_CACHE][language]
# Get the set of components # Get the set of components
components = hass.config.components | set(config_flows.FLOWS) components = (hass.config.components |
await async_get_config_flows(hass))
# Calculate the missing components # Calculate the missing components
missing_components = components - set(translation_cache) missing_components = components - set(translation_cache)

View File

@ -36,9 +36,9 @@ DEPENDENCY_BLACKLIST = {'config'}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DATA_COMPONENTS = 'components' DATA_COMPONENTS = 'components'
DATA_INTEGRATIONS = 'integrations' DATA_INTEGRATIONS = 'integrations'
DATA_CUSTOM_COMPONENTS = 'custom_components'
PACKAGE_CUSTOM_COMPONENTS = 'custom_components' PACKAGE_CUSTOM_COMPONENTS = 'custom_components'
PACKAGE_BUILTIN = 'homeassistant.components' PACKAGE_BUILTIN = 'homeassistant.components'
LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] LOOKUP_PATHS = [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
@ -63,6 +63,81 @@ def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict:
} }
async def _async_get_custom_components(
hass: 'HomeAssistant') -> Dict[str, 'Integration']:
"""Return list of custom integrations."""
try:
import custom_components
except ImportError:
return {}
def get_sub_directories(paths: List) -> List:
"""Return all sub directories in a set of paths."""
return [
entry
for path in paths
for entry in pathlib.Path(path).iterdir()
if entry.is_dir()
]
dirs = await hass.async_add_executor_job(
get_sub_directories, custom_components.__path__)
integrations = await asyncio.gather(*[
hass.async_add_executor_job(
Integration.resolve_from_root,
hass,
custom_components,
comp.name)
for comp in dirs
])
return {
integration.domain: integration
for integration in integrations
if integration is not None
}
async def async_get_custom_components(
hass: 'HomeAssistant') -> Dict[str, 'Integration']:
"""Return cached list of custom integrations."""
reg_or_evt = hass.data.get(DATA_CUSTOM_COMPONENTS)
if reg_or_evt is None:
evt = hass.data[DATA_CUSTOM_COMPONENTS] = asyncio.Event()
reg = await _async_get_custom_components(hass)
hass.data[DATA_CUSTOM_COMPONENTS] = reg
evt.set()
return reg
if isinstance(reg_or_evt, asyncio.Event):
await reg_or_evt.wait()
return cast(Dict[str, 'Integration'],
hass.data.get(DATA_CUSTOM_COMPONENTS))
return cast(Dict[str, 'Integration'],
reg_or_evt)
async def async_get_config_flows(hass: 'HomeAssistant') -> Set[str]:
"""Return cached list of config flows."""
from homeassistant.generated.config_flows import FLOWS
flows = set() # type: Set[str]
flows.update(FLOWS)
integrations = await async_get_custom_components(hass)
flows.update([
integration.domain
for integration in integrations.values()
if integration.config_flow
])
return flows
class Integration: class Integration:
"""An integration in Home Assistant.""" """An integration in Home Assistant."""
@ -121,6 +196,7 @@ class Integration:
self.after_dependencies = manifest.get( self.after_dependencies = manifest.get(
'after_dependencies') # type: Optional[List[str]] 'after_dependencies') # type: Optional[List[str]]
self.requirements = manifest['requirements'] # type: List[str] self.requirements = manifest['requirements'] # type: List[str]
self.config_flow = manifest.get('config_flow', False) # type: bool
_LOGGER.info("Loaded %s from %s", self.domain, pkg_path) _LOGGER.info("Loaded %s from %s", self.domain, pkg_path)
@property @property
@ -177,20 +253,14 @@ async def async_get_integration(hass: 'HomeAssistant', domain: str)\
event = cache[domain] = asyncio.Event() event = cache[domain] = asyncio.Event()
try: # Instead of using resolve_from_root we use the cache of custom
import custom_components # components to find the integration.
integration = await hass.async_add_executor_job( integration = (await async_get_custom_components(hass)).get(domain)
Integration.resolve_from_root, hass, custom_components, domain if integration is not None:
) _LOGGER.warning(CUSTOM_WARNING, domain)
if integration is not None: cache[domain] = integration
_LOGGER.warning(CUSTOM_WARNING, domain) event.set()
cache[domain] = integration return integration
event.set()
return integration
except ImportError:
# Import error if "custom_components" doesn't exist
pass
from homeassistant import components from homeassistant import components

View File

@ -128,7 +128,7 @@ def test_available_flows(hass, client):
'/api/config/config_entries/flow_handlers') '/api/config/config_entries/flow_handlers')
assert resp.status == 200 assert resp.status == 200
data = yield from resp.json() data = yield from resp.json()
assert data == ['hello', 'world'] assert set(data) == set(['hello', 'world'])
############################ ############################

View File

@ -1,4 +1,5 @@
"""Test to verify that we can load components.""" """Test to verify that we can load components."""
from asynctest.mock import ANY, patch
import pytest import pytest
import homeassistant.loader as loader import homeassistant.loader as loader
@ -172,3 +173,57 @@ async def test_integrations_only_once(hass):
loader.async_get_integration(hass, 'hue')) loader.async_get_integration(hass, 'hue'))
assert await int_1 is await int_2 assert await int_1 is await int_2
async def test_get_custom_components_internal(hass):
"""Test that we can a list of custom components."""
# pylint: disable=protected-access
integrations = await loader._async_get_custom_components(hass)
assert integrations == {
'test': ANY,
"test_package": ANY
}
def _get_test_integration(hass, name, config_flow):
"""Return a generated test integration."""
return loader.Integration(
hass, "homeassistant.components.{}".format(name), None, {
'name': name,
'domain': name,
'config_flow': config_flow,
'dependencies': [],
'requirements': []})
async def test_get_custom_components(hass):
"""Verify that custom components are cached."""
test_1_integration = _get_test_integration(hass, 'test_1', False)
test_2_integration = _get_test_integration(hass, 'test_2', True)
name = 'homeassistant.loader._async_get_custom_components'
with patch(name) as mock_get:
mock_get.return_value = {
'test_1': test_1_integration,
'test_2': test_2_integration,
}
integrations = await loader.async_get_custom_components(hass)
assert integrations == mock_get.return_value
integrations = await loader.async_get_custom_components(hass)
assert integrations == mock_get.return_value
mock_get.assert_called_once_with(hass)
async def test_get_config_flows(hass):
"""Verify that custom components with config_flow are available."""
test_1_integration = _get_test_integration(hass, 'test_1', False)
test_2_integration = _get_test_integration(hass, 'test_2', True)
with patch('homeassistant.loader.async_get_custom_components') as mock_get:
mock_get.return_value = {
'test_1': test_1_integration,
'test_2': test_2_integration,
}
flows = await loader.async_get_config_flows(hass)
assert 'test_2' in flows
assert 'test_1' not in flows