Skip to content

Commit 4794980

Browse files
committed
Improve min_deps_check.py script, invoke automatically
The min_deps_check.py script now acts on its own advice and will update requirements-minimum.txt automatically by invoking `pip-compile`. The update_pinned_dependencies.sh script now invokes the min_deps_check.py script.
1 parent 3dd00ff commit 4794980

File tree

2 files changed

+156
-51
lines changed

2 files changed

+156
-51
lines changed

scripts/min_deps_check.py

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
#!/usr/bin/env python
22
"""
3-
Fetch from conda database all available versions of the emsarray dependencies and their
4-
publication date. Compare it against continuous-integration/min-deps.yaml to verify the
5-
policy on obsolete dependencies is being followed. Print a pretty report :)
3+
Fetch from PyPI all available versions of the emsarray dependencies and their
4+
publication date. Compare it against continuous-integration/requirements-minimum.txt
5+
to verify the policy on obsolete dependencies is being followed.
6+
Update the pinned dependencies using `pip-compile`.
67
78
Based heavily on `min_deps_check.py` from xarray
89
but rewritten to pull requirements from a Python requirements.txt and available versions from PyPI.
910
1011
Needs the following extra deps installed:
1112
12-
$ pip3 install packaging requests python-dateutil
13+
$ pip3 install packaging requests python-dateutil pip-tools
14+
15+
This is automatically run as part of the `scripts/update_pinned_dependencies.sh` script.
1316
1417
See also
1518
========
1619
https://github.com/pydata/xarray/blob/v2024.06.0/ci/min_deps_check.py
1720
"""
21+
import dataclasses
1822
import datetime
23+
import enum
24+
import itertools
25+
import shlex
26+
import subprocess
1927
import sys
2028
from collections.abc import Iterator
2129

@@ -24,6 +32,23 @@
2432
import requests
2533
from dateutil.relativedelta import relativedelta
2634

