Skip to content

Commit dc1e3e5

Browse files
committed
Merge pull request #481 from satra/enh/versioning
added versioning to traits and interfaces
2 parents 7cf5b21 + b77f3b4 commit dc1e3e5

File tree

9 files changed

+304
-68
lines changed

9 files changed

+304
-68
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Next release
88
* ENH: New examples: how to use ANTS template building workflows (smri_ants_build_tmeplate),
99
how to set SGE specific options (smri_ants_build_template_new)
1010
* ENH: added no_flatten option to Merge
11+
* ENH: added versioning option and checking to traits
1112
* ENH: added deprecation metadata to traits
1213
* ENH: Slicer interfaces were updated to version 4.1
1314

doc/devel/interface_specs.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,15 @@ Common
263263
should be symlinked, while `True` indicates that the contents should be
264264
copied over.
265265

266-
``deprecated``
266+
``min_ver`` and ``max_ver``
267+
These metadata determine if a particular trait will be available when a
268+
given version of the underlying interface runs. Note that this check is
269+
performed at runtime.::
270+
271+
class RealignInputSpec(BaseInterfaceInputSpec):
272+
jobtype = traits.Enum('estwrite', 'estimate', 'write', min_ver='5',
273+
usedefault=True)
274+
``deprecated`` and ``new_name``
267275
This is metadata for removing or renaming an input field from a spec.::
268276

269277
class RealignInputSpec(BaseInterfaceInputSpec):
@@ -277,8 +285,8 @@ Common
277285
from current release. Raises `TraitError` after package version crosses the
278286
deprecation version.
279287

280-
``new_name``
281-
For inputs that are being renamed, one can specify the new name of the field.::
288+
For inputs that are being renamed, one can specify the new name of the
289+
field.::
282290

283291
class RealignInputSpec(BaseInterfaceInputSpec):
284292
jobtype = traits.Enum('estwrite', 'estimate', 'write',
@@ -293,6 +301,11 @@ Common
293301
When `new_name` is provided it must exist as a trait, otherwise an exception
294302
will be raised.
295303

304+
.. note::
305+
306+
The version information for `min_ver`, `max_ver` and `deprecated` has to be
307+
provided as a string. For example, `min_ver='0.1'`.
308+
296309
CommandLine
297310
^^^^^^^^^^^
298311

doc/users/config_file.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ Execution
109109
data through (without copying) (possible values: ``true`` and
110110
``false``; default value: ``false``)
111111

112+
*stop_on_unknown_version*
113+
If this is set to True, an underlying interface will raise an error, when no
114+
version information is available. Please notify developers or submit a
115+
patch.
116+
112117
Example
113118
~~~~~~~
114119

nipype/interfaces/base.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
has_metadata)
2828
from ..utils.filemanip import (md5, hash_infile, FileNotFoundError,
2929
hash_timestamp)
30-
from ..utils.misc import is_container, trim
30+
from ..utils.misc import is_container, trim, str2bool
3131
from .. import config, logging, LooseVersion
3232
from .. import __version__
3333
from nipype.utils.filemanip import split_filename
@@ -639,6 +639,10 @@ def _get_filecopy_info(self):
639639
"""
640640
raise NotImplementedError
641641

642+
@property
643+
def version(self):
644+
raise NotImplementedError
645+
642646

643647
class BaseInterfaceInputSpec(TraitedSpec):
644648
ignore_exception = traits.Bool(False, desc="Print an error message instead \
@@ -665,6 +669,7 @@ class BaseInterface(Interface):
665669
666670
"""
667671
input_spec = BaseInterfaceInputSpec
672+
_version = None
668673

669674
def __init__(self, **inputs):
670675
if not self.input_spec:
@@ -824,6 +829,36 @@ def _check_mandatory_inputs(self):
824829
transient=None).items():
825830
self._check_requires(spec, name, getattr(self.inputs, name))
826831

832+
def _check_version_requirements(self, trait_object, raise_exception=True):
833+
""" Raises an exception on version mismatch
834+
"""
835+
unavailable_traits = []
836+
version = LooseVersion(str(self.version))
837+
if not version:
838+
return
839+
# check minimum version
840+
names = trait_object.trait_names(**dict(min_ver=lambda t: t is not None))
841+
for name in names:
842+
min_ver = LooseVersion(str(trait_object.traits()[name].min_ver))
843+
if min_ver > version:
844+
unavailable_traits.append(name)
845+
if not isdefined(getattr(trait_object, name)):
846+
continue
847+
if raise_exception:
848+
raise Exception('Trait %s (%s) (version %s < required %s)' %
849+
(name, self.__class__.__name__, version, min_ver))
850+
names = trait_object.trait_names(**dict(max_ver=lambda t: t is not None))
851+
for name in names:
852+
max_ver = LooseVersion(str(trait_object.traits()[name].max_ver))
853+
if max_ver < version:
854+
unavailable_traits.append(name)
855+
if not isdefined(getattr(trait_object, name)):
856+
continue
857+
if raise_exception:
858+
raise Exception('Trait %s (%s) (version %s > required %s)' %
859+
(name, self.__class__.__name__, version, max_ver))
860+
return unavailable_traits
861+
827862
def _run_interface(self, runtime):
828863
""" Core function that executes interface
829864
"""
@@ -846,6 +881,7 @@ def run(self, **inputs):
846881
"""
847882
self.inputs.set(**inputs)
848883
self._check_mandatory_inputs()
884+
self._check_version_requirements(self.inputs)
849885
interface = self.__class__
850886
# initialize provenance tracking
851887
env = deepcopy(os.environ.data)
@@ -905,9 +941,18 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None):
905941
predicted_outputs = self._list_outputs()
906942
outputs = self._outputs()
907943
if predicted_outputs:
944+
_unavailable_outputs = []
945+
if outputs:
946+
_unavailable_outputs = \
947+
self._check_version_requirements(self._outputs())
908948
for key, val in predicted_outputs.items():
909949
if needed_outputs and key not in needed_outputs:
910950
continue
951+
if key in _unavailable_outputs:
952+
raise KeyError(('Output trait %s not available in version '
953+
'%s of interface %s. Please inform '
954+
'developers.') % (key, self.version,
955+
self.__class__.__name__))
911956
try:
912957
setattr(outputs, key, val)
913958
_ = getattr(outputs, key)
@@ -920,6 +965,14 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None):
920965
raise error
921966
return outputs
922967

968+
@property
969+
def version(self):
970+
if self._version is None:
971+
if str2bool(config.get('execution', 'stop_on_unknown_version')):
972+
raise ValueError('Interface %s has no version information' %
973+
self.__class__.__name__)
974+
return self._version
975+
923976

924977
class Stream(object):
925978
"""Function to capture stdout and stderr streams with timestamps
@@ -1063,6 +1116,7 @@ class must be instantiated with a command argument
10631116

