Skip to content

Commit d26beef

Browse files
Copilotjan-janssen
andauthored
Add unit tests for filename input handling across all workflow frameworks (#182)
* Initial progress report - planning tests Agent-Logs-Url: https://github.com/pythonworkflow/python-workflow-definition/sessions/d3136b3e-7700-4e91-9463-1cd77bb36e95 Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> * Add unit tests for filename input handling across all workflow frameworks Agent-Logs-Url: https://github.com/pythonworkflow/python-workflow-definition/sessions/d3136b3e-7700-4e91-9463-1cd77bb36e95 Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> * Clean up committed test artifacts and add them to .gitignore Agent-Logs-Url: https://github.com/pythonworkflow/python-workflow-definition/sessions/d3136b3e-7700-4e91-9463-1cd77bb36e95 Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> * Clean up .gitignore by removing temporary files Removed test-generated temporary files from .gitignore. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jan-janssen <3854739+jan-janssen@users.noreply.github.com> Co-authored-by: Jan Janssen <jan-janssen@users.noreply.github.com>
1 parent 3e0e529 commit d26beef

6 files changed

Lines changed: 279 additions & 3 deletions

File tree

tests/test_executorlib.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import unittest
23
from executorlib import SingleNodeExecutor
34
from python_workflow_definition.executorlib import load_workflow_json
@@ -36,6 +37,26 @@ def get_square(x):
3637
]
3738
}"""
3839

40+
echo_function_str = """
41+
def echo(filename):
42+
return filename
43+
"""
44+
45+
filename_workflow_str = """
46+
{
47+
"version": "0.1.0",
48+
"nodes": [
49+
{"id": 0, "type": "function", "value": "echo_module.echo"},
50+
{"id": 1, "type": "input", "value": "image.png", "name": "filename"},
51+
{"id": 2, "type": "output", "name": "result"}
52+
],
53+
"edges": [
54+
{"target": 0, "targetPort": "filename", "source": 1, "sourcePort": null},
55+
{"target": 2, "targetPort": null, "source": 0, "sourcePort": null}
56+
]
57+
}"""
58+
59+
3960
class TestExecutorlib(unittest.TestCase):
4061
def test_executorlib(self):
4162
with open("workflow.py", "w") as f:
@@ -46,3 +67,19 @@ def test_executorlib(self):
4667

4768
with SingleNodeExecutor(max_workers=1) as exe:
4869
self.assertEqual(load_workflow_json(file_name="workflow.json", exe=exe).result(), 6.25)
70+
71+
def test_executorlib_filename_input(self):
72+
"""A filename string like 'image.png' must be passed through as a plain
73+
string input, not interpreted as a Python module path or a float."""
74+
with open("echo_module.py", "w") as f:
75+
f.write(echo_function_str)
76+
sys.modules.pop("echo_module", None)
77+
78+
with open("filename_workflow.json", "w") as f:
79+
f.write(filename_workflow_str)
80+
81+
with SingleNodeExecutor(max_workers=1) as exe:
82+
result = load_workflow_json(
83+
file_name="filename_workflow.json", exe=exe
84+
).result()
85+
self.assertEqual(result, "image.png")

tests/test_jobflow.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import unittest
1+
import json
22
import os
3+
import unittest
34
from jobflow import job, Flow
45
from jobflow.managers.local import run_locally
56
from python_workflow_definition.jobflow import load_workflow_json, write_workflow_json
@@ -17,6 +18,10 @@ def get_square(x):
1718
return x ** 2
1819

1920

21+
def echo(filename):
22+
return filename
23+
24+
2025
class TestJobflow(unittest.TestCase):
2126
def test_jobflow(self):
2227
workflow_json_filename = "jobflow_simple.json"
@@ -33,3 +38,27 @@ def test_jobflow(self):
3338

3439
self.assertTrue(os.path.exists(workflow_json_filename))
3540
self.assertEqual(result[list(result.keys())[-1]][1].output, 6.25)
41+
42+
def test_jobflow_filename_input(self):
43+
"""A filename string like 'image.png' must be passed through as a plain
44+
string input, not interpreted as a Python module path or a float."""
45+
workflow_json_filename = "jobflow_filename.json"
46+
echo_job = job(echo)
47+
result_job = echo_job(filename="image.png")
48+
flow = Flow([result_job])
49+
50+
write_workflow_json(flow=flow, file_name=workflow_json_filename)
51+
self.assertTrue(os.path.exists(workflow_json_filename))
52+
53+
with open(workflow_json_filename) as f:
54+
saved = json.load(f)
55+
input_values = [
56+
n["value"]
57+
for n in saved["nodes"]
58+
if n["type"] == "input"
59+
]
60+
self.assertIn("image.png", input_values)
61+
62+
flow = load_workflow_json(file_name=workflow_json_filename)
63+
result = run_locally(flow)
64+
self.assertEqual(result[list(result.keys())[-1]][1].output, "image.png")

tests/test_models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def test_input_node_valid_values(self):
5151
1,
5252
1.1,
5353
"string",
54+
"image.png",
55+
"path/to/file.tar.gz",
56+
"my.module.like.string",
5457
True,
5558
None,
5659
[1, 2],
@@ -73,6 +76,50 @@ def test_input_node_valid_values(self):
7376
).value
7477
)
7578

79+
def test_input_node_filename_value_roundtrip(self):
80+
"""Input nodes with filename-like values (e.g. 'image.png') must survive a
81+
full JSON serialise/deserialise round-trip without being misinterpreted as
82+
a Python module path or a floating-point number."""
83+
filenames = [
84+
"image.png",
85+
"archive.tar.gz",
86+
"report.2024.pdf",
87+
"data.csv",
88+
]
89+
for filename in filenames:
90+
with self.subTest(filename=filename):
91+
node = PythonWorkflowDefinitionInputNode(
92+
id=1, type="input", name="file_input", value=filename
93+
)
94+
self.assertEqual(node.value, filename)
95+
dumped = node.model_dump(mode="json")
96+
self.assertEqual(dumped["value"], filename)
97+
reloaded = PythonWorkflowDefinitionInputNode.model_validate(dumped)
98+
self.assertEqual(reloaded.value, filename)
99+
100+
def test_workflow_with_filename_input_roundtrip(self):
101+
"""A full workflow containing a filename as an input value must serialise and
102+
deserialise correctly through dump_json / load_json_str."""
103+
workflow_dict = {
104+
"version": "1.0",
105+
"nodes": [
106+
{"id": 1, "type": "input", "name": "file_input", "value": "image.png"},
107+
{"id": 2, "type": "function", "value": "module.process"},
108+
{"id": 3, "type": "output", "name": "result"},
109+
],
110+
"edges": [
111+
{"source": 1, "target": 2, "targetPort": "filename"},
112+
{"source": 2, "target": 3, "sourcePort": None},
113+
],
114+
}
115+
wf = PythonWorkflowDefinitionWorkflow(**workflow_dict)
116+
json_str = wf.dump_json()
117+
reloaded_dict = PythonWorkflowDefinitionWorkflow.load_json_str(json_str)
118+
reloaded_wf = PythonWorkflowDefinitionWorkflow(**reloaded_dict)
119+
input_node = reloaded_wf.nodes[0]
120+
self.assertIsInstance(input_node, PythonWorkflowDefinitionInputNode)
121+
self.assertEqual(input_node.value, "image.png")
122+
76123
def test_input_node_invalid_value_raises(self):
77124
bad_values = (
78125
{1: 2},

tests/test_purepython.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import unittest
23
from python_workflow_definition.purepython import load_workflow_json
34

@@ -35,6 +36,26 @@ def get_square(x):
3536
]
3637
}"""
3738