35+
36+
class VersionStatus(enum.StrEnum):
37+
older = '<'
38+
current = '~='
39+
newer = '>'
40+
41+
42+
@dataclasses.dataclass
43+
class Dependency:
44+
package_name: str
45+
requirements_version: packaging.version.Version
46+
requirements_date: datetime.date | None
47+
policy_version: packaging.version.Version
48+
policy_date: datetime.date | None
49+
status: VersionStatus
50+
51+
2752
IGNORE_DEPS: set[str] = {
2853
'certifi',
2954
'pytz',
@@ -49,13 +74,12 @@ def warning(msg: str) -> None:
4974

5075

5176
def parse_requirements(
52-
fname: str
77+
filename: str
5378
) -> Iterator[tuple[str, packaging.specifiers.Specifier, packaging.version.Version]]:
54-
"""Load requirements/min-all-deps.yml
55-
56-
Yield (package name, major version, minor version, patch version)
5779
"""
58-
for line_number, line in enumerate(open(fname), start=1):
80+
Parse a requirements file, yield (package name, specifier, version) for each requirement.
81+
"""
82+
for line_number, line in enumerate(open(filename), start=1):
5983
if '#' in line:
6084
line = line[:line.index('#')]
6185
line = line.strip()
@@ -71,9 +95,6 @@ def parse_requirements(
7195
continue
7296
specifier = next(iter(requirement.specifier))
7397

74-
if specifier.operator != '~=':
75-
error(f"Specificity for dependency {requirement.name} should be '~='")
76-
7798
version = packaging.version.parse(specifier.version)
7899
if version.micro is None:
79100
warning(
@@ -110,7 +131,7 @@ def process_pkg(
110131
pkg: str,
111132
specifier: packaging.specifiers.Specifier,
112133
version: packaging.version.Version,
113-
) -> tuple[str, str, str, str, str, str]:
134+
) -> Dependency:
114135
"""Compare package version from requirements file to available versions in conda.
115136
Return row to build pandas dataframe:
116137
@@ -148,7 +169,7 @@ def process_pkg(
148169
policy_version = packaging.version.parse(policy_specifier.version)
149170

150171
# Find the release date of the policy version
151-
policy_release_date = min(
172+
policy_date = min(
152173
(
153174
release_date
154175
for release_version, release_date
@@ -158,17 +179,17 @@ def process_pkg(
158179
)
159180

160181
if version in policy_specifier:
161-
status = "~="
182+
status = VersionStatus.current
162183
else:
163184
if version < policy_version:
164-
status = '<'
185+
status = VersionStatus.older
165186
warning(
166187
f"Requirement {pkg} {version} was published on {req_published:%Y-%m-%d} "
167188
f"which is older than the required {policy_months} months of support. "
168189
f"Minimum policy supported version is {pkg}{policy_specifier}."
169190
)
170191
elif version > policy_version:
171-
status = '> (!)'
192+
status = VersionStatus.newer
172193
if req_published is None:
173194
error(
174195
f"Package version is newer than policy version. "
@@ -186,26 +207,85 @@ def process_pkg(
186207
f"Update requirement to {pkg}{policy_specifier}."
187208
)
188209

189-
return (
190-
pkg,
191-
str(version),
192-
req_published.strftime("%Y-%m-%d") if req_published else "-",
193-
str(policy_version),
194-
policy_release_date.strftime("%Y-%m-%d") if policy_release_date else "-",
195-
status,
210+
return Dependency(
211+
package_name=pkg,
212+
requirements_version=version,
213+
requirements_date=req_published,
214+
policy_version=policy_version,
215+
policy_date=policy_date,
216+
status=status,
196217
)
197218

198219

199220
def main() -> None:
221+
if len(sys.argv) < 2:
222+
print(f"Usage: {sys.argv[0]} continuous-integration/requirements-minimum.txt")
223+
sys.exit(1)
224+
200225
requirements_file = sys.argv[1]
201-
rows = [process_pkg(pkg, specifier, version) for pkg, specifier, version in parse_requirements(requirements_file)]
226+
dependencies = [
227+
process_pkg(pkg, specifier, version)
228+
for pkg, specifier, version in parse_requirements(requirements_file)
229+
]
202230

203231
print()
204232
print("Package Required Status Policy ")
205233
print("-------------------- ----------------------- ------ -----------------------")
206234
fmt = "{0:20} {1:10} ({2:10}) {5:^6} {3:10} ({4:10})"
207-
for row in rows:
208-
print(fmt.format(*row))
235+
for d in dependencies:
236+
requirements_date = (
237+
d.requirements_date.strftime("%Y-%m-%d")
238+
if d.requirements_date is not None
239+
else "-"
240+
)
241+
policy_date = (
242+
d.policy_date.strftime("%Y-%m-%d")
243+
if d.policy_date is not None
244+
else "-"
245+
)
246+
print(
247+
f"{d.package_name:20} {d.requirements_version!s:10} ({requirements_date:10}) "
248+
f"{d.status:^6} {d.policy_version!s:10} ({policy_date:10})"
249+
)
250+
251+
upgrade_args = list(itertools.chain.from_iterable(
252+
['--upgrade-package', f'{d.package_name}~={d.policy_version}']
253+
for d in dependencies
254+
if d.status is VersionStatus.older
255+
))
256+
maintain_args = list(itertools.chain.from_iterable(
257+
['--upgrade-package', f'{d.package_name}~={d.requirements_version}']
258+
for d in dependencies
259+
if d.status in {VersionStatus.current, VersionStatus.newer}
260+
))
261+
if upgrade_args:
262+
ignored_args = list(itertools.chain.from_iterable(
263+
['--unsafe-package', ignored]
264+
for ignored in IGNORE_DEPS
265+
))
266+
cmd = [
267+
'pip-compile',
268+
'--quiet',
269+
'--extra', 'complete',
270+
'--strip-extras',
271+
'--unsafe-package', 'emsarray',
272+
'--no-allow-unsafe',
273+
# '--no-header',
274+
# '--no-annotate',
275+
'--output-file', requirements_file,
276+
] + upgrade_args + maintain_args + ignored_args + [
277+
'pyproject.toml',
278+
]
279+
print('$', shlex.join(cmd))
280+
subprocess.check_call(cmd)
281+
cmd = [
282+
'sed',
283+
'-i',
284+
's/==/~=/',
285+
requirements_file,
286+
]
287+
print('$', shlex.join(cmd))
288+
subprocess.check_call(cmd)
209289

210290
if errors:
211291
print("\nErrors:")

scripts/update_pinned_dependencies.sh

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
set -e
44

5-
PYTHON_VERSIONS=('3.10' '3.11' '3.12')
5+
PYTHON_VERSIONS=('3.11' '3.12' '3.13')
66
HERE="$( cd -- "$( realpath -- "$( dirname -- "$0" )" )" && pwd )"
77
PROJECT_ROOT="$( dirname "$HERE" )"
88

@@ -11,28 +11,53 @@ cd "$PROJECT_ROOT"
1111
conda_venv_root=$( mktemp -d emsarray-conda-environments.XXXXXXX )
1212
echo "Working in ${conda_venv_root}"
1313

14-
for version in "${PYTHON_VERSIONS[@]}" ; do
15-
requirements_file="./continuous-integration/requirements-${version}.txt"
16-
echo "Updating $requirements_file"
17-
18-
conda_prefix="${conda_venv_root}/py${version}"
19-
conda create \
20-
--yes --quiet \
21-
--prefix="${conda_prefix}" \
22-
--no-default-packages
23-
conda install \
24-
--yes \
25-
--prefix="${conda_prefix}" \
26-
"python=${version}" \
27-
pip-tools
28-
conda run \
29-
--prefix="${conda_prefix}" \
30-
pip-compile \
31-
--upgrade \
32-
--extra="testing" \
33-
--output-file="${requirements_file}" \
34-
setup.cfg
35-
conda env remove --yes --prefix="${conda_prefix}"
36-
done
14+
version="${PYTHON_VERSIONS[0]}"
15+
requirements_file="./continuous-integration/requirements-minimum.txt"
16+
echo "Updating $requirements_file"
17+
conda_prefix="${conda_venv_root}/py-min"
18+
conda create \
19+
--yes --quiet \
20+
--prefix="${conda_prefix}" \
21+
--no-default-packages
22+
conda install \
23+
--yes \
24+
--prefix="${conda_prefix}" \
25+
--channel conda-forge \
26+
"python=${version}" pip
27+
conda run \
28+
--prefix="${conda_prefix}" \
29+
pip install pip-tools packaging requests python-dateutil pip-tools
30+
conda run \
31+
--prefix="${conda_prefix}" \
32+
python3 ./scripts/min_deps_check.py "$requirements_file"
33+
conda env remove --yes --prefix="${conda_prefix}"
34+
35+
# for version in "${PYTHON_VERSIONS[@]}" ; do
36+
# requirements_file="./continuous-integration/requirements-${version}.txt"
37+
# echo "Updating $requirements_file"
38+
#
39+
# conda_prefix="${conda_venv_root}/py${version}"
40+
# conda create \
41+
# --yes --quiet \
42+
# --prefix="${conda_prefix}" \
43+
# --no-default-packages
44+
# conda install \
45+
# --yes \
46+
# --prefix="${conda_prefix}" \
47+
# --channel conda-forge \
48+
# "python=${version}" \
49+
# pip-tools
50+
# conda run \
51+
# --prefix="${conda_prefix}" \
52+
# pip-compile \
53+
# --upgrade \
54+
# --extra="testing" \
55+
# --output-file="${requirements_file}" \
56+
# --unsafe-package emsarray \
57+
# --no-allow-unsafe \
58+
# pyproject.toml
59+
# conda env remove --yes --prefix="${conda_prefix}"
60+
# done
61+
3762

3863
echo rm -rf "$conda_venv_root"

0 commit comments

Comments
 (0)