diff --git a/src/packaging/tags.py b/src/packaging/tags.py index 99e418373..d81d5564e 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -206,12 +206,25 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: """ Determine if the Python version supports abi3. - PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) + PEP 384 was first implemented in Python 3.2. The free-threaded builds do not support abi3. """ return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading +def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool: + """ + Determine if the Python version supports abi3t. + + PEP 803 was first implemented in Python 3.15 but, per PEP 803, this + returns tags going back to Python 3.2 to mirror the abi3 + implementation and leave open the possibility of abi3t wheels + supporting older Python versions. + + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) and threading + + def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] @@ -258,14 +271,18 @@ def cpython_tags( The specific tags generated are: - ``cp--`` - - ``cp-abi3-`` + - ``cp--`` - ``cp-none-`` - - ``cp-abi3-`` where "older version" is all older + - ``cp--`` where "older version" is all older minor versions down to Python 3.2 (when ``abi3`` was introduced) If ``python_version`` only provides a major-only version then only user-provided ABIs via ``abis`` and the ``none`` ABI will be used. + The ``stable_abi`` will be either ``abi3`` or ``abi3t`` if `abi` is a + GIL-enabled ABI like `"cp315"` or a free-threaded ABI like `"cp315t"`, + respectively. + :param Sequence python_version: A one- or two-item sequence representing the targeted Python version. Defaults to ``sys.version_info[:2]``. @@ -297,16 +314,27 @@ def cpython_tags( threading = _is_threaded_cpython(abis) use_abi3 = _abi3_applies(python_version, threading) + use_abi3t = _abi3t_applies(python_version, threading) + if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + if use_abi3t: + yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) - if use_abi3: + if use_abi3 or use_abi3t: for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: version = _version_nodot((python_version[0], minor_version)) interpreter = f"cp{version}" - yield Tag(interpreter, "abi3", platform_) + if use_abi3: + yield Tag(interpreter, "abi3", platform_) + if use_abi3t: + # Support for abi3t was introduced in Python 3.15, but in + # principle abi3t wheels are possible for older limited API + # versions, so allow things like ("cp37", "abi3t", "platform") + yield Tag(interpreter, "abi3t", platform_) def _generic_abi() -> list[str]: @@ -764,6 +792,8 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]: .. versionchanged:: 21.3 Added the `pp3-none-any` tag (:issue:`311`). + .. versionchanged:: 27.0 + Added the `abi3t` tag (:issue:`1099`). """ interp_name = interpreter_name() diff --git a/tests/test_tags.py b/tests/test_tags.py index 74d40a546..2d00060d0 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1038,10 +1038,104 @@ def test_all_args(self) -> None: assert result == [ tags.Tag("cp313", "cp313t", "plat1"), tags.Tag("cp313", "cp313t", "plat2"), + tags.Tag("cp313", "abi3t", "plat1"), + tags.Tag("cp313", "abi3t", "plat2"), tags.Tag("cp313", "none", "plat1"), tags.Tag("cp313", "none", "plat2"), + tags.Tag("cp312", "abi3t", "plat1"), + tags.Tag("cp312", "abi3t", "plat2"), + tags.Tag("cp311", "abi3t", "plat1"), + tags.Tag("cp311", "abi3t", "plat2"), + tags.Tag("cp310", "abi3t", "plat1"), + tags.Tag("cp310", "abi3t", "plat2"), + tags.Tag("cp39", "abi3t", "plat1"), + tags.Tag("cp39", "abi3t", "plat2"), + tags.Tag("cp38", "abi3t", "plat1"), + tags.Tag("cp38", "abi3t", "plat2"), + tags.Tag("cp37", "abi3t", "plat1"), + tags.Tag("cp37", "abi3t", "plat2"), + tags.Tag("cp36", "abi3t", "plat1"), + tags.Tag("cp36", "abi3t", "plat2"), + tags.Tag("cp35", "abi3t", "plat1"), + tags.Tag("cp35", "abi3t", "plat2"), + tags.Tag("cp34", "abi3t", "plat1"), + tags.Tag("cp34", "abi3t", "plat2"), + tags.Tag("cp33", "abi3t", "plat1"), + tags.Tag("cp33", "abi3t", "plat2"), + tags.Tag("cp32", "abi3t", "plat1"), + tags.Tag("cp32", "abi3t", "plat2"), ] + result = list(tags.cpython_tags((3, 15), ["cp315t"], ["platform"])) + assert result == [ + tags.Tag("cp315", "cp315t", "platform"), + tags.Tag("cp315", "abi3t", "platform"), + tags.Tag("cp315", "none", "platform"), + tags.Tag("cp314", "abi3t", "platform"), + tags.Tag("cp313", "abi3t", "platform"), + tags.Tag("cp312", "abi3t", "platform"), + tags.Tag("cp311", "abi3t", "platform"), + tags.Tag("cp310", "abi3t", "platform"), + tags.Tag("cp39", "abi3t", "platform"), + tags.Tag("cp38", "abi3t", "platform"), + tags.Tag("cp37", "abi3t", "platform"), + tags.Tag("cp36", "abi3t", "platform"), + tags.Tag("cp35", "abi3t", "platform"), + tags.Tag("cp34", "abi3t", "platform"), + tags.Tag("cp33", "abi3t", "platform"), + tags.Tag("cp32", "abi3t", "platform"), + ] + + result = list(tags.cpython_tags((3, 16), ["cp316t"], ["platform"])) + assert result == [ + tags.Tag("cp316", "cp316t", "platform"), + tags.Tag("cp316", "abi3t", "platform"), + tags.Tag("cp316", "none", "platform"), + tags.Tag("cp315", "abi3t", "platform"), + tags.Tag("cp314", "abi3t", "platform"), + tags.Tag("cp313", "abi3t", "platform"), + tags.Tag("cp312", "abi3t", "platform"), + tags.Tag("cp311", "abi3t", "platform"), + tags.Tag("cp310", "abi3t", "platform"), + tags.Tag("cp39", "abi3t", "platform"), + tags.Tag("cp38", "abi3t", "platform"), + tags.Tag("cp37", "abi3t", "platform"), + tags.Tag("cp36", "abi3t", "platform"), + tags.Tag("cp35", "abi3t", "platform"), + tags.Tag("cp34", "abi3t", "platform"), + tags.Tag("cp33", "abi3t", "platform"), + tags.Tag("cp32", "abi3t", "platform"), + ] + + result = list(tags.cpython_tags((3, 16), ["cp316"], ["platform"])) + assert result == [ + tags.Tag("cp316", "cp316", "platform"), + tags.Tag("cp316", "abi3", "platform"), + tags.Tag("cp316", "none", "platform"), + tags.Tag("cp315", "abi3", "platform"), + tags.Tag("cp314", "abi3", "platform"), + tags.Tag("cp313", "abi3", "platform"), + tags.Tag("cp312", "abi3", "platform"), + tags.Tag("cp311", "abi3", "platform"), + tags.Tag("cp310", "abi3", "platform"), + tags.Tag("cp39", "abi3", "platform"), + tags.Tag("cp38", "abi3", "platform"), + tags.Tag("cp37", "abi3", "platform"), + tags.Tag("cp36", "abi3", "platform"), + tags.Tag("cp35", "abi3", "platform"), + tags.Tag("cp34", "abi3", "platform"), + tags.Tag("cp33", "abi3", "platform"), + tags.Tag("cp32", "abi3", "platform"), + ] + + def test_no_abi3t_in_non_threaded_interpreter(self) -> None: + result = list(tags.cpython_tags((3, 16), ["cp316"], ["platform"])) + assert all(t.abi in ("cp316", "none", "abi3") for t in result) + + def test_no_abi3_in_threaded_interpreter(self) -> None: + result = list(tags.cpython_tags((3, 16), ["cp316t"], ["platform"])) + assert all(t.abi in ("cp316t", "none", "abi3t") for t in result) + def test_python_version_defaults(self) -> None: tag = next(tags.cpython_tags(abis=["abi3"], platforms=["any"])) interpreter = "cp" + tags._version_nodot(sys.version_info[:2])