39+
echo_function_str = """
40+
def echo(filename):
41+
return filename
42+
"""
43+
44+
filename_workflow_str = """
45+
{
46+
"version": "0.1.0",
47+
"nodes": [
48+
{"id": 0, "type": "function", "value": "echo_module.echo"},
49+
{"id": 1, "type": "input", "value": "image.png", "name": "filename"},
50+
{"id": 2, "type": "output", "name": "result"}
51+
],
52+
"edges": [
53+
{"target": 0, "targetPort": "filename", "source": 1, "sourcePort": null},
54+
{"target": 2, "targetPort": null, "source": 0, "sourcePort": null}
55+
]
56+
}"""
57+
58+
3859
class TestPurePython(unittest.TestCase):
3960
def test_pure_python(self):
4061
with open("workflow.py", "w") as f:
@@ -44,3 +65,42 @@ def test_pure_python(self):
4465
f.write(workflow_str)
4566

4667
self.assertEqual(load_workflow_json(file_name="workflow.json"), 6.25)
68+
69+
def test_purepython_filename_input(self):
70+
"""A filename string like 'image.png' must be passed through as a plain
71+
string input, not interpreted as a Python module path or a float."""
72+
with open("echo_module.py", "w") as f:
73+
f.write(echo_function_str)
74+
sys.modules.pop("echo_module", None)
75+
76+
with open("filename_workflow.json", "w") as f:
77+
f.write(filename_workflow_str)
78+
79+
result = load_workflow_json(file_name="filename_workflow.json")
80+
self.assertEqual(result, "image.png")
81+
82+
def test_purepython_filename_input_multiple_dots(self):
83+
"""Filenames with multiple dots (e.g. 'archive.tar.gz') must also be
84+
treated as plain string inputs, not as nested module references."""
85+
multi_dot_workflow_str = """
86+
{
87+
"version": "0.1.0",
88+
"nodes": [
89+
{"id": 0, "type": "function", "value": "echo_module.echo"},
90+
{"id": 1, "type": "input", "value": "archive.tar.gz", "name": "filename"},
91+
{"id": 2, "type": "output", "name": "result"}
92+
],
93+
"edges": [
94+
{"target": 0, "targetPort": "filename", "source": 1, "sourcePort": null},
95+
{"target": 2, "targetPort": null, "source": 0, "sourcePort": null}
96+
]
97+
}"""
98+
with open("echo_module.py", "w") as f:
99+
f.write(echo_function_str)
100+
sys.modules.pop("echo_module", None)
101+
102+
with open("multi_dot_workflow.json", "w") as f:
103+
f.write(multi_dot_workflow_str)
104+
105+
result = load_workflow_json(file_name="multi_dot_workflow.json")
106+
self.assertEqual(result, "archive.tar.gz")

