From f0464250e80ff624fe3994dbd503990b81523c7c Mon Sep 17 00:00:00 2001 From: Naofal-Helal Date: Sun, 22 Sep 2024 10:22:36 +0300 Subject: [PATCH 1/8] Add test case --- tests/integration/test_uninstall.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/integration/test_uninstall.py b/tests/integration/test_uninstall.py index e78f90dc23..168210e916 100644 --- a/tests/integration/test_uninstall.py +++ b/tests/integration/test_uninstall.py @@ -336,3 +336,26 @@ def test_category_not_sorted_without_directive(pipenv_instance_private_pypi): "colorama", "atomicwrites", ] + + +@pytest.mark.uninstall +def test_uninstall_whithout_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 + assert list(p.pipfile["packages"].keys()) == [ + "parse", + "colorama", + "atomicwrites", + ] From 146be17698eb00440f3d615ee095cbdf5c6ec76f Mon Sep 17 00:00:00 2001 From: Naofal-Helal Date: Sun, 22 Sep 2024 13:02:40 +0300 Subject: [PATCH 2/8] Fix #6185 --- news/6185.bugfix.rst | 1 + pipenv/environment.py | 39 +++++++++++++++++++++-------- pipenv/routines/uninstall.py | 12 ++++++--- tests/integration/test_uninstall.py | 9 +++---- 4 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 news/6185.bugfix.rst 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..afe320178c 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -7,6 +7,7 @@ import site import sys import typing +from collections import namedtuple from functools import cached_property from pathlib import Path from sysconfig import get_paths, get_python_version, get_scheme_names @@ -19,6 +20,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 +74,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 +99,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 e: + if e.ok: + 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 +709,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): @@ -761,6 +768,8 @@ def run_activate_this(self): code = compile(f.read(), activate_this, "exec") exec(code, {"__file__": activate_this}) + EnvironmentActivationResult = namedtuple("EnvironmentActivationResult", ["ok"]) + @contextlib.contextmanager def activated(self): """Helper context manager to activate the environment. @@ -781,6 +790,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 self.EnvironmentActivationResult(ok=False) + return + original_path = sys.path original_prefix = sys.prefix prefix = self.prefix.as_posix() @@ -806,7 +825,7 @@ def activated(self): sys.path = self.sys_path sys.prefix = self.sys_prefix try: - yield + yield self.EnvironmentActivationResult(ok=True) finally: sys.path = original_path sys.prefix = original_prefix diff --git a/pipenv/routines/uninstall.py b/pipenv/routines/uninstall.py index 81b3d0f4ff..7b7afdffdc 100644 --- a/pipenv/routines/uninstall.py +++ b/pipenv/routines/uninstall.py @@ -3,6 +3,7 @@ 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.dependencies import ( expansive_install_req_from_line, @@ -18,10 +19,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 e: + if not e.ok: + return False + + click.secho(f"Uninstalling {package}...", fg="green", bold=True) cmd = [ project_python(project, system=system), get_runnable_pip(), @@ -38,7 +42,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 168210e916..04c338bd90 100644 --- a/tests/integration/test_uninstall.py +++ b/tests/integration/test_uninstall.py @@ -339,7 +339,7 @@ def test_category_not_sorted_without_directive(pipenv_instance_private_pypi): @pytest.mark.uninstall -def test_uninstall_whithout_venv(pipenv_instance_private_pypi): +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 = """ @@ -354,8 +354,5 @@ def test_uninstall_whithout_venv(pipenv_instance_private_pypi): c = p.pipenv("uninstall --all") assert c.returncode == 0 - assert list(p.pipfile["packages"].keys()) == [ - "parse", - "colorama", - "atomicwrites", - ] + # uninstall --all shold not remove packages from Pipfile + assert list(p.pipfile["packages"].keys()) == ["colorama", "atomicwrites"] From 31edd0054e6041ccbc52bd9a3d717ce99df53aa2 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 22:58:26 -0400 Subject: [PATCH 3/8] Update environment.py --- pipenv/environment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pipenv/environment.py b/pipenv/environment.py index afe320178c..90073618c2 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -7,7 +7,6 @@ import site import sys import typing -from collections import namedtuple from functools import cached_property from pathlib import Path from sysconfig import get_paths, get_python_version, get_scheme_names @@ -797,7 +796,7 @@ def activated(self): and not self.prefix.exists() or not hasattr(self, "prefix") ): - yield self.EnvironmentActivationResult(ok=False) + yield False return original_path = sys.path @@ -825,7 +824,8 @@ def activated(self): sys.path = self.sys_path sys.prefix = self.sys_prefix try: - yield self.EnvironmentActivationResult(ok=True) + yield True finally: sys.path = original_path sys.prefix = original_prefix + From 71b556047df7a2af7f6d378ae49812d4c078f4ed Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 23:00:42 -0400 Subject: [PATCH 4/8] Update uninstall.py --- pipenv/routines/uninstall.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pipenv/routines/uninstall.py b/pipenv/routines/uninstall.py index 7b7afdffdc..7114bd0c23 100644 --- a/pipenv/routines/uninstall.py +++ b/pipenv/routines/uninstall.py @@ -5,6 +5,7 @@ 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, @@ -21,11 +22,11 @@ def _uninstall_from_environment(project: Project, package, system=False): # Execute the uninstall command for the package - with project.environment.activated() as e: - if not e.ok: + with project.environment.activated() as is_active: + if not is_active: return False - click.secho(f"Uninstalling {package}...", fg="green", bold=True) + console.print(f"Uninstalling {package}...", style="bold green") cmd = [ project_python(project, system=system), get_runnable_pip(), From d19aa93db0ddc3b812225f94d33da639a069a8cb Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 23:01:41 -0400 Subject: [PATCH 5/8] Update environment.py --- pipenv/environment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pipenv/environment.py b/pipenv/environment.py index 90073618c2..eee0367306 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -767,8 +767,6 @@ def run_activate_this(self): code = compile(f.read(), activate_this, "exec") exec(code, {"__file__": activate_this}) - EnvironmentActivationResult = namedtuple("EnvironmentActivationResult", ["ok"]) - @contextlib.contextmanager def activated(self): """Helper context manager to activate the environment. From cdc6499c789a19eda38b7f0c92c3e05de93e8090 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 23:05:24 -0400 Subject: [PATCH 6/8] Update environment.py --- pipenv/environment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pipenv/environment.py b/pipenv/environment.py index eee0367306..d328a5c97f 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -826,4 +826,3 @@ def activated(self): finally: sys.path = original_path sys.prefix = original_prefix - From ff8bbe40545d81c002b63558a954d54ab53e58a9 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 23:05:59 -0400 Subject: [PATCH 7/8] Update uninstall.py --- pipenv/routines/uninstall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/routines/uninstall.py b/pipenv/routines/uninstall.py index 7114bd0c23..7939ccbd35 100644 --- a/pipenv/routines/uninstall.py +++ b/pipenv/routines/uninstall.py @@ -5,7 +5,7 @@ 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 import console from pipenv.utils.dependencies import ( expansive_install_req_from_line, get_lockfile_section_using_pipfile_category, From 3cabf795f14842e65b5d6d5a26b38378867f4366 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 22 Sep 2024 23:14:23 -0400 Subject: [PATCH 8/8] Update environment.py --- pipenv/environment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pipenv/environment.py b/pipenv/environment.py index d328a5c97f..a93817303d 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -99,8 +99,8 @@ def safe_import(self, name: str) -> ModuleType: @cached_property def python_version(self) -> str | None: - with self.activated() as e: - if e.ok: + with self.activated() as active: + if active: sysconfig = self.safe_import("sysconfig") py_version = sysconfig.get_python_version() return py_version