Skip to content

Commit 4038ebc

Browse files
matteiusclaude
andcommitted
fix: don't mutate cached parsed_pipfile when locking deps
get_locked_dep popped ``version`` and ``ref`` directly off the entry it received from ``pipfile_section``. Since #6649 made ``parsed_pipfile`` return a cached TOMLDocument by reference, those pops persisted across the rest of the pipenv invocation — a subsequent ``write_toml`` (e.g. ``add_pipfile_entry_to_pipfile`` for the newly installed package) would emit ``six = {}`` instead of ``six = {version = "*"}`` and strip the version from any inline-table or outline-table siblings. Copy the dict before scrubbing those keys. Add a unit regression test that asserts get_locked_dep leaves the section untouched. Fixes the integration regression hit by ``test_rewrite_outline_table`` and ``test_rewrite_outline_table_ooo`` on main since the pip 26.1 vendoring run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 75a07fc commit 4038ebc

3 files changed

Lines changed: 57 additions & 4 deletions

File tree

news/6657.bugfix.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix ``pipenv install`` corrupting existing inline-table or outline-table
2+
Pipfile entries (``six = {version = "*"}``, ``[packages.requests]``). The
3+
locker was popping ``version``/``ref`` keys directly off the cached
4+
``parsed_pipfile`` document, so subsequent writes emitted
5+
``six = {}`` and dropped the version specifier from sibling packages.

pipenv/utils/locking.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,14 @@ def get_locked_dep(project, dep, pipfile_section, current_entry=None):
171171
if pep423_name(pipfile_key) == dep_name or pipfile_key == dep_name:
172172
is_top_level = True
173173
if isinstance(pipfile_entry, dict):
174-
if pipfile_entry.get("version"):
175-
pipfile_entry.pop("version")
176-
if pipfile_entry.get("ref"):
177-
pipfile_entry.pop("ref")
174+
# Copy before mutating: pipfile_section is the cached
175+
# parsed_pipfile, so popping in place strips the original
176+
# Pipfile entry and corrupts the next write_toml.
177+
pipfile_entry = {
178+
k: v
179+
for k, v in pipfile_entry.items()
180+
if k not in ("version", "ref")
181+
}
178182
dep.update(pipfile_entry)
179183
break
180184

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Regression test for Pipfile cache corruption during lock.
2+
3+
Before this fix, ``get_locked_dep`` popped ``version`` and ``ref`` keys
4+
in-place from the entry it received. That entry is sourced from the
5+
cached ``parsed_pipfile`` document, so the mutation persisted across the
6+
rest of the pipenv invocation — the next ``write_toml`` call would emit
7+
``six = {}`` instead of ``six = {version = "*"}``.
8+
9+
This test patches ``clean_resolved_dep`` (the only project-dependent
10+
collaborator of ``get_locked_dep``) to a no-op and asserts that the
11+
input ``pipfile_section`` is left untouched.
12+
"""
13+
from unittest import mock
14+
15+
from pipenv.utils import locking
16+
17+
18+
def _identity_clean_resolved_dep(project, dep, is_top_level=False, current_entry=None):
19+
return {dep["name"]: dict(dep)}
20+
21+
22+
def test_get_locked_dep_does_not_mutate_pipfile_section():
23+
pipfile_section = {
24+
"six": {"version": "*"},
25+
"requests": {"version": "*", "extras": ["socks"]},
26+
"mypkg": {"git": "https://example.com/mypkg.git", "ref": "main"},
27+
}
28+
snapshot = {k: dict(v) for k, v in pipfile_section.items()}
29+
30+
with mock.patch.object(
31+
locking, "clean_resolved_dep", side_effect=_identity_clean_resolved_dep
32+
):
33+
for dep in (
34+
{"name": "six", "version": "==1.16.0"},
35+
{"name": "requests", "version": "==2.32.3"},
36+
{"name": "mypkg"},
37+
):
38+
locking.get_locked_dep(
39+
project=None, dep=dep, pipfile_section=pipfile_section
40+
)
41+
42+
assert pipfile_section["six"] == snapshot["six"]
43+
assert pipfile_section["requests"] == snapshot["requests"]
44+
assert pipfile_section["mypkg"] == snapshot["mypkg"]

0 commit comments

Comments
 (0)