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 |
