Skip to content

[RF] Detachable run_command process #2787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
737dd6d
[RF] Detachable ``run_command`` process
oesteban Nov 21, 2018
27a999b
be more restrictive about ``shell=True``
oesteban Nov 21, 2018
cc13277
Merge branch 'fix/plugin-by-instance' into experimental
oesteban Nov 21, 2018
942a927
fix error
oesteban Nov 22, 2018
fae1500
normalizing terminal_output, reading in standard i/o
oesteban Nov 22, 2018
1a08ff9
Merge remote-tracking branch 'upstream/master' into maint/refactor-ru…
oesteban Nov 22, 2018
9b4e32d
Merge branch 'experimental' into maint/refactor-run-command
oesteban Nov 22, 2018
283769a
fix missing terminal_output in pbs plugin
oesteban Nov 22, 2018
18a9e73
fixing tests
oesteban Nov 22, 2018
8446d64
fix doctest
oesteban Nov 22, 2018
eecc9a8
Merge remote-tracking branch 'upstream/master' into maint/refactor-ru…
oesteban Nov 22, 2018
7ea2774
Merge branch 'maint/use-has_traits' into maint/refactor-run-command
oesteban Nov 22, 2018
8c7d688
undo traits_set
oesteban Nov 22, 2018
70aa9ce
do not discard first character
oesteban Nov 22, 2018
91c3ea1
Merge branch 'maint/use-has_traits' into maint/refactor-run-command
oesteban Nov 22, 2018
132cddf
[MAINT] Import Sequence from collections to avoid DeprecationWarning
oesteban Nov 22, 2018
ee9195e
Merge branch 'maint/configparser-deprecation' into maint/refactor-run…
oesteban Nov 22, 2018
1873bfb
test passing with python 3.7.1, 60 skipped tests
oesteban Nov 22, 2018
d9c8229
Merge remote-tracking branch 'upstream/master' into maint/refactor-ru…
oesteban Nov 22, 2018
a0f26be
add terminal output to matlab subcommands
oesteban Nov 23, 2018
adec5cf
use our capture std i/o context
oesteban Nov 23, 2018
1cbf064
changedir before test
oesteban Nov 23, 2018
25a7030
fix python 2.7 tests
oesteban Nov 23, 2018
e2ac4c7
[MAINT] Offload interfaces with help formatting
oesteban Nov 24, 2018
c9eea71
add a couple of tests
oesteban Nov 24, 2018
454c6ac
add documentation
oesteban Nov 24, 2018
f09b3bf
fix old _get_trait_desc calls
oesteban Nov 24, 2018
2575d4d
fix old ``_{in,out}puts_help`` calls
oesteban Nov 24, 2018
1512cc3
re-introduce __init__
oesteban Nov 24, 2018
7cb2038
[MAINT] Outsource checks of inputs from interface
oesteban Nov 25, 2018
a15c6fb
remove forgotten print
oesteban Nov 25, 2018
a3600d9
fix mutually-exclusive check
oesteban Nov 25, 2018
c095d65
fix bug trying to append a spec.xor that is None
oesteban Nov 25, 2018
61d553c
fix precedence
oesteban Nov 25, 2018
1ade70c
outsource ``_check_version_requirements``
oesteban Nov 25, 2018
94c0823
fix multiple xor check
oesteban Nov 25, 2018
fddec69
fix check_requires for non-mandatory requires
oesteban Nov 25, 2018
34cb9c9
fix massaging xored inputs
oesteban Nov 25, 2018
569f8ee
Merge remote-tracking branch 'upstream/master' into maint/strip-help-…
oesteban Nov 25, 2018
523475e
Merge remote-tracking branch 'upstream/master' into maint/outsource-c…
oesteban Nov 25, 2018
41a48bd
fix test_extra_Registration
oesteban Nov 25, 2018
8f8ceb1
fixed fsl.utils doctests
oesteban Nov 25, 2018
d0c6ab8
cmdline returns ``None`` instead of raising if error on inputs
oesteban Nov 25, 2018
1b948cf
fix one more use-case
oesteban Nov 25, 2018
1a558a4
fixed tests
oesteban Nov 25, 2018
b08b5ad
fix errors checking xor
oesteban Nov 25, 2018
44a724d
fix fs.preprocess test
oesteban Nov 25, 2018
f7b5650
fix fsl.tests.test_preprocess
oesteban Nov 26, 2018
294850b
fix fsl.tests.test_preprocess
oesteban Nov 26, 2018
13ac0b9
Merge branch 'maint/outsource-checks' of github.com:oesteban/nipype i…
oesteban Nov 26, 2018
89da56a
install caplog fixture
oesteban Nov 26, 2018
fff59eb
pin pytest>=3.4 for reliable caplog fixture
oesteban Nov 26, 2018
8c2ad3c
Merge remote-tracking branch 'upstream/master' into maint/refactor-ru…
oesteban Nov 26, 2018
eed5763
Merge branch 'maint/outsource-checks' into maint/refactor-run-command
oesteban Nov 26, 2018
e4c8223
Merge branch 'maint/strip-help-from-interfaces' into maint/refactor-r…
oesteban Nov 26, 2018
ea44179
avoid ``CommandLine("which")`` calls, use internal which
oesteban Nov 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 10 additions & 44 deletions doc/devel/interface_specs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ In case of trouble, we encourage you to post on `NeuroStars <https://neurostars.
NeuroStars.org is a platform similar to StackOverflow but dedicated to neuroinformatics.
You can also post on the nipype developers mailing list: http://mail.python.org/mailman/listinfo/neuroimaging.
As we are sharing a mailing list with the nipy community, please add ``[nipype]`` to the message title.
Alternatively, you're welcome to chat with us in the Nipype
`Gitter <https://gitter.im/nipy/nipype>`_ channel or in the
Alternatively, you're welcome to chat with us in the Nipype
`Gitter <https://gitter.im/nipy/nipype>`_ channel or in the
BrainHack `Slack <https://brainhack.slack.com/messages/C1FR76RAL>`_ channel.
(Click `here <https://brainhack-slack-invite.herokuapp.com>`_ to join the Slack workspace.)

