Skip to content

Commit 0e19499

Browse files
committed
support reloading allow all imports through reload service and config entry options UI
1 parent 01d752d commit 0e19499

File tree

7 files changed

+149
-8
lines changed

7 files changed

+149
-8
lines changed

custom_components/pyscript/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ async def async_setup_entry(hass, config_entry):
6565
await hass.async_add_executor_job(os.makedirs, pyscript_folder)
6666

6767
hass.data.setdefault(DOMAIN, {})
68-
hass.data[DOMAIN][CONF_ALLOW_ALL_IMPORTS] = config_entry.data.get(CONF_ALLOW_ALL_IMPORTS)
68+
hass.data[DOMAIN] = config_entry
6969

7070
State.set_pyscript_config(config_entry.data)
7171

custom_components/pyscript/config_flow.py

+42-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import voluptuous as vol
66

77
from homeassistant import config_entries
8-
from homeassistant.config_entries import SOURCE_IMPORT
8+
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
9+
from homeassistant.core import callback
910

1011
from .const import CONF_ALLOW_ALL_IMPORTS, DOMAIN
1112

@@ -14,12 +15,52 @@
1415
)
1516

1617

18+
class PyscriptOptionsConfigFlow(config_entries.OptionsFlow):
19+
"""Handle a pyscript options flow."""
20+
21+
def __init__(self, config_entry: ConfigEntry) -> None:
22+
"""Initialize pyscript options flow."""
23+
self.config_entry = config_entry
24+
25+
async def async_step_init(self, user_input: Dict[str, Any] = None) -> Dict[str, Any]:
26+
"""Manage the pyscript options."""
27+
if self.config_entry.source == SOURCE_IMPORT:
28+
return self.async_abort(reason="no_ui_configuration_allowed")
29+
30+
if user_input is None:
31+
return self.async_show_form(
32+
step_id="init",
33+
data_schema=vol.Schema(
34+
{
35+
vol.Optional(
36+
CONF_ALLOW_ALL_IMPORTS, default=self.config_entry.data[CONF_ALLOW_ALL_IMPORTS],
37+
): bool
38+
},
39+
extra=vol.ALLOW_EXTRA,
40+
),
41+
)
42+
43+
if user_input[CONF_ALLOW_ALL_IMPORTS] != self.config_entry.data[CONF_ALLOW_ALL_IMPORTS]:
44+
updated_data = self.config_entry.data.copy()
45+
updated_data[CONF_ALLOW_ALL_IMPORTS] = user_input[CONF_ALLOW_ALL_IMPORTS]
46+
self.hass.config_entries.async_update_entry(entry=self.config_entry, data=updated_data)
47+
return self.async_create_entry(title="", data=user_input)
48+
49+
return self.async_abort(reason="no_update")
50+
51+
1752
class PyscriptConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
1853
"""Handle a pyscript config flow."""
1954

2055
VERSION = 1
2156
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH
2257

58+
@staticmethod
59+
@callback
60+
def async_get_options_flow(config_entry: ConfigEntry) -> PyscriptOptionsConfigFlow:
61+
"""Get the options flow for this handler."""
62+
return PyscriptOptionsConfigFlow(config_entry)
63+
2364
async def async_step_user(self, user_input: Dict[str, Any] = None) -> Dict[str, Any]:
2465
"""Handle a flow initialized by the user."""
2566
if user_input is not None:

custom_components/pyscript/eval.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from homeassistant.const import SERVICE_RELOAD
1717
from homeassistant.helpers.service import async_set_service_schema
1818

19-
from .const import ALLOWED_IMPORTS, DOMAIN, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
19+
from .const import ALLOWED_IMPORTS, CONF_ALLOW_ALL_IMPORTS, DOMAIN, LOGGER_PATH, SERVICE_JUPYTER_KERNEL_START
2020
from .function import Function
2121
from .state import State
2222

