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 which(program, ignore_own_venv=False): """ Args: program (str | pathlib.Path | None): Program name to find via env var PATH ignore_own_venv (bool): If True, do not resolve to executables in current venv Returns: (str | None): Full path to program, if one exists and is executable """ if not program: return None program = str(program) if os.path.basename(program) != program: program = resolved_path(program) if SYS_INFO.platform_id.is_windows: # pragma: no cover return _windows_exe(program) return program if is_executable(program) else None for p in os.environ.get("PATH", "").split(os.pathsep): fp = os.path.join(p, program) if SYS_INFO.platform_id.is_windows: # pragma: no cover fp = _windows_exe(fp) if fp and (not ignore_own_venv or not SYS_INFO.venv_bin_folder or not fp.startswith(SYS_INFO.venv_bin_folder)): if is_executable(fp): return fp program = os.path.join(os.getcwd(), program) if is_executable(program): return program return None
def readlines(path, first=None, errors="ignore", fatal=False, logger=False): """ Args: path (str | Path | None): Path to file to read lines from first (int | None): Return only the 'first' lines when specified errors (str | None): Optional string specifying how encoding errors are to be handled 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 Yields: (str): Lines read, newlines and trailing spaces stripped """ try: with io.open(resolved_path(path), errors=errors) as fh: if not first: first = -1 for line in fh: if first == 0: return yield decode(line).rstrip() first -= 1 except Exception as e: message = "Can't read %s" % short(path) if fatal: abort(_R.actual_message(message), exc_info=e, fatal=fatal, logger=logger) _R.hlog(logger, message, exc_info=e)
def python_exes_in_folder(path, version=None): """ Args: path (pathlib.Path | str): Path to python exe or folder with a python installation version (Version | None): Optional, major/minor version to search for Returns: Yields all python executable names """ if path: path = resolved_path(path) if os.path.isdir(path): bin_folder = os.path.join(path, "bin") if os.path.isdir(bin_folder): path = bin_folder candidates = [] if version and version.given_components: if len(version.given_components) > 1: candidates.append("python%s.%s" % (version.major, version.minor)) candidates.append("python%s" % version.major) else: candidates.append("python3") candidates.append("python") for name in candidates: candidate = os.path.join(path, name) if is_executable(candidate): yield candidate elif is_executable(path): yield path
def parent_folder(path, base=None): """Parent folder of `path`, relative to `base` Args: path (str | Path | None): Path to file or folder base (str | None): Base folder to use for relative paths (default: current working dir) Returns: (str): Absolute path of parent folder """ return path and os.path.dirname(resolved_path(path, base=base))
def python_from_path(self, path): if path: short_name = str(path) spec = self.spec_from_path(path, family=short_name) if spec: exes = list(PythonDepot.python_exes_in_folder(path)) problem = None if exes else "invalid python installation" exes.append(resolved_path(path)) return PythonInstallation(exes[0], spec, equivalents=exes, problem=problem, short_name=short_name)
def __init__(self, text, family=None): """ Args: text: Text describing desired python (note: an empty or None `text` will yield a generic "cpython:" spec) family (str | None): Additional text to examine to determine python family """ text = stringified(text, none="").strip() self.text = text if not text or text == "invoker": self.family = guess_family(family or sys.version) self.canonical = "invoker" self.version = get_current_version() return if _is_path(text): self.family = guess_family(family or text) self.canonical = resolved_path(text) return m = _R.lc.rx_spec.match(text) if not m: m = _R.lc.rx_family.match(text) if m: self.family = guess_family(family or m.group(1)) self.canonical = "%s:" % self.family else: self.canonical = "?%s" % text return version_text = m.group(3) if version_text and version_text.endswith("+"): self.is_min_spec = "+" version_text = version_text[:-1] if version_text: if len(version_text) > 1 and "." not in version_text: version_text = "%s.%s" % (version_text[0], version_text[1:]) self.version = Version.from_text(version_text, strict=True) if self.version: self.family = guess_family(family or m.group(1)) self.canonical = "%s:%s%s" % (self.family, self.version or "", self.is_min_spec) else: self.canonical = "?%s" % text
def ask_once(name, instructions, default=None, base="~/.config", serializer=stringified, fatal=False, logger=False): """ Args: name (str): Name under which to store provided answer (will be stored in ~/.config/<name>.json) instructions (str): Instructions to show to user when prompt is necessary default: Default value to return if answer not available base (str): Base folder where to stored provided answer serializer (callable): Function that will turn provided value into object to be stored logger (callable | bool | None): Logger to use, True to print(), False to trace(), None to disable log chatter fatal (type | bool | None): True: abort execution on failure, False: don't abort but log, None: don't abort, don't log Returns: Value given by user (or 'default' if given), optionally wrapped via `serializer` """ path = resolved_path(name, base=base) if not path.endswith(".json"): path += ".json" existing = read_json(path, logger=logger) if existing is not None: return existing if not SYS_INFO.terminal.is_stdout_tty: return _R.habort(default, fatal, logger, "Can't prompt for %s, not on a tty" % name) try: provided = input(instructions) if provided: value = serializer(provided) if value is not None: save_json(value, path, fatal=fatal, logger=logger) return value return _R.habort(default, fatal, logger, "Invalid value provided for %s" % name) return _R.habort(default, fatal, logger, "No value provided for %s" % name) except KeyboardInterrupt: return _R.habort(default, fatal, logger, "Cancelled by user")
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 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)