Expand Down Expand Up @@ -163,62 +163,28 @@ Controlling outputs to terminal

It is very likely that the software wrapped within the interface writes
to the standard output or the standard error of the terminal.
Interfaces provide a means to access and retrieve these outputs, by
using the ``terminal_output`` attribute: ::
Interfaces redirect both streams to logfiles under the interface's working
directory (by default: ``.nipype.out`` and ``.nipype.err``, respectively) ::

import nipype.interfaces.fsl as fsl
mybet = fsl.BET(from_file='bet-settings.json')
mybet.terminal_output = 'file_split'

In the example, the ``terminal_output = 'file_split'`` will redirect the
standard output and the standard error to split files (called
``stdout.nipype`` and ``stderr.nipype`` respectively).
The possible values for ``terminal_output`` are:

*file*
Redirects both standard output and standard error to the same file
called ``output.nipype``.
Messages from both streams will be overlapped as they arrive to
the file.

*file_split*
Redirects the output streams separately, to ``stdout.nipype``
and ``stderr.nipype`` respectively, as described in the example.

*file_stdout*
Only the standard output will be redirected to ``stdout.nipype``
and the standard error will be discarded.

*file_stderr*
Only the standard error will be redirected to ``stderr.nipype``
and the standard output will be discarded.
By default, the contents of both logs are copied to the standard output
and error streams.

*stream*
Both output streams are redirected to the current logger printing
their messages interleaved and immediately to the terminal.

*allatonce*
Both output streams will be forwarded to a buffer and stored
separately in the `runtime` object that the `run()` method returns.
No files are written nor streams printed out to terminal.
Both standard output and standard error are copied to the
terminal.

*none*
Both outputs are discarded

