Skip to content

Fix access to config data now that we are using config flow #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Oct 13, 2020
23 changes: 13 additions & 10 deletions custom_components/pyscript/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -81,11 +81,14 @@ async def reload_scripts_handler(call):
_LOGGER.error(err)
return

integration = await async_get_integration(hass, DOMAIN)
config = PYSCRIPT_SCHEMA(conf.get(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 config != config_entry.data:
await hass.config_entries.flow.async_init(DOMAIN, context={"source": SOURCE_IMPORT}, data=config)

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():
Expand All @@ -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(".")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 25 additions & 8 deletions custom_components/pyscript/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Config flow for pyscript."""
import json
from typing import Any, Dict

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import SOURCE_IMPORT

from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN

Expand All @@ -18,7 +20,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:
Expand All @@ -29,20 +31,35 @@ 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."""
# Convert OrderedDict to dict
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)
if entries:
entry = entries[0]
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)
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)
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")
Expand Down
73 changes: 71 additions & 2 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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({})
)
Expand All @@ -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(
Expand All @@ -105,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}}}
16 changes: 3 additions & 13 deletions tests/test_decorator_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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: {}})
Expand Down
16 changes: 3 additions & 13 deletions tests/test_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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: {}})
Expand Down
43 changes: 11 additions & 32 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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={}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading