Skip to content

Commit 54e3182

Browse files
committed
Fix issues around lower bounds
1 parent 3689720 commit 54e3182

5 files changed

Lines changed: 97 additions & 42 deletions

File tree

spec0_action/__init__.py

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ def update_pyproject_dependencies(dependencies: list, schedule: Dict[str, str]):
108108
dependencies[i] = new_dep_str
109109

110110

111+
def iter_pep_dependency_lists(pyproject_data: dict, project_data: dict):
112+
dependencies = project_data.get("dependencies")
113+
if isinstance(dependencies, list):
114+
yield dependencies
115+
116+
optional_dependencies = project_data.get("optional-dependencies", {})
117+
if isinstance(optional_dependencies, dict):
118+
for dependencies in optional_dependencies.values():
119+
if isinstance(dependencies, list):
120+
yield dependencies
121+
122+
dependency_groups = pyproject_data.get("dependency-groups", {})
123+
if isinstance(dependency_groups, dict):
124+
for dependencies in dependency_groups.values():
125+
if isinstance(dependencies, list):
126+
yield dependencies
127+
128+
111129
def update_dependency_table(dep_table: dict, new_versions: dict):
112130
for pkg, pkg_data in dep_table.items():
113131
schedule_key = canonicalize_name(pkg)
@@ -119,7 +137,10 @@ def update_dependency_table(dep_table: dict, new_versions: dict):
119137
if not is_url_spec(pkg_data):
120138
spec = parse_version_spec(pkg_data)
121139
new_lower_bound = Version(new_versions[schedule_key])
122-
spec = tighten_lower_bound(spec, new_lower_bound)
140+
try:
141+
spec = tighten_lower_bound(spec, new_lower_bound)
142+
except ValueError:
143+
continue
123144
dep_table[pkg] = repr_spec_set(spec)
124145
else:
125146
# We don't do anything with url spec dependencies
@@ -131,7 +152,10 @@ def update_dependency_table(dep_table: dict, new_versions: dict):
131152
continue
132153
spec = parse_version_spec(pkg_data["version"])
133154
new_lower_bound = Version(new_versions[schedule_key])
134-
spec = tighten_lower_bound(spec, new_lower_bound)
155+
try:
156+
spec = tighten_lower_bound(spec, new_lower_bound)
157+
except ValueError:
158+
continue
135159
pkg_data["version"] = repr_spec_set(spec)
136160

137161

@@ -188,42 +212,25 @@ def update_pyproject_toml(
188212
python_spec = parse_version_spec(new_version["python"])
189213
project_data["requires-python"] = repr_spec_set(python_spec)
190214

191-
dependencies = project_data.get("dependencies")
192-
if isinstance(dependencies, list):
215+
for dependencies in iter_pep_dependency_lists(pyproject_data, project_data):
193216
update_pyproject_dependencies(dependencies, new_version)
194217

195-
optional_dependencies = project_data.get("optional-dependencies", {})
196-
if isinstance(optional_dependencies, dict):
197-
for dependencies in optional_dependencies.values():
198-
if isinstance(dependencies, list):
199-
update_pyproject_dependencies(dependencies, new_version)
200-
201-
dependency_groups = pyproject_data.get("dependency-groups", {})
202-
if isinstance(dependency_groups, dict):
203-
for dependencies in dependency_groups.values():
204-
if isinstance(dependencies, list):
205-
update_pyproject_dependencies(dependencies, new_version)
206-
207218
if "tool" in pyproject_data and "pixi" in pyproject_data["tool"]:
208219
pixi_data = pyproject_data["tool"]["pixi"]
209220
update_pixi_dependencies(pixi_data, new_version)
210221
if update_all is not None:
211-
deps = project_data.get("dependencies", [])
212-
for i, dep_str in enumerate(deps):
213-
if not isinstance(dep_str, str):
214-
continue
215-
pkg, extras, spec, env = parse_pep_dependency(dep_str)
216-
if (
217-
canonicalize_name(pkg) in new_version
218-
or isinstance(spec, Url)
219-
or spec is None
220-
):
221-
continue
222-
min_ver = _get_oldest_version_in_window(pkg, update_all)
223-
if min_ver is None:
224-
continue
225-
try:
226-
updated = tighten_lower_bound(spec, min_ver)
227-
deps[i] = f"{pkg}{extras or ''}{repr_spec_set(updated)}{env or ''}"
228-
except ValueError:
229-
continue
222+
for deps in iter_pep_dependency_lists(pyproject_data, project_data):
223+
for i, dep_str in enumerate(deps):
224+
if not isinstance(dep_str, str):
225+
continue
226+
pkg, extras, spec, env = parse_pep_dependency(dep_str)
227+
if canonicalize_name(pkg) in new_version or isinstance(spec, Url):
228+
continue
229+
min_ver = _get_oldest_version_in_window(pkg, update_all)
230+
if min_ver is None:
231+
continue
232+
try:
233+
updated = tighten_lower_bound(spec or SpecifierSet(), min_ver)
234+
deps[i] = f"{pkg}{extras or ''}{repr_spec_set(updated)}{env or ''}"
235+
except ValueError:
236+
continue

spec0_action/versions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
def tighten_lower_bound(
66
spec_set: SpecifierSet, new_lower_bound: Version
77
) -> SpecifierSet:
8+
if new_lower_bound not in spec_set:
9+
raise ValueError(f"{new_lower_bound} does not satisfy {spec_set}")
10+
811
out = []
912
contains_lower_bound = False
1013

tests/test_data/pyproject_pixi_updated.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ xarray = ">=2024.1.0"
3131
bar = ["foo"]
3232

3333
[tool.pixi.dependencies]
34-
numpy = ">=2.0.0,<2"
34+
numpy = ">=1.10.0,<2"

tests/test_update_pyproject_toml.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ def test_update_all_noop_when_not_set(patch_datetime_now):
9292
mock_pypi.assert_not_called()
9393

9494

95+
def test_update_all_updates_optional_dependency_groups_and_unbounded(
96+
patch_datetime_now,
97+
):
98+
pyproject = _minimal_pyproject("requests")
99+
pyproject["project"]["optional-dependencies"] = {
100+
"test": ["idna>=3.0.0"],
101+
}
102+
pyproject["dependency-groups"] = {
103+
"dev": ["charset-normalizer>=3.0.0", {"include-group": "test"}],
104+
}
105+
schedule = read_schedule("tests/test_data/test_schedule.json")
106+
with patch.object(
107+
spec0_action, "_get_oldest_version_in_window", return_value=Version("9.0.0")
108+
):
109+
update_pyproject_toml(pyproject, schedule, update_all=2.0)
110+
111+
assert pyproject["project"]["dependencies"] == ["requests>=9.0.0"]
112+
assert pyproject["project"]["optional-dependencies"]["test"] == ["idna>=9.0.0"]
113+
assert pyproject["dependency-groups"]["dev"] == [
114+
"charset-normalizer>=9.0.0",
115+
{"include-group": "test"},
116+
]
117+
118+
95119
def test_requires_python_preserves_existing_restrictions(patch_datetime_now):
96120
pyproject = _minimal_pyproject()
97121
pyproject["project"]["requires-python"] = ">=3.9,<3.14,!=3.13.*"
@@ -104,6 +128,18 @@ def test_requires_python_preserves_existing_restrictions(patch_datetime_now):
104128
)
105129

