diff --git a/news/6185.bugfix.rst b/news/6185.bugfix.rst new file mode 100644 index 0000000000..bc0bd085e0 --- /dev/null +++ b/news/6185.bugfix.rst @@ -0,0 +1 @@ +Fixed ``pipenv uninstall --all`` failing when the virtual environment no longer exists. diff --git a/pipenv/environment.py b/pipenv/environment.py index d75243894a..a93817303d 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -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 @@ -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() @@ -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]: @@ -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): @@ -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() @@ -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 diff --git a/pipenv/routines/uninstall.py b/pipenv/routines/uninstall.py index 81b3d0f4ff..7939ccbd35 100644 --- a/pipenv/routines/uninstall.py +++ b/pipenv/routines/uninstall.py @@ -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, @@ -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(), @@ -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, diff --git a/tests/integration/test_uninstall.py b/tests/integration/test_uninstall.py index e78f90dc23..04c338bd90 100644 --- a/tests/integration/test_uninstall.py +++ b/tests/integration/test_uninstall.py @@ -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"]