From 7cb203825af13de69d2f15444e8b74ae1dffaaf0 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 24 Nov 2018 20:44:28 -0800 Subject: [PATCH 01/20] [MAINT] Outsource checks of inputs from interface Another PR removing stuff from the interface class definition. --- nipype/interfaces/base/core.py | 59 +++-------------- nipype/interfaces/base/specs.py | 65 ++++++++++++++++++- nipype/interfaces/base/tests/test_core.py | 5 -- nipype/interfaces/base/tests/test_specs.py | 74 ++++++++++++++++++++-- nipype/interfaces/freesurfer/preprocess.py | 8 ++- nipype/utils/errors.py | 60 ++++++++++++++++++ 6 files changed, 208 insertions(+), 63 deletions(-) create mode 100644 nipype/utils/errors.py diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 40efa1f33a..06ef43c7cf 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -40,7 +40,8 @@ from .traits_extension import traits, isdefined, TraitError from .specs import (BaseInterfaceInputSpec, CommandLineInputSpec, - StdOutCommandLineInputSpec, MpiCommandLineInputSpec) + StdOutCommandLineInputSpec, MpiCommandLineInputSpec, + check_mandatory_inputs) from .support import (Bunch, InterfaceResult, NipypeInterfaceError) from future import standard_library @@ -343,53 +344,6 @@ def _get_filecopy_info(cls): info.append(dict(key=name, copy=spec.copyfile)) return info - def _check_requires(self, spec, name, value): - """ check if required inputs are satisfied - """ - if spec.requires: - values = [ - not isdefined(getattr(self.inputs, field)) - for field in spec.requires - ] - if any(values) and isdefined(value): - msg = ("%s requires a value for input '%s' because one of %s " - "is set. For a list of required inputs, see %s.help()" % - (self.__class__.__name__, name, - ', '.join(spec.requires), self.__class__.__name__)) - raise ValueError(msg) - - def _check_xor(self, spec, name, value): - """ check if mutually exclusive inputs are satisfied - """ - if spec.xor: - values = [ - isdefined(getattr(self.inputs, field)) for field in spec.xor - ] - if not any(values) and not isdefined(value): - msg = ("%s requires a value for one of the inputs '%s'. " - "For a list of required inputs, see %s.help()" % - (self.__class__.__name__, ', '.join(spec.xor), - self.__class__.__name__)) - raise ValueError(msg) - - def _check_mandatory_inputs(self): - """ Raises an exception if a mandatory input is Undefined - """ - for name, spec in list(self.inputs.traits(mandatory=True).items()): - value = getattr(self.inputs, name) - self._check_xor(spec, name, value) - if not isdefined(value) and spec.xor is None: - msg = ("%s requires a value for input '%s'. " - "For a list of required inputs, see %s.help()" % - (self.__class__.__name__, name, - self.__class__.__name__)) - raise ValueError(msg) - if isdefined(value): - self._check_requires(spec, name, value) - for name, spec in list( - self.inputs.traits(mandatory=None, transient=None).items()): - self._check_requires(spec, name, getattr(self.inputs, name)) - def _check_version_requirements(self, trait_object, raise_exception=True): """ Raises an exception on version mismatch """ @@ -474,7 +428,7 @@ def run(self, cwd=None, ignore_exception=None, **inputs): enable_rm = config.resource_monitor and self.resource_monitor self.inputs.trait_set(**inputs) - self._check_mandatory_inputs() + check_mandatory_inputs(self.inputs) self._check_version_requirements(self.inputs) interface = self.__class__ self._duecredit_cite() @@ -818,7 +772,12 @@ def cmd(self): def cmdline(self): """ `command` plus any arguments (args) validates arguments and generates command line""" - self._check_mandatory_inputs() + if not check_mandatory_inputs(self.inputs, raise_exc=False): + iflogger.warning( + 'Some inputs are not valid. Please make sure all mandatory ' + 'inputs, required inputs and mutually-exclusive inputs are ' + 'set or in a sane state.') + allargs = [self._cmd_prefix + self.cmd] + self._parse_inputs() return ' '.join(allargs) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index f02d5bd709..23beca98c0 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -19,6 +19,8 @@ from packaging.version import Version from ...utils.filemanip import md5, hash_infile, hash_timestamp, to_str +from ...utils.errors import ( + MandatoryInputError, MutuallyExclusiveInputError, RequiredInputError) from .traits_extension import ( traits, Undefined, @@ -114,9 +116,8 @@ def _xor_warn(self, obj, name, old, new): trait_change_notify=False, **{ '%s' % name: Undefined }) - msg = ('Input "%s" is mutually exclusive with input "%s", ' - 'which is already set') % (name, trait_name) - raise IOError(msg) + raise MutuallyExclusiveInputError( + self, name, name_other=trait_name) def _deprecated_warn(self, obj, name, old, new): """Checks if a user assigns a value to a deprecated trait @@ -375,3 +376,61 @@ class MpiCommandLineInputSpec(CommandLineInputSpec): n_procs = traits.Int(desc="Num processors to specify to mpiexec. Do not " "specify if this is managed externally (e.g. through " "SGE)") + + +def check_requires(inputs, spec, value): + """check if required inputs are satisfied + """ + if not spec.requires: + return True + + # Check value and all required inputs' values defined + values = [isdefined(value)] + [ + isdefined(getattr(inputs, field)) + for field in spec.requires] + return all(values) + +def check_xor(inputs, spec, value): + """ check if mutually exclusive inputs are satisfied + """ + if not spec.xor: + return True + + print(inputs) + values = [isdefined(value)] + [ + isdefined(getattr(inputs, field)) for field in spec.xor] + return sum(values) + +def check_mandatory_inputs(inputs, raise_exc=True): + """ Raises an exception if a mandatory input is Undefined + """ + # Check mandatory, not xor-ed inputs. + for name, spec in list(inputs.traits(mandatory=True).items()): + value = getattr(inputs, name) + # Mandatory field is defined, check xor'ed inputs + cxor = check_xor(inputs, spec, value) + if cxor != 1: + if raise_exc: + raise MutuallyExclusiveInputError(inputs, name, cxor) + return False + + # Simplest case: no xor metadata and not defined + if cxor is True and not isdefined(value): + if raise_exc: + raise MandatoryInputError(inputs, name) + return False + + # Check whether mandatory inputs require others + if not check_requires(inputs, spec, value): + if raise_exc: + raise RequiredInputError(inputs, name) + return False + + # Check requirements of non-mandatory inputs + for name, spec in list( + inputs.traits(mandatory=None, transient=None).items()): + if not check_requires(inputs, spec, getattr(inputs, name)): + if raise_exc: + raise RequiredInputError(inputs, name) + + return True diff --git a/nipype/interfaces/base/tests/test_core.py b/nipype/interfaces/base/tests/test_core.py index 392d38706f..19c82f34a0 100644 --- a/nipype/interfaces/base/tests/test_core.py +++ b/nipype/interfaces/base/tests/test_core.py @@ -95,11 +95,6 @@ class DerivedInterface(nib.BaseInterface): assert DerivedInterface._get_filecopy_info()[1]['key'] == 'zoo' assert not DerivedInterface._get_filecopy_info()[1]['copy'] assert DerivedInterface().inputs.foo == nib.Undefined - with pytest.raises(ValueError): - DerivedInterface()._check_mandatory_inputs() - assert DerivedInterface(goo=1)._check_mandatory_inputs() is None - with pytest.raises(ValueError): - DerivedInterface().run() with pytest.raises(NotImplementedError): DerivedInterface(goo=1).run() diff --git a/nipype/interfaces/base/tests/test_specs.py b/nipype/interfaces/base/tests/test_specs.py index f08fa68adf..4bf2dbd534 100644 --- a/nipype/interfaces/base/tests/test_specs.py +++ b/nipype/interfaces/base/tests/test_specs.py @@ -2,15 +2,18 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: from __future__ import print_function, unicode_literals -from future import standard_library import os import warnings - import pytest +from future import standard_library from ....utils.filemanip import split_filename from ... import base as nib from ...base import traits, Undefined +from ..specs import ( + check_mandatory_inputs, MandatoryInputError, + MutuallyExclusiveInputError, RequiredInputError +) from ....interfaces import fsl from ...utility.wrappers import Function from ....pipeline import Node @@ -55,8 +58,8 @@ def test_TraitedSpec_tab_completion(): bet_nd = Node(fsl.BET(), name='bet') bet_interface = fsl.BET() bet_inputs = bet_nd.inputs.class_editable_traits() - bet_outputs = bet_nd.outputs.class_editable_traits() - + bet_outputs = bet_nd.outputs.class_editable_traits() + # Check __all__ for bet node and interface inputs assert set(bet_nd.inputs.__all__) == set(bet_inputs) assert set(bet_interface.inputs.__all__) == set(bet_inputs) @@ -433,3 +436,66 @@ def test_ImageFile(): with pytest.raises(nib.TraitError): x.nocompress = 'test.nii.gz' x.nocompress = 'test.mgh' + + +def test_inputs_checks(): + + class InputSpec(nib.TraitedSpec): + goo = nib.traits.Int(desc='a random int', mandatory=True) + + class DerivedInterface(nib.BaseInterface): + input_spec = InputSpec + resource_monitor = False + + assert check_mandatory_inputs( + DerivedInterface(goo=1).inputs) + with pytest.raises(MandatoryInputError): + check_mandatory_inputs( + DerivedInterface().inputs) + with pytest.raises(MandatoryInputError): + DerivedInterface().run() + + class InputSpec(nib.TraitedSpec): + goo = nib.traits.Int(desc='a random int', mandatory=True, + requires=['woo']) + woo = nib.traits.Int(desc='required by goo') + + class DerivedInterface(nib.BaseInterface): + input_spec = InputSpec + resource_monitor = False + + assert check_mandatory_inputs( + DerivedInterface(goo=1, woo=1).inputs) + with pytest.raises(RequiredInputError): + check_mandatory_inputs( + DerivedInterface(goo=1).inputs) + with pytest.raises(RequiredInputError): + DerivedInterface(goo=1).run() + + class InputSpec(nib.TraitedSpec): + goo = nib.traits.Int(desc='a random int', mandatory=True, + xor=['woo']) + woo = nib.traits.Int(desc='a random int', mandatory=True, + xor=['goo']) + + class DerivedInterface(nib.BaseInterface): + input_spec = InputSpec + resource_monitor = False + + # If either goo or woo are set, then okay! + assert check_mandatory_inputs( + DerivedInterface(goo=1).inputs) + assert check_mandatory_inputs( + DerivedInterface(woo=1).inputs) + + # None are set, raise MandatoryInputError + with pytest.raises(MutuallyExclusiveInputError): + check_mandatory_inputs( + DerivedInterface().inputs) + + # Both are set, raise MutuallyExclusiveInputError + with pytest.raises(MutuallyExclusiveInputError): + check_mandatory_inputs( + DerivedInterface(goo=1, woo=1).inputs) + with pytest.raises(MutuallyExclusiveInputError): + DerivedInterface(goo=1, woo=1).run() diff --git a/nipype/interfaces/freesurfer/preprocess.py b/nipype/interfaces/freesurfer/preprocess.py index 2941968f85..b25c7fc6f6 100644 --- a/nipype/interfaces/freesurfer/preprocess.py +++ b/nipype/interfaces/freesurfer/preprocess.py @@ -21,6 +21,7 @@ from ..base import (TraitedSpec, File, traits, Directory, InputMultiPath, OutputMultiPath, CommandLine, CommandLineInputSpec, isdefined) +from ..base.specs import check_mandatory_inputs from .base import (FSCommand, FSTraitedSpec, FSTraitedSpecOpenMP, FSCommandOpenMP, Info) from .utils import copy2subjdir @@ -634,7 +635,12 @@ def _get_filelist(self, outdir): def cmdline(self): """ `command` plus any arguments (args) validates arguments and generates command line""" - self._check_mandatory_inputs() + if not check_mandatory_inputs(self.inputs, raise_exc=False): + iflogger.warning( + 'Some inputs are not valid. Please make sure all mandatory ' + 'inputs, required inputs and mutually-exclusive inputs are ' + 'set or in a sane state.') + outdir = self._get_outdir() cmd = [] if not os.path.exists(outdir): diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py new file mode 100644 index 0000000000..74cf1340c6 --- /dev/null +++ b/nipype/utils/errors.py @@ -0,0 +1,60 @@ +# -*- 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: +"""Errors and exceptions +""" +from __future__ import (print_function, division, unicode_literals, + absolute_import) + + +class MandatoryInputError(ValueError): + """Raised when one input with the ``mandatory`` metadata set to ``True`` is + not defined.""" + def __init__(self, inputspec, name): + classname = inputspec.__class__.__name__ + if classname.endswith('InputSpec'): + classname = classname[:len('InputSpec')] + msg = ( + 'Interface "{classname}" requires a value for input {name}. ' + 'For a list of required inputs, see {classname}.help().').format( + classname=classname, name=name) + super(MandatoryInputError, self).__init__(msg) + +class MutuallyExclusiveInputError(ValueError): + """Raised when none or more than one mutually-exclusive inputs are set.""" + def __init__(self, inputspec, name, values_defined=None, name_other=None): + classname = inputspec.__class__.__name__ + if classname.endswith('InputSpec') and classname != 'InputSpec': + classname = classname[:-len('InputSpec')] + + if values_defined: + xor = [name]+ inputspec.traits()[name].xor + msg = ('Interface "{classname}" has mutually-exclusive inputs. ' + 'Exactly one of ({xor}) should be set, but {n:d} were set. ' + 'For a list of mutually-exclusive inputs, see ' + '{classname}.help().').format(classname=classname, + xor='|'.join(xor), + n=values_defined) + + else: + msg = ('Interface "{classname}" has mutually-exclusive inputs. ' + 'Input "{name}" is mutually exclusive with input ' + '"{name_other}", which is already set').format( + classname=classname, name=name, name_other=name_other) + super(MutuallyExclusiveInputError, self).__init__(msg) + +class RequiredInputError(ValueError): + """Raised when one input requires some other and those or some of + those are ``Undefined``.""" + def __init__(self, inputspec, name): + classname = inputspec.__class__.__name__ + if classname.endswith('InputSpec'): + classname = classname[:-len('InputSpec')] + requires = inputspec.traits()[name].requires + + msg = ('Interface "{classname}" requires a value for input {name} ' + 'because one of ({requires}) is set. For a list of required ' + 'inputs, see {classname}.help().').format( + classname=classname, name=name, + requires=', '.join(requires)) + super(RequiredInputError, self).__init__(msg) From a15c6fb5e9786f5911f39eccbf56ad7049e0e0f7 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 24 Nov 2018 21:05:50 -0800 Subject: [PATCH 02/20] remove forgotten print --- nipype/interfaces/base/specs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 23beca98c0..107a90672c 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -396,7 +396,6 @@ def check_xor(inputs, spec, value): if not spec.xor: return True - print(inputs) values = [isdefined(value)] + [ isdefined(getattr(inputs, field)) for field in spec.xor] return sum(values) From a3600d910ba3487e138dc3a1f0532f4f21e2d005 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 24 Nov 2018 22:49:04 -0800 Subject: [PATCH 03/20] fix mutually-exclusive check --- nipype/interfaces/base/specs.py | 23 ++++++++++++----------- nipype/utils/errors.py | 4 ++-- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 107a90672c..1e5bca3494 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -378,26 +378,25 @@ class MpiCommandLineInputSpec(CommandLineInputSpec): "SGE)") -def check_requires(inputs, spec, value): +def check_requires(inputs, requires, value): """check if required inputs are satisfied """ - if not spec.requires: + if not requires: return True # Check value and all required inputs' values defined values = [isdefined(value)] + [ isdefined(getattr(inputs, field)) - for field in spec.requires] + for field in requires] return all(values) -def check_xor(inputs, spec, value): +def check_xor(inputs, xor): """ check if mutually exclusive inputs are satisfied """ - if not spec.xor: + if len(xor) == 1: return True - values = [isdefined(value)] + [ - isdefined(getattr(inputs, field)) for field in spec.xor] + values = [isdefined(getattr(inputs, field)) for field in xor] return sum(values) def check_mandatory_inputs(inputs, raise_exc=True): @@ -407,10 +406,12 @@ def check_mandatory_inputs(inputs, raise_exc=True): for name, spec in list(inputs.traits(mandatory=True).items()): value = getattr(inputs, name) # Mandatory field is defined, check xor'ed inputs - cxor = check_xor(inputs, spec, value) + xor = list(set([name] + spec.xor)) + cxor = check_xor(inputs, xor) if cxor != 1: if raise_exc: - raise MutuallyExclusiveInputError(inputs, name, cxor) + raise MutuallyExclusiveInputError( + inputs, name, values_defined=cxor) return False # Simplest case: no xor metadata and not defined @@ -420,7 +421,7 @@ def check_mandatory_inputs(inputs, raise_exc=True): return False # Check whether mandatory inputs require others - if not check_requires(inputs, spec, value): + if not check_requires(inputs, spec.requires, value): if raise_exc: raise RequiredInputError(inputs, name) return False @@ -428,7 +429,7 @@ def check_mandatory_inputs(inputs, raise_exc=True): # Check requirements of non-mandatory inputs for name, spec in list( inputs.traits(mandatory=None, transient=None).items()): - if not check_requires(inputs, spec, getattr(inputs, name)): + if not check_requires(inputs, spec.requires, getattr(inputs, name)): if raise_exc: raise RequiredInputError(inputs, name) diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py index 74cf1340c6..a55f3179f2 100644 --- a/nipype/utils/errors.py +++ b/nipype/utils/errors.py @@ -27,8 +27,8 @@ def __init__(self, inputspec, name, values_defined=None, name_other=None): if classname.endswith('InputSpec') and classname != 'InputSpec': classname = classname[:-len('InputSpec')] - if values_defined: - xor = [name]+ inputspec.traits()[name].xor + if values_defined is not None: + xor = list(set([name]+ inputspec.traits()[name].xor)) msg = ('Interface "{classname}" has mutually-exclusive inputs. ' 'Exactly one of ({xor}) should be set, but {n:d} were set. ' 'For a list of mutually-exclusive inputs, see ' From c095d65e867415341883fbcf77cc3243a20232fa Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 24 Nov 2018 23:19:49 -0800 Subject: [PATCH 04/20] fix bug trying to append a spec.xor that is None --- nipype/interfaces/base/specs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 1e5bca3494..dc476965c5 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -406,7 +406,7 @@ def check_mandatory_inputs(inputs, raise_exc=True): for name, spec in list(inputs.traits(mandatory=True).items()): value = getattr(inputs, name) # Mandatory field is defined, check xor'ed inputs - xor = list(set([name] + spec.xor)) + xor = list(set([name] + spec.xor or [])) cxor = check_xor(inputs, xor) if cxor != 1: if raise_exc: From 61d553c42c24bc27b0977cabccabccec84b2b92c Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sat, 24 Nov 2018 23:45:40 -0800 Subject: [PATCH 05/20] fix precedence --- nipype/interfaces/base/specs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index dc476965c5..a62cdec437 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -406,7 +406,7 @@ def check_mandatory_inputs(inputs, raise_exc=True): for name, spec in list(inputs.traits(mandatory=True).items()): value = getattr(inputs, name) # Mandatory field is defined, check xor'ed inputs - xor = list(set([name] + spec.xor or [])) + xor = list(set([name] + (spec.xor or []))) cxor = check_xor(inputs, xor) if cxor != 1: if raise_exc: From 1ade70c18b0b033a2fc9216a018cea38549846ea Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 00:42:48 -0800 Subject: [PATCH 06/20] outsource ``_check_version_requirements`` --- nipype/interfaces/base/core.py | 50 +------- nipype/interfaces/base/specs.py | 51 ++++++++- nipype/interfaces/base/tests/test_core.py | 127 +-------------------- nipype/interfaces/base/tests/test_specs.py | 90 ++++++++++++++- nipype/utils/errors.py | 28 ++++- 5 files changed, 171 insertions(+), 175 deletions(-) diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 06ef43c7cf..dcc4580191 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -29,7 +29,7 @@ import simplejson as json from dateutil.parser import parse as parseutc -from ... import config, logging, LooseVersion +from ... import config, logging from ...utils.provenance import write_provenance from ...utils.misc import trim, str2bool, rgetcwd from ...utils.filemanip import (FileNotFoundError, split_filename, @@ -41,7 +41,7 @@ from .traits_extension import traits, isdefined, TraitError from .specs import (BaseInterfaceInputSpec, CommandLineInputSpec, StdOutCommandLineInputSpec, MpiCommandLineInputSpec, - check_mandatory_inputs) + check_mandatory_inputs, check_version) from .support import (Bunch, InterfaceResult, NipypeInterfaceError) from future import standard_library @@ -344,46 +344,6 @@ def _get_filecopy_info(cls): info.append(dict(key=name, copy=spec.copyfile)) return info - def _check_version_requirements(self, trait_object, raise_exception=True): - """ Raises an exception on version mismatch - """ - unavailable_traits = [] - # check minimum version - check = dict(min_ver=lambda t: t is not None) - names = trait_object.trait_names(**check) - - if names and self.version: - version = LooseVersion(str(self.version)) - for name in names: - min_ver = LooseVersion( - str(trait_object.traits()[name].min_ver)) - if min_ver > version: - unavailable_traits.append(name) - if not isdefined(getattr(trait_object, name)): - continue - if raise_exception: - raise Exception( - 'Trait %s (%s) (version %s < required %s)' % - (name, self.__class__.__name__, version, min_ver)) - - # check maximum version - check = dict(max_ver=lambda t: t is not None) - names = trait_object.trait_names(**check) - if names and self.version: - version = LooseVersion(str(self.version)) - for name in names: - max_ver = LooseVersion( - str(trait_object.traits()[name].max_ver)) - if max_ver < version: - unavailable_traits.append(name) - if not isdefined(getattr(trait_object, name)): - continue - if raise_exception: - raise Exception( - 'Trait %s (%s) (version %s > required %s)' % - (name, self.__class__.__name__, version, max_ver)) - return unavailable_traits - def _run_interface(self, runtime): """ Core function that executes interface """ @@ -429,7 +389,7 @@ def run(self, cwd=None, ignore_exception=None, **inputs): enable_rm = config.resource_monitor and self.resource_monitor self.inputs.trait_set(**inputs) check_mandatory_inputs(self.inputs) - self._check_version_requirements(self.inputs) + check_version(self.inputs, version=self.version) interface = self.__class__ self._duecredit_cite() @@ -555,8 +515,8 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None): if predicted_outputs: _unavailable_outputs = [] if outputs: - _unavailable_outputs = \ - self._check_version_requirements(self._outputs()) + _unavailable_outputs = check_version( + self._outputs(), self.version) for key, val in list(predicted_outputs.items()): if needed_outputs and key not in needed_outputs: continue diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index a62cdec437..c6e860ff51 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -20,7 +20,8 @@ from ...utils.filemanip import md5, hash_infile, hash_timestamp, to_str from ...utils.errors import ( - MandatoryInputError, MutuallyExclusiveInputError, RequiredInputError) + MandatoryInputError, MutuallyExclusiveInputError, RequiredInputError, + VersionIOError) from .traits_extension import ( traits, Undefined, @@ -434,3 +435,51 @@ def check_mandatory_inputs(inputs, raise_exc=True): raise RequiredInputError(inputs, name) return True + +def check_version(traited_spec, version=None, raise_exc=True): + """ Raises an exception on version mismatch + """ + + # no version passed on to check against + if not version: + return [] + + # check minimum version + names = traited_spec.trait_names( + min_ver=lambda t: t is not None) + \ + traited_spec.trait_names( + max_ver=lambda t: t is not None) + + # no traits defined any versions + if not names: + return [] + + version = Version(str(version)) + unavailable_traits = [] + for name in names: + value_set = isdefined(getattr(traited_spec, name)) + min_ver = traited_spec.traits()[name].min_ver + if min_ver: + min_ver = Version(str(min_ver)) + + max_ver = traited_spec.traits()[name].max_ver + if max_ver: + max_ver = Version(str(max_ver)) + + if min_ver and max_ver: + if max_ver < min_ver: + raise AssertionError( + 'Trait "%s" (%s) has incongruent version metadata ' + '(``max_ver`` is lower than ``min_ver``).' % ( + traited_spec.__class__.__name__, name)) + + if min_ver and (min_ver > version): + unavailable_traits.append(name) + if value_set and raise_exc: + raise VersionIOError(traited_spec, name, version) + if max_ver and (max_ver < version): + unavailable_traits.append(name) + if value_set and raise_exc: + raise VersionIOError(traited_spec, name, version) + + return list(set(unavailable_traits)) diff --git a/nipype/interfaces/base/tests/test_core.py b/nipype/interfaces/base/tests/test_core.py index 19c82f34a0..cba88be0a5 100644 --- a/nipype/interfaces/base/tests/test_core.py +++ b/nipype/interfaces/base/tests/test_core.py @@ -172,136 +172,15 @@ def __init__(self, **inputs): assert '8562a5623562a871115eb14822ee8d02' == hashvalue -class MinVerInputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int', min_ver='0.9') - -class MaxVerInputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int', max_ver='0.7') - - -def test_input_version_1(): - class DerivedInterface1(nib.BaseInterface): - input_spec = MinVerInputSpec - - obj = DerivedInterface1() - obj._check_version_requirements(obj.inputs) - +def test_stop_on_unknown_version(): config.set('execution', 'stop_on_unknown_version', True) + ci = nib.CommandLine(command='which') with pytest.raises(ValueError) as excinfo: - obj._check_version_requirements(obj.inputs) + _ = ci.version assert "no version information" in str(excinfo.value) - config.set_default_config() - -def test_input_version_2(): - class DerivedInterface1(nib.BaseInterface): - input_spec = MinVerInputSpec - _version = '0.8' - - obj = DerivedInterface1() - obj.inputs.foo = 1 - with pytest.raises(Exception) as excinfo: - obj._check_version_requirements(obj.inputs) - assert "version 0.8 < required 0.9" in str(excinfo.value) - - -def test_input_version_3(): - class DerivedInterface1(nib.BaseInterface): - input_spec = MinVerInputSpec - _version = '0.10' - - obj = DerivedInterface1() - obj._check_version_requirements(obj.inputs) - - -def test_input_version_4(): - class DerivedInterface1(nib.BaseInterface): - input_spec = MinVerInputSpec - _version = '0.9' - - obj = DerivedInterface1() - obj.inputs.foo = 1 - obj._check_version_requirements(obj.inputs) - - -def test_input_version_5(): - class DerivedInterface2(nib.BaseInterface): - input_spec = MaxVerInputSpec - _version = '0.8' - - obj = DerivedInterface2() - obj.inputs.foo = 1 - with pytest.raises(Exception) as excinfo: - obj._check_version_requirements(obj.inputs) - assert "version 0.8 > required 0.7" in str(excinfo.value) - - -def test_input_version_6(): - class DerivedInterface1(nib.BaseInterface): - input_spec = MaxVerInputSpec - _version = '0.7' - - obj = DerivedInterface1() - obj.inputs.foo = 1 - obj._check_version_requirements(obj.inputs) - - -def test_output_version(): - class InputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int') - - class OutputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int', min_ver='0.9') - - class DerivedInterface1(nib.BaseInterface): - input_spec = InputSpec - output_spec = OutputSpec - _version = '0.10' - resource_monitor = False - - obj = DerivedInterface1() - assert obj._check_version_requirements(obj._outputs()) == [] - - class InputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int') - - class OutputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int', min_ver='0.11') - - class DerivedInterface1(nib.BaseInterface): - input_spec = InputSpec - output_spec = OutputSpec - _version = '0.10' - resource_monitor = False - - obj = DerivedInterface1() - assert obj._check_version_requirements(obj._outputs()) == ['foo'] - - class InputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int') - - class OutputSpec(nib.TraitedSpec): - foo = nib.traits.Int(desc='a random int', min_ver='0.11') - - class DerivedInterface1(nib.BaseInterface): - input_spec = InputSpec - output_spec = OutputSpec - _version = '0.10' - resource_monitor = False - - def _run_interface(self, runtime): - return runtime - - def _list_outputs(self): - return {'foo': 1} - - obj = DerivedInterface1() - with pytest.raises(KeyError): - obj.run() - - def test_Commandline(): with pytest.raises(Exception): nib.CommandLine() diff --git a/nipype/interfaces/base/tests/test_specs.py b/nipype/interfaces/base/tests/test_specs.py index 4bf2dbd534..9e3fba03de 100644 --- a/nipype/interfaces/base/tests/test_specs.py +++ b/nipype/interfaces/base/tests/test_specs.py @@ -11,8 +11,9 @@ from ... import base as nib from ...base import traits, Undefined from ..specs import ( - check_mandatory_inputs, MandatoryInputError, - MutuallyExclusiveInputError, RequiredInputError + check_mandatory_inputs, check_version, + MandatoryInputError, MutuallyExclusiveInputError, + RequiredInputError, VersionIOError ) from ....interfaces import fsl from ...utility.wrappers import Function @@ -137,7 +138,7 @@ class MyInterface(nib.BaseInterface): myif.inputs.foo = 1 assert myif.inputs.foo == 1 set_bar = lambda: setattr(myif.inputs, 'bar', 1) - with pytest.raises(IOError): + with pytest.raises(MutuallyExclusiveInputError): set_bar() assert myif.inputs.foo == 1 myif.inputs.kung = 2 @@ -499,3 +500,86 @@ class DerivedInterface(nib.BaseInterface): DerivedInterface(goo=1, woo=1).inputs) with pytest.raises(MutuallyExclusiveInputError): DerivedInterface(goo=1, woo=1).run() + + + +def test_input_version(): + class MinVerInputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', min_ver='0.5') + + + assert check_version(MinVerInputSpec(), '0.6') == [] + assert check_version(MinVerInputSpec(), '0.4') == ['foo'] + with pytest.raises(VersionIOError): + check_version(MinVerInputSpec(foo=1), '0.4') + + + class MaxVerInputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', max_ver='0.7') + + + assert check_version(MaxVerInputSpec(), '0.6') == [] + assert check_version(MaxVerInputSpec(), '0.8') == ['foo'] + with pytest.raises(VersionIOError): + check_version(MaxVerInputSpec(foo=1), '0.8') + + + class MinMaxVerInputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', max_ver='0.7', + min_ver='0.5') + + + assert check_version(MinMaxVerInputSpec(), '0.6') == [] + assert check_version(MinMaxVerInputSpec(), '0.4') == ['foo'] + assert check_version(MinMaxVerInputSpec(), '0.8') == ['foo'] + with pytest.raises(VersionIOError): + check_version(MinMaxVerInputSpec(foo=1), '0.8') + with pytest.raises(VersionIOError): + check_version(MinMaxVerInputSpec(foo=1), '0.4') + + + class FixedVerInputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', max_ver='0.6.2', + min_ver='0.6.2') + + + assert check_version(FixedVerInputSpec(), '0.6.2') == [] + assert check_version(FixedVerInputSpec(), '0.6.1') == ['foo'] + assert check_version(FixedVerInputSpec(), '0.6.3') == ['foo'] + with pytest.raises(VersionIOError): + check_version(FixedVerInputSpec(foo=1), '0.6.1') + with pytest.raises(VersionIOError): + check_version(FixedVerInputSpec(foo=1), '0.6.3') + + + class IncongruentVerInputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', max_ver='0.5', + min_ver='0.7') + + with pytest.raises(AssertionError): + check_version(IncongruentVerInputSpec(), '0.6') + with pytest.raises(AssertionError): + check_version(IncongruentVerInputSpec(foo=1), '0.6') + + + class InputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int') + + class OutputSpec(nib.TraitedSpec): + foo = nib.traits.Int(desc='a random int', min_ver='0.11') + + class DerivedInterface1(nib.BaseInterface): + input_spec = InputSpec + output_spec = OutputSpec + _version = '0.10' + resource_monitor = False + + def _run_interface(self, runtime): + return runtime + + def _list_outputs(self): + return {'foo': 1} + + obj = DerivedInterface1() + with pytest.raises(KeyError): + obj.run() diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py index a55f3179f2..c1ceb38b5c 100644 --- a/nipype/utils/errors.py +++ b/nipype/utils/errors.py @@ -12,7 +12,7 @@ class MandatoryInputError(ValueError): not defined.""" def __init__(self, inputspec, name): classname = inputspec.__class__.__name__ - if classname.endswith('InputSpec'): + if classname.endswith('InputSpec') and classname != 'InputSpec': classname = classname[:len('InputSpec')] msg = ( 'Interface "{classname}" requires a value for input {name}. ' @@ -48,7 +48,7 @@ class RequiredInputError(ValueError): those are ``Undefined``.""" def __init__(self, inputspec, name): classname = inputspec.__class__.__name__ - if classname.endswith('InputSpec'): + if classname.endswith('InputSpec') and classname != 'InputSpec': classname = classname[:-len('InputSpec')] requires = inputspec.traits()[name].requires @@ -58,3 +58,27 @@ def __init__(self, inputspec, name): classname=classname, name=name, requires=', '.join(requires)) super(RequiredInputError, self).__init__(msg) + +class VersionIOError(ValueError): + """Raised when one input with the ``mandatory`` metadata set to ``True`` is + not defined.""" + def __init__(self, spec, name, version): + classname = spec.__class__.__name__ + if classname.endswith('InputSpec') and classname != 'InputSpec': + classname = classname[:len('InputSpec')] + if classname.endswith('OutputSpec') and classname != 'OutputSpec': + classname = classname[:len('OutputSpec')] + + max_ver = spec.traits()[name].max_ver + min_ver = spec.traits()[name].min_ver + + msg = ('Interface "{classname}" has version requirements for ' + '{name}, but version {version} was found. ').format( + classname=classname, name=name, version=version) + + if min_ver: + msg += 'Minimum version is %s. ' % min_ver + if max_ver: + msg += 'Maximum version is %s. ' % max_ver + + super(VersionIOError, self).__init__(msg) From 94c08234de3dec4309bc1614d653e9e79602a4b5 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 01:38:04 -0800 Subject: [PATCH 07/20] fix multiple xor check --- nipype/interfaces/base/specs.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index c6e860ff51..4da090c8c5 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -397,7 +397,9 @@ def check_xor(inputs, xor): if len(xor) == 1: return True - values = [isdefined(getattr(inputs, field)) for field in xor] + values = [isdefined(getattr(inputs, xor[0]))] + values += [any([isdefined(getattr(inputs, field)) + for field in xor[1:]])] return sum(values) def check_mandatory_inputs(inputs, raise_exc=True): From fddec6968caddb227627ee686b4ab992a8d71e28 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 01:55:42 -0800 Subject: [PATCH 08/20] fix check_requires for non-mandatory requires --- nipype/interfaces/base/specs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 4da090c8c5..2b130463f0 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -386,9 +386,8 @@ def check_requires(inputs, requires, value): return True # Check value and all required inputs' values defined - values = [isdefined(value)] + [ - isdefined(getattr(inputs, field)) - for field in requires] + values = [isdefined(getattr(inputs, field)) + for field in requires] return all(values) def check_xor(inputs, xor): @@ -432,7 +431,8 @@ def check_mandatory_inputs(inputs, raise_exc=True): # Check requirements of non-mandatory inputs for name, spec in list( inputs.traits(mandatory=None, transient=None).items()): - if not check_requires(inputs, spec.requires, getattr(inputs, name)): + value = getattr(inputs, name) # value must be set to follow requires + if isdefined(value) and not check_requires(inputs, spec.requires, value): if raise_exc: raise RequiredInputError(inputs, name) From 34cb9c9e79e15f73da9399dba6cf3a9e94430232 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 02:39:44 -0800 Subject: [PATCH 09/20] fix massaging xored inputs --- nipype/interfaces/base/specs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 2b130463f0..f65ffd4008 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -408,7 +408,10 @@ def check_mandatory_inputs(inputs, raise_exc=True): for name, spec in list(inputs.traits(mandatory=True).items()): value = getattr(inputs, name) # Mandatory field is defined, check xor'ed inputs - xor = list(set([name] + (spec.xor or []))) + xor = spec.xor or [] + xor = list(xor) if isinstance(xor, (list, tuple)) \ + else [xor] + xor = list(set([name] + xor)) cxor = check_xor(inputs, xor) if cxor != 1: if raise_exc: From 41a48bdd946adea929eaeb60897c3bfc3257c7c7 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 08:10:10 -0800 Subject: [PATCH 10/20] fix test_extra_Registration --- .../ants/tests/test_extra_Registration.py | 7 ++-- nipype/utils/errors.py | 33 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/nipype/interfaces/ants/tests/test_extra_Registration.py b/nipype/interfaces/ants/tests/test_extra_Registration.py index 745b825c65..c65bb445be 100644 --- a/nipype/interfaces/ants/tests/test_extra_Registration.py +++ b/nipype/interfaces/ants/tests/test_extra_Registration.py @@ -1,9 +1,10 @@ # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: from __future__ import unicode_literals -from nipype.interfaces.ants import registration import os import pytest +from nipype.interfaces.ants import registration +from nipype.utils.errors import MandatoryInputError def test_ants_mand(tmpdir): @@ -17,6 +18,6 @@ def test_ants_mand(tmpdir): ants.inputs.fixed_image = [os.path.join(datadir, 'T1.nii')] ants.inputs.metric = ['MI'] - with pytest.raises(ValueError) as er: + with pytest.raises(MandatoryInputError) as er: ants.run() - assert "ANTS requires a value for input 'radius'" in str(er.value) + assert 'Interface "ANTS" requires a value for input radius.' in str(er.value) diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py index c1ceb38b5c..18c4a25116 100644 --- a/nipype/utils/errors.py +++ b/nipype/utils/errors.py @@ -11,9 +11,7 @@ class MandatoryInputError(ValueError): """Raised when one input with the ``mandatory`` metadata set to ``True`` is not defined.""" def __init__(self, inputspec, name): - classname = inputspec.__class__.__name__ - if classname.endswith('InputSpec') and classname != 'InputSpec': - classname = classname[:len('InputSpec')] + classname = _classname_from_spec(inputspec) msg = ( 'Interface "{classname}" requires a value for input {name}. ' 'For a list of required inputs, see {classname}.help().').format( @@ -23,9 +21,7 @@ def __init__(self, inputspec, name): class MutuallyExclusiveInputError(ValueError): """Raised when none or more than one mutually-exclusive inputs are set.""" def __init__(self, inputspec, name, values_defined=None, name_other=None): - classname = inputspec.__class__.__name__ - if classname.endswith('InputSpec') and classname != 'InputSpec': - classname = classname[:-len('InputSpec')] + classname = _classname_from_spec(inputspec) if values_defined is not None: xor = list(set([name]+ inputspec.traits()[name].xor)) @@ -47,9 +43,7 @@ class RequiredInputError(ValueError): """Raised when one input requires some other and those or some of those are ``Undefined``.""" def __init__(self, inputspec, name): - classname = inputspec.__class__.__name__ - if classname.endswith('InputSpec') and classname != 'InputSpec': - classname = classname[:-len('InputSpec')] + classname = _classname_from_spec(inputspec) requires = inputspec.traits()[name].requires msg = ('Interface "{classname}" requires a value for input {name} ' @@ -63,12 +57,7 @@ class VersionIOError(ValueError): """Raised when one input with the ``mandatory`` metadata set to ``True`` is not defined.""" def __init__(self, spec, name, version): - classname = spec.__class__.__name__ - if classname.endswith('InputSpec') and classname != 'InputSpec': - classname = classname[:len('InputSpec')] - if classname.endswith('OutputSpec') and classname != 'OutputSpec': - classname = classname[:len('OutputSpec')] - + classname = _classname_from_spec(spec) max_ver = spec.traits()[name].max_ver min_ver = spec.traits()[name].min_ver @@ -82,3 +71,17 @@ def __init__(self, spec, name, version): msg += 'Maximum version is %s. ' % max_ver super(VersionIOError, self).__init__(msg) + +def _classname_from_spec(spec): + classname = spec.__class__.__name__ + + kind = 'Output' if 'Output' in classname else 'Input' + # General pattern is that spec ends in KindSpec + if classname.endswith(kind + 'Spec') and classname != (kind + 'Spec'): + classname = classname[:-len(kind + 'Spec')] + + # Catch some special cases such as ANTS + if classname.endswith(kind) and classname != kind: + classname = classname[:-len(kind)] + + return classname From 8f8ceb178e4115401630983c285342a7c11129e5 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 08:21:47 -0800 Subject: [PATCH 11/20] fixed fsl.utils doctests --- nipype/interfaces/fsl/utils.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nipype/interfaces/fsl/utils.py b/nipype/interfaces/fsl/utils.py index f4ef73c0e9..45d09b53e6 100644 --- a/nipype/interfaces/fsl/utils.py +++ b/nipype/interfaces/fsl/utils.py @@ -238,16 +238,31 @@ class Smooth(FSLCommand): >>> sm.cmdline # doctest: +ELLIPSIS 'fslmaths functional2.nii -kernel gauss 3.397 -fmean functional2_smooth.nii.gz' - One of sigma or fwhm must be set: + One of sigma or fwhm must be set. Printing the ``cmdline`` will issue + a warning: >>> from nipype.interfaces.fsl import Smooth >>> sm = Smooth() >>> sm.inputs.output_type = 'NIFTI_GZ' >>> sm.inputs.in_file = 'functional2.nii' - >>> sm.cmdline #doctest: +ELLIPSIS + >>> sm.cmdline # doctest: +ELLIPSIS + 'fslmaths functional2.nii functional2_smooth.nii.gz' + + The warning is: :: + 181125-08:12:09,489 nipype.interface WARNING: + Some inputs are not valid. Please make sure all mandatory + inputs, required inputs and mutually-exclusive inputs are + set or in a sane state. + + Attempting to run the interface without the necessary inputs will + lead to an error (in this case, a ``MutuallyExclusiveInputError``): + + >>> sm.run() # doctest: +ELLIPSIS Traceback (most recent call last): ... - ValueError: Smooth requires a value for one of the inputs ... + nipype.utils.errors.MutuallyExclusiveInputError: \ + Interface "Smooth" has mutually-exclusive inputs. \ + Exactly one of (fwhm|sigma) should be set... """ From d0c6ab852d1fd566e45ad52080abe84f8df7da78 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 08:52:22 -0800 Subject: [PATCH 12/20] cmdline returns ``None`` instead of raising if error on inputs --- nipype/interfaces/base/core.py | 8 ++- .../freesurfer/tests/test_preprocess.py | 70 ++++++++++++------- nipype/interfaces/fsl/utils.py | 8 +-- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 335c6fecae..77f1991df6 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -726,9 +726,11 @@ def cmdline(self): validates arguments and generates command line""" if not check_mandatory_inputs(self.inputs, raise_exc=False): iflogger.warning( - 'Some inputs are not valid. Please make sure all mandatory ' - 'inputs, required inputs and mutually-exclusive inputs are ' - 'set or in a sane state.') + 'Command line could not be generated because some inputs ' + 'are not valid. Please make sure all mandatory inputs, ' + 'required inputs and mutually-exclusive inputs are set ' + 'or in a sane state.') + return None allargs = [self._cmd_prefix + self.cmd] + self._parse_inputs() return ' '.join(allargs) diff --git a/nipype/interfaces/freesurfer/tests/test_preprocess.py b/nipype/interfaces/freesurfer/tests/test_preprocess.py index f9fc09515a..6853bb7015 100644 --- a/nipype/interfaces/freesurfer/tests/test_preprocess.py +++ b/nipype/interfaces/freesurfer/tests/test_preprocess.py @@ -7,24 +7,26 @@ import pytest from nipype.testing.fixtures import create_files_in_directory -from nipype.interfaces import freesurfer +from nipype.interfaces import freesurfer as fs from nipype.interfaces.freesurfer import Info from nipype import LooseVersion +from nipype.utils import errors as nue @pytest.mark.skipif( - freesurfer.no_freesurfer(), reason="freesurfer is not installed") + fs.no_freesurfer(), reason="freesurfer is not installed") def test_robustregister(create_files_in_directory): filelist, outdir = create_files_in_directory - reg = freesurfer.RobustRegister() + reg = fs.RobustRegister() cwd = os.getcwd() # make sure command gets called assert reg.cmd == 'mri_robust_register' + assert reg.cmdline is None # test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): reg.run() # .inputs based parameters setting @@ -36,7 +38,7 @@ def test_robustregister(create_files_in_directory): (cwd, filelist[0][:-4], filelist[0], filelist[1])) # constructor based parameter setting - reg2 = freesurfer.RobustRegister( + reg2 = fs.RobustRegister( source_file=filelist[0], target_file=filelist[1], outlier_sens=3.0, @@ -49,17 +51,18 @@ def test_robustregister(create_files_in_directory): @pytest.mark.skipif( - freesurfer.no_freesurfer(), reason="freesurfer is not installed") + fs.no_freesurfer(), reason="freesurfer is not installed") def test_fitmsparams(create_files_in_directory): filelist, outdir = create_files_in_directory - fit = freesurfer.FitMSParams() + fit = fs.FitMSParams() # make sure command gets called assert fit.cmd == 'mri_ms_fitparms' + assert fit.cmdline is None # test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): fit.run() # .inputs based parameters setting @@ -69,7 +72,7 @@ def test_fitmsparams(create_files_in_directory): filelist[1], outdir) # constructor based parameter setting - fit2 = freesurfer.FitMSParams( + fit2 = fs.FitMSParams( in_files=filelist, te_list=[1.5, 3.5], flip_list=[20, 30], @@ -80,17 +83,18 @@ def test_fitmsparams(create_files_in_directory): @pytest.mark.skipif( - freesurfer.no_freesurfer(), reason="freesurfer is not installed") + fs.no_freesurfer(), reason="freesurfer is not installed") def test_synthesizeflash(create_files_in_directory): filelist, outdir = create_files_in_directory - syn = freesurfer.SynthesizeFLASH() + syn = fs.SynthesizeFLASH() # make sure command gets called assert syn.cmd == 'mri_synthesize' + assert syn.cmdline is None # test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): syn.run() # .inputs based parameters setting @@ -105,7 +109,7 @@ def test_synthesizeflash(create_files_in_directory): os.path.join(outdir, 'synth-flash_30.mgz'))) # constructor based parameters setting - syn2 = freesurfer.SynthesizeFLASH( + syn2 = fs.SynthesizeFLASH( t1_image=filelist[0], pd_image=filelist[1], flip_angle=20, te=5, tr=25) assert syn2.cmdline == ('mri_synthesize 25.00 20.00 5.000 %s %s %s' % (filelist[0], filelist[1], @@ -113,16 +117,17 @@ def test_synthesizeflash(create_files_in_directory): @pytest.mark.skipif( - freesurfer.no_freesurfer(), reason="freesurfer is not installed") + fs.no_freesurfer(), reason="freesurfer is not installed") def test_mandatory_outvol(create_files_in_directory): filelist, outdir = create_files_in_directory - mni = freesurfer.MNIBiasCorrection() + mni = fs.MNIBiasCorrection() # make sure command gets called assert mni.cmd == "mri_nu_correct.mni" + assert mni.cmdline is None # test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): mni.cmdline # test with minimal args @@ -141,7 +146,7 @@ def test_mandatory_outvol(create_files_in_directory): 'mri_nu_correct.mni --i %s --n 4 --o new_corrected_file.mgz' % (filelist[0])) # constructor based tests - mni2 = freesurfer.MNIBiasCorrection( + mni2 = fs.MNIBiasCorrection( in_file=filelist[0], out_file='bias_corrected_output', iterations=2) assert mni2.cmdline == ( 'mri_nu_correct.mni --i %s --n 2 --o bias_corrected_output' % @@ -149,17 +154,24 @@ def test_mandatory_outvol(create_files_in_directory): @pytest.mark.skipif( - freesurfer.no_freesurfer(), reason="freesurfer is not installed") -def test_bbregister(create_files_in_directory): + fs.no_freesurfer(), reason="freesurfer is not installed") +def test_bbregister(capsys, create_files_in_directory): filelist, outdir = create_files_in_directory - bbr = freesurfer.BBRegister() + bbr = fs.BBRegister() # make sure command gets called assert bbr.cmd == "bbregister" + # cmdline issues a warning in this stats + assert bbr.cmdline is None + captured = capsys.readouterr() + + assert 'WARNING' in captured.out + assert 'Some inputs are not valid.' in captured.out + # test raising error with mandatory args absent - with pytest.raises(ValueError): - bbr.cmdline + with pytest.raises(nue.MandatoryInputError): + bbr.run() bbr.inputs.subject_id = 'fsaverage' bbr.inputs.source_file = filelist[0] @@ -167,10 +179,14 @@ def test_bbregister(create_files_in_directory): # Check that 'init' is mandatory in FS < 6, but not in 6+ if Info.looseversion() < LooseVersion("6.0.0"): - with pytest.raises(ValueError): - bbr.cmdline + assert bbr.cmdline is None + captured = capsys.readouterr() + assert 'Some inputs are not valid.' in captured.out + + with pytest.raises(nue.VersionIOError): + bbr.run() else: - bbr.cmdline + assert bbr.cmdline is not None bbr.inputs.init = 'fsl' @@ -187,5 +203,5 @@ def test_bbregister(create_files_in_directory): def test_FSVersion(): """Check that FSVersion is a string that can be compared with LooseVersion """ - assert isinstance(freesurfer.preprocess.FSVersion, str) - assert LooseVersion(freesurfer.preprocess.FSVersion) >= LooseVersion("0") + assert isinstance(fs.preprocess.FSVersion, str) + assert LooseVersion(fs.preprocess.FSVersion) >= LooseVersion("0") diff --git a/nipype/interfaces/fsl/utils.py b/nipype/interfaces/fsl/utils.py index 45d09b53e6..fac48dcec5 100644 --- a/nipype/interfaces/fsl/utils.py +++ b/nipype/interfaces/fsl/utils.py @@ -238,15 +238,15 @@ class Smooth(FSLCommand): >>> sm.cmdline # doctest: +ELLIPSIS 'fslmaths functional2.nii -kernel gauss 3.397 -fmean functional2_smooth.nii.gz' - One of sigma or fwhm must be set. Printing the ``cmdline`` will issue - a warning: + One of sigma or fwhm must be set. Accessing the ``cmdline`` property + will return ``None`` and issue a warning: >>> from nipype.interfaces.fsl import Smooth >>> sm = Smooth() >>> sm.inputs.output_type = 'NIFTI_GZ' >>> sm.inputs.in_file = 'functional2.nii' - >>> sm.cmdline # doctest: +ELLIPSIS - 'fslmaths functional2.nii functional2_smooth.nii.gz' + >>> sm.cmdline is None # doctest: +ELLIPSIS + True The warning is: :: 181125-08:12:09,489 nipype.interface WARNING: From 1b948cfedf02202b0e2d2b8ffff2c1bd1fd48883 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 11:35:43 -0800 Subject: [PATCH 13/20] fix one more use-case --- nipype/interfaces/base/specs.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 96289f47a0..434cf0e6fa 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -427,6 +427,15 @@ def check_mandatory_inputs(inputs, raise_exc=True): value = getattr(inputs, name) # Mandatory field is defined, check xor'ed inputs xor = spec.xor or [] + has_xor = bool(xor) + has_value = isdefined(value) + + # Simplest case: no xor metadata and not defined + if not has_xor and not has_value: + if raise_exc: + raise MandatoryInputError(inputs, name) + return False + xor = list(xor) if isinstance(xor, (list, tuple)) \ else [xor] xor = list(set([name] + xor)) @@ -437,14 +446,8 @@ def check_mandatory_inputs(inputs, raise_exc=True): inputs, name, values_defined=cxor) return False - # Simplest case: no xor metadata and not defined - if cxor is True and not isdefined(value): - if raise_exc: - raise MandatoryInputError(inputs, name) - return False - # Check whether mandatory inputs require others - if not check_requires(inputs, spec.requires): + if has_value and not check_requires(inputs, spec.requires): if raise_exc: raise RequiredInputError(inputs, name) return False From 1a558a4d0250191afec8f13927810cb863c1f51e Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Sun, 25 Nov 2018 11:48:35 -0800 Subject: [PATCH 14/20] fixed tests --- .../freesurfer/tests/test_preprocess.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/nipype/interfaces/freesurfer/tests/test_preprocess.py b/nipype/interfaces/freesurfer/tests/test_preprocess.py index 6853bb7015..3d908914b8 100644 --- a/nipype/interfaces/freesurfer/tests/test_preprocess.py +++ b/nipype/interfaces/freesurfer/tests/test_preprocess.py @@ -11,6 +11,7 @@ from nipype.interfaces.freesurfer import Info from nipype import LooseVersion from nipype.utils import errors as nue +from nipype.utils.tests.test_cmd import capture_sys_output @pytest.mark.skipif( @@ -128,7 +129,7 @@ def test_mandatory_outvol(create_files_in_directory): assert mni.cmdline is None # test raising error with mandatory args absent with pytest.raises(nue.MandatoryInputError): - mni.cmdline + mni.run() # test with minimal args mni.inputs.in_file = filelist[0] @@ -155,7 +156,7 @@ def test_mandatory_outvol(create_files_in_directory): @pytest.mark.skipif( fs.no_freesurfer(), reason="freesurfer is not installed") -def test_bbregister(capsys, create_files_in_directory): +def test_bbregister(create_files_in_directory): filelist, outdir = create_files_in_directory bbr = fs.BBRegister() @@ -163,11 +164,12 @@ def test_bbregister(capsys, create_files_in_directory): assert bbr.cmd == "bbregister" # cmdline issues a warning in this stats - assert bbr.cmdline is None - captured = capsys.readouterr() + with capture_sys_output() as (stdout, stderr): + assert bbr.cmdline is None - assert 'WARNING' in captured.out - assert 'Some inputs are not valid.' in captured.out + captured = stdout.getvalue() + assert 'WARNING' in captured + assert 'Command line could not be generated' in captured # test raising error with mandatory args absent with pytest.raises(nue.MandatoryInputError): @@ -179,9 +181,10 @@ def test_bbregister(capsys, create_files_in_directory): # Check that 'init' is mandatory in FS < 6, but not in 6+ if Info.looseversion() < LooseVersion("6.0.0"): - assert bbr.cmdline is None - captured = capsys.readouterr() - assert 'Some inputs are not valid.' in captured.out + with capture_sys_output() as (stdout, stderr): + assert bbr.cmdline is None + captured = stdout.getvalue() + assert 'Command line could not be generated' in captured with pytest.raises(nue.VersionIOError): bbr.run() From b08b5ad4acd40d8486c1bbdf62257e959e1e1d93 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 15:42:18 -0800 Subject: [PATCH 15/20] fix errors checking xor --- nipype/interfaces/base/specs.py | 17 +++++++++-------- nipype/interfaces/fsl/utils.py | 4 +--- nipype/utils/errors.py | 7 +++++-- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index 434cf0e6fa..d4c828699d 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -408,15 +408,15 @@ def check_requires(inputs, requires): for field in requires] return all(values) -def check_xor(inputs, xor): +def check_xor(inputs, name, xor): """ check if mutually exclusive inputs are satisfied """ - if len(xor) == 1: + if len(xor) == 0: return True - values = [isdefined(getattr(inputs, xor[0]))] + values = [isdefined(getattr(inputs, name))] values += [any([isdefined(getattr(inputs, field)) - for field in xor[1:]])] + for field in xor])] return sum(values) def check_mandatory_inputs(inputs, raise_exc=True): @@ -436,10 +436,11 @@ def check_mandatory_inputs(inputs, raise_exc=True): raise MandatoryInputError(inputs, name) return False - xor = list(xor) if isinstance(xor, (list, tuple)) \ - else [xor] - xor = list(set([name] + xor)) - cxor = check_xor(inputs, xor) + xor = set(list(xor) if isinstance(xor, (list, tuple)) + else [xor]) + xor.discard(name) + xor = list(xor) + cxor = check_xor(inputs, name, xor) if cxor != 1: if raise_exc: raise MutuallyExclusiveInputError( diff --git a/nipype/interfaces/fsl/utils.py b/nipype/interfaces/fsl/utils.py index fac48dcec5..dcf545525f 100644 --- a/nipype/interfaces/fsl/utils.py +++ b/nipype/interfaces/fsl/utils.py @@ -260,9 +260,7 @@ class Smooth(FSLCommand): >>> sm.run() # doctest: +ELLIPSIS Traceback (most recent call last): ... - nipype.utils.errors.MutuallyExclusiveInputError: \ - Interface "Smooth" has mutually-exclusive inputs. \ - Exactly one of (fwhm|sigma) should be set... + MutuallyExclusiveInputError: Interface ... """ diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py index 18c4a25116..7051eda366 100644 --- a/nipype/utils/errors.py +++ b/nipype/utils/errors.py @@ -25,12 +25,15 @@ def __init__(self, inputspec, name, values_defined=None, name_other=None): if values_defined is not None: xor = list(set([name]+ inputspec.traits()[name].xor)) - msg = ('Interface "{classname}" has mutually-exclusive inputs. ' + msg = ('Interface "{classname}" has mutually-exclusive inputs ' + '(processing "{name}", with value={value}). ' 'Exactly one of ({xor}) should be set, but {n:d} were set. ' 'For a list of mutually-exclusive inputs, see ' '{classname}.help().').format(classname=classname, xor='|'.join(xor), - n=values_defined) + n=values_defined, + name=name, + value=getattr(inputspec, name)) else: msg = ('Interface "{classname}" has mutually-exclusive inputs. ' From 44a724d0c1053948ce1fd4efeefa51ecfe504d6c Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 15:48:57 -0800 Subject: [PATCH 16/20] fix fs.preprocess test --- .../interfaces/freesurfer/tests/test_preprocess.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/nipype/interfaces/freesurfer/tests/test_preprocess.py b/nipype/interfaces/freesurfer/tests/test_preprocess.py index 3d908914b8..01b99eaa2f 100644 --- a/nipype/interfaces/freesurfer/tests/test_preprocess.py +++ b/nipype/interfaces/freesurfer/tests/test_preprocess.py @@ -11,7 +11,6 @@ from nipype.interfaces.freesurfer import Info from nipype import LooseVersion from nipype.utils import errors as nue -from nipype.utils.tests.test_cmd import capture_sys_output @pytest.mark.skipif( @@ -156,7 +155,7 @@ def test_mandatory_outvol(create_files_in_directory): @pytest.mark.skipif( fs.no_freesurfer(), reason="freesurfer is not installed") -def test_bbregister(create_files_in_directory): +def test_bbregister(caplog, create_files_in_directory): filelist, outdir = create_files_in_directory bbr = fs.BBRegister() @@ -164,11 +163,9 @@ def test_bbregister(create_files_in_directory): assert bbr.cmd == "bbregister" # cmdline issues a warning in this stats - with capture_sys_output() as (stdout, stderr): - assert bbr.cmdline is None + assert bbr.cmdline is None - captured = stdout.getvalue() - assert 'WARNING' in captured + captured = caplog.text assert 'Command line could not be generated' in captured # test raising error with mandatory args absent @@ -181,9 +178,8 @@ def test_bbregister(create_files_in_directory): # Check that 'init' is mandatory in FS < 6, but not in 6+ if Info.looseversion() < LooseVersion("6.0.0"): - with capture_sys_output() as (stdout, stderr): - assert bbr.cmdline is None - captured = stdout.getvalue() + assert bbr.cmdline is None + captured = caplog.text assert 'Command line could not be generated' in captured with pytest.raises(nue.VersionIOError): From f7b5650b817da5fdb6493658c84f41cd6a9de800 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 16:01:45 -0800 Subject: [PATCH 17/20] fix fsl.tests.test_preprocess --- .../freesurfer/tests/test_preprocess.py | 2 +- .../interfaces/fsl/tests/test_preprocess.py | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/nipype/interfaces/freesurfer/tests/test_preprocess.py b/nipype/interfaces/freesurfer/tests/test_preprocess.py index 01b99eaa2f..64dd3b8379 100644 --- a/nipype/interfaces/freesurfer/tests/test_preprocess.py +++ b/nipype/interfaces/freesurfer/tests/test_preprocess.py @@ -162,7 +162,7 @@ def test_bbregister(caplog, create_files_in_directory): # make sure command gets called assert bbr.cmd == "bbregister" - # cmdline issues a warning in this stats + # cmdline issues a warning: mandatory inputs missing assert bbr.cmdline is None captured = caplog.text diff --git a/nipype/interfaces/fsl/tests/test_preprocess.py b/nipype/interfaces/fsl/tests/test_preprocess.py index 4b387201cf..578fb745a2 100644 --- a/nipype/interfaces/fsl/tests/test_preprocess.py +++ b/nipype/interfaces/fsl/tests/test_preprocess.py @@ -15,6 +15,7 @@ from nipype.interfaces.fsl import Info from nipype.interfaces.base import File, TraitError, Undefined, isdefined from nipype.interfaces.fsl import no_fsl +from nipype.utils import errors as nue def fsl_name(obj, fname): @@ -39,7 +40,7 @@ def test_bet(setup_infile): assert better.cmd == 'bet' # Test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): better.run() # Test generated outfile name @@ -195,7 +196,7 @@ def setup_flirt(tmpdir): @pytest.mark.skipif(no_fsl(), reason="fsl is not installed") -def test_flirt(setup_flirt): +def test_flirt(caplog, setup_flirt): # setup tmpdir, infile, reffile = setup_flirt @@ -230,12 +231,24 @@ def test_flirt(setup_flirt): flirter = fsl.FLIRT() # infile not specified - with pytest.raises(ValueError): - flirter.cmdline + assert flirter.cmdline is None + captured = caplog.text + assert 'Command line could not be generated' in captured + + # interface should raise error with mandatory inputs unset + with pytest.raises(nue.MandatoryInputError): + flirter.run() + flirter.inputs.in_file = infile # reference not specified - with pytest.raises(ValueError): - flirter.cmdline + assert flirter.cmdline is None + captured = caplog.text + assert 'Command line could not be generated' in captured + + # interface should raise error with reference still unset + with pytest.raises(nue.MandatoryInputError): + flirter.run() + flirter.inputs.reference = reffile # Generate outfile and outmatrix @@ -380,10 +393,10 @@ def test_mcflirt_opt(setup_flirt): def test_mcflirt_noinput(): # Test error is raised when missing required args fnt = fsl.MCFLIRT() - with pytest.raises(ValueError) as excinfo: + with pytest.raises(nue.MandatoryInputError) as excinfo: fnt.run() assert str(excinfo.value).startswith( - "MCFLIRT requires a value for input 'in_file'") + 'Interface "MCFLIRT" requires a value for input in_file.') # test fnirt @@ -441,9 +454,9 @@ def test_fnirt(setup_flirt): iout) assert fnirt.cmdline == cmd - # Test ValueError is raised when missing mandatory args + # Test nue.MandatoryInputError is raised when missing mandatory args fnirt = fsl.FNIRT() - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): fnirt.run() fnirt.inputs.in_file = infile fnirt.inputs.ref_file = reffile From 294850bc9c56668ec00c6696419a530a12195373 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 16:01:45 -0800 Subject: [PATCH 18/20] fix fsl.tests.test_preprocess --- .../freesurfer/tests/test_preprocess.py | 2 +- .../interfaces/fsl/tests/test_preprocess.py | 33 +++++++++++++------ nipype/interfaces/fsl/utils.py | 2 +- nipype/pytest.ini | 2 +- nipype/utils/errors.py | 5 ++- 5 files changed, 30 insertions(+), 14 deletions(-) diff --git a/nipype/interfaces/freesurfer/tests/test_preprocess.py b/nipype/interfaces/freesurfer/tests/test_preprocess.py index 01b99eaa2f..64dd3b8379 100644 --- a/nipype/interfaces/freesurfer/tests/test_preprocess.py +++ b/nipype/interfaces/freesurfer/tests/test_preprocess.py @@ -162,7 +162,7 @@ def test_bbregister(caplog, create_files_in_directory): # make sure command gets called assert bbr.cmd == "bbregister" - # cmdline issues a warning in this stats + # cmdline issues a warning: mandatory inputs missing assert bbr.cmdline is None captured = caplog.text diff --git a/nipype/interfaces/fsl/tests/test_preprocess.py b/nipype/interfaces/fsl/tests/test_preprocess.py index 4b387201cf..578fb745a2 100644 --- a/nipype/interfaces/fsl/tests/test_preprocess.py +++ b/nipype/interfaces/fsl/tests/test_preprocess.py @@ -15,6 +15,7 @@ from nipype.interfaces.fsl import Info from nipype.interfaces.base import File, TraitError, Undefined, isdefined from nipype.interfaces.fsl import no_fsl +from nipype.utils import errors as nue def fsl_name(obj, fname): @@ -39,7 +40,7 @@ def test_bet(setup_infile): assert better.cmd == 'bet' # Test raising error with mandatory args absent - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): better.run() # Test generated outfile name @@ -195,7 +196,7 @@ def setup_flirt(tmpdir): @pytest.mark.skipif(no_fsl(), reason="fsl is not installed") -def test_flirt(setup_flirt): +def test_flirt(caplog, setup_flirt): # setup tmpdir, infile, reffile = setup_flirt @@ -230,12 +231,24 @@ def test_flirt(setup_flirt): flirter = fsl.FLIRT() # infile not specified - with pytest.raises(ValueError): - flirter.cmdline + assert flirter.cmdline is None + captured = caplog.text + assert 'Command line could not be generated' in captured + + # interface should raise error with mandatory inputs unset + with pytest.raises(nue.MandatoryInputError): + flirter.run() + flirter.inputs.in_file = infile # reference not specified - with pytest.raises(ValueError): - flirter.cmdline + assert flirter.cmdline is None + captured = caplog.text + assert 'Command line could not be generated' in captured + + # interface should raise error with reference still unset + with pytest.raises(nue.MandatoryInputError): + flirter.run() + flirter.inputs.reference = reffile # Generate outfile and outmatrix @@ -380,10 +393,10 @@ def test_mcflirt_opt(setup_flirt): def test_mcflirt_noinput(): # Test error is raised when missing required args fnt = fsl.MCFLIRT() - with pytest.raises(ValueError) as excinfo: + with pytest.raises(nue.MandatoryInputError) as excinfo: fnt.run() assert str(excinfo.value).startswith( - "MCFLIRT requires a value for input 'in_file'") + 'Interface "MCFLIRT" requires a value for input in_file.') # test fnirt @@ -441,9 +454,9 @@ def test_fnirt(setup_flirt): iout) assert fnirt.cmdline == cmd - # Test ValueError is raised when missing mandatory args + # Test nue.MandatoryInputError is raised when missing mandatory args fnirt = fsl.FNIRT() - with pytest.raises(ValueError): + with pytest.raises(nue.MandatoryInputError): fnirt.run() fnirt.inputs.in_file = infile fnirt.inputs.ref_file = reffile diff --git a/nipype/interfaces/fsl/utils.py b/nipype/interfaces/fsl/utils.py index dcf545525f..b85101f867 100644 --- a/nipype/interfaces/fsl/utils.py +++ b/nipype/interfaces/fsl/utils.py @@ -259,7 +259,7 @@ class Smooth(FSLCommand): >>> sm.run() # doctest: +ELLIPSIS Traceback (most recent call last): - ... + ... MutuallyExclusiveInputError: Interface ... """ diff --git a/nipype/pytest.ini b/nipype/pytest.ini index 70f12b64aa..5f22555598 100644 --- a/nipype/pytest.ini +++ b/nipype/pytest.ini @@ -1,6 +1,6 @@ [pytest] norecursedirs = .git build dist doc nipype/external tools examples src addopts = --doctest-modules -n auto -doctest_optionflags = ALLOW_UNICODE NORMALIZE_WHITESPACE +doctest_optionflags = ALLOW_UNICODE NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL env = PYTHONHASHSEED=0 diff --git a/nipype/utils/errors.py b/nipype/utils/errors.py index 7051eda366..1fff7404c6 100644 --- a/nipype/utils/errors.py +++ b/nipype/utils/errors.py @@ -24,7 +24,10 @@ def __init__(self, inputspec, name, values_defined=None, name_other=None): classname = _classname_from_spec(inputspec) if values_defined is not None: - xor = list(set([name]+ inputspec.traits()[name].xor)) + xor = inputspec.traits()[name].xor or [] + xor = set(list(xor) if isinstance(xor, (list, tuple)) + else [xor]) + xor.add(name) msg = ('Interface "{classname}" has mutually-exclusive inputs ' '(processing "{name}", with value={value}). ' 'Exactly one of ({xor}) should be set, but {n:d} were set. ' From 89da56a19a2d3d32f4771568fda979e8a9866296 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 19:10:52 -0800 Subject: [PATCH 19/20] install caplog fixture --- nipype/info.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/nipype/info.py b/nipype/info.py index 9e162524dd..c21dfbd8bf 100644 --- a/nipype/info.py +++ b/nipype/info.py @@ -157,7 +157,13 @@ def get_nipype_gitversion(): if sys.version_info <= (3, 4): REQUIRES.append('configparser') -TESTS_REQUIRES = ['pytest-cov', 'codecov', 'pytest-env', 'coverage<5'] +TESTS_REQUIRES = [ + 'pytest-capturelog', + 'pytest-cov', + 'pytest-env', + 'codecov', + 'coverage<5', +] EXTRA_REQUIRES = { 'doc': ['Sphinx>=1.4', 'numpydoc', 'matplotlib', 'pydotplus', 'pydot>=1.2.3'], From fff59eb64a43ef33a96aba257e9bbdbe7bd6ce98 Mon Sep 17 00:00:00 2001 From: oesteban Date: Sun, 25 Nov 2018 20:31:25 -0800 Subject: [PATCH 20/20] pin pytest>=3.4 for reliable caplog fixture --- nipype/info.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nipype/info.py b/nipype/info.py index c21dfbd8bf..8d41b3c88a 100644 --- a/nipype/info.py +++ b/nipype/info.py @@ -107,7 +107,7 @@ def get_nipype_gitversion(): SCIPY_MIN_VERSION = '0.14' TRAITS_MIN_VERSION = '4.6' DATEUTIL_MIN_VERSION = '2.2' -PYTEST_MIN_VERSION = '3.0' +PYTEST_MIN_VERSION = '3.4' FUTURE_MIN_VERSION = '0.16.0' SIMPLEJSON_MIN_VERSION = '3.8.0' PROV_VERSION = '1.5.2' @@ -158,7 +158,6 @@ def get_nipype_gitversion(): REQUIRES.append('configparser') TESTS_REQUIRES = [ - 'pytest-capturelog', 'pytest-cov', 'pytest-env', 'codecov',