Skip to content

Commit

Permalink
Add support for wheel compatibility with the limited API.
Browse files Browse the repository at this point in the history
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 pypa#225
  • Loading branch information
stewartmiles committed Sep 20, 2024
1 parent d767287 commit f0d87d1
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 25 deletions.
63 changes: 40 additions & 23 deletions distlib/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand All @@ -1000,6 +1005,7 @@ def compatible_tags():
if ABI != 'none':
abis.insert(0, ABI)
abis.append('none')

result = []

arches = [ARCH]
Expand Down Expand Up @@ -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'))
Expand Down
25 changes: 23 additions & 2 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit f0d87d1

Please sign in to comment.