In all cases, except for the ``'none'`` setting of ``terminal_output``,
the ``run()`` method will return a "runtime" object that will contain
the streams in the corresponding properties (``runtime.stdout``
for the standard output, ``runtime.stderr`` for the standard error, and
``runtime.merged`` for both when streams are mixed, eg. when using the
*file* option). ::
The ``runtime`` object will keep the location of both logging files:

import nipype.interfaces.fsl as fsl
mybet = fsl.BET(from_file='bet-settings.json')
mybet.terminal_output = 'file_split'
...
result = mybet.run()
result.runtime.stdout
' ... captured standard output ...'
'/path/to/interface/cwd/.nipype.out'



Expand Down
4 changes: 3 additions & 1 deletion doc/devel/matlab_example2.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ def run(self, **inputs):
# Inject your script
self.inputs.script = self._my_script()
results = super(MatlabCommand, self).run(**inputs)
stdout = results.runtime.stdout

with open(results.runtime.stdout, 'rt') as f:
stdout = f.read()
# Attach stdout to outputs to access matlab results
results.outputs.matlab_output = stdout
return results
Expand Down
2 changes: 1 addition & 1 deletion examples/nipype_tutorial.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@
"convert = MRIConvert(in_file='../ds107/sub001/BOLD/task001_run001/bold.nii.gz',\n",
" out_file='ds107.nii')\n",
"print(convert.cmdline)\n",
"results = convert.run(terminal_output='none') # allatonce, stream (default), file"
"results = convert.run(terminal_output='default') # default, stream"
],
"language": "python",
"metadata": {},
Expand Down
2 changes: 1 addition & 1 deletion examples/rsfmri_vol_surface_preprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import os

from nipype.interfaces.base import CommandLine
CommandLine.set_default_terminal_output('allatonce')
CommandLine.set_default_terminal_output('default')

from dicom import read_file

Expand Down
2 changes: 1 addition & 1 deletion examples/rsfmri_vol_surface_preprocessing_nipy.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
import os

from nipype.interfaces.base import CommandLine
CommandLine.set_default_terminal_output('allatonce')
CommandLine.set_default_terminal_output('default')

# https://github.com/moloney/dcmstack
from dcmstack.extract import default_extractor
Expand Down
9 changes: 7 additions & 2 deletions nipype/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -157,7 +157,12 @@ 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-cov',
'pytest-env',
'codecov',
'coverage<5',
]

