Skip to content

Commit

Permalink
Add support for wheel compatibility with the limited API. (#228)
Browse files Browse the repository at this point in the history
* 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 pypa/pip#4445).
  • Loading branch information
stewartmiles authored Oct 7, 2024
1 parent fac84c7 commit 2c4d2fa
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 24 deletions.
67 changes: 45 additions & 22 deletions distlib/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Expand Down Expand Up @@ -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'))
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 = 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)
Expand Down

0 comments on commit 2c4d2fa

Please sign in to comment.