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/
|
||||
|
||||
all_known_setup.yaml
|
||||
|
||||
@@ -24,12 +24,16 @@ repos:
|
||||
args: ["--line-length=120"]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.790
|
||||
rev: v0.800
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(cibuildwheel/|test/|bin/projects.py|bin/update_pythons.py|unit_test/)
|
||||
pass_filenames: false
|
||||
additional_dependencies: [packaging, click]
|
||||
exclude: ^(bin/|cibuildwheel/resources/).*py$
|
||||
additional_dependencies: [packaging]
|
||||
- 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
|
||||
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 |
|
||||
| | [`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_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 |
|
||||
| | [`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 |
|
||||
|
||||
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 typing import Dict, List, Optional, Set, Union, overload
|
||||
|
||||
from packaging.specifiers import SpecifierSet
|
||||
|
||||
import cibuildwheel
|
||||
import cibuildwheel.linux
|
||||
import cibuildwheel.macos
|
||||
import cibuildwheel.windows
|
||||
from cibuildwheel.architecture import Architecture, allowed_architectures_check
|
||||
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.util import (
|
||||
BuildOptions,
|
||||
@@ -26,10 +29,10 @@ from cibuildwheel.util import (
|
||||
|
||||
|
||||
@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
|
||||
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: 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
|
||||
'''
|
||||
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='')
|
||||
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)
|
||||
|
||||
try:
|
||||
@@ -186,15 +200,11 @@ def main() -> None:
|
||||
# This needs to be passed on to the docker container in linux.py
|
||||
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:
|
||||
archs_config_str = args.archs
|
||||
else:
|
||||
archs_config_str = get_option_from_environment('CIBW_ARCHS', platform=platform, default='auto')
|
||||
|
||||
archs = Architecture.parse_config(archs_config_str, platform=platform)
|
||||
|
||||
identifiers = get_build_identifiers(platform, build_selector, archs)
|
||||
@@ -334,6 +344,7 @@ def get_build_identifiers(
|
||||
python_configurations: Union[List[cibuildwheel.linux.PythonConfiguration],
|
||||
List[cibuildwheel.windows.PythonConfiguration],
|
||||
List[cibuildwheel.macos.PythonConfiguration]]
|
||||
|
||||
if platform == 'linux':
|
||||
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
|
||||
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 certifi
|
||||
import toml
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version
|
||||
|
||||
from .architecture import Architecture
|
||||
from .environment import ParsedEnvironment
|
||||
from .typing import PathOrStr, PlatformName
|
||||
|
||||
resources_dir = Path(__file__).parent / 'resources'
|
||||
|
||||
get_pip_script = resources_dir / 'get-pip.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
|
||||
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.skip_patterns = skip_config.split()
|
||||
self.requires_python = requires_python
|
||||
|
||||
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)
|
||||
skip_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in self.skip_patterns)
|
||||
|
||||
@@ -78,6 +93,8 @@ class BuildSelector(IdentifierSelector):
|
||||
pass
|
||||
|
||||
|
||||
# Note that requires-python is not needed for TestSelector, as you can't test
|
||||
# what you can't build.
|
||||
class TestSelector(IdentifierSelector):
|
||||
def __init__(self, *, skip_config: str):
|
||||
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`.
|
||||
|
||||
### `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
|
||||
|
||||
### `CIBW_ENVIRONMENT` {: #environment}
|
||||
|
||||
@@ -32,6 +32,7 @@ install_requires =
|
||||
toml
|
||||
certifi
|
||||
bracex
|
||||
packaging
|
||||
typing_extensions; python_version < '3.8'
|
||||
|
||||
[options.package_data]
|
||||
@@ -42,17 +43,17 @@ dev =
|
||||
click
|
||||
mkdocs-include-markdown-plugin==2.1.1
|
||||
mkdocs==1.0.4
|
||||
mypy
|
||||
pip-tools
|
||||
pygithub
|
||||
ghapi
|
||||
pymdown-extensions
|
||||
pytest
|
||||
pytest>=4
|
||||
pytest-timeout
|
||||
pyyaml
|
||||
requests
|
||||
typing-extensions
|
||||
packaging>=20.8
|
||||
rich>=9.6
|
||||
mypy>=0.800
|
||||
|
||||
[options.packages.find]
|
||||
include =
|
||||
@@ -83,14 +84,14 @@ junit_family=xunit2
|
||||
|
||||
[mypy]
|
||||
python_version = 3.6
|
||||
files = cibuildwheel,test,unit_test,bin
|
||||
files = cibuildwheel/*.py,test/**/*.py,unit_test/**/*.py
|
||||
warn_unused_configs = True
|
||||
warn_redundant_casts = True
|
||||
|
||||
[mypy-test.*]
|
||||
[mypy-test.*,mypy-unit_test.*]
|
||||
check_untyped_defs = True
|
||||
|
||||
[mypy-cibuildwheel.*,unit_test.*]
|
||||
[mypy-cibuildwheel.*]
|
||||
disallow_any_generics = True
|
||||
disallow_subclassing_any = True
|
||||
disallow_untyped_calls = True
|
||||
@@ -122,6 +123,9 @@ ignore_missing_imports = True
|
||||
[mypy-bracex.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-importlib_resources.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[tool:isort]
|
||||
profile=black
|
||||
multi_line_output=3
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import pytest
|
||||
import toml
|
||||
|
||||
from cibuildwheel.util import resources_dir
|
||||
|
||||
Version = pytest.importorskip("packaging.version").Version
|
||||
from packaging.version import Version
|
||||
|
||||
from cibuildwheel.extra import InlineArrayDictEncoder # noqa: E402
|
||||
from cibuildwheel.util import resources_dir
|
||||
|
||||
|
||||
def test_compare_configs():
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from packaging.specifiers import SpecifierSet
|
||||
|
||||
from cibuildwheel.util import BuildSelector
|
||||
|
||||
|
||||
@@ -69,3 +71,36 @@ def test_build_braces():
|
||||
assert build_selector('cp37-manylinux1_x86_64')
|
||||
assert not build_selector('cp38-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
|
||||
assert isinstance(intercepted_build_selector, BuildSelector)
|
||||
assert intercepted_build_selector('build-this')
|
||||
assert not intercepted_build_selector('skip-that')
|
||||
assert intercepted_build_selector('build24-this')
|
||||
assert not intercepted_build_selector('skip65-that')
|
||||
# This unit test is just testing the options of 'main'
|
||||
# Unit tests for BuildSelector are in build_selector_test.py
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from conftest import MOCK_PACKAGE_DIR # noqa: I100
|
||||
|
||||
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):
|
||||
|
||||
@@ -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