Skip to content

Commit bb7ab62

Browse files
committed
Accommodating pipes in programs.py
Adding in docker pipes unit test
1 parent f3df8ff commit bb7ab62

2 files changed

Lines changed: 91 additions & 12 deletions

File tree

src/toil_scripts/lib/programs.py

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ def docker_call(tool,
2525
outputs=None,
2626
docker_parameters=None,
2727
check_output=False,
28+
return_stderr=False,
2829
mock=None):
2930
"""
3031
Calls Docker, passing along parameters and tool.
3132
32-
:param str tool: Name of the Docker image to be used (e.g. quay.io/ucsc_cgl/samtools)
33+
:param str OR list[str] tool: Single str or list of names of the Docker images and order to be used
34+
(e.g. quay.io/ucsc_cgl/samtools)
3335
:param list[str] parameters: Command line arguments to be passed to the tool
3436
:param str work_dir: Directory to mount into the container via `-v`. Destination convention is /data
3537
:param bool rm: Set to True to pass `--rm` flag.
@@ -41,8 +43,24 @@ def docker_call(tool,
4143
or a url. The value is only used if mock=True
4244
:param dict[str,str] docker_parameters: Parameters to pass to docker
4345
:param bool check_output: When True, this function returns docker's output
46+
:param bool return_stderr: When True, this function includes stderr in docker's output
4447
:param bool mock: Whether to run in mock mode. If this variable is unset, its value will be determined by
4548
the environment variable.
49+
50+
Piping docker command examples:
51+
Running 'pipe-in-single-container' mode for command
52+
'head -c 1M </dev/urandom | tee >(md5sum 1>&2) | gzip | gunzip | md5sum 1>&2':
53+
command= ['head -c 1M </dev/urandom | tee >(md5sum 1>&2)', 'gzip -', 'gunzip -', 'md5sum - 1>&2']
54+
work_dir=curr_work_dir
55+
docker_tools=['ubuntu']
56+
stdout = docker_call(work_dir=docker_work_dir, parameters=command, tool=docker_tools, check_output=True)
57+
58+
Running 'pipe-of-containers' mode for command
59+
'head -c 1M </dev/urandom | tee >(md5sum 1>&2) | gzip | gunzip | md5sum 1>&2':
60+
command= ['head -c 1M </dev/urandom | tee >(md5sum 1>&2)', 'gzip -', 'gunzip -', 'md5sum - 1>&2']
61+
work_dir=curr_work_dir
62+
docker_tools=['ubuntu', 'ubuntu', 'ubuntu', 'ubuntu']
63+
stdout = docker_call(work_dir=docker_work_dir, parameters=command, tool=docker_tools, check_output=True)
4664
"""
4765
from toil_scripts.lib.urls import download_url
4866

