diff --git a/nipype/algorithms/tests/test_rapidart.py b/nipype/algorithms/tests/test_rapidart.py index 9c29648626..ab149da0a6 100644 --- a/nipype/algorithms/tests/test_rapidart.py +++ b/nipype/algorithms/tests/test_rapidart.py @@ -79,12 +79,13 @@ def test_sc_init(): def test_sc_populate_inputs(): sc = ra.StimulusCorrelation() - inputs = Bunch( - realignment_parameters=None, - intensity_values=None, - spm_mat_file=None, - concatenated_design=None) - assert set(sc.inputs.__dict__.keys()) == set(inputs.__dict__.keys()) + expected = [ + 'concatenated_design', + 'intensity_values', + 'realignment_parameters', + 'spm_mat_file', + ] + assert sorted(sc.inputs.get().keys()) == expected def test_sc_output_filenames(): diff --git a/nipype/info.py b/nipype/info.py index d4a5df9cac..c902485c4a 100644 --- a/nipype/info.py +++ b/nipype/info.py @@ -135,26 +135,27 @@ def get_nipype_gitversion(): VERSION = __version__ PROVIDES = ['nipype'] REQUIRES = [ - 'nibabel>=%s' % NIBABEL_MIN_VERSION, - 'networkx>=%s,<=%s ; python_version < "3.0"' % (NETWORKX_MIN_VERSION, NETWORKX_MAX_VERSION_27), + 'click>=%s' % CLICK_MIN_VERSION, + 'configparser; python_version <= "3.4"', + 'funcsigs', + 'future>=%s' % FUTURE_MIN_VERSION, + 'futures; python_version == "2.7"', 'networkx>=%s ; python_version >= "3.0"' % NETWORKX_MIN_VERSION, - 'numpy>=%s,!=%s ; python_version == "2.7"' % (NUMPY_MIN_VERSION, NUMPY_BAD_VERSION_27), + 'networkx>=%s,<=%s ; python_version < "3.0"' % (NETWORKX_MIN_VERSION, NETWORKX_MAX_VERSION_27), + 'neurdflib', + 'nibabel>=%s' % NIBABEL_MIN_VERSION, 'numpy>=%s ; python_version > "3.0" and python_version < "3.7"' % NUMPY_MIN_VERSION, 'numpy>=%s ; python_version >= "3.7"' % NUMPY_MIN_VERSION_37, + 'numpy>=%s,!=%s ; python_version == "2.7"' % (NUMPY_MIN_VERSION, NUMPY_BAD_VERSION_27), + 'packaging', + 'pathlib2; python_version <= "3.4"', + 'prov>=%s' % PROV_VERSION, + 'pydot>=%s' % PYDOT_MIN_VERSION, + 'pydotplus', 'python-dateutil>=%s' % DATEUTIL_MIN_VERSION, 'scipy>=%s' % SCIPY_MIN_VERSION, - 'traits>=%s,!=5.0' % TRAITS_MIN_VERSION, - 'future>=%s' % FUTURE_MIN_VERSION, 'simplejson>=%s' % SIMPLEJSON_MIN_VERSION, - 'prov>=%s' % PROV_VERSION, - 'neurdflib', - 'click>=%s' % CLICK_MIN_VERSION, - 'funcsigs', - 'pydotplus', - 'pydot>=%s' % PYDOT_MIN_VERSION, - 'packaging', - 'futures; python_version == "2.7"', - 'configparser; python_version <= "3.4"', + 'traits>=%s,!=5.0' % TRAITS_MIN_VERSION, ] TESTS_REQUIRES = [ @@ -170,7 +171,7 @@ def get_nipype_gitversion(): 'doc': ['Sphinx>=1.4', 'numpydoc', 'matplotlib', 'pydotplus', 'pydot>=1.2.3'], 'tests': TESTS_REQUIRES, 'specs': ['yapf'], - 'nipy': ['nitime', 'nilearn<0.5.0', 'dipy', 'nipy', 'matplotlib'], + 'nipy': ['nitime', 'nilearn<0.5.0', 'dipy!=0.15,>=0.14', 'nipy', 'matplotlib'], 'profiler': ['psutil>=5.0'], 'duecredit': ['duecredit'], 'xvfbwrapper': ['xvfbwrapper'], diff --git a/nipype/interfaces/afni/base.py b/nipype/interfaces/afni/base.py index d4b8e474ff..1851b42654 100644 --- a/nipype/interfaces/afni/base.py +++ b/nipype/interfaces/afni/base.py @@ -115,12 +115,6 @@ class AFNICommandInputSpec(CommandLineInputSpec): 1, usedefault=True, nohash=True, desc='set number of threads') outputtype = traits.Enum( 'AFNI', list(Info.ftypes.keys()), desc='AFNI output filetype') - out_file = File( - name_template="%s_afni", - desc='output image file name', - argstr='-prefix %s', - name_source=["in_file"]) - class AFNICommandOutputSpec(TraitedSpec): out_file = File(desc='output file', exists=True) diff --git a/nipype/interfaces/afni/preprocess.py b/nipype/interfaces/afni/preprocess.py index c2ccb1988a..75f0cedcc7 100644 --- a/nipype/interfaces/afni/preprocess.py +++ b/nipype/interfaces/afni/preprocess.py @@ -13,7 +13,7 @@ from ...utils.filemanip import (load_json, save_json, split_filename, fname_presuffix) from ..base import (CommandLineInputSpec, CommandLine, TraitedSpec, traits, - isdefined, File, InputMultiPath, Undefined, Str, + isdefined, File, Directory, InputMultiPath, Undefined, Str, InputMultiObject) from .base import (AFNICommandBase, AFNICommand, AFNICommandInputSpec, @@ -757,8 +757,7 @@ class BandpassInputSpec(AFNICommandInputSpec): desc='output file from 3dBandpass', argstr='-prefix %s', position=1, - name_source='in_file', - genfile=True) + name_source='in_file') lowpass = traits.Float( desc='lowpass', argstr='%f', position=-2, mandatory=True) highpass = traits.Float( @@ -909,6 +908,11 @@ class BlurToFWHMInputSpec(AFNICommandInputSpec): argstr='-input %s', mandatory=True, exists=True) + out_file = File( + name_template='%s_afni', + desc='output image file name', + argstr='-prefix %s', + name_source='in_file') automask = traits.Bool( desc='Create an automask from the input dataset.', argstr='-automask') fwhm = traits.Float( @@ -1042,6 +1046,11 @@ class DegreeCentralityInputSpec(CentralityInputSpec): mandatory=True, exists=True, copyfile=False) + out_file = File( + name_template='%s_afni', + desc='output image file name', + argstr='-prefix %s', + name_source='in_file') sparsity = traits.Float( desc='only take the top percent of connections', argstr='-sparsity %f') oned_file = Str( @@ -1188,6 +1197,11 @@ class ECMInputSpec(CentralityInputSpec): mandatory=True, exists=True, copyfile=False) + out_file = File( + name_template='%s_afni', + desc='output image file name', + argstr='-prefix %s', + name_source='in_file') sparsity = traits.Float( desc='only take the top percent of connections', argstr='-sparsity %f') full = traits.Bool( @@ -1453,6 +1467,11 @@ class LFCDInputSpec(CentralityInputSpec): mandatory=True, exists=True, copyfile=False) + out_file = File( + name_template='%s_afni', + desc='output image file name', + argstr='-prefix %s', + name_source='in_file') class LFCD(AFNICommand): @@ -3072,15 +3091,13 @@ def _list_outputs(self): class QwarpInputSpec(AFNICommandInputSpec): in_file = File( - desc= - 'Source image (opposite phase encoding direction than base image).', + desc='Source image (opposite phase encoding direction than base image).', argstr='-source %s', mandatory=True, exists=True, copyfile=False) base_file = File( - desc= - 'Base image (opposite phase encoding direction than source image).', + desc='Base image (opposite phase encoding direction than source image).', argstr='-base %s', mandatory=True, exists=True, @@ -3088,42 +3105,42 @@ class QwarpInputSpec(AFNICommandInputSpec): out_file = File( argstr='-prefix %s', name_template='%s_QW', - name_source=['in_file'], - genfile=True, - desc='out_file ppp' - 'Sets the prefix for the output datasets.' - '* The source dataset is warped to match the base' - 'and gets prefix \'ppp\'. (Except if \'-plusminus\' is used.)' - '* The final interpolation to this output dataset is' - 'done using the \'wsinc5\' method. See the output of' - ' 3dAllineate -HELP' - '(in the "Modifying \'-final wsinc5\'" section) for' - 'the lengthy technical details.' - '* The 3D warp used is saved in a dataset with' - 'prefix \'ppp_WARP\' -- this dataset can be used' - 'with 3dNwarpApply and 3dNwarpCat, for example.' - '* To be clear, this is the warp from source dataset' - ' coordinates to base dataset coordinates, where the' - ' values at each base grid point are the xyz displacments' - ' needed to move that grid point\'s xyz values to the' - ' corresponding xyz values in the source dataset:' - ' base( (x,y,z) + WARP(x,y,z) ) matches source(x,y,z)' - ' Another way to think of this warp is that it \'pulls\'' - ' values back from source space to base space.' - '* 3dNwarpApply would use \'ppp_WARP\' to transform datasets' - 'aligned with the source dataset to be aligned with the' - 'base dataset.' - '** If you do NOT want this warp saved, use the option \'-nowarp\'.' - '-->> (However, this warp is usually the most valuable possible output!)' - '* If you want to calculate and save the inverse 3D warp,' - 'use the option \'-iwarp\'. This inverse warp will then be' - 'saved in a dataset with prefix \'ppp_WARPINV\'.' - '* This inverse warp could be used to transform data from base' - 'space to source space, if you need to do such an operation.' - '* You can easily compute the inverse later, say by a command like' - ' 3dNwarpCat -prefix Z_WARPINV \'INV(Z_WARP+tlrc)\'' - 'or the inverse can be computed as needed in 3dNwarpApply, like' - ' 3dNwarpApply -nwarp \'INV(Z_WARP+tlrc)\' -source Dataset.nii ...') + name_source='in_file', + desc="""\ +out_file ppp\ +Sets the prefix for the output datasets.\ +* The source dataset is warped to match the base\ +and gets prefix \'ppp\'. (Except if \'-plusminus\' is used.)\ +* The final interpolation to this output dataset is\ +done using the \'wsinc5\' method. See the output of\ + 3dAllineate -HELP\ +(in the "Modifying \'-final wsinc5\'" section) for\ +the lengthy technical details.\ +* The 3D warp used is saved in a dataset with\ +prefix \'ppp_WARP\' -- this dataset can be used\ +with 3dNwarpApply and 3dNwarpCat, for example.\ +* To be clear, this is the warp from source dataset\ + coordinates to base dataset coordinates, where the\ + values at each base grid point are the xyz displacments\ + needed to move that grid point\'s xyz values to the\ + corresponding xyz values in the source dataset:\ + base( (x,y,z) + WARP(x,y,z) ) matches source(x,y,z)\ + Another way to think of this warp is that it \'pulls\'\ + values back from source space to base space.\ +* 3dNwarpApply would use \'ppp_WARP\' to transform datasets\ +aligned with the source dataset to be aligned with the\ +base dataset.\ +** If you do NOT want this warp saved, use the option \'-nowarp\'.\ +-->> (However, this warp is usually the most valuable possible output!)\ +* If you want to calculate and save the inverse 3D warp,\ +use the option \'-iwarp\'. This inverse warp will then be\ +saved in a dataset with prefix \'ppp_WARPINV\'.\ +* This inverse warp could be used to transform data from base\ +space to source space, if you need to do such an operation.\ +* You can easily compute the inverse later, say by a command like\ + 3dNwarpCat -prefix Z_WARPINV \'INV(Z_WARP+tlrc)\'\ +or the inverse can be computed as needed in 3dNwarpApply, like\ + 3dNwarpApply -nwarp \'INV(Z_WARP+tlrc)\' -source Dataset.nii ...""") resample = traits.Bool( desc='This option simply resamples the source dataset to match the' 'base dataset grid. You can use this if the two datasets' diff --git a/nipype/interfaces/afni/tests/test_auto_AFNICommand.py b/nipype/interfaces/afni/tests/test_auto_AFNICommand.py index 724c98dcb2..6d051d49b7 100644 --- a/nipype/interfaces/afni/tests/test_auto_AFNICommand.py +++ b/nipype/interfaces/afni/tests/test_auto_AFNICommand.py @@ -14,11 +14,6 @@ def test_AFNICommand_inputs(): nohash=True, usedefault=True, ), - out_file=dict( - argstr='-prefix %s', - name_source=['in_file'], - name_template='%s_afni', - ), outputtype=dict(), ) inputs = AFNICommand.input_spec() diff --git a/nipype/interfaces/afni/tests/test_auto_AFNIPythonCommand.py b/nipype/interfaces/afni/tests/test_auto_AFNIPythonCommand.py index ba2411edfb..a3903c652b 100644 --- a/nipype/interfaces/afni/tests/test_auto_AFNIPythonCommand.py +++ b/nipype/interfaces/afni/tests/test_auto_AFNIPythonCommand.py @@ -14,11 +14,6 @@ def test_AFNIPythonCommand_inputs(): nohash=True, usedefault=True, ), - out_file=dict( - argstr='-prefix %s', - name_source=['in_file'], - name_template='%s_afni', - ), outputtype=dict(), ) inputs = AFNIPythonCommand.input_spec() diff --git a/nipype/interfaces/afni/tests/test_auto_Bandpass.py b/nipype/interfaces/afni/tests/test_auto_Bandpass.py index fb0861a747..d225136e94 100644 --- a/nipype/interfaces/afni/tests/test_auto_Bandpass.py +++ b/nipype/interfaces/afni/tests/test_auto_Bandpass.py @@ -46,7 +46,6 @@ def test_Bandpass_inputs(): orthogonalize_file=dict(argstr='-ort %s', ), out_file=dict( argstr='-prefix %s', - genfile=True, name_source='in_file', name_template='%s_bp', position=1, diff --git a/nipype/interfaces/afni/tests/test_auto_BlurToFWHM.py b/nipype/interfaces/afni/tests/test_auto_BlurToFWHM.py index 2f88a1edcb..4fd8bef47e 100644 --- a/nipype/interfaces/afni/tests/test_auto_BlurToFWHM.py +++ b/nipype/interfaces/afni/tests/test_auto_BlurToFWHM.py @@ -25,7 +25,7 @@ def test_BlurToFWHM_inputs(): ), out_file=dict( argstr='-prefix %s', - name_source=['in_file'], + name_source='in_file', name_template='%s_afni', ), outputtype=dict(), diff --git a/nipype/interfaces/afni/tests/test_auto_DegreeCentrality.py b/nipype/interfaces/afni/tests/test_auto_DegreeCentrality.py index 664cca5985..056712dd38 100644 --- a/nipype/interfaces/afni/tests/test_auto_DegreeCentrality.py +++ b/nipype/interfaces/afni/tests/test_auto_DegreeCentrality.py @@ -26,7 +26,7 @@ def test_DegreeCentrality_inputs(): oned_file=dict(argstr='-out1D %s', ), out_file=dict( argstr='-prefix %s', - name_source=['in_file'], + name_source='in_file', name_template='%s_afni', ), outputtype=dict(), diff --git a/nipype/interfaces/afni/tests/test_auto_ECM.py b/nipype/interfaces/afni/tests/test_auto_ECM.py index 8a4793fb7f..0b59b2aaf5 100644 --- a/nipype/interfaces/afni/tests/test_auto_ECM.py +++ b/nipype/interfaces/afni/tests/test_auto_ECM.py @@ -30,7 +30,7 @@ def test_ECM_inputs(): ), out_file=dict( argstr='-prefix %s', - name_source=['in_file'], + name_source='in_file', name_template='%s_afni', ), outputtype=dict(), diff --git a/nipype/interfaces/afni/tests/test_auto_LFCD.py b/nipype/interfaces/afni/tests/test_auto_LFCD.py index 9cbde10b56..9be4450e57 100644 --- a/nipype/interfaces/afni/tests/test_auto_LFCD.py +++ b/nipype/interfaces/afni/tests/test_auto_LFCD.py @@ -25,7 +25,7 @@ def test_LFCD_inputs(): ), out_file=dict( argstr='-prefix %s', - name_source=['in_file'], + name_source='in_file', name_template='%s_afni', ), outputtype=dict(), diff --git a/nipype/interfaces/afni/tests/test_auto_Qwarp.py b/nipype/interfaces/afni/tests/test_auto_Qwarp.py index f6df3d0ab5..1a2a1ee216 100644 --- a/nipype/interfaces/afni/tests/test_auto_Qwarp.py +++ b/nipype/interfaces/afni/tests/test_auto_Qwarp.py @@ -117,8 +117,7 @@ def test_Qwarp_inputs(): ), out_file=dict( argstr='-prefix %s', - genfile=True, - name_source=['in_file'], + name_source='in_file', name_template='%s_QW', ), out_weight_file=dict(argstr='-wtprefix %s', ), diff --git a/nipype/interfaces/afni/tests/test_auto_TCorrMap.py b/nipype/interfaces/afni/tests/test_auto_TCorrMap.py index 32778fcf11..823762fdad 100644 --- a/nipype/interfaces/afni/tests/test_auto_TCorrMap.py +++ b/nipype/interfaces/afni/tests/test_auto_TCorrMap.py @@ -62,11 +62,6 @@ def test_TCorrMap_inputs(): nohash=True, usedefault=True, ), - out_file=dict( - argstr='-prefix %s', - name_source=['in_file'], - name_template='%s_afni', - ), outputtype=dict(), pmean=dict( argstr='-Pmean %s', diff --git a/nipype/interfaces/base/specs.py b/nipype/interfaces/base/specs.py index dbbc816dc9..64eb6397e5 100644 --- a/nipype/interfaces/base/specs.py +++ b/nipype/interfaces/base/specs.py @@ -32,6 +32,14 @@ from ... import config, __version__ + +USING_PATHLIB2 = False +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path # noqa + USING_PATHLIB2 = True + FLOAT_FORMAT = '{:.10f}'.format nipype_version = Version(__version__) @@ -314,6 +322,39 @@ def __all__(self): return self.copyable_trait_names() +def _deepcopypatch(self, memo): + """ + Replace the ``__deepcopy__`` member with a traits-friendly implementation. + + A bug in ``__deepcopy__`` for ``HasTraits`` results in weird cloning behaviors. + Occurs for all specs in Python<3 and only for DynamicTraitedSpec in Python>2. + + """ + id_self = id(self) + if id_self in memo: + return memo[id_self] + dup_dict = deepcopy(self.trait_get(), memo) + # access all keys + for key in self.copyable_trait_names(): + if key in self.__dict__.keys(): + _ = getattr(self, key) + # clone once + dup = self.clone_traits(memo=memo) + for key in self.copyable_trait_names(): + try: + _ = getattr(dup, key) + except: + pass + # clone twice + dup = self.clone_traits(memo=memo) + dup.trait_set(**dup_dict) + return dup + + +if USING_PATHLIB2: + BaseTraitedSpec.__deepcopy__ = _deepcopypatch + + class TraitedSpec(BaseTraitedSpec): """ Create a subclass with strict traits. @@ -333,29 +374,9 @@ class DynamicTraitedSpec(BaseTraitedSpec): functioning well together. """ - def __deepcopy__(self, memo): - """ bug in deepcopy for HasTraits results in weird cloning behavior for - added traits - """ - id_self = id(self) - if id_self in memo: - return memo[id_self] - dup_dict = deepcopy(self.trait_get(), memo) - # access all keys - for key in self.copyable_trait_names(): - if key in self.__dict__.keys(): - _ = getattr(self, key) - # clone once - dup = self.clone_traits(memo=memo) - for key in self.copyable_trait_names(): - try: - _ = getattr(dup, key) - except: - pass - # clone twice - dup = self.clone_traits(memo=memo) - dup.trait_set(**dup_dict) - return dup + +if not USING_PATHLIB2: + DynamicTraitedSpec.__deepcopy__ = _deepcopypatch class CommandLineInputSpec(BaseInterfaceInputSpec): diff --git a/nipype/interfaces/base/support.py b/nipype/interfaces/base/support.py index de9d46f61a..0fd1d27674 100644 --- a/nipype/interfaces/base/support.py +++ b/nipype/interfaces/base/support.py @@ -278,7 +278,7 @@ def _inputs_help(cls): >>> from nipype.interfaces.afni import GCOR >>> _inputs_help(GCOR) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE - ['Inputs::', '', '\t[Mandatory]', '\tin_file: (an existing file name)', ... + ['Inputs::', '', '\t[Mandatory]', '\tin_file: (a pathlike object or string... """ helpstr = ['Inputs::'] diff --git a/nipype/interfaces/base/tests/test_specs.py b/nipype/interfaces/base/tests/test_specs.py index bab112e96d..b27daea6a8 100644 --- a/nipype/interfaces/base/tests/test_specs.py +++ b/nipype/interfaces/base/tests/test_specs.py @@ -2,9 +2,9 @@ # 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 +from future import standard_library import pytest @@ -420,7 +420,8 @@ def test_ImageFile(): # setup traits x.add_trait('nifti', nib.ImageFile(types=['nifti1', 'dicom'])) x.add_trait('anytype', nib.ImageFile()) - x.add_trait('newtype', nib.ImageFile(types=['nifti10'])) + with pytest.raises(ValueError): + x.add_trait('newtype', nib.ImageFile(types=['nifti10'])) x.add_trait('nocompress', nib.ImageFile(types=['mgh'], allow_compressed=False)) @@ -428,10 +429,8 @@ def test_ImageFile(): x.nifti = 'test.mgz' x.nifti = 'test.nii' x.anytype = 'test.xml' - with pytest.raises(AttributeError): - x.newtype = 'test.nii' with pytest.raises(nib.TraitError): - x.nocompress = 'test.nii.gz' + x.nocompress = 'test.mgz' x.nocompress = 'test.mgh' diff --git a/nipype/interfaces/base/traits_extension.py b/nipype/interfaces/base/traits_extension.py index 7a464cc557..22ffc7f231 100644 --- a/nipype/interfaces/base/traits_extension.py +++ b/nipype/interfaces/base/traits_extension.py @@ -1,4 +1,3 @@ -# -*- 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: """ @@ -24,28 +23,73 @@ absolute_import) from builtins import str, bytes -import os from collections import Sequence # perform all external trait imports here from traits import __version__ as traits_version import traits.api as traits -from traits.trait_handlers import TraitDictObject, TraitListObject +from traits.trait_handlers import TraitType, NoDefaultSpecified, TraitDictObject, TraitListObject from traits.trait_errors import TraitError -from traits.trait_base import _Undefined, class_of +from traits.trait_base import _Undefined -from traits.api import BaseUnicode from traits.api import Unicode from future import standard_library +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + + if traits_version < '3.7.0': raise ImportError('Traits version 3.7.0 or higher must be installed') standard_library.install_aliases() +IMG_FORMATS = { + 'afni': ('.HEAD', '.BRIK'), + 'cifti2': ('.nii', '.nii.gz'), + 'dicom': ('.dcm', '.IMA', '.tar', '.tar.gz'), + 'gifti': ('.gii', '.gii.gz'), + 'mgh': ('.mgh', '.mgz', '.mgh.gz'), + 'nifti1': ('.nii', '.nii.gz', '.hdr', '.img', '.img.gz'), + 'nifti2': ('.nii', '.nii.gz'), + 'nrrd': ('.nrrd', '.nhdr'), +} +IMG_ZIP_FMT = set(['.nii.gz', 'tar.gz', '.gii.gz', '.mgz', '.mgh.gz', 'img.gz']) + +""" +The functions that pop-up the Traits GUIs, edit_traits and +configure_traits, were failing because all of our inputs default to +Undefined deep and down in traits/ui/wx/list_editor.py it checks for +the len() of the elements of the list. The _Undefined class in traits +does not define the __len__ method and would error. I tried defining +our own Undefined and even sublassing Undefined, but both of those +failed with a TraitError in our initializer when we assign the +Undefined to the inputs because of an incompatible type: + +TraitError: The 'vertical_gradient' trait of a BetInputSpec instance must be \ +a float, but a value of was specified. + +So... in order to keep the same type but add the missing method, I +monkey patched. +""" + + +def _length(self): + return 0 + + +########################################################################## +# Apply monkeypatch here +_Undefined.__len__ = _length +########################################################################## + +Undefined = _Undefined() + class Str(Unicode): - """Replacement for the default traits.Str based in bytes""" + """Replaces the default traits.Str based in bytes.""" # Monkeypatch Str and DictStrStr for Python 2 compatibility @@ -54,240 +98,250 @@ class Str(Unicode): traits.DictStrStr = DictStrStr -class File(BaseUnicode): - """ Defines a trait whose value must be the name of a file. - """ +class BasePath(TraitType): + """Defines a trait whose value must be a valid filesystem path.""" # A description of the type of value this trait accepts: - info_text = 'a file name' - - def __init__(self, - value='', - filter=None, - auto_set=False, - entries=0, - exists=False, - **metadata): - """ Creates a File trait. - - Parameters - ---------- - value : string - The default value for the trait - filter : string - A wildcard string to filter filenames in the file dialog box used by - the attribute trait editor. - auto_set : boolean - Indicates whether the file editor updates the trait value after - every key stroke. - exists : boolean - Indicates whether the trait value must be an existing file or - not. - - Default Value - ------------- - *value* or '' - """ - self.filter = filter - self.auto_set = auto_set - self.entries = entries + info_text = 'a pathlike object or string' + exists = False + pathlike = False + resolve = False + _is_file = False + _is_dir = False + + def __init__(self, value=Undefined, + exists=False, pathlike=False, resolve=False, **metadata): + """Create a BasePath trait.""" self.exists = exists + self.resolve = resolve + self.pathlike = pathlike + if any((exists, self._is_file, self._is_dir)): + self.info_text += ' representing a' + if exists: + self.info_text += 'n existing' + if self._is_file: + self.info_text += ' file' + elif self._is_dir: + self.info_text += ' directory' + else: + self.info_text += ' file or directory' - if exists: - self.info_text = 'an existing file name' + super(BasePath, self).__init__(value, **metadata) - super(File, self).__init__(value, **metadata) + def validate(self, object, name, value, return_pathlike=False): + """Validate a value change.""" + try: + value = Path('%s' % value) # Use pathlib's validation + except Exception: + self.error(object, name, str(value)) - def validate(self, object, name, value): - """ Validates that a specified value is valid for this trait.""" - validated_value = super(File, self).validate(object, name, value) - if not self.exists: - return validated_value - elif os.path.isfile(value): - return validated_value - else: - raise TraitError( - args='The trait \'{}\' of {} instance is {}, but the path ' - ' \'{}\' does not exist.'.format(name, class_of(object), - self.info_text, value)) + if self.exists: + if self.exists and not value.exists(): + self.error(object, name, str(value)) - self.error(object, name, value) + if self._is_file and not value.is_file(): + self.error(object, name, str(value)) + + if self._is_dir and not value.is_dir(): + self.error(object, name, str(value)) + + if self.resolve: + try: + value = value.resolve(strict=self.exists) + except TypeError: + if self.exists: + value = value.resolve() + elif not value.is_absolute(): + value = Path().resolve() / value + + if not return_pathlike and not self.pathlike: + value = str(value) + + return value + + # def get_value(self, object, name, trait=None): + # value = super(BasePath, self).get_value(object, name) + # if value is Undefined: + # return self.default_value + + # if self.pathlike: + # return value + # return str(value) + + +class Directory(BasePath): + """ + Defines a trait whose value must be a directory path. + + Examples:: + + >>> from nipype.interfaces.base import Directory, TraitedSpec, TraitError + >>> class A(TraitedSpec): + ... foo = Directory(exists=False) + >>> a = A() + >>> a.foo + + + >>> a.foo = '/some/made/out/path' + >>> a.foo + '/some/made/out/path' + + >>> class A(TraitedSpec): + ... foo = Directory(exists=False, resolve=True) + >>> a = A(foo='relative_dir') + >>> a.foo # doctest: +ELLIPSIS + '.../relative_dir' + + >>> class A(TraitedSpec): + ... foo = Directory(exists=True, resolve=True) + >>> a = A() + >>> a.foo = 'relative_dir' # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + TraitError: + + >>> from os import mkdir + >>> mkdir('relative_dir') + >>> a.foo = 'relative_dir' + >>> a.foo # doctest: +ELLIPSIS + '.../relative_dir' + + >>> class A(TraitedSpec): + ... foo = Directory(exists=True, resolve=False) + >>> a = A(foo='relative_dir') + >>> a.foo + 'relative_dir' -# ------------------------------------------------------------------------------- -# 'Directory' trait -# ------------------------------------------------------------------------------- + >>> class A(TraitedSpec): + ... foo = Directory('tmpdir') + >>> a = A() + >>> a.foo # doctest: +ELLIPSIS + + + >>> class A(TraitedSpec): + ... foo = Directory('tmpdir', usedefault=True) + >>> a = A() + >>> a.foo # doctest: +ELLIPSIS + 'tmpdir' -class Directory(BaseUnicode): """ - Defines a trait whose value must be the name of a directory. + + _is_dir = True + + +class File(BasePath): """ + Defines a trait whose value must be a file path. - # A description of the type of value this trait accepts: - info_text = 'a directory name' - - def __init__(self, - value='', - auto_set=False, - entries=0, - exists=False, - **metadata): - """ Creates a Directory trait. - - Parameters - ---------- - value : string - The default value for the trait - auto_set : boolean - Indicates whether the directory editor updates the trait value - after every key stroke. - exists : boolean - Indicates whether the trait value must be an existing directory or - not. - - Default Value - ------------- - *value* or '' - """ - self.entries = entries - self.auto_set = auto_set - self.exists = exists + >>> from nipype.interfaces.base import File, TraitedSpec, TraitError + >>> class A(TraitedSpec): + ... foo = File() + >>> a = A() + >>> a.foo + - if exists: - self.info_text = 'an existing directory name' + >>> a.foo = '/some/made/out/path/to/file' + >>> a.foo + '/some/made/out/path/to/file' - super(Directory, self).__init__(value, **metadata) + >>> class A(TraitedSpec): + ... foo = File(exists=False, resolve=True) + >>> a = A(foo='idontexist.txt') + >>> a.foo # doctest: +ELLIPSIS + '.../idontexist.txt' - def validate(self, object, name, value): - """ Validates that a specified value is valid for this trait.""" - if isinstance(value, (str, bytes)): - if not self.exists: - return value - if os.path.isdir(value): - return value - else: - raise TraitError( - args='The trait \'{}\' of {} instance is {}, but the path ' - ' \'{}\' does not exist.'.format(name, class_of(object), - self.info_text, value)) + >>> class A(TraitedSpec): + ... foo = File(exists=True, resolve=True) + >>> a = A() + >>> a.foo = 'idontexist.txt' # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + TraitError: - self.error(object, name, value) + >>> open('idoexist.txt', 'w').close() + >>> a.foo = 'idoexist.txt' + >>> a.foo # doctest: +ELLIPSIS + '.../idoexist.txt' + >>> class A(TraitedSpec): + ... foo = File('idoexist.txt') + >>> a = A() + >>> a.foo + -# lists of tuples -# each element consists of : -# - uncompressed (tuple[0]) extension -# - compressed (tuple[1]) extension -img_fmt_types = { - 'nifti1': [('.nii', '.nii.gz'), (('.hdr', '.img'), ('.hdr', '.img.gz'))], - 'mgh': [('.mgh', '.mgz'), ('.mgh', '.mgh.gz')], - 'nifti2': [('.nii', '.nii.gz')], - 'cifti2': [('.nii', '.nii.gz')], - 'gifti': [('.gii', '.gii.gz')], - 'dicom': [('.dcm', '.dcm'), ('.IMA', '.IMA'), ('.tar', '.tar.gz')], - 'nrrd': [('.nrrd', 'nrrd'), ('nhdr', 'nhdr')], - 'afni': [('.HEAD', '.HEAD'), ('.BRIK', '.BRIK')] -} + >>> class A(TraitedSpec): + ... foo = File('idoexist.txt', usedefault=True) + >>> a = A() + >>> a.foo + 'idoexist.txt' + >>> class A(TraitedSpec): + ... foo = File(exists=True, resolve=True, extensions=['.txt', 'txt.gz']) + >>> a = A() + >>> a.foo = 'idoexist.badtxt' # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + TraitError: -class ImageFile(File): - """ Defines a trait of specific neuroimaging files """ - - def __init__(self, - value='', - filter=None, - auto_set=False, - entries=0, - exists=False, - types=[], - allow_compressed=True, - **metadata): - """ Trait handles neuroimaging files. - - Parameters - ---------- - types : list - Strings of file format types accepted - compressed : boolean - Indicates whether the file format can compressed - """ - self.types = types - self.allow_compressed = allow_compressed - super(ImageFile, self).__init__(value, filter, auto_set, entries, - exists, **metadata) - - def info(self): - existing = 'n existing' if self.exists else '' - comma = ',' if self.exists and not self.allow_compressed else '' - uncompressed = ' uncompressed' if not self.allow_compressed else '' - with_ext = ' (valid extensions: [{}])'.format( - ', '.join(self.grab_exts())) if self.types else '' - return 'a{existing}{comma}{uncompressed} file{with_ext}'.format( - existing=existing, comma=comma, uncompressed=uncompressed, - with_ext=with_ext) - - def grab_exts(self): - # TODO: file type validation - exts = [] - for fmt in self.types: - if fmt in img_fmt_types: - exts.extend( - sum([[u for u in y[0]] - if isinstance(y[0], tuple) else [y[0]] - for y in img_fmt_types[fmt]], [])) - if self.allow_compressed: - exts.extend( - sum([[u for u in y[-1]] - if isinstance(y[-1], tuple) else [y[-1]] - for y in img_fmt_types[fmt]], [])) - else: - raise AttributeError( - 'Information has not been added for format' - ' type {} yet. Supported formats include: ' - '{}'.format(fmt, ', '.join(img_fmt_types.keys()))) - return list(set(exts)) + >>> a.foo = 'idoexist.txt' + >>> a.foo # doctest: +ELLIPSIS + '.../idoexist.txt' - def validate(self, object, name, value): - """ Validates that a specified value is valid for this trait. - """ - validated_value = super(ImageFile, self).validate(object, name, value) - if validated_value and self.types: - _exts = self.grab_exts() - if not any(validated_value.endswith(x) for x in _exts): - raise TraitError( - args="{} is not included in allowed types: {}".format( - validated_value, ', '.join(_exts))) - return validated_value + """ + _is_file = True + _exts = None -""" -The functions that pop-up the Traits GUIs, edit_traits and -configure_traits, were failing because all of our inputs default to -Undefined deep and down in traits/ui/wx/list_editor.py it checks for -the len() of the elements of the list. The _Undefined class in traits -does not define the __len__ method and would error. I tried defining -our own Undefined and even sublassing Undefined, but both of those -failed with a TraitError in our initializer when we assign the -Undefined to the inputs because of an incompatible type: + def __init__(self, value=NoDefaultSpecified, exists=False, pathlike=False, + resolve=False, allow_compressed=True, extensions=None, **metadata): + """Create a File trait.""" + if extensions is not None: + if isinstance(extensions, (bytes, str)): + extensions = [extensions] -TraitError: The 'vertical_gradient' trait of a BetInputSpec instance must be a float, but a value of was specified. + if allow_compressed is False: + extensions = list(set(extensions) - IMG_ZIP_FMT) -So... in order to keep the same type but add the missing method, I -monkey patched. -""" + self._exts = sorted(set(['.%s' % ext if not ext.startswith('.') else ext + for ext in extensions])) + super(File, self).__init__(value=value, exists=exists, + pathlike=pathlike, resolve=resolve, **metadata) -def length(self): - return 0 + def validate(self, object, name, value): + """Validate a value change.""" + value = super(File, self).validate(object, name, value, return_pathlike=True) + if self._exts: + ext = ''.join(value.suffixes) + if ext not in self._exts: + self.error(object, name, str(value)) + if not self.pathlike: + value = str(value) -########################################################################## -# Apply monkeypatch here -_Undefined.__len__ = length -########################################################################## + return value -Undefined = _Undefined() + +class ImageFile(File): + """Defines a trait whose value must be a known neuroimaging file.""" + + def __init__(self, value=NoDefaultSpecified, exists=False, + pathlike=False, resolve=False, types=None, **metadata): + """Create an ImageFile trait.""" + extensions = None + if types is not None: + if isinstance(types, (bytes, str)): + types = [types] + + if set(types) - set(IMG_FORMATS.keys()): + invalid = set(types) - set(IMG_FORMATS.keys()) + raise ValueError("""\ +Unknown value(s) %s for metadata type of an ImageFile input.\ +""" % ', '.join(['"%s"' % t for t in invalid])) + extensions = [ext for t in types for ext in IMG_FORMATS[t]] + + super(ImageFile, self).__init__( + value=value, exists=exists, extensions=extensions, + pathlike=pathlike, resolve=resolve, **metadata) def isdefined(object): diff --git a/nipype/interfaces/dipy/base.py b/nipype/interfaces/dipy/base.py index 27f26e989a..225d6649a3 100644 --- a/nipype/interfaces/dipy/base.py +++ b/nipype/interfaces/dipy/base.py @@ -6,7 +6,6 @@ import os.path as op import inspect import numpy as np -from ... import logging from ..base import (traits, File, isdefined, LibraryBaseInterface, BaseInterfaceInputSpec, TraitedSpec) @@ -23,19 +22,27 @@ def no_dipy(): - """ Check if dipy is available """ + """Check if dipy is available.""" global HAVE_DIPY return not HAVE_DIPY def dipy_version(): - """ Check dipy version """ + """Return dipy version.""" if no_dipy(): return None return dipy.__version__ +def check_version(minversion): + """Check version is greater or equal to a given version.""" + from packaging.version import Version + if no_dipy(): + return None + return Version(dipy.__version__) >= Version(minversion) + + class DipyBaseInterface(LibraryBaseInterface): """ A base interface for py:mod:`dipy` computations diff --git a/nipype/interfaces/dipy/tests/test_base.py b/nipype/interfaces/dipy/tests/test_base.py index 1d475ac0f7..a6684a65a1 100644 --- a/nipype/interfaces/dipy/tests/test_base.py +++ b/nipype/interfaces/dipy/tests/test_base.py @@ -3,7 +3,7 @@ from ...base import traits, TraitedSpec, BaseInterfaceInputSpec from ..base import (convert_to_traits_type, create_interface_specs, dipy_to_nipype_interface, DipyBaseInterface, no_dipy, - get_dipy_workflows) + get_dipy_workflows, check_version) def test_convert_to_traits_type(): @@ -85,6 +85,7 @@ def test_create_interface_specs(): @pytest.mark.skipif(no_dipy(), reason="DIPY is not installed") +@pytest.mark.skipif(not check_version('0.16'), reason='dipy must be >=0.16') def test_dipy_to_nipype_interface(): from dipy.workflows.workflow import Workflow diff --git a/nipype/interfaces/io.py b/nipype/interfaces/io.py index 60e8b6fafc..4b9afd1ce1 100644 --- a/nipype/interfaces/io.py +++ b/nipype/interfaces/io.py @@ -1024,15 +1024,18 @@ def _list_outputs(self): def s3tolocal(self, s3path, bkt): import boto # path formatting - if not os.path.split(self.inputs.local_directory)[1] == '': - self.inputs.local_directory += '/' - if not os.path.split(self.inputs.bucket_path)[1] == '': - self.inputs.bucket_path += '/' - if self.inputs.template[0] == '/': - self.inputs.template = self.inputs.template[1:] - - localpath = s3path.replace(self.inputs.bucket_path, - self.inputs.local_directory) + local_directory = str(self.inputs.local_directory) + bucket_path = str(self.inputs.bucket_path) + template = str(self.inputs.template) + if not os.path.split(local_directory)[1] == '': + local_directory += '/' + if not os.path.split(bucket_path)[1] == '': + bucket_path += '/' + if template[0] == '/': + template = template[1:] + + localpath = s3path.replace(bucket_path, + local_directory) localdir = os.path.split(localpath)[0] if not os.path.exists(localdir): os.makedirs(localdir) diff --git a/nipype/interfaces/spm/base.py b/nipype/interfaces/spm/base.py index fd93dfc522..bbabbf42ba 100644 --- a/nipype/interfaces/spm/base.py +++ b/nipype/interfaces/spm/base.py @@ -32,6 +32,7 @@ from ..base import (BaseInterface, traits, isdefined, InputMultiPath, BaseInterfaceInputSpec, Directory, Undefined, ImageFile, PackageInfo) +from ..base.traits_extension import NoDefaultSpecified from ..matlab import MatlabCommand from ...external.due import due, Doi, BibTeX @@ -597,30 +598,11 @@ def _make_matlab_command(self, contents, postscript=None): class ImageFileSPM(ImageFile): - """ - Defines an ImageFile trait specific to SPM interfaces. - """ - - def __init__(self, - value='', - filter=None, - auto_set=False, - entries=0, - exists=False, - types=['nifti1', 'nifti2'], - allow_compressed=False, - **metadata): - """ Trait handles neuroimaging files. - - Parameters - ---------- - types : list - Strings of file format types accepted - compressed : boolean - Indicates whether the file format can compressed - """ - self.types = types - self.allow_compressed = allow_compressed - super(ImageFileSPM, - self).__init__(value, filter, auto_set, entries, exists, types, - allow_compressed, **metadata) + """Defines a trait whose value must be a NIfTI file.""" + + def __init__(self, value=NoDefaultSpecified, exists=False, + pathlike=False, resolve=False, **metadata): + """Create an ImageFileSPM trait.""" + super(ImageFileSPM, self).__init__( + value=value, exists=exists, types=['nifti1', 'nifti2'], + pathlike=pathlike, resolve=resolve, **metadata) diff --git a/requirements.txt b/requirements.txt index 0d951f49c0..64fb55cc2a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,3 +14,4 @@ configparser pydotplus pydot>=1.2.3 packaging +pathlib2