From 8f2567f30d79168ee9aa0e33efa8cfc9d28427d2 Mon Sep 17 00:00:00 2001 From: Tom Schneider Date: Fri, 6 Mar 2020 19:17:30 +0100 Subject: [PATCH] Add config_flow to shopping_list (#32388) * Add config_flow to shopping_list * Fix pylint unused import error * Use _abort_if_unique_id_configured * Remove SHOPPING_LIST const * Use const.py for DOMAIN and CONF_TYPE * Fix tests * Remove unchanged variable _errors * Revert CONF_TYPE (wrong usage) * Use consts in test * Remove import check * Remove data={} * Remove parameters and default values * Re-add data={}, because it's needed * Unique ID checks and reverts for default parameters * remove pylint comment * Remove block till done * Address change requests * Update homeassistant/components/shopping_list/strings.json Co-Authored-By: Quentame * Update homeassistant/components/shopping_list/strings.json Co-Authored-By: Quentame * Update tests/components/shopping_list/test_config_flow.py Co-Authored-By: Quentame * Update tests/components/shopping_list/test_config_flow.py Co-Authored-By: Quentame * Update tests/components/shopping_list/test_config_flow.py Co-Authored-By: Quentame * Update tests/components/shopping_list/test_config_flow.py Co-Authored-By: Quentame * Only test config_flow * Generate translations * Move data to end * @asyncio.coroutine --> async def, yield from --> await * @asyncio.coroutine --> async def, yield from --> await (tests) * Remove init in config flow * remove if not hass.config_entries.async_entries(DOMAIN) * Add DOMAIN not in config * Fix tests * Update homeassistant/components/shopping_list/config_flow.py Co-Authored-By: Paulus Schoutsen * Fix tests * Update homeassistant/components/shopping_list/__init__.py Co-Authored-By: Martin Hjelmare Co-authored-by: Quentame Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- .../shopping_list/.translations/en.json | 14 +++ .../components/shopping_list/__init__.py | 34 ++++--- .../components/shopping_list/config_flow.py | 24 +++++ .../components/shopping_list/const.py | 2 + .../components/shopping_list/manifest.json | 1 + .../components/shopping_list/strings.json | 14 +++ homeassistant/generated/config_flows.py | 1 + tests/components/shopping_list/conftest.py | 13 ++- .../shopping_list/test_config_flow.py | 36 ++++++++ tests/components/shopping_list/test_init.py | 89 ++++++++----------- 10 files changed, 163 insertions(+), 65 deletions(-) create mode 100644 homeassistant/components/shopping_list/.translations/en.json create mode 100644 homeassistant/components/shopping_list/config_flow.py create mode 100644 homeassistant/components/shopping_list/const.py create mode 100644 homeassistant/components/shopping_list/strings.json create mode 100644 tests/components/shopping_list/test_config_flow.py diff --git a/homeassistant/components/shopping_list/.translations/en.json b/homeassistant/components/shopping_list/.translations/en.json new file mode 100644 index 00000000000..6a22409e8c6 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "The shopping list is already configured." + }, + "step": { + "user": { + "description": "Do you want to configure the shopping list?", + "title": "Shopping List" + } + }, + "title": "Shopping List" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 3f61f70f858..11f61d6d626 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -1,10 +1,10 @@ """Support to manage a shopping list.""" -import asyncio import logging import uuid import voluptuous as vol +from homeassistant import config_entries from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND @@ -12,9 +12,10 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from .const import DOMAIN + ATTR_NAME = "name" -DOMAIN = "shopping_list" _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) EVENT = "shopping_list_updated" @@ -53,20 +54,32 @@ SCHEMA_WEBSOCKET_CLEAR_ITEMS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( ) -@asyncio.coroutine -def async_setup(hass, config): +async def async_setup(hass, config): """Initialize the shopping list.""" - @asyncio.coroutine - def add_item_service(call): + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up shopping list from config flow.""" + + async def add_item_service(call): """Add an item with `name`.""" data = hass.data[DOMAIN] name = call.data.get(ATTR_NAME) if name is not None: data.async_add(name) - @asyncio.coroutine - def complete_item_service(call): + async def complete_item_service(call): """Mark the item provided via `name` as completed.""" data = hass.data[DOMAIN] name = call.data.get(ATTR_NAME) @@ -80,7 +93,7 @@ def async_setup(hass, config): data.async_update(item["id"], {"name": name, "complete": True}) data = hass.data[DOMAIN] = ShoppingData(hass) - yield from data.async_load() + await data.async_load() hass.services.async_register( DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA @@ -206,8 +219,7 @@ class CreateShoppingListItemView(http.HomeAssistantView): name = "api:shopping_list:item" @RequestDataValidator(vol.Schema({vol.Required("name"): str})) - @asyncio.coroutine - def post(self, request, data): + async def post(self, request, data): """Create a new shopping list item.""" item = request.app["hass"].data[DOMAIN].async_add(data["name"]) request.app["hass"].bus.async_fire(EVENT) diff --git a/homeassistant/components/shopping_list/config_flow.py b/homeassistant/components/shopping_list/config_flow.py new file mode 100644 index 00000000000..974174640be --- /dev/null +++ b/homeassistant/components/shopping_list/config_flow.py @@ -0,0 +1,24 @@ +"""Config flow to configure ShoppingList component.""" +from homeassistant import config_entries + +from .const import DOMAIN + + +class ShoppingListFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for ShoppingList component.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + # Check if already configured + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + if user_input is not None: + return self.async_create_entry(title="Shopping List", data=user_input) + + return self.async_show_form(step_id="user") + + async_step_import = async_step_user diff --git a/homeassistant/components/shopping_list/const.py b/homeassistant/components/shopping_list/const.py new file mode 100644 index 00000000000..4878d317780 --- /dev/null +++ b/homeassistant/components/shopping_list/const.py @@ -0,0 +1,2 @@ +"""All constants related to the shopping list component.""" +DOMAIN = "shopping_list" diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index 0c8b66b9a03..ad060f16756 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -5,5 +5,6 @@ "requirements": [], "dependencies": ["http"], "codeowners": [], + "config_flow": true, "quality_scale": "internal" } diff --git a/homeassistant/components/shopping_list/strings.json b/homeassistant/components/shopping_list/strings.json new file mode 100644 index 00000000000..9e56dd7eaa4 --- /dev/null +++ b/homeassistant/components/shopping_list/strings.json @@ -0,0 +1,14 @@ +{ + "config": { + "title": "Shopping List", + "step": { + "user": { + "title": "Shopping List", + "description": "Do you want to configure the shopping list?" + } + }, + "abort": { + "already_configured": "The shopping list is already configured." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cb5d7105131..3d752a955c5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -82,6 +82,7 @@ FLOWS = [ "samsungtv", "sense", "sentry", + "shopping_list", "simplisafe", "smartthings", "smhi", diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index 44c8000efa2..f63363e2f63 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,10 +1,10 @@ """Shopping list test helpers.""" -from unittest.mock import patch - +from asynctest import patch import pytest from homeassistant.components.shopping_list import intent as sl_intent -from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry @pytest.fixture(autouse=True) @@ -19,5 +19,10 @@ def mock_shopping_list_io(): @pytest.fixture async def sl_setup(hass): """Set up the shopping list.""" - assert await async_setup_component(hass, "shopping_list", {}) + + entry = MockConfigEntry(domain="shopping_list") + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await sl_intent.async_setup_intents(hass) diff --git a/tests/components/shopping_list/test_config_flow.py b/tests/components/shopping_list/test_config_flow.py new file mode 100644 index 00000000000..dfc23e18504 --- /dev/null +++ b/tests/components/shopping_list/test_config_flow.py @@ -0,0 +1,36 @@ +"""Test config flow.""" + +from homeassistant import data_entry_flow +from homeassistant.components.shopping_list.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER + + +async def test_import(hass): + """Test entry will be imported.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_user(hass): + """Test we can start a config flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_user_confirm(hass): + """Test we can finish a config flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].data == {} diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 74c354848a3..bb65dcf631f 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,36 +1,33 @@ """Test shopping list component.""" -import asyncio from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.helpers import intent -@asyncio.coroutine -def test_add_item(hass, sl_setup): +async def test_add_item(hass, sl_setup): """Test adding an item intent.""" - response = yield from intent.async_handle( + response = await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) assert response.speech["plain"]["speech"] == "I've added beer to your shopping list" -@asyncio.coroutine -def test_recent_items_intent(hass, sl_setup): +async def test_recent_items_intent(hass, sl_setup): """Test recent items.""" - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} ) - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}} ) - response = yield from intent.async_handle(hass, "test", "HassShoppingListLastItems") + response = await intent.async_handle(hass, "test", "HassShoppingListLastItems") assert ( response.speech["plain"]["speech"] @@ -38,22 +35,21 @@ def test_recent_items_intent(hass, sl_setup): ) -@asyncio.coroutine -def test_deprecated_api_get_all(hass, hass_client, sl_setup): +async def test_deprecated_api_get_all(hass, hass_client, sl_setup): """Test the API.""" - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} ) - client = yield from hass_client() - resp = yield from client.get("/api/shopping_list") + client = await hass_client() + resp = await client.get("/api/shopping_list") assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert len(data) == 2 assert data[0]["name"] == "beer" assert not data[0]["complete"] @@ -88,35 +84,34 @@ async def test_ws_get_items(hass, hass_ws_client, sl_setup): assert not data[1]["complete"] -@asyncio.coroutine -def test_deprecated_api_update(hass, hass_client, sl_setup): +async def test_deprecated_api_update(hass, hass_client, sl_setup): """Test the API.""" - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} ) beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] - client = yield from hass_client() - resp = yield from client.post( + client = await hass_client() + resp = await client.post( "/api/shopping_list/item/{}".format(beer_id), json={"name": "soda"} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == {"id": beer_id, "name": "soda", "complete": False} - resp = yield from client.post( + resp = await client.post( "/api/shopping_list/item/{}".format(wine_id), json={"complete": True} ) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data == {"id": wine_id, "name": "wine", "complete": True} beer, wine = hass.data["shopping_list"].items @@ -166,23 +161,20 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): assert wine == {"id": wine_id, "name": "wine", "complete": True} -@asyncio.coroutine -def test_api_update_fails(hass, hass_client, sl_setup): +async def test_api_update_fails(hass, hass_client, sl_setup): """Test the API.""" - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - client = yield from hass_client() - resp = yield from client.post( - "/api/shopping_list/non_existing", json={"name": "soda"} - ) + client = await hass_client() + resp = await client.post("/api/shopping_list/non_existing", json={"name": "soda"}) assert resp.status == 404 beer_id = hass.data["shopping_list"].items[0]["id"] - resp = yield from client.post( + resp = await client.post( "/api/shopping_list/item/{}".format(beer_id), json={"name": 123} ) @@ -212,29 +204,28 @@ async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): assert msg["success"] is False -@asyncio.coroutine -def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): +async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): """Test the API.""" - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) - yield from intent.async_handle( + await intent.async_handle( hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} ) beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] - client = yield from hass_client() + client = await hass_client() # Mark beer as completed - resp = yield from client.post( + resp = await client.post( "/api/shopping_list/item/{}".format(beer_id), json={"complete": True} ) assert resp.status == 200 - resp = yield from client.post("/api/shopping_list/clear_completed") + resp = await client.post("/api/shopping_list/clear_completed") assert resp.status == 200 items = hass.data["shopping_list"].items @@ -272,15 +263,14 @@ async def test_ws_clear_items(hass, hass_ws_client, sl_setup): assert items[0] == {"id": wine_id, "name": "wine", "complete": False} -@asyncio.coroutine -def test_deprecated_api_create(hass, hass_client, sl_setup): +async def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" - client = yield from hass_client() - resp = yield from client.post("/api/shopping_list/item", json={"name": "soda"}) + client = await hass_client() + resp = await client.post("/api/shopping_list/item", json={"name": "soda"}) assert resp.status == 200 - data = yield from resp.json() + data = await resp.json() assert data["name"] == "soda" assert data["complete"] is False @@ -290,12 +280,11 @@ def test_deprecated_api_create(hass, hass_client, sl_setup): assert items[0]["complete"] is False -@asyncio.coroutine -def test_deprecated_api_create_fail(hass, hass_client, sl_setup): +async def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" - client = yield from hass_client() - resp = yield from client.post("/api/shopping_list/item", json={"name": 1234}) + client = await hass_client() + resp = await client.post("/api/shopping_list/item", json={"name": 1234}) assert resp.status == 400 assert len(hass.data["shopping_list"].items) == 0