From f0d87d16d91803e430648c8a8332c406ca36f524 Mon Sep 17 00:00:00 2001 From: Stewart Miles Date: Fri, 20 Sep 2024 11:07:48 -0700 Subject: [PATCH] Add support for wheel compatibility with the limited API. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://docs.python.org/3/c-api/stable.html#limited-c-api > Python 3.2 introduced the Limited API, a subset of Python’s C API. > Extensions that only use the Limited API can be compiled once and > work with multiple versions of Python Python 3.2 This adds `abi3` to the list of compatible ABIs for all versions of Python beyond 3.2. Fixes #225 --- distlib/wheel.py | 63 ++++++++++++++++++++++++++++----------------- tests/test_wheel.py | 25 ++++++++++++++++-- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/distlib/wheel.py b/distlib/wheel.py index e04a515..92a1bee 100644 --- a/distlib/wheel.py +++ b/distlib/wheel.py @@ -987,11 +987,16 @@ def compatible_tags(): """ Return (pyver, abi, arch) tuples compatible with this Python. """ - versions = [VER_SUFFIX] - major = VER_SUFFIX[0] - for minor in range(sys.version_info[1] - 1, -1, -1): - versions.append(''.join([major, str(minor)])) - + class _Version: + def __init__(self, major, minor): + self.major = major + self.major_minor = (major, minor) + self.suffix = ''.join((str(major), str(minor))) + + versions = [ + _Version(sys.version_info.major, minor_version) + for minor_version in range(sys.version_info.minor, -1, -1) + ] abis = [] for suffix in _get_suffixes(): if suffix.startswith('.abi'): @@ -1000,6 +1005,7 @@ def compatible_tags(): if ABI != 'none': abis.insert(0, ABI) abis.append('none') + result = [] arches = [ARCH] @@ -1027,31 +1033,42 @@ def compatible_tags(): minor -= 1 # Most specific - our Python version, ABI and arch - for abi in abis: - for arch in arches: - result.append((''.join((IMP_PREFIX, versions[0])), abi, arch)) - # manylinux - if abi != 'none' and sys.platform.startswith('linux'): - arch = arch.replace('linux_', '') - parts = _get_glibc_version() - if len(parts) == 2: - if parts >= (2, 5): - result.append((''.join((IMP_PREFIX, versions[0])), abi, 'manylinux1_%s' % arch)) - if parts >= (2, 12): - result.append((''.join((IMP_PREFIX, versions[0])), abi, 'manylinux2010_%s' % arch)) - if parts >= (2, 17): - result.append((''.join((IMP_PREFIX, versions[0])), abi, 'manylinux2014_%s' % arch)) - result.append((''.join( - (IMP_PREFIX, versions[0])), abi, 'manylinux_%s_%s_%s' % (parts[0], parts[1], arch))) + for i, version_object in enumerate(versions): + version = version_object.suffix + add_abis = [] + if i == 0: + add_abis = abis + elif IMP_PREFIX == 'cp' and version_object.major_minor >= (3, 2): + limited_api_abi = 'abi' + str(version_object.major) + add_abis = [limited_api_abi] if limited_api_abi in abis else [] + + for abi in add_abis: + for arch in arches: + result.append((''.join((IMP_PREFIX, version)), abi, arch)) + # manylinux + if abi != 'none' and sys.platform.startswith('linux'): + arch = arch.replace('linux_', '') + parts = _get_glibc_version() + if len(parts) == 2: + if parts >= (2, 5): + result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux1_%s' % arch)) + if parts >= (2, 12): + result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2010_%s' % arch)) + if parts >= (2, 17): + result.append((''.join((IMP_PREFIX, version)), abi, 'manylinux2014_%s' % arch)) + result.append((''.join( + (IMP_PREFIX, version)), abi, 'manylinux_%s_%s_%s' % (parts[0], parts[1], arch))) # where no ABI / arch dependency, but IMP_PREFIX dependency - for i, version in enumerate(versions): + for i, version_object in enumerate(versions): + version = version_object.suffix result.append((''.join((IMP_PREFIX, version)), 'none', 'any')) if i == 0: result.append((''.join((IMP_PREFIX, version[0])), 'none', 'any')) # no IMP_PREFIX, ABI or arch dependency - for i, version in enumerate(versions): + for i, version_object in enumerate(versions): + version = version_object.suffix result.append((''.join(('py', version)), 'none', 'any')) if i == 0: result.append((''.join(('py', version[0])), 'none', 'any')) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 80cabc7..010ce6c 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -24,8 +24,8 @@ from distlib.metadata import Metadata, METADATA_FILENAME, LEGACY_METADATA_FILENAME from distlib.scripts import ScriptMaker from distlib.util import get_executable -from distlib.wheel import (Wheel, PYVER, IMPVER, ARCH, ABI, COMPATIBLE_TAGS, IMP_PREFIX, is_compatible, - _get_glibc_version) +from distlib.wheel import (Wheel, PYVER, IMPVER, ARCH, ABI, COMPATIBLE_TAGS, IMP_PREFIX, + VER_SUFFIX, is_compatible, _get_glibc_version) try: with open(os.devnull, 'wb') as junk: @@ -356,6 +356,27 @@ def test_is_compatible(self): s = 'manylinux_%s_%s_' % parts self.assertIn(s, arch) + def test_is_compatible_limited_abi(self): + major_version = int(VER_SUFFIX[0]) + minor_version = int(VER_SUFFIX[1]) + minimum_abi3_version = (3, 2) + if not ((major_version, minor_version) >= minimum_abi3_version and IMP_PREFIX == 'cp'): + self.skipTest('Python %s does not support the limited API' % VER_SUFFIX) + + compatible_wheel_filenames = [ + 'dummy-0.1-cp%d%d-abi3-%s.whl' % (major_version, current_minor_version, ARCH) + for current_minor_version in range(minor_version, -1, -1) + if (major_version, current_minor_version) >= minimum_abi3_version + ] + incompatible_wheel_filenames = [ + 'dummy-0.1-cp%d%d-%s-%s.whl' % (major_version, current_minor_version, ABI, ARCH) + for current_minor_version in range(minor_version - 1, -1, -1) + ] + for wheel_filename in compatible_wheel_filenames: + self.assertTrue(is_compatible(wheel_filename), msg=wheel_filename) + for wheel_filename in incompatible_wheel_filenames: + self.assertFalse(is_compatible(wheel_filename), msg=wheel_filename) + def test_metadata(self): fn = os.path.join(HERE, 'dummy-0.1-py27-none-any.whl') w = Wheel(fn)