Skip to content

Commit

Permalink
Upgrade: add option to upgrade injected packages (#563)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernát Gábor <[email protected]>
  • Loading branch information
itsayellow and gaborbernat authored Nov 28, 2020
1 parent 53d1dd1 commit 4adfb4e
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 46 deletions.
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dev
- pipx now reinstalls its internal shared libraries when the user executes `reinstall-all`.
- Made sure shell exit codes from every pipx command are correct. In the past some (like from `pipx upgrade`) were wrong. The exit code from `pipx runpip` is now the exit code from the `pip` command run. The exit code from `pipx list` will be 1 if one or more venvs have problems that need to be addressed.
- pipx now writes a log file for each pipx command executed to `$PIPX_HOME/logs`, typically `~/.local/pipx/logs`. pipx keeps the most recent 10 logs and deletes others.
- `pipx upgrade` and `pipx upgrade-all` now have a `--upgrade-injected` option which directs pipx to also upgrade injected packages.
- `pipx list` now detects, identifies, and suggests a remedy for venvs with old-internal data (internal venv names) that need to be updated.
- Added a "Troubleshooting" page to the pipx web documentation for common problems pipx users may encounter.

Expand Down
141 changes: 98 additions & 43 deletions src/pipx/commands/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,68 +12,47 @@
from pipx.venv import Venv, VenvContainer


def _upgrade_venv(
venv_dir: Path,
def _upgrade_package(
venv: Venv,
package: str,
pip_args: List[str],
verbose: bool,
*,
upgrading_all: bool,
is_main_package: bool,
force: bool,
upgrading_all: bool,
) -> int:
"""Returns 1 if package version changed, 0 if same version"""
if not venv_dir.is_dir():
raise PipxError(
f"Package is not installed. Expected to find {str(venv_dir)}, "
"but it does not exist."
)

venv = Venv(venv_dir, verbose=verbose)
package = venv.main_package_name

if not venv.package_metadata:
raise PipxError(
f"Not upgrading {red(bold(package))}. It has missing internal pipx metadata.\n"
f" It was likely installed using a pipx version before 0.15.0.0.\n"
f" Please uninstall and install this package to fix."
)

package_metadata = venv.package_metadata[package]

if package_metadata.package_or_url is None:
raise PipxError(f"Internal Error: package {package} has corrupt pipx metadata.")

package_or_url = parse_specifier_for_upgrade(package_metadata.package_or_url)
old_version = package_metadata.package_version
include_apps = package_metadata.include_apps
include_dependencies = package_metadata.include_dependencies

# Upgrade shared libraries (pip, setuptools and wheel)
venv.upgrade_packaging_libraries(pip_args)

venv.upgrade_package(
package,
package_or_url,
pip_args,
include_dependencies=include_dependencies,
include_apps=include_apps,
is_main_package=True,
include_dependencies=package_metadata.include_dependencies,
include_apps=package_metadata.include_apps,
is_main_package=is_main_package,
suffix=package_metadata.suffix,
)
# TODO 20191026: upgrade injected packages also (Issue #79)

package_metadata = venv.package_metadata[package]

display_name = f"{package_metadata.package}{package_metadata.suffix}"
new_version = package_metadata.package_version

expose_apps_globally(
constants.LOCAL_BIN_DIR,
package_metadata.app_paths,
force=force,
suffix=package_metadata.suffix,
)
if package_metadata.include_apps:
expose_apps_globally(
constants.LOCAL_BIN_DIR,
package_metadata.app_paths,
force=force,
suffix=package_metadata.suffix,
)

if include_dependencies:
if package_metadata.include_dependencies:
for _, app_paths in package_metadata.app_paths_of_dependencies.items():
expose_apps_globally(
constants.LOCAL_BIN_DIR,
Expand All @@ -87,29 +66,104 @@ def _upgrade_venv(
pass
else:
print(
f"{display_name} is already at latest version {old_version} (location: {str(venv_dir)})"
f"{display_name} is already at latest version {old_version} "
f"(location: {str(venv.root)})"
)
return 0
else:
print(
f"upgraded package {display_name} from {old_version} to {new_version} (location: {str(venv_dir)})"
f"upgraded package {display_name} from {old_version} to {new_version} "
f"(location: {str(venv.root)})"
)
return 1


def _upgrade_venv(
venv_dir: Path,
pip_args: List[str],
verbose: bool,
*,
include_injected: bool,
upgrading_all: bool,
force: bool,
) -> int:
"""Returns number of packages with changed versions."""
if not venv_dir.is_dir():
raise PipxError(
f"Package is not installed. Expected to find {str(venv_dir)}, "
"but it does not exist."
)

venv = Venv(venv_dir, verbose=verbose)

if not venv.package_metadata:
raise PipxError(
f"Not upgrading {red(bold(venv_dir.name))}. It has missing internal pipx metadata.\n"
f" It was likely installed using a pipx version before 0.15.0.0.\n"
f" Please uninstall and install this package to fix."
)

# Upgrade shared libraries (pip, setuptools and wheel)
venv.upgrade_packaging_libraries(pip_args)

versions_updated = 0

package = venv.main_package_name
versions_updated += _upgrade_package(
venv,
package,
pip_args,
is_main_package=True,
force=force,
upgrading_all=upgrading_all,
)

if include_injected:
for package in venv.package_metadata:
if package == venv.main_package_name:
continue
versions_updated += _upgrade_package(
venv,
package,
pip_args,
is_main_package=False,
force=force,
upgrading_all=upgrading_all,
)

return versions_updated


def upgrade(
venv_dir: Path, pip_args: List[str], verbose: bool, *, force: bool
venv_dir: Path,
pip_args: List[str],
verbose: bool,
*,
include_injected: bool,
force: bool,
) -> ExitCode:
"""Returns pipx exit code."""

_ = _upgrade_venv(venv_dir, pip_args, verbose, upgrading_all=False, force=force)
_ = _upgrade_venv(
venv_dir,
pip_args,
verbose,
include_injected=include_injected,
upgrading_all=False,
force=force,
)

# Any error in upgrade will raise PipxError from vevn._run_pip()
# Any error in upgrade will raise PipxError (e.g. from venv._run_pip())
return EXIT_CODE_OK


def upgrade_all(
venv_container: VenvContainer, verbose: bool, *, skip: Sequence[str], force: bool
venv_container: VenvContainer,
verbose: bool,
*,
include_injected: bool,
skip: Sequence[str],
force: bool,
) -> ExitCode:
"""Returns pipx exit code."""
venv_error = False
Expand All @@ -126,6 +180,7 @@ def upgrade_all(
venv_dir,
venv.pipx_metadata.main_package.pip_args,
verbose,
include_injected=include_injected,
upgrading_all=True,
force=force,
)
Expand Down
25 changes: 22 additions & 3 deletions src/pipx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,20 @@ def run_pipx_command(args: argparse.Namespace) -> ExitCode: # noqa: C901
force=args.force,
)
elif args.command == "upgrade":
return commands.upgrade(venv_dir, pip_args, verbose, force=args.force)
return commands.upgrade(
venv_dir,
pip_args,
verbose,
include_injected=args.include_injected,
force=args.force,
)
elif args.command == "upgrade-all":
return commands.upgrade_all(
venv_container, verbose, skip=skip_list, force=args.force
venv_container,
verbose,
include_injected=args.include_injected,
skip=skip_list,
force=args.force,
)
elif args.command == "list":
return commands.list_packages(venv_container, args.include_injected)
Expand Down Expand Up @@ -363,6 +373,11 @@ def _add_upgrade(subparsers, venv_completer) -> None:
description="Upgrade a package in a pipx-managed Virtual Environment by running 'pip install --upgrade PACKAGE'",
)
p.add_argument("package").completer = venv_completer
p.add_argument(
"--include-injected",
action="store_true",
help="Also upgrade packages injected into the main app's environment",
)
p.add_argument(
"--force",
"-f",
Expand All @@ -379,7 +394,11 @@ def _add_upgrade_all(subparsers) -> None:
help="Upgrade all packages. Runs `pip install -U <pkgname>` for each package.",
description="Upgrades all packages within their virtual environments by running 'pip install --upgrade PACKAGE'",
)

p.add_argument(
"--include-injected",
action="store_true",
help="Also upgrade packages injected into the main app's environment",
)
p.add_argument("--skip", nargs="+", default=[], help="skip these packages")
p.add_argument(
"--force",
Expand Down
20 changes: 20 additions & 0 deletions tests/test_upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,23 @@ def test_upgrade_specifier(pipx_temp_env, capsys):
assert not run_pipx_cli(["upgrade", f"{name}"])
captured = capsys.readouterr()
assert f"upgraded package {name} from {initial_version} to" in captured.out


def test_upgrade_include_injected(pipx_temp_env, capsys):
assert not run_pipx_cli(["install", "pylint==2.5.3"])
assert not run_pipx_cli(["inject", "pylint", "black==18.9.b0"])
captured = capsys.readouterr()
assert not run_pipx_cli(["upgrade", "--include-injected", "pylint"])
captured = capsys.readouterr()
assert "upgraded package pylint" in captured.out
assert "upgraded package black" in captured.out


def test_upgrade_no_include_injected(pipx_temp_env, capsys):
assert not run_pipx_cli(["install", "pylint==2.5.3"])
assert not run_pipx_cli(["inject", "pylint", "black==18.9.b0"])
captured = capsys.readouterr()
assert not run_pipx_cli(["upgrade", "pylint"])
captured = capsys.readouterr()
assert "upgraded package pylint" in captured.out
assert "upgraded package black" not in captured.out

0 comments on commit 4adfb4e

Please sign in to comment.