Skip to content

Commit 047d4bd

Browse files
authored
Fix for the install/upgrade star specifier (#6378)
* fix the star specifier * fix the star specifier +tests * add news fragment * reconsider * reconsider * correction
1 parent cf293d5 commit 047d4bd

File tree

3 files changed

+134
-17
lines changed

3 files changed

+134
-17
lines changed

news/6378.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix for parsing and using the star specifier in install and update/upgrade commands.

pipenv/routines/update.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,22 +160,44 @@ def check_version_conflicts(
160160
Returns set of conflicting packages.
161161
"""
162162
conflicts = set()
163-
try:
164-
new_version_obj = Version(new_version)
165-
except InvalidVersion:
166-
new_version_obj = SpecifierSet(new_version)
163+
164+
# Handle various wildcard patterns
165+
if new_version == "*":
166+
# Full wildcard - matches any version
167+
# We'll use a very permissive specifier
168+
new_version_obj = SpecifierSet(">=0.0.0")
169+
elif new_version.endswith(".*"):
170+
# Major version wildcard like '2.*'
171+
try:
172+
major = int(new_version[:-2])
173+
new_version_obj = SpecifierSet(f">={major},<{major+1}")
174+
except (ValueError, TypeError):
175+
# If we can't parse the major version, use a permissive specifier
176+
new_version_obj = SpecifierSet(">=0.0.0")
177+
else:
178+
try:
179+
new_version_obj = Version(new_version)
180+
except InvalidVersion:
181+
try:
182+
# Try to parse as a specifier set
183+
new_version_obj = SpecifierSet(new_version)
184+
except Exception: # noqa: PERF203
185+
# If we can't parse the version at all, return no conflicts
186+
# This allows the installation to proceed and let pip handle it
187+
return conflicts
167188

168189
for dependent, req_version in reverse_deps.get(package_name, set()):
169190
if req_version == "Any":
170191
continue
171192

172-
try:
173-
specifier_set = SpecifierSet(req_version)
193+
specifier_set = SpecifierSet(req_version)
194+
# For Version objects, we check if the specifier contains the version
195+
# For SpecifierSet objects, we need to check compatibility differently
196+
if isinstance(new_version_obj, Version):
174197
if not specifier_set.contains(new_version_obj):
175198
conflicts.add(dependent)
176-
except Exception: # noqa: PERF203
177-
# If we can't parse the version requirement, assume it's a conflict
178-
conflicts.add(dependent)
199+
# Otherwise this is a complex case where we have a specifier vs specifier ...
200+
# We'll let the resolver figure those out
179201

180202
return conflicts
181203

@@ -296,15 +318,21 @@ def _detect_conflicts(package_args, reverse_deps, lockfile):
296318
"""Detect version conflicts in package arguments."""
297319
conflicts_found = False
298320
for package in package_args:
321+
# Handle both == and = version specifiers
299322
if "==" in package:
300-
name, version = package.split("==")
301-
conflicts = check_version_conflicts(name, version, reverse_deps, lockfile)
302-
if conflicts:
303-
conflicts_found = True
304-
err.print(
305-
f"[red bold]Error[/red bold]: Updating [bold]{name}[/bold] "
306-
f"to version {version} would create conflicts with: {', '.join(sorted(conflicts))}"
307-
)
323+
name, version = package.split("==", 1) # Split only on the first occurrence
324+
elif "=" in package and not package.startswith("-e"): # Avoid matching -e flag
325+
name, version = package.split("=", 1) # Split only on the first occurrence
326+
else:
327+
continue # Skip packages without version specifiers
328+
329+
conflicts = check_version_conflicts(name, version, reverse_deps, lockfile)
330+
if conflicts:
331+
conflicts_found = True
332+
err.print(
333+
f"[red bold]Error[/red bold]: Updating [bold]{name}[/bold] "
334+
f"to version {version} would create conflicts with: {', '.join(sorted(conflicts))}"
335+
)
308336

309337
return conflicts_found
310338

tests/integration/test_install_misc.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,91 @@ def test_install_uri_with_extras(pipenv_instance_pypi):
2525
assert c.returncode == 0
2626
assert "plette" in p.lockfile["default"]
2727
assert "cerberus" in p.lockfile["default"]
28+
29+
30+
@pytest.mark.star
31+
@pytest.mark.install
32+
def test_install_major_version_star_specifier(pipenv_instance_pypi):
33+
"""Test that major version star specifiers like '1.*' work correctly."""
34+
with pipenv_instance_pypi() as p:
35+
with open(p.pipfile_path, "w") as f:
36+
contents = f"""
37+
[[source]]
38+
url = "{p.index_url}"
39+
verify_ssl = true
40+
name = "pypi"
41+
42+
[packages]
43+
six = "==1.*"
44+
"""
45+
f.write(contents)
46+
c = p.pipenv("install")
47+
assert c.returncode == 0
48+
assert "six" in p.lockfile["default"]
49+
50+
51+
@pytest.mark.star
52+
@pytest.mark.install
53+
def test_install_full_wildcard_specifier(pipenv_instance_pypi):
54+
"""Test that full wildcard specifiers '*' work correctly."""
55+
with pipenv_instance_pypi() as p:
56+
with open(p.pipfile_path, "w") as f:
57+
contents = f"""
58+
[[source]]
59+
url = "{p.index_url}"
60+
verify_ssl = true
61+
name = "pypi"
62+
63+
[packages]
64+
requests = "*"
65+
"""
66+
f.write(contents)
67+
c = p.pipenv("install")
68+
assert c.returncode == 0
69+
assert "requests" in p.lockfile["default"]
70+
71+
72+
@pytest.mark.star
73+
@pytest.mark.install
74+
def test_install_single_equals_star_specifier(pipenv_instance_pypi):
75+
"""Test that single equals star specifiers like '=8.*' work correctly."""
76+
with pipenv_instance_pypi() as p:
77+
with open(p.pipfile_path, "w") as f:
78+
contents = f"""
79+
[[source]]
80+
url = "{p.index_url}"
81+
verify_ssl = true
82+
name = "pypi"
83+
84+
[packages]
85+
requests = "==2.*"
86+
"""
87+
f.write(contents)
88+
c = p.pipenv("install")
89+
assert c.returncode == 0
90+
assert "requests" in p.lockfile["default"]
91+
assert p.lockfile["default"]["requests"]["version"].startswith("==2.")
92+
93+
94+
@pytest.mark.star
95+
@pytest.mark.install
96+
def test_install_command_with_star_specifier(pipenv_instance_pypi):
97+
"""Test that star specifiers work when used in the install command."""
98+
with pipenv_instance_pypi() as p:
99+
# Initialize pipfile first
100+
with open(p.pipfile_path, "w") as f:
101+
contents = f"""
102+
[[source]]
103+
url = "{p.index_url}"
104+
verify_ssl = true
105+
name = "pypi"
106+
107+
[packages]
108+
"""
109+
f.write(contents)
110+
111+
# Test with single equals and star specifier
112+
c = p.pipenv("install urllib3==1.*")
113+
assert c.returncode == 0
114+
assert "urllib3" in p.lockfile["default"]
115+

0 commit comments

Comments
 (0)