diff --git a/nipype/interfaces/base/__init__.py b/nipype/interfaces/base/__init__.py index f617064b2f..2284c1763a 100644 --- a/nipype/interfaces/base/__init__.py +++ b/nipype/interfaces/base/__init__.py @@ -22,5 +22,4 @@ OutputMultiObject, InputMultiObject, OutputMultiPath, InputMultiPath) -from .support import (Bunch, InterfaceResult, load_template, - NipypeInterfaceError) +from .support import (Bunch, InterfaceResult, NipypeInterfaceError) diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 5f6d902f9c..ae002cf17f 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -20,18 +20,17 @@ from copy import deepcopy from datetime import datetime as dt import os -import re import platform import subprocess as sp import shlex import sys -from textwrap import wrap import simplejson as json from dateutil.parser import parse as parseutc +from future import standard_library from ... import config, logging, LooseVersion from ...utils.provenance import write_provenance -from ...utils.misc import trim, str2bool, rgetcwd +from ...utils.misc import str2bool, rgetcwd from ...utils.filemanip import (FileNotFoundError, split_filename, which, get_dependencies) from ...utils.subprocess import run_command @@ -42,9 +41,9 @@ from .specs import (BaseInterfaceInputSpec, CommandLineInputSpec, StdOutCommandLineInputSpec, MpiCommandLineInputSpec, get_filecopy_info) -from .support import (Bunch, InterfaceResult, NipypeInterfaceError) +from .support import (Bunch, InterfaceResult, NipypeInterfaceError, + format_help) -from future import standard_library standard_library.install_aliases() iflogger = logging.getLogger('nipype.interface') @@ -68,38 +67,24 @@ class Interface(object): input_spec = None # A traited input specification output_spec = None # A traited output specification - - # defines if the interface can reuse partial results after interruption - _can_resume = False + _can_resume = False # See property below + _always_run = False # See property below @property def can_resume(self): + """Defines if the interface can reuse partial results after interruption. + Only applies to interfaces being run within a workflow context.""" return self._can_resume - # should the interface be always run even if the inputs were not changed? - _always_run = False - @property def always_run(self): + """Should the interface be always run even if the inputs were not changed? + Only applies to interfaces being run within a workflow context.""" return self._always_run - def __init__(self, **inputs): - """Initialize command with given args and inputs.""" - raise NotImplementedError - - @classmethod - def help(cls): - """ Prints class help""" - raise NotImplementedError - - @classmethod - def _inputs_help(cls): - """ Prints inputs help""" - raise NotImplementedError - - @classmethod - def _outputs_help(cls): - """ Prints outputs help""" + @property + def version(self): + """interfaces should implement a version property""" raise NotImplementedError @classmethod @@ -107,8 +92,17 @@ def _outputs(cls): """ Initializes outputs""" raise NotImplementedError - @property - def version(self): + @classmethod + def help(cls, returnhelp=False): + """ Prints class help """ + allhelp = format_help(cls) + if returnhelp: + return allhelp + print(allhelp) + return None # R1710 + + def __init__(self): + """Subclasses must implement __init__""" raise NotImplementedError def run(self): @@ -190,142 +184,6 @@ def __init__(self, from_file=None, resource_monitor=None, for name, value in list(inputs.items()): setattr(self.inputs, name, value) - @classmethod - def help(cls, returnhelp=False): - """ Prints class help - """ - - if cls.__doc__: - # docstring = cls.__doc__.split('\n') - # docstring = [trim(line, '') for line in docstring] - docstring = trim(cls.__doc__).split('\n') + [''] - else: - docstring = [''] - - allhelp = '\n'.join(docstring + cls._inputs_help( - ) + [''] + cls._outputs_help() + [''] + cls._refs_help() + ['']) - if returnhelp: - return allhelp - else: - print(allhelp) - - @classmethod - def _refs_help(cls): - """ Prints interface references. - """ - if not cls.references_: - return [] - - helpstr = ['References::'] - - for r in cls.references_: - helpstr += ['{}'.format(r['entry'])] - - return helpstr - - @classmethod - def _get_trait_desc(self, inputs, name, spec): - desc = spec.desc - xor = spec.xor - requires = spec.requires - argstr = spec.argstr - - manhelpstr = ['\t%s' % name] - - type_info = spec.full_info(inputs, name, None) - - default = '' - if spec.usedefault: - default = ', nipype default value: %s' % str( - spec.default_value()[1]) - line = "(%s%s)" % (type_info, default) - - manhelpstr = wrap( - line, - 70, - initial_indent=manhelpstr[0] + ': ', - subsequent_indent='\t\t ') - - if desc: - for line in desc.split('\n'): - line = re.sub("\s+", " ", line) - manhelpstr += wrap( - line, 70, initial_indent='\t\t', subsequent_indent='\t\t') - - if argstr: - pos = spec.position - if pos is not None: - manhelpstr += wrap( - 'flag: %s, position: %s' % (argstr, pos), - 70, - initial_indent='\t\t', - subsequent_indent='\t\t') - else: - manhelpstr += wrap( - 'flag: %s' % argstr, - 70, - initial_indent='\t\t', - subsequent_indent='\t\t') - - if xor: - line = '%s' % ', '.join(xor) - manhelpstr += wrap( - line, - 70, - initial_indent='\t\tmutually_exclusive: ', - subsequent_indent='\t\t ') - - if requires: - others = [field for field in requires if field != name] - line = '%s' % ', '.join(others) - manhelpstr += wrap( - line, - 70, - initial_indent='\t\trequires: ', - subsequent_indent='\t\t ') - return manhelpstr - - @classmethod - def _inputs_help(cls): - """ Prints description for input parameters - """ - helpstr = ['Inputs::'] - - inputs = cls.input_spec() - if len(list(inputs.traits(transient=None).items())) == 0: - helpstr += ['', '\tNone'] - return helpstr - - manhelpstr = ['', '\t[Mandatory]'] - mandatory_items = inputs.traits(mandatory=True) - for name, spec in sorted(mandatory_items.items()): - manhelpstr += cls._get_trait_desc(inputs, name, spec) - - opthelpstr = ['', '\t[Optional]'] - for name, spec in sorted(inputs.traits(transient=None).items()): - if name in mandatory_items: - continue - opthelpstr += cls._get_trait_desc(inputs, name, spec) - - if manhelpstr: - helpstr += manhelpstr - if opthelpstr: - helpstr += opthelpstr - return helpstr - - @classmethod - def _outputs_help(cls): - """ Prints description for output parameters - """ - helpstr = ['Outputs::', ''] - if cls.output_spec: - outputs = cls.output_spec() - for name, spec in sorted(outputs.traits(transient=None).items()): - helpstr += cls._get_trait_desc(outputs, name, spec) - if len(helpstr) == 2: - helpstr += ['\tNone'] - return helpstr - def _outputs(self): """ Returns a bunch containing output fields for the class """ @@ -645,7 +503,7 @@ def save_inputs_to_json(self, json_file): A convenient way to save current inputs to a JSON file. """ inputs = self.inputs.get_traitsfree() - iflogger.debug('saving inputs {}', inputs) + iflogger.debug('saving inputs %s', inputs) with open(json_file, 'w' if PY3 else 'wb') as fhandle: json.dump(inputs, fhandle, indent=4, ensure_ascii=False) @@ -777,14 +635,6 @@ def set_default_terminal_output(cls, output_type): raise AttributeError( 'Invalid terminal output_type: %s' % output_type) - @classmethod - def help(cls, returnhelp=False): - allhelp = 'Wraps command **{cmd}**\n\n{help}'.format( - cmd=cls._cmd, help=super(CommandLine, cls).help(returnhelp=True)) - if returnhelp: - return allhelp - print(allhelp) - def __init__(self, command=None, terminal_output=None, **inputs): super(CommandLine, self).__init__(**inputs) self._environ = None @@ -804,6 +654,10 @@ def __init__(self, command=None, terminal_output=None, **inputs): @property def cmd(self): """sets base command, immutable""" + if not self._cmd: + raise NotImplementedError( + 'CommandLineInterface should wrap an executable, but ' + 'none has been set.') return self._cmd @property diff --git a/nipype/interfaces/base/support.py b/nipype/interfaces/base/support.py index 543d4b6c40..de9d46f61a 100644 --- a/nipype/interfaces/base/support.py +++ b/nipype/interfaces/base/support.py @@ -13,12 +13,15 @@ import os from copy import deepcopy +from textwrap import wrap +import re from ... import logging from ...utils.misc import is_container from ...utils.filemanip import md5, to_str, hash_infile iflogger = logging.getLogger('nipype.interface') +HELP_LINEWIDTH = 70 class NipypeInterfaceError(Exception): """Custom error for interfaces""" @@ -235,14 +238,166 @@ def version(self): return self._version -def load_template(name): +def format_help(cls): """ - Deprecated stub for backwards compatibility, - please use nipype.interfaces.fsl.model.load_template + Prints help text of a Nipype interface + + >>> from nipype.interfaces.afni import GCOR + >>> GCOR.help() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + Wraps the executable command ``@compute_gcor``. + + Computes the average correlation between every voxel + and ever other voxel, over any give mask. + + + For complete details, ... """ - from ..fsl.model import load_template - iflogger.warning( - 'Deprecated in 1.0.0, and will be removed in 1.1.0, ' - 'please use nipype.interfaces.fsl.model.load_template instead.') - return load_template(name) + from ...utils.misc import trim + + docstring = [] + cmd = getattr(cls, '_cmd', None) + if cmd: + docstring += ['Wraps the executable command ``%s``.' % cmd, ''] + + if cls.__doc__: + docstring += trim(cls.__doc__).split('\n') + [''] + + allhelp = '\n'.join( + docstring + + _inputs_help(cls) + [''] + + _outputs_help(cls) + [''] + + _refs_help(cls) + ) + return allhelp.expandtabs(8) + + +def _inputs_help(cls): + r""" + Prints description for input parameters + + >>> from nipype.interfaces.afni import GCOR + >>> _inputs_help(GCOR) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ['Inputs::', '', '\t[Mandatory]', '\tin_file: (an existing file name)', ... + + """ + helpstr = ['Inputs::'] + mandatory_keys = [] + optional_items = [] + + if cls.input_spec: + inputs = cls.input_spec() + mandatory_items = list(inputs.traits(mandatory=True).items()) + if mandatory_items: + helpstr += ['', '\t[Mandatory]'] + for name, spec in mandatory_items: + helpstr += get_trait_desc(inputs, name, spec) + + mandatory_keys = {item[0] for item in mandatory_items} + optional_items = ['\n'.join(get_trait_desc(inputs, name, val)) + for name, val in inputs.traits(transient=None).items() + if name not in mandatory_keys] + if optional_items: + helpstr += ['', '\t[Optional]'] + optional_items + + if not mandatory_keys and not optional_items: + helpstr += ['', '\tNone'] + return helpstr + + +def _outputs_help(cls): + r""" + Prints description for output parameters + + >>> from nipype.interfaces.afni import GCOR + >>> _outputs_help(GCOR) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + ['Outputs::', '', '\tout: (a float)\n\t\tglobal correlation value'] + + """ + helpstr = ['Outputs::', '', '\tNone'] + if cls.output_spec: + outputs = cls.output_spec() + outhelpstr = [ + '\n'.join(get_trait_desc(outputs, name, spec)) + for name, spec in outputs.traits(transient=None).items()] + if outhelpstr: + helpstr = helpstr[:-1] + outhelpstr + return helpstr + + +def _refs_help(cls): + """Prints interface references.""" + references = getattr(cls, 'references_', None) + if not references: + return [] + + helpstr = ['References:', '-----------'] + for r in references: + helpstr += ['{}'.format(r['entry'])] + + return helpstr + + +def get_trait_desc(inputs, name, spec): + """Parses a HasTraits object into a nipype documentation string""" + desc = spec.desc + xor = spec.xor + requires = spec.requires + argstr = spec.argstr + + manhelpstr = ['\t%s' % name] + + type_info = spec.full_info(inputs, name, None) + + default = '' + if spec.usedefault: + default = ', nipype default value: %s' % str( + spec.default_value()[1]) + line = "(%s%s)" % (type_info, default) + + manhelpstr = wrap( + line, + HELP_LINEWIDTH, + initial_indent=manhelpstr[0] + ': ', + subsequent_indent='\t\t ') + + if desc: + for line in desc.split('\n'): + line = re.sub(r"\s+", " ", line) + manhelpstr += wrap( + line, HELP_LINEWIDTH, + initial_indent='\t\t', + subsequent_indent='\t\t') + + if argstr: + pos = spec.position + if pos is not None: + manhelpstr += wrap( + 'argument: ``%s``, position: %s' % (argstr, pos), + HELP_LINEWIDTH, + initial_indent='\t\t', + subsequent_indent='\t\t') + else: + manhelpstr += wrap( + 'argument: ``%s``' % argstr, + HELP_LINEWIDTH, + initial_indent='\t\t', + subsequent_indent='\t\t') + + if xor: + line = '%s' % ', '.join(xor) + manhelpstr += wrap( + line, + HELP_LINEWIDTH, + initial_indent='\t\tmutually_exclusive: ', + subsequent_indent='\t\t ') + + if requires: + others = [field for field in requires if field != name] + line = '%s' % ', '.join(others) + manhelpstr += wrap( + line, + HELP_LINEWIDTH, + initial_indent='\t\trequires: ', + subsequent_indent='\t\t ') + return manhelpstr diff --git a/nipype/interfaces/base/tests/test_core.py b/nipype/interfaces/base/tests/test_core.py index bcbd43db28..265edc444f 100644 --- a/nipype/interfaces/base/tests/test_core.py +++ b/nipype/interfaces/base/tests/test_core.py @@ -12,6 +12,7 @@ from .... import config from ....testing import example_data from ... import base as nib +from ..support import _inputs_help standard_library.install_aliases() @@ -42,14 +43,6 @@ def test_Interface(): assert nib.Interface.output_spec is None with pytest.raises(NotImplementedError): nib.Interface() - with pytest.raises(NotImplementedError): - nib.Interface.help() - with pytest.raises(NotImplementedError): - nib.Interface._inputs_help() - with pytest.raises(NotImplementedError): - nib.Interface._outputs_help() - with pytest.raises(NotImplementedError): - nib.Interface._outputs() class DerivedInterface(nib.Interface): def __init__(self): @@ -85,7 +78,7 @@ class DerivedInterface(nib.BaseInterface): resource_monitor = False assert DerivedInterface.help() is None - assert 'moo' in ''.join(DerivedInterface._inputs_help()) + assert 'moo' in ''.join(_inputs_help(DerivedInterface)) assert DerivedInterface()._outputs() is None assert DerivedInterface().inputs.foo == nib.Undefined with pytest.raises(ValueError): diff --git a/nipype/scripts/utils.py b/nipype/scripts/utils.py index f4b8a86fb1..ce9acde7fd 100644 --- a/nipype/scripts/utils.py +++ b/nipype/scripts/utils.py @@ -13,6 +13,7 @@ from .instance import import_module from ..interfaces.base import InputMultiPath, traits +from ..interfaces.base.support import get_trait_desc # different context options CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -61,8 +62,7 @@ def add_args_options(arg_parser, interface): """Add arguments to `arg_parser` to create a CLI for `interface`.""" inputs = interface.input_spec() for name, spec in sorted(interface.inputs.traits(transient=None).items()): - desc = "\n".join(interface._get_trait_desc(inputs, name, - spec))[len(name) + 2:] + desc = "\n".join(get_trait_desc(inputs, name, spec))[len(name) + 2:] # Escape any % signs with a % desc = desc.replace('%', '%%') args = {} diff --git a/nipype/utils/nipype_cmd.py b/nipype/utils/nipype_cmd.py index b31795aa92..36fa69b3c1 100644 --- a/nipype/utils/nipype_cmd.py +++ b/nipype/utils/nipype_cmd.py @@ -8,6 +8,7 @@ import sys from ..interfaces.base import Interface, InputMultiPath, traits +from ..interfaces.base.support import get_trait_desc from .misc import str2bool @@ -30,8 +31,7 @@ def add_options(parser=None, module=None, function=None): inputs = interface.input_spec() for name, spec in sorted( interface.inputs.traits(transient=None).items()): - desc = "\n".join(interface._get_trait_desc(inputs, name, - spec))[len(name) + 2:] + desc = "\n".join(get_trait_desc(inputs, name, spec))[len(name) + 2:] args = {} if spec.is_trait_type(traits.Bool):