Skip to content

Commit 83c06e3

Browse files
committed
added shell_task decorator
1 parent ac15a1e commit 83c06e3

File tree

3 files changed

+140
-13
lines changed

3 files changed

+140
-13
lines changed

pydra/mark/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .functions import annotate, task
2-
from .shell_commands import cmd_arg, cmd_out
2+
from .shell_commands import shell_task, shell_arg, shell_out
33

4-
__all__ = ("annotate", "task", "cmd_arg", "cmd_out")
4+
__all__ = ("annotate", "task", "shell_task", "shell_arg", "shell_out")

pydra/mark/shell_commands.py

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,136 @@
22
from __future__ import annotations
33
import typing as ty
44
import attrs
5+
import pydra.engine.specs
56

67

7-
def cmd_arg(
8+
def shell_task(
9+
klass_or_name: ty.Union[type, str],
10+
executable: ty.Optional[str] = None,
11+
input_fields: ty.Optional[dict[str, dict]] = None,
12+
output_fields: ty.Optional[dict[str, dict]] = None,
13+
bases: ty.Optional[list[type]] = None,
14+
input_bases: ty.Optional[list[type]] = None,
15+
output_bases: ty.Optional[list[type]] = None,
16+
) -> type:
17+
"""
18+
Construct an analysis class and validate all the components fit together
19+
20+
Parameters
21+
----------
22+
klass_or_name : type or str
23+
Either the class decorated by the @shell_task decorator or the name for a
24+
dynamically generated class
25+
executable : str, optional
26+
If dynamically constructing a class (instead of decorating an existing one) the
27+
name of the executable to run is provided
28+
input_fields : dict[str, dict], optional
29+
If dynamically constructing a class (instead of decorating an existing one) the
30+
input fields can be provided as a dictionary of dictionaries, where the keys
31+
are the name of the fields and the dictionary contents are passed as keyword
32+
args to cmd_arg, with the exception of "type", which is used as the type annotation
33+
of the field.
34+
output_fields : dict[str, dict], optional
35+
If dynamically constructing a class (instead of decorating an existing one) the
36+
output fields can be provided as a dictionary of dictionaries, where the keys
37+
are the name of the fields and the dictionary contents are passed as keyword
38+
args to cmd_out, with the exception of "type", which is used as the type annotation
39+
of the field.
40+
bases : list[type]
41+
Base classes for dynamically constructed shell command classes
42+
input_bases : list[type]
43+
Base classes for the input spec of dynamically constructed shell command classes
44+
output_bases : list[type]
45+
Base classes for the input spec of dynamically constructed shell command classes
46+
47+
Returns
48+
-------
49+
type
50+
the shell command task class
51+
"""
52+
53+
if isinstance(klass_or_name, str):
54+
if None in (executable, input_fields):
55+
raise RuntimeError(
56+
"Dynamically constructed shell tasks require an executable and "
57+
"input_field arguments"
58+
)
59+
name = klass_or_name
60+
if output_fields is None:
61+
output_fields = {}
62+
if bases is None:
63+
bases = [pydra.engine.task.ShellCommandTask]
64+
if input_bases is None:
65+
input_bases = [pydra.engine.specs.ShellSpec]
66+
if output_bases is None:
67+
output_bases = [pydra.engine.specs.ShellOutSpec]
68+
Inputs = type("Inputs", tuple(input_bases), input_fields)
69+
Outputs = type("Outputs", tuple(output_bases), output_fields)
70+
else:
71+
if (
72+
executable,
73+
input_fields,
74+
output_fields,
75+
bases,
76+
input_bases,
77+
output_bases,
78+
) != (None, None, None, None, None, None):
79+
raise RuntimeError(
80+
"When used as a decorator on a class `shell_task` should not be provided "
81+
"executable, input_field or output_field arguments"
82+
)
83+
klass = klass_or_name
84+
name = klass.__name__
85+
try:
86+
executable = klass.executable
87+
except KeyError:
88+
raise RuntimeError(
89+
"Classes decorated by `shell_task` should contain an `executable` attribute "
90+
"specifying the shell tool to run"
91+
)
92+
try:
93+
Inputs = klass.Inputs
94+
except KeyError:
95+
raise RuntimeError(
96+
"Classes decorated by `shell_task` should contain an `Inputs` class attribute "
97+
"specifying the inputs to the shell tool"
98+
)
99+
if not issubclass(Inputs, pydra.engine.specs.ShellSpec):
100+
Inputs = type("Inputs", (Inputs, pydra.engine.specs.ShellSpec), {})
101+
try:
102+
Outputs = klass.Outputs
103+
except KeyError:
104+
Outputs = type("Outputs", (pydra.engine.specs.ShellOutSpec,))
105+
bases = [klass]
106+
if not issubclass(klass, pydra.engine.task.ShellCommandTask):
107+
bases.append(pydra.engine.task.ShellCommandTask)
108+
109+
Inputs = attrs.define(kw_only=True, slots=False)(Inputs)
110+
Outputs = attrs.define(kw_only=True, slots=False)(Outputs)
111+
112+
dct = {
113+
"executable": executable,
114+
"Inputs": Outputs,
115+
"Outputs": Inputs,
116+
"inputs": attrs.field(factory=Inputs),
117+
"outputs": attrs.field(factory=Outputs),
118+
"__annotations__": {
119+
"executable": str,
120+
"inputs": Inputs,
121+
"outputs": Outputs,
122+
},
123+
}
124+
125+
return attrs.define(kw_only=True, slots=False)(
126+
type(
127+
name,
128+
tuple(bases),
129+
dct,
130+
)
131+
)
132+
133+
134+
def shell_arg(
8135
help_string: str,
9136
default: ty.Any = attrs.NOTHING,
10137
argstr: str = None,
@@ -103,7 +230,7 @@ def cmd_arg(
103230
)
104231

105232

106-
def cmd_out(
233+
def shell_out(
107234
help_string: str,
108235
mandatory: bool = False,
109236
output_file_template: str = None,

pydra/mark/tests/test_shell_commands.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,36 @@
33
from pathlib import Path
44
import attrs
55
import pydra.engine
6-
from pydra.mark import cmd_arg, cmd_out
6+
from pydra.mark import shell_task, shell_arg, shell_out
77

88

9-
def test_shell_cmd():
9+
def test_shell_task_full():
1010
@attrs.define(kw_only=True, slots=False)
1111
class LsInputSpec(pydra.specs.ShellSpec):
12-
directory: os.PathLike = cmd_arg(
12+
directory: os.PathLike = shell_arg(
1313
help_string="the directory to list the contents of",
1414
argstr="",
1515
mandatory=True,
1616
)
17-
hidden: bool = cmd_arg(help_string=("display hidden FS objects"), argstr="-a")
18-
long_format: bool = cmd_arg(
17+
hidden: bool = shell_arg(help_string=("display hidden FS objects"), argstr="-a")
18+
long_format: bool = shell_arg(
1919
help_string=(
2020
"display properties of FS object, such as permissions, size and timestamps "
2121
),
2222
argstr="-l",
2323
)
24-
human_readable: bool = cmd_arg(
24+
human_readable: bool = shell_arg(
2525
help_string="display file sizes in human readable form",
2626
argstr="-h",
2727
requires=["long_format"],
2828
)
29-
complete_date: bool = cmd_arg(
29+
complete_date: bool = shell_arg(
3030
help_string="Show complete date in long format",
3131
argstr="-T",
3232
requires=["long_format"],
3333
xor=["date_format_str"],
3434
)
35-
date_format_str: str = cmd_arg(
35+
date_format_str: str = shell_arg(
3636
help_string="format string for ",
3737
argstr="-D",
3838
requires=["long_format"],
@@ -44,7 +44,7 @@ def list_outputs(stdout):
4444

4545
@attrs.define(kw_only=True, slots=False)
4646
class LsOutputSpec(pydra.specs.ShellOutSpec):
47-
entries: list = cmd_out(
47+
entries: list = shell_out(
4848
help_string="list of entries returned by ls command", callable=list_outputs
4949
)
5050

0 commit comments

Comments
 (0)