Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix uninstall --all fails when venv is deleted #6250

Merged
merged 8 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6185.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``pipenv uninstall --all`` failing when the virtual environment no longer exists.
36 changes: 26 additions & 10 deletions pipenv/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
from pipenv.patched.pip._vendor.typing_extensions import Iterable
from pipenv.utils import console
from pipenv.utils.fileutils import normalize_path, temp_path
from pipenv.utils.funktools import chunked, unnest
Expand Down Expand Up @@ -72,8 +73,9 @@ def __init__(
pipfile = project.parsed_pipfile
self.pipfile = pipfile
self.extra_dists = []
prefix = prefix if prefix else sys.prefix
self.prefix = Path(prefix)
if self.is_venv and prefix is not None and not Path(prefix).exists():
return
self.prefix = Path(prefix if prefix else sys.prefix)
self._base_paths = {}
if self.is_venv:
self._base_paths = self.get_paths()
Expand All @@ -96,11 +98,14 @@ def safe_import(self, name: str) -> ModuleType:
return module

@cached_property
def python_version(self) -> str:
with self.activated():
sysconfig = self.safe_import("sysconfig")
py_version = sysconfig.get_python_version()
return py_version
def python_version(self) -> str | None:
with self.activated() as active:
if active:
sysconfig = self.safe_import("sysconfig")
py_version = sysconfig.get_python_version()
return py_version
else:
return None

@property
def python_info(self) -> dict[str, str]:
Expand Down Expand Up @@ -703,9 +708,10 @@ def reverse_dependencies(self):
}
return rdeps

def get_working_set(self):
def get_working_set(self) -> Iterable:
"""Retrieve the working set of installed packages for the environment."""

if not hasattr(self, "sys_path"):
return []
return importlib_metadata.distributions(path=self.sys_path)

def is_installed(self, pkgname):
Expand Down Expand Up @@ -781,6 +787,16 @@ def activated(self):
to `os.environ["PATH"]` to ensure that calls to `~Environment.run()` use the
environment's path preferentially.
"""

# Fail if the virtualenv is needed but cannot be found
if self.is_venv and (
hasattr(self, "prefix")
and not self.prefix.exists()
or not hasattr(self, "prefix")
):
yield False
return

original_path = sys.path
original_prefix = sys.prefix
prefix = self.prefix.as_posix()
Expand All @@ -806,7 +822,7 @@ def activated(self):
sys.path = self.sys_path
sys.prefix = self.sys_prefix
try:
yield
yield True
finally:
sys.path = original_path
sys.prefix = original_prefix
13 changes: 9 additions & 4 deletions pipenv/routines/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

from pipenv import exceptions
from pipenv.patched.pip._internal.build_env import get_runnable_pip
from pipenv.project import Project
from pipenv.routines.lock import do_lock
from pipenv.utils import console
from pipenv.utils.dependencies import (
expansive_install_req_from_line,
get_lockfile_section_using_pipfile_category,
Expand All @@ -18,10 +20,13 @@
from pipenv.vendor.importlib_metadata.compat.py39 import normalized_name


def _uninstall_from_environment(project, package, system=False):
def _uninstall_from_environment(project: Project, package, system=False):
# Execute the uninstall command for the package
click.secho(f"Uninstalling {package}...", fg="green", bold=True)
with project.environment.activated():
with project.environment.activated() as is_active:
if not is_active:
return False

console.print(f"Uninstalling {package}...", style="bold green")
cmd = [
project_python(project, system=system),
get_runnable_pip(),
Expand All @@ -38,7 +43,7 @@ def _uninstall_from_environment(project, package, system=False):


def do_uninstall(
project,
project: Project,
packages=None,
editable_packages=None,
python=False,
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/test_uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,23 @@ def test_category_not_sorted_without_directive(pipenv_instance_private_pypi):
"colorama",
"atomicwrites",
]


@pytest.mark.uninstall
def test_uninstall_without_venv(pipenv_instance_private_pypi):
with pipenv_instance_private_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[packages]
colorama = "*"
atomicwrites = "*"
""".strip()
f.write(contents)

c = p.pipenv("install")
assert c.returncode == 0

c = p.pipenv("uninstall --all")
assert c.returncode == 0
# uninstall --all shold not remove packages from Pipfile
assert list(p.pipfile["packages"].keys()) == ["colorama", "atomicwrites"]
Loading