@@ -83,21 +101,53 @@ def docker_call(tool,
83101
if env:
84102
for e, v in env.iteritems():
85103
base_docker_call.extend(['-e', '{}={}'.format(e, v)])
104+
86105
if docker_parameters:
87106
base_docker_call += docker_parameters
88-
89-
_log.debug("Calling docker with %s." % " ".join(base_docker_call + [tool] + parameters))
90-
91-
docker_call = base_docker_call + [tool] + parameters
92-
107+
108+
docker_call = []
109+
110+
# Pipe functionality
111+
# each element in the parameters list must represent a sub-pipe command
112+
shell_flag = False # Flag for running subprocess with string command or list command
113+
if isinstance(tool, list):
114+
command_list = []
115+
if len(tool) > 1:
116+
# If tool is a list containing multiple docker container name strings
117+
# then format the docker call in the 'pipe-of-containers' mode
118+
docker_call.extend(base_docker_call + ['--entrypoint /bin/bash', tool[0], '-c \'{}\''.format(parameters[0])])
119+
for i in xrange(1, len(tool)):
120+
docker_call.extend(['|'] + base_docker_call + ['-i --entrypoint /bin/bash', tool[i], '-c \'{}\''.format(parameters[i])])
121+
docker_call = " ".join(docker_call)
122+
shell_flag=True
123+
print("Calling docker with {}".format(docker_call))
124+
_log.debug("Calling docker with %s." % docker_call)
125+
126+
elif len(tool) == 1:
127+
# If tool is a list containing a single docker container name string
128+
# then format the docker call in the 'pipe-in-single-container' mode
129+
docker_call.extend(base_docker_call + ['--entrypoint /bin/bash', tool[0], '-c \'{}\''.format(" | ".join(parameters))])
130+
docker_call = " ".join(docker_call)
131+
shell_flag=True
132+
print("Calling docker with {}".format(docker_call))
133+
_log.debug("Calling docker with %s." % docker_call)
134+
135+
else:
136+
docker_call = base_docker_call + [tool] + parameters
137+
_log.debug("Calling docker with %s." % " ".join(docker_call))
138+
93139
try:
94140
if outfile:
95-
subprocess.check_call(docker_call, stdout=outfile)
141+
subprocess.check_call(docker_call, stdout=outfile, shell=shell_flag)
96142
else:
97143
if check_output:
98-
return subprocess.check_output(docker_call)
144+
#TODO change stderr
145+
if return_stderr:
146+
return subprocess.check_output(docker_call, shell=shell_flag, stderr=subprocess.STDOUT)
147+
else:
148+
return subprocess.check_output(docker_call, shell=shell_flag)
99149
else:
100-
subprocess.check_call(docker_call)
150+
subprocess.check_call(docker_call, shell=shell_flag)
101151
# Fix root ownership of output files
102152
except:
103153
# Panic avoids hiding the exception raised in the try block
@@ -122,5 +172,19 @@ def _fix_permissions(base_docker_call, tool, work_dir):
122172
"""
123173
base_docker_call.append('--entrypoint=chown')
124174
stat = os.stat(work_dir)
125-
command = base_docker_call + [tool] + ['-R', '{}:{}'.format(stat.st_uid, stat.st_gid), '/data']
126-
subprocess.check_call(command)
175+
command = []
176+
command_list = []
177+
if isinstance(tool, list):
178+
for i in xrange(len(tool)):
179+
command = base_docker_call + [tool[i]] + ['-R', '{}:{}'.format(stat.st_uid, stat.st_gid), '/data']
180+
command_list.append(command)
181+
else:
182+
command = base_docker_call + [tool] + ['-R', '{}:{}'.format(stat.st_uid, stat.st_gid), '/data']
183+
command_list.append(command)
184+
185+
for i in xrange(len(command_list)):
186+
subprocess.check_call(command_list[i])
187+
188+
189+
190+

src/toil_scripts/lib/test/test_programs.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import os
2-
2+
import re
33

44
def test_docker_call(tmpdir):
55
from toil_scripts.lib.programs import docker_call
@@ -12,3 +12,18 @@ def test_docker_call(tmpdir):
1212
with open(fpath, 'w') as f:
1313
docker_call(tool='ubuntu', env=dict(foo='bar'), parameters=['printenv', 'foo'], outfile=f)
1414
assert open(fpath).read() == 'bar\n'
15+
16+
# Test pipe functionality
17+
# download ubuntu docker image
18+
docker_call(work_dir=work_dir, tool="ubuntu")
19+
command = ['head -c 1G </dev/urandom | tee >(md5sum 1>&2)', 'gzip -', 'gunzip -', 'md5sum - 1>&2']
20+
# Test 'pipe-in-single-container' mode
21+
docker_tools1=['ubuntu']
22+
stdout1 = docker_call(work_dir=work_dir, parameters=command, tool=docker_tools1, check_output=True, return_stderr=True)
23+
test1 = re.findall(r"([a-fA-F\d]{32})", stdout1)
24+
assert test1[0] == test1[1]
25+
# Test 'pipe-of-containers' mode
26+
docker_tools2=['ubuntu', 'ubuntu', 'ubuntu', 'ubuntu']
27+
stdout2 = docker_call(work_dir=work_dir, parameters=command, tool=docker_tools2, check_output=True, return_stderr=True)
28+
test2 = re.findall(r"([a-fA-F\d]{32})", stdout2)
29+
assert test2[0] == test2[1]

0 commit comments

Comments
 (0)