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:
Henry Schreiner
2021-01-31 17:14:15 -05:00
committed by GitHub
parent b95dca50aa
commit eb700a164c
17 changed files with 591 additions and 30 deletions
+2
View File
@@ -105,3 +105,5 @@ env3?/
# MyPy cache # MyPy cache
.mypy_cache/ .mypy_cache/
all_known_setup.yaml
+8 -4
View File
@@ -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
+1
View File
@@ -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 |
+110
View File
@@ -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()
+20 -9
View File
@@ -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':
+79
View File
@@ -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
View File
@@ -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)
+55
View File
@@ -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}
+10 -6
View File
@@ -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
View File
+2 -5
View File
@@ -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():
+35
View File
@@ -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')
View File
+2 -2
View File
@@ -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
+3 -2
View File
@@ -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')
+142
View File
@@ -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