Report unmet dependencies for failing config flows (#65061)

* Report unmet dependencies for failing config flows

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/setup.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Modify error message

* Add test

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Erik Montnemery 2022-02-02 15:06:27 +01:00 committed by GitHub
parent 40c727df66
commit 0eb2caabcf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 10 deletions

View file

@ -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."""

View file

@ -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

View file

@ -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):

View file

@ -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 = []