diff --git a/news/6362.bugfix.rst b/news/6362.bugfix.rst new file mode 100644 index 000000000..2a4ff2472 --- /dev/null +++ b/news/6362.bugfix.rst @@ -0,0 +1 @@ +Fix for PEP660 editable VCS dependencies not reinstalled correctly. diff --git a/pipenv/environment.py b/pipenv/environment.py index 71009d507..2001e6348 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -15,6 +15,7 @@ import pipenv from pipenv.patched.pip._internal.commands.install import InstallCommand from pipenv.patched.pip._internal.index.package_finder import PackageFinder +from pipenv.patched.pip._internal.locations.base import get_src_prefix from pipenv.patched.pip._internal.req.req_install import InstallRequirement from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name @@ -745,6 +746,24 @@ def is_satisfied(self, req: InstallRequirement): match.version, prereleases=True ) if req.link is None: + return True + elif req.editable and req.link.is_vcs: + # For editable VCS dependencies, check if the source directory exists + # This ensures we reinstall if the source checkout is missing + # Use get_src_prefix() to get the appropriate src directory + # This handles both virtualenv and non-virtualenv cases + src_dir = get_src_prefix() + + # If the src directory doesn't exist, the requirement is not satisfied + if not os.path.exists(src_dir): + return False + + # If we have a specific package directory, check that too + if req.name: + pkg_dir = os.path.join(src_dir, req.name) + if not os.path.exists(pkg_dir): + return False + return True elif req.editable and req.link.is_file: requested_path = req.link.file_path diff --git a/tests/integration/test_editable_vcs.py b/tests/integration/test_editable_vcs.py new file mode 100644 index 000000000..52f38b267 --- /dev/null +++ b/tests/integration/test_editable_vcs.py @@ -0,0 +1,64 @@ +import shutil +from pathlib import Path + +import pytest + + +@pytest.mark.integration +@pytest.mark.install +@pytest.mark.editable +@pytest.mark.vcs +def test_editable_vcs_reinstall(pipenv_instance_private_pypi): + """Test that editable VCS dependencies are reinstalled when the source checkout is missing.""" + with pipenv_instance_private_pypi() as p: + # Create a Pipfile with an editable VCS dependency + with open(p.pipfile_path, "w") as f: + f.write(""" +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +gunicorn = {git = "https://github.com/benoitc/gunicorn", ref = "23.0.0", editable = true} + """.strip()) + + # Install the dependency + c = p.pipenv("install") + assert c.returncode == 0, f"Failed to install: {c.stderr}" + + # Verify the src directory was created + # The src directory could be in the project directory or in the virtualenv directory + src_dir_project = Path(p.path) / "src" + src_dir_venv = Path(p.virtualenv_location) / "src" + + # Check if either src directory exists + src_dir = src_dir_project if src_dir_project.exists() else src_dir_venv + assert src_dir.exists(), f"src directory was not created in either {src_dir_project} or {src_dir_venv}" + assert any(src_dir.iterdir()), "src directory is empty" + + # Import the package to verify it's installed correctly + c = p.pipenv("run python -c 'import gunicorn'") + assert c.returncode == 0, f"Failed to import gunicorn: {c.stderr}" + + # Remove the src directory to simulate the issue + shutil.rmtree(src_dir) + assert not src_dir.exists(), "Failed to remove src directory" + + # Run pipenv install again to see if it reinstalls the dependency + c = p.pipenv("install") + assert c.returncode == 0, f"Failed to reinstall: {c.stderr}" + + # Verify the src directory was recreated + # Check both possible locations again + src_dir_project = Path(p.path) / "src" + src_dir_venv = Path(p.virtualenv_location) / "src" + + # Check if either src directory exists + src_dir = src_dir_project if src_dir_project.exists() else src_dir_venv + assert src_dir.exists(), f"src directory was not recreated in either {src_dir_project} or {src_dir_venv}" + assert any(src_dir.iterdir()), "recreated src directory is empty" + + # Import the package again to verify it's reinstalled correctly + c = p.pipenv("run python -c 'import gunicorn'") + assert c.returncode == 0, f"Failed to import gunicorn after reinstall: {c.stderr}"