diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 1e26a9c46d4..887c0517d05 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +from aiohttp import web import aiohttp.web_exceptions import voluptuous as vol @@ -11,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES, POLICY_EDIT from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import Unauthorized +from homeassistant.exceptions import DependencyError, Unauthorized from homeassistant.helpers.data_entry_flow import ( FlowManagerIndexView, FlowManagerResourceView, @@ -127,7 +128,13 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") # pylint: disable=no-value-for-parameter - return await super().post(request) + try: + return await super().post(request) + except DependencyError as exc: + return web.Response( + text=f"Failed dependencies {', '.join(exc.failed_dependencies)}", + status=HTTPStatus.BAD_REQUEST, + ) def _prepare_result_json(self, result): """Convert result to JSON.""" diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index ff8fb295cd5..052d3de4768 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -199,3 +199,15 @@ class RequiredParameterMissing(HomeAssistantError): ), ) self.parameter_names = parameter_names + + +class DependencyError(HomeAssistantError): + """Raised when dependencies can not be setup.""" + + def __init__(self, failed_dependencies: list[str]) -> None: + """Initialize error.""" + super().__init__( + self, + f"Could not setup dependencies: {', '.join(failed_dependencies)}", + ) + self.failed_dependencies = failed_dependencies diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5ff6519f6ec..5c56cb55b19 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -18,7 +18,7 @@ from .const import ( Platform, ) from .core import CALLBACK_TYPE -from .exceptions import HomeAssistantError +from .exceptions import DependencyError, HomeAssistantError from .helpers.typing import ConfigType from .util import dt as dt_util, ensure_unique_string @@ -83,8 +83,11 @@ async def async_setup_component( async def _async_process_dependencies( hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration -) -> bool: - """Ensure all dependencies are set up.""" +) -> list[str]: + """Ensure all dependencies are set up. + + Returns a list of dependencies which failed to set up. + """ dependencies_tasks = { dep: hass.loop.create_task(async_setup_component(hass, dep, config)) for dep in integration.dependencies @@ -104,7 +107,7 @@ async def _async_process_dependencies( ) if not dependencies_tasks and not after_dependencies_tasks: - return True + return [] if dependencies_tasks: _LOGGER.debug( @@ -135,8 +138,7 @@ async def _async_process_dependencies( ", ".join(failed), ) - return False - return True + return failed async def _async_setup_component( @@ -341,8 +343,8 @@ async def async_process_deps_reqs( elif integration.domain in processed: return - if not await _async_process_dependencies(hass, config, integration): - raise HomeAssistantError("Could not set up all dependencies.") + if failed_deps := await _async_process_dependencies(hass, config, integration): + raise DependencyError(failed_deps) if not hass.config.skip_pip and integration.requirements: async with hass.timeout.async_freeze(integration.domain): diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 7f88d9b482b..6608bf3471d 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -252,6 +252,35 @@ async def test_initialize_flow(hass, client): } +async def test_initialize_flow_unmet_dependency(hass, client): + """Test unmet dependencies are listed.""" + mock_entity_platform(hass, "config_flow.test", None) + + config_schema = vol.Schema({"comp_conf": {"hello": str}}, required=True) + mock_integration( + hass, MockModule(domain="dependency_1", config_schema=config_schema) + ) + # The test2 config flow should fail because dependency_1 can't be automatically setup + mock_integration( + hass, + MockModule(domain="test2", partial_manifest={"dependencies": ["dependency_1"]}), + ) + + class TestFlow(core_ce.ConfigFlow): + async def async_step_user(self, user_input=None): + pass + + with patch.dict(HANDLERS, {"test2": TestFlow}): + resp = await client.post( + "/api/config/config_entries/flow", + json={"handler": "test2", "show_advanced_options": True}, + ) + + assert resp.status == HTTPStatus.BAD_REQUEST + data = await resp.text() + assert data == "Failed dependencies dependency_1" + + async def test_initialize_flow_unauth(hass, client, hass_admin_user): """Test we can initialize a flow.""" hass_admin_user.groups = []