Skip to content

Commit 8fba851

Browse files
authored
Merge pull request #348 from djarecka/mnt/nipype_interf_convert
[mnt, wip] ShellCommandTask updates (motivated my nipype interfaces)
2 parents 7ea9477 + e469555 commit 8fba851

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

pydra/engine/specs.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def check_fields_input_spec(self):
135135

136136
# checking if fields meet the xor and requires are
137137
if "xor" in mdata:
138-
if [el for el in mdata["xor"] if el in names]:
138+
if [el for el in mdata["xor"] if (el in names and el != fld.name)]:
139139
raise AttributeError(
140140
f"{fld.name} is mutually exclusive with {mdata['xor']}"
141141
)
@@ -473,6 +473,8 @@ def _check_requires(self, fld, inputs):
473473
""" checking if all fields from the requires and template are set in the input
474474
if requires is a list of list, checking if at least one list has all elements set
475475
"""
476+
from .helpers import ensure_list
477+
476478
if "requires" in fld.metadata:
477479
# if requires is a list of list it is treated as el[0] OR el[1] OR...
478480
if all([isinstance(el, list) for el in fld.metadata["requires"]]):
@@ -512,7 +514,7 @@ def _check_requires(self, fld, inputs):
512514
required_found = False
513515
break
514516
elif isinstance(inp, tuple): # (name, allowed values)
515-
inp, allowed_val = inp
517+
inp, allowed_val = inp[0], ensure_list(inp[1])
516518
if not hasattr(inputs, inp):
517519
raise Exception(
518520
f"{inp} is not a valid input field, can't be used in requires"

pydra/engine/task.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import inspect
4444
import typing as ty
4545
from pathlib import Path
46+
import warnings
4647

4748
from .core import TaskBase, is_lazy
4849
from ..utils.messenger import AuditFlag
@@ -216,6 +217,9 @@ def _run_task(self):
216217
class ShellCommandTask(TaskBase):
217218
"""Wrap a shell command as a task element."""
218219

220+
input_spec = None
221+
output_spec = None
222+
219223
def __new__(cls, container_info=None, *args, **kwargs):
220224
if not container_info:
221225
return super().__new__(cls)
@@ -275,15 +279,29 @@ def __init__(
275279
TODO
276280
277281
"""
278-
if input_spec is None:
279-
input_spec = SpecInfo(name="Inputs", fields=[], bases=(ShellSpec,))
280-
self.input_spec = input_spec
281-
if output_spec is None:
282-
output_spec = SpecInfo(name="Output", fields=[], bases=(ShellOutSpec,))
283282

284-
self.output_spec = output_spec
283+
# using provided spec, class attribute or setting the default SpecInfo
284+
self.input_spec = (
285+
input_spec
286+
or self.input_spec
287+
or SpecInfo(name="Inputs", fields=[], bases=(ShellSpec,))
288+
)
289+
self.output_spec = (
290+
output_spec
291+
or self.output_spec
292+
or SpecInfo(name="Output", fields=[], bases=(ShellOutSpec,))
293+
)
285294
self.output_spec = output_from_inputfields(self.output_spec, self.input_spec)
286295

296+
for special_inp in ["executable", "args"]:
297+
if hasattr(self, special_inp):
298+
if special_inp not in kwargs:
299+
kwargs[special_inp] = getattr(self, special_inp)
300+
elif kwargs[special_inp] != getattr(self, special_inp):
301+
warnings.warn(
302+
f"you are changing the executable from {getattr(self, special_inp)} to {kwargs[special_inp]}"
303+
)
304+
287305
super().__init__(
288306
name=name,
289307
inputs=kwargs,
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import attr
2+
import typing as ty
3+
import os, sys
4+
import pytest
5+
from pathlib import Path
6+
7+
8+
from ..task import ShellCommandTask
9+
from ..submitter import Submitter
10+
from ..core import Workflow
11+
from ..specs import ShellOutSpec, ShellSpec, SpecInfo, File
12+
from .utils import result_no_submitter, result_submitter, use_validator
13+
14+
interf_input_spec = SpecInfo(
15+
name="Input", fields=[("test", ty.Any, {"help_string": "test"})], bases=(ShellSpec,)
16+
)
17+
interf_output_spec = SpecInfo(
18+
name="Output", fields=[("test_out", File, "*.txt")], bases=(ShellOutSpec,)
19+
)
20+
21+
22+
class Interf_1(ShellCommandTask):
23+
"""class with customized input/output specs"""
24+
25+
input_spec = interf_input_spec
26+
output_spec = interf_output_spec
27+
28+
29+
class Interf_2(ShellCommandTask):
30+
"""class with customized input/output specs and executables"""
31+
32+
input_spec = interf_input_spec
33+
output_spec = interf_output_spec
34+
executable = "testing command"
35+
36+
37+
class TouchInterf(ShellCommandTask):
38+
"""class with customized input and executables"""
39+
40+
input_spec = SpecInfo(
41+
name="Input",
42+
fields=[
43+
(
44+
"new_file",
45+
str,
46+
{
47+
"help_string": "new_file",
48+
"argstr": "",
49+
"output_file_template": "{new_file}",
50+
},
51+
)
52+
],
53+
bases=(ShellSpec,),
54+
)
55+
executable = "touch"
56+
57+
58+
def test_interface_specs_1():
59+
"""testing if class input/output spec are set properly"""
60+
task = Interf_1(executable="ls")
61+
assert task.input_spec == interf_input_spec
62+
assert task.output_spec == interf_output_spec
63+
64+
65+
def test_interface_specs_2():
66+
"""testing if class input/output spec are overwritten properly by the user's specs"""
67+
my_input_spec = SpecInfo(
68+
name="Input",
69+
fields=[("my_inp", ty.Any, {"help_string": "my inp"})],
70+
bases=(ShellSpec,),
71+
)
72+
my_output_spec = SpecInfo(
73+
name="Output", fields=[("my_out", File, "*.txt")], bases=(ShellOutSpec,)
74+
)
75+
task = Interf_1(input_spec=my_input_spec, output_spec=my_output_spec)
76+
assert task.input_spec == my_input_spec
77+
assert task.output_spec == my_output_spec
78+
79+
80+
def test_interface_executable_1():
81+
"""testing if the class executable is properly set and used in the command line"""
82+
task = Interf_2()
83+
assert task.executable == "testing command"
84+
assert task.inputs.executable == "testing command"
85+
assert task.cmdline == "testing command"
86+
87+
88+
def test_interface_executable_2():
89+
"""testing if the class executable is overwritten by the user's input (and if the warning is raised)"""
90+
# warning that the user changes the executable from the one that is set as a class attribute
91+
with pytest.warns(UserWarning, match="changing the executable"):
92+
task = Interf_2(executable="i want a different command")
93+
assert task.executable == "testing command"
94+
# task.executable stays the same, but input.executable is changed, so the cmd is changed
95+
assert task.inputs.executable == "i want a different command"
96+
assert task.cmdline == "i want a different command"
97+
98+
99+
def test_interface_run_1():
100+
"""testing execution of a simple interf with customized input and executable"""
101+
task = TouchInterf(new_file="hello.txt")
102+
assert task.cmdline == "touch hello.txt"
103+
res = task()
104+
assert res.output.new_file.exists()

0 commit comments

Comments
 (0)