Skip to content

Commit 5b22fc6

Browse files
mayeuthenryiiijoerick
authored
feat: add CPython 3.10 pre-release support (#675)
* Add CPython 3.10 support * Fix logger displaying CPython 3.1 instead of 3.10 * Fix tests failing with CPython 3.10 * Use pytest instead of nose * feat: --pre flag Apply suggestions from code review Co-authored-by: Matthieu Darbois <[email protected]> * fix: update Python update script to process beta versions * refactor: prerelease-pythons * Use `strtobool` to parse `CIBW_PRERELEASE_PYTHONS` env var * Update python version filtering for universal2 & arm64 * Filter out CPython 3.10 and above for `test_manylinuxXXXX_only[manylinux1]` test * Use CIBW_BUILD filtering rather than CIBW_SKIP for test_docker_images * Use skip_patterns to filter out pre-releases * Use `prerelease_pythons` instead of `pre` * Reword `CIBW_PRERELEASE_PYTHONS` doc per review. * Use `CIBW_PRERELEASE_PYTHONS: True` for usage example. * Update `cibuildwheel --help` doc * docs: add note on spec.filter * Clean up the BuildSelector __repr__ by refactoring * fix: remove platform variants for CIBW_PRERELEASE_PYTHONS * docs: mention the flag Co-authored-by: Henry Schreiner <[email protected]> Co-authored-by: Joe Rickerby <[email protected]>
1 parent f294cab commit 5b22fc6

17 files changed

+166
-53
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ What does it do?
2828
| CPython 3.7 || N/A ||||||||
2929
| CPython 3.8 || N/A¹ ||||||||
3030
| CPython 3.9 ||||||||||
31+
| CPython 3.10² ||||||||||
3132
| PyPy 3.7 v7.3 || N/A || N/A |||| N/A | N/A |
3233

3334
<sup>¹ CPython 3.8's final binary release has experimental Universal2 support, but does not support macOS 10.x, so this is not currently available.</sup><br>
35+
<sup>² Available as a prerelease under a [flag](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons)</sup><br>
3436

3537
- Builds manylinux, macOS 10.9+, and Windows wheels for CPython and PyPy
3638
- Works on GitHub Actions, Azure Pipelines, Travis CI, AppVeyor, CircleCI, and GitLab CI
@@ -108,6 +110,7 @@ Options
108110
| | [`CIBW_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) <br> [`CIBW_SKIP`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip) | Choose the Python versions to build |
109111
| | [`CIBW_ARCHS`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Change the architectures built on your machine by default |
110112
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.readthedocs.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
113+
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python |
111114
| **Build customization** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
112115
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
113116
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |

bin/update_pythons.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import difflib
77
import logging
88
from pathlib import Path
9-
from typing import Any, Union
9+
from typing import Any, Iterable, Union, cast
1010

1111
import click
1212
import requests
@@ -79,23 +79,26 @@ def __init__(self, arch_str: ArchStr) -> None:
7979
response.raise_for_status()
8080
cp_info = response.json()
8181

82-
versions = (Version(v) for v in cp_info["versions"])
83-
self.versions = sorted(v for v in versions if not v.is_devrelease)
82+
self.version_dict = {Version(v): v for v in cp_info["versions"]}
8483

8584
def update_version_windows(self, spec: Specifier) -> ConfigWinCP | None:
86-
versions = sorted(v for v in self.versions if spec.contains(v))
87-
if not all(v.is_prerelease for v in versions):
88-
versions = [v for v in versions if not v.is_prerelease]
85+
86+
# Specifier.filter selects all non pre-releases that match the spec,
87+
# unless there are only pre-releases, then it selects pre-releases
88+
# instead (like pip)
89+
unsorted_versions = cast(Iterable[Version], spec.filter(self.version_dict))
90+
versions = sorted(unsorted_versions, reverse=True)
91+
8992
log.debug(f"Windows {self.arch} {spec} has {', '.join(str(v) for v in versions)}")
9093

9194
if not versions:
9295
return None
9396

94-
version = versions[-1]
97+
version = versions[0]
9598
identifier = f"cp{version.major}{version.minor}-{self.arch}"
9699
return ConfigWinCP(
97100
identifier=identifier,
98-
version=str(version),
101+
version=self.version_dict[version],
99102
arch=self.arch_str,
100103
)
101104

@@ -190,21 +193,23 @@ def __init__(self) -> None:
190193
# Removing the prefix, Python 3.9 would use: release["name"].removeprefix("Python ")
191194
version = Version(release["name"][7:])
192195

193-
if not version.is_prerelease and not version.is_devrelease:
194-
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
195-
self.versions_dict[version] = uri
196+
uri = int(release["resource_uri"].rstrip("/").split("/")[-1])
197+
self.versions_dict[version] = uri
196198

197199
def update_version_macos(
198200
self, identifier: str, version: Version, spec: Specifier
199201
) -> ConfigMacOS | None:
200-
sorted_versions = sorted(v for v in self.versions_dict if spec.contains(v))
202+
203+
# see note above on Specifier.filter
204+
unsorted_versions = cast(Iterable[Version], spec.filter(self.versions_dict))
205+
sorted_versions = sorted(unsorted_versions, reverse=True)
201206

202207
if version <= Version("3.8.9999"):
203208
file_ident = "macosx10.9.pkg"
204209
else:
205210
file_ident = "macos11.pkg"
206211

207-
for new_version in reversed(sorted_versions):
212+
for new_version in sorted_versions:
208213
# Find the first patch version that contains the requested file
209214
uri = self.versions_dict[new_version]
210215
response = requests.get(
@@ -270,7 +275,7 @@ def update_config(self, config: dict[str, str]) -> None:
270275
@click.command()
271276
@click.option("--force", is_flag=True)
272277
@click.option(
273-
"--level", default="INFO", type=click.Choice(["INFO", "DEBUG", "TRACE"], case_sensitive=False)
278+
"--level", default="INFO", type=click.Choice(["WARNING", "INFO", "DEBUG"], case_sensitive=False)
274279
)
275280
def update_pythons(force: bool, level: str) -> None:
276281

cibuildwheel/__main__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ def main() -> None:
132132
help="Do not report an error code if the build does not match any wheels.",
133133
)
134134

135+
parser.add_argument(
136+
"--prerelease-pythons",
137+
action="store_true",
138+
help="Enable pre-release Python versions if available.",
139+
)
140+
135141
args = parser.parse_args()
136142

137143
detect_obsolete_options()
@@ -206,6 +212,9 @@ def main() -> None:
206212
build_verbosity_str = get_option_from_environment(
207213
"CIBW_BUILD_VERBOSITY", platform=platform, default=""
208214
)
215+
prerelease_pythons = args.prerelease_pythons or cibuildwheel.util.strtobool(
216+
os.environ.get("CIBW_PRERELEASE_PYTHONS", "0")
217+
)
209218

210219
package_files = {"setup.py", "setup.cfg", "pyproject.toml"}
211220

@@ -222,8 +231,12 @@ def main() -> None:
222231
) or get_requires_python_str(package_dir)
223232
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
224233

234+
# Hardcode pre-releases here, current: Python 3.10
225235
build_selector = BuildSelector(
226-
build_config=build_config, skip_config=skip_config, requires_python=requires_python
236+
build_config=build_config,
237+
skip_config=skip_config,
238+
requires_python=requires_python,
239+
prerelease_pythons=prerelease_pythons,
227240
)
228241
test_selector = TestSelector(skip_config=test_skip)
229242

cibuildwheel/logger.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def build_description_from_identifier(identifier: str) -> str:
186186
build_description = ""
187187

188188
python_interpreter = python_identifier[0:2]
189-
python_version = python_identifier[2:4]
189+
python_version = python_identifier[2:]
190190

191191
if python_interpreter == "cp":
192192
build_description += "CPython"
@@ -195,7 +195,7 @@ def build_description_from_identifier(identifier: str) -> str:
195195
else:
196196
raise Exception("unknown python")
197197

198-
build_description += f" {python_version[0]}.{python_version[1]} "
198+
build_description += f" {python_version[0]}.{python_version[1:]} "
199199

200200
try:
201201
build_description += PLATFORM_IDENTIFIER_DESCIPTIONS[platform_identifier]

cibuildwheel/macos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ def setup_python(
270270
config_is_arm64 = python_configuration.identifier.endswith("arm64")
271271
config_is_universal2 = python_configuration.identifier.endswith("universal2")
272272

273-
if python_configuration.version == "3.9":
273+
if python_configuration.version not in {"3.6", "3.7", "3.8"}:
274274
if python_configuration.identifier.endswith("x86_64"):
275275
# even on the macos11.0 Python installer, on the x86_64 side it's
276276
# compatible back to 10.9.

cibuildwheel/resources/build-platforms.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@ python_configurations = [
44
{ identifier = "cp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
55
{ identifier = "cp38-manylinux_x86_64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
66
{ identifier = "cp39-manylinux_x86_64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
7+
{ identifier = "cp310-manylinux_x86_64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
78
{ identifier = "cp36-manylinux_i686", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
89
{ identifier = "cp37-manylinux_i686", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
910
{ identifier = "cp38-manylinux_i686", version = "3.8", path_str = "/opt/python/cp38-cp38" },
1011
{ identifier = "cp39-manylinux_i686", version = "3.9", path_str = "/opt/python/cp39-cp39" },
12+
{ identifier = "cp310-manylinux_i686", version = "3.10", path_str = "/opt/python/cp310-cp310" },
1113
{ identifier = "pp37-manylinux_x86_64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
1214
{ identifier = "cp36-manylinux_aarch64", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
1315
{ identifier = "cp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
1416
{ identifier = "cp38-manylinux_aarch64", version = "3.8", path_str = "/opt/python/cp38-cp38" },
1517
{ identifier = "cp39-manylinux_aarch64", version = "3.9", path_str = "/opt/python/cp39-cp39" },
18+
{ identifier = "cp310-manylinux_aarch64", version = "3.10", path_str = "/opt/python/cp310-cp310" },
1619
{ identifier = "cp36-manylinux_ppc64le", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
1720
{ identifier = "cp37-manylinux_ppc64le", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
1821
{ identifier = "cp38-manylinux_ppc64le", version = "3.8", path_str = "/opt/python/cp38-cp38" },
1922
{ identifier = "cp39-manylinux_ppc64le", version = "3.9", path_str = "/opt/python/cp39-cp39" },
23+
{ identifier = "cp310-manylinux_ppc64le", version = "3.10", path_str = "/opt/python/cp310-cp310" },
2024
{ identifier = "cp36-manylinux_s390x", version = "3.6", path_str = "/opt/python/cp36-cp36m" },
2125
{ identifier = "cp37-manylinux_s390x", version = "3.7", path_str = "/opt/python/cp37-cp37m" },
2226
{ identifier = "cp38-manylinux_s390x", version = "3.8", path_str = "/opt/python/cp38-cp38" },
2327
{ identifier = "cp39-manylinux_s390x", version = "3.9", path_str = "/opt/python/cp39-cp39" },
28+
{ identifier = "cp310-manylinux_s390x", version = "3.10", path_str = "/opt/python/cp310-cp310" },
2429
{ identifier = "pp37-manylinux_aarch64", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
2530
{ identifier = "pp37-manylinux_i686", version = "3.7", path_str = "/opt/python/pp37-pypy37_pp73" },
2631
]
@@ -33,6 +38,9 @@ python_configurations = [
3338
{ identifier = "cp39-macosx_x86_64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.5/python-3.9.5-macos11.pkg" },
3439
{ identifier = "cp39-macosx_arm64", version = "3.9", url = "https://www.python.org/ftp/python/3.9.5/python-3.9.5-macos11.pkg" },
3540
{ identifier = "cp39-macosx_universal2", version = "3.9", url = "https://www.python.org/ftp/python/3.9.5/python-3.9.5-macos11.pkg" },
41+
{ identifier = "cp310-macosx_x86_64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.0/python-3.10.0b1-macos11.pkg" },
42+
{ identifier = "cp310-macosx_arm64", version = "3.10", url = "https://www.python.org/ftp/python/3.10.0/python-3.10.0b1-macos11.pkg" },
43+
{ identifier = "cp310-macosx_universal2", version = "3.10", url = "https://www.python.org/ftp/python/3.10.0/python-3.10.0b1-macos11.pkg" },
3644
{ identifier = "pp37-macosx_x86_64", version = "3.7", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-osx64.tar.bz2" },
3745
]
3846

@@ -46,5 +54,7 @@ python_configurations = [
4654
{ identifier = "cp38-win_amd64", version = "3.8.10", arch = "64" },
4755
{ identifier = "cp39-win32", version = "3.9.5", arch = "32" },
4856
{ identifier = "cp39-win_amd64", version = "3.9.5", arch = "64" },
57+
{ identifier = "cp310-win32", version = "3.10.0-b1", arch = "32" },
58+
{ identifier = "cp310-win_amd64", version = "3.10.0-b1", arch = "64" },
4959
{ identifier = "pp37-win_amd64", version = "3.7", arch = "64", url = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-win64.zip" },
5060
]

cibuildwheel/util.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,25 @@ class IdentifierSelector:
5858
This class holds a set of build/skip patterns. You call an instance with a
5959
build identifier, and it returns True if that identifier should be
6060
included. Only call this on valid identifiers, ones that have at least 2
61-
numeric digits before the first dash.
61+
numeric digits before the first dash. If a pre-release version X.Y is present,
62+
you can filter it with prerelease="XY".
6263
"""
6364

65+
# a pattern that skips prerelease versions, when include_prereleases is False.
66+
PRERELEASE_SKIP = "cp310-*"
67+
6468
def __init__(
65-
self, *, build_config: str, skip_config: str, requires_python: Optional[SpecifierSet] = None
69+
self,
70+
*,
71+
build_config: str,
72+
skip_config: str,
73+
requires_python: Optional[SpecifierSet] = None,
74+
prerelease_pythons: bool = False,
6675
):
6776
self.build_patterns = build_config.split()
6877
self.skip_patterns = skip_config.split()
6978
self.requires_python = requires_python
79+
self.prerelease_pythons = prerelease_pythons
7080

7181
def __call__(self, build_id: str) -> bool:
7282
# Filter build selectors by python_requires if set
@@ -81,17 +91,33 @@ def __call__(self, build_id: str) -> bool:
8191
build_patterns = itertools.chain.from_iterable(
8292
bracex.expand(p) for p in self.build_patterns
8393
)
84-
skip_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.skip_patterns)
94+
95+
unexpanded_skip_patterns = self.skip_patterns.copy()
96+
97+
if not self.prerelease_pythons:
98+
# filter out the prerelease pythons, alongside the user-defined
99+
# skip patterns
100+
unexpanded_skip_patterns += BuildSelector.PRERELEASE_SKIP.split()
101+
102+
skip_patterns = itertools.chain.from_iterable(
103+
bracex.expand(p) for p in unexpanded_skip_patterns
104+
)
85105

86106
build: bool = any(fnmatch.fnmatch(build_id, pat) for pat in build_patterns)
87107
skip: bool = any(fnmatch.fnmatch(build_id, pat) for pat in skip_patterns)
88108
return build and not skip
89109

90110
def __repr__(self) -> str:
91-
if not self.skip_patterns:
92-
return f'{self.__class__.__name__}({" ".join(self.build_patterns)!r})'
93-
else:
94-
return f'{self.__class__.__name__}({" ".join(self.build_patterns)!r} - {" ".join(self.skip_patterns)!r})'
111+
result = f'{self.__class__.__name__}(build_config={" ".join(self.build_patterns)!r}'
112+
113+
if self.skip_patterns:
114+
result += f', skip_config={" ".join(self.skip_patterns)!r}'
115+
if self.prerelease_pythons:
116+
result += ", prerelease_pythons=True"
117+
118+
result += ")"
119+
120+
return result
95121

96122

97123
class BuildSelector(IdentifierSelector):

docs/options.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,32 @@ the package is compatible with all versions of Python that it can build.
299299
CIBW_PROJECT_REQUIRES_PYTHON: ">=3.6"
300300
```
301301

302+
### `CIBW_PRERELEASE_PYTHONS` {: #prerelease-pythons}
303+
> Enable building with pre-release versions of Python
304+
305+
During the beta period, when new versions of Python are being tested,
306+
cibuildwheel will often gain early support for beta releases. If you would
307+
like to test wheel building with these versions, you can enable this flag.
308+
309+
!!! caution
310+
This option is provided for testing purposes only. It is not
311+
recommended to distribute wheels built when `CIBW_PRERELEASE_PYTHONS` is
312+
set, such as uploading to PyPI. Please _do not_ upload these wheels to
313+
PyPI, as they are not guaranteed to work with the final Python release.
314+
Once Python is ABI stable and enters the release candidate phase, that
315+
version of Python will become available without this flag.
316+
317+
Default: Off (0) if Python is available in beta phase. No effect otherwise.
318+
319+
This option can also be set using the [command-line option](#command-line) `--prerelease-pythons`.
320+
321+
#### Examples
322+
323+
```yaml
324+
# Include latest Python beta
325+
CIBW_PRERELEASE_PYTHONS: True
326+
```
327+
302328
## Build customization
303329

304330
### `CIBW_ENVIRONMENT` {: #environment}
@@ -715,6 +741,7 @@ CIBW_BUILD_VERBOSITY: 1
715741
usage: cibuildwheel [-h] [--platform {auto,linux,macos,windows}]
716742
[--archs ARCHS] [--output-dir OUTPUT_DIR]
717743
[--print-build-identifiers] [--allow-empty]
744+
[--prerelease-pythons]
718745
[package_dir]
719746
720747
Build wheels for all the platforms.
@@ -741,15 +768,16 @@ optional arguments:
741768
natively supported on this machine. Set this option to
742769
build an architecture via emulation, for example,
743770
using binfmt_misc and QEMU. Default: auto. Choices:
744-
auto, native, all, x86_64, i686, aarch64, ppc64le,
745-
s390x, x86, AMD64
771+
auto, auto64, auto32, native, all, x86_64, i686,
772+
aarch64, ppc64le, s390x, universal2, arm64, x86, AMD64
746773
--output-dir OUTPUT_DIR
747774
Destination folder for the wheels.
748775
--print-build-identifiers
749776
Print the build identifiers matched by the current
750777
invocation and exit.
751778
--allow-empty Do not report an error code if the build does not
752779
match any wheels.
780+
--prerelease-pythons Enable pre-release Python versions if available.
753781
```
754782

755783
<style>

test/test_0_basic.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,7 @@ def test_build_identifiers(tmp_path):
5353
# check that the number of expected wheels matches the number of build
5454
# identifiers
5555
expected_wheels = utils.expected_wheels("spam", "0.1.0")
56-
build_identifiers = utils.cibuildwheel_get_build_identifiers(project_dir)
56+
build_identifiers = utils.cibuildwheel_get_build_identifiers(
57+
project_dir, prerelease_pythons=True
58+
)
5759
assert len(expected_wheels) == len(build_identifiers)

test/test_before_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ def test(tmp_path):
4949
# checked in setup.py
5050
"CIBW_BEFORE_TEST": """python -c "import sys; open('/tmp/pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('/tmp/pythonprefix.txt', 'w').write(sys.prefix)" && python -m pip install {project}/dependency""",
5151
"CIBW_BEFORE_TEST_WINDOWS": """python -c "import sys; open('c:\\pythonversion.txt', 'w').write(sys.version)" && python -c "import sys; open('c:\\pythonprefix.txt', 'w').write(sys.prefix)" && python -m pip install {project}/dependency""",
52-
"CIBW_TEST_REQUIRES": "nose",
52+
"CIBW_TEST_REQUIRES": "pytest",
5353
# the 'false ||' bit is to ensure this command runs in a shell on
5454
# mac/linux.
55-
"CIBW_TEST_COMMAND": "false || nosetests {project}/test",
56-
"CIBW_TEST_COMMAND_WINDOWS": "nosetests {project}/test",
55+
"CIBW_TEST_COMMAND": "false || pytest {project}/test",
56+
"CIBW_TEST_COMMAND_WINDOWS": "pytest {project}/test",
5757
},
5858
)
5959

0 commit comments

Comments
 (0)