From 33459f0eeb4f6158020bec7b437e58d6be25ab74 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Feb 2026 11:13:17 -0700 Subject: [PATCH 1/6] Add support for abi3.abi3t tag from PEP 803 --- src/packaging/tags.py | 22 +++++++++++++++++++++- tests/test_tags.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index e3cc602ec..322433261 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -173,12 +173,20 @@ 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) -> bool: + """ + Determine if the Python version supports abi3.abi3t. + + PEP 803 was first implemented in Python 3.15. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 15) + def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] @@ -256,8 +264,13 @@ def cpython_tags( threading = _is_threaded_cpython(abis) use_abi3 = _abi3_applies(python_version, threading) + use_abi3t = _abi3t_applies(python_version) + if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + if use_abi3t: + yield from (Tag(interpreter, "abi3.abi3t", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) if use_abi3: @@ -267,6 +280,13 @@ def cpython_tags( interpreter = f"cp{version}" yield Tag(interpreter, "abi3", platform_) + if use_abi3t: + for minor_version in range(python_version[1] - 1, 14, -1): + for platform_ in platforms: + version = _version_nodot((python_version[0], minor_version)) + interpreter = f"cp{version}" + yield Tag(interpreter, "abi3.abi3t", platform_) + def _generic_abi() -> list[str]: """ diff --git a/tests/test_tags.py b/tests/test_tags.py index 0439377ac..07d78a112 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1042,6 +1042,44 @@ def test_all_args(self) -> None: tags.Tag("cp313", "none", "plat2"), ] + result = list(tags.cpython_tags((3, 15), ["cp315t"], ["platform"])) + assert result == [ + tags.Tag("cp315", "cp315t", "platform"), + tags.Tag("cp315", "abi3.abi3t", "platform"), + tags.Tag("cp315", "none", "platform"), + ] + + result = list(tags.cpython_tags((3, 16), ["cp316t"], ["platform"])) + assert result == [ + tags.Tag("cp316", "cp316t", "platform"), + tags.Tag("cp316", "abi3.abi3t", "platform"), + tags.Tag("cp316", "none", "platform"), + tags.Tag("cp315", "abi3.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", "abi3.abi3t", "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"), + tags.Tag("cp315", "abi3.abi3t", "platform"), + ] + 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]) From a8e97af48fb70469d9a79bfbe8fb3dca2359cad0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 24 Feb 2026 12:28:06 -0700 Subject: [PATCH 2/6] fix linter --- src/packaging/tags.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index 322433261..b28b032ea 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -187,6 +187,7 @@ def _abi3t_applies(python_version: PythonVersion) -> bool: """ return len(python_version) > 1 and tuple(python_version) >= (3, 15) + def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: py_version = tuple(py_version) # To allow for version comparison. abis = [] @@ -269,7 +270,9 @@ def cpython_tags( if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) if use_abi3t: - yield from (Tag(interpreter, "abi3.abi3t", platform_) for platform_ in platforms) + yield from ( + Tag(interpreter, "abi3.abi3t", platform_) for platform_ in platforms + ) yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) From dfa8bd1b8bccc61b040b1f000409a79f25b750ac Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 5 Mar 2026 11:54:30 -0700 Subject: [PATCH 3/6] Don't return compressed tag sets from cpython_tags --- src/packaging/tags.py | 6 ++---- tests/test_tags.py | 10 +++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index b28b032ea..c77e14a59 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -270,9 +270,7 @@ def cpython_tags( if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) if use_abi3t: - yield from ( - Tag(interpreter, "abi3.abi3t", platform_) for platform_ in platforms - ) + yield from (Tag(interpreter, "abi3t", platform_) for platform_ in platforms) yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) @@ -288,7 +286,7 @@ def cpython_tags( for platform_ in platforms: version = _version_nodot((python_version[0], minor_version)) interpreter = f"cp{version}" - yield Tag(interpreter, "abi3.abi3t", platform_) + yield Tag(interpreter, "abi3t", platform_) def _generic_abi() -> list[str]: diff --git a/tests/test_tags.py b/tests/test_tags.py index 07d78a112..dabded7de 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1045,23 +1045,23 @@ def test_all_args(self) -> None: result = list(tags.cpython_tags((3, 15), ["cp315t"], ["platform"])) assert result == [ tags.Tag("cp315", "cp315t", "platform"), - tags.Tag("cp315", "abi3.abi3t", "platform"), + tags.Tag("cp315", "abi3t", "platform"), tags.Tag("cp315", "none", "platform"), ] result = list(tags.cpython_tags((3, 16), ["cp316t"], ["platform"])) assert result == [ tags.Tag("cp316", "cp316t", "platform"), - tags.Tag("cp316", "abi3.abi3t", "platform"), + tags.Tag("cp316", "abi3t", "platform"), tags.Tag("cp316", "none", "platform"), - tags.Tag("cp315", "abi3.abi3t", "platform"), + tags.Tag("cp315", "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", "abi3.abi3t", "platform"), + tags.Tag("cp316", "abi3t", "platform"), tags.Tag("cp316", "none", "platform"), tags.Tag("cp315", "abi3", "platform"), tags.Tag("cp314", "abi3", "platform"), @@ -1077,7 +1077,7 @@ def test_all_args(self) -> None: tags.Tag("cp34", "abi3", "platform"), tags.Tag("cp33", "abi3", "platform"), tags.Tag("cp32", "abi3", "platform"), - tags.Tag("cp315", "abi3.abi3t", "platform"), + tags.Tag("cp315", "abi3t", "platform"), ] def test_python_version_defaults(self) -> None: From 972f4eccb885e915b53702c2b00ebec4d0f1714c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 5 Mar 2026 16:27:25 -0700 Subject: [PATCH 4/6] abi3t comes just after abi3 in priority --- src/packaging/tags.py | 14 +++++--------- tests/test_tags.py | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index c77e14a59..6e53d6480 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -274,19 +274,15 @@ def cpython_tags( 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_abi3t: - for minor_version in range(python_version[1] - 1, 14, -1): - for platform_ in platforms: - version = _version_nodot((python_version[0], minor_version)) - interpreter = f"cp{version}" - yield Tag(interpreter, "abi3t", platform_) + if use_abi3: + yield Tag(interpreter, "abi3", platform_) + if use_abi3t and minor_version > 14: + yield Tag(interpreter, "abi3t", platform_) def _generic_abi() -> list[str]: diff --git a/tests/test_tags.py b/tests/test_tags.py index dabded7de..6e19014be 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1064,6 +1064,7 @@ def test_all_args(self) -> None: tags.Tag("cp316", "abi3t", "platform"), tags.Tag("cp316", "none", "platform"), tags.Tag("cp315", "abi3", "platform"), + tags.Tag("cp315", "abi3t", "platform"), tags.Tag("cp314", "abi3", "platform"), tags.Tag("cp313", "abi3", "platform"), tags.Tag("cp312", "abi3", "platform"), @@ -1077,7 +1078,6 @@ def test_all_args(self) -> None: tags.Tag("cp34", "abi3", "platform"), tags.Tag("cp33", "abi3", "platform"), tags.Tag("cp32", "abi3", "platform"), - tags.Tag("cp315", "abi3t", "platform"), ] def test_python_version_defaults(self) -> None: From 3b0a55bdea0277948a3b385734b6d857e6590c3e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 5 Mar 2026 16:40:02 -0700 Subject: [PATCH 5/6] Don't set the abi3t tag for GIL-enabled builds --- src/packaging/tags.py | 6 +++--- tests/test_tags.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index 6e53d6480..80d6cb925 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -179,13 +179,13 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading -def _abi3t_applies(python_version: PythonVersion) -> bool: +def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool: """ Determine if the Python version supports abi3.abi3t. PEP 803 was first implemented in Python 3.15. """ - return len(python_version) > 1 and tuple(python_version) >= (3, 15) + return len(python_version) > 1 and tuple(python_version) >= (3, 15) and threading def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: @@ -265,7 +265,7 @@ def cpython_tags( threading = _is_threaded_cpython(abis) use_abi3 = _abi3_applies(python_version, threading) - use_abi3t = _abi3t_applies(python_version) + use_abi3t = _abi3t_applies(python_version, threading) if use_abi3: yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) diff --git a/tests/test_tags.py b/tests/test_tags.py index 6e19014be..e272de313 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1061,10 +1061,8 @@ def test_all_args(self) -> None: assert result == [ tags.Tag("cp316", "cp316", "platform"), tags.Tag("cp316", "abi3", "platform"), - tags.Tag("cp316", "abi3t", "platform"), tags.Tag("cp316", "none", "platform"), tags.Tag("cp315", "abi3", "platform"), - tags.Tag("cp315", "abi3t", "platform"), tags.Tag("cp314", "abi3", "platform"), tags.Tag("cp313", "abi3", "platform"), tags.Tag("cp312", "abi3", "platform"), From a7b31429b21e6170c3380760bbcab9553ebffbed Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Mar 2026 10:53:23 -0700 Subject: [PATCH 6/6] Respond to review comments --- src/packaging/tags.py | 9 ++++--- tests/test_tags.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/packaging/tags.py b/src/packaging/tags.py index 80d6cb925..a3e931b8a 100644 --- a/src/packaging/tags.py +++ b/src/packaging/tags.py @@ -181,11 +181,11 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: def _abi3t_applies(python_version: PythonVersion, threading: bool) -> bool: """ - Determine if the Python version supports abi3.abi3t. + Determine if the Python version supports abi3t. PEP 803 was first implemented in Python 3.15. """ - return len(python_version) > 1 and tuple(python_version) >= (3, 15) and threading + return len(python_version) > 1 and tuple(python_version) >= (3, 2) and threading def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: @@ -281,7 +281,10 @@ def cpython_tags( interpreter = f"cp{version}" if use_abi3: yield Tag(interpreter, "abi3", platform_) - if use_abi3t and minor_version > 14: + 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_) diff --git a/tests/test_tags.py b/tests/test_tags.py index e272de313..8c86d656d 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -1038,8 +1038,32 @@ 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"])) @@ -1047,6 +1071,19 @@ def test_all_args(self) -> None: 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"])) @@ -1055,6 +1092,19 @@ def test_all_args(self) -> None: 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"])) @@ -1078,6 +1128,14 @@ def test_all_args(self) -> None: 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])