From 80f66f301bab86a90d945d5cde5de1078cca5183 Mon Sep 17 00:00:00 2001 From: Ruslan Sayfutdinov Date: Thu, 15 Apr 2021 18:17:07 +0100 Subject: [PATCH] Define data flow result type (#49260) * Define data flow result type * Revert explicit definitions * Fix tests * Specific mypy ignore --- homeassistant/auth/__init__.py | 9 +-- homeassistant/auth/mfa_modules/__init__.py | 3 +- homeassistant/auth/mfa_modules/notify.py | 5 +- homeassistant/auth/mfa_modules/totp.py | 3 +- homeassistant/auth/providers/__init__.py | 12 ++-- homeassistant/auth/providers/command_line.py | 6 +- homeassistant/auth/providers/homeassistant.py | 6 +- .../auth/providers/insecure_example.py | 8 ++- .../auth/providers/legacy_api_password.py | 8 ++- .../auth/providers/trusted_networks.py | 6 +- homeassistant/components/bond/config_flow.py | 9 ++- homeassistant/components/hassio/__init__.py | 2 +- .../components/huawei_lte/config_flow.py | 16 +++-- .../components/hyperion/config_flow.py | 25 ++++--- .../components/zwave_js/config_flow.py | 30 ++++---- homeassistant/config_entries.py | 39 ++++++----- homeassistant/data_entry_flow.py | 69 +++++++++++++------ homeassistant/helpers/config_entry_flow.py | 14 ++-- .../helpers/config_entry_oauth2_flow.py | 9 +-- homeassistant/helpers/data_entry_flow.py | 4 +- 20 files changed, 169 insertions(+), 114 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3830419c5375..89a05e20eb22 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -4,13 +4,14 @@ from __future__ import annotations import asyncio from collections import OrderedDict from datetime import timedelta -from typing import Any, Dict, Optional, Tuple, cast +from typing import Any, Dict, Mapping, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.util import dt as dt_util from . import auth_store, models @@ -97,8 +98,8 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): return await auth_provider.async_login_flow(context) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: FlowResultDict + ) -> FlowResultDict: """Return a user as result of login flow.""" flow = cast(LoginFlow, flow) @@ -115,7 +116,7 @@ class AuthManagerFlowManager(data_entry_flow.FlowManager): raise KeyError(f"Unknown auth provider {result['handler']}") credentials = await auth_provider.async_get_or_create_credentials( - result["data"] + cast(Mapping[str, str], result["data"]), ) if flow.context.get("credential_only"): diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index d6989b6416fc..80e0a0d834a3 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -12,6 +12,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from homeassistant.util.decorator import Registry @@ -105,7 +106,7 @@ class SetupFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 76a5676d562c..c590b6195e46 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -14,6 +14,7 @@ import voluptuous as vol from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv @@ -292,7 +293,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Let user select available notify services.""" errors: dict[str, str] = {} @@ -318,7 +319,7 @@ class NotifySetupFlow(SetupFlow): async def async_step_setup( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Verify user can receive one-time password.""" errors: dict[str, str] = {} diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index d20c84655463..cb9ff95f808a 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.auth.models import User from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from . import ( MULTI_FACTOR_AUTH_MODULE_SCHEMA, @@ -189,7 +190,7 @@ class TotpSetupFlow(SetupFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of setup flow. Return self.async_show_form(step_id='init') if user_input is None. diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index 6e188be1ffc9..cdd5029f1d98 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -1,6 +1,7 @@ """Auth providers for Home Assistant.""" from __future__ import annotations +from collections.abc import Mapping import importlib import logging import types @@ -12,6 +13,7 @@ from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry @@ -102,7 +104,7 @@ class AuthProvider: raise NotImplementedError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" raise NotImplementedError @@ -198,7 +200,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the first step of login flow. Return self.async_show_form(step_id='init') if user_input is None. @@ -208,7 +210,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_select_mfa_module( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of select mfa module.""" errors = {} @@ -233,7 +235,7 @@ class LoginFlow(data_entry_flow.FlowHandler): async def async_step_mfa( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of mfa validation.""" assert self.credential assert self.user @@ -285,6 +287,6 @@ class LoginFlow(data_entry_flow.FlowHandler): errors=errors, ) - async def async_finish(self, flow_result: Any) -> dict: + async def async_finish(self, flow_result: Any) -> FlowResultDict: """Handle the pass of login flow.""" return self.async_create_entry(title=self._auth_provider.name, data=flow_result) diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 47a56d87097c..9413072fd4b6 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio.subprocess import collections +from collections.abc import Mapping import logging import os from typing import Any, cast @@ -10,6 +11,7 @@ from typing import Any, cast import voluptuous as vol from homeassistant.const import CONF_COMMAND +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -100,7 +102,7 @@ class CommandLineAuthProvider(AuthProvider): self._user_meta[username] = meta async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -127,7 +129,7 @@ class CommandLineLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 54d82013a758..7544ae9aa148 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import base64 from collections import OrderedDict +from collections.abc import Mapping import logging from typing import Any, cast @@ -12,6 +13,7 @@ import voluptuous as vol from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -277,7 +279,7 @@ class HassAuthProvider(AuthProvider): await self.data.async_save() async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" if self.data is None: @@ -319,7 +321,7 @@ class HassLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index c938a6fac815..ac6171a346c5 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -2,12 +2,14 @@ from __future__ import annotations from collections import OrderedDict +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow @@ -62,7 +64,7 @@ class ExampleAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" username = flow_result["username"] @@ -97,7 +99,7 @@ class ExampleLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 522751c70d69..5ffb59638dbb 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -5,12 +5,14 @@ It will be removed when auth system production ready """ from __future__ import annotations +from collections.abc import Mapping import hmac -from typing import Any, cast +from typing import cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -57,7 +59,7 @@ class LegacyApiPasswordAuthProvider(AuthProvider): raise InvalidAuthError async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Return credentials for this login.""" credentials = await self.async_credentials() @@ -82,7 +84,7 @@ class LegacyLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" errors = {} diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index 85b43d89f3fb..a6a5cfb94f08 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -5,6 +5,7 @@ Abort login flow if not access from trusted network. """ from __future__ import annotations +from collections.abc import Mapping from ipaddress import ( IPv4Address, IPv4Network, @@ -18,6 +19,7 @@ from typing import Any, Dict, List, Union, cast import voluptuous as vol from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv @@ -127,7 +129,7 @@ class TrustedNetworksAuthProvider(AuthProvider): ) async def async_get_or_create_credentials( - self, flow_result: dict[str, str] + self, flow_result: Mapping[str, str] ) -> Credentials: """Get credentials based on the flow result.""" user_id = flow_result["user"] @@ -199,7 +201,7 @@ class TrustedNetworksLoginFlow(LoginFlow): async def async_step_init( self, user_input: dict[str, str] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the step of the form.""" try: cast( diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py index 2e1f106193ef..d4bf0275ad9e 100644 --- a/homeassistant/components/bond/config_flow.py +++ b/homeassistant/components/bond/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import DiscoveryInfoType @@ -91,7 +92,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _, hub_name = await _validate_input(self.hass, self._discovered) self._discovered[CONF_NAME] = hub_name - async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType) -> dict[str, Any]: # type: ignore + async def async_step_zeroconf( # type: ignore[override] + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle a flow initialized by zeroconf discovery.""" name: str = discovery_info[CONF_NAME] host: str = discovery_info[CONF_HOST] @@ -115,7 +118,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle confirmation flow for discovered bond hub.""" errors = {} if user_input is not None: @@ -156,7 +159,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index a5a2a1886d74..6dd2a067c890 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -323,7 +323,7 @@ def get_core_info(hass): @callback @bind_hass -def is_hassio(hass): +def is_hassio(hass: HomeAssistant) -> bool: """Return true if Hass.io is loaded. Async friendly. diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index fc455f865fd3..5f1cdf932528 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -29,6 +29,8 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONNECTION_TIMEOUT, @@ -58,7 +60,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None, errors: dict[str, str] | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: if user_input is None: user_input = {} return self.async_show_form( @@ -85,7 +87,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle import initiated config flow.""" return await self.async_step_user(user_input) @@ -99,7 +101,7 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle user initiated config flow.""" if user_input is None: return await self._async_show_user_form() @@ -211,9 +213,9 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) - async def async_step_ssdp( # type: ignore # mypy says signature incompatible with supertype, but it's the same? - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp( # type: ignore[override] + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle SSDP initiated config flow.""" await self.async_set_unique_id(discovery_info[ssdp.ATTR_UPNP_UDN]) self._abort_if_unique_id_configured() @@ -254,7 +256,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle options flow.""" # Recipients are persisted as a list, but handled as comma separated string in UI diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index 7ceedcbf0054..1a087460151f 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -27,6 +27,7 @@ from homeassistant.const import ( CONF_TOKEN, ) from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResultDict import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -130,7 +131,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def _advance_to_auth_step_if_necessary( self, hyperion_client: client.HyperionClient - ) -> dict[str, Any]: + ) -> FlowResultDict: """Determine if auth is required.""" auth_resp = await hyperion_client.async_is_auth_required() @@ -145,7 +146,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_reauth( self, config_data: ConfigType, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a reauthentication flow.""" self._data = dict(config_data) async with self._create_client(raw_connection=True) as hyperion_client: @@ -153,9 +154,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) - async def async_step_ssdp( # type: ignore[override] - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_ssdp(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] """Handle a flow initiated by SSDP.""" # Sample data provided by SSDP: { # 'ssdp_location': 'http://192.168.0.1:8090/description.xml', @@ -226,7 +225,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initiated by the user.""" errors = {} if user_input: @@ -297,7 +296,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_auth( self, user_input: ConfigType | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the auth step of a flow.""" errors = {} if user_input: @@ -326,7 +325,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Send a request for a new token.""" if user_input is None: self._auth_id = client.generate_random_auth_id() @@ -352,7 +351,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_external( self, auth_resp: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle completion of the request for a new token.""" if auth_resp is not None and client.ResponseOK(auth_resp): token = auth_resp.get(const.KEY_INFO, {}).get(const.KEY_TOKEN) @@ -365,7 +364,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_success( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create an entry after successful token creation.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -381,7 +380,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_create_token_fail( self, _: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show an error on the auth form.""" # Clean-up the request task. await self._cancel_request_token_task() @@ -389,7 +388,7 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_confirm( self, user_input: ConfigType | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Get final confirmation before entry creation.""" if user_input is None and self._require_confirm: return self.async_show_form( @@ -449,7 +448,7 @@ class HyperionOptionsFlow(OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Manage the options.""" effects = {source: source for source in const.KEY_COMPONENTID_EXTERNAL_SOURCES} diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 313f4e146a51..a2429a25c1b1 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -14,7 +14,7 @@ from homeassistant import config_entries, exceptions from homeassistant.components.hassio import is_hassio from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback -from homeassistant.data_entry_flow import AbortFlow +from homeassistant.data_entry_flow import AbortFlow, FlowResultDict from homeassistant.helpers.aiohttp_client import async_get_clientsession from .addon import AddonError, AddonManager, get_addon_manager @@ -89,16 +89,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle the initial step.""" - if is_hassio(self.hass): # type: ignore # no-untyped-call + if is_hassio(self.hass): return await self.async_step_on_supervisor() return await self.async_step_manual() async def async_step_manual( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a manual configuration.""" if user_input is None: return self.async_show_form( @@ -134,9 +134,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) - async def async_step_hassio( # type: ignore # override - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + async def async_step_hassio(self, discovery_info: dict[str, Any]) -> FlowResultDict: # type: ignore[override] """Receive configuration from add-on discovery info. This flow is triggered by the Z-Wave JS add-on. @@ -154,7 +152,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_hassio_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm the add-on discovery.""" if user_input is not None: return await self.async_step_on_supervisor( @@ -164,7 +162,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="hassio_confirm") @callback - def _async_create_entry_from_vars(self) -> dict[str, Any]: + def _async_create_entry_from_vars(self) -> FlowResultDict: """Return a config entry for the flow.""" return self.async_create_entry( title=TITLE, @@ -179,7 +177,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle logic when on Supervisor host.""" if user_input is None: return self.async_show_form( @@ -203,7 +201,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Install Z-Wave JS add-on.""" if not self.install_task: self.install_task = self.hass.async_create_task(self._async_install_addon()) @@ -223,13 +221,13 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_install_failed( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Add-on installation failed.""" return self.async_abort(reason="addon_install_failed") async def async_step_configure_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Ask for config for Z-Wave JS add-on.""" addon_config = await self._async_get_addon_config() @@ -265,7 +263,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_addon( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Start Z-Wave JS add-on.""" if not self.start_task: self.start_task = self.hass.async_create_task(self._async_start_addon()) @@ -283,7 +281,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_start_failed( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Add-on start failed.""" return self.async_abort(reason="addon_start_failed") @@ -320,7 +318,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_finish_addon_setup( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Prepare info needed to complete the config entry. Get add-on discovery info and server version info. diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9df6dff8316c..c69cd0c9d5b4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping from contextvars import ContextVar import functools import logging @@ -21,7 +22,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event -from homeassistant.helpers.typing import UNDEFINED, UndefinedType +from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util @@ -146,7 +147,7 @@ class ConfigEntry: version: int, domain: str, title: str, - data: dict, + data: Mapping[str, Any], source: str, connection_class: str, system_options: dict, @@ -559,8 +560,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): self._hass_config = hass_config async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) @@ -668,7 +669,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): return flow async def async_post_init( - self, flow: data_entry_flow.FlowHandler, result: dict + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict ) -> None: """After a flow is initialised trigger new flow notifications.""" source = flow.context["source"] @@ -931,7 +932,7 @@ class ConfigEntries: unique_id: str | dict | None | UndefinedType = UNDEFINED, title: str | dict | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, - options: dict | UndefinedType = UNDEFINED, + options: Mapping[str, Any] | UndefinedType = UNDEFINED, system_options: dict | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -956,7 +957,7 @@ class ConfigEntries: changed = True entry.data = MappingProxyType(data) - if options is not UNDEFINED and entry.options != options: # type: ignore + if options is not UNDEFINED and entry.options != options: changed = True entry.options = MappingProxyType(options) @@ -1147,7 +1148,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): } @callback - def _async_in_progress(self, include_uninitialized: bool = False) -> list[dict]: + def _async_in_progress( + self, include_uninitialized: bool = False + ) -> list[data_entry_flow.FlowResultDict]: """Return other in progress flows for current domain.""" return [ flw @@ -1157,18 +1160,22 @@ class ConfigFlow(data_entry_flow.FlowHandler): if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] - async def async_step_ignore(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def async_step_ignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResultDict: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) return self.async_create_entry(title=user_input["title"], data={}) - async def async_step_unignore(self, user_input: dict[str, Any]) -> dict[str, Any]: + async def async_step_unignore( + self, user_input: dict[str, Any] + ) -> data_entry_flow.FlowResultDict: """Rediscover a config entry by it's unique_id.""" return self.async_abort(reason="not_implemented") async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> data_entry_flow.FlowResultDict: """Handle a flow initiated by the user.""" return self.async_abort(reason="not_implemented") @@ -1197,8 +1204,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): raise data_entry_flow.AbortFlow("already_in_progress") async def async_step_discovery( - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + self, discovery_info: DiscoveryInfoType + ) -> data_entry_flow.FlowResultDict: """Handle a flow initialized by discovery.""" await self._async_handle_discovery_without_unique_id() return await self.async_step_user() @@ -1206,7 +1213,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> data_entry_flow.FlowResultDict: """Abort the config flow.""" # Remove reauth notification if no reauth flows are in progress if self.source == SOURCE_REAUTH and not any( @@ -1254,8 +1261,8 @@ class OptionsFlowManager(data_entry_flow.FlowManager): return cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) async def async_finish_flow( - self, flow: data_entry_flow.FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index a9a78337b17c..3a38cd0da712 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -5,7 +5,7 @@ import abc import asyncio from collections.abc import Mapping from types import MappingProxyType -from typing import Any +from typing import Any, TypedDict import uuid import voluptuous as vol @@ -51,6 +51,29 @@ class AbortFlow(FlowError): self.description_placeholders = description_placeholders +class FlowResultDict(TypedDict, total=False): + """Typed result dict.""" + + version: int + type: str + flow_id: str + handler: str + title: str + data: Mapping[str, Any] + step_id: str + data_schema: vol.Schema + extra: str + required: bool + errors: dict[str, str] | None + description: str | None + description_placeholders: dict[str, Any] | None + progress_action: str + url: str + reason: str + context: dict[str, Any] + result: Any + + class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" @@ -88,15 +111,17 @@ class FlowManager(abc.ABC): @abc.abstractmethod async def async_finish_flow( - self, flow: FlowHandler, result: dict[str, Any] - ) -> dict[str, Any]: + self, flow: FlowHandler, result: FlowResultDict + ) -> FlowResultDict: """Finish a config flow and add an entry.""" - async def async_post_init(self, flow: FlowHandler, result: dict[str, Any]) -> None: + async def async_post_init(self, flow: FlowHandler, result: FlowResultDict) -> None: """Entry has finished executing its first step asynchronously.""" @callback - def async_progress(self, include_uninitialized: bool = False) -> list[dict]: + def async_progress( + self, include_uninitialized: bool = False + ) -> list[FlowResultDict]: """Return the flows in progress.""" return [ { @@ -110,8 +135,8 @@ class FlowManager(abc.ABC): ] async def async_init( - self, handler: str, *, context: dict | None = None, data: Any = None - ) -> Any: + self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None + ) -> FlowResultDict: """Start a configuration flow.""" if context is None: context = {} @@ -160,7 +185,7 @@ class FlowManager(abc.ABC): async def async_configure( self, flow_id: str, user_input: dict | None = None - ) -> Any: + ) -> FlowResultDict: """Continue a configuration flow.""" flow = self._progress.get(flow_id) @@ -217,7 +242,7 @@ class FlowManager(abc.ABC): step_id: str, user_input: dict | None, step_done: asyncio.Future | None = None, - ) -> dict: + ) -> FlowResultDict: """Handle a step of a flow.""" method = f"async_step_{step_id}" @@ -230,7 +255,7 @@ class FlowManager(abc.ABC): ) try: - result: dict = await getattr(flow, method)(user_input) + result: FlowResultDict = await getattr(flow, method)(user_input) except AbortFlow as err: result = _create_abort_data( flow.flow_id, flow.handler, err.reason, err.description_placeholders @@ -265,7 +290,7 @@ class FlowManager(abc.ABC): return result # We pass a copy of the result because we're mutating our version - result = await self.async_finish_flow(flow, dict(result)) + result = await self.async_finish_flow(flow, result.copy()) # _async_finish_flow may change result type, check it again if result["type"] == RESULT_TYPE_FORM: @@ -288,7 +313,7 @@ class FlowHandler: hass: HomeAssistant = None # type: ignore handler: str = None # type: ignore # Ensure the attribute has a subscriptable, but immutable, default value. - context: dict = MappingProxyType({}) # type: ignore + context: dict[str, Any] = MappingProxyType({}) # type: ignore # Set by _async_create_flow callback init_step = "init" @@ -318,9 +343,9 @@ class FlowHandler: *, step_id: str, data_schema: vol.Schema = None, - errors: dict | None = None, - description_placeholders: dict | None = None, - ) -> dict[str, Any]: + errors: dict[str, str] | None = None, + description_placeholders: dict[str, Any] | None = None, + ) -> FlowResultDict: """Return the definition of a form to gather user input.""" return { "type": RESULT_TYPE_FORM, @@ -340,7 +365,7 @@ class FlowHandler: data: Mapping[str, Any], description: str | None = None, description_placeholders: dict | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Finish config flow and create a config entry.""" return { "version": self.VERSION, @@ -356,7 +381,7 @@ class FlowHandler: @callback def async_abort( self, *, reason: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Abort the config flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders @@ -365,7 +390,7 @@ class FlowHandler: @callback def async_external_step( self, *, step_id: str, url: str, description_placeholders: dict | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP, @@ -377,7 +402,7 @@ class FlowHandler: } @callback - def async_external_step_done(self, *, next_step_id: str) -> dict[str, Any]: + def async_external_step_done(self, *, next_step_id: str) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_EXTERNAL_STEP_DONE, @@ -393,7 +418,7 @@ class FlowHandler: step_id: str, progress_action: str, description_placeholders: dict | None = None, - ) -> dict[str, Any]: + ) -> FlowResultDict: """Show a progress message to the user, without user input allowed.""" return { "type": RESULT_TYPE_SHOW_PROGRESS, @@ -405,7 +430,7 @@ class FlowHandler: } @callback - def async_show_progress_done(self, *, next_step_id: str) -> dict[str, Any]: + def async_show_progress_done(self, *, next_step_id: str) -> FlowResultDict: """Mark the progress done.""" return { "type": RESULT_TYPE_SHOW_PROGRESS_DONE, @@ -421,7 +446,7 @@ def _create_abort_data( handler: str, reason: str, description_placeholders: dict | None = None, -) -> dict[str, Any]: +) -> FlowResultDict: """Return the definition of an external step for the user to take.""" return { "type": RESULT_TYPE_ABORT, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6abcf0ece56b..c9ac765ecbbe 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -5,6 +5,8 @@ from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.helpers.typing import DiscoveryInfoType DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] @@ -29,7 +31,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a flow initialized by the user.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -40,7 +42,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async def async_step_confirm( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Confirm setup.""" if user_input is None: self._set_confirm_only() @@ -69,8 +71,8 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title=self._title, data={}) async def async_step_discovery( - self, discovery_info: dict[str, Any] - ) -> dict[str, Any]: + self, discovery_info: DiscoveryInfoType + ) -> FlowResultDict: """Handle a flow initialized by discovery.""" if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -85,7 +87,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_homekit = async_step_discovery async_step_dhcp = async_step_discovery - async def async_step_import(self, _: dict[str, Any] | None) -> dict[str, Any]: + async def async_step_import(self, _: dict[str, Any] | None) -> FlowResultDict: """Handle a flow initialized by import.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -135,7 +137,7 @@ class WebhookFlowHandler(config_entries.ConfigFlow): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Handle a user initiated set up flow to create a webhook.""" if not self._allow_multiple and self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 795c08dd1c98..891d6c7d28c4 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -23,6 +23,7 @@ from yarl import URL from homeassistant import config_entries from homeassistant.components import http from homeassistant.core import HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResultDict from homeassistant.helpers.network import NoURLAvailableError from .aiohttp_client import async_get_clientsession @@ -234,7 +235,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_pick_implementation( self, user_input: dict | None = None - ) -> dict: + ) -> FlowResultDict: """Handle a flow start.""" implementations = await async_get_implementations(self.hass, self.DOMAIN) @@ -265,7 +266,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_auth( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create an entry for auth.""" # Flow has been triggered by external data if user_input: @@ -291,7 +292,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async def async_step_creation( self, user_input: dict[str, Any] | None = None - ) -> dict[str, Any]: + ) -> FlowResultDict: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) # Force int for non-compliant oauth2 providers @@ -308,7 +309,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): {"auth_implementation": self.flow_impl.domain, "token": token} ) - async def async_oauth_create_entry(self, data: dict) -> dict: + async def async_oauth_create_entry(self, data: dict) -> FlowResultDict: """Create an entry for the flow. Ok to override if you want to fetch extra info or even add another step. diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 00d12d3ab907..af0ea22d5035 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -21,7 +21,9 @@ class _BaseFlowManagerView(HomeAssistantView): self._flow_mgr = flow_mgr # pylint: disable=no-self-use - def _prepare_result_json(self, result: dict[str, Any]) -> dict[str, Any]: + def _prepare_result_json( + self, result: data_entry_flow.FlowResultDict + ) -> data_entry_flow.FlowResultDict: """Convert result to JSON.""" if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: data = result.copy()