From 168a052b33de4797baa09b2dff55f8d94f166a0e Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 24 Apr 2025 15:11:18 -0400 Subject: [PATCH 1/6] Cleanup unused dependencies --- pipenv/routines/update.py | 62 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/pipenv/routines/update.py b/pipenv/routines/update.py index 41882e7b7..5ca629784 100644 --- a/pipenv/routines/update.py +++ b/pipenv/routines/update.py @@ -441,6 +441,39 @@ def _resolve_and_update_lockfile( return upgrade_lock_data +def _clean_unused_dependencies( + project, lockfile, category, full_lock_resolution, original_lockfile +): + """ + Remove dependencies that are no longer needed after an upgrade. + + Args: + project: The project instance + lockfile: The current lockfile being built + category: The category to clean (e.g., 'default', 'develop') + full_lock_resolution: The complete resolution of dependencies + original_lockfile: The original lockfile before the upgrade + """ + if category not in lockfile or category not in original_lockfile: + return + + # Get the set of packages in the new resolution + resolved_packages = set(full_lock_resolution.keys()) + + # Get the set of packages in the original lockfile for this category + original_packages = set(original_lockfile[category].keys()) + + # Find packages that were in the original lockfile but not in the new resolution + unused_packages = original_packages - resolved_packages + + # Remove unused packages from the lockfile + for package_name in unused_packages: + if package_name in lockfile[category]: + if project.s.is_verbose(): + err.print(f"Removing unused dependency: {package_name}") + del lockfile[category][package_name] + + def upgrade( project, pre=False, @@ -456,6 +489,11 @@ def upgrade( ): """Enhanced upgrade command with dependency conflict detection.""" lockfile = project.lockfile() + # Store the original lockfile for comparison later + original_lockfile = { + k: v.copy() if isinstance(v, dict) else v for k, v in lockfile.items() + } + if not pre: pre = project.settings.get("allow_prereleases") @@ -504,6 +542,8 @@ def upgrade( # Process each category requested_packages = defaultdict(dict) + category_resolutions = {} + for category in categories: pipfile_category = get_pipfile_category_using_lockfile_section(category) @@ -528,7 +568,7 @@ def upgrade( ) # Resolve dependencies and update lockfile - _resolve_and_update_lockfile( + upgrade_lock_data = _resolve_and_update_lockfile( project, requested_packages, pipfile_category, @@ -540,6 +580,26 @@ def upgrade( lockfile, ) + # Store the full resolution for this category + if upgrade_lock_data: + complete_packages = project.parsed_pipfile.get(pipfile_category, {}) + full_lock_resolution = venv_resolve_deps( + complete_packages, + which=project._which, + project=project, + lockfile={}, + pipfile_category=pipfile_category, + pre=pre, + allow_global=system, + pypi_mirror=pypi_mirror, + ) + category_resolutions[category] = full_lock_resolution + + # Clean up unused dependencies + _clean_unused_dependencies( + project, lockfile, category, full_lock_resolution, original_lockfile + ) + # Reset package args for next category if needed if not has_package_args: package_args = [] From 0933a70153d362d2047e9fbf03f68f21ba169e82 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 24 Apr 2025 15:13:40 -0400 Subject: [PATCH 2/6] Cleanup unused dependencies --- tests/integration/test_upgrade_cleanup.py | 49 +++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/integration/test_upgrade_cleanup.py diff --git a/tests/integration/test_upgrade_cleanup.py b/tests/integration/test_upgrade_cleanup.py new file mode 100644 index 000000000..425d38e71 --- /dev/null +++ b/tests/integration/test_upgrade_cleanup.py @@ -0,0 +1,49 @@ +import os +import shutil +from pathlib import Path + +import pytest + +from pipenv.project import Project +from pipenv.utils.processes import subprocess_run + + +@pytest.mark.upgrade +@pytest.mark.cleanup +def test_upgrade_removes_unused_dependencies(PipenvInstance): + """Test that `pipenv upgrade` removes dependencies that are no longer needed.""" + with PipenvInstance(chdir=True) as p: + # Create a Pipfile with Django 3.2.10 (which depends on pytz) + with open(p.pipfile_path, "w") as f: + f.write(""" +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "==3.2.10" + +[dev-packages] + +[requires] +python_version = "3.11" +""") + + # Install dependencies + c = p.pipenv("install") + assert c.returncode == 0 + + # Verify pytz is in the lockfile + project = Project() + lockfile = project.lockfile() + assert "pytz" in lockfile["default"] + + # Upgrade Django to 4.2.7 (which doesn't depend on pytz) + c = p.pipenv("upgrade django==4.2.7") + assert c.returncode == 0 + + # Verify pytz is no longer in the lockfile + project = Project() + lockfile = project.lockfile() + assert "pytz" not in lockfile["default"] From f6c7ca92a29a50dc8d3d56f21d9e8bef5c901475 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 25 Apr 2025 03:27:03 -0400 Subject: [PATCH 3/6] Cleanup unused dependencies --- tests/integration/test_upgrade_cleanup.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/test_upgrade_cleanup.py b/tests/integration/test_upgrade_cleanup.py index 425d38e71..64a32d48a 100644 --- a/tests/integration/test_upgrade_cleanup.py +++ b/tests/integration/test_upgrade_cleanup.py @@ -10,9 +10,9 @@ @pytest.mark.upgrade @pytest.mark.cleanup -def test_upgrade_removes_unused_dependencies(PipenvInstance): +def test_upgrade_removes_unused_dependencies(pipenv_instance_pypi): """Test that `pipenv upgrade` removes dependencies that are no longer needed.""" - with PipenvInstance(chdir=True) as p: + with pipenv_instance_pypi() as p: # Create a Pipfile with Django 3.2.10 (which depends on pytz) with open(p.pipfile_path, "w") as f: f.write(""" @@ -35,15 +35,11 @@ def test_upgrade_removes_unused_dependencies(PipenvInstance): assert c.returncode == 0 # Verify pytz is in the lockfile - project = Project() - lockfile = project.lockfile() - assert "pytz" in lockfile["default"] + assert "pytz" in p.lockfile["default"] # Upgrade Django to 4.2.7 (which doesn't depend on pytz) c = p.pipenv("upgrade django==4.2.7") assert c.returncode == 0 # Verify pytz is no longer in the lockfile - project = Project() - lockfile = project.lockfile() - assert "pytz" not in lockfile["default"] + assert "pytz" not in p.lockfile["default"] From fd050da532b44b89a825579ea89adaf194356d08 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 25 Apr 2025 03:27:15 -0400 Subject: [PATCH 4/6] Cleanup unused dependencies --- tests/integration/test_upgrade_cleanup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/test_upgrade_cleanup.py b/tests/integration/test_upgrade_cleanup.py index 64a32d48a..338c3716f 100644 --- a/tests/integration/test_upgrade_cleanup.py +++ b/tests/integration/test_upgrade_cleanup.py @@ -1,12 +1,6 @@ -import os -import shutil -from pathlib import Path import pytest -from pipenv.project import Project -from pipenv.utils.processes import subprocess_run - @pytest.mark.upgrade @pytest.mark.cleanup From 816b53fa6a15801da67fbd154ac843f6c6999459 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 25 Apr 2025 03:28:44 -0400 Subject: [PATCH 5/6] Add news --- news/6386.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6386.bugfix.rst diff --git a/news/6386.bugfix.rst b/news/6386.bugfix.rst new file mode 100644 index 000000000..fd9851163 --- /dev/null +++ b/news/6386.bugfix.rst @@ -0,0 +1 @@ +Cleanup unused dependencies when upgrading packages. From 68cbf59459768e709dc927e4b5e6b1b7225a5b60 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 25 Apr 2025 03:43:32 -0400 Subject: [PATCH 6/6] Correction --- tests/integration/test_upgrade_cleanup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/integration/test_upgrade_cleanup.py b/tests/integration/test_upgrade_cleanup.py index 338c3716f..11b529d9b 100644 --- a/tests/integration/test_upgrade_cleanup.py +++ b/tests/integration/test_upgrade_cleanup.py @@ -19,9 +19,6 @@ def test_upgrade_removes_unused_dependencies(pipenv_instance_pypi): django = "==3.2.10" [dev-packages] - -[requires] -python_version = "3.11" """) # Install dependencies