Skip to content

Commit 39a9a45

Browse files
committed
feat(find_updates): support ~=, !=, === constraint operators
Extend _get_constraint_version() to support additional constraint types: compatible release (~=), not equal (!=), and arbitrary equality (===). Changes: - Update _get_constraint_version() to recognize ~=, !=, === operators - Update function and command docstrings with new supported operators - Add comprehensive unit tests for new constraint types - Remove nonsensical test cases (==,<= and >=,> combinations) - Remove version format tests that tested packaging library, not our logic - Focus test suite on actual constraint parsing logic The find-updates command now handles more constraint types from pip constraints files, providing better coverage for dependency update analysis. Chat log: https://gist.github.com/dhellmann/79167f4bda606517d325f0ee7dbb0c9d Co-authored-by: Claude 3.5 Sonnet (Anthropic AI Assistant) Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
1 parent 7a848b9 commit 39a9a45

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

src/fromager/commands/find_updates.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,16 @@ def find_updates(
6464
"""Find available updates for packages with specific version constraints.
6565
6666
This command reads a constraints file and for each package that has a
67-
supported constraint (==, <, <=), it lists newer versions available.
67+
supported constraint (==, <, <=, ~=, !=, ===), it lists newer versions available.
6868
Constraints with lower bounds (>=) are ignored as they don't pin to specific versions.
6969
7070
The CONSTRAINTS_FILE should be a pip-style constraints file with entries like:
7171
- "package_name==1.0.0" (will look for versions > 1.0.0)
7272
- "package_name<2.0.0" (will look for versions >= 2.0.0)
7373
- "package_name<=1.5.0" (will look for versions > 1.5.0)
74+
- "package_name~=1.4.0" (will look for versions outside compatible range)
75+
- "package_name!=1.0.0" (will look for versions other than 1.0.0)
76+
- "package_name===1.0.0" (will look for versions other than exactly 1.0.0)
7477
- "package_name>=1.0.0" (ignored - not a specific constraint)
7578
7679
Distribution types:
@@ -159,14 +162,17 @@ def find_updates(
159162
output_file.close()
160163

161164

165+
CONSTRAINT_OPERATORS = ("==", "<", "<=", "~=", "!=", "===")
166+
167+
162168
def _get_constraint_version(constraint: Requirement) -> Version | None:
163-
"""Extract version from supported constraint types (==, <, <=), return None if not supported."""
169+
"""Extract version from supported constraint types (==, <, <=, ~=, !=, ===), return None if not supported."""
164170
if not constraint.specifier:
165171
return None
166172

167173
# Look for supported constraint operators
168174
supported_specs = [
169-
spec for spec in constraint.specifier if spec.operator in ("==", "<", "<=")
175+
spec for spec in constraint.specifier if spec.operator in CONSTRAINT_OPERATORS
170176
]
171177
if len(supported_specs) != 1:
172178
return None
@@ -240,12 +246,12 @@ def _find_newer_versions(
240246

241247
# Get the constraint operator to determine what "update" means
242248
constraint_spec = next(
243-
spec for spec in constraint.specifier if spec.operator in ("==", "<", "<=")
249+
spec for spec in constraint.specifier if spec.operator in CONSTRAINT_OPERATORS
244250
)
245-
if constraint_spec.operator == "==":
251+
if constraint_spec.operator in ("==", "==="):
246252
# For equality constraints, find versions newer than the pinned version
247253
newer_versions = [v for v in all_versions if v > constraint_version]
248-
elif constraint_spec.operator in ("<", "<="):
254+
elif constraint_spec.operator in ("<", "<=", "~=", "!="):
249255
# For upper bound constraints, find versions that exceed the constraint
250256
# (these would be versions that violate the current constraint)
251257
newer_versions = [v for v in all_versions if v >= constraint_version]

tests/test_find_updates.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from packaging.requirements import Requirement
2+
from packaging.version import Version
3+
4+
from fromager.commands.find_updates import _get_constraint_version
5+
6+
7+
class TestGetConstraintVersion:
8+
"""Test cases for _get_constraint_version function."""
9+
10+
def test_equality_constraint(self):
11+
"""Test that equality constraints return the correct version."""
12+
req = Requirement("package==1.2.3")
13+
result = _get_constraint_version(req)
14+
assert result == Version("1.2.3")
15+
16+
def test_less_than_constraint(self):
17+
"""Test that less than constraints return the correct version."""
18+
req = Requirement("package<2.0.0")
19+
result = _get_constraint_version(req)
20+
assert result == Version("2.0.0")
21+
22+
def test_less_than_or_equal_constraint(self):
23+
"""Test that less than or equal constraints return the correct version."""
24+
req = Requirement("package<=1.5.0")
25+
result = _get_constraint_version(req)
26+
assert result == Version("1.5.0")
27+
28+
def test_greater_than_constraint_unsupported(self):
29+
"""Test that greater than constraints return None (unsupported)."""
30+
req = Requirement("package>1.0.0")
31+
result = _get_constraint_version(req)
32+
assert result is None
33+
34+
def test_greater_than_or_equal_constraint_unsupported(self):
35+
"""Test that greater than or equal constraints return None (unsupported)."""
36+
req = Requirement("package>=1.0.0")
37+
result = _get_constraint_version(req)
38+
assert result is None
39+
40+
def test_no_version_constraint(self):
41+
"""Test that requirements without version constraints return None."""
42+
req = Requirement("package")
43+
result = _get_constraint_version(req)
44+
assert result is None
45+
46+
def test_multiple_constraints_with_one_supported(self):
47+
"""Test that multiple constraints with one supported constraint return the supported one."""
48+
req = Requirement("package>=1.0.0,<2.0.0")
49+
result = _get_constraint_version(req)
50+
assert result == Version("2.0.0")
51+
52+
def test_constraint_with_markers(self):
53+
"""Test that constraints with environment markers still work correctly."""
54+
req = Requirement("package==1.2.3; python_version >= '3.8'")
55+
result = _get_constraint_version(req)
56+
assert result == Version("1.2.3")
57+
58+
def test_tilde_equal_constraint_supported(self):
59+
"""Test that tilde equal constraints (~=) return the correct version."""
60+
req = Requirement("package~=1.4.2")
61+
result = _get_constraint_version(req)
62+
assert result == Version("1.4.2")
63+
64+
def test_not_equal_constraint_supported(self):
65+
"""Test that not equal constraints (!=) return the correct version."""
66+
req = Requirement("package!=1.0.0")
67+
result = _get_constraint_version(req)
68+
assert result == Version("1.0.0")
69+
70+
def test_arbitrary_equality_constraint_supported(self):
71+
"""Test that arbitrary equality constraints (===) return the correct version."""
72+
req = Requirement("package===1.0.0")
73+
result = _get_constraint_version(req)
74+
assert result == Version("1.0.0")
75+
76+
def test_invalid_version_format(self):
77+
"""Test that invalid version formats return None."""
78+
# We need to test the case where Version() construction fails
79+
# Let's create a requirement and then mock the Version parsing
80+
import unittest.mock
81+
82+
req = Requirement("package==1.0.0")
83+
84+
with unittest.mock.patch(
85+
"fromager.commands.find_updates.Version"
86+
) as mock_version:
87+
mock_version.side_effect = ValueError("Invalid version")
88+
result = _get_constraint_version(req)
89+
assert result is None

0 commit comments

Comments
 (0)