Skip to content

Commit 4925b20

Browse files
committed
tests: Add fail_on_changes to toxgen
1 parent 74b3bbf commit 4925b20

File tree

3 files changed

+136
-48
lines changed

3 files changed

+136
-48
lines changed

scripts/populate_tox/populate_tox.py

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
"""
44

55
import functools
6+
import hashlib
67
import os
78
import sys
89
import time
910
from bisect import bisect_left
1011
from collections import defaultdict
12+
from datetime import datetime, timezone
1113
from importlib.metadata import metadata
1214
from packaging.specifiers import SpecifierSet
1315
from packaging.version import Version
@@ -106,7 +108,9 @@ def fetch_release(package: str, version: Version) -> dict:
106108
return pypi_data.json()
107109

108110

109-
def _prefilter_releases(integration: str, releases: dict[str, dict]) -> list[Version]:
111+
def _prefilter_releases(
112+
integration: str, releases: dict[str, dict], older_than: Optional[datetime] = None
113+
) -> list[Version]:
110114
"""
111115
Filter `releases`, removing releases that are for sure unsupported.
112116
@@ -135,6 +139,10 @@ def _prefilter_releases(integration: str, releases: dict[str, dict]) -> list[Ver
135139
if meta["yanked"]:
136140
continue
137141

142+
if older_than is not None:
143+
if datetime.fromisoformat(meta["upload_time_iso_8601"]) > older_than:
144+
continue
145+
138146
version = Version(release)
139147

140148
if min_supported and version < min_supported:
@@ -160,19 +168,24 @@ def _prefilter_releases(integration: str, releases: dict[str, dict]) -> list[Ver
160168
return sorted(filtered_releases)
161169

162170

163-
def get_supported_releases(integration: str, pypi_data: dict) -> list[Version]:
171+
def get_supported_releases(
172+
integration: str, pypi_data: dict, older_than: Optional[datetime] = None
173+
) -> list[Version]:
164174
"""
165175
Get a list of releases that are currently supported by the SDK.
166176
167177
This takes into account a handful of parameters (Python support, the lowest
168178
version we've defined for the framework, the date of the release).
179+
180+
If an `older_than` timestamp is provided, no release newer than that will be
181+
considered.
169182
"""
170183
package = pypi_data["info"]["name"]
171184

172185
# Get a consolidated list without taking into account Python support yet
173186
# (because that might require an additional API call for some
174187
# of the releases)
175-
releases = _prefilter_releases(integration, pypi_data["releases"])
188+
releases = _prefilter_releases(integration, pypi_data["releases"], older_than)
176189

177190
# Determine Python support
178191
expected_python_versions = TEST_SUITE_CONFIG[integration].get("python")
@@ -381,7 +394,9 @@ def _render_dependencies(integration: str, releases: list[Version]) -> list[str]
381394
return rendered
382395

383396

384-
def write_tox_file(packages: dict) -> None:
397+
def write_tox_file(
398+
packages: dict, update_timestamp: bool, last_updated: datetime
399+
) -> None:
385400
template = ENV.get_template("tox.jinja")
386401

387402
context = {"groups": {}}
@@ -400,6 +415,11 @@ def write_tox_file(packages: dict) -> None:
400415
}
401416
)
402417

418+
if update_timestamp:
419+
context["updated"] = datetime.now(tz=timezone.utc).isoformat()
420+
else:
421+
context["updated"] = last_updated.isoformat()
422+
403423
rendered = template.render(context)
404424