@@ -721,7 +721,7 @@ def __init__(self, name, global_ctx, logger_name=None):
721721
self.logger_handlers = set()
722722
self.logger = None
723723
self.set_logger_name(logger_name if logger_name is not None else self.name)
724-
self.allow_all_imports = Function.hass.data.get(DOMAIN, {}).get("allow_all_imports", False)
724+
self.config_entry = Function.hass.data.get(DOMAIN, {})
725725

726726
async def ast_not_implemented(self, arg, *args):
727727
"""Raise NotImplementedError exception for unimplemented AST types."""
@@ -769,7 +769,10 @@ async def ast_import(self, arg):
769769
self.exception_long = error_ctx.exception_long
770770
raise self.exception_obj
771771
if not mod:
772-
if not self.allow_all_imports and imp.name not in ALLOWED_IMPORTS:
772+
if (
773+
not self.config_entry.data.get(CONF_ALLOW_ALL_IMPORTS, False)
774+
and imp.name not in ALLOWED_IMPORTS
775+
):
773776
raise ModuleNotFoundError(f"import of {imp.name} not allowed")
774777
if imp.name not in sys.modules:
775778
mod = await Function.hass.async_add_executor_job(importlib.import_module, imp.name)
@@ -799,7 +802,10 @@ async def ast_importfrom(self, arg):
799802
self.exception_long = error_ctx.exception_long
800803
raise self.exception_obj
801804
if not mod:
802-
if not self.allow_all_imports and arg.module not in ALLOWED_IMPORTS:
805+
if (
806+
not self.config_entry.data.get(CONF_ALLOW_ALL_IMPORTS, False)
807+
and arg.module not in ALLOWED_IMPORTS
808+
):
803809
raise ModuleNotFoundError(f"import from {arg.module} not allowed")
804810
if arg.module not in sys.modules:
805811
mod = await Function.hass.async_add_executor_job(importlib.import_module, arg.module)

custom_components/pyscript/strings.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,19 @@
1414
"single_instance_allowed": "Already configured. Only a single configuration possible.",
1515
"updated_entry": "This entry has already been setup but the configuration has been updated."
1616
}
17+
},
18+
"options": {
19+
"step": {
20+
"init": {
21+
"title": "Update pyscript configuration",
22+
"data": {
23+
"allow_all_imports": "Allow All Imports?"
24+
}
25+
}
26+
},
27+
"abort": {
28+
"no_ui_configuration_allowed": "This entry was created via `configuration.yaml`, so all configuration parameters must be updated there. The [`pyscript.reload`](developer-tools/service) service will allow you to apply the changes you make to `configuration.yaml` without restarting your Home Assistant instance.",
29+
"no_update": "There was nothing to update."
30+
}
1731
}
18-
}
32+
}

custom_components/pyscript/translations/en.json

+15-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,19 @@
1414
"single_instance_allowed": "Already configured. Only a single configuration possible.",
1515
"updated_entry": "This entry has already been setup but the configuration has been updated."
1616
}
17+
},
18+
"options": {
19+
"step": {
20+
"init": {
21+
"title": "Update pyscript configuration",
22+
"data": {
23+
"allow_all_imports": "Allow All Imports?"
24+
}
25+
}
26+
},
27+
"abort": {
28+
"no_ui_configuration_allowed": "This entry was created via `configuration.yaml`, so all configuration parameters must be updated there. The [`pyscript.reload`](developer-tools/service) service will allow you to apply the changes you make to `configuration.yaml` without restarting your Home Assistant instance.",
29+
"no_update": "There is nothing to update."
30+
}
1731
}
18-
}
32+
}

tests/test_config_flow.py

+62
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,65 @@ async def test_import_flow_update_import(hass):
174174
assert result["reason"] == "updated_entry"
175175

