Skip to content

Cleanup unused dependencies #6386

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

Merged
merged 6 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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/6386.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cleanup unused dependencies when upgrading packages.
62 changes: 61 additions & 1 deletion pipenv/routines/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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")

Expand Down Expand Up @@ -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)

Expand All @@ -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,
Expand All @@ -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 = []
Expand Down
39 changes: 39 additions & 0 deletions tests/integration/test_upgrade_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

import pytest


@pytest.mark.upgrade
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome!

@pytest.mark.cleanup
def test_upgrade_removes_unused_dependencies(pipenv_instance_pypi):
"""Test that `pipenv upgrade` removes dependencies that are no longer needed."""
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("""
[[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
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
assert "pytz" not in p.lockfile["default"]
Loading