Skip to content

Commit 4d533fd

Browse files
committed
Normalize package names for schedule matching, preserve existing Python specifiers, update optional and grouped dependencies, and skip non-version Pixi table dependencies.
For update_all, select versions by their original release date instead of newer file upload dates.
1 parent aa86cd9 commit 4d533fd

1 file changed

Lines changed: 91 additions & 31 deletions

File tree

spec0_action/__init__.py

Lines changed: 91 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import contextlib
2+
from collections import defaultdict
23
from packaging.specifiers import SpecifierSet
34
from typing import Sequence, Dict
45
import datetime
@@ -15,7 +16,14 @@
1516
read_toml,
1617
write_toml,
1718
)
18-
from packaging.version import Version, InvalidVersion
19+
from packaging.version import Version
20+
from packaging.utils import (
21+
InvalidSdistFilename,
22+
InvalidWheelFilename,
23+
canonicalize_name,
24+
parse_sdist_filename,
25+
parse_wheel_filename,
26+
)
1927

2028
__all__ = ["read_schedule", "read_toml", "write_toml", "update_pyproject_toml"]
2129

@@ -37,17 +45,12 @@ def _get_oldest_version_in_window(package: str, years: float) -> Version | None:
3745
data = resp.json()
3846
except Exception:
3947
return None
40-
candidates: list[Version] = []
48+
release_dates: dict[Version, list[datetime.datetime]] = defaultdict(list)
4149
for f in data.get("files", []):
42-
parts = f.get("filename", "").split("-")
43-
if len(parts) < 2:
44-
continue
45-
try:
46-
ver = Version(parts[1])
47-
except InvalidVersion:
48-
continue
49-
if ver.is_prerelease:
50+
ver = _version_from_filename(f.get("filename", ""))
51+
if ver is None or ver.is_prerelease:
5052
continue
53+
5154
upload_str = f.get("upload-time", "")
5255
upload_time = None
5356
for fmt in ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"]:
@@ -56,21 +59,43 @@ def _get_oldest_version_in_window(package: str, years: float) -> Version | None:
5659
tzinfo=datetime.timezone.utc
5760
)
5861
break
59-
if upload_time is None or upload_time < cutoff:
62+
if upload_time is None:
6063
continue
61-
candidates.append(ver)
64+
release_dates[ver].append(upload_time)
6265

66+
candidates = [
67+
ver
68+
for ver, upload_times in release_dates.items()
69+
if min(upload_times) >= cutoff
70+
]
6371
return min(candidates, default=None)
6472

6573

66-
def update_pyproject_dependencies(dependencies: dict, schedule: Dict[str, str]):
74+
def _version_from_filename(filename: str) -> Version | None:
75+
try:
76+
_, version, _, _ = parse_wheel_filename(filename)
77+
return version
78+
except InvalidWheelFilename:
79+
pass
80+
81+
try:
82+
_, version = parse_sdist_filename(filename)
83+
return version
84+
except InvalidSdistFilename:
85+
return None
86+
87+
88+
def update_pyproject_dependencies(dependencies: list, schedule: Dict[str, str]):
6789
# Iterate by idx because we want to update it inplace
6890
for i in range(len(dependencies)):
6991
dep_str = dependencies[i]
92+
if not isinstance(dep_str, str):
93+
continue
7094
pkg, extras, spec, env = parse_pep_dependency(dep_str)
71-
if isinstance(spec, Url) or pkg not in schedule:
95+
schedule_key = canonicalize_name(pkg)
96+
if isinstance(spec, Url) or schedule_key not in schedule:
7297
continue
73-
new_lower_bound = Version(schedule[pkg])
98+
new_lower_bound = Version(schedule[schedule_key])
7499
try:
75100
spec = tighten_lower_bound(spec or SpecifierSet(), new_lower_bound)
76101
# Will raise a value error if bound is already tighter, in this case we just do nothing and continue
@@ -85,26 +110,27 @@ def update_pyproject_dependencies(dependencies: dict, schedule: Dict[str, str]):
85110

86111
def update_dependency_table(dep_table: dict, new_versions: dict):
87112
for pkg, pkg_data in dep_table.items():
113+
schedule_key = canonicalize_name(pkg)
88114
# Don't do anything for pkgs that aren't in our schedule
89-
if pkg not in new_versions:
115+
if schedule_key not in new_versions:
90116
continue
91117
# Like pkg = ">x.y.z,<a"
92118
if isinstance(pkg_data, str):
93119
if not is_url_spec(pkg_data):
94120
spec = parse_version_spec(pkg_data)
95-
new_lower_bound = Version(new_versions[pkg])
121+
new_lower_bound = Version(new_versions[schedule_key])
96122
spec = tighten_lower_bound(spec, new_lower_bound)
97123
dep_table[pkg] = repr_spec_set(spec)
98124
else:
99125
# We don't do anything with url spec dependencies
100126
continue
101127
else:
102128
# Table like in tests = {path = "."}
103-
if "path" in pkg_data:
104-
# We don't do anything with path dependencies
129+
if not isinstance(pkg_data, dict) or "version" not in pkg_data:
130+
# We don't do anything with path, url, git, or other non-version dependencies
105131
continue
106-
spec = SpecifierSet(pkg_data["version"])
107-
new_lower_bound = Version(new_versions[pkg])
132+
spec = parse_version_spec(pkg_data["version"])
133+
new_lower_bound = Version(new_versions[schedule_key])
108134
spec = tighten_lower_bound(spec, new_lower_bound)
109135
pkg_data["version"] = repr_spec_set(spec)
110136

@@ -119,6 +145,8 @@ def update_pixi_dependencies(pixi_tables: dict, schedule: Dict[str, str]):
119145
for _, feature_data in pixi_tables["feature"].items():
120146
if "dependencies" in feature_data:
121147
update_dependency_table(feature_data["dependencies"], schedule)
148+
if "pypi-dependencies" in feature_data:
149+
update_dependency_table(feature_data["pypi-dependencies"], schedule)
122150

123151

124152
def update_pyproject_toml(
@@ -138,26 +166,58 @@ def update_pyproject_toml(
138166
for schedule in applicable:
139167
# Fill in the latest known requirement (schedule is sorted, newer entries overwrite older)
140168
for pkg, version in schedule["packages"].items():
141-
new_version[pkg] = version
169+
new_version[canonicalize_name(pkg)] = version
142170
if not new_version:
143171
raise RuntimeError(
144172
"Could not find schedule that applies to current time, perhaps your schedule is outdated."
145173
)
146-
if "python" in new_version:
147-
pyproject_data["project"]["requires-python"] = repr_spec_set(
148-
parse_version_spec(new_version["python"])
149-
)
150-
update_pyproject_dependencies(
151-
pyproject_data["project"]["dependencies"], new_version
152-
)
174+
project_data = pyproject_data.get("project", {})
175+
if not isinstance(project_data, dict):
176+
project_data = {}
177+
if "python" in new_version and isinstance(project_data, dict):
178+
current_requires_python = project_data.get("requires-python")
179+
if current_requires_python:
180+
try:
181+
python_spec = tighten_lower_bound(
182+
parse_version_spec(current_requires_python),
183+
Version(new_version["python"]),
184+
)
185+
except ValueError:
186+
python_spec = parse_version_spec(current_requires_python)
187+
else:
188+
python_spec = parse_version_spec(new_version["python"])
189+
project_data["requires-python"] = repr_spec_set(python_spec)
190+
191+
dependencies = project_data.get("dependencies")
192+
if isinstance(dependencies, list):
193+
update_pyproject_dependencies(dependencies, new_version)
194+
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+
153207
if "tool" in pyproject_data and "pixi" in pyproject_data["tool"]:
154208
pixi_data = pyproject_data["tool"]["pixi"]
155209
update_pixi_dependencies(pixi_data, new_version)
156210
if update_all is not None:
157-
deps = pyproject_data.get("project", {}).get("dependencies", [])
211+
deps = project_data.get("dependencies", [])
158212
for i, dep_str in enumerate(deps):
213+
if not isinstance(dep_str, str):
214+
continue
159215
pkg, extras, spec, env = parse_pep_dependency(dep_str)
160-
if pkg in new_version or isinstance(spec, Url) or spec is None:
216+
if (
217+
canonicalize_name(pkg) in new_version
218+
or isinstance(spec, Url)
219+
or spec is None
220+
):
161221
continue
162222
min_ver = _get_oldest_version_in_window(pkg, update_all)
163223
if min_ver is None:

0 commit comments

Comments
 (0)