176176
assert hass.config_entries.async_entries(DOMAIN)[0].data == {"apps": {"test_app": {"param": 1}}}
177+
178+
179+
async def test_options_flow_import(hass):
180+
"""Test options flow aborts because configuration needs to be managed via configuration.yaml."""
181+
result = await hass.config_entries.flow.async_init(
182+
DOMAIN, context={"source": SOURCE_IMPORT}, data=PYSCRIPT_SCHEMA({CONF_ALLOW_ALL_IMPORTS: True})
183+
)
184+
await hass.async_block_till_done()
185+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
186+
entry = result["result"]
187+
188+
await hass.config_entries.options.async_init(entry.entry_id, data=None)
189+
190+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
191+
assert result["reason"] == "no_ui_configuration_allowed"
192+
193+
194+
async def test_options_flow_user_change(hass):
195+
"""Test options flow updates config entry when options change."""
196+
result = await hass.config_entries.flow.async_init(
197+
DOMAIN, context={"source": SOURCE_USER}, data=PYSCRIPT_SCHEMA({CONF_ALLOW_ALL_IMPORTS: True})
198+
)
199+
await hass.async_block_till_done()
200+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
201+
entry = result["result"]
202+
203+
result = await hass.config_entries.options.async_init(entry.entry_id)
204+
205+
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
206+
assert result["step_id"] == "init"
207+
208+
result = await hass.config_entries.options.async_configure(
209+
result["flow_id"], user_input={CONF_ALLOW_ALL_IMPORTS: False}
210+
)
211+
212+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
213+
assert result["title"] == ""
214+
assert result["data"][CONF_ALLOW_ALL_IMPORTS] is False
215+
216+
assert entry.data[CONF_ALLOW_ALL_IMPORTS] is False
217+
218+
219+
async def test_options_flow_user_no_change(hass):
220+
"""Test options flow aborts when options don't change."""
221+
result = await hass.config_entries.flow.async_init(
222+
DOMAIN, context={"source": SOURCE_USER}, data=PYSCRIPT_SCHEMA({CONF_ALLOW_ALL_IMPORTS: True})
223+
)
224+
await hass.async_block_till_done()
225+
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
226+
entry = result["result"]
227+
228+
result = await hass.config_entries.options.async_init(entry.entry_id)
229+
230+
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
231+
assert result["step_id"] == "init"
232+
233+
result = await hass.config_entries.options.async_configure(
234+
result["flow_id"], user_input={CONF_ALLOW_ALL_IMPORTS: True}
235+
)
236+
237+
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
238+
assert result["reason"] == "no_update"

tests/test_unit_eval.py

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Unit tests for Python interpreter."""
22

3+
from custom_components.pyscript.const import CONF_ALLOW_ALL_IMPORTS, DOMAIN
34
from custom_components.pyscript.eval import AstEval
45
from custom_components.pyscript.function import Function
56
from custom_components.pyscript.global_ctx import GlobalContext, GlobalContextMgr
67
from custom_components.pyscript.state import State
8+
from pytest_homeassistant_custom_component.common import MockConfigEntry
79

810
evalTests = [
911
["1", 1],
@@ -882,6 +884,7 @@ async def run_one_test(test_data):
882884

883885
async def test_eval(hass):
884886
"""Test interpreter."""
887+
hass.data[DOMAIN] = MockConfigEntry(domain=DOMAIN, data={CONF_ALLOW_ALL_IMPORTS: False})
885888
Function.init(hass)
886889
State.init(hass)
887890
State.register_functions()
@@ -1062,6 +1065,7 @@ async def run_one_test_exception(test_data):
10621065

10631066
async def test_eval_exceptions(hass):
10641067
"""Test interpreter exceptions."""
1068+
hass.data[DOMAIN] = MockConfigEntry(domain=DOMAIN, data={CONF_ALLOW_ALL_IMPORTS: False})
10651069
Function.init(hass)
10661070
State.init(hass)
10671071
State.register_functions()

0 commit comments

Comments
 (0)