From b00c07bc9dd0bf317e571a0cbc032826a749d6c1 Mon Sep 17 00:00:00 2001 From: Ross Markello Date: Wed, 10 Oct 2018 10:03:30 -0400 Subject: [PATCH 1/2] ENH: Fixes BoutiqueInterface.run() --- nipype/interfaces/base/boutiques.py | 17 ++++++------ nipype/testing/data/boutiques_fslbet.json | 32 +++++++++++------------ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/nipype/interfaces/base/boutiques.py b/nipype/interfaces/base/boutiques.py index ef9a20f7b5..8a80465ea2 100644 --- a/nipype/interfaces/base/boutiques.py +++ b/nipype/interfaces/base/boutiques.py @@ -7,6 +7,7 @@ from itertools import chain import json import os +import re from .core import CommandLine from .specs import DynamicTraitedSpec @@ -187,7 +188,6 @@ def _populate_input_spec(self, input_list): (self.input_spec,), input_spec) - # TODO: value-keys aren't necessarily mutually exclusive; use id as key self.value_keys = value_keys def _populate_output_spec(self, output_list): @@ -243,8 +243,10 @@ def _list_outputs(self): # replace all value-keys in output name for name, valkey in self.value_keys.items(): + if valkey not in output_filename: + continue repl = self.inputs.trait_get().get(name) - # if input is specified, strip extensions + replace in output + # if input is specified, strip ext + replace value in output if repl is not None and isdefined(repl): for ext in strip: repl = repl[:-len(ext)] if repl.endswith(ext) else repl @@ -271,14 +273,13 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None): if len(val) == 0: val = Undefined setattr(outputs, key, val) - else: + if isdefined(val): if not os.path.exists(val): - val = Undefined - if not os.path.exists(val): - setattr(outputs, key, Undefined) + setattr(outputs, key, Undefined) return outputs + @property def cmdline(self): """ Prints command line with all specified arguments """ @@ -295,12 +296,12 @@ def cmdline(self): elif spec.argstr: value = self._format_arg(name, spec, value) args = args.replace(valkey, value) - return self._cmd + ' ' + args + # normalize excessive whitespace before returning + return re.sub(r'\s\s+', ' ', self._cmd + ' ' + args).strip() def help(self, returnhelp=False): """ Prints interface help """ - docs = self.boutique_spec.get('description') if docs is not None: docstring = trim(docs).split('\n') + [''] diff --git a/nipype/testing/data/boutiques_fslbet.json b/nipype/testing/data/boutiques_fslbet.json index 22e0a5f306..e2077b3b6c 100644 --- a/nipype/testing/data/boutiques_fslbet.json +++ b/nipype/testing/data/boutiques_fslbet.json @@ -14,7 +14,7 @@ { "description": "Name of generated, skulltripped image (e.g. img_bet.nii.gz)", "value-key": "[OUT_FILE]", - "type": "File", + "type": "String", "optional": false, "id": "out_file", "name": "Output file" @@ -249,15 +249,15 @@ "path-template": "[OUT_FILE].nii.gz", "description": "Default skullstripped image generated by BET", "optional": true, - "id": "outfile", - "name": "Output mask file", + "id": "output_file", + "name": "Output skullstripped file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, { "path-template": "[OUT_FILE]_mask.nii.gz", "description": "Binary mask file (from -m option)", "optional": true, - "id": "binary_mask", + "id": "binary_skull_file", "name": "Output binary mask file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -273,7 +273,7 @@ "path-template": "[OUT_FILE]_skull.nii.gz", "description": "Approximate skull image file", "optional": true, - "id": "approx_skull_img", + "id": "approx_skull_img_file", "name": "Approximate skull file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -281,7 +281,7 @@ "path-template": "[OUT_FILE]_mesh.vtk", "description": "Mesh in VTK format", "optional": true, - "id": "output_vtk_mesh", + "id": "vtk_mesh_file", "name": "VTK mesh", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -289,7 +289,7 @@ "path-template": "[OUT_FILE]_skull_mask.nii.gz", "description": "Output mask for skull image", "optional": true, - "id": "skull_mask", + "id": "skull_mask_file", "name": "Skull mask image", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -297,7 +297,7 @@ "path-template": "[OUT_FILE]_inskull_mask.nii.gz", "description": "The in-skull mask file from betsurf (from -A or -A2)", "optional": true, - "id": "out_inskull_mask", + "id": "inskull_mask_file", "name": "Output in-skull mask file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -305,7 +305,7 @@ "path-template": "[OUT_FILE]_inskull_mesh.nii.gz", "description": "The in-skull mesh file from betsurf (from -A or -A2)", "optional": true, - "id": "out_inskull_mesh", + "id": "inskull_mesh_file", "name": "Output in-skull mesh file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -313,7 +313,7 @@ "path-template": "[OUT_FILE]_inskull_mesh.off", "description": "The in-skull mesh .off file from betsurf (from -A or -A2)", "optional": true, - "id": "out_inskull_off", + "id": "inskull_off_file", "name": "Output in-skull mesh off file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -321,7 +321,7 @@ "path-template": "[OUT_FILE]_outskin_mask.nii.gz", "description": "The out-skin mask file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskin_mask", + "id": "outskin_mask_file", "name": "Output out-skin mask file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -329,7 +329,7 @@ "path-template": "[OUT_FILE]_outskin_mesh.nii.gz", "description": "The out-skin mesh file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskin_mesh", + "id": "outskin_mesh_file", "name": "Output out-skin mesh file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -337,7 +337,7 @@ "path-template": "[OUT_FILE]_outskin_mesh.off", "description": "The out-skin mesh .off file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskin_off", + "id": "outskin_off_file", "name": "Output out-skin mesh off file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -345,7 +345,7 @@ "path-template": "[OUT_FILE]_outskull_mask.nii.gz", "description": "The out-skull mask file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskull_mask", + "id": "outskull_mask_file", "name": "Output out-skull mask file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -353,7 +353,7 @@ "path-template": "[OUT_FILE]_outskull_mesh.nii.gz", "description": "The out-skull mesh file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskull_mesh", + "id": "outskull_mesh_file", "name": "Output out-skull mesh file", "path-template-stripped-extensions": [".nii.gz", ".nii"] }, @@ -361,7 +361,7 @@ "path-template": "[OUT_FILE]_outskull_mesh.off", "description": "The out-skull mesh .off file from betsurf (from -A or -A2)", "optional": true, - "id": "out_outskull_off", + "id": "outskull_off_file", "name": "Output out-skull mesh off file", "path-template-stripped-extensions": [".nii.gz", ".nii"] } From 8cac9a82cd29204e418c1f6f73df68009217cf20 Mon Sep 17 00:00:00 2001 From: Ross Markello Date: Wed, 10 Oct 2018 10:06:33 -0400 Subject: [PATCH 2/2] TEST: Adds basic test for BoutiqueInterface Checks that input/output spec was generated and that calls to .help() returns a string --- nipype/interfaces/base/__init__.py | 2 + .../interfaces/base/tests/test_boutiques.py | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 nipype/interfaces/base/tests/test_boutiques.py diff --git a/nipype/interfaces/base/__init__.py b/nipype/interfaces/base/__init__.py index f617064b2f..bd0ba65be9 100644 --- a/nipype/interfaces/base/__init__.py +++ b/nipype/interfaces/base/__init__.py @@ -8,6 +8,8 @@ This module defines the API of all nipype interfaces. """ +from .boutiques import (BoutiqueInterface) + from .core import (Interface, BaseInterface, SimpleInterface, CommandLine, StdOutCommandLine, MpiCommandLine, SEMLikeCommandLine, LibraryBaseInterface, PackageInfo) diff --git a/nipype/interfaces/base/tests/test_boutiques.py b/nipype/interfaces/base/tests/test_boutiques.py new file mode 100644 index 0000000000..8d996183ea --- /dev/null +++ b/nipype/interfaces/base/tests/test_boutiques.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: + +from ...base import BoutiqueInterface +from ....testing import example_data + + +EXAMPLE_SPECS = [ + dict( + spec=example_data('boutiques_fslbet.json'), + inputs=[ + 'additional_surfaces_flag', 'additional_surfaces_t2', + 'approx_skull_flag', 'binary_mask_flag', 'center_of_gravity', + 'debug_flag', 'fractional_intensity', 'head_radius', 'in_file', + 'no_seg_output_flag', 'out_file', 'overlay_flag', + 'reduce_bias_flag', 'residual_optic_cleanup_flag', + 'robust_iters_flag', 'slice_padding_flag', 'thresholding_flag', + 'verbose_flag', 'vg_fractional_intensity', 'vtk_mesh', + 'whole_set_mask_flag' + ], + outputs=[ + 'approx_skull_img_file', 'binary_skull_file', 'inskull_mask_file', + 'inskull_mesh_file', 'inskull_off_file', 'output_file', + 'outskin_mask_file', 'outskin_mesh_file', 'outskin_off_file', + 'outskull_mask_file', 'outskull_mesh_file', 'outskull_off_file', + 'overlay_file', 'skull_mask_file', 'vtk_mesh_file' + ] + ) +] + + +def test_BoutiqueInterface(): + for example in EXAMPLE_SPECS: + interface = BoutiqueInterface(example['spec']) + + # check interface input/output spec generation + inputs = list(interface.input_spec().get()) + outputs = list(interface.output_spec().get()) + assert all([f in inputs for f in example['inputs']]) + assert all([f in outputs for f in example['outputs']]) + + # confirm help works + assert isinstance(interface.help(True), str)