Skip to content

Commit 5216a25

Browse files
committed
Merge branch 'use-scriptrunner' into main-master
* use-scriptrunner: RF: change "below" to "above" for higher directory BF: workaround missing __package__ in Python 3.2 RF: move script test machinery into own module
2 parents 47a4183 + 7dd04d5 commit 5216a25

File tree

2 files changed

+193
-69
lines changed

2 files changed

+193
-69
lines changed

nibabel/tests/scriptrunner.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
""" Module to help tests check script output
2+
3+
Provides class to be instantiated in tests that check scripts. Usually works
4+
something like this in a test module::
5+
6+
from .scriptrunner import ScriptRunner
7+
runner = ScriptRunner()
8+
9+
Then, in the tests, something like::
10+
11+
code, stdout, stderr = runner.run_command(['my-script', my_arg])
12+
assert_equal(code, 0)
13+
assert_equal(stdout, b'This script ran OK')
14+
"""
15+
import sys
16+
import os
17+
from os.path import (dirname, join as pjoin, isfile, isdir, realpath, pathsep)
18+
19+
from subprocess import Popen, PIPE
20+
21+
try: # Python 2
22+
string_types = basestring,
23+
except NameError: # Python 3
24+
string_types = str,
25+
26+
27+
def _get_package():
28+
""" Workaround for missing ``__package__`` in Python 3.2
29+
"""
30+
if '__package__' in globals() and not __package__ is None:
31+
return __package__
32+
return __name__.split('.', 1)[0]
33+
34+
35+
# Same as __package__ for Python 2.6, 2.7 and >= 3.3
36+
MY_PACKAGE=_get_package()
37+
38+
39+
def local_script_dir(script_sdir):
40+
""" Get local script directory if running in development dir, else None
41+
"""
42+
# Check for presence of scripts in development directory. ``realpath``
43+
# allows for the situation where the development directory has been linked
44+
# into the path.
45+
package_path = dirname(__import__(MY_PACKAGE).__file__)
46+
above_us = realpath(pjoin(package_path, '..'))
47+
devel_script_dir = pjoin(above_us, script_sdir)
48+
if isfile(pjoin(above_us, 'setup.py')) and isdir(devel_script_dir):
49+
return devel_script_dir
50+
return None
51+
52+
53+
def local_module_dir(module_name):
54+
""" Get local module directory if running in development dir, else None
55+
"""
56+
mod = __import__(module_name)
57+
containing_path = dirname(dirname(realpath(mod.__file__)))
58+
if containing_path == realpath(os.getcwd()):
59+
return containing_path
60+
return None
61+
62+
63+
class ScriptRunner(object):
64+
""" Class to run scripts and return output
65+
66+
Finds local scripts and local modules if running in the development
67+
directory, otherwise finds system scripts and modules.
68+
"""
69+
def __init__(self,
70+
script_sdir = 'scripts',
71+
module_sdir = MY_PACKAGE,
72+
debug_print_var = None,
73+
output_processor = lambda x : x
74+
):
75+
""" Init ScriptRunner instance
76+
77+
Parameters
78+
----------
79+
script_sdir : str, optional
80+
Name of subdirectory in top-level directory (directory containing
81+
setup.py), to find scripts in development tree. Typically
82+
'scripts', but might be 'bin'.
83+
module_sdir : str, optional
84+
Name of subdirectory in top-level directory (directory containing
85+
setup.py), to find main package directory.
86+
debug_print_vsr : str, optional
87+
Name of environment variable that indicates whether to do debug
88+
printing or no.
89+
output_processor : callable
90+
Callable to run on the stdout, stderr outputs before returning
91+
them. Use this to convert bytes to unicode, strip whitespace, etc.
92+
"""
93+
self.local_script_dir = local_script_dir(script_sdir)
94+
self.local_module_dir = local_module_dir(module_sdir)
95+
if debug_print_var is None:
96+
debug_print_var = '{0}_DEBUG_PRINT'.format(module_sdir.upper())
97+
self.debug_print = os.environ.get(debug_print_var, False)
98+
self.output_processor = output_processor
99+
100+
def run_command(self, cmd, check_code=True):
101+
""" Run command sequence `cmd` returning exit code, stdout, stderr
102+
103+
Parameters
104+
----------
105+
cmd : str or sequence
106+
string with command name or sequence of strings defining command
107+
check_code : {True, False}, optional
108+
If True, raise error for non-zero return code
109+
110+
Returns
111+
-------
112+
returncode : int
113+
return code from execution of `cmd`
114+
stdout : bytes (python 3) or str (python 2)
115+
stdout from `cmd`
116+
stderr : bytes (python 3) or str (python 2)
117+
stderr from `cmd`
118+
"""
119+
if isinstance(cmd, string_types):
120+
cmd = [cmd]
121+
else:
122+
cmd = list(cmd)
123+
if not self.local_script_dir is None:
124+
# Windows can't run script files without extensions natively so we need
125+
# to run local scripts (no extensions) via the Python interpreter. On
126+
# Unix, we might have the wrong incantation for the Python interpreter
127+
# in the hash bang first line in the source file. So, either way, run
128+
# the script through the Python interpreter
129+
cmd = [sys.executable,
130+
pjoin(self.local_script_dir, cmd[0])] + cmd[1:]
131+
elif os.name == 'nt':
132+
# Need .bat file extension for windows
133+
cmd[0] += '.bat'
134+
if os.name == 'nt':
135+
# Quote all arguments because they might be files with spaces
136+
# The quotes delimit the arguments on Windows. On Unix the list
137+
# elements are each separate arguments.
138+
cmd = ['"{0}"'.format(c) for c in cmd]
139+
if self.debug_print:
140+
print("Running command '%s'" % cmd)
141+
env = os.environ
142+
if not self.local_module_dir is None:
143+
# module likely comes from the current working directory. We might need
144+
# that directory on the path if we're running the scripts from a
145+
# temporary directory
146+
env = env.copy()
147+
pypath = env.get('PYTHONPATH', None)
148+
if pypath is None:
149+
env['PYTHONPATH'] = self.local_module_dir
150+
else:
151+
env['PYTHONPATH'] = self.local_module_dir + pathsep + pypath
152+
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
153+
stdout, stderr = proc.communicate()
154+
if proc.poll() == None:
155+
proc.terminate()
156+
if check_code and proc.returncode != 0:
157+
raise RuntimeError(
158+
"""Command "{0}" failed with
159+
stdout
160+
------
161+
{1}
162+
stderr
163+
------
164+
{2}
165+
""".format(cmd, stdout, stderr))
166+
opp = self.output_processor
167+
return proc.returncode, opp(stdout), opp(stderr)

