11import configparser
22import functools
3+ import time
34from datetime import datetime , timedelta
45from pathlib import Path
56
67import requests
78
8- from ..split_tox_gh_actions .split_tox_gh_actions import GROUPS
9-
10- print (GROUPS )
119
1210# Only consider package versions going back this far
13- CUTOFF = datetime .now () - timedelta (days = 365 * 3 )
11+ CUTOFF = datetime .now () - timedelta (days = 365 * 5 )
1412LOWEST_SUPPORTED_PY_VERSION = "3.6"
1513
1614TOX_FILE = Path (__file__ ).resolve ().parent .parent .parent / "tox.ini"
1715
1816PYPI_PROJECT_URL = "https://pypi.python.org/pypi/{project}/json"
1917PYPI_VERSION_URL = "https://pypi.python.org/pypi/{project}/{version}/json"
2018
19+ CLASSIFIER_PREFIX = "Programming Language :: Python :: "
20+
2121EXCLUDE = {
2222 "common" ,
2323}
2727
2828@functools .total_ordering
2929class Version :
30- def __init__ (self , version , metadata ):
30+ def __init__ (self , version , metadata = None ):
3131 self .raw = version
3232 self .metadata = metadata
3333
@@ -36,10 +36,14 @@ def __init__(self, version, metadata):
3636 self .patch = None
3737 self .parsed = None
3838
39+ self .python_versions = []
40+
3941 try :
4042 parsed = version .split ("." )
41- if parsed [2 ].isnumeric ():
43+ if len ( parsed ) == 3 and parsed [2 ].isnumeric ():
4244 self .major , self .minor , self .patch = (int (p ) for p in parsed )
45+ elif len (parsed ) == 2 and parsed [1 ].isnumeric ():
46+ self .major , self .minor = (int (p ) for p in parsed )
4347 except Exception :
4448 # This will fail for e.g. prereleases, but we don't care about those
4549 # for now
@@ -97,50 +101,125 @@ def parse_tox():
97101 print (f"ERROR reading line { line } " )
98102
99103
100- def fetch_metadata (package ):
104+ def fetch_package (package : str ) -> dict :
105+ """Fetch package metadata from PYPI."""
101106 url = PYPI_PROJECT_URL .format (project = package )
102107 pypi_data = requests .get (url )
103108
104109 if pypi_data .status_code != 200 :
105110 print (f"{ package } not found" )
106111
107- import pprint
108-
109- pprint .pprint (package )
110- pprint .pprint (pypi_data .json ())
111112 return pypi_data .json ()
112113
113114
114- def parse_metadata ( data ) :
115- package = data ["info" ]["name" ]
115+ def get_releases ( pypi_data : dict ) -> list [ Version ] :
116+ package = pypi_data ["info" ]["name" ]
116117
117- majors = {}
118+ versions = []
119+
120+ for release , metadata in pypi_data ["releases" ].items ():
121+ if not metadata :
122+ continue
118123
119- for release , metadata in data ["releases" ].items ():
120124 meta = metadata [0 ]
121125 if datetime .fromisoformat (meta ["upload_time" ]) < CUTOFF :
122126 continue
123127
124128 version = Version (release , meta )
125129 if not version .valid :
126- print (f"Failed to parse version { release } of package { package } " )
130+ print (
131+ f"Failed to parse version { release } of package { package } . Ignoring..."
132+ )
127133 continue
128134
129- if version .major not in majors :
130- # 0 -> [min 0.x version, max 0.x version]
131- majors [version .major ] = [version , version ]
132- continue
135+ versions .append (version )
133136
134- if version < majors [version .major ][0 ]:
135- majors [version .major ][0 ] = version
136- if version > majors [version .major ][1 ]:
137- majors [version .major ][1 ] = version
137+ return sorted (versions )
138+
139+
140+ def pick_releases_to_test (releases : list [Version ]) -> list [Version ]:
141+ indexes = [
142+ 0 , # oldest version younger than CUTOFF
143+ len (releases ) // 3 ,
144+ len (releases ) // 3 * 2 ,
145+ - 1 , # latest
146+ ]
147+ return [releases [i ] for i in indexes ]
148+
149+
150+ def fetch_release (package : str , version : Version ) -> dict :
151+ url = PYPI_VERSION_URL .format (project = package , version = version )
152+ pypi_data = requests .get (url )
153+
154+ if pypi_data .status_code != 200 :
155+ print (f"{ package } not found" )
156+
157+ return pypi_data .json ()
158+
159+
160+ def determine_python_versions (
161+ package : str , version : Version , pypi_data : dict
162+ ) -> list [str ]:
163+ try :
164+ classifiers = pypi_data ["info" ]["classifiers" ]
165+ except (AttributeError , IndexError ):
166+ print (f"{ package } { version } has no classifiers" )
167+ return []
168+
169+ python_versions = []
170+ for classifier in classifiers :
171+ if classifier .startswith (CLASSIFIER_PREFIX ):
172+ python_version = classifier [len (CLASSIFIER_PREFIX ) :]
173+ if "." in python_version :
174+ # we don't care about stuff like
175+ # Programming Language :: Python :: 3 :: Only
176+ # Programming Language :: Python :: 3
177+ # etc., we're only interested in specific versions like 3.13
178+ python_versions .append (python_version )
179+
180+ python_versions = [
181+ version
182+ for version in python_versions
183+ if Version (version ) >= Version (LOWEST_SUPPORTED_PY_VERSION )
184+ ]
185+
186+ return python_versions
187+
188+
189+ def write_tox_file (package , versions ):
190+ for version in versions :
191+ print (
192+ "{python_versions}-{package}-v{version}" .format (
193+ python_versions = "," .join ([f"py{ v } " for v in version .python_versions ]),
194+ package = package ,
195+ version = version ,
196+ )
197+ )
198+
199+ print ()
200+
201+ for version in versions :
202+ print (f"{ package } -v{ version } : { package } =={ version .raw } " )
203+
204+
205+ if __name__ == "__main__" :
206+ for package in ("celery" , "django" ):
207+ pypi_data = fetch_package (package )
208+ releases = get_releases (pypi_data )
209+ test_releases = pick_releases_to_test (releases )
210+ for release in test_releases :
211+ release_pypi_data = fetch_release (package , release )
212+ release .python_versions = determine_python_versions (
213+ package , release , release_pypi_data
214+ )
215+ # XXX if no supported python versions -> delete
138216
139- print (release , "not too old" , meta ["upload_time" ])
217+ print (release , " on " , release .python_versions )
218+ time .sleep (0.1 )
140219
141- return majors
220+ print (releases )
221+ print (test_releases )
142222
223+ write_tox_file (package , test_releases )
143224
144- print (parse_tox ())
145- print (packages )
146- print (parse_metadata (fetch_metadata ("celery" )))
225+ print (parse_tox ())
0 commit comments