tests/test_pyiron_base.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import unittest
1+
import json
22
import os
3+
import unittest
34
from pyiron_base import job
45
from python_workflow_definition.pyiron_base import load_workflow_json, write_workflow_json
56

@@ -16,6 +17,10 @@ def get_square(x):
1617
return x ** 2
1718

1819

20+
def echo(filename):
21+
return filename
22+
23+
1924
class TestPyironBase(unittest.TestCase):
2025
def test_pyiron_base(self):
2126
workflow_json_filename = "pyiron_arithmetic.json"
@@ -31,3 +36,25 @@ def test_pyiron_base(self):
3136

3237
self.assertTrue(os.path.exists(workflow_json_filename))
3338
self.assertEqual(delayed_object_lst[-1].pull(), 6.25)
39+
40+
def test_pyiron_base_filename_input(self):
41+
"""A filename string like 'image.png' must be passed through as a plain
42+
string input, not interpreted as a Python module path or a float."""
43+
workflow_json_filename = "pyiron_filename.json"
44+
echo_job_wrapper = job(echo)
45+
result_delayed = echo_job_wrapper(filename="image.png")
46+
47+
write_workflow_json(delayed_object=result_delayed, file_name=workflow_json_filename)
48+
self.assertTrue(os.path.exists(workflow_json_filename))
49+
50+
with open(workflow_json_filename) as f:
51+
saved = json.load(f)
52+
input_values = [
53+
n["value"]
54+
for n in saved["nodes"]
55+
if n["type"] == "input"
56+
]
57+
self.assertIn("image.png", input_values)
58+
59+
delayed_object_lst = load_workflow_json(file_name=workflow_json_filename)
60+
self.assertEqual(delayed_object_lst[-1].pull(), "image.png")

