def auto_upgrade(force, package): """ Auto-upgrade a package """ package = system.PackageSpec(package) p = PACKAGERS.resolved(package) if not p.current.valid: sys.exit("%s is not currently installed" % package) ping = system.SETTINGS.meta.full_path(package.dashed, ".ping") if not force and runez.file.is_younger( ping, system.SETTINGS.version_check_seconds): # We checked for auto-upgrade recently, no need to check again yet print("Skipping auto-upgrade, checked recently") sys.exit(0) runez.touch(ping) try: p.internal_install() except SoftLockException: print( "Skipping auto-upgrade, %s is currently being installed by another process" % package) sys.exit(0)
def check(force, verbose, packages): """ Check whether specified packages need an upgrade """ code = 0 packages = system.SETTINGS.resolved_packages( packages) or system.installed_names() if not packages: print("No packages installed") else: for name in packages: p = PACKAGERS.resolved(name) p.refresh_desired(force=force) if not p.desired.valid: LOG.error(p.desired.representation(verbose)) code = 1 elif not p.current.version or not p.current.valid: print( p.desired.representation(verbose, note="is not installed")) code = 1 elif p.current.version != p.desired.version: print( p.current.representation(verbose, note="can be upgraded to %s" % p.desired.version)) code = 1 else: print(p.current.representation(verbose, note="is installed")) sys.exit(code)
def install(force, packages): """ Install a package from pypi """ packages = system.SETTINGS.resolved_packages(packages) for name in packages: p = PACKAGERS.resolved(name) p.install(force=force)
def package(build, dist, symlink, relocatable, sanity_check, folder): """ Package a project from source checkout """ build = runez.resolved_path(build) root = None target_dist = dist if target_dist.startswith("root/"): # Special case: we're targeting 'root/...' probably for a debian, use target in that case to avoid venv relocation issues target = target_dist[4:] if os.path.isdir(target): root = target_dist[:4] target_dist = target LOG.debug("debian mode: %s -> %s", dist, target) folder = runez.resolved_path(folder) if not os.path.isdir(folder): sys.exit("Folder %s does not exist" % short(folder)) system.SETTINGS.set_base(build) setup_py = os.path.join(folder, "setup.py") if not os.path.exists(setup_py): sys.exit("No setup.py in %s" % short(folder)) with runez.CurrentFolder(folder): # Some setup.py's assume their working folder is the folder where they're in result = system.run_python(setup_py, "--name", fatal=False, dryrun=False) name = result.output if result.failed or not name: sys.exit("Could not determine package name from %s" % short(setup_py)) package_spec = system.PackageSpec(name) runez.Anchored.add(folder) p = PACKAGERS.resolved(package_spec) p.build_folder = build p.dist_folder = runez.resolved_path(target_dist) p.relocatable = relocatable p.source_folder = folder p.package() p.create_symlinks(symlink, root=root) p.sanity_check(sanity_check) if p.executables: overview = "produced: %s" % runez.quoted(p.executables) else: overview = "package has no entry-points" print("Packaged %s successfully, %s" % (short(folder), overview)) runez.Anchored.pop(folder)
def install(force, packages): """ Install a package from pypi """ system.setup_audit_log() packages = system.resolved_package_specs(packages) for name in packages: p = PACKAGERS.resolved(name) p.install(force=force)
def test_channel(*_): p = PACKAGERS.get(system.VENV_PACKAGER)("foo") p.refresh_desired() assert p.desired.representation( verbose=True ) == "foo 1.0 (as venv wrap, channel: stable, source: test:channel.stable.foo)" with runez.CaptureOutput(dryrun=True) as logged: p.executables = ["foo/bar"] assert p.create_symlinks("foo:baz", fatal=False) == 1 assert "Would symlink /bar <- baz/bar" in logged.pop()
def list(verbose): """ List installed packages """ packages = system.installed_names() if not packages: print("No packages installed") else: for name in packages: p = PACKAGERS.resolved(name) print(p.current.representation(verbose))
def list(verbose): """ List installed packages """ packages = system.resolved_package_specs(None, auto_complete=True) if not packages: print("No packages installed") else: for name in packages: p = PACKAGERS.resolved(name) print(p.current.representation(verbose))
def uninstall(force, packages): """ Uninstall packages """ packages = system.SETTINGS.resolved_packages(packages) errors = 0 for name in packages: p = PACKAGERS.resolved(name) if not force and not p.current.file_exists: errors += 1 LOG.error("%s was not installed with pickley", name) continue eps = p.entry_points ep_uninstalled = 0 ep_missed = 0 meta_deleted = runez.delete(system.SETTINGS.meta.full_path(name), fatal=False) if not eps and force: eps = {name: ""} if eps and meta_deleted >= 0: for entry_point in eps: path = system.SETTINGS.base.full_path(entry_point) handler = runez.delete if meta_deleted > 0 else uninstall_existing r = handler(path, fatal=False) if r < 0: ep_missed += 1 elif r > 0: ep_uninstalled += 1 if ep_missed or meta_deleted < 0: # Error was already reported errors += 1 continue if ep_uninstalled + meta_deleted == 0: system.inform("Nothing to uninstall for %s" % name) continue message = "Would uninstall" if runez.DRYRUN else "Uninstalled" message = "%s %s" % (message, name) if ep_uninstalled > 1: message += " (%s entry points)" % ep_uninstalled system.inform(message) if errors: sys.exit(1)
def test_delivery(temp_base): # Test copy folder tox = system.PackageSpec("tox") deliver = DELIVERERS.get("copy")(tox) target = os.path.join(temp_base, "t1") source = os.path.join(temp_base, "t1-source") source_file = os.path.join(source, "foo") runez.touch(source_file) deliver.install(target, source) assert os.path.isdir(target) assert os.path.isfile(os.path.join(target, "foo")) # Test copy file deliver = DELIVERERS.get("copy")(tox) target = os.path.join(temp_base, "t2") source = os.path.join(temp_base, "t2-source") runez.touch(source) deliver.install(target, source) assert os.path.isfile(target) # Test symlink deliver = DELIVERERS.get("symlink")(tox) target = os.path.join(temp_base, "l2") source = os.path.join(temp_base, "l2-source") runez.touch(source) deliver.install(target, source) assert os.path.islink(target) # Test wrapper p = PACKAGERS.get(system.VENV_PACKAGER)(tox) assert p.create_symlinks(None) == 0 assert p.create_symlinks("foo", fatal=False) == 0 p.executables = ["foo"] assert p.create_symlinks("foo:bar", fatal=False) == -1 assert str(p) == "venv tox" target = os.path.join(temp_base, "tox") source = os.path.join(temp_base, "tox-source") runez.touch(source) deliver = DELIVERERS.get("wrap")(system.PackageSpec("tox")) deliver.install(target, source) assert runez.is_executable(target)
def package(build, dist, symlink, relocatable, sanity_check, folder): """ Package a project from source checkout """ build = runez.resolved_path(build) dist = runez.resolved_path(dist) folder = runez.resolved_path(folder) system.SETTINGS.meta = meta_folder(build) if not os.path.isdir(folder): sys.exit("Folder %s does not exist" % short(folder)) setup_py = os.path.join(folder, "setup.py") if not os.path.exists(setup_py): sys.exit("No setup.py in %s" % short(folder)) with runez.CurrentFolder(folder): # Some setup.py's assume their working folder is the folder where they're in name = system.run_python(setup_py, "--name", fatal=False, dryrun=False) if not name: sys.exit("Could not determine package name from %s" % short(setup_py)) runez.Anchored.add(folder) p = PACKAGERS.resolved(name) p.build_folder = build p.dist_folder = dist p.relocatable = relocatable p.source_folder = folder p.package() p.create_symlinks(symlink) p.sanity_check(sanity_check) print("Packaged %s successfully, produced: %s" % (short(folder), runez.represented_args(p.executables))) runez.Anchored.pop(folder)
def test_special_packages(*_): p = PACKAGERS.get("venv")("awscli") assert p.get_entry_points() == {"aws": "scripts.aws:main"}
def test_versions(_, __, temp_base): p = PACKAGERS.get("pex")("foo") p.pip_wheel = lambda *_: None assert p.specced_command() == "pex" p.implementation_version = "1.4.5" assert p.specced_command() == "pex==1.4.5" p.refresh_desired() assert p.desired.representation( verbose=True ) == "foo: can't determine latest version from pypi (channel: latest, source: pypi)" with patch("pickley.package.latest_pypi_version", return_value="error: test failed"): p.refresh_desired() assert p.desired.representation() == "foo: test failed" system.SETTINGS.cli.contents["channel"] = "stable" p.refresh_desired() assert p.desired.representation() == "foo: can't determine stable version" p.version = None assert "can't determine stable version" in verify_abort(p.install) # Without a build folder assert p.get_entry_points() is None # With an empty build fodler runez.ensure_folder(p.build_folder, folder=True) assert p.get_entry_points() is None # With a bogus wheel with runez.CaptureOutput() as logged: p.version = "0.0.0" whl = os.path.join(p.build_folder, "foo-0.0.0-py2.py3-none-any.whl") runez.touch(whl) assert p.get_entry_points() is None assert "Can't read wheel" in logged runez.delete(whl) p.refresh_desired() assert p.desired.channel == "adhoc" assert p.desired.source == "cli" assert p.desired.version == "0.0.0" assert not p.desired.problem p.version = None p.refresh_desired() assert p.desired.problem # Ambiguous package() call assert "Need either source_folder or version in order to package" in verify_abort( p.package) # Package bogus folder without a setup.py p.source_folder = temp_base assert "No setup.py" in verify_abort(p.package) # Package with a bogus setup.py setup_py = os.path.join(temp_base, "setup.py") runez.touch(setup_py) assert "Could not determine version" in verify_abort(p.package) # Provide a minimal setup.py runez.write( setup_py, "from setuptools import setup\nsetup(name='foo', version='0.0.0')\n") # Package project without entry points p.get_entry_points = lambda *_: None assert "is not a CLI" in verify_abort(p.required_entry_points)
metavar="PATH", help="Base installation folder to use (default: folder containing pickley)" ) @click.option("--index", "-i", metavar="PATH", help="Pypi index to use") @click.option("--config", "-c", metavar="PATH", help="Extra config to load") @click.option("--python", "-P", metavar="PATH", help="Python interpreter to use") @click.option("--delivery", "-d", type=click.Choice(DELIVERERS.names()), help="Delivery method to use") @click.option("--packager", "-p", help="Packager to use (one of: %s)" % ",".join(PACKAGERS.names()) ) def main(ctx, debug, dryrun, base, index, config, python, delivery, packager): """ Package manager for python CLIs """ if any(opt in sys.argv for opt in ctx.help_option_names): # pragma: no cover return if dryrun: debug = True if base: base = runez.resolved_path(base) if not os.path.exists(base):
def uninstall(all, force, packages): """ Uninstall packages """ if packages and all: sys.exit( "Either specify packages to uninstall, or --all (but not both)") if not packages and not all: sys.exit("Specify packages to uninstall, or --all") if packages and system.PICKLEY in packages: sys.exit( "Run 'uninstall --all' if you wish to uninstall pickley itself (and everything it installed)" ) system.setup_audit_log() package_specs = system.resolved_package_specs(packages, auto_complete=all) errors = 0 for package_spec in package_specs: p = PACKAGERS.resolved(package_spec) if not force and not p.current.file_exists: errors += 1 LOG.error("%s was not installed with pickley", package_spec) continue eps = p.entry_points ep_uninstalled = 0 ep_missed = 0 meta_deleted = runez.delete(system.SETTINGS.meta.full_path( package_spec.dashed), fatal=False) if not eps and force: eps = {package_spec.dashed: ""} if eps and meta_deleted >= 0: for entry_point in eps: path = system.SETTINGS.base.full_path(entry_point) handler = runez.delete if meta_deleted > 0 else uninstall_existing r = handler(path, fatal=False) if r < 0: ep_missed += 1 elif r > 0: ep_uninstalled += 1 if ep_missed or meta_deleted < 0: # Error was already reported errors += 1 continue if ep_uninstalled + meta_deleted == 0: system.inform("Nothing to uninstall for %s" % package_spec) continue message = "Would uninstall" if runez.DRYRUN else "Uninstalled" message = "%s %s" % (message, package_spec) if ep_uninstalled > 1: message += " (%s entry points)" % ep_uninstalled system.inform(message) if all and not errors: runez.delete(system.SETTINGS.meta.path) if errors: sys.exit(1)
def test_install(cli): tox = system.SETTINGS.base.full_path("tox") p = PACKAGERS.resolved("tox") p.refresh_desired() tox_version = p.desired.version assert not os.path.exists(tox) assert runez.first_line(tox) is None cli.expect_success("--dryrun -b{base} --delivery wrap install tox", "Would wrap", "Would install tox", base=cli.context) cli.expect_success("--dryrun -b{base} --delivery symlink install tox", "Would symlink", "Would install tox", base=cli.context) cli.expect_failure("--dryrun -b{base} --delivery foo install tox", "invalid choice: foo", base=cli.context) cli.expect_success("--dryrun uninstall /dev/null --force", "Nothing to uninstall") runez.touch("foo") assert os.path.exists("foo") cli.expect_failure("uninstall foo", "foo was not installed with pickley") cli.expect_success("uninstall foo --force", "Uninstalled foo") assert not os.path.exists("foo") assert runez.ensure_folder("foo", folder=True) == 1 cli.expect_failure("uninstall foo --force", "Can't automatically uninstall") cli.expect_failure("-b{base} check tox foo/bar", "is not installed", "can't determine latest version", base=cli.context) cli.expect_failure("-b{base} install six", "'six' is not a CLI", base=cli.context) # Install tox, but add a few files + a bogus previous entry point to test cleanup wep1 = system.SETTINGS.base.full_path("tox-old-entrypoint1") tep10 = system.SETTINGS.meta.full_path("tox", "tox-old-entrypoint1-1.0") tep11 = system.SETTINGS.meta.full_path("tox", "tox-old-entrypoint1-1.1") t00 = system.SETTINGS.meta.full_path("tox", "tox-0.0.0") tfoo = system.SETTINGS.meta.full_path("tox", "tox-foo") runez.touch(wep1) runez.touch(tep10) runez.touch(tep11) runez.touch(t00) runez.touch(tfoo) eppath = system.SETTINGS.meta.full_path("tox", ".entry-points.json") runez.write(eppath, '["tox-old-entrypoint1", "tox-old-entrypoint2"]\n') cli.expect_success("-b{base} --delivery wrap install tox", "Installed tox", base=cli.context) # Old entry point removed immediately assert not os.path.exists(wep1) # Only 1 cleaned up immediately (latest + 1 kept) assert not os.path.exists(tep10) assert os.path.exists(tep11) assert not os.path.exists(t00) assert os.path.exists(tfoo) assert runez.is_executable(tox) output = run_program(tox, "--version") assert "tox" in output assert tox_version in output cli.expect_success("-b{base} auto-upgrade tox", "Skipping auto-upgrade", base=cli.context) runez.delete(system.SETTINGS.meta.full_path("tox", ".ping")) cli.expect_success("-b{base} auto-upgrade tox", "already installed", base=cli.context) version = output.partition(" ")[0] cli.expect_success("copy .pickley/tox/tox-%s tox-copy" % version, "Copied") cli.expect_success("move tox-copy tox-relocated", "Moved") # Verify that older versions and removed entry-points do get cleaned up runez.save_json({"install_timeout": 0}, "custom-timeout.json") cli.expect_success("-b{base} -ccustom-timeout.json install tox", "already installed", base=cli.context) # All cleaned up when enough time went by assert not os.path.exists(tep10) assert not os.path.exists(tep11) assert not os.path.exists(t00) assert not os.path.exists(tfoo) cli.expect_success("-b{base} check", "tox", "is installed", base=cli.context) cli.expect_success( "-b{base} check --verbose", "tox", "is installed (as %s wrap, channel: " % system.VENV_PACKAGER, base=cli.context, ) p = PACKAGERS.get(system.VENV_PACKAGER)("tox") p.refresh_latest() p.latest.version = "10000.0" p.latest.save() cli.expect_failure("-b{base} check", "tox", "can be upgraded to 10000.0", base=cli.context) cli.expect_success("-b{base} -ppex install twine", "Installed twine", base=cli.context) cli.expect_success("-b{base} list", "tox", "twine", base=cli.context) cli.expect_success("-b{base} list --verbose", "tox", "twine", base=cli.context) tmp = os.path.realpath(cli.context) assert find_uninstaller(os.path.join(tmp, "tox")) assert find_uninstaller(os.path.join(tmp, "twine")) cli.expect_success("-b{base} uninstall twine", "Uninstalled twine", base=cli.context) runez.delete(p.current._path) runez.touch(p.current._path) cli.expect_failure("-b{base} check", "tox", "Couldn't read", "is not installed", base=cli.context) cli.expect_success("-b{base} uninstall tox", "Uninstalled tox", "entry points", base=cli.context)