Skip to content

Commit ed488d2

Browse files
committed
Added tests and fixed implementation
1 parent 918452c commit ed488d2

5 files changed

Lines changed: 252 additions & 48 deletions

File tree

extension_helpers/_setup_helpers.py

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
import logging
88
import os
9-
import re
10-
import sys
119
import shutil
1210
import subprocess
1311
import sys
@@ -16,7 +14,12 @@
1614
from setuptools import Extension, find_packages
1715
from setuptools.command.build_ext import new_compiler
1816

19-
from ._utils import import_file, walk_skip_hidden
17+
from ._utils import (
18+
abi_to_versions,
19+
get_limited_api_option,
20+
import_file,
21+
walk_skip_hidden,
22+
)
2023

2124
__all__ = ["get_compiler", "get_extensions", "pkg_config"]
2225

@@ -35,19 +38,6 @@
3538
"""
3639

3740

38-
def _abi_to_version_info(abi):
39-
match = re.fullmatch(r'^cp(\d)(\d+)$', abi)
40-
if match is None:
41-
return None
42-
else:
43-
return int(match[1]), int(match[2])
44-
45-
46-
def _version_info_to_version_hex(major=0, minor=0):
47-
"""Returns a PY_VERSION_HEX for {major}.{minor).0"""
48-
return f'0x{major:02x}{minor:02x}0000'
49-
50-
5141
def get_compiler():
5242
"""
5343
Determines the compiler that will be used to build extension modules.
@@ -163,22 +153,20 @@ def get_extensions(srcdir="."):
163153
extension.sources = sources
164154

165155
use_limited_api = False
166-
abi = get_distutils_option('py_limited_api', ['bdist_wheel'])
156+
abi = get_limited_api_option(srcdir=srcdir)
167157
if abi:
168-
version_info = _abi_to_version_info(abi)
169-
if version_info:
170-
use_limited_api = True
158+
version_info, version_hex = abi_to_versions(abi)
159+
160+
if version_info is None:
161+
log.warn("Unrecognized abi version for limited API: {abi}")
171162

172-
if use_limited_api:
173163
log.info(
174-
'Targeting PEP 384 limited API supporting Python >= %d.%d',
175-
*version_info)
176-
version_hex = _version_info_to_version_hex(*version_info)
164+
f"Targeting PEP 384 limited API supporting Python >= {version_info[0], version_info[1]}"
165+
)
166+
177167
for ext in ext_modules:
178168
ext.py_limited_api = True
179-
ext.define_macros.append(('Py_LIMITED_API', version_hex))
180-
else:
181-
log.warn(_PEP_384_WARNING)
169+
ext.define_macros.append(("Py_LIMITED_API", version_hex))
182170

183171
return ext_modules
184172

extension_helpers/_utils.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
# Licensed under a 3-clause BSD style license - see LICENSE.rst
22

33
import os
4+
import re
45
import sys
6+
from configparser import ConfigParser
57
from importlib import machinery as import_machinery
68
from importlib.util import module_from_spec, spec_from_file_location
79
from pathlib import Path
810

9-
__all__ = ["write_if_different", "import_file"]
11+
if sys.version_info >= (3, 11):
12+
import tomllib
13+
else:
14+
import tomli as tomllib
15+
16+
17+
__all__ = ["write_if_different", "import_file", "get_limited_api_option", "abi_to_versions"]
1018

1119

1220
if sys.platform == "win32":
@@ -138,3 +146,53 @@ def import_file(filename, name=None):
138146
loader.exec_module(mod)
139147

140148
return mod
149+
150+
151+
def get_limited_api_option(srcdir):
152+
"""
153+
Checks setup.cfg and pyproject.toml files in the current directory
154+
for the py_limited_api setting
155+
"""
156+
157+
srcdir = Path(srcdir)
158+
159+
setup_cfg = srcdir / "setup.cfg"
160+
161+
if setup_cfg.exists():
162+
cfg = ConfigParser()
163+
cfg.read(setup_cfg)
164+
if cfg.has_option("bdist_wheel", "py_limited_api"):
165+
return cfg.get("bdist_wheel", "py_limited_api")
166+
167+
pyproject = srcdir / "pyproject.toml"
168+
if pyproject.exists():
169+
with pyproject.open("rb") as f:
170+
pyproject_cfg = tomllib.load(f)
171+
if (
172+
"tool" in pyproject_cfg
173+
and "distutils" in pyproject_cfg["tool"]
174+
and "bdist_wheel" in pyproject_cfg["tool"]["distutils"]
175+
and "py-limited-api" in pyproject_cfg["tool"]["distutils"]["bdist_wheel"]
176+
):
177+
return pyproject_cfg["tool"]["distutils"]["bdist_wheel"]["py-limited-api"]
178+
179+
180+
def _abi_to_version_info(abi):
181+
match = re.fullmatch(r"^cp(\d)(\d+)$", abi)
182+
if match is None:
183+
return None
184+
else:
185+
return int(match[1]), int(match[2])
186+
187+
188+
def _version_info_to_version_hex(major=0, minor=0):
189+
"""Returns a PY_VERSION_HEX for {major}.{minor).0"""
190+
return f"0x{major:02X}{minor:02X}0000"
191+
192+
193+
def abi_to_versions(abi):
194+
version_info = _abi_to_version_info(abi)
195+
if version_info is None:
196+
return None, None
197+
else:
198+
return version_info, _version_info_to_version_hex(*version_info)