10641117
input_spec = CommandLineInputSpec
10651118
_cmd = None
1119+
_version = None
10661120

10671121
def __init__(self, command=None, **inputs):
10681122
super(CommandLine, self).__init__(**inputs)
@@ -1106,6 +1160,32 @@ def help(cls, returnhelp=False):
11061160
else:
11071161
print allhelp
11081162

1163+
def _get_environ(self):
1164+
out_environ = {}
1165+
try:
1166+
display_var = config.get('execution', 'display_variable')
1167+
out_environ = {'DISPLAY': display_var}
1168+
except NoOptionError:
1169+
pass
1170+
iflogger.debug(out_environ)
1171+
if isdefined(self.inputs.environ):
1172+
out_environ.update(self.inputs.environ)
1173+
return out_environ
1174+
1175+
def version_from_command(self, flag='-v'):
1176+
cmdname = self.cmd.split()[0]
1177+
if self._exists_in_path(cmdname):
1178+
env = deepcopy(os.environ.data)
1179+
out_environ = self._get_environ()
1180+
env.update(out_environ)
1181+
proc = subprocess.Popen(' '.join((cmdname, flag)),
1182+
shell=True,
1183+
env=env,
1184+
stdout=subprocess.PIPE,
1185+
stderr=subprocess.PIPE,
1186+
)
1187+
o, e = proc.communicate()
1188+
return o
11091189

11101190
def _run_interface(self, runtime):
11111191
"""Execute command via subprocess
@@ -1122,15 +1202,7 @@ def _run_interface(self, runtime):
11221202
setattr(runtime, 'stdout', None)
11231203
setattr(runtime, 'stderr', None)
11241204
setattr(runtime, 'cmdline', self.cmdline)
1125-
out_environ = {}
1126-
try:
1127-
display_var = config.get('execution', 'display_variable')
1128-
out_environ = {'DISPLAY': display_var}
1129-
except NoOptionError:
1130-
pass
1131-
iflogger.debug(out_environ)
1132-
if isdefined(self.inputs.environ):
1133-
out_environ.update(self.inputs.environ)
1205+
out_environ = self._get_environ()
11341206
runtime.environ.update(out_environ)
11351207
if not self._exists_in_path(self.cmd.split()[0]):
11361208
raise IOError("%s could not be found on host %s" % (self.cmd.split()[0],

nipype/interfaces/freesurfer/base.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,12 @@ def _gen_fname(self, basename, fname=None, cwd=None, suffix='_fs',
146146
fname = fname_presuffix(basename, suffix=suffix,
147147
use_ext=use_ext, newpath=cwd)
148148
return fname
149+
150+
@property
151+
def version(self):
152+
ver = Info.version()
153+
if ver:
154+
if 'dev' in ver:
155+
return ver.rstrip().split('-')[-1] + '.dev'
156+
else:
157+
return ver.rstrip().split('-v')[-1]

nipype/interfaces/fsl/base.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ def output_type(cls):
113113
try:
114114
return os.environ['FSLOUTPUTTYPE']
115115
except KeyError:
116-
warnings.warn('FSL environment variables not set. setting output type to NIFTI')
116+
warnings.warn(('FSL environment variables not set. setting output '
117+
'type to NIFTI'))
117118
return 'NIFTI'
118119

119120
@staticmethod
@@ -234,6 +235,10 @@ def _gen_fname(self, basename, cwd=None, suffix=None, change_ext=True,
234235
use_ext=False, newpath=cwd)
235236
return fname
236237

238+
@property
239+
def version(self):
240+
return Info.version()
241+
237242

238243
def check_fsl():
239244
ver = Info.version()
@@ -248,7 +253,7 @@ def no_fsl():
248253
used with skipif to skip tests that will
249254
fail if FSL is not installed"""
250255

251-
if Info.version() == None:
256+
if Info.version() is None:
252257
return True
253258
else:
254259
return False

0 commit comments

Comments
 (0)