canvas 0.13.3__py3-none-any.whl → 0.15.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of canvas might be problematic. Click here for more details.
- {canvas-0.13.3.dist-info → canvas-0.15.0.dist-info}/METADATA +1 -3
- {canvas-0.13.3.dist-info → canvas-0.15.0.dist-info}/RECORD +69 -46
- canvas_cli/templates/plugins/default/{{ cookiecutter.__project_slug }}/protocols/my_protocol.py +1 -1
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +6 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +8 -0
- canvas_sdk/commands/tests/protocol/tests.py +3 -1
- canvas_sdk/commands/tests/test_utils.py +76 -18
- canvas_sdk/effects/banner_alert/tests.py +41 -20
- canvas_sdk/effects/questionnaire_result.py +29 -0
- canvas_sdk/events/base.py +1 -3
- canvas_sdk/handlers/action_button.py +5 -2
- canvas_sdk/handlers/application.py +1 -1
- canvas_sdk/handlers/cron_task.py +1 -1
- canvas_sdk/protocols/clinical_quality_measure.py +1 -1
- canvas_sdk/v1/apps.py +7 -0
- canvas_sdk/v1/data/__init__.py +75 -4
- canvas_sdk/v1/data/allergy_intolerance.py +3 -7
- canvas_sdk/v1/data/billing.py +2 -5
- canvas_sdk/v1/data/command.py +19 -8
- canvas_sdk/v1/data/condition.py +3 -7
- canvas_sdk/v1/data/detected_issue.py +4 -9
- canvas_sdk/v1/data/device.py +4 -8
- canvas_sdk/v1/data/imaging.py +12 -17
- canvas_sdk/v1/data/lab.py +16 -29
- canvas_sdk/v1/data/medication.py +3 -7
- canvas_sdk/v1/data/note.py +7 -14
- canvas_sdk/v1/data/observation.py +4 -11
- canvas_sdk/v1/data/organization.py +1 -2
- canvas_sdk/v1/data/patient.py +0 -1
- canvas_sdk/v1/data/practicelocation.py +2 -4
- canvas_sdk/v1/data/protocol_override.py +3 -6
- canvas_sdk/v1/data/questionnaire.py +5 -15
- canvas_sdk/v1/data/staff.py +5 -7
- canvas_sdk/v1/data/task.py +5 -11
- canvas_sdk/v1/data/user.py +0 -1
- canvas_sdk/v1/models.py +4 -0
- plugin_runner/aws_headers.py +77 -0
- plugin_runner/plugin_installer.py +26 -8
- plugin_runner/plugin_runner.py +5 -25
- plugin_runner/sandbox.py +105 -9
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/CANVAS_MANIFEST.json +38 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/README.md +11 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/protocols/my_protocol.py +33 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/__init__.py +3 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/templates/base.py +6 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/__init__.py +5 -0
- plugin_runner/tests/fixtures/plugins/test_implicit_imports_plugin/utils/base.py +4 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/base.py +10 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/CANVAS_MANIFEST.json +29 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/README.md +12 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/other_module/base.py +10 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/__init__.py +0 -0
- plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_runtime_plugin/protocols/my_protocol.py +18 -0
- plugin_runner/tests/test_application.py +9 -9
- plugin_runner/tests/test_plugin_installer.py +22 -13
- plugin_runner/tests/test_plugin_runner.py +171 -32
- plugin_runner/tests/test_sandbox.py +18 -14
- settings.py +7 -1
- canvas_sdk/models/__init__.py +0 -7
- {canvas-0.13.3.dist-info → canvas-0.15.0.dist-info}/WHEEL +0 -0
- {canvas-0.13.3.dist-info → canvas-0.15.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from canvas_sdk.effects import Effect
|
|
2
|
+
from canvas_sdk.events import EventType
|
|
3
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
4
|
+
from logger import log
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Forbidden(BaseProtocol):
|
|
8
|
+
"""You should put a helpful description of this protocol's behavior here."""
|
|
9
|
+
|
|
10
|
+
# Name the event type you wish to run in response to
|
|
11
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
12
|
+
|
|
13
|
+
def compute(self) -> list[Effect]:
|
|
14
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
15
|
+
from test_implicit_imports_plugin.utils.base import OtherClass
|
|
16
|
+
|
|
17
|
+
OtherClass()
|
|
18
|
+
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Allowed(BaseProtocol):
|
|
23
|
+
"""You should put a helpful description of this protocol's behavior here."""
|
|
24
|
+
|
|
25
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
26
|
+
|
|
27
|
+
def compute(self) -> list[Effect]:
|
|
28
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
29
|
+
from test_implicit_imports_plugin.templates import Template
|
|
30
|
+
|
|
31
|
+
log.info(Template().render())
|
|
32
|
+
|
|
33
|
+
return []
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/CANVAS_MANIFEST.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "test_module_forbidden_imports_plugin",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "test_module_forbidden_imports_plugin.protocols.my_protocol:Protocol",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"content": [],
|
|
20
|
+
"effects": [],
|
|
21
|
+
"views": []
|
|
22
|
+
},
|
|
23
|
+
"secrets": [],
|
|
24
|
+
"tags": {},
|
|
25
|
+
"references": [],
|
|
26
|
+
"license": "",
|
|
27
|
+
"diagram": false,
|
|
28
|
+
"readme": "./README.md"
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
==========================
|
|
2
|
+
test_module_forbidden_imports_plugin
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
A description of this plugin
|
|
8
|
+
|
|
9
|
+
### Important Note!
|
|
10
|
+
|
|
11
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
12
|
+
gets updated if you add, remove, or rename protocols.
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/other_module/__init__.py
ADDED
|
File without changes
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/__init__.py
ADDED
|
File without changes
|
plugin_runner/tests/fixtures/plugins/test_module_forbidden_imports_plugin/protocols/my_protocol.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from test_module_forbidden_imports_plugin.other_module.base import import_me
|
|
2
|
+
|
|
3
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
4
|
+
from canvas_sdk.events import EventType
|
|
5
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Protocol(BaseProtocol):
|
|
9
|
+
"""
|
|
10
|
+
You should put a helpful description of this protocol's behavior here.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# Name the event type you wish to run in response to
|
|
14
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
15
|
+
|
|
16
|
+
def compute(self) -> list[Effect]:
|
|
17
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
18
|
+
return [Effect(type=EffectType.LOG, payload=import_me())]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sdk_version": "0.1.4",
|
|
3
|
+
"plugin_version": "0.0.1",
|
|
4
|
+
"name": "test_module_forbidden_imports_runtime_plugin",
|
|
5
|
+
"description": "Edit the description in CANVAS_MANIFEST.json",
|
|
6
|
+
"components": {
|
|
7
|
+
"protocols": [
|
|
8
|
+
{
|
|
9
|
+
"class": "test_module_forbidden_imports_runtime_plugin.protocols.my_protocol:Protocol",
|
|
10
|
+
"description": "A protocol that does xyz...",
|
|
11
|
+
"data_access": {
|
|
12
|
+
"event": "",
|
|
13
|
+
"read": [],
|
|
14
|
+
"write": []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"commands": [],
|
|
19
|
+
"content": [],
|
|
20
|
+
"effects": [],
|
|
21
|
+
"views": []
|
|
22
|
+
},
|
|
23
|
+
"secrets": [],
|
|
24
|
+
"tags": {},
|
|
25
|
+
"references": [],
|
|
26
|
+
"license": "",
|
|
27
|
+
"diagram": false,
|
|
28
|
+
"readme": "./README.md"
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
==========================
|
|
2
|
+
test_module_forbidden_imports_runtime_plugin
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
A description of this plugin
|
|
8
|
+
|
|
9
|
+
### Important Note!
|
|
10
|
+
|
|
11
|
+
The CANVAS_MANIFEST.json is used when installing your plugin. Please ensure it
|
|
12
|
+
gets updated if you add, remove, or rename protocols.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from canvas_sdk.effects import Effect, EffectType
|
|
2
|
+
from canvas_sdk.events import EventType
|
|
3
|
+
from canvas_sdk.protocols import BaseProtocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Protocol(BaseProtocol):
|
|
7
|
+
"""
|
|
8
|
+
You should put a helpful description of this protocol's behavior here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Name the event type you wish to run in response to
|
|
12
|
+
RESPONDS_TO = EventType.Name(EventType.UNKNOWN)
|
|
13
|
+
|
|
14
|
+
def compute(self) -> list[Effect]:
|
|
15
|
+
"""This method gets called when an event of the type RESPONDS_TO is fired."""
|
|
16
|
+
from test_module_forbidden_imports_runtime_plugin.other_module.base import import_me
|
|
17
|
+
|
|
18
|
+
return [Effect(type=EffectType.LOG, payload=import_me())]
|
|
@@ -6,7 +6,7 @@ from canvas_sdk.events import Event, EventRequest, EventType
|
|
|
6
6
|
from canvas_sdk.handlers.application import Application
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
9
|
+
class ExampleApplication(Application):
|
|
10
10
|
"""A concrete implementation of the Application class for testing."""
|
|
11
11
|
|
|
12
12
|
def on_open(self) -> Effect:
|
|
@@ -15,9 +15,9 @@ class TestApplication(Application):
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
@pytest.fixture
|
|
18
|
-
def app_instance(event: Event) ->
|
|
18
|
+
def app_instance(event: Event) -> ExampleApplication:
|
|
19
19
|
"""Provide an instance of the TestApplication with a mocked event."""
|
|
20
|
-
app =
|
|
20
|
+
app = ExampleApplication(event)
|
|
21
21
|
return app
|
|
22
22
|
|
|
23
23
|
|
|
@@ -25,7 +25,7 @@ def test_compute_event_not_targeted() -> None:
|
|
|
25
25
|
"""Test that compute filters out events not targeted for the app."""
|
|
26
26
|
request = EventRequest(type=EventType.APPLICATION__ON_OPEN, target="some_identifier")
|
|
27
27
|
event = Event(request)
|
|
28
|
-
app =
|
|
28
|
+
app = ExampleApplication(event)
|
|
29
29
|
|
|
30
30
|
result = app.compute()
|
|
31
31
|
|
|
@@ -36,10 +36,10 @@ def test_compute_event_targeted() -> None:
|
|
|
36
36
|
"""Test that compute processes events targeted for the app."""
|
|
37
37
|
request = EventRequest(
|
|
38
38
|
type=EventType.APPLICATION__ON_OPEN,
|
|
39
|
-
target=f"{
|
|
39
|
+
target=f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}",
|
|
40
40
|
)
|
|
41
41
|
event = Event(request)
|
|
42
|
-
app =
|
|
42
|
+
app = ExampleApplication(event)
|
|
43
43
|
result = app.compute()
|
|
44
44
|
|
|
45
45
|
assert len(result) == 1, "Expected a single effect if the event target is the app identifier"
|
|
@@ -48,13 +48,13 @@ def test_compute_event_targeted() -> None:
|
|
|
48
48
|
|
|
49
49
|
def test_identifier_property() -> None:
|
|
50
50
|
"""Test the identifier property of the Application class."""
|
|
51
|
-
expected_identifier = f"{
|
|
51
|
+
expected_identifier = f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}"
|
|
52
52
|
request = EventRequest(
|
|
53
53
|
type=EventType.APPLICATION__ON_OPEN,
|
|
54
|
-
target=f"{
|
|
54
|
+
target=f"{ExampleApplication.__module__}:{ExampleApplication.__qualname__}",
|
|
55
55
|
)
|
|
56
56
|
event = Event(request)
|
|
57
|
-
app =
|
|
57
|
+
app = ExampleApplication(event)
|
|
58
58
|
|
|
59
59
|
assert app.identifier == expected_identifier, "The identifier property is incorrect"
|
|
60
60
|
|
|
@@ -2,11 +2,10 @@ import json
|
|
|
2
2
|
import tarfile
|
|
3
3
|
import tempfile
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from unittest.mock import MagicMock
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
6
|
|
|
7
7
|
from pytest_mock import MockerFixture
|
|
8
8
|
|
|
9
|
-
import settings
|
|
10
9
|
from plugin_runner.plugin_installer import (
|
|
11
10
|
PluginAttributes,
|
|
12
11
|
_extract_rows_to_dict,
|
|
@@ -84,8 +83,19 @@ def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
|
|
|
84
83
|
tarball_2 = _create_tarball("plugin2")
|
|
85
84
|
|
|
86
85
|
mocker.patch("plugin_runner.plugin_installer.enabled_plugins", return_value=mock_plugins)
|
|
86
|
+
|
|
87
|
+
def mock_download_plugin(package: str) -> MagicMock:
|
|
88
|
+
mock_context = mocker.Mock()
|
|
89
|
+
if package == "plugins/plugin1.tar.gz":
|
|
90
|
+
mock_context.__enter__ = mocker.Mock(return_value=tarball_1)
|
|
91
|
+
elif package == "plugins/plugin2.tar":
|
|
92
|
+
mock_context.__enter__ = mocker.Mock(return_value=tarball_2)
|
|
93
|
+
mock_context.__exit__ = mocker.Mock(return_value=None)
|
|
94
|
+
return mock_context
|
|
95
|
+
|
|
87
96
|
mocker.patch(
|
|
88
|
-
"plugin_runner.plugin_installer.download_plugin",
|
|
97
|
+
"plugin_runner.plugin_installer.download_plugin",
|
|
98
|
+
side_effect=mock_download_plugin,
|
|
89
99
|
)
|
|
90
100
|
|
|
91
101
|
install_plugins()
|
|
@@ -106,13 +116,12 @@ def test_plugin_installation_from_tarball(mocker: MockerFixture) -> None:
|
|
|
106
116
|
|
|
107
117
|
def test_download(mocker: MockerFixture) -> None:
|
|
108
118
|
"""Test that the plugin package can be written to disk, mocking out S3."""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
119
|
+
mock_response = MagicMock()
|
|
120
|
+
mock_response.status_code = 200
|
|
121
|
+
mock_response.content = b"some content in a file"
|
|
122
|
+
with patch("requests.request", return_value=mock_response) as mock_request:
|
|
123
|
+
plugin_package = "plugins/plugin1.tar.gz"
|
|
124
|
+
with download_plugin(plugin_package) as plugin_path:
|
|
125
|
+
assert plugin_path.exists()
|
|
126
|
+
assert plugin_path.read_bytes() == b"some content in a file"
|
|
127
|
+
mock_request.assert_called_once()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import shutil
|
|
2
3
|
from collections.abc import Generator
|
|
3
4
|
from pathlib import Path
|
|
@@ -7,18 +8,18 @@ import pytest
|
|
|
7
8
|
|
|
8
9
|
from canvas_generated.messages.effects_pb2 import EffectType
|
|
9
10
|
from canvas_generated.messages.plugins_pb2 import ReloadPluginsRequest
|
|
10
|
-
from canvas_sdk.events import EventRequest, EventType
|
|
11
|
+
from canvas_sdk.events import Event, EventRequest, EventType
|
|
11
12
|
from plugin_runner.plugin_runner import (
|
|
12
13
|
EVENT_HANDLER_MAP,
|
|
13
14
|
LOADED_PLUGINS,
|
|
14
15
|
PluginRunner,
|
|
16
|
+
load_or_reload_plugin,
|
|
15
17
|
load_plugins,
|
|
16
|
-
sandbox_from_package,
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@pytest.fixture
|
|
21
|
-
def
|
|
22
|
+
def install_test_plugin(request: pytest.FixtureRequest) -> Generator[Path, None, None]:
|
|
22
23
|
"""Copies a specified plugin from the fixtures directory to the data directory
|
|
23
24
|
and removes it after the test.
|
|
24
25
|
|
|
@@ -51,6 +52,17 @@ def setup_test_plugin(request: pytest.FixtureRequest) -> Generator[Path, None, N
|
|
|
51
52
|
shutil.rmtree(dest_plugin_path)
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def load_test_plugins() -> Generator[None, None, None]:
|
|
57
|
+
"""Manages the lifecycle of test plugins by loading and unloading them."""
|
|
58
|
+
try:
|
|
59
|
+
load_plugins()
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
LOADED_PLUGINS.clear()
|
|
63
|
+
EVENT_HANDLER_MAP.clear()
|
|
64
|
+
|
|
65
|
+
|
|
54
66
|
@pytest.fixture
|
|
55
67
|
def plugin_runner() -> PluginRunner:
|
|
56
68
|
"""Fixture to initialize PluginRunner with mocks."""
|
|
@@ -59,11 +71,9 @@ def plugin_runner() -> PluginRunner:
|
|
|
59
71
|
return runner
|
|
60
72
|
|
|
61
73
|
|
|
62
|
-
@pytest.mark.parametrize("
|
|
63
|
-
def test_load_plugins_with_valid_plugin(
|
|
74
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
75
|
+
def test_load_plugins_with_valid_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
|
|
64
76
|
"""Test loading plugins with a valid plugin."""
|
|
65
|
-
load_plugins()
|
|
66
|
-
|
|
67
77
|
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
68
78
|
assert (
|
|
69
79
|
LOADED_PLUGINS["example_plugin:example_plugin.protocols.my_protocol:Protocol"]["active"]
|
|
@@ -72,12 +82,11 @@ def test_load_plugins_with_valid_plugin(setup_test_plugin: Path) -> None:
|
|
|
72
82
|
|
|
73
83
|
|
|
74
84
|
@pytest.mark.asyncio
|
|
75
|
-
@pytest.mark.parametrize("
|
|
85
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
|
|
76
86
|
async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin_package(
|
|
77
|
-
|
|
87
|
+
install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
|
|
78
88
|
) -> None:
|
|
79
89
|
"""Test loading plugins with a valid plugin that imports other modules within the current plugin package."""
|
|
80
|
-
load_plugins()
|
|
81
90
|
assert (
|
|
82
91
|
"test_module_imports_plugin:test_module_imports_plugin.protocols.my_protocol:Protocol"
|
|
83
92
|
in LOADED_PLUGINS
|
|
@@ -102,7 +111,7 @@ async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin
|
|
|
102
111
|
|
|
103
112
|
|
|
104
113
|
@pytest.mark.parametrize(
|
|
105
|
-
"
|
|
114
|
+
"install_test_plugin",
|
|
106
115
|
[
|
|
107
116
|
"test_module_imports_outside_plugin_v1",
|
|
108
117
|
"test_module_imports_outside_plugin_v2",
|
|
@@ -111,18 +120,108 @@ async def test_load_plugins_with_plugin_that_imports_other_modules_within_plugin
|
|
|
111
120
|
indirect=True,
|
|
112
121
|
)
|
|
113
122
|
def test_load_plugins_with_plugin_that_imports_other_modules_outside_plugin_package(
|
|
114
|
-
|
|
123
|
+
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
115
124
|
) -> None:
|
|
116
125
|
"""Test loading plugins with an invalid plugin that imports other modules outside the current plugin package."""
|
|
117
|
-
with
|
|
118
|
-
|
|
126
|
+
with caplog.at_level(logging.ERROR):
|
|
127
|
+
load_or_reload_plugin(install_test_plugin)
|
|
128
|
+
|
|
129
|
+
assert any(
|
|
130
|
+
"Error importing module" in record.message for record in caplog.records
|
|
131
|
+
), "log.error() was not called with the expected message."
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.parametrize(
|
|
135
|
+
"install_test_plugin",
|
|
136
|
+
[
|
|
137
|
+
"test_module_forbidden_imports_plugin",
|
|
138
|
+
],
|
|
139
|
+
indirect=True,
|
|
140
|
+
)
|
|
141
|
+
def test_load_plugins_with_plugin_that_imports_forbidden_modules(
|
|
142
|
+
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
143
|
+
) -> None:
|
|
144
|
+
"""Test loading plugins with an invalid plugin that imports forbidden modules."""
|
|
145
|
+
with caplog.at_level(logging.ERROR):
|
|
146
|
+
load_or_reload_plugin(install_test_plugin)
|
|
147
|
+
|
|
148
|
+
assert any(
|
|
149
|
+
"Error importing module" in record.message for record in caplog.records
|
|
150
|
+
), "log.error() was not called with the expected message."
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.parametrize(
|
|
154
|
+
"install_test_plugin",
|
|
155
|
+
[
|
|
156
|
+
"test_module_forbidden_imports_runtime_plugin",
|
|
157
|
+
],
|
|
158
|
+
indirect=True,
|
|
159
|
+
)
|
|
160
|
+
def test_load_plugins_with_plugin_that_imports_forbidden_modules_at_runtime(
|
|
161
|
+
install_test_plugin: Path,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Test loading plugins with an invalid plugin that imports forbidden modules at runtime."""
|
|
164
|
+
with pytest.raises(ImportError, match="is not an allowed import."):
|
|
165
|
+
load_or_reload_plugin(install_test_plugin)
|
|
166
|
+
class_handler = LOADED_PLUGINS[
|
|
167
|
+
"test_module_forbidden_imports_runtime_plugin:test_module_forbidden_imports_runtime_plugin.protocols.my_protocol:Protocol"
|
|
168
|
+
]["class"]
|
|
169
|
+
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.parametrize(
|
|
173
|
+
"install_test_plugin",
|
|
174
|
+
[
|
|
175
|
+
"test_implicit_imports_plugin",
|
|
176
|
+
],
|
|
177
|
+
indirect=True,
|
|
178
|
+
)
|
|
179
|
+
def test_plugin_that_implicitly_imports_allowed_modules(
|
|
180
|
+
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Test loading plugins with a plugin that implicitly imports allowed modules."""
|
|
183
|
+
with caplog.at_level(logging.INFO):
|
|
184
|
+
load_or_reload_plugin(install_test_plugin)
|
|
185
|
+
class_handler = LOADED_PLUGINS[
|
|
186
|
+
"test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Allowed"
|
|
187
|
+
]["class"]
|
|
188
|
+
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
189
|
+
|
|
190
|
+
assert any(
|
|
191
|
+
"Hello, World!" in record.message for record in caplog.records
|
|
192
|
+
), "log.info() with Template.render() was not called."
|
|
119
193
|
|
|
120
194
|
|
|
121
|
-
@pytest.mark.parametrize(
|
|
122
|
-
|
|
195
|
+
@pytest.mark.parametrize(
|
|
196
|
+
"install_test_plugin",
|
|
197
|
+
[
|
|
198
|
+
"test_implicit_imports_plugin",
|
|
199
|
+
],
|
|
200
|
+
indirect=True,
|
|
201
|
+
)
|
|
202
|
+
def test_plugin_that_implicitly_imports_forbidden_modules(
|
|
203
|
+
install_test_plugin: Path, caplog: pytest.LogCaptureFixture
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Test loading plugins with an invalid plugin that implicitly imports forbidden modules."""
|
|
206
|
+
with (
|
|
207
|
+
caplog.at_level(logging.INFO),
|
|
208
|
+
pytest.raises(ImportError, match="'os' is not an allowed import."),
|
|
209
|
+
):
|
|
210
|
+
load_or_reload_plugin(install_test_plugin)
|
|
211
|
+
class_handler = LOADED_PLUGINS[
|
|
212
|
+
"test_implicit_imports_plugin:test_implicit_imports_plugin.protocols.my_protocol:Forbidden"
|
|
213
|
+
]["class"]
|
|
214
|
+
class_handler(Event(EventRequest(type=EventType.UNKNOWN))).compute()
|
|
215
|
+
|
|
216
|
+
assert (
|
|
217
|
+
any("os list dir" in record.message for record in caplog.records) is False
|
|
218
|
+
), "log.info() with os.listdir() was called."
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
222
|
+
def test_reload_plugin(install_test_plugin: Path, load_test_plugins: None) -> None:
|
|
123
223
|
"""Test reloading a plugin."""
|
|
124
224
|
load_plugins()
|
|
125
|
-
load_plugins()
|
|
126
225
|
|
|
127
226
|
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
128
227
|
assert (
|
|
@@ -131,18 +230,22 @@ def test_reload_plugin(setup_test_plugin: Path) -> None:
|
|
|
131
230
|
)
|
|
132
231
|
|
|
133
232
|
|
|
134
|
-
@pytest.mark.parametrize("
|
|
135
|
-
def test_remove_plugin_should_be_removed_from_loaded_plugins(
|
|
233
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
234
|
+
def test_remove_plugin_should_be_removed_from_loaded_plugins(
|
|
235
|
+
install_test_plugin: Path, load_test_plugins: None
|
|
236
|
+
) -> None:
|
|
136
237
|
"""Test removing a plugin."""
|
|
137
|
-
load_plugins()
|
|
138
238
|
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" in LOADED_PLUGINS
|
|
139
|
-
shutil.rmtree(
|
|
239
|
+
shutil.rmtree(install_test_plugin)
|
|
140
240
|
load_plugins()
|
|
141
241
|
assert "example_plugin:example_plugin.protocols.my_protocol:Protocol" not in LOADED_PLUGINS
|
|
142
242
|
|
|
143
243
|
|
|
144
|
-
@pytest.mark.parametrize("
|
|
145
|
-
|
|
244
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
245
|
+
@pytest.mark.parametrize("load_test_plugins", [None], indirect=True)
|
|
246
|
+
def test_load_plugins_should_refresh_event_protocol_map(
|
|
247
|
+
load_test_plugins: None, install_test_plugin: Path
|
|
248
|
+
) -> None:
|
|
146
249
|
"""Test that the event protocol map is refreshed when loading plugins."""
|
|
147
250
|
assert EVENT_HANDLER_MAP == {}
|
|
148
251
|
load_plugins()
|
|
@@ -153,13 +256,11 @@ def test_load_plugins_should_refresh_event_protocol_map(setup_test_plugin: Path)
|
|
|
153
256
|
|
|
154
257
|
|
|
155
258
|
@pytest.mark.asyncio
|
|
156
|
-
@pytest.mark.parametrize("
|
|
259
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
157
260
|
async def test_handle_plugin_event_returns_expected_result(
|
|
158
|
-
|
|
261
|
+
install_test_plugin: Path, plugin_runner: PluginRunner, load_test_plugins: None
|
|
159
262
|
) -> None:
|
|
160
263
|
"""Test that HandleEvent successfully calls the relevant plugins and returns the expected result."""
|
|
161
|
-
load_plugins()
|
|
162
|
-
|
|
163
264
|
event = EventRequest(type=EventType.UNKNOWN)
|
|
164
265
|
|
|
165
266
|
result = []
|
|
@@ -174,11 +275,11 @@ async def test_handle_plugin_event_returns_expected_result(
|
|
|
174
275
|
|
|
175
276
|
|
|
176
277
|
@pytest.mark.asyncio
|
|
177
|
-
@pytest.mark.parametrize("
|
|
178
|
-
async def
|
|
179
|
-
|
|
278
|
+
@pytest.mark.parametrize("install_test_plugin", ["example_plugin"], indirect=True)
|
|
279
|
+
async def test_reload_plugins_event_handler_successfully_publishes_message(
|
|
280
|
+
install_test_plugin: Path, plugin_runner: PluginRunner
|
|
180
281
|
) -> None:
|
|
181
|
-
"""Test ReloadPlugins Event handler successfully
|
|
282
|
+
"""Test ReloadPlugins Event handler successfully publishes a message with restart action."""
|
|
182
283
|
with patch("plugin_runner.plugin_runner.publish_message", MagicMock()) as mock_publish_message:
|
|
183
284
|
request = ReloadPluginsRequest()
|
|
184
285
|
|
|
@@ -190,4 +291,42 @@ async def test_reload_plugins_event_handler_successfully_loads_plugins(
|
|
|
190
291
|
|
|
191
292
|
assert len(result) == 1
|
|
192
293
|
assert result[0].success is True
|
|
193
|
-
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@pytest.mark.asyncio
|
|
297
|
+
@pytest.mark.parametrize("install_test_plugin", ["test_module_imports_plugin"], indirect=True)
|
|
298
|
+
async def test_changes_to_plugin_modules_should_be_reflected_after_reload(
|
|
299
|
+
install_test_plugin: Path, load_test_plugins: None, plugin_runner: PluginRunner
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Test that changes to plugin modules are reflected after reloading the plugin."""
|
|
302
|
+
event = EventRequest(type=EventType.UNKNOWN)
|
|
303
|
+
|
|
304
|
+
result = []
|
|
305
|
+
async for response in plugin_runner.HandleEvent(event, None):
|
|
306
|
+
result.append(response)
|
|
307
|
+
|
|
308
|
+
assert len(result) == 1
|
|
309
|
+
assert result[0].success is True
|
|
310
|
+
assert len(result[0].effects) == 1
|
|
311
|
+
assert result[0].effects[0].type == EffectType.LOG
|
|
312
|
+
assert result[0].effects[0].payload == "Successfully imported!"
|
|
313
|
+
|
|
314
|
+
NEW_CODE = """
|
|
315
|
+
def import_me() -> str:
|
|
316
|
+
return "Successfully changed!"
|
|
317
|
+
"""
|
|
318
|
+
file_path = install_test_plugin / "other_module" / "base.py"
|
|
319
|
+
file_path.write_text(NEW_CODE, encoding="utf-8")
|
|
320
|
+
|
|
321
|
+
# Reload the plugin
|
|
322
|
+
load_plugins()
|
|
323
|
+
|
|
324
|
+
result = []
|
|
325
|
+
async for response in plugin_runner.HandleEvent(event, None):
|
|
326
|
+
result.append(response)
|
|
327
|
+
|
|
328
|
+
assert len(result) == 1
|
|
329
|
+
assert result[0].success is True
|
|
330
|
+
assert len(result[0].effects) == 1
|
|
331
|
+
assert result[0].effects[0].type == EffectType.LOG
|
|
332
|
+
assert result[0].effects[0].payload == "Successfully changed!"
|