comparison env/lib/python3.9/site-packages/planemo/io.py @ 0:4f3585e2f14b draft default tip

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