From 7d82041763d1340747339c04ae7d37d7d60b525a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:25:52 -0400 Subject: [PATCH 01/13] fix access to config data now that we are using config flow --- custom_components/pyscript/__init__.py | 25 +++++++++++++---------- custom_components/pyscript/config_flow.py | 14 ++++++++++++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/custom_components/pyscript/__init__.py b/custom_components/pyscript/__init__.py index fc571ad..34a05ee 100644 --- a/custom_components/pyscript/__init__.py +++ b/custom_components/pyscript/__init__.py @@ -7,7 +7,7 @@ import voluptuous as vol -from homeassistant.config import async_hass_config_yaml, async_process_component_config +from homeassistant.config import async_hass_config_yaml from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( EVENT_HOMEASSISTANT_STARTED, @@ -17,7 +17,7 @@ ) from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.loader import bind_hass from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN, FOLDER, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START from .eval import AstEval @@ -81,11 +81,14 @@ async def reload_scripts_handler(call): _LOGGER.error(err) return - integration = await async_get_integration(hass, DOMAIN) - - config = await async_process_component_config(hass, conf, integration) + # If data in config doesn't match config entry, trigger a config import + # so that the config entry can get updated + if DOMAIN in conf and conf[DOMAIN] != config_entry.data: + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf[DOMAIN] + ) - State.set_pyscript_config(config.get(DOMAIN, {})) + State.set_pyscript_config(config_entry.data) ctx_delete = {} for global_ctx_name, global_ctx in GlobalContextMgr.items(): @@ -97,7 +100,7 @@ async def reload_scripts_handler(call): for global_ctx_name, global_ctx in ctx_delete.items(): await GlobalContextMgr.delete(global_ctx_name) - await load_scripts(hass, config) + await load_scripts(hass, config_entry.data) for global_ctx_name, global_ctx in GlobalContextMgr.items(): idx = global_ctx_name.find(".") @@ -188,14 +191,14 @@ async def async_unload_entry(hass, config_entry): @bind_hass -async def load_scripts(hass, config): +async def load_scripts(hass, data): """Load all python scripts in FOLDER.""" pyscript_dir = hass.config.path(FOLDER) - def glob_files(load_paths, config): + def glob_files(load_paths, data): source_files = [] - apps_config = config.get(DOMAIN, {}).get("apps", None) + apps_config = data.get("apps", None) for path, match, check_config in load_paths: for this_path in sorted(glob.glob(os.path.join(pyscript_dir, path, match))): rel_import_path = None @@ -227,7 +230,7 @@ def glob_files(load_paths, config): ["", "*.py", False], ] - source_files = await hass.async_add_executor_job(glob_files, load_paths, config) + source_files = await hass.async_add_executor_job(glob_files, load_paths, data) for global_ctx_name, source_file, rel_import_path, fq_mod_name in source_files: global_ctx = GlobalContext( global_ctx_name, diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index fbe113e..baf15ca 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -36,11 +36,23 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> None: entries = self.hass.config_entries.async_entries(DOMAIN) if entries: entry = entries[0] + updated_data = entry.data.copy() + + # Update "allow_all_imports" if it has been changed if entry.data.get(CONF_ALLOW_ALL_IMPORTS, False) != import_config.get( CONF_ALLOW_ALL_IMPORTS, False ): - updated_data = entry.data.copy() updated_data[CONF_ALLOW_ALL_IMPORTS] = import_config.get(CONF_ALLOW_ALL_IMPORTS, False) + + # Update "apps" if it has been changed + if entry.data.get("apps") != import_config.get("apps"): + if import_config.get("apps"): + updated_data["apps"] = import_config["apps"] + else: + updated_data.pop("apps") + + # Update and reload entry + if updated_data != entry.data: self.hass.config_entries.async_update_entry(entry=entry, data=updated_data) await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="updated_entry") From df412a475b2f7a1747a8fd14f44efeb6a86737f6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:33:05 -0400 Subject: [PATCH 02/13] add tests for updating a config entry --- tests/test_config_flow.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 7f76f35..a5ef121 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -75,8 +75,8 @@ async def test_import_flow(hass, pyscript_bypass_setup): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -async def test_import_flow_update_entry(hass): - """Test import config flow updates existing entry.""" +async def test_import_flow_update_allow_all_imports(hass): + """Test import config flow updates existing entry when `allow_all_imports` has changed.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({}) ) @@ -91,6 +91,36 @@ async def test_import_flow_update_entry(hass): assert result["reason"] == "updated_entry" +async def test_import_flow_update_apps_from_none(hass): + """Test import config flow updates existing entry when `apps` has changed from None to something.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({}) + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={"apps": {"test_app": {"param": 1}}} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + + +async def test_import_flow_update_apps_to_none(hass): + """Test import config flow updates existing entry when `apps` has changed from something to None.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({"apps": {"test_app": {"param": 1}}}) + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_IMPORT}, data={}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + + async def test_import_flow_no_update(hass): """Test import config flow doesn't update existing entry when data is same.""" result = await hass.config_entries.flow.async_init( From c29b12444dcde73d01bbe89f29c1689d71f877e6 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:41:33 -0400 Subject: [PATCH 03/13] empty commit to retrigger CI From 5da1d6bd9fa19c81bb4da17223dab0a06c8f26bd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 11 Oct 2020 22:53:20 -0400 Subject: [PATCH 04/13] make config update logic more generic --- custom_components/pyscript/config_flow.py | 23 +++++++++-------------- custom_components/pyscript/const.py | 1 + 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index baf15ca..cb0d51d 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -5,7 +5,7 @@ from homeassistant import config_entries -from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN +from .const import CONF_ALL_KEYS, CONF_ALLOW_ALL_IMPORTS, DOMAIN PYSCRIPT_SCHEMA = vol.Schema( {vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): bool}, extra=vol.ALLOW_EXTRA, @@ -38,20 +38,15 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> None: entry = entries[0] updated_data = entry.data.copy() - # Update "allow_all_imports" if it has been changed - if entry.data.get(CONF_ALLOW_ALL_IMPORTS, False) != import_config.get( - CONF_ALLOW_ALL_IMPORTS, False - ): - updated_data[CONF_ALLOW_ALL_IMPORTS] = import_config.get(CONF_ALLOW_ALL_IMPORTS, False) + # Update key if it's value has been changed + for key in CONF_ALL_KEYS: + if entry.data.get(key) != import_config.get(key): + if import_config.get(key) is not None: + updated_data[key] = import_config[key] + else: + updated_data.pop(key) - # Update "apps" if it has been changed - if entry.data.get("apps") != import_config.get("apps"): - if import_config.get("apps"): - updated_data["apps"] = import_config["apps"] - else: - updated_data.pop("apps") - - # Update and reload entry + # Update and reload entry if data needs to be updated if updated_data != entry.data: self.hass.config_entries.async_update_entry(entry=entry, data=updated_data) await self.hass.config_entries.async_reload(entry.entry_id) diff --git a/custom_components/pyscript/const.py b/custom_components/pyscript/const.py index cc9c7d7..f3ad2ea 100644 --- a/custom_components/pyscript/const.py +++ b/custom_components/pyscript/const.py @@ -5,6 +5,7 @@ FOLDER = "pyscript" CONF_ALLOW_ALL_IMPORTS = "allow_all_imports" +CONF_ALL_KEYS = [CONF_ALLOW_ALL_IMPORTS, "apps"] SERVICE_JUPYTER_KERNEL_START = "jupyter_kernel_start" From 1cd9bac8a9b0f5ed48cbd32b7d9db074c103fff5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 10:42:14 -0400 Subject: [PATCH 05/13] simplify config update logic to accept all updates --- custom_components/pyscript/config_flow.py | 13 +++++-------- custom_components/pyscript/const.py | 1 - 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index cb0d51d..f791cf7 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -4,8 +4,9 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.config_entries import SOURCE_IMPORT -from .const import CONF_ALL_KEYS, CONF_ALLOW_ALL_IMPORTS, DOMAIN +from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN PYSCRIPT_SCHEMA = vol.Schema( {vol.Optional(CONF_ALLOW_ALL_IMPORTS, default=False): bool}, extra=vol.ALLOW_EXTRA, @@ -38,13 +39,9 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> None: entry = entries[0] updated_data = entry.data.copy() - # Update key if it's value has been changed - for key in CONF_ALL_KEYS: - if entry.data.get(key) != import_config.get(key): - if import_config.get(key) is not None: - updated_data[key] = import_config[key] - else: - updated_data.pop(key) + for k, v in import_config.items(): + if entry.source == SOURCE_IMPORT or k != CONF_ALLOW_ALL_IMPORTS: + updated_data[k] = v # Update and reload entry if data needs to be updated if updated_data != entry.data: diff --git a/custom_components/pyscript/const.py b/custom_components/pyscript/const.py index f3ad2ea..cc9c7d7 100644 --- a/custom_components/pyscript/const.py +++ b/custom_components/pyscript/const.py @@ -5,7 +5,6 @@ FOLDER = "pyscript" CONF_ALLOW_ALL_IMPORTS = "allow_all_imports" -CONF_ALL_KEYS = [CONF_ALLOW_ALL_IMPORTS, "apps"] SERVICE_JUPYTER_KERNEL_START = "jupyter_kernel_start" From bbbab4da5dee9192bbab9acd984a190694f30659 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 10:54:38 -0400 Subject: [PATCH 06/13] handle key removal --- custom_components/pyscript/config_flow.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index f791cf7..eeaeaa4 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -39,10 +39,20 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> None: entry = entries[0] updated_data = entry.data.copy() + # Update values for all keys, excluding `allow_all_imports` for entries + # set up through the UI. for k, v in import_config.items(): if entry.source == SOURCE_IMPORT or k != CONF_ALLOW_ALL_IMPORTS: updated_data[k] = v + # Remove values for all keys in entry.data that are not in the imported config, + # excluding `allow_all_imports` for entries set up through the UI. + for key in entry.data: + if ( + entry.source == SOURCE_IMPORT or key != CONF_ALLOW_ALL_IMPORTS + ) and key not in import_config: + updated_data.pop(key) + # Update and reload entry if data needs to be updated if updated_data != entry.data: self.hass.config_entries.async_update_entry(entry=entry, data=updated_data) From b65aed46022cd2b4018869ac2c8a651a69b17628 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 16:56:18 -0400 Subject: [PATCH 07/13] fix timeout issue and typehints --- custom_components/pyscript/__init__.py | 15 +++++++++------ custom_components/pyscript/config_flow.py | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/custom_components/pyscript/__init__.py b/custom_components/pyscript/__init__.py index 34a05ee..d2edcf8 100644 --- a/custom_components/pyscript/__init__.py +++ b/custom_components/pyscript/__init__.py @@ -81,12 +81,15 @@ async def reload_scripts_handler(call): _LOGGER.error(err) return - # If data in config doesn't match config entry, trigger a config import - # so that the config entry can get updated - if DOMAIN in conf and conf[DOMAIN] != config_entry.data: - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf[DOMAIN] - ) + if DOMAIN in conf: + config = PYSCRIPT_SCHEMA(conf[DOMAIN]) + + # If data in config doesn't match config entry, trigger a config import + # so that the config entry can get updated + if config != config_entry.data: + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) State.set_pyscript_config(config_entry.data) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index eeaeaa4..6a1cd72 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -19,7 +19,7 @@ class PyscriptConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH - async def async_step_user(self, user_input: Dict[str, Any] = None) -> None: + async def async_step_user(self, user_input: Dict[str, Any] = None) -> Dict[str, Any]: """Handle a flow initialized by the user.""" if user_input is not None: if len(self.hass.config_entries.async_entries(DOMAIN)) > 0: @@ -30,7 +30,7 @@ async def async_step_user(self, user_input: Dict[str, Any] = None) -> None: return self.async_show_form(step_id="user", data_schema=PYSCRIPT_SCHEMA) - async def async_step_import(self, import_config: Dict[str, Any] = None) -> None: + async def async_step_import(self, import_config: Dict[str, Any] = None) -> Dict[str, Any]: """Import a config entry from configuration.yaml.""" # Check if import config entry matches any existing config entries # so we can update it if necessary From 84e9a8ebae4de528e9642ac3560480f63d73e7c5 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 17:04:13 -0400 Subject: [PATCH 08/13] ensure we never use an OrderedDict --- custom_components/pyscript/config_flow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index 6a1cd72..be15f4c 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -1,4 +1,5 @@ """Config flow for pyscript.""" +import json from typing import Any, Dict import voluptuous as vol @@ -32,6 +33,8 @@ async def async_step_user(self, user_input: Dict[str, Any] = None) -> Dict[str, async def async_step_import(self, import_config: Dict[str, Any] = None) -> Dict[str, Any]: """Import a config entry from configuration.yaml.""" + import_config = json.loads(json.dumps(import_config)) + # Check if import config entry matches any existing config entries # so we can update it if necessary entries = self.hass.config_entries.async_entries(DOMAIN) From eb1abd12831e8eae034868e5f32c80d92b23090b Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 17:19:26 -0400 Subject: [PATCH 09/13] remove unused patches and test variables in tests --- tests/test_decorator_errors.py | 16 +++---------- tests/test_function.py | 16 +++---------- tests/test_init.py | 43 +++++++++------------------------- tests/test_jupyter.py | 16 +++---------- tests/test_unique.py | 16 +++---------- 5 files changed, 23 insertions(+), 84 deletions(-) diff --git a/tests/test_decorator_errors.py b/tests/test_decorator_errors.py index a529c96..4fce5b5 100644 --- a/tests/test_decorator_errors.py +++ b/tests/test_decorator_errors.py @@ -2,13 +2,11 @@ from ast import literal_eval import asyncio from datetime import datetime as dt -import pathlib from custom_components.pyscript.const import DOMAIN import custom_components.pyscript.trigger as trigger from pytest_homeassistant.async_mock import mock_open, patch -from homeassistant import loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED from homeassistant.setup import async_setup_component @@ -18,18 +16,10 @@ async def setup_script(hass, notify_q, now, source): scripts = [ "/some/config/dir/pyscripts/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( - "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, - ), patch( + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True,), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ): assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) diff --git a/tests/test_function.py b/tests/test_function.py index e89072f..a24a031 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -2,7 +2,6 @@ from ast import literal_eval import asyncio from datetime import datetime as dt -import pathlib import time from custom_components.pyscript.const import DOMAIN @@ -11,7 +10,6 @@ import pytest from pytest_homeassistant.async_mock import MagicMock, Mock, mock_open, patch -from homeassistant import loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED from homeassistant.setup import async_setup_component @@ -104,18 +102,10 @@ async def setup_script(hass, notify_q, now, source): scripts = [ "/some/config/dir/pyscripts/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( - "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, - ), patch( + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True,), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ): assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) diff --git a/tests/test_init.py b/tests/test_init.py index d897d08..eadc6ec 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -24,18 +24,10 @@ async def setup_script(hass, notify_q, now, source): scripts = [ "/some/config/dir/pyscript/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( - "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, - ), patch( + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True,), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ), patch( "homeassistant.config.load_yaml_config_file", return_value={} @@ -67,16 +59,9 @@ async def wait_until_done(notify_q): async def test_setup_makedirs_on_no_dir(hass, caplog): """Test setup calls os.makedirs when no dir found.""" - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=False - ), patch("custom_components.pyscript.os.makedirs") as makedirs_call: + with patch("custom_components.pyscript.os.path.isdir", return_value=False), patch( + "custom_components.pyscript.os.makedirs" + ) as makedirs_call: res = await async_setup_component(hass, "pyscript", {DOMAIN: {}}) assert res @@ -237,7 +222,7 @@ def func_yaml_doc_string(param2=None, param3=None): {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, ) - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( + with patch( "homeassistant.loader.async_get_custom_components", return_value={"pyscript": integration}, ): descriptions = await async_get_all_descriptions(hass) @@ -442,16 +427,10 @@ def func5(var_name=None, value=None): scripts = [ "/some/config/dir/pyscript/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( + + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch( "custom_components.pyscript.global_ctx.open", mock_open(read_data=next_source), create=True, ), patch( "custom_components.pyscript.trigger.dt_now", return_value=now diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index 148de80..94f6996 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -6,7 +6,6 @@ import hashlib import hmac import json -import pathlib import uuid from custom_components.pyscript.const import DOMAIN @@ -14,7 +13,6 @@ import custom_components.pyscript.trigger as trigger from pytest_homeassistant.async_mock import mock_open, patch -from homeassistant import loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component @@ -112,18 +110,10 @@ async def setup_script(hass, now, source, no_connect=False): scripts = [ "/some/config/dir/pyscripts/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - with patch("homeassistant.loader.async_get_integration", return_value=integration,), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( - "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, - ), patch( + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True,), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ): assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) diff --git a/tests/test_unique.py b/tests/test_unique.py index dda91bc..f702354 100644 --- a/tests/test_unique.py +++ b/tests/test_unique.py @@ -2,13 +2,11 @@ from ast import literal_eval import asyncio from datetime import datetime as dt -import pathlib from custom_components.pyscript.const import DOMAIN import custom_components.pyscript.trigger as trigger from pytest_homeassistant.async_mock import mock_open, patch -from homeassistant import loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_STATE_CHANGED from homeassistant.setup import async_setup_component @@ -18,18 +16,10 @@ async def setup_script(hass, notify_q, now, source): scripts = [ "/some/config/dir/pyscripts/hello.py", ] - integration = loader.Integration( - hass, - "custom_components.pyscript", - pathlib.Path("custom_components/pyscript"), - {"name": "pyscript", "dependencies": [], "requirements": [], "domain": "automation"}, - ) - with patch("homeassistant.loader.async_get_integration", return_value=integration), patch( - "custom_components.pyscript.os.path.isdir", return_value=True - ), patch("custom_components.pyscript.glob.iglob", return_value=scripts), patch( - "custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True, - ), patch( + with patch("custom_components.pyscript.os.path.isdir", return_value=True), patch( + "custom_components.pyscript.glob.iglob", return_value=scripts + ), patch("custom_components.pyscript.global_ctx.open", mock_open(read_data=source), create=True,), patch( "custom_components.pyscript.trigger.dt_now", return_value=now ): assert await async_setup_component(hass, "pyscript", {DOMAIN: {}}) From 6bda2c1b7942eefd8c7b04f46da8e2b57f2c8f27 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 17:42:00 -0400 Subject: [PATCH 10/13] add tests to ensure that allow_all_imports updates are handled properly for user vs import updates --- tests/test_config_flow.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index a5ef121..9ebffee 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -135,3 +135,42 @@ async def test_import_flow_no_update(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured_service" + + +async def test_import_flow_update_user(hass): + """Test import config flow update excludes `allow_all_imports` from being updated when updated entry was a user entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=PYSCRIPT_SCHEMA({CONF_ALLOW_ALL_IMPORTS: True}) + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={"apps": {"test_app": {"param": 1}}} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + + hass.config_entries.async_entries(DOMAIN)[0].data == { + CONF_ALLOW_ALL_IMPORTS: True, + "apps": {"test_app": {"param": 1}}, + } + + +async def test_import_flow_update_import(hass): + """Test import config flow update includes `allow_all_imports` in update when updated entry was imported entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({CONF_ALLOW_ALL_IMPORTS: True}) + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={"apps": {"test_app": {"param": 1}}} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "updated_entry" + + hass.config_entries.async_entries(DOMAIN)[0].data == {"apps": {"test_app": {"param": 1}}} From 41e36eaca827c7ebf6c20a65d29a4676ce147c87 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 18:46:59 -0400 Subject: [PATCH 11/13] remove unnecessary reload --- custom_components/pyscript/config_flow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index be15f4c..2a94eb1 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -59,7 +59,6 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> Dict[ # Update and reload entry if data needs to be updated if updated_data != entry.data: self.hass.config_entries.async_update_entry(entry=entry, data=updated_data) - await self.hass.config_entries.async_reload(entry.entry_id) return self.async_abort(reason="updated_entry") return self.async_abort(reason="already_configured_service") From 0769efefb615e058dbea797cbcdad65b21a40443 Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 18:49:08 -0400 Subject: [PATCH 12/13] add comment --- custom_components/pyscript/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/pyscript/config_flow.py b/custom_components/pyscript/config_flow.py index 2a94eb1..043eb30 100644 --- a/custom_components/pyscript/config_flow.py +++ b/custom_components/pyscript/config_flow.py @@ -33,6 +33,7 @@ async def async_step_user(self, user_input: Dict[str, Any] = None) -> Dict[str, async def async_step_import(self, import_config: Dict[str, Any] = None) -> Dict[str, Any]: """Import a config entry from configuration.yaml.""" + # Convert OrderedDict to dict import_config = json.loads(json.dumps(import_config)) # Check if import config entry matches any existing config entries From d4ddf318b67d10512de701e4192647464afb6a7e Mon Sep 17 00:00:00 2001 From: raman325 <7243222+raman325@users.noreply.github.com> Date: Mon, 12 Oct 2020 18:50:43 -0400 Subject: [PATCH 13/13] always update entry during reload --- custom_components/pyscript/__init__.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/custom_components/pyscript/__init__.py b/custom_components/pyscript/__init__.py index d2edcf8..6e28673 100644 --- a/custom_components/pyscript/__init__.py +++ b/custom_components/pyscript/__init__.py @@ -81,15 +81,12 @@ async def reload_scripts_handler(call): _LOGGER.error(err) return - if DOMAIN in conf: - config = PYSCRIPT_SCHEMA(conf[DOMAIN]) - - # If data in config doesn't match config entry, trigger a config import - # so that the config entry can get updated - if config != config_entry.data: - await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) + config = PYSCRIPT_SCHEMA(conf.get(DOMAIN, {})) + + # If data in config doesn't match config entry, trigger a config import + # so that the config entry can get updated + if config != config_entry.data: + await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_IMPORT}, data=config) State.set_pyscript_config(config_entry.data)