Skip to content

Commit 5d2e224

Browse files
authored
Merge pull request #3291 from PennSIVE/r-interface
ENH: Interface for R
2 parents 97f2127 + e576c90 commit 5d2e224

File tree

3 files changed

+210
-0
lines changed

3 files changed

+210
-0
lines changed

nipype/interfaces/r.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
"""Interfaces to run R scripts."""
5+
import os
6+
from shutil import which
7+
8+
from .. import config
9+
from .base import (
10+
CommandLineInputSpec,
11+
InputMultiPath,
12+
isdefined,
13+
CommandLine,
14+
traits,
15+
File,
16+
Directory,
17+
)
18+
19+
20+
def get_r_command():
21+
if "NIPYPE_NO_R" in os.environ:
22+
return None
23+
r_cmd = os.getenv("RCMD", default="R")
24+
25+
return r_cmd if which(r_cmd) else None
26+
27+
28+
no_r = get_r_command() is None
29+
30+
31+
class RInputSpec(CommandLineInputSpec):
32+
"""Basic expected inputs to R interface"""
33+
34+
script = traits.Str(
35+
argstr='-e "%s"', desc="R code to run", mandatory=True, position=-1
36+
)
37+
# non-commandline options
38+
rfile = traits.Bool(True, desc="Run R using R script", usedefault=True)
39+
script_file = File(
40+
"pyscript.R", usedefault=True, desc="Name of file to write R code to"
41+
)
42+
43+
44+
class RCommand(CommandLine):
45+
"""Interface that runs R code
46+
47+
>>> import nipype.interfaces.r as r
48+
>>> r = r.RCommand(rfile=False) # doctest: +SKIP
49+
>>> r.inputs.script = "Sys.getenv('USER')" # doctest: +SKIP
50+
>>> out = r.run() # doctest: +SKIP
51+
"""
52+
53+
_cmd = get_r_command()
54+
input_spec = RInputSpec
55+
56+
def __init__(self, r_cmd=None, **inputs):
57+
"""initializes interface to r
58+
(default 'R')
59+
"""
60+
super(RCommand, self).__init__(**inputs)
61+
if r_cmd and isdefined(r_cmd):
62+
self._cmd = r_cmd
63+
64+
# For r commands force all output to be returned since r
65+
# does not have a clean way of notifying an error
66+
self.terminal_output = "allatonce"
67+
68+
def set_default_r_cmd(self, r_cmd):
69+
"""Set the default R command line for R classes.
70+
71+
This method is used to set values for all R
72+
subclasses.
73+
"""
74+
self._cmd = r_cmd
75+
76+
def set_default_rfile(self, rfile):
77+
"""Set the default R script file format for R classes.
78+
79+
This method is used to set values for all R
80+
subclasses.
81+
"""
82+
self._rfile = rfile
83+
84+
def _run_interface(self, runtime):
85+
self.terminal_output = "allatonce"
86+
runtime = super(RCommand, self)._run_interface(runtime)
87+
if "R code threw an exception" in runtime.stderr:
88+
self.raise_exception(runtime)
89+
return runtime
90+
91+
def _format_arg(self, name, trait_spec, value):
92+
if name in ["script"]:
93+
argstr = trait_spec.argstr
94+
return self._gen_r_command(argstr, value)
95+
return super(RCommand, self)._format_arg(name, trait_spec, value)
96+
97+
def _gen_r_command(self, argstr, script_lines):
98+
"""Generates commands and, if rfile specified, writes it to disk."""
99+
if not self.inputs.rfile:
100+
# replace newlines with ;, strip comments
101+
script = "; ".join(
102+
[
103+
line
104+
for line in script_lines.split("\n")
105+
if not line.strip().startswith("#")
106+
]
107+
)
108+
# escape " and $
109+
script = script.replace('"', '\\"')
110+
script = script.replace("$", "\\$")
111+
else:
112+
script_path = os.path.join(os.getcwd(), self.inputs.script_file)
113+
with open(script_path, "wt") as rfile:
114+
rfile.write(script_lines)
115+
script = "source('%s')" % script_path
116+
117+
return argstr % script
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from ..r import RCommand
3+
4+
5+
def test_RCommand_inputs():
6+
input_map = dict(
7+
args=dict(
8+
argstr="%s",
9+
),
10+
environ=dict(
11+
nohash=True,
12+
usedefault=True,
13+
),
14+
rfile=dict(
15+
usedefault=True,
16+
),
17+
script=dict(
18+
argstr='-e "%s"',
19+
mandatory=True,
20+
position=-1,
21+
),
22+
script_file=dict(
23+
extensions=None,
24+
usedefault=True,
25+
),
26+
)
27+
inputs = RCommand.input_spec()
28+
29+
for key, metadata in list(input_map.items()):
30+
for metakey, value in list(metadata.items()):
31+
assert getattr(inputs.traits()[key], metakey) == value

nipype/interfaces/tests/test_r.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
import os
5+
6+
import pytest
7+
from nipype.interfaces import r
8+
9+
no_r = r.no_r
10+
11+
12+
@pytest.mark.skipif(no_r, reason="R is not available")
13+
def test_cmdline(tmp_path):
14+
default_script_file = str(tmp_path / "testscript")
15+
ri = r.RCommand(script="1 + 1", script_file=default_script_file, rfile=False)
16+
r_cmd = r.get_r_command()
17+
18+
assert ri.cmdline == r_cmd + (' -e "1 + 1"')
19+
20+
assert ri.inputs.script == "1 + 1"
21+
assert ri.inputs.script_file == default_script_file
22+
assert not os.path.exists(ri.inputs.script_file), "scriptfile should not exist"
23+
assert not os.path.exists(
24+
default_script_file
25+
), "default scriptfile should not exist."
26+
27+
28+
@pytest.mark.skipif(no_r, reason="R is not available")
29+
def test_run_interface(tmpdir):
30+
cwd = tmpdir.chdir()
31+
default_script_file = r.RInputSpec().script_file
32+
33+
rc = r.RCommand(r_cmd="foo_m")
34+
assert not os.path.exists(default_script_file), "scriptfile should not exist 1."
35+
with pytest.raises(ValueError):
36+
rc.run() # script is mandatory
37+
assert not os.path.exists(default_script_file), "scriptfile should not exist 2."
38+
if os.path.exists(default_script_file): # cleanup
39+
os.remove(default_script_file)
40+
41+
rc.inputs.script = "a=1;"
42+
assert not os.path.exists(default_script_file), "scriptfile should not exist 3."
43+
with pytest.raises(IOError):
44+
rc.run() # foo_m is not an executable
45+
assert os.path.exists(default_script_file), "scriptfile should exist 3."
46+
if os.path.exists(default_script_file): # cleanup
47+
os.remove(default_script_file)
48+
cwd.chdir()
49+
50+
51+
@pytest.mark.skipif(no_r, reason="R is not available")
52+
def test_set_rcmd(tmpdir):
53+
cwd = tmpdir.chdir()
54+
default_script_file = r.RInputSpec().script_file
55+
56+
ri = r.RCommand()
57+
_default_r_cmd = ri._cmd
58+
ri.set_default_r_cmd("foo")
59+
assert not os.path.exists(default_script_file), "scriptfile should not exist."
60+
assert ri._cmd == "foo"
61+
ri.set_default_r_cmd(_default_r_cmd)
62+
cwd.chdir()

0 commit comments

Comments
 (0)