Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit fbed1f3

Browse files
authoredDec 14, 2018
Merge pull request #2797 from oesteban/maint/strip-help-from-interfaces
[MAINT] Offload interfaces with help formatting
2 parents 8173dcf + 7861a4a commit fbed1f3

File tree

6 files changed

+199
-198
lines changed

6 files changed

+199
-198
lines changed
 

‎nipype/interfaces/base/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,4 @@
2222
OutputMultiObject, InputMultiObject,
2323
OutputMultiPath, InputMultiPath)
2424

25-
from .support import (Bunch, InterfaceResult, load_template,
26-
NipypeInterfaceError)
25+
from .support import (Bunch, InterfaceResult, NipypeInterfaceError)

‎nipype/interfaces/base/core.py

Lines changed: 29 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,17 @@
2020
from copy import deepcopy
2121
from datetime import datetime as dt
2222
import os
23-
import re
2423
import platform
2524
import subprocess as sp
2625
import shlex
2726
import sys
28-
from textwrap import wrap
2927
import simplejson as json
3028
from dateutil.parser import parse as parseutc
29+
from future import standard_library
3130

3231
from ... import config, logging, LooseVersion
3332
from ...utils.provenance import write_provenance
34-
from ...utils.misc import trim, str2bool, rgetcwd
33+
from ...utils.misc import str2bool, rgetcwd
3534
from ...utils.filemanip import (FileNotFoundError, split_filename,
3635
which, get_dependencies)
3736
from ...utils.subprocess import run_command
@@ -42,9 +41,9 @@
4241
from .specs import (BaseInterfaceInputSpec, CommandLineInputSpec,
4342
StdOutCommandLineInputSpec, MpiCommandLineInputSpec,
4443
get_filecopy_info)
45-
from .support import (Bunch, InterfaceResult, NipypeInterfaceError)
44+
from .support import (Bunch, InterfaceResult, NipypeInterfaceError,
45+
format_help)
4646

47-
from future import standard_library
4847
standard_library.install_aliases()
4948

5049
iflogger = logging.getLogger('nipype.interface')
@@ -68,47 +67,42 @@ class Interface(object):
6867

6968
input_spec = None # A traited input specification
7069
output_spec = None # A traited output specification
71-
72-
# defines if the interface can reuse partial results after interruption
73-
_can_resume = False
70+
_can_resume = False # See property below
71+
_always_run = False # See property below
7472

7573
@property
7674
def can_resume(self):
75+
"""Defines if the interface can reuse partial results after interruption.
76+
Only applies to interfaces being run within a workflow context."""
7777
return self._can_resume
7878

79-
# should the interface be always run even if the inputs were not changed?
80-
_always_run = False
81-
8279
@property
8380
def always_run(self):
81+
"""Should the interface be always run even if the inputs were not changed?
82+
Only applies to interfaces being run within a workflow context."""
8483
return self._always_run
8584

86-
def __init__(self, **inputs):
87-
"""Initialize command with given args and inputs."""
88-
raise NotImplementedError
89-
90-
@classmethod
91-
def help(cls):
92-
""" Prints class help"""
93-
raise NotImplementedError
94-
95-
@classmethod
96-
def _inputs_help(cls):
97-
""" Prints inputs help"""
98-
raise NotImplementedError
99-
100-
@classmethod
101-
def _outputs_help(cls):
102-
""" Prints outputs help"""
85+
@property
86+
def version(self):
87+
"""interfaces should implement a version property"""
10388
raise NotImplementedError
10489

10590
@classmethod
10691
def _outputs(cls):
10792
""" Initializes outputs"""
10893
raise NotImplementedError
10994

110-
@property
111-
def version(self):
95+
@classmethod
96+
def help(cls, returnhelp=False):
97+
""" Prints class help """
98+
allhelp = format_help(cls)
99+
if returnhelp:
100+
return allhelp
101+
print(allhelp)
102+
return None # R1710
103+
104+
def __init__(self):
105+
"""Subclasses must implement __init__"""
112106
raise NotImplementedError
113107

