diff --git a/pydra/engine/core.py b/pydra/engine/core.py index b9a57a7f36..266788908c 100644 --- a/pydra/engine/core.py +++ b/pydra/engine/core.py @@ -311,9 +311,33 @@ def set_state(self, splitter, combiner=None): @property def output_names(self): - """Get the names of the outputs generated by the task.""" + """Get the names of the outputs from the task's output_spec + (not everything has to be generated, see generated_output_names). + """ return [f.name for f in attr.fields(make_klass(self.output_spec))] + @property + def generated_output_names(self): + """ Get the names of the outputs generated by the task. + If the spec doesn't have generated_output_names method, + it uses output_names. + The results depends on the input provided to the task + """ + output_klass = make_klass(self.output_spec) + if hasattr(output_klass, "generated_output_names"): + output = output_klass(**{f.name: None for f in attr.fields(output_klass)}) + # using updated input (after filing the templates) + _inputs = deepcopy(self.inputs) + modified_inputs = template_update(_inputs, self.output_dir) + if modified_inputs: + _inputs = attr.evolve(_inputs, **modified_inputs) + + return output.generated_output_names( + inputs=_inputs, output_dir=self.output_dir + ) + else: + return self.output_names + @property def can_resume(self): """Whether the task accepts checkpoint-restart.""" diff --git a/pydra/engine/helpers_file.py b/pydra/engine/helpers_file.py index 09e69badd9..b1e55aca3b 100644 --- a/pydra/engine/helpers_file.py +++ b/pydra/engine/helpers_file.py @@ -519,7 +519,7 @@ def template_update(inputs, output_dir, map_copyfiles=None): if fld.type not in [str, ty.Union[str, bool]]: raise Exception( f"fields with output_file_template" - "has to be a string or Union[str, bool]" + " has to be a string or Union[str, bool]" ) dict_[fld.name] = template_update_single( field=fld, inputs_dict=dict_, output_dir=output_dir diff --git a/pydra/engine/specs.py b/pydra/engine/specs.py index 1c950931f5..874a304e43 100644 --- a/pydra/engine/specs.py +++ b/pydra/engine/specs.py @@ -408,7 +408,7 @@ def collect_additional_outputs(self, inputs, output_dir): raise AttributeError( "File has to have default value or metadata" ) - elif not fld.default == attr.NOTHING: + elif fld.default != attr.NOTHING: additional_out[fld.name] = self._field_defaultvalue( fld, output_dir ) @@ -420,6 +420,36 @@ def collect_additional_outputs(self, inputs, output_dir): raise Exception("not implemented (collect_additional_output)") return additional_out + def generated_output_names(self, inputs, output_dir): + """ Returns a list of all outputs that will be generated by the task. + Takes into account the task input and the requires list for the output fields. + TODO: should be in all Output specs? + """ + # checking the input (if all mandatory fields are provided, etc.) + inputs.check_fields_input_spec() + output_names = ["return_code", "stdout", "stderr"] + for fld in attr_fields(self): + if fld.name not in ["return_code", "stdout", "stderr"]: + if fld.type is File: + # assuming that field should have either default or metadata, but not both + if ( + fld.default is None or fld.default == attr.NOTHING + ) and not fld.metadata: # TODO: is it right? + raise AttributeError( + "File has to have default value or metadata" + ) + elif fld.default != attr.NOTHING: + output_names.append(fld.name) + elif ( + fld.metadata + and self._field_metadata(fld, inputs, output_dir) + != attr.NOTHING + ): + output_names.append(fld.name) + else: + raise Exception("not implemented (collect_additional_output)") + return output_names + def _field_defaultvalue(self, fld, output_dir): """Collect output file if the default value specified.""" if not isinstance(fld.default, (str, Path)): diff --git a/pydra/engine/tests/test_shelltask.py b/pydra/engine/tests/test_shelltask.py index b8938910a7..740e22d78c 100644 --- a/pydra/engine/tests/test_shelltask.py +++ b/pydra/engine/tests/test_shelltask.py @@ -2654,6 +2654,12 @@ def test_shell_cmd_inputspec_outputspec_2(): ) shelly.inputs.file1 = "new_file_1.txt" shelly.inputs.file2 = "new_file_2.txt" + # all fileds from output_spec should be in output_names and generated_output_names + assert ( + shelly.output_names + == shelly.generated_output_names + == ["return_code", "stdout", "stderr", "newfile1", "newfile2"] + ) res = shelly() assert res.output.stdout == "" @@ -2714,6 +2720,20 @@ def test_shell_cmd_inputspec_outputspec_2a(): output_spec=my_output_spec, ) shelly.inputs.file1 = "new_file_1.txt" + # generated_output_names shoule know that newfile2 will not be generated + assert shelly.output_names == [ + "return_code", + "stdout", + "stderr", + "newfile1", + "newfile2", + ] + assert shelly.generated_output_names == [ + "return_code", + "stdout", + "stderr", + "newfile1", + ] res = shelly() assert res.output.stdout == "" @@ -2834,6 +2854,20 @@ def test_shell_cmd_inputspec_outputspec_3a(): ) shelly.inputs.file1 = "new_file_1.txt" shelly.inputs.file2 = "new_file_2.txt" + # generated_output_names shoule know that newfile2 will not be generated + assert shelly.output_names == [ + "return_code", + "stdout", + "stderr", + "newfile1", + "newfile2", + ] + assert shelly.generated_output_names == [ + "return_code", + "stdout", + "stderr", + "newfile1", + ] res = shelly() assert res.output.stdout == "" @@ -2884,6 +2918,12 @@ def test_shell_cmd_inputspec_outputspec_4(): ) shelly.inputs.file1 = "new_file_1.txt" shelly.inputs.additional_inp = 2 + # generated_output_names should be the same as output_names + assert ( + shelly.output_names + == shelly.generated_output_names + == ["return_code", "stdout", "stderr", "newfile1"] + ) res = shelly() assert res.output.stdout == ""