-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #992 from dimagi/fr/code-node
Add CodeNode
- Loading branch information
Showing
10 changed files
with
655 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"description": "Runs python", | ||
"properties": { | ||
"code": { | ||
"default": "# You must define a main function, which takes the node input as a string.\n# Return a string to pass to the next node.\ndef main(input: str, **kwargs) -> str:\n return input\n", | ||
"description": "The code to run", | ||
"title": "Code", | ||
"type": "string", | ||
"ui:widget": "code" | ||
} | ||
}, | ||
"title": "CodeNode", | ||
"type": "object", | ||
"ui:flow_node_type": "pipelineNode", | ||
"ui:label": "Python Node" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import json | ||
from unittest import mock | ||
|
||
import pytest | ||
|
||
from apps.pipelines.exceptions import PipelineNodeBuildError, PipelineNodeRunError | ||
from apps.pipelines.nodes.base import PipelineState | ||
from apps.pipelines.tests.utils import ( | ||
code_node, | ||
create_runnable, | ||
end_node, | ||
start_node, | ||
) | ||
from apps.utils.factories.pipelines import PipelineFactory | ||
from apps.utils.pytest import django_db_with_data | ||
|
||
|
||
@pytest.fixture() | ||
def pipeline(): | ||
return PipelineFactory() | ||
|
||
|
||
IMPORTS = """ | ||
import json | ||
import datetime | ||
import re | ||
import time | ||
def main(input, **kwargs): | ||
return json.loads(input) | ||
""" | ||
|
||
|
||
@django_db_with_data(available_apps=("apps.service_providers",)) | ||
@mock.patch("apps.pipelines.nodes.base.PipelineNode.logger", mock.Mock()) | ||
@pytest.mark.parametrize( | ||
("code", "input", "output"), | ||
[ | ||
("def main(input, **kwargs):\n\treturn f'Hello, {input}!'", "World", "Hello, World!"), | ||
("", "foo", "foo"), # No code just returns the input | ||
("def main(input, **kwargs):\n\t'foo'", "", "None"), # No return value will return "None" | ||
(IMPORTS, json.dumps({"a": "b"}), str(json.loads('{"a": "b"}'))), # Importing json will work | ||
], | ||
) | ||
def test_code_node(pipeline, code, input, output): | ||
nodes = [ | ||
start_node(), | ||
code_node(code), | ||
end_node(), | ||
] | ||
assert create_runnable(pipeline, nodes).invoke(PipelineState(messages=[input]))["messages"][-1] == output | ||
|
||
|
||
EXTRA_FUNCTION = """ | ||
def other(foo): | ||
return f"other {foo}" | ||
def main(input, **kwargs): | ||
return other(input) | ||
""" | ||
|
||
|
||
@django_db_with_data(available_apps=("apps.service_providers",)) | ||
@mock.patch("apps.pipelines.nodes.base.PipelineNode.logger", mock.Mock()) | ||
@pytest.mark.parametrize( | ||
("code", "input", "error"), | ||
[ | ||
("this{}", "", "SyntaxError: invalid syntax at statement: 'this{}"), | ||
( | ||
EXTRA_FUNCTION, | ||
"", | ||
( | ||
"You can only define a single function, 'main' at the top level. " | ||
"You may use nested functions inside that function if required" | ||
), | ||
), | ||
("def other(input):\n\treturn input", "", "You must define a 'main' function"), | ||
( | ||
"def main(input, others, **kwargs):\n\treturn input", | ||
"", | ||
r"The main function should have the signature main\(input, \*\*kwargs\) only\.", | ||
), | ||
], | ||
) | ||
def test_code_node_build_errors(pipeline, code, input, error): | ||
nodes = [ | ||
start_node(), | ||
code_node(code), | ||
end_node(), | ||
] | ||
with pytest.raises(PipelineNodeBuildError, match=error): | ||
create_runnable(pipeline, nodes).invoke(PipelineState(messages=[input]))["messages"][-1] | ||
|
||
|
||
@django_db_with_data(available_apps=("apps.service_providers",)) | ||
@mock.patch("apps.pipelines.nodes.base.PipelineNode.logger", mock.Mock()) | ||
@pytest.mark.parametrize( | ||
("code", "input", "error"), | ||
[ | ||
( | ||
"import collections\ndef main(input, **kwargs):\n\treturn input", | ||
"", | ||
"Importing 'collections' is not allowed", | ||
), | ||
("def main(input, **kwargs):\n\treturn f'Hello, {blah}!'", "", "name 'blah' is not defined"), | ||
], | ||
) | ||
def test_code_node_runtime_errors(pipeline, code, input, error): | ||
nodes = [ | ||
start_node(), | ||
code_node(code), | ||
end_node(), | ||
] | ||
with pytest.raises(PipelineNodeRunError, match=error): | ||
create_runnable(pipeline, nodes).invoke(PipelineState(messages=[input]))["messages"][-1] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.