def make_executable(path, fatal=True, logger=UNSET, dryrun=UNSET): """ Args: path (str): chmod file with 'path' as executable fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting Returns: (int): In non-fatal mode, 1: successfully done, 0: was no-op, -1: failed """ if is_executable(path): return 0 if _R.hdry(dryrun, logger, "make %s executable" % short(path)): return 1 if not os.path.exists(path): return abort("%s does not exist, can't make it executable" % short(path), return_value=-1, fatal=fatal, logger=logger) try: os.chmod(path, 0o755) # nosec _R.hlog(logger, "Made '%s' executable" % short(path)) return 1 except Exception as e: return abort("Can't chmod %s" % short(path), exc_info=e, return_value=-1, fatal=fatal, logger=logger)
def delete(path, fatal=True, logger=UNSET, dryrun=UNSET): """ Args: path (str | Path | None): Path to file or folder to delete fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting Returns: (int): In non-fatal mode, 1: successfully done, 0: was no-op, -1: failed """ path = resolved_path(path) islink = path and os.path.islink(path) if not islink and (not path or not os.path.exists(path)): return 0 if _R.hdry(dryrun, logger, "delete %s" % short(path)): return 1 try: _do_delete(path, islink, fatal) _R.hlog(logger, "Deleted %s" % short(path)) return 1 except Exception as e: return abort("Can't delete %s" % short(path), exc_info=e, return_value=-1, fatal=fatal, logger=logger)
def ensure_folder(path, clean=False, fatal=True, logger=UNSET, dryrun=UNSET): """Ensure folder with 'path' exists Args: path (str | Path | None): Path to file or folder clean (bool): True: If True, ensure folder is clean (delete any file/folder it may have) fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting Returns: (int): In non-fatal mode, >=1: successfully done, 0: was no-op, -1: failed """ path = resolved_path(path) if not path: return 0 if os.path.isdir(path): if not clean: return 0 cleaned = 0 for fname in os.listdir(path): cleaned += delete(os.path.join(path, fname), fatal=fatal, logger=None, dryrun=dryrun) if cleaned: msg = "%s from %s" % (_R.lc.rm.plural(cleaned, "file"), short(path)) if not _R.hdry(dryrun, logger, "clean %s" % msg): _R.hlog(logger, "Cleaned %s" % msg) return cleaned if _R.hdry(dryrun, logger, "create %s" % short(path)): return 1 try: os.makedirs(path) _R.hlog(logger, "Created folder %s" % short(path)) return 1 except Exception as e: return abort("Can't create folder %s" % short(path), exc_info=e, return_value=-1, fatal=fatal, logger=logger)
def hdry(cls, message, dryrun=UNSET, logger=UNSET): """Handle dryrun, allows to handle dryrun=UNSET with a code pattern of the form: if runez.log.hdry("it was a dryrun"): return Args: message (str | callable | None): Message to log dryrun (bool | UNSET | None): Optionally override current dryrun setting logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter Returns: (bool): True if we were indeed in dryrun mode, and we logged the message """ return _R.hdry(dryrun, logger, message)
def _get_response(self, method, url, fatal, logger, dryrun=False, state=None, action=None, **kwargs): """ Args: method (str): Underlying method to call url (str): Remote URL (may be absolute, or relative to self.base_url) fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting state (DataState | None): For PUT/POST requests action (str | None): Action to refer to in dryrun message (default: method) **kwargs: Passed through to underlying client Returns: (RestResponse): Response from underlying call """ absolute_url = self.full_url(url) message = "%s %s" % (action or method, absolute_url) if _R.hdry(dryrun, logger, message): return RestResponse(method, absolute_url, MockResponse(200, dict(message="dryrun %s" % message))) full_headers = self.headers headers = kwargs.get("headers") if headers: full_headers = dict(full_headers) full_headers.update(headers) keyword_args = dict(kwargs) keyword_args["headers"] = full_headers keyword_args.setdefault("timeout", self.timeout) if state is not None: state.complete(keyword_args) try: raw_response = self._protected_get(method, absolute_url, keyword_args) response = self.handler.to_rest_response(method, absolute_url, raw_response) if fatal or logger is not None: msg = response.description() if fatal and not response.ok: abort(msg, fatal=fatal, logger=logger) _R.hlog(logger, msg) return response finally: if state is not None: state.close()
def write(path, contents, fatal=True, logger=UNSET, dryrun=UNSET): """Write `contents` to file with `path` Args: path (str | Path | None): Path to file contents (str | bytes | None): Contents to write (only touch file if None) fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting Returns: (int): In non-fatal mode, 1: successfully done, 0: was no-op, -1: failed """ if not path: return 0 path = resolved_path(path) byte_size = _R.lc.rm.represented_bytesize(len(contents), unit="bytes") if contents else "" def dryrun_msg(): return "%s %s" % ("write %s to" % byte_size if byte_size else "touch", short(path)) if _R.hdry(dryrun, logger, dryrun_msg): return 1 ensure_folder(parent_folder(path), fatal=fatal, logger=None, dryrun=dryrun) try: mode = "wb" if isinstance(contents, bytes) else "wt" with io.open(path, mode) as fh: if contents is None: os.utime(path, None) else: fh.write(contents) _R.hlog(logger, "%s %s" % ("Wrote %s to" % byte_size if byte_size else "Touched", short(path))) return 1 except Exception as e: return abort("Can't write to %s" % short(path), exc_info=e, return_value=-1, fatal=fatal, logger=logger)
def _file_op(source, destination, func, overwrite, fatal, logger, dryrun, must_exist=True, ignore=None, **extra): """Call func(source, destination) Args: source (str | None): Source file or folder destination (str | None): Destination file or folder func (callable): Implementation function overwrite (bool | None): True: replace existing, False: fail if destination exists, None: no destination check fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting must_exist (bool): If True, verify that source does indeed exist ignore (callable | list | str | None): Names to be ignored **extra: Passed-through to 'func' Returns: (int): In non-fatal mode, 1: successfully done, 0: was no-op, -1: failed """ if not source or not destination or source == destination: return 0 action = func.__name__[1:] indicator = "<-" if action == "symlink" else "->" description = "%s %s %s %s" % (action, short(source), indicator, short(destination)) psource = parent_folder(source) pdest = resolved_path(destination) if psource.startswith(pdest): message = "Can't %s: source contained in destination" % description return abort(message, return_value=-1, fatal=fatal, logger=logger) if _R.hdry(dryrun, logger, description): return 1 if must_exist and not (os.path.exists(source) or os.path.islink(source)): message = "%s does not exist, can't %s to %s" % (short(source), action.lower(), short(destination)) return abort(message, return_value=-1, fatal=fatal, logger=logger) if overwrite is not None: islink = os.path.islink(pdest) if islink or os.path.exists(pdest): if not overwrite: message = "%s exists, can't %s" % (short(destination), action.lower()) return abort(message, return_value=-1, fatal=fatal, logger=logger) _do_delete(pdest, islink, fatal) try: # Ensure parent folder exists ensure_folder(parent_folder(destination), fatal=fatal, logger=None, dryrun=dryrun) _R.hlog(logger, "%s%s" % (description[0].upper(), description[1:])) if ignore is not None: if not callable(ignore): given = ignore def ignore(*_): return given extra["ignore"] = ignore func(source, destination, **extra) return 1 except Exception as e: return abort("Can't %s" % description, exc_info=e, return_value=-1, fatal=fatal, logger=logger)
def run(program, *args, background=False, fatal=True, logger=UNSET, dryrun=UNSET, short_exe=UNSET, passthrough=False, path_env=None, strip="\r\n", stdout=subprocess.PIPE, stderr=subprocess.PIPE, **popen_args): """Run 'program' with 'args' Args: program (str | pathlib.Path): Program to run (full path, or basename) *args: Command line args to call 'program' with background (bool): When True, background the spawned process (detach from console and current process) fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter dryrun (bool | UNSET | None): Optionally override current dryrun setting short_exe (str | bool | None): Try to log a compact representation of executable passthrough (bool | file | None): If True-ish, pass-through stderr/stdout in addition to capturing it as well as 'passthrough' itself if it has a write() function path_env (dict | None): Allows to inject PATH-like env vars, see `_added_env_paths()` strip (str | bool | None): If provided, `strip()` the captured output [default: strip "\n" newlines] stdout (int | IO[Any] | None): Passed-through to subprocess.Popen, [default: subprocess.PIPE] stderr (int | IO[Any] | None): Passed-through to subprocess.Popen, [default: subprocess.PIPE] **popen_args: Passed through to `subprocess.Popen` Returns: (RunResult): Run outcome, use .failed, .succeeded, .output, .error etc to inspect the outcome """ if path_env: popen_args["env"] = _added_env_paths(path_env, env=popen_args.get("env")) args = flattened(args, shellify=True) full_path = which(program) result = RunResult(audit=RunAudit(full_path or program, args, popen_args)) description = result.audit.run_description(short_exe=short_exe) if background: description += " &" abort_logger = None if logger is None else UNSET if logger is True or logger is print: # When logger is True, we just print() the message, so we may as well color it nicely description = _R.colored(description, "bold") if _R.hdry(dryrun, logger, "run: %s" % description): result.audit.dryrun = True result.exit_code = 0 if stdout is not None: result.output = "[dryrun] %s" % description # Properly simulate a successful run if stdout is not None: result.error = "" return result if not full_path: if program and os.path.basename(program) == program: result.error = "%s is not installed (PATH=%s)" % ( short(program), short(os.environ.get("PATH"))) else: result.error = "%s is not an executable" % short(program) return abort(result.error, return_value=result, fatal=fatal, logger=abort_logger) _R.hlog(logger, "Running: %s" % description) if background: child_pid = daemonize() if child_pid: result.pid = child_pid # In parent process, we just report a successful run (we don't wait/check on background process) result.exit_code = 0 return result fatal = False # pragma: no cover, non-fatal mode in background process (there is no more console etc to report anything) with _WrappedArgs([full_path] + args) as wrapped_args: try: p, out, err = _run_popen(wrapped_args, popen_args, passthrough, fatal, stdout, stderr) result.output = decode(out, strip=strip) result.error = decode(err, strip=strip) result.pid = p.pid result.exit_code = p.returncode except Exception as e: if fatal: # Don't re-wrap with an abort(), let original stacktrace show through raise result.exc_info = e if not result.error: result.error = "%s failed: %s" % ( short(program), repr(e) if isinstance(e, OSError) else e) if fatal and result.exit_code: base_message = "%s exited with code %s" % (short(program), result.exit_code) if passthrough and (result.output or result.error): exception = _R.abort_exception(override=fatal) if exception is SystemExit: raise SystemExit(result.exit_code) if isinstance(exception, type) and issubclass( exception, BaseException): raise exception(base_message) message = [] if abort_logger is not None and not passthrough: # Log full output, unless user explicitly turned it off message.append("Run failed: %s" % description) if result.error: message.append("\nstderr:\n%s" % result.error) if result.output: message.append("\nstdout:\n%s" % result.output) message.append(base_message) abort("\n".join(message), code=result.exit_code, exc_info=result.exc_info, fatal=fatal, logger=abort_logger) if background: os._exit( result.exit_code ) # pragma: no cover, simply exit forked process (don't go back to caller) return result