From 2c4d2fa5747628a320369ed97b0b5f8033c56dbe Mon Sep 17 00:00:00 2001 From: Stewart Miles Date: Mon, 7 Oct 2024 07:35:31 -0700 Subject: [PATCH] Add support for wheel compatibility with the limited API. (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add support for wheel compatibility with the limited API. 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 * Add missing import for test_wheel.py * Unconditionally add limited ABI support to each version. On Windows imp.get_suffixes() doesn't return the limited ABI as supported even though - when carefully building an extension - it is (see the discussion on https://github.com/pypa/pip/issues/4445). --- distlib/wheel.py | 67 ++++++++++++++++++++++++++++++--------------- tests/test_wheel.py | 25 +++++++++++++++-- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/distlib/wheel.py b/distlib/wheel.py index e04a515..62ab10f 100644 --- a/distlib/wheel.py +++ b/distlib/wheel.py @@ -987,11 +987,20 @@ 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.string = ''.join((str(major), str(minor))) + def __str__(self): + return self.string + + + 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'): @@ -1027,31 +1036,45 @@ 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 = str(version_object) + add_abis = [] + + if i == 0: + add_abis = abis + + if IMP_PREFIX == 'cp' and version_object.major_minor >= (3, 2): + limited_api_abi = 'abi' + str(version_object.major) + if limited_api_abi not in add_abis: + add_abis.append(limited_api_abi) + + 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 = str(version_object) 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 = str(version_object) 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..70b3884 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 = sys.version_info.major + minor_version = sys.version_info.minor + 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)