405425
with open(TOX_FILE, "w") as file:
@@ -453,7 +473,52 @@ def _add_python_versions_to_release(
453473
release.rendered_python_versions = _render_python_versions(release.python_versions)
454474

455475

456-
def main() -> None:
476+
def get_file_hash() -> str:
477+
"""Calculate a hash of the tox.ini file."""
478+
hasher = hashlib.md5()
479+
480+
with open(TOX_FILE, "rb") as f:
481+
buf = f.read()
482+
hasher.update(buf)
483+
484+
return hasher.hexdigest()
485+
486+
487+
def get_last_updated() -> Optional[datetime]:
488+
timestamp = None
489+
490+
with open(TOX_FILE, "r") as f:
491+
for line in f:
492+
if line.startswith("# Last generated:"):
493+
timestamp = datetime.fromisoformat(line.strip().split[-1])
494+
break
495+
496+
if timestamp is None:
497+
print(
498+
"Failed to find out when tox.ini was last generated; the timestamp seems to be missing from the file."
499+
)
500+
501+
return timestamp
502+
503+
504+
def main(fail_on_changes: bool = False) -> None:
505+
"""
506+
Generate tox.ini from the tox.jinja template.
507+
508+
The script has two modes of operation:
509+
- check mode (if `fail_on_changes` is True)
510+
- normal mode (if `fail_on_changes` is False)
511+
512+
Check mode is run on every PR to make sure that `tox.ini`, `tox.jinja` and
513+
this script don't go out of sync because of manual changes in one place but
514+
not the other.
515+
516+
Normal mode is meant to be run as a cron job, regenerating tox.ini and
517+
proposing the changes via a PR.
518+
"""
519+
mode = "check" if fail_on_changes else "normal"
520+
print(f"Running in {mode} mode.")
521+
457522
global MIN_PYTHON_VERSION, MAX_PYTHON_VERSION
458523
sdk_python_versions = _parse_python_versions_from_classifiers(
459524
metadata("sentry-sdk").get_all("Classifier")
@@ -464,6 +529,12 @@ def main() -> None:
464529
f"The SDK supports Python versions {MIN_PYTHON_VERSION} - {MAX_PYTHON_VERSION}."
465530
)
466531

532+
# If this script is run in check mode (fail_on_changes is True), we need to
533+
# make the script ignore any new releases after the `last_updated` timestamp
534+
# so that we don't fail CI on a PR just because a new package version was
535+
# released, leading to unrelated changes in tox.ini.
536+
last_updated = get_last_updated()
537+
467538
packages = defaultdict(list)
468539

469540
for group, integrations in GROUPS.items():
@@ -480,7 +551,9 @@ def main() -> None:
480551
pypi_data = fetch_package(package)
481552

482553
# Get the list of all supported releases
483-
releases = get_supported_releases(integration, pypi_data)
554+
# If in check mode, ignore releases newer than `last_updated`
555+
older_than = last_updated if fail_on_changes else None
556+
releases = get_supported_releases(integration, pypi_data, older_than)
484557
if not releases:
485558
print(" Found no supported releases.")
486559
continue
@@ -510,8 +583,29 @@ def main() -> None:
510583
}
511584
)
512585

513-
write_tox_file(packages)
586+
if fail_on_changes:
587+
old_file_hash = get_file_hash()
588+
589+
write_tox_file(
590+
packages, update_timestamp=not fail_on_changes, last_updated=last_updated
591+
)
592+
593+
if fail_on_changes:
594+
new_file_hash = get_file_hash()
595+
if old_file_hash != new_file_hash:
596+
raise RuntimeError(
597+
"The yaml configuration files have changed. This means that either `tox.ini` "
598+
"or one of the constants in `split_tox_gh_actions.py` has changed "
599+
"but the changes have not been propagated to the GitHub actions config files. "
600+
"Please run `python scripts/split_tox_gh_actions/split_tox_gh_actions.py` "
601+
"locally and commit the changes of the yaml configuration files to continue. "
602+
)
603+
604+
print(
605+
"Done generating tox.ini. Make sure to also update the CI YAML files to reflect the new test targets."
606+
)
514607

515608

516609
if __name__ == "__main__":
517-
main()
610+
fail_on_changes = len(sys.argv) == 2 and sys.argv[1] == "--fail-on-changes"
611+
main(fail_on_changes)

scripts/populate_tox/tox.jinja

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# or in the script (if you want to change the auto-generated part).
1010
# The file (and all resulting CI YAMLs) then need to be regenerated via
1111
# "scripts/generate-test-files.sh".
12+
#
13+
# Last generated: {{ updated }}
1214

1315
[tox]
1416
requires =

tox.ini

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# or in the script (if you want to change the auto-generated part).
1010
# The file (and all resulting CI YAMLs) then need to be regenerated via
1111
# "scripts/generate-test-files.sh".
12+
#
13+
# Last generated: 2025-02-18T12:26:42.990713+00:00
1214

1315
[tox]
1416
requires =
@@ -176,17 +178,16 @@ envlist =
176178
# ~~~ DBs ~~~
177179
{py3.7,py3.11,py3.12}-clickhouse_driver-v0.2.9
178180

