Skip to content

Commit a3046ae

Browse files
authored
Merge pull request #201 from djarecka/mnt/undefined_input
using attr.NOTHING for undefined input fields
2 parents c8ecd29 + ad78e34 commit a3046ae

File tree

9 files changed

+153
-15
lines changed

9 files changed

+153
-15
lines changed

pydra/engine/core.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,7 @@ def __init__(
132132
# todo should be used to input_check in spec??
133133
self.inputs = klass(
134134
**{
135-
(f.name[1:] if f.name.startswith("_") else f.name): (
136-
None if f.default == attr.NOTHING else f.default
137-
)
135+
(f.name[1:] if f.name.startswith("_") else f.name): f.default
138136
for f in attr.fields(klass)
139137
}
140138
)
@@ -220,7 +218,7 @@ def version(self):
220218
def checksum(self):
221219
"""Calculate a unique checksum of this task."""
222220
# if checksum is called before run the _graph_checksums is not ready
223-
if is_workflow(self) and self.inputs._graph_checksums is None:
221+
if is_workflow(self) and self.inputs._graph_checksums is attr.NOTHING:
224222
self.inputs._graph_checksums = [nd.checksum for nd in self.graph_sorted]
225223

226224
input_hash = self.inputs.hash

pydra/engine/helpers_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def template_update(inputs, map_copyfiles=None):
494494
fields = attr_fields(inputs)
495495
# TODO: Create a dependency graph first and then traverse it
496496
for fld in fields:
497-
if getattr(inputs, fld.name) is not None:
497+
if getattr(inputs, fld.name) is not attr.NOTHING:
498498
continue
499499
if fld.metadata.get("output_file_template"):
500500
if fld.type is str:

pydra/engine/specs.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def hash(self):
4747
"output_file_template"
4848
):
4949
continue
50+
# removing values that are notset from hash calculation
51+
if getattr(self, field.name) is attr.NOTHING:
52+
continue
5053
inp_dict[field.name] = hash_value(
5154
value=getattr(self, field.name), tp=field.type, metadata=field.metadata
5255
)
@@ -241,7 +244,7 @@ def check_fields_input_spec(self):
241244
for fld in fields:
242245
mdata = fld.metadata
243246
# checking if the mandatory field is provided
244-
if getattr(self, fld.name) is None:
247+
if getattr(self, fld.name) is attr.NOTHING:
245248
if mdata.get("mandatory"):
246249
raise Exception(f"{fld.name} is mandatory, but no value provided")
247250
else:
@@ -370,7 +373,9 @@ def _field_metadata(self, fld, inputs, output_dir):
370373
class ContainerSpec(ShellSpec):
371374
"""Refine the generic command-line specification to container execution."""
372375