nibabel/tests/test_scripts.py

Lines changed: 26 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@
22
# vi: set ft=python sts=4 ts=4 sw=4 et:
33
""" Test scripts
44
5-
If we appear to be running from the development directory, use the scripts in
6-
the top-level folder ``scripts``. Otherwise try and get the scripts from the
7-
path
5+
Test running scripts
86
"""
97
from __future__ import division, print_function, absolute_import
108

11-
import sys
129
import os
13-
from os.path import dirname, join as pjoin, isfile, isdir, abspath, realpath, pardir
10+
from os.path import dirname, join as pjoin, abspath
1411
import re
1512

16-
from subprocess import Popen, PIPE
17-
1813
import numpy as np
1914

2015
from ..tmpdirs import InTemporaryDirectory
@@ -24,95 +19,58 @@
2419

2520
from numpy.testing import assert_almost_equal
2621

22+
from .scriptrunner import ScriptRunner
23+
24+
25+
def _proc_stdout(stdout):
26+
stdout_str = stdout.decode('latin1').strip()
27+
return stdout_str.replace(os.linesep, '\n')
28+
29+
30+
runner = ScriptRunner(
31+
script_sdir = 'bin',
32+
debug_print_var = 'NIPY_DEBUG_PRINT',
33+
output_processor=_proc_stdout)
34+
run_command = runner.run_command
35+
36+
2737
def script_test(func):
2838
# Decorator to label test as a script_test
2939
func.script_test = True
3040
return func
3141
script_test.__test__ = False # It's not a test
3242