179-
{py3.6}-pymongo-v3.5.1
180-
{py3.6,py3.10,py3.11}-pymongo-v3.13.0
181-
{py3.6,py3.9,py3.10}-pymongo-v4.0.2
181+
{py3.7}-pymongo-v3.7.2
182+
{py3.7,py3.10,py3.11}-pymongo-v3.13.0
183+
{py3.7,py3.9,py3.10}-pymongo-v4.0.2
182184
{py3.9,py3.12,py3.13}-pymongo-v4.11.1
183185

184-
{py3.6}-redis_py_cluster_legacy-v1.3.6
185-
{py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0
186-
{py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3
186+
{py3.7}-redis_py_cluster_legacy-v2.0.0
187+
{py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3
187188

188-
{py3.6,py3.7}-sqlalchemy-v1.3.9
189-
{py3.6,py3.11,py3.12}-sqlalchemy-v1.4.54
189+
{py3.7}-sqlalchemy-v1.3.9
190+
{py3.7,py3.11,py3.12}-sqlalchemy-v1.4.54
190191
{py3.7,py3.10,py3.11}-sqlalchemy-v2.0.9
191192
{py3.7,py3.12,py3.13}-sqlalchemy-v2.0.38
192193

@@ -211,10 +212,10 @@ envlist =
211212
{py3.8,py3.11,py3.12}-ariadne-v0.24.0
212213
{py3.8,py3.11,py3.12}-ariadne-v0.25.2
213214

214-
{py3.6,py3.9,py3.10}-gql-v3.4.1
215+
{py3.7,py3.9,py3.10}-gql-v3.4.1
215216
{py3.7,py3.11,py3.12}-gql-v3.5.0
216217

217-
{py3.6,py3.9,py3.10}-graphene-v3.3
218+
{py3.7,py3.9,py3.10}-graphene-v3.3
218219
{py3.8,py3.12,py3.13}-graphene-v3.4.3
219220

220221
{py3.8,py3.10,py3.11}-strawberry-v0.209.8
@@ -231,12 +232,12 @@ envlist =
231232

232233

233234
# ~~~ Tasks ~~~
234-
{py3.6,py3.7,py3.8}-celery-v4.4.7
235-
{py3.6,py3.7,py3.8}-celery-v5.0.5
235+
{py3.7,py3.8}-celery-v4.4.7
236+
{py3.7,py3.8}-celery-v5.0.5
236237
{py3.8,py3.11,py3.12}-celery-v5.4.0
237238

238-
{py3.6,py3.7}-dramatiq-v1.9.0
239-
{py3.6,py3.8,py3.9}-dramatiq-v1.12.3
239+
{py3.7}-dramatiq-v1.9.0
240+
{py3.7,py3.8,py3.9}-dramatiq-v1.12.3
240241
{py3.7,py3.10,py3.11}-dramatiq-v1.15.0
241242
{py3.8,py3.12,py3.13}-dramatiq-v1.17.1
242243

@@ -247,50 +248,47 @@ envlist =
247248

248249

249250
# ~~~ Web 1 ~~~
250-
{py3.6,py3.7,py3.8}-flask-v1.1.4
251+
{py3.7,py3.8}-flask-v1.1.4
251252
{py3.8,py3.12,py3.13}-flask-v2.3.3
252253
{py3.8,py3.12,py3.13}-flask-v3.0.3
253254
{py3.9,py3.12,py3.13}-flask-v3.1.0
254255

255-
{py3.6,py3.9,py3.10}-starlette-v0.16.0
256+
{py3.7,py3.9,py3.10}-starlette-v0.16.0
256257
{py3.7,py3.10,py3.11}-starlette-v0.26.1
257258
{py3.8,py3.11,py3.12}-starlette-v0.36.3
258259
{py3.9,py3.12,py3.13}-starlette-v0.45.3
259260

260261

261262
# ~~~ Web 2 ~~~
262-
{py3.6,py3.7}-bottle-v0.12.25
263-
{py3.6,py3.8,py3.9}-bottle-v0.13.2
263+
{py3.7}-bottle-v0.12.25
264+
{py3.7,py3.8,py3.9}-bottle-v0.13.2
264265

265-
{py3.6}-falcon-v1.4.1
266-
{py3.6,py3.7}-falcon-v2.0.0
267-
{py3.6,py3.11,py3.12}-falcon-v3.1.3
266+
{py3.7}-falcon-v2.0.0
267+
{py3.7,py3.11,py3.12}-falcon-v3.1.3
268268
{py3.8,py3.11,py3.12}-falcon-v4.0.2
269269

270-
{py3.6}-pyramid-v1.8.6
271-
{py3.6,py3.8,py3.9}-pyramid-v1.10.8
272-
{py3.6,py3.10,py3.11}-pyramid-v2.0.2
270+
{py3.7,py3.8,py3.9}-pyramid-v1.10.8
271+
{py3.7,py3.10,py3.11}-pyramid-v2.0.2
273272

274273
{py3.8,py3.10,py3.11}-starlite-v1.48.1
275274
{py3.8,py3.10,py3.11}-starlite-v1.49.0
276275
{py3.8,py3.10,py3.11}-starlite-v1.50.2
277276
{py3.8,py3.10,py3.11}-starlite-v1.51.16
278277

279-
{py3.6,py3.7,py3.8}-tornado-v6.0.4
280-
{py3.6,py3.8,py3.9}-tornado-v6.1
278+
{py3.7,py3.8}-tornado-v6.0.4
279+
{py3.7,py3.8,py3.9}-tornado-v6.1
281280
{py3.7,py3.9,py3.10}-tornado-v6.2
282281
{py3.8,py3.10,py3.11}-tornado-v6.4.2
283282

284283

285284
# ~~~ Misc ~~~
286-
{py3.6,py3.12,py3.13}-loguru-v0.7.3
285+
{py3.7,py3.12,py3.13}-loguru-v0.7.3
287286

288-
{py3.6}-trytond-v4.6.9
289-
{py3.6}-trytond-v4.8.18
290-
{py3.6,py3.7,py3.8}-trytond-v5.8.16
287+
{py3.7}-trytond-v5.0.9
288+
{py3.7,py3.8}-trytond-v5.8.16
291289
{py3.8,py3.10,py3.11}-trytond-v6.8.17
292290
{py3.8,py3.11,py3.12}-trytond-v7.0.9
293-
{py3.8,py3.11,py3.12}-trytond-v7.4.5
291+
{py3.8,py3.11,py3.12}-trytond-v7.4.6
294292

295293
{py3.7,py3.11,py3.12}-typer-v0.15.1
296294

@@ -555,13 +553,12 @@ deps =
555553
# ~~~ DBs ~~~
556554
clickhouse_driver-v0.2.9: clickhouse-driver==0.2.9
557555

558-
pymongo-v3.5.1: pymongo==3.5.1
556+
pymongo-v3.7.2: pymongo==3.7.2
559557
pymongo-v3.13.0: pymongo==3.13.0
560558
pymongo-v4.0.2: pymongo==4.0.2
561559
pymongo-v4.11.1: pymongo==4.11.1
562560
pymongo: mockupdb
563561

564-
redis_py_cluster_legacy-v1.3.6: redis-py-cluster==1.3.6
565562
redis_py_cluster_legacy-v2.0.0: redis-py-cluster==2.0.0
566563
redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3
567564

@@ -674,12 +671,10 @@ deps =
674671
bottle-v0.13.2: bottle==0.13.2
675672
bottle: werkzeug<2.1.0
676673

677-
falcon-v1.4.1: falcon==1.4.1
678674
falcon-v2.0.0: falcon==2.0.0
679675
falcon-v3.1.3: falcon==3.1.3
680676
falcon-v4.0.2: falcon==4.0.2
681677

682-
pyramid-v1.8.6: pyramid==1.8.6
683678
pyramid-v1.10.8: pyramid==1.10.8
684679
pyramid-v2.0.2: pyramid==2.0.2
685680
pyramid: werkzeug<2.1.0
@@ -709,15 +704,12 @@ deps =
709704
# ~~~ Misc ~~~
710705
loguru-v0.7.3: loguru==0.7.3
711706

712-
trytond-v4.6.9: trytond==4.6.9
713-
trytond-v4.8.18: trytond==4.8.18
707+
trytond-v5.0.9: trytond==5.0.9
714708
trytond-v5.8.16: trytond==5.8.16
715709
trytond-v6.8.17: trytond==6.8.17
716710
trytond-v7.0.9: trytond==7.0.9
717-
trytond-v7.4.5: trytond==7.4.5
711+
trytond-v7.4.6: trytond==7.4.6
718712
trytond: werkzeug
719-
trytond-v4.6.9: werkzeug<1.0
720-
trytond-v4.8.18: werkzeug<1.0
721713

722714
typer-v0.15.1: typer==0.15.1
723715

0 commit comments

Comments
 (0)