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

Prevent regression with install upgrading packages; duplicated sources in Pipfile prevention #6309

Merged
merged 4 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all 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/6306.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix issue where pipenv install would unintentionally upgrade packages that had wildcard (*) specifiers in the Pipfile, even when locked versions existed and no upgrade was requested.
29 changes: 24 additions & 5 deletions pipenv/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -1273,26 +1273,45 @@ def src_name_from_url(self, index_url):
return name

def add_index_to_pipfile(self, index, verify_ssl=True):
"""Adds a given index to the Pipfile."""
"""
Adds a given index to the Pipfile if it doesn't already exist.
Returns the source name regardless of whether it was newly added or already existed.
"""
# Read and append Pipfile.
p = self.parsed_pipfile
source = None

# Try to find existing source by URL or name
try:
source = self.get_source(url=index)
except SourceNotFound:
with contextlib.suppress(SourceNotFound):
source = self.get_source(name=index)

# If we found an existing source with a name, return it
if source is not None and source.get("name"):
return source["name"]
source = {"url": index, "verify_ssl": verify_ssl}
source["name"] = self.src_name_from_url(index)
# Add the package to the group.

# Check if the URL already exists in any source
if "source" in p:
for existing_source in p["source"]:
if existing_source.get("url") == index:
return existing_source.get("name")

# If we reach here, the source doesn't exist, so create and add it
source = {
"url": index,
"verify_ssl": verify_ssl,
"name": self.src_name_from_url(index),
}

# Add the source to the group
if "source" not in p:
p["source"] = [tomlkit.item(source)]
else:
p["source"].append(tomlkit.item(source))
# Write Pipfile.

# Write Pipfile
self.write_toml(p)
return source["name"]

Expand Down
2 changes: 1 addition & 1 deletion pipenv/routines/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def handle_new_packages(
project.update_settings({"allow_prereleases": pre})

# Use the update routine for new packages
if perform_upgrades:
if perform_upgrades and (packages or editable_packages):
try:
do_update(
project,
Expand Down
5 changes: 1 addition & 4 deletions pipenv/routines/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,7 @@ def upgrade(

index_name = None
if index_url:
if project.get_index_by_url(index_url):
index_name = project.get_index_by_url(index_url)["name"]
else:
index_name = add_index_to_pipfile(project, index_url)
index_name = add_index_to_pipfile(project, index_url)

if extra_pip_args:
os.environ["PIPENV_EXTRA_PIP_ARGS"] = json.dumps(extra_pip_args)
Expand Down
66 changes: 66 additions & 0 deletions tests/integration/test_install_basic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import re
import sys
Expand Down Expand Up @@ -740,3 +741,68 @@ def test_category_sorted_with_directive_when_insalling_with_extras(
"requests",
"six",
]


@pytest.mark.basic
@pytest.mark.install
def test_install_respects_lockfile_versions(pipenv_instance_pypi):
"""Ensure that `pipenv install` respects versions from existing lockfile."""
with pipenv_instance_pypi() as p:
with open(p.pipfile_path, "w") as f:
contents = """
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
sh = "*"
""".strip()
f.write(contents)

with open(p.lockfile_path, "w") as f:
contents = """
{
"_meta": {
"hash": {
"sha256": "f9adf532d46f4787b6afe331abe415d5698ef7523cd6225605328b61f361d427"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"sh": {
"hashes": [
"sha256:39aa9af22f6558a0c5d132881cf43e34828ca03e4ae11114852ca6a55c7c1d8e",
"sha256:75e86a836f47de095d4531718fe8489e6f7446c75ddfa5596f632727b919ffae"
],
"index": "pypi",
"version": "==1.14.1"
}
},
"develop": {}
}
""".strip()
f.write(contents)

c = p.pipenv("install")
assert c.returncode == 0

# Verify that the locked version is installed, not the latest
c = p.pipenv("graph --json")
assert c.returncode == 0

graph_data = json.loads(c.stdout)
for entry in graph_data:
if entry["package"]["package_name"] == "sh":
assert entry["package"]["installed_version"] == "1.14.1"
break
else:
pytest.fail("sh package not found in graph output")
2 changes: 1 addition & 1 deletion tests/integration/test_install_vcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_vcs_dev_package_install(pipenv_instance_pypi):

# Verify package appears in develop section of lockfile
assert "pytest-xdist" in p.lockfile["develop"]
assert p.lockfile["develop"]["pytest-xdist"]["git"] == "git+https://github.com/pytest-dev/pytest-xdist.git"
assert p.lockfile["develop"]["pytest-xdist"]["git"] == "https://github.com/pytest-dev/pytest-xdist.git"
assert p.lockfile["develop"]["pytest-xdist"]["ref"] == "4dd2978031eaf7017c84a1cc77667379a2b11c64"

# Verify the package is importable
Expand Down
Loading