mirror of
https://github.com/kennethreitz/pipenv.git
synced 2026-06-05 06:46:15 +00:00
Smarter uninstall (#6029)
* Initial take at making uninstall like the inverse of upgrade. * Updates based on testing the uninstall command * Handle pre flag * Add news fragment
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
The ``uninstall`` command now does the inverse of ``upgrade`` which means it no longer invokes a full ``lock`` cycle which was problematic for projects with many dependencies.
|
||||||
@@ -284,15 +284,18 @@ def uninstall(ctx, state, all_dev=False, all=False, **kwargs):
|
|||||||
"""Uninstalls a provided package and removes it from Pipfile."""
|
"""Uninstalls a provided package and removes it from Pipfile."""
|
||||||
from pipenv.routines.uninstall import do_uninstall
|
from pipenv.routines.uninstall import do_uninstall
|
||||||
|
|
||||||
|
pre = state.installstate.pre
|
||||||
|
|
||||||
retcode = do_uninstall(
|
retcode = do_uninstall(
|
||||||
state.project,
|
state.project,
|
||||||
packages=state.installstate.packages,
|
packages=state.installstate.packages,
|
||||||
editable_packages=state.installstate.editables,
|
editable_packages=state.installstate.editables,
|
||||||
python=state.python,
|
python=state.python,
|
||||||
system=state.system,
|
system=state.system,
|
||||||
lock=True,
|
lock=False,
|
||||||
all_dev=all_dev,
|
all_dev=all_dev,
|
||||||
all=all,
|
all=all,
|
||||||
|
pre=pre,
|
||||||
pypi_mirror=state.pypi_mirror,
|
pypi_mirror=state.pypi_mirror,
|
||||||
categories=state.installstate.categories,
|
categories=state.installstate.categories,
|
||||||
ctx=ctx,
|
ctx=ctx,
|
||||||
|
|||||||
@@ -1133,6 +1133,16 @@ class Project:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def reset_category_in_pipfile(self, category):
|
||||||
|
# Read and append Pipfile.
|
||||||
|
p = self.parsed_pipfile
|
||||||
|
if category:
|
||||||
|
del p[category]
|
||||||
|
p[category] = {}
|
||||||
|
self.write_toml(p)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def remove_packages_from_pipfile(self, packages):
|
def remove_packages_from_pipfile(self, packages):
|
||||||
parsed = self.parsed_pipfile
|
parsed = self.parsed_pipfile
|
||||||
packages = {pep423_name(pkg) for pkg in packages}
|
packages = {pep423_name(pkg) for pkg in packages}
|
||||||
|
|||||||
+114
-120
@@ -3,22 +3,39 @@ import sys
|
|||||||
|
|
||||||
from pipenv import exceptions
|
from pipenv import exceptions
|
||||||
from pipenv.patched.pip._internal.build_env import get_runnable_pip
|
from pipenv.patched.pip._internal.build_env import get_runnable_pip
|
||||||
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
|
|
||||||
from pipenv.routines.lock import do_lock
|
from pipenv.routines.lock import do_lock
|
||||||
from pipenv.utils.dependencies import (
|
from pipenv.utils.dependencies import (
|
||||||
expansive_install_req_from_line,
|
expansive_install_req_from_line,
|
||||||
get_canonical_names,
|
|
||||||
get_lockfile_section_using_pipfile_category,
|
get_lockfile_section_using_pipfile_category,
|
||||||
get_pipfile_category_using_lockfile_section,
|
get_pipfile_category_using_lockfile_section,
|
||||||
pep423_name,
|
pep423_name,
|
||||||
)
|
)
|
||||||
from pipenv.utils.processes import run_command, subprocess_run
|
from pipenv.utils.processes import run_command, subprocess_run
|
||||||
from pipenv.utils.project import ensure_project
|
|
||||||
from pipenv.utils.requirements import BAD_PACKAGES
|
from pipenv.utils.requirements import BAD_PACKAGES
|
||||||
|
from pipenv.utils.resolver import venv_resolve_deps
|
||||||
from pipenv.utils.shell import cmd_list_to_shell, project_python
|
from pipenv.utils.shell import cmd_list_to_shell, project_python
|
||||||
from pipenv.vendor import click
|
from pipenv.vendor import click
|
||||||
|
|
||||||
|
|
||||||
|
def _uninstall_from_environment(project, package, system=False):
|
||||||
|
# Execute the uninstall command for the package
|
||||||
|
click.secho(f"Uninstalling {package}...", fg="green", bold=True)
|
||||||
|
with project.environment.activated():
|
||||||
|
cmd = [
|
||||||
|
project_python(project, system=system),
|
||||||
|
get_runnable_pip(),
|
||||||
|
"uninstall",
|
||||||
|
package,
|
||||||
|
"-y",
|
||||||
|
]
|
||||||
|
c = run_command(cmd, is_verbose=project.s.is_verbose())
|
||||||
|
click.secho(c.stdout, fg="cyan")
|
||||||
|
if c.returncode != 0:
|
||||||
|
click.echo(f"Error occurred while uninstalling package {package}.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def do_uninstall(
|
def do_uninstall(
|
||||||
project,
|
project,
|
||||||
packages=None,
|
packages=None,
|
||||||
@@ -28,147 +45,124 @@ def do_uninstall(
|
|||||||
lock=False,
|
lock=False,
|
||||||
all_dev=False,
|
all_dev=False,
|
||||||
all=False,
|
all=False,
|
||||||
|
pre=False,
|
||||||
pypi_mirror=None,
|
pypi_mirror=None,
|
||||||
ctx=None,
|
ctx=None,
|
||||||
categories=None,
|
categories=None,
|
||||||
):
|
):
|
||||||
# Automatically use an activated virtualenv.
|
# Initialization similar to the upgrade function
|
||||||
if project.s.PIPENV_USE_SYSTEM:
|
|
||||||
system = True
|
|
||||||
# Ensure that virtualenv is available.
|
|
||||||
ensure_project(project, python=python, pypi_mirror=pypi_mirror)
|
|
||||||
# Uninstall all dependencies, if --all was provided.
|
|
||||||
if not any([packages, editable_packages, all_dev, all]):
|
if not any([packages, editable_packages, all_dev, all]):
|
||||||
raise exceptions.PipenvUsageError("No package provided!", ctx=ctx)
|
raise exceptions.PipenvUsageError("No package provided!", ctx=ctx)
|
||||||
|
|
||||||
if not categories:
|
if not categories:
|
||||||
categories = project.get_package_categories(for_lockfile=True)
|
categories = ["default"]
|
||||||
editable_pkgs = []
|
|
||||||
for p in editable_packages:
|
lockfile_content = project.lockfile_content
|
||||||
if p:
|
|
||||||
install_req, name = expansive_install_req_from_line(f"-e {p}")
|
|
||||||
editable_pkgs.append(name)
|
|
||||||
packages += editable_pkgs
|
|
||||||
package_names = {p for p in packages if p}
|
|
||||||
package_map = {canonicalize_name(p): p for p in packages if p}
|
|
||||||
installed_package_names = project.installed_package_names
|
|
||||||
if project.lockfile_exists:
|
|
||||||
project_pkg_names = project.lockfile_package_names
|
|
||||||
else:
|
|
||||||
project_pkg_names = project.pipfile_package_names
|
|
||||||
# Uninstall [dev-packages], if --dev was provided.
|
|
||||||
if all_dev:
|
if all_dev:
|
||||||
if (
|
|
||||||
"dev-packages" not in project.parsed_pipfile
|
|
||||||
and not project_pkg_names["develop"]
|
|
||||||
):
|
|
||||||
click.echo(
|
|
||||||
click.style(
|
|
||||||
"No {} to uninstall.".format(
|
|
||||||
click.style("[dev-packages]", fg="yellow")
|
|
||||||
),
|
|
||||||
bold=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
click.secho(
|
click.secho(
|
||||||
click.style(
|
click.style(
|
||||||
"Un-installing {}...".format(click.style("[dev-packages]", fg="yellow")),
|
"Un-installing all {}...".format(
|
||||||
bold=True,
|
click.style("[dev-packages]", fg="yellow")
|
||||||
)
|
|
||||||
)
|
|
||||||
preserve_packages = set()
|
|
||||||
dev_packages = set()
|
|
||||||
for category in project.get_package_categories(for_lockfile=True):
|
|
||||||
if category == "develop":
|
|
||||||
dev_packages |= set(project_pkg_names[category])
|
|
||||||
else:
|
|
||||||
preserve_packages |= set(project_pkg_names[category])
|
|
||||||
|
|
||||||
package_names = dev_packages - preserve_packages
|
|
||||||
|
|
||||||
# Remove known "bad packages" from the list.
|
|
||||||
bad_pkgs = get_canonical_names(BAD_PACKAGES)
|
|
||||||
ignored_packages = bad_pkgs & set(package_map)
|
|
||||||
for ignored_pkg in get_canonical_names(ignored_packages):
|
|
||||||
if project.s.is_verbose():
|
|
||||||
click.echo(f"Ignoring {ignored_pkg}.", err=True)
|
|
||||||
package_names.discard(package_map[ignored_pkg])
|
|
||||||
|
|
||||||
used_packages = project_pkg_names["combined"] & installed_package_names
|
|
||||||
failure = False
|
|
||||||
if all:
|
|
||||||
click.echo(
|
|
||||||
click.style(
|
|
||||||
"Un-installing all {} and {}...".format(
|
|
||||||
click.style("[dev-packages]", fg="yellow"),
|
|
||||||
click.style("[packages]", fg="yellow"),
|
|
||||||
),
|
),
|
||||||
bold=True,
|
bold=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
do_purge(project, bare=False, allow_global=system)
|
# Uninstall all dev-packages from environment
|
||||||
sys.exit(0)
|
for package in project.get_pipfile_section("dev-packages"):
|
||||||
|
_uninstall_from_environment(project, package, system=system)
|
||||||
|
# Remove the package from the Pipfile
|
||||||
|
if project.reset_category_in_pipfile(category="dev-packages"):
|
||||||
|
click.echo("Removed [dev-packages] from Pipfile.")
|
||||||
|
# Finalize changes to lockfile
|
||||||
|
lockfile_content["develop"] = {}
|
||||||
|
lockfile_content.update({"_meta": project.get_lockfile_meta()})
|
||||||
|
project.write_lockfile(lockfile_content)
|
||||||
|
|
||||||
selected_pkg_map = {canonicalize_name(p): p for p in package_names}
|
if all:
|
||||||
packages_to_remove = [
|
click.secho(
|
||||||
package_name
|
click.style(
|
||||||
for normalized, package_name in selected_pkg_map.items()
|
"Un-installing all {}...".format(click.style("[packages]", fg="yellow")),
|
||||||
if normalized in (used_packages - bad_pkgs)
|
bold=True,
|
||||||
]
|
)
|
||||||
lockfile = project.get_or_create_lockfile(categories=categories)
|
)
|
||||||
|
# Uninstall all dev-packages from environment
|
||||||
|
for package in project.get_pipfile_section("packages"):
|
||||||
|
_uninstall_from_environment(project, package, system=system)
|
||||||
|
# Remove the package from the Pipfile
|
||||||
|
if project.reset_category_in_pipfile(category="packages"):
|
||||||
|
click.echo("Removed [packages] from Pipfile.")
|
||||||
|
|
||||||
|
# Finalize changes to lockfile
|
||||||
|
lockfile_content["default"] = {}
|
||||||
|
lockfile_content.update({"_meta": project.get_lockfile_meta()})
|
||||||
|
project.write_lockfile(lockfile_content)
|
||||||
|
|
||||||
|
package_args = list(packages) + [f"-e {pkg}" for pkg in editable_packages]
|
||||||
|
|
||||||
|
# Determine packages and their dependencies for removal
|
||||||
for category in categories:
|
for category in categories:
|
||||||
category = get_lockfile_section_using_pipfile_category(category)
|
category = get_lockfile_section_using_pipfile_category(
|
||||||
for normalized_name, package_name in selected_pkg_map.items():
|
category
|
||||||
if normalized_name in project.lockfile_content[category]:
|
) # In case they passed pipfile category
|
||||||
click.echo(
|
pipfile_category = get_pipfile_category_using_lockfile_section(category)
|
||||||
"{} {} {} {}".format(
|
|
||||||
click.style("Removing", fg="cyan"),
|
|
||||||
click.style(package_name, fg="green"),
|
|
||||||
click.style("from", fg="cyan"),
|
|
||||||
click.style("Pipfile.lock...", fg="white"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if normalized_name in lockfile[category]:
|
|
||||||
del lockfile[category][normalized_name]
|
|
||||||
lockfile.write()
|
|
||||||
|
|
||||||
pipfile_category = get_pipfile_category_using_lockfile_section(category)
|
for package in package_args[:]:
|
||||||
|
install_req, _ = expansive_install_req_from_line(package, expand_env=True)
|
||||||
|
name, normalized_name, pipfile_entry = project.generate_package_pipfile_entry(
|
||||||
|
install_req, package, category=pipfile_category
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the package from the Pipfile
|
||||||
if project.remove_package_from_pipfile(
|
if project.remove_package_from_pipfile(
|
||||||
package_name, category=pipfile_category
|
normalized_name, category=pipfile_category
|
||||||
):
|
):
|
||||||
click.secho(
|
click.echo(f"Removed {normalized_name} from Pipfile.")
|
||||||
f"Removed {package_name} from Pipfile category {pipfile_category}",
|
|
||||||
fg="green",
|
|
||||||
)
|
|
||||||
|
|
||||||
for normalized_name, package_name in selected_pkg_map.items():
|
# Rebuild the dependencies for resolution from the updated Pipfile
|
||||||
still_remains = False
|
updated_packages = project.get_pipfile_section(pipfile_category)
|
||||||
for category in project.get_package_categories():
|
|
||||||
if project.get_package_name_in_pipfile(normalized_name, category=category):
|
# Resolve dependencies with the package removed
|
||||||
still_remains = True
|
resolved_lock_data = venv_resolve_deps(
|
||||||
if not still_remains:
|
updated_packages,
|
||||||
# Uninstall the package.
|
which=project._which,
|
||||||
if package_name in packages_to_remove:
|
project=project,
|
||||||
click.secho(
|
lockfile={},
|
||||||
f"Uninstalling {click.style(package_name)}...",
|
category=pipfile_category,
|
||||||
fg="green",
|
pre=pre,
|
||||||
bold=True,
|
allow_global=system,
|
||||||
)
|
pypi_mirror=pypi_mirror,
|
||||||
with project.environment.activated():
|
)
|
||||||
cmd = [
|
|
||||||
project_python(project, system=system),
|
# Determine which dependencies are no longer needed
|
||||||
get_runnable_pip(),
|
try:
|
||||||
"uninstall",
|
current_lock_data = lockfile_content[category]
|
||||||
package_name,
|
if current_lock_data:
|
||||||
"-y",
|
deps_to_remove = [
|
||||||
|
dep for dep in current_lock_data if dep not in resolved_lock_data
|
||||||
]
|
]
|
||||||
c = run_command(cmd, is_verbose=project.s.is_verbose())
|
# Remove unnecessary dependencies from Pipfile and lockfile
|
||||||
click.secho(c.stdout, fg="cyan")
|
for dep in deps_to_remove:
|
||||||
if c.returncode != 0:
|
if (
|
||||||
failure = True
|
category in lockfile_content
|
||||||
|
and dep in lockfile_content[category]
|
||||||
|
):
|
||||||
|
del lockfile_content[category][dep]
|
||||||
|
except KeyError:
|
||||||
|
pass # No lockfile data for this category
|
||||||
|
|
||||||
|
# Finalize changes to lockfile
|
||||||
|
lockfile_content.update({"_meta": project.get_lockfile_meta()})
|
||||||
|
project.write_lockfile(lockfile_content)
|
||||||
|
|
||||||
|
# Perform uninstallation of packages and dependencies
|
||||||
|
failure = False
|
||||||
|
for package in package_args:
|
||||||
|
_uninstall_from_environment(project, package, system=system)
|
||||||
|
|
||||||
if lock:
|
if lock:
|
||||||
do_lock(project, system=system, pypi_mirror=pypi_mirror)
|
do_lock(project, system=system, pypi_mirror=pypi_mirror)
|
||||||
|
|
||||||
sys.exit(int(failure))
|
sys.exit(int(failure))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -771,10 +771,10 @@ def venv_resolve_deps(
|
|||||||
|
|
||||||
if not deps:
|
if not deps:
|
||||||
if not project.pipfile_exists:
|
if not project.pipfile_exists:
|
||||||
return None
|
return {}
|
||||||
deps = project.parsed_pipfile.get(category, {})
|
deps = project.parsed_pipfile.get(category, {})
|
||||||
if not deps:
|
if not deps:
|
||||||
return None
|
return {}
|
||||||
|
|
||||||
if not pipfile:
|
if not pipfile:
|
||||||
pipfile = getattr(project, category, {})
|
pipfile = getattr(project, category, {})
|
||||||
|
|||||||
@@ -90,11 +90,8 @@ def test_uninstall_all_local_files(pipenv_instance_private_pypi, testsroot):
|
|||||||
c = p.pipenv(f"install {file_uri}")
|
c = p.pipenv(f"install {file_uri}")
|
||||||
assert c.returncode == 0
|
assert c.returncode == 0
|
||||||
c = p.pipenv("uninstall --all")
|
c = p.pipenv("uninstall --all")
|
||||||
assert c.returncode == 0
|
assert "tablib" not in p.pipfile["packages"]
|
||||||
assert "tablib" in c.stdout
|
assert "tablib" not in p.lockfile["default"]
|
||||||
# Uninstall --all is not supposed to remove things from the pipfile
|
|
||||||
# Note that it didn't before, but that instead local filenames showed as hashes
|
|
||||||
assert "tablib" in p.pipfile["packages"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.install
|
@pytest.mark.install
|
||||||
@@ -218,7 +215,7 @@ def test_uninstall_category_with_shared_requirement(pipenv_instance_pypi):
|
|||||||
c = p.pipenv("install")
|
c = p.pipenv("install")
|
||||||
assert c.returncode == 0
|
assert c.returncode == 0
|
||||||
|
|
||||||
c = p.pipenv("uninstall six --categories packages")
|
c = p.pipenv("uninstall six --categories default")
|
||||||
assert c.returncode == 0
|
assert c.returncode == 0
|
||||||
|
|
||||||
assert "six" in p.lockfile["prereq"]
|
assert "six" in p.lockfile["prereq"]
|
||||||
|
|||||||
Reference in New Issue
Block a user