Skip to content
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