Skip to content

Commit e392825

Browse files
committed
better py version picking
1 parent 5cff258 commit e392825

File tree

4 files changed

+74
-55
lines changed

4 files changed

+74
-55
lines changed

.github/workflows/test-integrations-dbs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ jobs:
124124
strategy:
125125
fail-fast: false
126126
matrix:
127-
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12","3.13"]
127+
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12"]
128128
# python3.6 reached EOL and is no longer being supported on
129129
# new versions of hosted runners on Github Actions
130130
# ubuntu-20.04 is the last version that supported python3.6

.github/workflows/test-integrations-web-1.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ jobs:
115115
strategy:
116116
fail-fast: false
117117
matrix:
118-
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12","3.13"]
118+
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11","3.12"]
119119
# python3.6 reached EOL and is no longer being supported on
120120
# new versions of hosted runners on Github Actions
121121
# ubuntu-20.04 is the last version that supported python3.6

scripts/populate_tox/populate_tox.py

Lines changed: 66 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
import functools
66
import time
7+
from bisect import bisect_left
78
from collections import defaultdict
89
from datetime import datetime, timedelta
9-
from packaging.specifiers import InvalidSpecifier, SpecifierSet
10+
from packaging.specifiers import SpecifierSet
1011
from packaging.version import Version
1112
from pathlib import Path
1213
from 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

204209
def 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

249255
def 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.")

tox.ini

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ envlist =
280280
# These come from the populate_tox.py script. Eventually we should move all
281281
# integration tests there.
282282
# ~~~ DBs ~~~
283-
{py3.7,py3.12,py3.13}-clickhouse_driver-v0.2.9
283+
{py3.7,py3.11,py3.12}-clickhouse_driver-v0.2.9
284284

285285

286286
# ~~~ GraphQL ~~~
@@ -291,17 +291,17 @@ envlist =
291291

292292

293293
# ~~~ Web 1 ~~~
294-
{py3.7,py3.12,py3.13}-fastapi-v0.79.1
295-
{py3.7,py3.12,py3.13}-fastapi-v0.91.0
296-
{py3.7,py3.12,py3.13}-fastapi-v0.103.2
297-
{py3.8,py3.12,py3.13}-fastapi-v0.115.6
294+
{py3.6,py3.9,py3.10}-fastapi-v0.79.1
295+
{py3.7,py3.10,py3.11}-fastapi-v0.91.0
296+
{py3.7,py3.10,py3.11}-fastapi-v0.103.2
297+
{py3.8,py3.11,py3.12}-fastapi-v0.115.6
298298

299299

300300
# ~~~ Web 2 ~~~
301301
{py3.6,py3.7}-bottle-v0.12.25
302302
{py3.6,py3.8,py3.9}-bottle-v0.13.2
303303

304-
{py3.6,py3.11,py3.12}-falcon-v3.0.1
304+
{py3.6,py3.8,py3.9}-falcon-v3.0.1
305305
{py3.6,py3.11,py3.12}-falcon-v3.1.3
306306
{py3.8,py3.11,py3.12}-falcon-v4.0.2
307307

0 commit comments

Comments
 (0)