EXTRA_REQUIRES = {
'doc': ['Sphinx>=1.4', 'numpydoc', 'matplotlib', 'pydotplus', 'pydot>=1.2.3'],
Expand Down
20 changes: 8 additions & 12 deletions nipype/interfaces/afni/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
"""Provide interface to AFNI commands."""
from __future__ import (print_function, division, unicode_literals,
absolute_import)
from builtins import object, str
from future.utils import raise_from

import os
from sys import platform
from distutils import spawn

from ... import logging, LooseVersion
from ...utils.filemanip import split_filename, fname_presuffix
from ...utils.filemanip import split_filename, fname_presuffix, which

from ..base import (CommandLine, traits, CommandLineInputSpec, isdefined, File,
TraitedSpec, PackageInfo)
Expand Down Expand Up @@ -85,16 +84,12 @@ def standard_image(img_name):
'''Grab an image from the standard location.

Could be made more fancy to allow for more relocatability'''
clout = CommandLine(
'which afni',
ignore_exception=True,
resource_monitor=False,
terminal_output='allatonce').run()
if clout.runtime.returncode is not 0:

afni_path = which('afni')
if not afni_path:
return None

out = clout.runtime.stdout
basedir = os.path.split(out)[0]
basedir = os.path.split(afni_path)[0]
return os.path.join(basedir, img_name)


Expand All @@ -104,10 +99,11 @@ class AFNICommandBase(CommandLine):
See http://afni.nimh.nih.gov/afni/community/board/read.php?1,145346,145347#msg-145347
"""

def _run_interface(self, runtime):
def _run_interface(self, runtime, correct_return_codes=(0, )):
if platform == 'darwin':
runtime.environ['DYLD_FALLBACK_LIBRARY_PATH'] = '/usr/local/afni/'
return super(AFNICommandBase, self)._run_interface(runtime)
return super(AFNICommandBase, self)._run_interface(
runtime, correct_return_codes=correct_return_codes)


class AFNICommandInputSpec(CommandLineInputSpec):
Expand Down
20 changes: 7 additions & 13 deletions nipype/interfaces/afni/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,9 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None):
return self.run().outputs
else:
clip_val = []
for line in runtime.stdout.split('\n'):
with open(runtime.stdout) as f:
stdout = f.read()
for line in stdout.splitlines():
if line:
values = line.split()
if len(values) > 1:
Expand Down Expand Up @@ -1688,27 +1690,18 @@ class OutlierCount(CommandLine):
_cmd = '3dToutcount'
input_spec = OutlierCountInputSpec
output_spec = OutlierCountOutputSpec
_terminal_output = 'file_split'

def _parse_inputs(self, skip=None):
if skip is None:
skip = []

# This is not strictly an input, but needs be
# set before run() is called.
if self.terminal_output == 'none':
self.terminal_output = 'file_split'

if not self.inputs.save_outliers:
skip += ['outliers_file']
return super(OutlierCount, self)._parse_inputs(skip)

def _run_interface(self, runtime):
runtime.stdout = op.abspath(self.inputs.out_file)
runtime = super(OutlierCount, self)._run_interface(runtime)

# Read from runtime.stdout or runtime.merged
with open(op.abspath(self.inputs.out_file), 'w') as outfh:
outfh.write(runtime.stdout or runtime.merged)
return runtime

def _list_outputs(self):
Expand Down Expand Up @@ -1922,7 +1915,6 @@ class ROIStats(AFNICommandBase):

"""
_cmd = '3dROIstats'
_terminal_output = 'allatonce'
input_spec = ROIStatsInputSpec
output_spec = ROIStatsOutputSpec

Expand Down Expand Up @@ -3055,7 +3047,9 @@ def _run_interface(self, runtime):
if self.inputs.save_warp:
import numpy as np
warp_file = self._list_outputs()['warp_file']
np.savetxt(warp_file, [runtime.stdout], fmt=str('%s'))
with open(runtime.stdout) as f:
stdout = f.read().strip()
np.savetxt(warp_file, [stdout], fmt=str('%s'))
return runtime

def _list_outputs(self):
Expand Down
18 changes: 12 additions & 6 deletions nipype/interfaces/afni/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,12 @@ class Autobox(AFNICommand):
def aggregate_outputs(self, runtime=None, needed_outputs=None):
outputs = super(Autobox, self).aggregate_outputs(
runtime, needed_outputs)
pattern = 'x=(?P<x_min>-?\d+)\.\.(?P<x_max>-?\d+) '\
'y=(?P<y_min>-?\d+)\.\.(?P<y_max>-?\d+) '\
'z=(?P<z_min>-?\d+)\.\.(?P<z_max>-?\d+)'
for line in runtime.stderr.split('\n'):
pattern = r'x=(?P<x_min>-?\d+)\.\.(?P<x_max>-?\d+) '\
r'y=(?P<y_min>-?\d+)\.\.(?P<y_max>-?\d+) '\
r'z=(?P<z_min>-?\d+)\.\.(?P<z_max>-?\d+)'
with open(runtime.stderr) as f:
stderr = f.read()
for line in stderr.splitlines():
m = re.search(pattern, line)
if m:
d = m.groupdict()
Expand Down Expand Up @@ -297,7 +299,9 @@ def aggregate_outputs(self, runtime=None, needed_outputs=None):
return self.run().outputs
else:
min_val = []
for line in runtime.stdout.split('\n'):
with open(runtime.stdout, 'rt') as f:
stdout = f.read()
for line in stdout.splitlines():
if line:
values = line.split()
if len(values) > 1:
Expand Down Expand Up @@ -3047,8 +3051,10 @@ class GCOR(CommandLine):
def _run_interface(self, runtime):
runtime = super(GCOR, self)._run_interface(runtime)

with open(runtime.stdout, 'rt') as f:
stdout = f.read()
gcor_line = [
line.strip() for line in runtime.stdout.split('\n')
line.strip() for line in stdout.splitlines()
if line.strip().startswith('GCOR = ')
][-1]
setattr(self, '_gcor', float(gcor_line[len('GCOR = '):]))
Expand Down
12 changes: 8 additions & 4 deletions nipype/interfaces/ants/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -940,9 +940,11 @@ def _run_interface(self, runtime, correct_return_codes=(0, )):
runtime = super(Registration, self)._run_interface(runtime)

# Parse some profiling info
output = runtime.stdout or runtime.merged
with open(runtime.stdout) as f:
output = f.read().strip()

if output:
lines = output.split('\n')
lines = output.splitlines()
for l in lines[::-1]:
# This should be the last line
if l.strip().startswith('Total elapsed time:'):
Expand Down Expand Up @@ -1489,8 +1491,10 @@ def _format_arg(self, opt, spec, val):

def aggregate_outputs(self, runtime=None, needed_outputs=None):
outputs = self._outputs()
stdout = runtime.stdout.split('\n')
outputs.similarity = float(stdout[0])
if runtime is not None and runtime.stdout:
with open(runtime.stdout, 'rf') as f:
stdout = f.read().splitlines()[0]
outputs.similarity = float(stdout[0])
return outputs


Expand Down
16 changes: 10 additions & 6 deletions nipype/interfaces/ants/resampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,16 @@ def _list_outputs(self):
(name, self.inputs.out_postfix, ext)))
return outputs

