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
78Based heavily on `min_deps_check.py` from xarray
89but rewritten to pull requirements from a Python requirements.txt and available versions from PyPI.
910
1011Needs 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
1417See also
1518========
1619https://github.com/pydata/xarray/blob/v2024.06.0/ci/min_deps_check.py
1720"""
21+ import dataclasses
1822import datetime
23+ import enum
24+ import itertools
25+ import shlex
26+ import subprocess
1927import sys
2028from collections .abc import Iterator
2129
2432import requests
2533from 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+
2752IGNORE_DEPS : set [str ] = {
2853 'certifi' ,
2954 'pytz' ,
@@ -49,13 +74,12 @@ def warning(msg: str) -> None:
4974
5075
5176def 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
199220def 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 ("\n Errors:" )
0 commit comments