def __init__(self, lock, venv_python): """ :param SoftLock lock: Acquired lock """ self.venv_python = venv_python self.lock = lock self.folder = lock.folder self.bin = os.path.join(self.folder, "bin") self.python = os.path.join(self.bin, "python") self._frozen = None if runez.file.is_younger(self.python, self.lock.keep): return runez.delete(self.folder) is_py2 = runez.PY2 if venv_python: is_py2 = runez.to_int(venv_python.major, default=2) < 3 if is_py2: venv = virtualenv_path() if not venv: runez.abort("Can't determine path to virtualenv.py") runez.run(self.venv_python.executable, venv, self.folder) else: runez.run(self.venv_python.executable, "-mvenv", self.folder) runez.run(self.python, "-mpip", "install", "wheel")
def create_symlinks(self, symlink, root=None, fatal=True): """ Use case: preparing a .tox/package/root folder to be packaged as a debian With a spec of "root:root/usr/local/bin", all executables produced under ./root will be symlinked to /usr/local/bin :param str symlink: A specification of the form "root:root/usr/local/bin" :param str root: Optionally, 'root' prefix if used :param bool fatal: Abort execution on failure if True :return int: 1 if effectively done, 0 if no-op, -1 on failure """ if not symlink or not self.executables: return 0 base, _, target = symlink.partition(":") if not target: return runez.abort("Invalid symlink specification '%s'", symlink, fatal=(fatal, -1)) base = runez.resolved_path(base) target = runez.resolved_path(target) for path in self.executables: if path and root: path = runez.resolved_path(root + path) if not path.startswith(base) or len(path) <= len(base): return runez.abort("Symlink base '%s' does not cover '%s'", base, path, fatal=(fatal, -1)) source = path[len(base):] basename = os.path.basename(path) destination = os.path.join(target, basename) runez.symlink(source, destination, must_exist=False, fatal=fatal, logger=LOG.info) return 1 if self.executables else 0
def package(self): """Package pypi module with 'self.name'""" if not self.version and not self.source_folder: return runez.abort( "Need either source_folder or version in order to package", fatal=(True, [])) if not self.version: setup_py = os.path.join(self.source_folder, "setup.py") if not os.path.isfile(setup_py): return runez.abort("No setup.py in %s", short(self.source_folder), fatal=(True, [])) self.version = system.run_python(setup_py, "--version", dryrun=False, fatal=False, package_name=self.name) if not self.version: return runez.abort("Could not determine version from %s", short(setup_py), fatal=(True, [])) self.pip_wheel() self.refresh_entry_points() runez.ensure_folder(self.dist_folder, folder=True) template = "{name}" if self.source_folder else "{name}-{version}" self.packaged = [] self.effective_package(template)
def install(self, force=False): """ :param bool force: If True, re-install even if package is already installed """ try: self.internal_install(force=force) except SoftLockException as e: LOG.error("%s is currently being installed by another process" % self.package_spec) runez.abort("If that is incorrect, please delete %s.lock", short(e.folder))
def run_git(target, fatal, *args): """Run git command on target, abort if command exits with error code""" error = target.git.run_raw_git_command(*args) if error.has_problems: if fatal: runez.abort(error.representation()) print(error.representation()) return 0 return 1
def required_entry_points(self): """ :return list: Entry points, abort execution if there aren't any """ ep = self.entry_points if not ep: runez.delete(system.SETTINGS.meta.full_path(self.name)) runez.abort( "'%s' is not a CLI, it has no console_scripts entry points", self.name) return ep
def get_target(path, **kwargs): """ :param str path: Path to target :param kwargs: Optional preferences """ prefs = MgitPreferences(**kwargs) actual_path = find_actual_path(path) if not actual_path or not os.path.isdir(actual_path): runez.abort("No folder '%s'" % runez.short(actual_path)) if os.path.isdir(os.path.join(actual_path, ".git")): return GitCheckout(actual_path, prefs=prefs) return ProjectDir(actual_path, prefs=prefs)
def handle_clean(target, what): if isinstance(target, GitCheckout): handle_single_clean(target, what) return if what in "remote reset": runez.abort( "Only '--clean show' and '--clean local' supported for multiple git checkouts for now" ) target.prefs.name_size = None target.prefs.set_short(True) for sub_target in target.checkouts: handle_single_clean(sub_target, what) print("----")
def clean_reset(target): """ :param GitCheckout target: Target to reset """ fallback = target.git.fallback_branch() if not fallback: runez.abort("Can't determine a branch that can be used for reset") run_git(target, True, "reset", "--hard", "HEAD") run_git(target, True, "clean", "-fdx") if fallback != target.git.branches.current: run_git(target, True, "checkout", fallback) run_git(target, True, "pull") target.git.reset_cached_properties() print(target.header())
def test_abort(logged): assert runez.abort("aborted", fatal=(False, "some-return")) == "some-return" assert "aborted" in logged.pop() assert runez.abort("aborted", fatal=(False, "some-return"), code=0) == "some-return" assert "aborted" in logged assert "ERROR" not in logged.pop() assert runez.abort("aborted", fatal=(None, "some-return")) == "some-return" assert not logged assert "stderr: oops" in runez.verify_abort(failed_function, "oops") with patch("runez.system.AbortException", side_effect=str): assert runez.abort("oops", logger=None) == "1"
def resolved(self, package_spec, default=None): """ :param system.PackageSpec package_spec: Pypi package spec :param default: Optional default value (takes precedence over system.SETTINGS.defaults only) :return: Corresponding implementation to use """ name = self.resolved_name(package_spec, default=default) name, version = system.despecced(name) if not name: runez.abort("No %s type configured for %s", self.key, package_spec) implementation = self.get(name) if not implementation: runez.abort("Unknown %s type '%s'", self.key, name) imp = implementation(package_spec) imp.implementation_version = version return imp
def internal_install(self, force=False, verbose=True): """ :param bool force: If True, re-install even if package is already installed :param bool verbose: If True, show more extensive info """ with SoftLock(self.dist_folder, timeout=system.SETTINGS.install_timeout): self.refresh_desired(force=force) if not self.desired.valid: system.setup_audit_log() return runez.abort("Can't install %s: %s", self.package_spec, self.desired.problem) if not force and self.current.equivalent(self.desired): system.inform(self.desired.representation(verbose=verbose, note="is already installed")) self.cleanup() return system.setup_audit_log() prev_entry_points = self.entry_points self.effective_install() new_entry_points = self.entry_points removed = set(prev_entry_points).difference(new_entry_points) if removed: old_removed = runez.read_json(self.removed_entry_points_path, default=[], fatal=False) removed = sorted(removed.union(old_removed)) runez.save_json(removed, self.removed_entry_points_path, fatal=False) # Delete wrapper/symlinks of removed entry points immediately for name in removed: runez.delete(system.SETTINGS.base.full_path(name)) self.cleanup() if self.package_spec.multi_named: # Clean up old installations with underscore in name runez.delete(system.SETTINGS.meta.full_path(self.package_spec.pythonified)) if not self.entry_points: runez.abort("'%s' is not a CLI, it has no console_scripts entry points", self.package_spec.dashed) self.current.set_from(self.desired) self.current.save() msg = "Would install" if runez.DRYRUN else "Installed" system.inform("%s %s" % (msg, self.desired.representation(verbose=verbose)))
def __init__(self, lock, venv_python): """ :param SoftLock lock: Acquired lock """ self.venv_python = venv_python self.lock = lock self.folder = lock.folder self.bin = os.path.join(self.folder, "bin") self.python = os.path.join(self.bin, "python") self.pip = os.path.join(self.bin, "pip") self._frozen = None if runez.is_younger(self.python, self.lock.keep): return runez.delete(self.folder) venv = system.virtualenv_path() if not venv: runez.abort("Can't determine path to virtualenv.py") runez.run(self.venv_python.executable, venv, self.folder)
def uninstall_existing(target, fatal=True): """ :param str target: Path to executable to auto-uninstall if needed :param bool|None fatal: Abort execution on failure if True :return int: 1 if successfully uninstalled, 0 if nothing to do, -1 if failed """ handler = find_uninstaller(target) if handler: return handler(target, fatal=fatal) return runez.abort("Can't automatically uninstall %s", short(target), fatal=(fatal, -1))
def version_check(programs): """Check that programs are present with a minimum version""" if not programs: runez.abort("Specify at least one program to check") specs = [] for program_spec in programs: program, _, min_version = program_spec.partition(":") min_version = Version(min_version) if not program or not min_version.is_valid: runez.abort( "Invalid argument '%s', expecting format <program>:<version>" % program_spec) specs.append((program, min_version)) overview = [] for program, min_version in specs: if runez.DRYRUN: runez.run(program, "--version") continue full_path = runez.which(program) if not full_path: runez.abort("%s is not installed" % program) r = runez.run(full_path, "--version", fatal=False, logger=None) if not r.succeeded: runez.abort("%s --version failed: %s" % (runez.short(full_path), runez.short(r.full_output))) version = Version.from_text(r.full_output) if not version or version < min_version: runez.abort("%s version too low: %s (need %s+)" % (runez.short(full_path), version, min_version)) overview.append("%s %s" % (program, version)) print(runez.short(runez.joined(overview, delimiter=" ; ")))
def package(self): """Package given python project""" if not self.desired.version and not self.source_folder: return runez.abort("Need either source_folder or version in order to package", fatal=(True, [])) if not self.desired.version: setup_py = os.path.join(self.source_folder, "setup.py") if not os.path.isfile(setup_py): return runez.abort("No setup.py in %s", short(self.source_folder), fatal=(True, [])) self.desired.version = None result = system.run_python(setup_py, "--version", dryrun=False, fatal=False, package_spec=self.package_spec) if result.succeeded: self.desired.version = result.output if not self.desired.version: return runez.abort("Could not determine version from %s", short(setup_py), fatal=(True, [])) self.pip_wheel() self.refresh_entry_points() self.packaged = [] template = "{name}" if self.source_folder else "{name}-{version}" self.effective_package(template)
def install(self, target, source): """ :param str target: Full path of executable to deliver (<base>/<entry_point>) :param str source: Path to original executable being delivered (.pickley/<package>/...) """ runez.delete(target) if runez.DRYRUN: LOG.debug("Would %s %s (source: %s)", self.implementation_name, short(target), short(source)) return if not os.path.exists(source): runez.abort("Can't %s, source %s does not exist", self.implementation_name, short(source)) try: LOG.debug("Delivering %s %s -> %s", self.implementation_name, short(target), short(source)) self._install(target, source) except Exception as e: runez.abort("Failed %s %s: %s", self.implementation_name, short(target), e)
def target_python(desired=None, package_name=None, fatal=True): """ :param str|None desired: Desired python (overrides anything else configured) :param str|None package_name: Target pypi package :param bool|None fatal: If True, abort execution if python invalid :return PythonInstallation: Python installation to use """ if not desired: desired = DESIRED_PYTHON or SETTINGS.resolved_value( "python", package_name=package_name) or INVOKER python = PythonInstallation(desired) if not python.is_valid: return runez.abort(python.problem, fatal=(fatal, python)) return python
def brew_uninstall(target, fatal=False): """ :param str target: Path of file to uninstall :param bool fatal: Abort if True :return int: 1 if successfully uninstalled, 0 if nothing to do, -1 if failed """ brew, name = find_brew_name(target) if not brew or not name: return -1 result = runez.run(brew, "uninstall", "-f", name, fatal=False, logger=LOG.info) if result.failed: # Failed brew uninstall return runez.abort("'%s uninstall %s' failed, please check", brew, name, fatal=(fatal, -1)) # All good return 1
def find_wheel(self, folder, fatal=True): """list[str]: Wheel for this package found in 'folder', if any""" if runez.DRYRUN: return ["%s-%s-py3-none-any.whl" % (self.wheelified, self.version)] result = [] if folder and os.path.isdir(folder): prefix = "%s-" % self.wheelified for fname in os.listdir(folder): if fname.startswith(prefix): result.append(os.path.join(folder, fname)) if len(result) == 1: return result[0] return runez.abort("Expecting 1 wheel, found: %s" % (result or "None"), fatal=fatal, return_value=None)
def handle_single_clean(target, what): """ :param GitCheckout target: Single checkout to clean :param str what: Operation """ report = target.git.fetch() if report.has_problems: if what != "reset": what = "clean" print( target.header( GitRunReport(report).add(problem="<can't %s" % what))) runez.abort("") if what == "reset": return clean_reset(target) if what == "show": return clean_show(target) total_cleaned = 0 print(target.header()) if what in "remote all": if not target.git.remote_cleanable_branches: print(" No remote branches can be cleaned") else: total = len(target.git.remote_cleanable_branches) cleaned = 0 for branch in target.git.remote_cleanable_branches: remote, _, name = branch.partition("/") if not remote and name: raise Exception("Unknown branch spec '%s'" % branch) if run_git(target, False, "branch", "--delete", "--remotes", branch): cleaned += run_git(target, False, "push", "--delete", remote, name) total_cleaned += cleaned if cleaned == total: print("%s cleaned" % runez.plural(cleaned, "remote branch")) else: print("%s/%s remote branches cleaned" % (cleaned, total)) target.git.reset_cached_properties() if what == "all": # Fetch to update remote branches (and correctly detect new dangling local) target.git.fetch() if what in "local all": if not target.git.local_cleanable_branches: print(" No local branches can be cleaned") else: total = len(target.git.local_cleanable_branches) cleaned = 0 for branch in target.git.local_cleanable_branches: if branch == target.git.branches.current: fallback = target.git.fallback_branch() if not fallback: print( "Skipping branch '%s', can't determine fallback branch" % target.git.branches.current) continue run_git(target, True, "checkout", fallback) run_git(target, True, "pull") cleaned += run_git(target, False, "branch", "--delete", branch) total_cleaned += cleaned if cleaned == total: print( runez.bold("%s cleaned" % runez.plural(cleaned, "local branch"))) else: print( runez.orange("%s/%s local branches cleaned" % (cleaned, total))) target.git.reset_cached_properties() if total_cleaned: print(target.header())
def failed_function(*args): with patch("runez.system.logging.root") as root: root.handlers = None runez.abort(*args)
def test_abort(logged, monkeypatch): assert runez.abort("aborted", fatal=False) is None assert "aborted" in logged.pop() assert runez.abort("aborted", fatal=None) is None assert not logged assert runez.abort("aborted", return_value="some-return", fatal=False) == "some-return" assert "ERROR" in logged assert "aborted" in logged.pop() # User wants their own logger called assert runez.abort("aborted", return_value="some-return", fatal=False, logger=logging.debug) == "some-return" assert "ERROR" not in logged assert "DEBUG" in logged assert "aborted" in logged.pop() assert runez.abort("aborted", return_value="some-return", fatal=False, logger=print) == "some-return" assert not logged.stderr assert logged.pop().strip() == "aborted" def on_log(message): print(message) assert runez.abort("aborted", return_value="some-return", fatal=False, logger=on_log, exc_info=Exception("oops")) == "some-return" assert not logged.stderr assert logged.pop().strip() == "aborted: oops" assert runez.abort("aborted", return_value="some-return", fatal=False, logger=on_log) == "some-return" assert not logged.stderr assert logged.pop().strip() == "aborted" assert runez.abort("aborted", return_value="some-return", fatal=None) == "some-return" assert not logged monkeypatch.setattr(runez.system.logging.root, "handlers", []) with pytest.raises(Exception): # logger is UNSET -> log failure runez.abort("oops") assert "oops" in logged.pop() with pytest.raises(Exception) as exc: # Message not logged, but part of raised exception runez.abort("oops", logger=None) assert "oops" in str(exc) assert not logged with pytest.raises(SystemExit): # Failure logged anyway due to sys.exit() runez.abort("oops", fatal=SystemExit, logger=None) assert "oops" in logged.pop() # Verify we still log failure when we're about to sys.exit(), even when logger given is explicitly None monkeypatch.setattr(runez.system, "AbortException", SystemExit) with pytest.raises(SystemExit): runez.abort("oops", logger=None) assert "oops" in logged.pop()