Mercurial > repos > shellac > sam_consensus_v3
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 |