def _run_interface(self, runtime, correct_return_codes=[0]):
runtime = super(WarpTimeSeriesImageMultiTransform,
self)._run_interface(
runtime, correct_return_codes=[0, 1])
if "100 % complete" not in runtime.stdout:
self.raise_exception(runtime)
def _run_interface(self, runtime, correct_return_codes=(0, 1)):
runtime = super(
WarpTimeSeriesImageMultiTransform, self)._run_interface(
runtime, correct_return_codes=correct_return_codes)

with open(runtime.stdout) as stdoutfh:
output_str = stdoutfh.read()

if "100 % complete" not in output_str:
self.raise_exception(runtime)
return runtime


Expand Down
16 changes: 10 additions & 6 deletions nipype/interfaces/ants/segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -818,8 +818,11 @@ def _run_interface(self, runtime, correct_return_codes=(0, )):
runtime = super(BrainExtraction, self)._run_interface(runtime)

# Still, double-check if it didn't found N4
if 'we cant find' in runtime.stdout:
for line in runtime.stdout.split('\n'):
with open(runtime.stdout) as stdoutfh:
stdout = stdoutfh.read()

if 'we cant find' in stdout:
for line in stdout.split('\n'):
if line.strip().startswith('we cant find'):
tool = line.strip().replace('we cant find the',
'').split(' ')[0]
Expand All @@ -828,10 +831,11 @@ def _run_interface(self, runtime, correct_return_codes=(0, )):
errmsg = (
'antsBrainExtraction.sh requires "%s" to be found in $ANTSPATH '
'($ANTSPATH="%s").') % (tool, ants_path)
if runtime.stderr is None:
runtime.stderr = errmsg
else:
runtime.stderr += '\n' + errmsg

# Append errmsg to stderr file
with open(runtime.stderr, 'w+') as stderrfh:
stderrfh.write(errmsg)

runtime.returncode = 1
self.raise_exception(runtime)

Expand Down
7 changes: 4 additions & 3 deletions nipype/interfaces/ants/tests/test_extra_Registration.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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)
Loading