106130

131+
def test_requires_python_keeps_incompatible_existing_restrictions(patch_datetime_now):
132+
pyproject = _minimal_pyproject()
133+
pyproject["project"]["requires-python"] = ">=3.9,<3.12"
134+
schedule = read_schedule("tests/test_data/test_schedule.json")
135+
136+
update_pyproject_toml(pyproject, schedule)
137+
138+
assert SpecifierSet(pyproject["project"]["requires-python"]) == SpecifierSet(
139+
">=3.9,<3.12"
140+
)
141+
142+
107143
def test_canonical_package_names_match_schedule(patch_datetime_now):
108144
pyproject = _minimal_pyproject("Numpy>=1.20", "scikit_learn>=1.0")
109145
schedule = read_schedule("tests/test_data/test_schedule.json")

tests/test_versions.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from packaging.version import Version
22
from spec0_action.versions import repr_spec_set, tighten_lower_bound
33
from packaging.specifiers import SpecifierSet
4+
import pytest
45

56

67
def test_repr_specset():
@@ -16,14 +17,22 @@ def test_tighter_lower_bound_any():
1617

1718

1819
def test_tighter_lower_bound_leaves_other_restrictions():
19-
spec = SpecifierSet("~= 0.9,>=1.0,!= 1.3.4.*,< 2.0")
20-
lower_bound = Version("3.8.0")
20+
spec = SpecifierSet(">=1.0,!= 1.3.4.*,< 2.0")
21+
lower_bound = Version("1.4.0")
2122
tightened = tighten_lower_bound(spec, lower_bound)
22-
assert tightened == SpecifierSet("~= 0.9,>=3.8.0,!=1.3.4.*,<2.0")
23+
assert tightened == SpecifierSet(">=1.4.0,!=1.3.4.*,<2.0")
2324

2425

2526
def test_tighter_lower_bound_adds_lower_bound_if_not_present():
26-
spec = SpecifierSet("~=0.9,!=1.3.4.*,<2.0")
27-
lower_bound = Version("3.8.0")
27+
spec = SpecifierSet("!=1.3.4.*,<2.0")
28+
lower_bound = Version("1.4.0")
2829
tightened = tighten_lower_bound(spec, lower_bound)
29-
assert tightened == SpecifierSet("~= 0.9, != 1.3.4.*, < 2.0, >=3.8.0")
30+
assert tightened == SpecifierSet("!=1.3.4.*,<2.0,>=1.4.0")
31+
32+
33+
def test_tighter_lower_bound_rejects_incompatible_restrictions():
34+
spec = SpecifierSet(">=1.0,<2.0")
35+
lower_bound = Version("2.0.0")
36+
37+
with pytest.raises(ValueError):
38+
tighten_lower_bound(spec, lower_bound)

0 commit comments

Comments
 (0)