tests/test_pyiron_workflow.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import unittest
1+
import json
22
import os
3+
import sys
4+
import unittest
35
from pyiron_workflow import Workflow, to_function_node
46
from python_workflow_definition.pyiron_workflow import load_workflow_json, write_workflow_json
57

@@ -16,6 +18,25 @@ def get_square(x):
1618
return x ** 2
1719
"""
1820

21+
echo_function_str = """
22+
def echo(filename):
23+
return filename
24+
"""
25+
26+
filename_workflow_str = """
27+
{
28+
"version": "0.1.0",
29+
"nodes": [
30+
{"id": 0, "type": "function", "value": "echo_module.echo"},
31+
{"id": 1, "type": "input", "value": "image.png", "name": "filename"},
32+
{"id": 2, "type": "output", "name": "result"}
33+
],
34+
"edges": [
35+
{"target": 0, "targetPort": "filename", "source": 1, "sourcePort": null},
36+
{"target": 2, "targetPort": null, "source": 0, "sourcePort": null}
37+
]
38+
}"""
39+
1940

2041
class TestPyironWorkflow(unittest.TestCase):
2142
def test_pyiron_workflow(self):
@@ -41,3 +62,58 @@ def test_pyiron_workflow(self):
4162
wf.run()
4263

4364
self.assertTrue(os.path.exists(workflow_json_filename))
65+
66+
def test_pyiron_workflow_filename_input(self):
67+
"""A filename string like 'image.png' must be passed through as a plain
68+
string input, not interpreted as a Python module path or a float."""
69+
workflow_json_filename = "pyiron_workflow_filename.json"
70+
with open("echo_module.py", "w") as f:
71+
f.write(echo_function_str)
72+
sys.modules.pop("echo_module", None)
73+
74+
with open(workflow_json_filename, "w") as f:
75+
f.write(filename_workflow_str)
76+
77+
with open(workflow_json_filename) as f:
78+
saved = json.load(f)
79+
input_values = [
80+
n["value"]
81+
for n in saved["nodes"]
82+
if n["type"] == "input"
83+
]
84+
self.assertIn("image.png", input_values)
85+
86+
wf = load_workflow_json(file_name=workflow_json_filename)
87+
wf.run()
88+
self.assertTrue(os.path.exists(workflow_json_filename))
89+
90+
def test_pyiron_workflow_filename_input_programmatic(self):
91+
"""Write and round-trip a workflow with a filename input using the
92+
programmatic write_workflow_json / load_workflow_json path."""
93+
workflow_json_filename = "pyiron_workflow_filename_prog.json"
94+
with open("echo_module.py", "w") as f:
95+
f.write(echo_function_str)
96+
sys.modules.pop("echo_module", None)
97+
98+
from echo_module import echo as _echo
99+
100+
echo_node = to_function_node("echo", _echo, "echo")
101+
wf = Workflow("filename_workflow")
102+
wf.filename = "image.png"
103+
wf.result = echo_node(filename=wf.filename)
104+
write_workflow_json(graph_as_dict=wf.graph_as_dict, file_name=workflow_json_filename)
105+
self.assertTrue(os.path.exists(workflow_json_filename))
106+
107+
with open(workflow_json_filename) as f:
108+
saved = json.load(f)
109+
input_values = [
110+
n["value"]
111+
for n in saved["nodes"]
112+
if n["type"] == "input"
113+
]
114+
self.assertIn("image.png", input_values)
115+
116+
sys.modules.pop("echo_module", None)
117+
wf2 = load_workflow_json(file_name=workflow_json_filename)
118+
wf2.run()
119+
self.assertTrue(os.path.exists(workflow_json_filename))

0 commit comments

Comments
 (0)