33-
# Need shell to get path to correct executables
34-
USE_SHELL = True
35-
36-
DEBUG_PRINT = os.environ.get('NIPY_DEBUG_PRINT', False)
37-
3843
DATA_PATH = abspath(pjoin(dirname(__file__), 'data'))
39-
IMPORT_PATH = abspath(pjoin(dirname(__file__), pardir, pardir))
40-
41-
def local_script_dir(script_sdir):
42-
# Check for presence of scripts in development directory. ``realpath``
43-
# checks for the situation where the development directory has been linked
44-
# into the path.
45-
below_us_2 = realpath(pjoin(dirname(__file__), '..', '..'))
46-
devel_script_dir = pjoin(below_us_2, script_sdir)
47-
if isfile(pjoin(below_us_2, 'setup.py')) and isdir(devel_script_dir):
48-
return devel_script_dir
49-
return None
50-
51-
LOCAL_SCRIPT_DIR = local_script_dir('bin')
52-
53-
def run_command(cmd):
54-
if LOCAL_SCRIPT_DIR is None:
55-
env = None
56-
else: # We are running scripts local to the source tree (not installed)
57-
# Windows can't run script files without extensions natively so we need
58-
# to run local scripts (no extensions) via the Python interpreter. On
59-
# Unix, we might have the wrong incantation for the Python interpreter
60-
# in the hash bang first line in the source file. So, either way, run
61-
# the script through the Python interpreter
62-
cmd = "%s %s" % (sys.executable, pjoin(LOCAL_SCRIPT_DIR, cmd))
63-
# If we're testing local script files, point subprocess to consider
64-
# current nibabel in favor of possibly installed different version
65-
env = {'PYTHONPATH': '%s:%s'
66-
% (IMPORT_PATH, os.environ.get('PYTHONPATH', ''))}
67-
if DEBUG_PRINT:
68-
print("Running command '%s'" % cmd)
69-
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=USE_SHELL,
70-
env=env)
71-
stdout, stderr = proc.communicate()
72-
if proc.poll() == None:
73-
proc.terminate()
74-
if proc.returncode != 0:
75-
raise RuntimeError('Command "%s" failed with stdout\n%s\nstderr\n%s\n'
76-
% (cmd, stdout, stderr))
77-
return proc.returncode, stdout, stderr
78-
79-
80-
def _proc_stdout(stdout):
81-
stdout_str = stdout.decode('latin1').strip()
82-
return stdout_str.replace(os.linesep, '\n')
83-
8444

8545
@script_test
8646
def test_nib_ls():
8747
# test nib-ls script
8848
fname = pjoin(DATA_PATH, 'example4d.nii.gz')
8949
expected_re = (" (int16|[<>]i2) \[128, 96, 24, 2\] "
9050
"2.00x2.00x2.20x2000.00 #exts: 2 sform$")
91-
# Need to quote out path in case it has spaces
92-
cmd = 'nib-ls "%s"' % (fname)
51+
cmd = ['nib-ls', fname]
9352
code, stdout, stderr = run_command(cmd)
94-
res = _proc_stdout(stdout)
95-
assert_equal(fname, res[:len(fname)])
96-
assert_not_equal(re.match(expected_re, res[len(fname):]), None)
53+
assert_equal(fname, stdout[:len(fname)])
54+
assert_not_equal(re.match(expected_re, stdout[len(fname):]), None)
9755

9856

9957
@script_test
10058
def test_nib_nifti_dx():
10159
# Test nib-nifti-dx script
10260
clean_hdr = pjoin(DATA_PATH, 'nifti1.hdr')
103-
cmd = 'nib-nifti-dx "%s"' % (clean_hdr,)
61+
cmd = ['nib-nifti-dx', clean_hdr]
10462
code, stdout, stderr = run_command(cmd)
105-
assert_equal(stdout.strip().decode('latin1'), 'Header for "%s" is clean' % clean_hdr)
63+
assert_equal(stdout.strip(), 'Header for "%s" is clean' % clean_hdr)
10664
dirty_hdr = pjoin(DATA_PATH, 'analyze.hdr')
107-
cmd = 'nib-nifti-dx "%s"' % (dirty_hdr,)
65+
cmd = ['nib-nifti-dx', dirty_hdr]
10866
code, stdout, stderr = run_command(cmd)
10967
expected = """Picky header check output for "%s"
11068
11169
pixdim[0] (qfac) should be 1 (default) or -1
11270
magic string "" is not valid
11371
sform_code 11776 not valid""" % (dirty_hdr,)
11472
# Split strings to remove line endings
115-
assert_equal(_proc_stdout(stdout), expected)
73+
assert_equal(stdout, expected)
11674

11775

11876
def vox_size(affine):
@@ -122,14 +80,13 @@ def vox_size(affine):
12280
@script_test
12381
def test_parrec2nii():
12482
# Test parrec2nii script
125-
cmd = 'parrec2nii --help'
83+
cmd = ['parrec2nii', '--help']
12684
code, stdout, stderr = run_command(cmd)
127-
stdout = stdout.decode('latin1')
12885
assert_true(stdout.startswith('Usage'))
12986
in_fname = pjoin(DATA_PATH, 'phantom_EPI_asc_CLEAR_2_1.PAR')
13087
out_froot = 'phantom_EPI_asc_CLEAR_2_1.nii'
13188
with InTemporaryDirectory():
132-
run_command('parrec2nii "{0}"'.format(in_fname))
89+
run_command(['parrec2nii', in_fname])
13390
img = load(out_froot)
13491
assert_equal(img.shape, (64, 64, 9, 3))
13592
assert_equal(img.get_data_dtype(), np.dtype(np.int16))

0 commit comments

Comments
 (0)