44
55import functools
66import time
7+ from bisect import bisect_left
78from collections import defaultdict
89from datetime import datetime , timedelta
9- from packaging .specifiers import InvalidSpecifier , SpecifierSet
10+ from packaging .specifiers import SpecifierSet
1011from packaging .version import Version
1112from pathlib import Path
1213from typing import Optional , Union
@@ -122,23 +123,18 @@ def fetch_release(package: str, version: Version) -> dict:
122123 return pypi_data .json ()
123124
124125
125- def get_supported_releases (integration : str , pypi_data : dict ) -> list [Version ]:
126+ def _prefilter_releases (integration : str , releases : dict [str , dict ]) -> list [Version ]:
127+ """Drop versions that are unsupported without making additional API calls."""
126128 min_supported = _MIN_VERSIONS .get (integration )
127129 if min_supported :
128130 min_supported = Version ("." .join (map (str , min_supported )))
129131 print (f" Minimum supported version for { integration } is { min_supported } ." )
130132 else :
131- print (
132- f" { integration } doesn't have a minimum version. Maybe we should define one?"
133- )
134-
135- custom_python_versions = TEST_SUITE_CONFIG [integration ].get ("python" )
136- if custom_python_versions :
137- custom_python_versions = SpecifierSet (custom_python_versions )
133+ print (f" { integration } doesn't have a minimum version. Consider defining one" )
138134
139- releases = []
135+ filtered_releases = []
140136
141- for release , metadata in pypi_data [ " releases" ] .items ():
137+ for release , metadata in releases .items ():
142138 if not metadata :
143139 continue
144140
@@ -158,47 +154,56 @@ def get_supported_releases(integration: str, pypi_data: dict) -> list[Version]:
158154 # TODO: consider the newest prerelease unless obsolete
159155 continue
160156
161- # The release listing that you get via the package endpoint doesn't
162- # contain all metadata for a release. `requires_python` is included,
163- # but classifiers are not (they require a separate call to the release
164- # endpoint).
165- # Some packages don't use `requires_python`, they supply classifiers
166- # instead.
167- version .python_versions = None
168- requires_python = meta .get ("requires_python" )
169- if requires_python :
170- try :
171- version .python_versions = supported_python_versions (
172- SpecifierSet (requires_python ), custom_python_versions
173- )
174- except InvalidSpecifier :
175- continue
176- else :
177- # No `requires_python`. Let's fetch the metadata to see
178- # the classifiers.
179- # XXX do something with this. no need to fetch every release ever
180- release_metadata = fetch_release (package , version )
181- version .python_versions = supported_python_versions (
182- determine_python_versions (release_metadata ), custom_python_versions
183- )
184- time .sleep (0.1 )
185-
186- if not version .python_versions :
187- continue
188-
189- for i , saved_version in enumerate (releases ):
157+ for i , saved_version in enumerate (filtered_releases ):
190158 if (
191159 version .major == saved_version .major
192160 and version .minor == saved_version .minor
193161 and version .micro > saved_version .micro
194162 ):
195163 # Don't save all patch versions of a release, just the newest one
196- releases [i ] = version
164+ filtered_releases [i ] = version
197165 break
198166 else :
199- releases .append (version )
167+ filtered_releases .append (version )
200168
201- return sorted (releases )
169+ return sorted (filtered_releases )
170+
171+
172+ def get_supported_releases (integration : str , pypi_data : dict ) -> list [Version ]:
173+ """
174+ Get a list of releases that are currently supported by the SDK.
175+
176+ This takes into account a handful of parameters (Python support, the lowest
177+ version we've defined for the framework, the date of the release).
178+ """
179+ # Get a consolidated list without taking into account Python support yet
180+ # (because that might require an additional API call for some
181+ # of the releases)
182+ releases = _prefilter_releases (integration , pypi_data ["releases" ])
183+
184+ # Determine Python support
185+ expected_python_versions = TEST_SUITE_CONFIG [integration ].get ("python" )
186+ if expected_python_versions :
187+ expected_python_versions = SpecifierSet (expected_python_versions )
188+ else :
189+ expected_python_versions = SpecifierSet (f">={ MIN_PYTHON_VERSION } " )
190+
191+ def _supports_lowest (release : Version ) -> bool :
192+ time .sleep (0.1 ) # don't DoS PYPI
193+ py_versions = determine_python_versions (fetch_release (package , release ))
194+ target_python_versions = TEST_SUITE_CONFIG [integration ].get ("python" )
195+ if target_python_versions :
196+ target_python_versions = SpecifierSet (target_python_versions )
197+ return bool (supported_python_versions (py_versions , target_python_versions ))
198+
199+ i = bisect_left (releases , True , key = _supports_lowest )
200+ if i != len (releases ) and _supports_lowest (releases [i ]):
201+ print (i )
202+ # we found the lowest version that supports at least some Python
203+ # version(s) that we do, cut off the rest
204+ releases = releases [i :]
205+
206+ return releases
202207
203208
204209def pick_releases_to_test (releases : list [Version ]) -> list [Version ]:
@@ -222,6 +227,7 @@ def pick_releases_to_test(releases: list[Version]) -> list[Version]:
222227 releases_by_major [release .major ][0 ] = release
223228 if release > releases_by_major [release .major ][1 ]:
224229 releases_by_major [release .major ][1 ] = release
230+
225231 for i , (min_version , max_version ) in enumerate (releases_by_major .values ()):
226232 filtered_releases .add (max_version )
227233 if i == len (releases_by_major ) - 1 :
@@ -247,15 +253,16 @@ def pick_releases_to_test(releases: list[Version]) -> list[Version]:
247253
248254
249255def supported_python_versions (
250- python_versions : SpecifierSet , custom_versions : Optional [SpecifierSet ] = None
256+ package_python_versions : Union [SpecifierSet , list [Version ]],
257+ custom_supported_versions : Optional [SpecifierSet ] = None ,
251258) -> list [Version ]:
252259 """Get an intersection of python_versions and Python versions supported in the SDK."""
253260 supported = []
254261
255262 curr = MIN_PYTHON_VERSION
256263 while curr <= MAX_PYTHON_VERSION :
257- if curr in python_versions :
258- if not custom_versions or curr in custom_versions :
264+ if curr in package_python_versions :
265+ if not custom_supported_versions or curr in custom_supported_versions :
259266 supported .append (curr )
260267
261268 next = [int (v ) for v in str (curr ).split ("." )]
@@ -283,6 +290,9 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi
283290 try :
284291 classifiers = pypi_data ["info" ]["classifiers" ]
285292 except (AttributeError , KeyError ):
293+ # This function assumes `pypi_data` contains classifiers. This is the case
294+ # for the most recent release in the /{project} endpoint or for any release
295+ # fetched via the /{project}/{version} endpoint.
286296 return []
287297
288298 python_versions = []
@@ -300,6 +310,9 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi
300310 python_versions .sort ()
301311 return python_versions
302312
313+ # We only use `requires_python` if there are no classifiers. This is because
314+ # `requires_python` doesn't tell us anything about the upper bound, which
315+ # depends on when the release first came out
303316 try :
304317 requires_python = pypi_data ["info" ]["requires_python" ]
305318 except (AttributeError , KeyError ):
@@ -404,9 +417,15 @@ def write_tox_file(packages: dict) -> None:
404417 test_releases = pick_releases_to_test (releases )
405418
406419 for release in test_releases :
420+ target_python_versions = TEST_SUITE_CONFIG [integration ].get ("python" )
421+ if target_python_versions :
422+ target_python_versions = SpecifierSet (target_python_versions )
407423 release_pypi_data = fetch_release (package , release )
408424 release .python_versions = pick_python_versions_to_test (
409- release .python_versions
425+ supported_python_versions (
426+ determine_python_versions (release_pypi_data ),
427+ target_python_versions ,
428+ )
410429 )
411430 if not release .python_versions :
412431 print (f" Release { release } has no Python versions, skipping." )
0 commit comments