feat: requires-python (#536)
* feat: requires-python refactor: updating to current proposal fix: add tests and fix a few issues docs: Update mostly from @joerick docs: update README from readthedocs * refactor: pull out config file reading * fix: always assume highest Python patch version * refactor: try new design * refactor: function and bump MyPy to 0.800 * fix: tighten AST to call only, add docs * fix: address review points from @joerick
This commit is contained in:
@@ -105,3 +105,5 @@ env3?/
|
|||||||
|
|
||||||
# MyPy cache
|
# MyPy cache
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
all_known_setup.yaml
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ repos:
|
|||||||
args: ["--line-length=120"]
|
args: ["--line-length=120"]
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.790
|
rev: v0.800
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(cibuildwheel/|test/|bin/projects.py|bin/update_pythons.py|unit_test/)
|
exclude: ^(bin/|cibuildwheel/resources/).*py$
|
||||||
pass_filenames: false
|
additional_dependencies: [packaging]
|
||||||
additional_dependencies: [packaging, click]
|
- id: mypy
|
||||||
|
name: mypy 3.7+ on bin/
|
||||||
|
files: ^bin/.*py$
|
||||||
|
additional_dependencies: [packaging]
|
||||||
|
args: ["--python-version=3.7", "--ignore-missing-imports", "--scripts-are-modules"]
|
||||||
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.7.4
|
rev: v2.7.4
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ Options
|
|||||||
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
|
| **Build selection** | [`CIBW_PLATFORM`](https://cibuildwheel.readthedocs.io/en/stable/options/#platform) | Override the auto-detected target platform |
|
||||||
| | [`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 |
|
| | [`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 |
|
||||||
| | [`CIBW_ARCHS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Build non-native architectures |
|
| | [`CIBW_ARCHS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#archs) | Build non-native architectures |
|
||||||
|
| | [`CIBW_PROJECT_REQUIRES_PYTHON`](https://cibuildwheel.readthedocs.io/en/stable/options/#requires-python) | Manually set the Python compatibility of your project |
|
||||||
| **Build customization** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
| **Build customization** | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
||||||
| | [`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. |
|
| | [`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. |
|
||||||
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
||||||
|
|||||||
Executable
+110
@@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Iterator, Optional
|
||||||
|
|
||||||
|
import click
|
||||||
|
import yaml
|
||||||
|
from ghapi.core import GhApi, HTTP404NotFoundError # type: ignore
|
||||||
|
from rich import print
|
||||||
|
|
||||||
|
from cibuildwheel.projectfiles import Analyzer
|
||||||
|
|
||||||
|
DIR = Path(__file__).parent.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def parse(contents: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
tree = ast.parse(contents)
|
||||||
|
analyzer = Analyzer()
|
||||||
|
analyzer.visit(tree)
|
||||||
|
return analyzer.requires_python or ""
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_repo(name: str, contents: str) -> str:
|
||||||
|
s = f" {name}: "
|
||||||
|
if name == "setup.py":
|
||||||
|
if "python_requires" not in contents:
|
||||||
|
s += "❌"
|
||||||
|
res = parse(contents)
|
||||||
|
if res is None:
|
||||||
|
s += "⚠️ "
|
||||||
|
elif res:
|
||||||
|
s += "✅ " + res
|
||||||
|
elif "python_requires" in contents:
|
||||||
|
s += "☑️"
|
||||||
|
|
||||||
|
elif name == "setup.cfg":
|
||||||
|
s += "✅" if "python_requires" in contents else "❌"
|
||||||
|
else:
|
||||||
|
s += "✅" if "requires-python" in contents else "❌"
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
class MaybeRemote:
|
||||||
|
def __init__(self, cached_file: Path | str, *, online: bool) -> None:
|
||||||
|
self.online = online
|
||||||
|
if self.online:
|
||||||
|
self.contents: dict[str, dict[str, Optional[str]]] = {
|
||||||
|
"setup.py": {},
|
||||||
|
"setup.cfg": {},
|
||||||
|
"pyproject.toml": {},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
with open(cached_file) as f:
|
||||||
|
self.contents = yaml.safe_load(f)
|
||||||
|
|
||||||
|
def get(self, repo: str, filename: str) -> Optional[str]:
|
||||||
|
if self.online:
|
||||||
|
try:
|
||||||
|
self.contents[filename][repo] = (
|
||||||
|
GhApi(*repo.split("/")).get_content(filename).decode()
|
||||||
|
)
|
||||||
|
except HTTP404NotFoundError:
|
||||||
|
self.contents[filename][repo] = None
|
||||||
|
return self.contents[filename][repo]
|
||||||
|
elif repo in self.contents[filename]:
|
||||||
|
return self.contents[filename][repo]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Trying to access {repo}:{filename} and not in cache, rebuild cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
def save(self, filename: Path | str) -> None:
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
yaml.safe_dump(self.contents, f, default_flow_style=False)
|
||||||
|
|
||||||
|
def on_each(self, repos: list[str]) -> Iterator[tuple[str, str, Optional[str]]]:
|
||||||
|
for repo in repos:
|
||||||
|
print(f"[bold]{repo}:")
|
||||||
|
for filename in sorted(self.contents, reverse=True):
|
||||||
|
yield repo, filename, self.get(repo, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option("--online", is_flag=True, help="Remember to set GITHUB_TOKEN")
|
||||||
|
def main(online: bool) -> None:
|
||||||
|
with open(DIR / "../docs/data/projects.yml") as f:
|
||||||
|
known = yaml.safe_load(f)
|
||||||
|
|
||||||
|
repos = [x["gh"] for x in known]
|
||||||
|
|
||||||
|
ghinfo = MaybeRemote("all_known_setup.yaml", online=online)
|
||||||
|
|
||||||
|
for _, filename, contents in ghinfo.on_each(repos):
|
||||||
|
if contents is None:
|
||||||
|
print(f"[red] {filename}: Not found")
|
||||||
|
else:
|
||||||
|
print(check_repo(filename, contents))
|
||||||
|
|
||||||
|
if online:
|
||||||
|
ghinfo.save("all_known_setup.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -7,12 +7,15 @@ from configparser import ConfigParser
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional, Set, Union, overload
|
from typing import Dict, List, Optional, Set, Union, overload
|
||||||
|
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
|
||||||
import cibuildwheel
|
import cibuildwheel
|
||||||
import cibuildwheel.linux
|
import cibuildwheel.linux
|
||||||
import cibuildwheel.macos
|
import cibuildwheel.macos
|
||||||
import cibuildwheel.windows
|
import cibuildwheel.windows
|
||||||
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||||
from cibuildwheel.environment import EnvironmentParseError, parse_environment
|
from cibuildwheel.environment import EnvironmentParseError, parse_environment
|
||||||
|
from cibuildwheel.projectfiles import get_requires_python_str
|
||||||
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
|
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
|
||||||
from cibuildwheel.util import (
|
from cibuildwheel.util import (
|
||||||
BuildOptions,
|
BuildOptions,
|
||||||
@@ -26,10 +29,10 @@ from cibuildwheel.util import (
|
|||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_option_from_environment(option_name: str, platform: Optional[str], default: str) -> str: ... # noqa: E704
|
def get_option_from_environment(option_name: str, *, platform: Optional[str] = None, default: str) -> str: ... # noqa: E704
|
||||||
@overload
|
@overload
|
||||||
def get_option_from_environment(option_name: str, platform: Optional[str] = None, default: None = None) -> Optional[str]: ... # noqa: E704 E302
|
def get_option_from_environment(option_name: str, *, platform: Optional[str] = None, default: None = None) -> Optional[str]: ... # noqa: E704 E302
|
||||||
def get_option_from_environment(option_name: str, platform: Optional[str] = None, default: Optional[str] = None) -> Optional[str]: # noqa: E302
|
def get_option_from_environment(option_name: str, *, platform: Optional[str] = None, default: Optional[str] = None) -> Optional[str]: # noqa: E302
|
||||||
'''
|
'''
|
||||||
Returns an option from the environment, optionally scoped by the platform.
|
Returns an option from the environment, optionally scoped by the platform.
|
||||||
|
|
||||||
@@ -156,7 +159,18 @@ def main() -> None:
|
|||||||
test_extras = get_option_from_environment('CIBW_TEST_EXTRAS', platform=platform, default='')
|
test_extras = get_option_from_environment('CIBW_TEST_EXTRAS', platform=platform, default='')
|
||||||
build_verbosity_str = get_option_from_environment('CIBW_BUILD_VERBOSITY', platform=platform, default='')
|
build_verbosity_str = get_option_from_environment('CIBW_BUILD_VERBOSITY', platform=platform, default='')
|
||||||
|
|
||||||
build_selector = BuildSelector(build_config=build_config, skip_config=skip_config)
|
package_files = {'setup.py', 'setup.cfg', 'pyproject.toml'}
|
||||||
|
|
||||||
|
if not any(package_dir.joinpath(name).exists() for name in package_files):
|
||||||
|
names = ', '.join(sorted(package_files, reverse=True))
|
||||||
|
print(f'cibuildwheel: Could not find any of {{{names}}} at root of package', file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Passing this in as an environment variable will override pyproject.toml, setup.cfg, or setup.py
|
||||||
|
requires_python_str: Optional[str] = os.environ.get('CIBW_PROJECT_REQUIRES_PYTHON') or get_requires_python_str(package_dir)
|
||||||
|
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
|
||||||
|
|
||||||
|
build_selector = BuildSelector(build_config=build_config, skip_config=skip_config, requires_python=requires_python)
|
||||||
test_selector = TestSelector(skip_config=test_skip)
|
test_selector = TestSelector(skip_config=test_skip)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -186,15 +200,11 @@ def main() -> None:
|
|||||||
# This needs to be passed on to the docker container in linux.py
|
# This needs to be passed on to the docker container in linux.py
|
||||||
os.environ['CIBUILDWHEEL'] = '1'
|
os.environ['CIBUILDWHEEL'] = '1'
|
||||||
|
|
||||||
if not any((package_dir / name).exists()
|
|
||||||
for name in ["setup.py", "setup.cfg", "pyproject.toml"]):
|
|
||||||
print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr)
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
if args.archs is not None:
|
if args.archs is not None:
|
||||||
archs_config_str = args.archs
|
archs_config_str = args.archs
|
||||||
else:
|
else:
|
||||||
archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto')
|
archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto')
|
||||||
|
|
||||||
archs = Architecture.parse_config(archs_config_str, platform=platform)
|
archs = Architecture.parse_config(archs_config_str, platform=platform)
|
||||||
|
|
||||||
identifiers = get_build_identifiers(platform, build_selector, archs)
|
identifiers = get_build_identifiers(platform, build_selector, archs)
|
||||||
@@ -334,6 +344,7 @@ def get_build_identifiers(
|
|||||||
python_configurations: Union[List[cibuildwheel.linux.PythonConfiguration],
|
python_configurations: Union[List[cibuildwheel.linux.PythonConfiguration],
|
||||||
List[cibuildwheel.windows.PythonConfiguration],
|
List[cibuildwheel.windows.PythonConfiguration],
|
||||||
List[cibuildwheel.macos.PythonConfiguration]]
|
List[cibuildwheel.macos.PythonConfiguration]]
|
||||||
|
|
||||||
if platform == 'linux':
|
if platform == 'linux':
|
||||||
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
|
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
|
||||||
elif platform == 'windows':
|
elif platform == 'windows':
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import ast
|
||||||
|
import sys
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import toml
|
||||||
|
|
||||||
|
if sys.version_info < (3, 8):
|
||||||
|
Constant = ast.Str
|
||||||
|
|
||||||
|
def get_constant(x: ast.Str) -> str:
|
||||||
|
return x.s
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
Constant = ast.Constant
|
||||||
|
|
||||||
|
def get_constant(x: ast.Constant) -> Any:
|
||||||
|
return x.value
|
||||||
|
|
||||||
|
|
||||||
|
class Analyzer(ast.NodeVisitor):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.requires_python: Optional[str] = None
|
||||||
|
|
||||||
|
def visit(self, content: ast.AST) -> None:
|
||||||
|
for node in ast.walk(content):
|
||||||
|
for child in ast.iter_child_nodes(node):
|
||||||
|
child.parent = node # type: ignore
|
||||||
|
super().visit(content)
|
||||||
|
|
||||||
|
def visit_keyword(self, node: ast.keyword) -> None:
|
||||||
|
self.generic_visit(node)
|
||||||
|
if node.arg == "python_requires":
|
||||||
|
# Must not be nested in an if or other structure
|
||||||
|
# This will be Module -> Expr -> Call -> keyword
|
||||||
|
if (
|
||||||
|
not hasattr(node.parent.parent.parent, "parent") # type: ignore
|
||||||
|
and isinstance(node.value, Constant)
|
||||||
|
):
|
||||||
|
self.requires_python = get_constant(node.value)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_py_python_requires(content: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
tree = ast.parse(content)
|
||||||
|
analyzer = Analyzer()
|
||||||
|
analyzer.visit(tree)
|
||||||
|
return analyzer.requires_python or None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_requires_python_str(package_dir: Path) -> Optional[str]:
|
||||||
|
"Return the python requires string from the most canonical source available, or None"
|
||||||
|
|
||||||
|
# Read in from pyproject.toml:project.requires-python
|
||||||
|
try:
|
||||||
|
info = toml.load(package_dir / 'pyproject.toml')
|
||||||
|
return str(info['project']['requires-python'])
|
||||||
|
except (FileNotFoundError, KeyError, IndexError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Read in from setup.cfg:options.python_requires
|
||||||
|
try:
|
||||||
|
config = ConfigParser()
|
||||||
|
config.read(package_dir / 'setup.cfg')
|
||||||
|
return str(config['options']['python_requires'])
|
||||||
|
except (FileNotFoundError, KeyError, IndexError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(package_dir / 'setup.py') as f:
|
||||||
|
return setup_py_python_requires(f.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
+19
-2
@@ -13,12 +13,15 @@ from typing import Dict, List, NamedTuple, Optional, Set
|
|||||||
import bracex
|
import bracex
|
||||||
import certifi
|
import certifi
|
||||||
import toml
|
import toml
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
from packaging.version import Version
|
||||||
|
|
||||||
from .architecture import Architecture
|
from .architecture import Architecture
|
||||||
from .environment import ParsedEnvironment
|
from .environment import ParsedEnvironment
|
||||||
from .typing import PathOrStr, PlatformName
|
from .typing import PathOrStr, PlatformName
|
||||||
|
|
||||||
resources_dir = Path(__file__).parent / 'resources'
|
resources_dir = Path(__file__).parent / 'resources'
|
||||||
|
|
||||||
get_pip_script = resources_dir / 'get-pip.py'
|
get_pip_script = resources_dir / 'get-pip.py'
|
||||||
install_certifi_script = resources_dir / "install_certifi.py"
|
install_certifi_script = resources_dir / "install_certifi.py"
|
||||||
|
|
||||||
@@ -53,13 +56,25 @@ class IdentifierSelector:
|
|||||||
"""
|
"""
|
||||||
This class holds a set of build/skip patterns. You call an instance with a
|
This class holds a set of build/skip patterns. You call an instance with a
|
||||||
build identifier, and it returns True if that identifier should be
|
build identifier, and it returns True if that identifier should be
|
||||||
included.
|
included. Only call this on valid identifiers, ones that have at least 2
|
||||||
|
numeric digits before the first dash.
|
||||||
"""
|
"""
|
||||||
def __init__(self, *, build_config: str, skip_config: str):
|
|
||||||
|
def __init__(self, *, build_config: str, skip_config: str, requires_python: Optional[SpecifierSet] = None):
|
||||||
self.build_patterns = build_config.split()
|
self.build_patterns = build_config.split()
|
||||||
self.skip_patterns = skip_config.split()
|
self.skip_patterns = skip_config.split()
|
||||||
|
self.requires_python = requires_python
|
||||||
|
|
||||||
def __call__(self, build_id: str) -> bool:
|
def __call__(self, build_id: str) -> bool:
|
||||||
|
# Filter build selectors by python_requires if set
|
||||||
|
if self.requires_python is not None:
|
||||||
|
py_ver_str = build_id.split('-')[0]
|
||||||
|
major = int(py_ver_str[2])
|
||||||
|
minor = int(py_ver_str[3:])
|
||||||
|
version = Version(f"{major}.{minor}.99")
|
||||||
|
if not self.requires_python.contains(version):
|
||||||
|
return False
|
||||||
|
|
||||||
build_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.build_patterns)
|
build_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.build_patterns)
|
||||||
skip_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.skip_patterns)
|
skip_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.skip_patterns)
|
||||||
|
|
||||||
@@ -78,6 +93,8 @@ class BuildSelector(IdentifierSelector):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Note that requires-python is not needed for TestSelector, as you can't test
|
||||||
|
# what you can't build.
|
||||||
class TestSelector(IdentifierSelector):
|
class TestSelector(IdentifierSelector):
|
||||||
def __init__(self, *, skip_config: str):
|
def __init__(self, *, skip_config: str):
|
||||||
super().__init__(build_config="*", skip_config=skip_config)
|
super().__init__(build_config="*", skip_config=skip_config)
|
||||||
|
|||||||
@@ -208,6 +208,61 @@ Platform-specific variants also available:<br/>
|
|||||||
|
|
||||||
This option can also be set using the [command-line option](#command-line) `--archs`.
|
This option can also be set using the [command-line option](#command-line) `--archs`.
|
||||||
|
|
||||||
|
### `CIBW_PROJECT_REQUIRES_PYTHON` {: #requires-python}
|
||||||
|
> Manually set the Python compatibility of your project
|
||||||
|
|
||||||
|
By default, cibuildwheel reads your package's Python compatibility from
|
||||||
|
`pyproject.toml` following [PEP621](https://www.python.org/dev/peps/pep-0621/)
|
||||||
|
or from `setup.cfg`; finally it will try to inspect the AST of `setup.py` for a
|
||||||
|
simple keyword assignment in a top level function call. If you need to override
|
||||||
|
this behaviour for some reason, you can use this option.
|
||||||
|
|
||||||
|
When setting this option, the syntax is the same as `project.requires-python`,
|
||||||
|
using 'version specifiers' like `>=3.6`, according to
|
||||||
|
[PEP440](https://www.python.org/dev/peps/pep-0440/#version-specifiers).
|
||||||
|
|
||||||
|
Default: reads your package's Python compatibility from `pyproject.toml`
|
||||||
|
(`project.requires-python`) or `setup.cfg` (`options.python_requires`) or
|
||||||
|
`setup.py` `setup(python_requires="...")`. If not found, cibuildwheel assumes
|
||||||
|
the package is compatible with all versions of Python that it can build.
|
||||||
|
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Rather than using this option, it's recommended you set
|
||||||
|
`project.requires-python` in `pyproject.toml` instead:
|
||||||
|
Example `pyproject.toml`:
|
||||||
|
|
||||||
|
[project]
|
||||||
|
requires-python = ">=3.6"
|
||||||
|
|
||||||
|
# Aside - in pyproject.toml you should always specify minimal build
|
||||||
|
# system options, like this:
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=42", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
|
||||||
|
Currently, setuptools has not yet added support for reading this value from
|
||||||
|
pyproject.toml yet, and so does not copy it to Requires-Python in the wheel
|
||||||
|
metadata. This mechanism is used by `pip` to scan through older versions of
|
||||||
|
your package until it finds a release compatible with the curernt version
|
||||||
|
of Python compatible when installing, so it is an important value to set if
|
||||||
|
you plan to drop support for a version of Python in the future.
|
||||||
|
|
||||||
|
If you don't want to list this value twice, you can also use the setuptools
|
||||||
|
specific location in `setup.cfg` and cibuildwheel will detect it from
|
||||||
|
there. Example `setup.cfg`:
|
||||||
|
|
||||||
|
[options]
|
||||||
|
python_requires = ">=3.6"
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
CIBW_PROJECT_REQUIRES_PYTHON: ">=3.6"
|
||||||
|
```
|
||||||
|
|
||||||
## Build customization
|
## Build customization
|
||||||
|
|
||||||
### `CIBW_ENVIRONMENT` {: #environment}
|
### `CIBW_ENVIRONMENT` {: #environment}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ install_requires =
|
|||||||
toml
|
toml
|
||||||
certifi
|
certifi
|
||||||
bracex
|
bracex
|
||||||
|
packaging
|
||||||
typing_extensions; python_version < '3.8'
|
typing_extensions; python_version < '3.8'
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
@@ -42,17 +43,17 @@ dev =
|
|||||||
click
|
click
|
||||||
mkdocs-include-markdown-plugin==2.1.1
|
mkdocs-include-markdown-plugin==2.1.1
|
||||||
mkdocs==1.0.4
|
mkdocs==1.0.4
|
||||||
mypy
|
|
||||||
pip-tools
|
pip-tools
|
||||||
pygithub
|
pygithub
|
||||||
|
ghapi
|
||||||
pymdown-extensions
|
pymdown-extensions
|
||||||
pytest
|
pytest>=4
|
||||||
pytest-timeout
|
pytest-timeout
|
||||||
pyyaml
|
pyyaml
|
||||||
requests
|
requests
|
||||||
typing-extensions
|
typing-extensions
|
||||||
packaging>=20.8
|
|
||||||
rich>=9.6
|
rich>=9.6
|
||||||
|
mypy>=0.800
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
include =
|
include =
|
||||||
@@ -83,14 +84,14 @@ junit_family=xunit2
|
|||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.6
|
python_version = 3.6
|
||||||
files = cibuildwheel,test,unit_test,bin
|
files = cibuildwheel/*.py,test/**/*.py,unit_test/**/*.py
|
||||||
warn_unused_configs = True
|
warn_unused_configs = True
|
||||||
warn_redundant_casts = True
|
warn_redundant_casts = True
|
||||||
|
|
||||||
[mypy-test.*]
|
[mypy-test.*,mypy-unit_test.*]
|
||||||
check_untyped_defs = True
|
check_untyped_defs = True
|
||||||
|
|
||||||
[mypy-cibuildwheel.*,unit_test.*]
|
[mypy-cibuildwheel.*]
|
||||||
disallow_any_generics = True
|
disallow_any_generics = True
|
||||||
disallow_subclassing_any = True
|
disallow_subclassing_any = True
|
||||||
disallow_untyped_calls = True
|
disallow_untyped_calls = True
|
||||||
@@ -122,6 +123,9 @@ ignore_missing_imports = True
|
|||||||
[mypy-bracex.*]
|
[mypy-bracex.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-importlib_resources.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|
||||||
[tool:isort]
|
[tool:isort]
|
||||||
profile=black
|
profile=black
|
||||||
multi_line_output=3
|
multi_line_output=3
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
import pytest
|
|
||||||
import toml
|
import toml
|
||||||
|
from packaging.version import Version
|
||||||
from cibuildwheel.util import resources_dir
|
|
||||||
|
|
||||||
Version = pytest.importorskip("packaging.version").Version
|
|
||||||
|
|
||||||
from cibuildwheel.extra import InlineArrayDictEncoder # noqa: E402
|
from cibuildwheel.extra import InlineArrayDictEncoder # noqa: E402
|
||||||
|
from cibuildwheel.util import resources_dir
|
||||||
|
|
||||||
|
|
||||||
def test_compare_configs():
|
def test_compare_configs():
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
|
||||||
from cibuildwheel.util import BuildSelector
|
from cibuildwheel.util import BuildSelector
|
||||||
|
|
||||||
|
|
||||||
@@ -69,3 +71,36 @@ def test_build_braces():
|
|||||||
assert build_selector('cp37-manylinux1_x86_64')
|
assert build_selector('cp37-manylinux1_x86_64')
|
||||||
assert not build_selector('cp38-manylinux1_x86_64')
|
assert not build_selector('cp38-manylinux1_x86_64')
|
||||||
assert not build_selector('cp39-manylinux1_x86_64')
|
assert not build_selector('cp39-manylinux1_x86_64')
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_limited_python():
|
||||||
|
build_selector = BuildSelector(build_config="*", skip_config="", requires_python=SpecifierSet(">=3.6"))
|
||||||
|
|
||||||
|
assert not build_selector('cp27-manylinux1_x86_64')
|
||||||
|
assert build_selector('cp36-manylinux1_x86_64')
|
||||||
|
assert build_selector('cp37-manylinux1_x86_64')
|
||||||
|
assert not build_selector('cp27-manylinux1_i686')
|
||||||
|
assert build_selector('cp36-manylinux1_i686')
|
||||||
|
assert build_selector('cp37-manylinux1_i686')
|
||||||
|
assert not build_selector('cp27-win32')
|
||||||
|
assert build_selector('cp36-win32')
|
||||||
|
assert build_selector('cp37-win32')
|
||||||
|
assert not build_selector('pp27-win32')
|
||||||
|
assert build_selector('pp36-win32')
|
||||||
|
assert build_selector('pp37-win32')
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_limited_python_partial():
|
||||||
|
build_selector = BuildSelector(build_config="*", skip_config="", requires_python=SpecifierSet(">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"))
|
||||||
|
|
||||||
|
assert build_selector('cp27-manylinux1_x86_64')
|
||||||
|
assert not build_selector('cp35-manylinux1_x86_64')
|
||||||
|
assert build_selector('cp36-manylinux1_x86_64')
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_limited_python_patch():
|
||||||
|
build_selector = BuildSelector(build_config="*", skip_config="", requires_python=SpecifierSet(">=2.7.9"))
|
||||||
|
|
||||||
|
assert build_selector('cp27-manylinux1_x86_64')
|
||||||
|
assert build_selector('cp36-manylinux1_x86_64')
|
||||||
|
assert build_selector('cp37-manylinux1_x86_64')
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ def test_build_selector(platform, intercepted_build_args, monkeypatch, allow_emp
|
|||||||
|
|
||||||
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
assert isinstance(intercepted_build_selector, BuildSelector)
|
assert isinstance(intercepted_build_selector, BuildSelector)
|
||||||
assert intercepted_build_selector('build-this')
|
assert intercepted_build_selector('build24-this')
|
||||||
assert not intercepted_build_selector('skip-that')
|
assert not intercepted_build_selector('skip65-that')
|
||||||
# This unit test is just testing the options of 'main'
|
# This unit test is just testing the options of 'main'
|
||||||
# Unit tests for BuildSelector are in build_selector_test.py
|
# Unit tests for BuildSelector are in build_selector_test.py
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from conftest import MOCK_PACKAGE_DIR # noqa: I100
|
|
||||||
|
|
||||||
from cibuildwheel.__main__ import main
|
from cibuildwheel.__main__ import main
|
||||||
from cibuildwheel.util import Architecture
|
from cibuildwheel.architecture import Architecture
|
||||||
|
|
||||||
|
from .conftest import MOCK_PACKAGE_DIR
|
||||||
|
|
||||||
|
|
||||||
def test_unknown_platform_non_ci(monkeypatch, capsys):
|
def test_unknown_platform_non_ci(monkeypatch, capsys):
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from packaging.specifiers import SpecifierSet
|
||||||
|
|
||||||
|
from cibuildwheel.__main__ import main
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True, scope="function")
|
||||||
|
def fake_package_dir(monkeypatch, tmp_path):
|
||||||
|
'''
|
||||||
|
Set up a fake project
|
||||||
|
'''
|
||||||
|
|
||||||
|
local_path = tmp_path / "tmp_project"
|
||||||
|
local_path.mkdir()
|
||||||
|
|
||||||
|
local_path.joinpath("setup.py").touch()
|
||||||
|
|
||||||
|
monkeypatch.setattr(sys, 'argv', ['cibuildwheel', str(local_path)])
|
||||||
|
|
||||||
|
return local_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_override(platform, monkeypatch, intercepted_build_args):
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
|
|
||||||
|
assert intercepted_build_selector('cp39-win32')
|
||||||
|
assert intercepted_build_selector('cp36-win32')
|
||||||
|
|
||||||
|
assert intercepted_build_selector.requires_python is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_env(platform, monkeypatch, intercepted_build_args):
|
||||||
|
monkeypatch.setenv('CIBW_PROJECT_REQUIRES_PYTHON', '>=3.8')
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
|
|
||||||
|
assert intercepted_build_selector.requires_python == SpecifierSet(">=3.8")
|
||||||
|
|
||||||
|
assert intercepted_build_selector('cp39-win32')
|
||||||
|
assert not intercepted_build_selector('cp36-win32')
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_setup_cfg(platform, monkeypatch, intercepted_build_args, fake_package_dir):
|
||||||
|
|
||||||
|
fake_package_dir.joinpath("setup.cfg").write_text(textwrap.dedent("""
|
||||||
|
[options]
|
||||||
|
python_requires = >=3.8
|
||||||
|
"""))
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
|
|
||||||
|
assert intercepted_build_selector.requires_python == SpecifierSet(">=3.8")
|
||||||
|
|
||||||
|
assert intercepted_build_selector('cp39-win32')
|
||||||
|
assert not intercepted_build_selector('cp36-win32')
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_pyproject_toml(platform, monkeypatch, intercepted_build_args, fake_package_dir):
|
||||||
|
|
||||||
|
fake_package_dir.joinpath("pyproject.toml").write_text(textwrap.dedent("""
|
||||||
|
[project]
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
"""))
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
|
|
||||||
|
assert intercepted_build_selector.requires_python == SpecifierSet(">=3.8")
|
||||||
|
|
||||||
|
assert intercepted_build_selector('cp39-win32')
|
||||||
|
assert not intercepted_build_selector('cp36-win32')
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_setup_py_simple(platform, monkeypatch, intercepted_build_args, fake_package_dir):
|
||||||
|
|
||||||
|
fake_package_dir.joinpath("setup.py").write_text(textwrap.dedent("""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = "other",
|
||||||
|
python_requires = ">=3.7",
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
||||||
|
intercepted_build_selector = intercepted_build_args.args[0].build_selector
|
||||||
|
|
||||||
|
assert intercepted_build_selector.requires_python == SpecifierSet(">=3.7")
|
||||||
|
|
||||||
|
assert intercepted_build_selector('cp39-win32')
|
||||||
|
assert not intercepted_build_selector('cp36-win32')
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from cibuildwheel.projectfiles import get_requires_python_str, setup_py_python_requires
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_py_simple(tmp_path):
|
||||||
|
with open(tmp_path / "setup.py", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = "hello",
|
||||||
|
other = 23,
|
||||||
|
example = ["item", "other"],
|
||||||
|
python_requires = "1.23",
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) == "1.23"
|
||||||
|
assert get_requires_python_str(tmp_path) == "1.23"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_py_full(tmp_path):
|
||||||
|
with open(tmp_path / "setup.py", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.randomfunc()
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name = "hello",
|
||||||
|
other = 23,
|
||||||
|
example = ["item", "other"],
|
||||||
|
python_requires = "1.24",
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) == "1.24"
|
||||||
|
assert get_requires_python_str(tmp_path) == "1.24"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_py_assign(tmp_path):
|
||||||
|
with open(tmp_path / "setup.py", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
REQUIRES = "3.21"
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name = "hello",
|
||||||
|
other = 23,
|
||||||
|
example = ["item", "other"],
|
||||||
|
python_requires = REQUIRES,
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
|
||||||
|
assert get_requires_python_str(tmp_path) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_py_None(tmp_path):
|
||||||
|
with open(tmp_path / "setup.py", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
REQUIRES = None
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name = "hello",
|
||||||
|
other = 23,
|
||||||
|
example = ["item", "other"],
|
||||||
|
python_requires = None,
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
|
||||||
|
assert get_requires_python_str(tmp_path) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_py_empty(tmp_path):
|
||||||
|
with open(tmp_path / "setup.py", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
REQUIRES = "3.21"
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
name = "hello",
|
||||||
|
other = 23,
|
||||||
|
example = ["item", "other"],
|
||||||
|
)
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert setup_py_python_requires(tmp_path.joinpath("setup.py").read_text()) is None
|
||||||
|
assert get_requires_python_str(tmp_path) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_cfg(tmp_path):
|
||||||
|
with open(tmp_path / "setup.cfg", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
[options]
|
||||||
|
python_requires = 1.234
|
||||||
|
[metadata]
|
||||||
|
something = other
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert get_requires_python_str(tmp_path) == "1.234"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_setup_cfg_empty(tmp_path):
|
||||||
|
with open(tmp_path / "setup.cfg", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
[options]
|
||||||
|
other = 1.234
|
||||||
|
[metadata]
|
||||||
|
something = other
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert get_requires_python_str(tmp_path) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_pyproject_toml(tmp_path):
|
||||||
|
with open(tmp_path / "pyproject.toml", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
[project]
|
||||||
|
requires-python = "1.654"
|
||||||
|
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
something = "other"
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert get_requires_python_str(tmp_path) == "1.654"
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_pyproject_toml_empty(tmp_path):
|
||||||
|
with open(tmp_path / "pyproject.toml", "w") as f:
|
||||||
|
f.write(dedent("""
|
||||||
|
[project]
|
||||||
|
other = 1.234
|
||||||
|
"""))
|
||||||
|
|
||||||
|
assert get_requires_python_str(tmp_path) is None
|
||||||
Reference in New Issue
Block a user