extension_helpers/tests/test_setup_helpers.py

Lines changed: 111 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ def test_get_compiler():
3535
assert get_compiler() in POSSIBLE_COMPILERS
3636

3737

38-
def _extension_test_package(tmp_path, request, extension_type="c", include_numpy=False):
38+
def _extension_test_package(
39+
tmp_path,
40+
request=None,
41+
extension_type="c",
42+
include_numpy=False,
43+
include_setup_py=True,
44+
):
3945
"""Creates a simple test package with an extension module."""
4046

4147
test_pkg = tmp_path / "test_pkg"
@@ -106,24 +112,25 @@ def get_extensions():
106112
)
107113
)
108114

109-
(test_pkg / "setup.py").write_text(
110-
dedent(
111-
f"""\
112-
import sys
113-
from os.path import join
114-
from setuptools import setup, find_packages
115-
sys.path.insert(0, r'{extension_helpers_PATH}')
116-
from extension_helpers import get_extensions
117-
118-
setup(
119-
name='helpers_test_package',
120-
version='0.1',
121-
packages=find_packages(),
122-
ext_modules=get_extensions()
123-
)
124-
"""
115+
if include_setup_py:
116+
(test_pkg / "setup.py").write_text(
117+
dedent(
118+
f"""\
119+
import sys
120+
from os.path import join
121+
from setuptools import setup, find_packages
122+
sys.path.insert(0, r'{extension_helpers_PATH}')
123+
from extension_helpers import get_extensions
124+
125+
setup(
126+
name='helpers_test_package',
127+
version='0.1',
128+
packages=find_packages(),
129+
ext_modules=get_extensions()
130+
)
131+
"""
132+
)
125133
)
126-
)
127134

128135
if "" in sys.path:
129136
sys.path.remove("")
@@ -133,7 +140,8 @@ def get_extensions():
133140
def finalize():
134141
cleanup_import("helpers_test_package")
135142

136-
request.addfinalizer(finalize)
143+
if request:
144+
request.addfinalizer(finalize)
137145

138146
return test_pkg
139147

@@ -455,3 +463,87 @@ def test():
455463
pass
456464
else:
457465
raise AssertionError(package_name + ".compiler_version should not exist")
466+
467+
468+
# Tests to make sure that limited API support works correctly
469+
470+
471+
@pytest.mark.parametrize("config", ("setup.cfg", "pyproject.toml"))
472+
@pytest.mark.parametrize("limited_api", (None, "cp311"))
473+
@pytest.mark.parametrize("extension_type", ("c", "pyx", "both"))
474+
def test_limited_api(tmp_path, config, limited_api, extension_type):
475+
476+
package = _extension_test_package(
477+
tmp_path, extension_type=extension_type, include_numpy=True, include_setup_py=False
478+
)
479+
480+
if config == "setup.cfg":
481+
482+
setup_cfg = dedent(
483+
"""\
484+
[metadata]
485+
name = helpers_test_package
486+
version = 0.1
487+
488+
[options]
489+
packages = find:
490+
491+
[extension-helpers]
492+
use_extension_helpers = true
493+
"""
494+
)
495+
496+
if limited_api:
497+
setup_cfg += f"\n[bdist_wheel]\npy_limited_api={limited_api}"
498+
499+
(package / "setup.cfg").write_text(setup_cfg)
500+
501+
# Still require a minimal pyproject.toml file if no setup.py file
502+
503+
(package / "pyproject.toml").write_text(
504+
dedent(
505+
"""
506+
[build-system]
507+
requires = ["setuptools>=43.0.0",
508+
"wheel"]
509+
build-backend = 'setuptools.build_meta'
510+
511+
[tool.extension-helpers]
512+
use_extension_helpers = true
513+
"""
514+
)
515+
)
516+
517+
elif config == "pyproject.toml":
518+
519+
pyproject_toml = dedent(
520+
"""\
521+
[build-system]
522+
requires = ["setuptools>=43.0.0",
523+
"wheel"]
524+
build-backend = 'setuptools.build_meta'
525+
526+
[project]
527+
name = "hehlpers_test_package"
528+
version = "0.1"
529+
530+
[tool.setuptools.packages]
531+
find = {namespaces = false}
532+
533+
[tool.extension-helpers]
534+
use_extension_helpers = true
535+
"""
536+
)
537+
538+
if limited_api:
539+
pyproject_toml += f'\n[tool.distutils.bdist_wheel]\npy-limited-api = "{limited_api}"'
540+
541+
(package / "pyproject.toml").write_text(pyproject_toml)
542+
543+
with chdir(package):
544+
subprocess.run([sys.executable, "-m", "build", "--wheel", "--no-isolation"], check=True)
545+
546+
wheels = os.listdir(package / "dist")
547+
548+
assert len(wheels) == 1
549+
assert ("abi3" in wheels[0]) == (limited_api is not None)

extension_helpers/tests/test_utils.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33

44
import pytest
55

6-
from .._utils import import_file, write_if_different
6+
from .._utils import (
7+
abi_to_versions,
8+
get_limited_api_option,
9+
import_file,
10+
write_if_different,
11+
)
712

813

914
@pytest.mark.parametrize("path_type", ("str", "path"))
@@ -32,3 +37,63 @@ def test_write_if_different(tmp_path, path_type):
3237
write_if_different(filepath, b"abcd")
3338
time3 = os.path.getmtime(filepath)
3439
assert time3 > time1
40+
41+
42+
class TestGetLimitedAPIOption:
43+
44+
def test_nofiles(self, tmp_path):
45+
assert get_limited_api_option(tmp_path) is None
46+
47+
def test_empty_setup_cfg(self, tmp_path):
48+
filepath = (tmp_path / "setup.cfg").write_text("")
49+
assert get_limited_api_option(tmp_path) is None
50+
51+
def test_empty_pyproject_toml(self, tmp_path):
52+
filepath = (tmp_path / "pyproject.toml").write_text("")
53+
assert get_limited_api_option(tmp_path) is None
54+
55+
def test_setup_cfg(self, tmp_path):
56+
57+
filepath = (tmp_path / "setup.cfg").write_text("[bdist_wheel]\npy_limited_api=cp311")
58+
assert get_limited_api_option(tmp_path) == "cp311"
59+
60+
# Make sure things still work even if an empty pyproject.toml file is present
61+
62+
filepath = (tmp_path / "pyproject.toml").write_text("")
63+
assert get_limited_api_option(tmp_path) == "cp311"
64+
65+
# And if the pyproject.toml has the right section but not the right option
66+
67+
filepath = (tmp_path / "setup.cfg.toml").write_text(
68+
"[tool.distutils.bdist_wheel]\nspam=1\n"
69+
)
70+
assert get_limited_api_option(tmp_path) == "cp311"
71+
72+
def test_pyproject(self, tmp_path):
73+
74+
filepath = (tmp_path / "pyproject.toml").write_text(
75+
'[tool.distutils.bdist_wheel]\npy-limited-api="cp312"\n'
76+
)
77+
assert get_limited_api_option(tmp_path) == "cp312"
78+
79+
# Make sure things still work even if an empty setup.cfg file is present
80+
81+
filepath = (tmp_path / "setup.cfg.toml").write_text("\n")
82+
assert get_limited_api_option(tmp_path) == "cp312"
83+
84+
# And if the setup.cfg has the right section but not the right option
85+
86+
filepath = (tmp_path / "setup.cfg.toml").write_text("[bdist_wheel]\nspam=1\n")
87+
assert get_limited_api_option(tmp_path) == "cp312"
88+
89+
90+
def test_abi_to_versions_invalid():
91+
assert abi_to_versions("spam") == (None, None)
92+
93+
94+
def test_abi_to_versions_valid():
95+
assert abi_to_versions("cp39") == ((3, 9), "0x03090000")
96+
assert abi_to_versions("cp310") == ((3, 10), "0x030A0000")
97+
assert abi_to_versions("cp311") == ((3, 11), "0x030B0000")
98+
assert abi_to_versions("cp312") == ((3, 12), "0x030C0000")
99+
assert abi_to_versions("cp313") == ((3, 13), "0x030D0000")

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test = [
3535
"pytest",
3636
"pytest-cov",
3737
"cython",
38+
"build"
3839
]
3940
docs = [
4041
"sphinx",

0 commit comments

Comments
 (0)