comparison env/lib/python3.7/site-packages/planemo/io.py @ 0:26e78fe6e8c4 draft

"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author shellac
date Sat, 02 May 2020 07:14:21 -0400
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:26e78fe6e8c4
1 from __future__ import absolute_import
2 from __future__ import print_function
3
4 import contextlib
5 import errno
6 import fnmatch
7 import os
8 import shutil
9 import subprocess
10 import sys
11 import tempfile
12 import time
13 from sys import platform as _platform
14 from xml.sax.saxutils import escape
15
16 import click
17 from galaxy.tool_util.deps import commands
18 from galaxy.tool_util.deps.commands import download_command
19 from six import (
20 string_types,
21 StringIO
22 )
23
24 from .exit_codes import (
25 EXIT_CODE_NO_SUCH_TARGET,
26 EXIT_CODE_OK,
27 )
28
29
30 IS_OS_X = _platform == "darwin"
31
32
33 def args_to_str(args):
34 if args is None or isinstance(args, string_types):
35 return args
36 else:
37 return commands.argv_to_str(args)
38
39
40 def communicate(cmds, **kwds):
41 cmd_string = args_to_str(cmds)
42 info(cmd_string)
43 p = commands.shell_process(cmds, **kwds)
44 if kwds.get("stdout", None) is None and commands.redirecting_io(sys=sys):
45 output = commands.redirect_aware_commmunicate(p)
46 else:
47 output = p.communicate()
48
49 if p.returncode != 0:
50 template = "Problem executing commands {0} - ({1}, {2})"
51 msg = template.format(cmd_string, output[0], output[1])
52 raise RuntimeError(msg)
53 return output
54
55
56 def shell(cmds, **kwds):
57 cmd_string = args_to_str(cmds)
58 info(cmd_string)
59 return commands.shell(cmds, **kwds)
60
61
62 def info(message, *args):
63 if args:
64 message = message % args
65 click.echo(click.style(message, bold=True, fg='green'))
66
67
68 def can_write_to_path(path, **kwds):
69 if not kwds["force"] and os.path.exists(path):
70 error("%s already exists, exiting." % path)
71 return False
72 return True
73
74
75 def error(message, *args):
76 if args:
77 message = message % args
78 click.echo(click.style(message, bold=True, fg='red'), err=True)
79
80
81 def warn(message, *args):
82 if args:
83 message = message % args
84 click.echo(click.style(message, fg='red'), err=True)
85
86
87 def shell_join(*args):
88 """Join potentially empty commands together with '&&'."""
89 return " && ".join(args_to_str(_) for _ in args if _)
90
91
92 def write_file(path, content, force=True):
93 if os.path.exists(path) and not force:
94 return
95
96 with open(path, "w") as f:
97 f.write(content)
98
99
100 def untar_to(url, tar_args=None, path=None, dest_dir=None):
101 if tar_args:
102 assert not (path and dest_dir)
103 if dest_dir:
104 if not os.path.exists(dest_dir):
105 os.makedirs(dest_dir)
106 tar_args[0:0] = ['-C', dest_dir]
107 if path:
108 tar_args.insert(0, '-O')
109
110 download_cmd = download_command(url)
111 download_p = commands.shell_process(download_cmd, stdout=subprocess.PIPE)
112 untar_cmd = ['tar'] + tar_args
113 if path:
114 with open(path, 'wb') as fh:
115 shell(untar_cmd, stdin=download_p.stdout, stdout=fh)
116 else:
117 shell(untar_cmd, stdin=download_p.stdout)
118 download_p.wait()
119 else:
120 cmd = download_command(url, to=path)
121 shell(cmd)
122
123
124 def find_matching_directories(path, pattern, recursive):
125 """Find directories below supplied path with file matching pattern.
126
127 Returns an empty list if no matches are found, and if recursive is False
128 only the top directory specified by path will be considered.
129 """
130 dirs = []
131 if recursive:
132 if not os.path.isdir(path):
133 template = "--recursive specified with non-directory path [%s]"
134 message = template % (path)
135 raise Exception(message)
136
137 for base_path, dirnames, filenames in os.walk(path):
138 dirnames.sort()
139 for filename in fnmatch.filter(filenames, pattern):
140 dirs.append(base_path)
141 else:
142 if os.path.exists(os.path.join(path, pattern)):
143 dirs.append(path)
144 elif os.path.basename(path) == pattern:
145 dirs.append(os.path.dirname(path))
146 return dirs
147
148
149 @contextlib.contextmanager
150 def real_io():
151 """Ensure stdout and stderr have supported ``fileno()`` method.
152
153 nosetests replaces these streams with :class:`StringIO` objects
154 that may not work the same in every situtation - :func:`subprocess.Popen`
155 calls in particular.
156 """
157 original_stdout = sys.stdout
158 original_stderr = sys.stderr
159 try:
160 if commands.redirecting_io(sys=sys):
161 sys.stdout = sys.__stdout__
162 sys.stderr = sys.__stderr__
163 yield
164 finally:
165 sys.stdout = original_stdout
166 sys.stderr = original_stderr
167
168
169 @contextlib.contextmanager
170 def temp_directory(prefix="planemo_tmp_", dir=None, **kwds):
171 if dir is not None:
172 try:
173 os.makedirs(dir)
174 except OSError as e:
175 if e.errno != errno.EEXIST:
176 raise
177 temp_dir = tempfile.mkdtemp(prefix=prefix, dir=dir, **kwds)
178 try:
179 yield temp_dir
180 finally:
181 shutil.rmtree(temp_dir)
182
183
184 def ps1_for_path(path, base="PS1"):
185 """ Used by environment commands to build a PS1 shell
186 variables for tool or directory of tools.
187 """
188 file_name = os.path.basename(path)
189 base_name = os.path.splitext(file_name)[0]
190 ps1 = "(%s)${%s}" % (base_name, base)
191 return ps1
192
193
194 def kill_pid_file(pid_file):
195 try:
196 os.stat(pid_file)
197 except OSError as e:
198 if e.errno == errno.ENOENT:
199 return False
200
201 with open(pid_file, "r") as fh:
202 pid = int(fh.read())
203 kill_posix(pid)
204 try:
205 os.unlink(pid_file)
206 except Exception:
207 pass
208
209
210 def kill_posix(pid):
211 def _check_pid():
212 try:
213 os.kill(pid, 0)
214 return True
215 except OSError:
216 return False
217
218 if _check_pid():
219 for sig in [15, 9]:
220 try:
221 os.kill(pid, sig)
222 except OSError:
223 return
224 time.sleep(1)
225 if not _check_pid():
226 return
227
228
229 @contextlib.contextmanager
230 def conditionally_captured_io(capture, tee=False):
231 captured_std = []
232 if capture:
233 with Capturing() as captured_std:
234 yield captured_std
235 if tee:
236 tee_captured_output(captured_std)
237 else:
238 yield
239
240
241 @contextlib.contextmanager
242 def captured_io_for_xunit(kwds, captured_io):
243 captured_std = []
244 with_xunit = kwds.get('report_xunit', False)
245 with conditionally_captured_io(with_xunit, tee=True):
246 time1 = time.time()
247 yield
248 time2 = time.time()
249
250 if with_xunit:
251 stdout = [escape(m['data']) for m in captured_std
252 if m['logger'] == 'stdout']
253 stderr = [escape(m['data']) for m in captured_std
254 if m['logger'] == 'stderr']
255 captured_io["stdout"] = stdout
256 captured_io["stderr"] = stderr
257 captured_io["time"] = (time2 - time1)
258 else:
259 captured_io["stdout"] = None
260 captured_io["stderr"] = None
261 captured_io["time"] = None
262
263
264 class Capturing(list):
265 """Function context which captures stdout/stderr
266
267 This keeps planemo's codebase clean without requiring planemo to hold onto
268 messages, or pass user-facing messages back at all. This could probably be
269 solved by swapping planemo entirely to a logger and reading from/writing
270 to that, but this is easier.
271
272 This swaps sys.std{out,err} with StringIOs and then makes that output
273 available.
274 """
275 # http://stackoverflow.com/a/16571630
276
277 def __enter__(self):
278 self._stdout = sys.stdout
279 self._stderr = sys.stderr
280 sys.stdout = self._stringio_stdout = StringIO()
281 sys.stderr = self._stringio_stderr = StringIO()
282 return self
283
284 def __exit__(self, *args):
285 self.extend([{'logger': 'stdout', 'data': x} for x in
286 self._stringio_stdout.getvalue().splitlines()])
287 self.extend([{'logger': 'stderr', 'data': x} for x in
288 self._stringio_stderr.getvalue().splitlines()])
289
290 sys.stdout = self._stdout
291 sys.stderr = self._stderr
292
293
294 def tee_captured_output(output):
295 """For messages captured with Capturing, send them to their correct
296 locations so as to not interfere with normal user experience.
297 """
298 for message in output:
299 # Append '\n' due to `splitlines()` above
300 if message['logger'] == 'stdout':
301 sys.stdout.write(message['data'] + '\n')
302 if message['logger'] == 'stderr':
303 sys.stderr.write(message['data'] + '\n')
304
305
306 def wait_on(function, desc, timeout=5, polling_backoff=0):
307 """Wait on given function's readiness. Grow the polling
308 interval incrementally by the polling_backoff."""
309 delta = .25
310 timing = 0
311 while True:
312 if timing > timeout:
313 message = "Timed out waiting on %s." % desc
314 raise Exception(message)
315 timing += delta
316 delta += polling_backoff
317 value = function()
318 if value is not None:
319 return value
320 time.sleep(delta)
321
322
323 @contextlib.contextmanager
324 def open_file_or_standard_output(path, *args, **kwds):
325 if path == "-":
326 yield sys.stdout
327 else:
328 yield open(path, *args, **kwds)
329
330
331 def filter_paths(paths, cwd=None, **kwds):
332 if cwd is None:
333 cwd = os.getcwd()
334
335 def norm(path):
336 if not os.path.isabs(path):
337 path = os.path.join(cwd, path)
338 return os.path.normpath(path)
339
340 def exclude_func(exclude_path):
341 def path_startswith(p):
342 """Check that p starts with exclude_path and that the first
343 character of p not included in exclude_path (if any) is the
344 directory separator.
345 """
346 norm_p = norm(p)
347 norm_exclude_path = norm(exclude_path)
348 if norm_p.startswith(norm_exclude_path):
349 return norm_p[len(norm_exclude_path):len(norm_exclude_path) + 1] in ['', os.sep]
350 return False
351 return path_startswith
352
353 filters_as_funcs = []
354 filters_as_funcs.extend(map(exclude_func, kwds.get("exclude", [])))
355
356 for exclude_paths_ins in kwds.get("exclude_from", []):
357 with open(exclude_paths_ins, "r") as f:
358 for line in f.readlines():
359 line = line.strip()
360 if not line or line.startswith("#"):
361 continue
362 filters_as_funcs.append(exclude_func(line))
363
364 return [p for p in paths if not any(f(p) for f in filters_as_funcs)]
365
366
367 def coalesce_return_codes(ret_codes, assert_at_least_one=False):
368 # Return 0 if everything is fine, otherwise pick the least
369 # specific non-0 return code - preferring to report errors
370 # to other non-0 exit codes.
371 if assert_at_least_one and len(ret_codes) == 0:
372 return EXIT_CODE_NO_SUCH_TARGET
373
374 coalesced_return_code = EXIT_CODE_OK
375 for ret_code in ret_codes:
376 # None is equivalent to 0 in these methods.
377 ret_code = 0 if ret_code is None else ret_code
378 if ret_code == 0:
379 # Everything is fine, keep moving...
380 pass
381 elif coalesced_return_code == 0:
382 coalesced_return_code = ret_code
383 # At this point in logic both ret_code and coalesced_return_code are
384 # are non-zero
385 elif ret_code < 0:
386 # Error state, this should override eveything else.
387 coalesced_return_code = ret_code
388 elif ret_code > 0 and coalesced_return_code < 0:
389 # Keep error state recorded.
390 pass
391 elif ret_code > 0:
392 # Lets somewhat arbitrarily call the smaller exit code
393 # the less specific.
394 coalesced_return_code = min(ret_code, coalesced_return_code)
395
396 if coalesced_return_code < 0:
397 # Map -1 => 254, -2 => 253, etc...
398 # Not sure it is helpful to have negative error codes
399 # this was a design and API mistake in planemo.
400 coalesced_return_code = 255 + coalesced_return_code
401
402 return coalesced_return_code