1111import subprocess
1212import sys
1313import time
14+ from dataclasses import dataclass
1415from bisect import bisect_left
1516from collections import defaultdict
1617from datetime import datetime , timedelta , timezone # noqa: F401
6869}
6970
7071
72+ @dataclass (order = True )
73+ class ThreadedVersion :
74+ version : Version
75+ no_gil : bool
76+
77+ def __init__ (self , version : str | Version , no_gil = False ):
78+ self .version = Version (version ) if isinstance (version , str ) else version
79+ self .no_gil = no_gil
80+
81+ def __str__ (self ):
82+ version = f"py{ self .version .major } .{ self .version .minor } "
83+ if self .no_gil :
84+ version += "t"
85+
86+ return version
87+
88+
89+ # Free-threading is experimentally supported in 3.13, and officially supported in 3.14.
90+ MIN_FREE_THREADING_SUPPORT = ThreadedVersion ("3.14" )
91+
92+
7193def _fetch_sdk_metadata () -> PackageMetadata :
7294 (dist ,) = distributions (
7395 name = "sentry-sdk" , path = [Path (__file__ ).parent .parent .parent ]
@@ -404,12 +426,18 @@ def supported_python_versions(
404426 return supported
405427
406428
407- def pick_python_versions_to_test (python_versions : list [Version ]) -> list [Version ]:
429+ def pick_python_versions_to_test (
430+ python_versions : list [Version ], has_free_threading_wheel : bool
431+ ) -> list [Version ]:
408432 """
409433 Given a list of Python versions, pick those that make sense to test on.
410434
411435 Currently, this is the oldest, the newest, and the second newest Python
412436 version.
437+
438+ the free-threaded variant of the newest version is also selected if
439+ - a free-threaded wheel is distributed; and
440+ - the SDK supports free-threading on the newest supported version.
413441 """
414442 filtered_python_versions = {
415443 python_versions [0 ],
@@ -421,7 +449,16 @@ def pick_python_versions_to_test(python_versions: list[Version]) -> list[Version
421449 except IndexError :
422450 pass
423451
424- return sorted (filtered_python_versions )
452+ versions_to_test = sorted (
453+ ThreadedVersion (version ) for version in filtered_python_versions
454+ )
455+
456+ if has_free_threading_wheel and versions_to_test [- 1 ] >= MIN_FREE_THREADING_SUPPORT :
457+ versions_to_test .append (
458+ ThreadedVersion (versions_to_test [- 1 ].version , no_gil = True )
459+ )
460+
461+ return versions_to_test
425462
426463
427464def _parse_python_versions_from_classifiers (classifiers : list [str ]) -> list [Version ]:
@@ -475,12 +512,23 @@ def determine_python_versions(pypi_data: dict) -> Union[SpecifierSet, list[Versi
475512 return []
476513
477514
478- def _render_python_versions (python_versions : list [Version ]) -> str :
479- return (
480- "{"
481- + "," .join (f"py{ version .major } .{ version .minor } " for version in python_versions )
482- + "}"
483- )
515+ def has_free_threading_wheel (pypi_data : dict ) -> bool :
516+ for download in pypi_data ["urls" ]:
517+ print (download )
518+
519+ if download ["packagetype" ] == "bdist_wheel" :
520+ abi_tag = download ["filename" ].removesuffix (".whl" ).split ("-" )[- 2 ]
521+
522+ if abi_tag == "none" or (
523+ abi_tag .endswith ("t" ) and abi_tag .startswith ("cp314" )
524+ ):
525+ return True
526+
527+ return False
528+
529+
530+ def _render_python_versions (python_versions : list [ThreadedVersion ]) -> str :
531+ return "{" + "," .join (str (version ) for version in python_versions ) + "}"
484532
485533
486534def _render_dependencies (integration : str , releases : list [Version ]) -> list [str ]:
@@ -590,7 +638,8 @@ def _add_python_versions_to_release(
590638 determine_python_versions (release_pypi_data ),
591639 target_python_versions ,
592640 release ,
593- )
641+ ),
642+ has_free_threading_wheel (release_pypi_data ),
594643 )
595644
596645 release .rendered_python_versions = _render_python_versions (release .python_versions )
@@ -647,8 +696,16 @@ def _normalize_name(package: str) -> str:
647696 return package .lower ().replace ("-" , "_" )
648697
649698
699+ def _extract_wheel_info_to_cache (wheel : dict ):
700+ return {
701+ "packagetype" : wheel ["packagetype" ],
702+ "filename" : wheel ["filename" ],
703+ }
704+
705+
650706def _normalize_release (release : dict ) -> dict :
651707 """Filter out unneeded parts of the release JSON."""
708+ urls = [_extract_wheel_info_to_cache (wheel ) for wheel in release ["urls" ]]
652709 normalized = {
653710 "info" : {
654711 "classifiers" : release ["info" ]["classifiers" ],
@@ -657,6 +714,7 @@ def _normalize_release(release: dict) -> dict:
657714 "version" : release ["info" ]["version" ],
658715 "yanked" : release ["info" ]["yanked" ],
659716 },
717+ "urls" : urls ,
660718 }
661719 return normalized
662720
@@ -766,7 +824,7 @@ def main() -> dict[str, list]:
766824
767825 print (
768826 "Done generating tox.ini. Make sure to also update the CI YAML "
769- "files to reflect the new test targets ."
827+ "files by executing split_tox_gh_actions.py ."
770828 )
771829
772830 return packages
0 commit comments