diff --git a/pydra/engine/helpers.py b/pydra/engine/helpers.py index 7d20968b0e..8e5be8acc2 100644 --- a/pydra/engine/helpers.py +++ b/pydra/engine/helpers.py @@ -24,6 +24,7 @@ File, Directory, attr_fields, + attr_field, Result, LazyField, MultiOutputObj, @@ -884,6 +885,12 @@ def argstr_formatting(argstr, inputs, value_updates=None): if fld_value is attr.NOTHING: # if value is NOTHING, nothing should be added to the command val_dict[fld_name] = "" + # if value is False, but the field has a template the output field should not be created + elif ( + fld_value is False + and "output_file_template" in attr_field(inputs, fld_name).metadata + ): + val_dict[fld_name] = "" else: val_dict[fld_name] = fld_value diff --git a/pydra/engine/helpers_file.py b/pydra/engine/helpers_file.py index 73d59b718f..53e4602503 100644 --- a/pydra/engine/helpers_file.py +++ b/pydra/engine/helpers_file.py @@ -579,7 +579,6 @@ def template_update(inputs, output_dir, state_ind=None, map_copyfiles=None): Should be run when all inputs used in the templates are already set. """ - inputs_dict_st = attr.asdict(inputs, recurse=False) if map_copyfiles is not None: inputs_dict_st.update(map_copyfiles) @@ -654,7 +653,7 @@ def template_update_single( return inputs_dict_st[field.name] elif spec_type == "input" and inputs_dict_st[field.name] is False: # if input fld is set to False, the fld shouldn't be used (setting NOTHING) - return attr.NOTHING + return False else: # inputs_dict[field.name] is True or spec_type is output value = _template_formatting(field, inputs, inputs_dict_st) # changing path so it is in the output_dir diff --git a/pydra/engine/specs.py b/pydra/engine/specs.py index 3ee2b03ea4..29af175a06 100644 --- a/pydra/engine/specs.py +++ b/pydra/engine/specs.py @@ -13,6 +13,10 @@ def attr_fields(spec, exclude_names=()): return [field for field in spec.__attrs_attrs__ if field.name not in exclude_names] +def attr_field(spec, name): + return [field for field in spec.__attrs_attrs__ if field.name == name][0] + + def attr_fields_dict(spec, exclude_names=()): return { field.name: field diff --git a/pydra/engine/task.py b/pydra/engine/task.py index f800276b7a..d3f93b8361 100644 --- a/pydra/engine/task.py +++ b/pydra/engine/task.py @@ -479,8 +479,12 @@ def _command_pos_args(self, field): value = cmd_el_str # if argstr has a more complex form, with "{input_field}" if "{" in argstr and "}" in argstr: - cmd_el_str = argstr.replace(f"{{{field.name}}}", str(value)) - cmd_el_str = argstr_formatting(cmd_el_str, self.inputs) + # output_file_template and argstr should not be created when the value is False + if not value and "output_file_template" in field.metadata: + cmd_el_str = "" + else: + cmd_el_str = argstr.replace(f"{{{field.name}}}", str(value)) + cmd_el_str = argstr_formatting(cmd_el_str, self.inputs) else: # argstr has a simple form, e.g. "-f", or "--f" if value: cmd_el_str = f"{argstr} {value}" diff --git a/pydra/engine/tests/test_shelltask_inputspec.py b/pydra/engine/tests/test_shelltask_inputspec.py index 240502dc1d..51aa2e29e3 100644 --- a/pydra/engine/tests/test_shelltask_inputspec.py +++ b/pydra/engine/tests/test_shelltask_inputspec.py @@ -1152,6 +1152,8 @@ def test_shell_cmd_inputs_template_6(): executable="executable", input_spec=my_input_spec, inpA="inpA" ) assert shelly.cmdline == f"executable inpA -o {str(shelly.output_dir / 'inpA_out')}" + # checking if the command is the same + assert shelly.cmdline == f"executable inpA -o {str(shelly.output_dir / 'inpA_out')}" # a string is provided for outA, so this should be used as the outA value shelly = ShellCommandTask( @@ -1170,6 +1172,8 @@ def test_shell_cmd_inputs_template_6(): executable="executable", input_spec=my_input_spec, inpA="inpA", outA=False ) assert shelly.cmdline == "executable inpA" + # checking of the command is the same + assert shelly.cmdline == "executable inpA" def test_shell_cmd_inputs_template_6a(): @@ -1214,6 +1218,8 @@ def test_shell_cmd_inputs_template_6a(): executable="executable", input_spec=my_input_spec, inpA="inpA" ) assert shelly.cmdline == "executable inpA" + # checking if the command is the same + assert shelly.cmdline == "executable inpA" # a string is provided for outA, so this should be used as the outA value shelly = ShellCommandTask( @@ -1232,6 +1238,58 @@ def test_shell_cmd_inputs_template_6a(): executable="executable", input_spec=my_input_spec, inpA="inpA", outA=False ) assert shelly.cmdline == "executable inpA" + # checking if the command is the same + assert shelly.cmdline == "executable inpA" + + +def test_shell_cmd_inputs_template_6b(): + """additional inputs with output_file_template that has type ty.Union[str, bool] + and default is set to False, + (using argstr in a format --argstr={}) + """ + my_input_spec = SpecInfo( + name="Input", + fields=[ + ( + "inpA", + attr.ib( + type=str, + metadata={ + "position": 1, + "help_string": "inpA", + "argstr": "", + "mandatory": True, + }, + ), + ), + ( + "outA", + attr.ib( + type=ty.Union[str, bool], + default=False, + metadata={ + "position": 2, + "help_string": "outA", + "argstr": "--o={outA}", + "output_file_template": "{inpA}_out", + }, + ), + ), + ], + bases=(ShellSpec,), + ) + + # no input for outA, but default is False, so the outA shouldn't be used + shelly = ShellCommandTask( + executable="executable", input_spec=my_input_spec, inpA="inpA" + ) + assert shelly.cmdline == "executable inpA" + + # a string is provided for outA, so this should be used as the outA value + shelly = ShellCommandTask( + executable="executable", input_spec=my_input_spec, inpA="inpA", outA="outA" + ) + assert shelly.cmdline == "executable inpA --o=outA" def test_shell_cmd_inputs_template_7(tmpdir): @@ -2080,7 +2138,7 @@ def test_shell_cmd_inputs_di(tmpdir, use_validator): == f"DenoiseImage -i {tmpdir.join('a_file.ext')} -s 1 -p 1 -r 2 -o [{str(shelly.output_dir / 'a_file_out.ext')}]" ) - # input file name, noiseImage is set to True, so template is used in the output + # # input file name, noiseImage is set to True, so template is used in the output shelly = ShellCommandTask( executable="DenoiseImage", inputImageFilename=my_input_file,