373-
image: ty.Union[File, str] = attr.ib(metadata={"help_string": "image"})
376+
image: ty.Union[File, str] = attr.ib(
377+
metadata={"help_string": "image", "mandatory": True}
378+
)
374379
"""The image to be containerized."""
375380
container: ty.Union[File, str, None] = attr.ib(
376381
metadata={"help_string": "container"}

pydra/engine/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ def container_check(self, container_type):
463463
raise AttributeError(
464464
f"Container type should be {container_type}, but {self.inputs.container} given"
465465
)
466-
if self.inputs.image is None:
466+
if self.inputs.image is attr.NOTHING:
467467
raise AttributeError("Container image is not specified")
468468

469469
def bind_paths(self, ind=None):

pydra/engine/tests/test_node_task.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import os
22
import shutil
3+
import attr
34
import numpy as np
45
import pytest
56

67
from .utils import (
78
fun_addtwo,
89
fun_addvar,
10+
fun_addvar_none,
11+
fun_addvar_default,
912
moment,
1013
fun_div,
1114
fun_dict,
@@ -317,6 +320,23 @@ def test_task_init_7(plugin, tmpdir):
317320
assert output_dir1.name != output_dir2.name
318321

319322

323+
def test_task_init_8():
324+
""" task without setting the input, the value should be set to attr.NOTHING"""
325+
nn = fun_addtwo(name="NA")
326+
assert nn.inputs.a is attr.NOTHING
327+
328+
329+
def test_task_init_9():
330+
""" task without setting the input, but using the default avlue from function"""
331+
nn1 = fun_addvar_default(name="NA", a=2)
332+
assert nn1.inputs.b == 1
333+
334+
nn2 = fun_addvar_default(name="NA", a=2, b=1)
335+
assert nn2.inputs.b == 1
336+
# both tasks should have the same checksum
337+
assert nn1.checksum == nn2.checksum
338+
339+
320340
def test_task_error():
321341
func = fun_div(name="div", a=1, b=0)
322342
with pytest.raises(ZeroDivisionError):
@@ -474,6 +494,31 @@ def test_task_nostate_5(plugin, tmpdir):
474494
assert nn.output_dir.exists()
475495

476496

497+
def test_task_nostate_6():
498+
""" checking if the function gets the None value"""
499+
nn = fun_addvar_none(name="NA", a=2, b=None)
500+
assert nn.inputs.b is None
501+
nn()
502+
assert nn.result().output.out == 2
503+
504+
505+
def test_task_nostate_6a_exception():
506+
""" checking if the function gets the attr.Nothing value"""
507+
nn = fun_addvar_none(name="NA", a=2)
508+
assert nn.inputs.b is attr.NOTHING
509+
with pytest.raises(TypeError) as excinfo:
510+
nn()
511+
assert "unsupported" in str(excinfo.value)
512+
513+
514+
def test_task_nostate_7():
515+
""" using the default value from the function for b input"""
516+
nn = fun_addvar_default(name="NA", a=2)
517+
assert nn.inputs.b == 1
518+
nn()
519+
assert nn.result().output.out == 3
520+
521+
477522
# Testing caching for tasks without states
478523

479524

pydra/engine/tests/test_shelltask.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ def test_shell_cmd_inputspec_3c(plugin, results_function):
458458
"text",
459459
attr.ib(
460460
type=str,
461+
default=None,
461462
metadata={"position": 1, "help_string": "text", "mandatory": False},
462463
),
463464
)

pydra/engine/tests/test_task.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -352,11 +352,9 @@ def test_shell_cmd(tmpdir):
352352

353353
def test_container_cmds(tmpdir):
354354
containy = DockerTask(name="containy", executable="pwd")
355-
with pytest.raises(AttributeError):
356-
containy.cmdline
357-
containy.inputs.container = "docker"
358-
with pytest.raises(AttributeError):
355+
with pytest.raises(AttributeError) as excinfo:
359356
containy.cmdline
357+
assert "not specified" in str(excinfo.value)
360358
containy.inputs.image = "busybox"
361359
assert containy.cmdline
362360

pydra/engine/tests/test_workflow.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
import shutil, os
33
import time
4-
import platform
4+
import attr
55

66
from .utils import (
77
add2,
@@ -12,11 +12,11 @@
1212
list_output,
1313
fun_addvar3,
1414
add2_sub2_res,
15+
fun_addvar_none,
16+
fun_addvar_default,
1517
)
1618
from ..submitter import Submitter
1719
from ..core import Workflow
18-
from ... import mark
19-
2020

2121
if bool(shutil.which("sbatch")):
2222
Plugins = ["cf", "slurm"]
@@ -229,6 +229,84 @@ def test_wf_2d_outpasdict(plugin):
229229
assert wf.output_dir.exists()
230230

231231

232+
@pytest.mark.parametrize("plugin", Plugins)
233+
def test_wf_3(plugin):
234+
""" testing None value for an input"""
235+
wf = Workflow(name="wf_3", input_spec=["x", "y"])
236+
wf.add(fun_addvar_none(name="addvar", a=wf.lzin.x, b=wf.lzin.y))
237+
wf.add(add2(name="add2", x=wf.addvar.lzout.out))
238+
wf.set_output([("out", wf.add2.lzout.out)])
239+
wf.inputs.x = 2
240+
wf.inputs.y = None
241+
wf.plugin = plugin
242+
243+
with Submitter(plugin=plugin) as sub:
244+
sub(wf)
245+
246+
assert wf.output_dir.exists()
247+
results = wf.result()
248+
assert 4 == results.output.out
249+
250+
251+
@pytest.mark.xfail(reason="the task error doesn't propagate")
252+
@pytest.mark.parametrize("plugin", Plugins)
253+
def test_wf_3a_exception(plugin):
254+
""" testinh wf without set input, attr.NOTHING should be set
255+
and the function should raise an exception
256+
"""
257+
wf = Workflow(name="wf_3", input_spec=["x", "y"])
258+
wf.add(fun_addvar_none(name="addvar", a=wf.lzin.x, b=wf.lzin.y))
259+
wf.add(add2(name="add2", x=wf.addvar.lzout.out))
260+
wf.set_output([("out", wf.add2.lzout.out)])
261+
wf.inputs.x = 2
262+
wf.inputs.y = attr.NOTHING
263+
wf.plugin = plugin
264+
265+
with pytest.raises(TypeError) as excinfo:
266+
with Submitter(plugin=plugin) as sub:
267+
sub(wf)
268+
assert "unsupported" in str(excinfo.value)
269+
270+
271+
@pytest.mark.parametrize("plugin", Plugins)
272+
def test_wf_4(plugin):
273+
"""wf with a task that doesn't set one input and use the function default value"""
274+
wf = Workflow(name="wf_4", input_spec=["x", "y"])
275+
wf.add(fun_addvar_default(name="addvar", a=wf.lzin.x))
276+
wf.add(add2(name="add2", x=wf.addvar.lzout.out))
277+
wf.set_output([("out", wf.add2.lzout.out)])
278+
wf.inputs.x = 2
279+
wf.plugin = plugin
280+
281+
with Submitter(plugin=plugin) as sub:
282+
sub(wf)
283+
284+
assert wf.output_dir.exists()
285+
results = wf.result()
286+
assert 5 == results.output.out
287+
288+
289+
@pytest.mark.parametrize("plugin", Plugins)
290+
def test_wf_4a(plugin):
291+
""" wf with a task that doesn't set one input,
292+
the unset input is send to the task input,
293+
so the task should use the function default value
294+
"""
295+
wf = Workflow(name="wf_4a", input_spec=["x", "y"])
296+
wf.add(fun_addvar_default(name="addvar", a=wf.lzin.x, y=wf.lzin.y))
297+
wf.add(add2(name="add2", x=wf.addvar.lzout.out))
298+
wf.set_output([("out", wf.add2.lzout.out)])
299+
wf.inputs.x = 2
300+
wf.plugin = plugin
301+
302+
with Submitter(plugin=plugin) as sub:
303+
sub(wf)
304+
305+
assert wf.output_dir.exists()
306+
results = wf.result()
307+
assert 5 == results.output.out
308+
309+
232310
@pytest.mark.parametrize("plugin", Plugins)
233311
def test_wf_st_1(plugin):
234312
""" Workflow with one task, a splitter for the workflow"""

pydra/engine/tests/utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ def fun_addvar(a, b):
2121
return a + b
2222

2323

24+
@mark.task
25+
def fun_addvar_none(a, b):
26+
if b is None:
27+
return a
28+
else:
29+
return a + b
30+
31+
32+
@mark.task
33+
def fun_addvar_default(a, b=1):
34+
return a + b
35+
36+
2437
@mark.task
2538
def fun_addvar3(a, b, c):
2639
return a + b + c

0 commit comments

Comments
 (0)