114108
def run(self):
@@ -190,142 +184,6 @@ def __init__(self, from_file=None, resource_monitor=None,
190184
for name, value in list(inputs.items()):
191185
setattr(self.inputs, name, value)
192186

193-
@classmethod
194-
def help(cls, returnhelp=False):
195-
""" Prints class help
196-
"""
197-
198-
if cls.__doc__:
199-
# docstring = cls.__doc__.split('\n')
200-
# docstring = [trim(line, '') for line in docstring]
201-
docstring = trim(cls.__doc__).split('\n') + ['']
202-
else:
203-
docstring = ['']
204-
205-
allhelp = '\n'.join(docstring + cls._inputs_help(
206-
) + [''] + cls._outputs_help() + [''] + cls._refs_help() + [''])
207-
if returnhelp:
208-
return allhelp
209-
else:
210-
print(allhelp)
211-
212-
@classmethod
213-
def _refs_help(cls):
214-
""" Prints interface references.
215-
"""
216-
if not cls.references_:
217-
return []
218-
219-
helpstr = ['References::']
220-
221-
for r in cls.references_:
222-
helpstr += ['{}'.format(r['entry'])]
223-
224-
return helpstr
225-
226-
@classmethod
227-
def _get_trait_desc(self, inputs, name, spec):
228-
desc = spec.desc
229-
xor = spec.xor
230-
requires = spec.requires
231-
argstr = spec.argstr
232-
233-
manhelpstr = ['\t%s' % name]
234-
235-
type_info = spec.full_info(inputs, name, None)
236-
237-
default = ''
238-
if spec.usedefault:
239-
default = ', nipype default value: %s' % str(
240-
spec.default_value()[1])
241-
line = "(%s%s)" % (type_info, default)
242-
243-
manhelpstr = wrap(
244-
line,
245-
70,
246-
initial_indent=manhelpstr[0] + ': ',
247-
subsequent_indent='\t\t ')
248-
249-
if desc:
250-
for line in desc.split('\n'):
251-
line = re.sub("\s+", " ", line)
252-
manhelpstr += wrap(
253-
line, 70, initial_indent='\t\t', subsequent_indent='\t\t')
254-
255-
if argstr:
256-
pos = spec.position
257-
if pos is not None:
258-
manhelpstr += wrap(
259-
'flag: %s, position: %s' % (argstr, pos),
260-
70,
261-
initial_indent='\t\t',
262-
subsequent_indent='\t\t')
263-
else:
264-
manhelpstr += wrap(
265-
'flag: %s' % argstr,
266-
70,
267-
initial_indent='\t\t',
268-
subsequent_indent='\t\t')
269-
270-
if xor:
271-
line = '%s' % ', '.join(xor)
272-
manhelpstr += wrap(
273-
line,
274-
70,
275-
initial_indent='\t\tmutually_exclusive: ',
276-
subsequent_indent='\t\t ')
277-
278-
if requires:
279-
others = [field for field in requires if field != name]
280-
line = '%s' % ', '.join(others)
281-
manhelpstr += wrap(
282-
line,
283-
70,
284-
initial_indent='\t\trequires: ',
285-
subsequent_indent='\t\t ')
286-
return manhelpstr
287-
288-
@classmethod
289-
def _inputs_help(cls):
290-
""" Prints description for input parameters
291-
"""
292-
helpstr = ['Inputs::']
293-
294-
inputs = cls.input_spec()
295-
if len(list(inputs.traits(transient=None).items())) == 0:
296-
helpstr += ['', '\tNone']
297-
return helpstr
298-
299-
manhelpstr = ['', '\t[Mandatory]']
300-
mandatory_items = inputs.traits(mandatory=True)
301-
for name, spec in sorted(mandatory_items.items()):
302-
manhelpstr += cls._get_trait_desc(inputs, name, spec)
303-
304-
opthelpstr = ['', '\t[Optional]']
305-
for name, spec in sorted(inputs.traits(transient=None).items()):
306-
if name in mandatory_items:
307-
continue
308-
opthelpstr += cls._get_trait_desc(inputs, name, spec)
309-
310-
if manhelpstr:
311-
helpstr += manhelpstr
312-
if opthelpstr:
313-
helpstr += opthelpstr
314-
return helpstr
315-
316-
@classmethod
317-
def _outputs_help(cls):
318-
""" Prints description for output parameters
319-
"""
320-
helpstr = ['Outputs::', '']
321-
if cls.output_spec:
322-
outputs = cls.output_spec()
323-
for name, spec in sorted(outputs.traits(transient=None).items()):
324-
helpstr += cls._get_trait_desc(outputs, name, spec)
325-
if len(helpstr) == 2:
326-
helpstr += ['\tNone']
327-
return helpstr
328-
329187
def _outputs(self):
330188
""" Returns a bunch containing output fields for the class
331189
"""
@@ -645,7 +503,7 @@ def save_inputs_to_json(self, json_file):
645503
A convenient way to save current inputs to a JSON file.
646504
"""
647505
inputs = self.inputs.get_traitsfree()
648-
iflogger.debug('saving inputs {}', inputs)
506+
iflogger.debug('saving inputs %s', inputs)
649507
with open(json_file, 'w' if PY3 else 'wb') as fhandle:
650508
json.dump(inputs, fhandle, indent=4, ensure_ascii=False)
651509

@@ -777,14 +635,6 @@ def set_default_terminal_output(cls, output_type):
777635
raise AttributeError(
778636
'Invalid terminal output_type: %s' % output_type)
779637

780-
@classmethod
781-
def help(cls, returnhelp=False):
782-
allhelp = 'Wraps command **{cmd}**\n\n{help}'.format(
783-
cmd=cls._cmd, help=super(CommandLine, cls).help(returnhelp=True))
784-
if returnhelp:
785-
return allhelp
786-
print(allhelp)
787-
788638
def __init__(self, command=None, terminal_output=None, **inputs):
789639
super(CommandLine, self).__init__(**inputs)
790640
self._environ = None
@@ -804,6 +654,10 @@ def __init__(self, command=None, terminal_output=None, **inputs):
804654
@property
805655
def cmd(self):
806656
"""sets base command, immutable"""
657+
if not self._cmd:
658+
raise NotImplementedError(
659+
'CommandLineInterface should wrap an executable, but '
660+
'none has been set.')
807661
return self._cmd
808662

809663
@property

‎nipype/interfaces/base/support.py

Lines changed: 163 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313

1414
import os
1515
from copy import deepcopy
16+
from textwrap import wrap
17+
import re
1618

1719
from ... import logging
1820
from ...utils.misc import is_container
1921
from ...utils.filemanip import md5, to_str, hash_infile
2022
iflogger = logging.getLogger('nipype.interface')
2123

24+
HELP_LINEWIDTH = 70
2225

2326
class NipypeInterfaceError(Exception):
2427
"""Custom error for interfaces"""
@@ -235,14 +238,166 @@ def version(self):
235238
return self._version
236239

237240

238-
def load_template(name):
241+
def format_help(cls):
239242
"""
240-
Deprecated stub for backwards compatibility,
241-
please use nipype.interfaces.fsl.model.load_template
243+
Prints help text of a Nipype interface
244+
245+
>>> from nipype.interfaces.afni import GCOR
246+
>>> GCOR.help() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
247+
Wraps the executable command ``@compute_gcor``.
248+
<BLANKLINE>
249+
Computes the average correlation between every voxel
250+
and ever other voxel, over any give mask.
251+
<BLANKLINE>
252+
<BLANKLINE>
253+
For complete details, ...
242254
243255
"""
244-
from ..fsl.model import load_template
245-
iflogger.warning(
246-
'Deprecated in 1.0.0, and will be removed in 1.1.0, '
247-
'please use nipype.interfaces.fsl.model.load_template instead.')
248-
return load_template(name)
256+
from ...utils.misc import trim
257+
258+
docstring = []
259+
cmd = getattr(cls, '_cmd', None)
260+
if cmd:
261+
docstring += ['Wraps the executable command ``%s``.' % cmd, '']
262+
263+
if cls.__doc__:
264+
docstring += trim(cls.__doc__).split('\n') + ['']
265+
266+
allhelp = '\n'.join(
267+
docstring +
268+
_inputs_help(cls) + [''] +
269+
_outputs_help(cls) + [''] +
270+
_refs_help(cls)
271+
)
272+
return allhelp.expandtabs(8)
273+
274+
275+
def _inputs_help(cls):
276+
r"""
277+
Prints description for input parameters
278+
279+
>>> from nipype.interfaces.afni import GCOR
280+
>>> _inputs_help(GCOR) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
281+
['Inputs::', '', '\t[Mandatory]', '\tin_file: (an existing file name)', ...
282+
283+
"""
284+
helpstr = ['Inputs::']
285+
mandatory_keys = []
286+
optional_items = []
287+
288+
if cls.input_spec:
289+
inputs = cls.input_spec()
290+
mandatory_items = list(inputs.traits(mandatory=True).items())
291+
if mandatory_items:
292+
helpstr += ['', '\t[Mandatory]']
293+
for name, spec in mandatory_items:
294+
helpstr += get_trait_desc(inputs, name, spec)
295+
296+
mandatory_keys = {item[0] for item in mandatory_items}
297+
optional_items = ['\n'.join(get_trait_desc(inputs, name, val))
298+
for name, val in inputs.traits(transient=None).items()
299+
if name not in mandatory_keys]
300+
if optional_items:
301+
helpstr += ['', '\t[Optional]'] + optional_items
302+
303+
if not mandatory_keys and not optional_items:
304+
helpstr += ['', '\tNone']
305+
return helpstr
306+
307+
308+
def _outputs_help(cls):
309+
r"""
310+
Prints description for output parameters
311+
312+
>>> from nipype.interfaces.afni import GCOR
313+
>>> _outputs_help(GCOR) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
314+
['Outputs::', '', '\tout: (a float)\n\t\tglobal correlation value']
315+
316+
"""
317+
helpstr = ['Outputs::', '', '\tNone']
318+
if cls.output_spec:
319+
outputs = cls.output_spec()
320+
outhelpstr = [
321+
'\n'.join(get_trait_desc(outputs, name, spec))
322+
for name, spec in outputs.traits(transient=None).items()]
323+
if outhelpstr:
324+
helpstr = helpstr[:-1] + outhelpstr
325+
return helpstr
326+
327+
328+
def _refs_help(cls):
329+
"""Prints interface references."""
330+
references = getattr(cls, 'references_', None)
331+
if not references:
332+
return []
333+
334+
helpstr = ['References:', '-----------']
335+
for r in references:
336+
helpstr += ['{}'.format(r['entry'])]
337+
338+
return helpstr
339+
340+
341+
def get_trait_desc(inputs, name, spec):
342+
"""Parses a HasTraits object into a nipype documentation string"""
343+
desc = spec.desc
344+
xor = spec.xor
345+
requires = spec.requires
346+
argstr = spec.argstr
347+
348+
manhelpstr = ['\t%s' % name]
349+
350+
type_info = spec.full_info(inputs, name, None)
351+
352+
default = ''
353+
if spec.usedefault:
354+
default = ', nipype default value: %s' % str(
355+
spec.default_value()[1])
356+
line = "(%s%s)" % (type_info, default)
357+
358+
manhelpstr = wrap(
359+
line,
360+
HELP_LINEWIDTH,
361+
initial_indent=manhelpstr[0] + ': ',
362+
subsequent_indent='\t\t ')
363+
364+
if desc:
365+
for line in desc.split('\n'):
366+
line = re.sub(r"\s+", " ", line)
367+
manhelpstr += wrap(
368+
line, HELP_LINEWIDTH,
369+
initial_indent='\t\t',
370+
subsequent_indent='\t\t')
371+
372+
if argstr:
373+
pos = spec.position
374+
if pos is not None:
375+
manhelpstr += wrap(
376+
'argument: ``%s``, position: %s' % (argstr, pos),
377+
HELP_LINEWIDTH,
378+
initial_indent='\t\t',
379+
subsequent_indent='\t\t')
380+
else:
381+
manhelpstr += wrap(
382+
'argument: ``%s``' % argstr,
383+
HELP_LINEWIDTH,
384+
initial_indent='\t\t',
385+
subsequent_indent='\t\t')
386+
387+
if xor:
388+
line = '%s' % ', '.join(xor)
389+
manhelpstr += wrap(
390+
line,
391+
HELP_LINEWIDTH,
392+
initial_indent='\t\tmutually_exclusive: ',
393+
subsequent_indent='\t\t ')
394+
395+
if requires:
396+
others = [field for field in requires if field != name]
397+
line = '%s' % ', '.join(others)
398+
manhelpstr += wrap(
399+
line,
400+
HELP_LINEWIDTH,
401+
initial_indent='\t\trequires: ',
402+
subsequent_indent='\t\t ')
403+
return manhelpstr

‎nipype/interfaces/base/tests/test_core.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .... import config
1313
from ....testing import example_data
1414
from ... import base as nib
15+
from ..support import _inputs_help
1516

1617
standard_library.install_aliases()
1718

@@ -42,14 +43,6 @@ def test_Interface():
4243
assert nib.Interface.output_spec is None
4344
with pytest.raises(NotImplementedError):
4445
nib.Interface()
45-
with pytest.raises(NotImplementedError):
46-
nib.Interface.help()
47-
with pytest.raises(NotImplementedError):
48-
nib.Interface._inputs_help()
49-
with pytest.raises(NotImplementedError):
50-
nib.Interface._outputs_help()
51-
with pytest.raises(NotImplementedError):
52-
nib.Interface._outputs()
5346

5447
class DerivedInterface(nib.Interface):
5548
def __init__(self):
@@ -85,7 +78,7 @@ class DerivedInterface(nib.BaseInterface):
8578
resource_monitor = False
8679

8780
assert DerivedInterface.help() is None
88-
assert 'moo' in ''.join(DerivedInterface._inputs_help())
81+
assert 'moo' in ''.join(_inputs_help(DerivedInterface))
8982
assert DerivedInterface()._outputs() is None
9083
assert DerivedInterface().inputs.foo == nib.Undefined
9184
with pytest.raises(ValueError):

‎nipype/scripts/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .instance import import_module
1515
from ..interfaces.base import InputMultiPath, traits
16+
from ..interfaces.base.support import get_trait_desc
1617

1718
# different context options
1819
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@@ -61,8 +62,7 @@ def add_args_options(arg_parser, interface):
6162
"""Add arguments to `arg_parser` to create a CLI for `interface`."""
6263
inputs = interface.input_spec()
6364
for name, spec in sorted(interface.inputs.traits(transient=None).items()):
64-
desc = "\n".join(interface._get_trait_desc(inputs, name,
65-
spec))[len(name) + 2:]
65+
desc = "\n".join(get_trait_desc(inputs, name, spec))[len(name) + 2:]
6666
# Escape any % signs with a %
6767
desc = desc.replace('%', '%%')
6868
args = {}

‎nipype/utils/nipype_cmd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import sys
99

1010
from ..interfaces.base import Interface, InputMultiPath, traits
11+
from ..interfaces.base.support import get_trait_desc
1112
from .misc import str2bool
1213

1314

@@ -30,8 +31,7 @@ def add_options(parser=None, module=None, function=None):
3031
inputs = interface.input_spec()
3132
for name, spec in sorted(
3233
interface.inputs.traits(transient=None).items()):
33-
desc = "\n".join(interface._get_trait_desc(inputs, name,
34-
spec))[len(name) + 2:]
34+
desc = "\n".join(get_trait_desc(inputs, name, spec))[len(name) + 2:]
3535
args = {}
3636

3737
if spec.is_trait_type(traits.Bool):

0 commit comments

Comments
 (0)
Please sign in to comment.