Skip to content

Commit cf4c3ec

Browse files
committed
Fix direct URL dependencies not stored in lockfile
When a package has an extra dependency with a direct URL using PEP 508 syntax (e.g., my-package @ https://server.com/my-package-1.0.0.whl), the URL was not being stored in the lockfile. This caused pipenv to fall back to searching PyPI for the package, which would fail if the package is only available at the private URL. Root cause: format_requirement_for_lockfile only checked for req.link.is_file (which only matches file:// URLs), but not for remote HTTP/HTTPS URLs. The fix adds handling for http and https schemes, storing the URL in the 'file' key of the lockfile entry (same as file:// URLs). Fixes #5967
1 parent ac013e3 commit cf4c3ec

File tree

2 files changed

+148
-2
lines changed

2 files changed

+148
-2
lines changed

pipenv/utils/locking.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,14 @@ def format_requirement_for_lockfile(
8585
entry["version"] = str(req.req.specifier)
8686
elif req.specifier:
8787
entry["version"] = str(req.specifier)
88-
if req.link and req.link.is_file:
89-
entry["file"] = req.link.url
88+
if req.link:
89+
if req.link.is_file:
90+
entry["file"] = req.link.url
91+
elif req.link.scheme in ("http", "https"):
92+
# Handle direct URL dependencies (PEP 508 style: package @ https://...)
93+
entry["file"] = req.link.url
94+
entry.pop("version", None) # URL deps don't need version
95+
entry.pop("index", None) # URL deps don't use index
9096
# Add index information
9197
if name in index_lookup:
9298
entry["index"] = index_lookup[name]

tests/unit/test_utils.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,3 +779,143 @@ def get_value(self, key):
779779
assert project.default_source["url"] in [primary_index, extra_index]
780780
finally:
781781
os.chdir(original_dir)
782+
783+
784+
class TestFormatRequirementForLockfile:
785+
"""Tests for format_requirement_for_lockfile in locking.py.
786+
787+
These tests verify that various requirement types are correctly formatted
788+
for the lockfile, including direct URL dependencies (PEP 508 style).
789+
"""
790+
791+
def test_direct_url_dependency_https(self):
792+
"""Test that HTTPS direct URL dependencies are stored in lockfile.
793+
794+
This is the fix for issue #5967 - when a package has an extra dependency
795+
with a direct URL like:
796+
my-private-dependency @ https://my-private-artifactory/.../package.whl
797+
The URL should be stored in the lockfile entry.
798+
"""
799+
from pipenv.patched.pip._internal.models.link import Link
800+
from pipenv.patched.pip._internal.req.constructors import (
801+
install_req_from_line,
802+
)
803+
from pipenv.utils.locking import format_requirement_for_lockfile
804+
805+
# Create an InstallRequirement with a direct HTTPS URL (PEP 508 style)
806+
req_str = "my-private-package @ https://my-artifactory.com/api/pypi/repo/my-private-package/1.0.0/my-private-package-1.0.0-py3-none-any.whl"
807+
req = install_req_from_line(req_str)
808+
809+
# Verify the link properties
810+
assert req.link is not None
811+
assert req.link.scheme == "https"
812+
assert req.link.is_file is False
813+
assert req.link.is_vcs is False
814+
815+
# Format for lockfile
816+
name, entry = format_requirement_for_lockfile(
817+
req,
818+
markers_lookup={},
819+
index_lookup={},
820+
original_deps={},
821+
pipfile_entries={},
822+
hashes=None,
823+
)
824+
825+
# Verify the URL is stored in the lockfile entry
826+
assert name == "my-private-package"
827+
assert "file" in entry, "Direct URL should be stored in 'file' key"
828+
assert entry["file"] == "https://my-artifactory.com/api/pypi/repo/my-private-package/1.0.0/my-private-package-1.0.0-py3-none-any.whl"
829+
assert "version" not in entry, "URL deps should not have version"
830+
assert "index" not in entry, "URL deps should not have index"
831+
832+
def test_direct_url_dependency_http(self):
833+
"""Test that HTTP direct URL dependencies are stored in lockfile."""
834+
from pipenv.patched.pip._internal.req.constructors import (
835+
install_req_from_line,
836+
)
837+
from pipenv.utils.locking import format_requirement_for_lockfile
838+
839+
# Create an InstallRequirement with a direct HTTP URL
840+
req_str = "example-package @ http://internal-server.local/packages/example-package-2.0.0.tar.gz"
841+
req = install_req_from_line(req_str)
842+
843+
# Verify the link properties
844+
assert req.link is not None
845+
assert req.link.scheme == "http"
846+
847+
# Format for lockfile
848+
name, entry = format_requirement_for_lockfile(
849+
req,
850+
markers_lookup={},
851+
index_lookup={},
852+
original_deps={},
853+
pipfile_entries={},
854+
hashes=None,
855+
)
856+
857+
# Verify the URL is stored
858+
assert name == "example-package"
859+
assert "file" in entry
860+
assert entry["file"] == "http://internal-server.local/packages/example-package-2.0.0.tar.gz"
861+
862+
def test_file_url_dependency(self):
863+
"""Test that local file:// URLs are still handled correctly."""
864+
from pipenv.patched.pip._internal.req.constructors import (
865+
install_req_from_line,
866+
)
867+
from pipenv.utils.locking import format_requirement_for_lockfile
868+
869+
# Create an InstallRequirement with a file:// URL
870+
req_str = "local-package @ file:///home/user/packages/local-package-1.0.0.whl"
871+
req = install_req_from_line(req_str)
872+
873+
# Verify the link properties
874+
assert req.link is not None
875+
assert req.link.scheme == "file"
876+
assert req.link.is_file is True
877+
878+
# Format for lockfile
879+
name, entry = format_requirement_for_lockfile(
880+
req,
881+
markers_lookup={},
882+
index_lookup={},
883+
original_deps={},
884+
pipfile_entries={},
885+
hashes=None,
886+
)
887+
888+
# Verify the URL is stored
889+
assert name == "local-package"
890+
assert "file" in entry
891+
assert entry["file"] == "file:///home/user/packages/local-package-1.0.0.whl"
892+
893+
def test_regular_pypi_dependency(self):
894+
"""Test that regular PyPI dependencies still work correctly."""
895+
from pipenv.patched.pip._internal.req.constructors import (
896+
install_req_from_line,
897+
)
898+
from pipenv.utils.locking import format_requirement_for_lockfile
899+
900+
# Create a regular PyPI requirement
901+
req_str = "requests==2.28.0"
902+
req = install_req_from_line(req_str)
903+
904+
# Regular requirements don't have a link
905+
assert req.link is None
906+
907+
# Format for lockfile
908+
name, entry = format_requirement_for_lockfile(
909+
req,
910+
markers_lookup={},
911+
index_lookup={},
912+
original_deps={},
913+
pipfile_entries={},
914+
hashes=None,
915+
)
916+
917+
# Verify version is stored, not URL
918+
assert name == "requests"
919+
assert "version" in entry
920+
assert entry["version"] == "==2.28.0"
921+
assert "file" not in entry

0 commit comments

Comments
 (0)