Skip to content

Commit 4ddb39f

Browse files
authored
Merge pull request #471 from neurorepro/input_spec_changes
Changes to Input/Output Specs
2 parents 118b9f3 + 91bcf78 commit 4ddb39f

8 files changed

+888
-21
lines changed

docs/input_spec.rst

+7-1
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,17 @@ In the example we used multiple keys in the metadata dictionary including `help_
162162
A flag that specifies if the file extension should be removed from the field value.
163163
Used in order to create an output specification.
164164
165-
166165
`readonly` (`bool`, default: `False`):
167166
If `True` the input field can't be provided by the user but it aggregates other input fields
168167
(for example the fields with `argstr: -o {fldA} {fldB}`).
169168
169+
`formatter` (`function`):
170+
If provided the `argstr` of the field is created using the function. This function can for example
171+
be used to combine several inputs into one command argument.
172+
The function can take `field` (this input field will be passed to the function),
173+
`inputs` (entire `inputs` will be passed) or any input field name
174+
(a specific input field will be sent).
175+
170176
171177
Validators
172178
----------

docs/output_spec.rst

+7-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ The metadata dictionary for `output_spec` can include:
6161
`help_string` (`str`, mandatory):
6262
A short description of the input field. The same as in `input_spec`.
6363

64+
`mandatory` (`bool`, default: `False`):
65+
If `True` the output file has to exist, otherwise an error will be raised.
66+
6467
`output_file_template` (`str`):
6568
If provided the output file name (or list of file names) is created using the template.
6669
The template can use other fields, e.g. `{file1}`. The same as in `input_spec`.
@@ -69,11 +72,14 @@ The metadata dictionary for `output_spec` can include:
6972
If provided the field is added to the output spec with changed name.
7073
The same as in `input_spec`.
7174

75+
`absolute_path` (`bool` default: `False`):
76+
A flag that specifies if the `output_file_template` is an absolute path. If the flag is not set,
77+
then the `output_file_template` is searched in the nodes output directory.
78+
7279
`keep_extension` (`bool`, default: `True`):
7380
A flag that specifies if the file extension should be removed from the field value.
7481
The same as in `input_spec`.
7582

76-
7783
`requires` (`list`):
7884
List of field names that are required to create a specific output.
7985
The fields do not have to be a part of the `output_file_template` and

pydra/engine/helpers_file.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -610,8 +610,15 @@ def template_update_single(
610610
return attr.NOTHING
611611
else: # inputs_dict[field.name] is True or spec_type is output
612612
value = _template_formatting(field, inputs, inputs_dict_st)
613-
# changing path so it is in the output_dir
614-
if output_dir and value is not attr.NOTHING:
613+
# changing path so it is in the output_dir, but only if absolute_path is not set
614+
if (
615+
output_dir
616+
and value is not attr.NOTHING
617+
and (
618+
"absolute_path" not in field.metadata
619+
or not field.metadata["absolute_path"]
620+
)
621+
):
615622
# should be converted to str, it is also used for input fields that should be str
616623
if type(value) is list:
617624
return [str(output_dir / Path(val).name) for val in value]

pydra/engine/specs.py

+39-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import re
77
from glob import glob
88

9-
from .helpers_file import template_update_single
9+
from .helpers_file import template_update_single, is_existing_file
1010

1111

1212
def attr_fields(spec, exclude_names=()):
@@ -400,6 +400,8 @@ def check_metadata(self):
400400
"keep_extension",
401401
"xor",
402402
"sep",
403+
"absolute_path",
404+
"formatter",
403405
}
404406
for fld in attr_fields(self, exclude_names=("_func", "_graph_checksums")):
405407
mdata = fld.metadata
@@ -496,7 +498,9 @@ def generated_output_names(self, inputs, output_dir):
496498
output_names.append(fld.name)
497499
elif (
498500
fld.metadata
499-
and self._field_metadata(fld, inputs, output_dir, outputs=None)
501+
and self._field_metadata(
502+
fld, inputs, output_dir, outputs=None, check_existance=False
503+
)
500504
!= attr.NOTHING
501505
):
502506
output_names.append(fld.name)
@@ -529,7 +533,9 @@ def _field_defaultvalue(self, fld, output_dir):
529533
else:
530534
raise AttributeError(f"no file matches {default.name}")
531535

532-
def _field_metadata(self, fld, inputs, output_dir, outputs=None):
536+
def _field_metadata(
537+
self, fld, inputs, output_dir, outputs=None, check_existance=True
538+
):
533539
"""Collect output file if metadata specified."""
534540
if self._check_requires(fld, inputs) is False:
535541
return attr.NOTHING
@@ -543,10 +549,38 @@ def _field_metadata(self, fld, inputs, output_dir, outputs=None):
543549
value = template_update_single(
544550
fld, inputs=inputs, output_dir=output_dir, spec_type="output"
545551
)
552+
546553
if fld.type is MultiOutputFile and type(value) is list:
547-
return [Path(val) for val in value]
554+
# TODO: how to deal with mandatory list outputs
555+
ret = []
556+
for val in value:
557+
val = Path(val)
558+
if check_existance and not val.exists():
559+
ret.append(attr.NOTHING)
560+
elif check_existance and (
561+
fld.metadata.get("absolute_path", False)
562+
and not val.is_absolute()
563+
):
564+
ret.append(attr.NOTHING)
565+
else:
566+
ret.append(val)
567+
return ret
548568
else:
549-
return Path(value)
569+
val = Path(value)
570+
# checking if the file exists
571+
if check_existance and not val.exists():
572+
# if mandatory raise exception
573+
if "mandatory" in fld.metadata:
574+
if fld.metadata["mandatory"]:
575+
raise Exception(
576+
f"mandatory output for variable {fld.name} does not exit"
577+
)
578+
return attr.NOTHING
579+
if check_existance and (
580+
fld.metadata.get("absolute_path", False) and not val.is_absolute()
581+
):
582+
return attr.NOTHING
583+
return val
550584
elif "callable" in fld.metadata:
551585
call_args = inspect.getargspec(fld.metadata["callable"])
552586
call_args_val = {}

pydra/engine/task.py

+44-5
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,11 @@ def _command_args_single(self, state_ind=None, index=None):
345345
exclude_names=("container", "image", "container_xargs", "bindings"),
346346
):
347347
name, meta = field.name, field.metadata
348-
if getattr(self.inputs, name) is attr.NOTHING and not meta.get("readonly"):
348+
if (
349+
getattr(self.inputs, name) is attr.NOTHING
350+
and not meta.get("readonly")
351+
and not meta.get("formatter")
352+
):
349353
continue
350354
if name == "executable":
351355
pos_args.append(
@@ -404,8 +408,9 @@ def _command_pos_args(self, field, state_ind, index):
404408
the specific field.
405409
"""
406410
argstr = field.metadata.get("argstr", None)
407-
if argstr is None:
408-
# assuming that input that has no arstr is not used in the command
411+
formatter = field.metadata.get("formatter", None)
412+
if argstr is None and formatter is None:
413+
# assuming that input that has no arstr is not used in the command, or a formatter is not provided too.
409414
return None
410415
pos = field.metadata.get("position", None)
411416
if pos is not None:
@@ -426,11 +431,45 @@ def _command_pos_args(self, field, state_ind, index):
426431
value = self._field_value(field, state_ind, index, check_file=True)
427432
if field.metadata.get("readonly", False) and value is not None:
428433
raise Exception(f"{field.name} is read only, the value can't be provided")
429-
elif value is None and not field.metadata.get("readonly", False):
434+
elif (
435+
value is None
436+
and not field.metadata.get("readonly", False)
437+
and formatter is None
438+
):
430439
return None
431440

441+
# getting stated inputs
442+
inputs_dict_st = attr.asdict(self.inputs)
443+
if state_ind is not None:
444+
for k, v in state_ind.items():
445+
k = k.split(".")[1]
446+
inputs_dict_st[k] = inputs_dict_st[k][v]
447+
432448
cmd_add = []
433-
if field.type is bool:
449+
# formatter that creates a custom command argument
450+
# it can thake the value of the filed, all inputs, or the value of other fields.
451+
if "formatter" in field.metadata:
452+
call_args = inspect.getargspec(field.metadata["formatter"])
453+
call_args_val = {}
454+
for argnm in call_args.args:
455+
if argnm == "field":
456+
call_args_val[argnm] = value
457+
elif argnm == "inputs":
458+
call_args_val[argnm] = inputs_dict_st
459+
else:
460+
if argnm in inputs_dict_st:
461+
call_args_val[argnm] = inputs_dict_st[argnm]
462+
else:
463+
raise AttributeError(
464+
f"arguments of the formatter function from {field.name} "
465+
f"has to be in inputs or be field or output_dir, "
466+
f"but {argnm} is used"
467+
)
468+
cmd_el_str = field.metadata["formatter"](**call_args_val)
469+
cmd_el_str = cmd_el_str.strip().replace(" ", " ")
470+
if cmd_el_str != "":
471+
cmd_add += cmd_el_str.split(" ")
472+
elif field.type is bool:
434473
# if value is simply True the original argstr is used,
435474
# if False, nothing is added to the command
436475
if value is True:

pydra/engine/tests/test_boutiques.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import os, shutil
22
import subprocess as sp
33
from pathlib import Path
4+
import attr
45
import pytest
56

67
from ..core import Workflow
78
from ..task import ShellCommandTask
89
from ..submitter import Submitter
910
from ..boutiques import BoshTask
1011
from .utils import result_no_submitter, result_submitter, no_win
12+
from ...engine.specs import File
1113

1214
need_bosh_docker = pytest.mark.skipif(
1315
shutil.which("docker") is None
@@ -36,12 +38,11 @@ def test_boutiques_1(maskfile, plugin, results_function, tmpdir):
3638

3739
assert res.output.return_code == 0
3840

39-
# checking if the outfile exists and if it has proper name
41+
# checking if the outfile exists and if it has a proper name
4042
assert res.output.outfile.name == "test_brain.nii.gz"
4143
assert res.output.outfile.exists()
42-
# other files should also have proper names, but they do not exist
43-
assert res.output.out_outskin_off.name == "test_brain_outskin_mesh.off"
44-
assert not res.output.out_outskin_off.exists()
44+
# files that do not exist were set to NOTHING
45+
assert res.output.out_outskin_off == attr.NOTHING
4546

4647

4748
@no_win

0 commit comments

Comments
 (0)