Skip to content

Commit

Permalink
refactor: try new design
Browse files Browse the repository at this point in the history
  • Loading branch information
henryiii committed Jan 22, 2021
1 parent f3f3ac2 commit b1cec95
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 67 deletions.
19 changes: 7 additions & 12 deletions cibuildwheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import cibuildwheel.windows
from cibuildwheel.architecture import Architecture, allowed_architectures_check
from cibuildwheel.environment import EnvironmentParseError, parse_environment
from cibuildwheel.projectfile import NoProjectFileError, ProjectFile
from cibuildwheel.projectfiles import ProjectFiles
from cibuildwheel.typing import PLATFORMS, PlatformName, assert_never
from cibuildwheel.util import (
BuildOptions,
Expand Down Expand Up @@ -159,19 +159,16 @@ 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='')

try:
project_file = ProjectFile(package_dir)
except NoProjectFileError as err:
print("cibuildwheel:", *err.args, file=sys.stderr)
project_files = ProjectFiles(package_dir)

if not project_files.exists():
print('cibuildwheel: Could not find setup.py, setup.cfg or pyproject.toml at root of package', file=sys.stderr)
sys.exit(2)

# Passing this in as an environment variable will override pyproject.toml or setup.cfg
requires_python_str = get_option_from_environment('CIBW_PROJECT_REQUIRES_PYTHON', platform=platform)
# 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 project_files.get_requires_python_str()
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)

if requires_python is None:
requires_python = project_file.get_requires_python()

build_selector = BuildSelector(build_config=build_config, skip_config=skip_config, requires_python=requires_python)
test_selector = TestSelector(skip_config=test_skip)

Expand Down Expand Up @@ -347,8 +344,6 @@ def get_build_identifiers(
List[cibuildwheel.windows.PythonConfiguration],
List[cibuildwheel.macos.PythonConfiguration]]

python_configurations: List[Any] = []

if platform == 'linux':
python_configurations = cibuildwheel.linux.get_python_configurations(build_selector, architectures)
elif platform == 'windows':
Expand Down
50 changes: 0 additions & 50 deletions cibuildwheel/projectfile.py

This file was deleted.

107 changes: 107 additions & 0 deletions cibuildwheel/projectfiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import ast
from configparser import ConfigParser
from pathlib import Path
from typing import Any, Dict, Mapping, Optional

import toml


class Analyzer(ast.NodeVisitor):
def __init__(self) -> None:
self.requires_python: Optional[str] = None
self.constants: Dict[str, str] = {}

def visit_Assign(self, node: ast.Assign) -> None:
for target in node.targets:
if (
isinstance(target, ast.Name)
and isinstance(node.value, ast.Constant)
and isinstance(node.value.value, str)
):
self.constants[target.id] = node.value.value

def visit_keyword(self, node: ast.keyword) -> None:
self.generic_visit(node)
if node.arg == "python_requires":
if isinstance(node.value, ast.Constant):
self.requires_python = node.value.value
elif isinstance(node.value, ast.Name):
self.requires_python = self.constants.get(node.value.id)


def dig(d: Mapping[str, Any], *keys: str) -> Any:
"""
Access a nested dictionary, returns None if any access is empty. Equivalent
to #dig in Ruby.
"""

try:
for key in keys:
d = d[key]
return d
except (KeyError, IndexError, TypeError):
return None


class ProjectFiles:
def __init__(self, package_dir: Path) -> None:
self.package_dir = package_dir

def __repr__(self) -> str:
return f'{self.__class__.__name__}({self.package_dir!r})'

@property
def setup_py_path(self) -> Path:
return self.package_dir / 'setup.py'

@property
def setup_cfg_path(self) -> Path:
return self.package_dir / 'setup.cfg'

@property
def pyproject_toml_path(self) -> Path:
return self.package_dir / 'pyproject.toml'

# Can cache eventually if needed more than once
# or just leave stateless.

@property
def pyproject_toml(self) -> Mapping[str, Any]:
try:
return toml.load(self.pyproject_toml_path)
except FileNotFoundError:
return {}

@property
def setup_cfg(self) -> Mapping[str, Any]:
try:
config = ConfigParser()
config.read(self.setup_cfg_path)
return config
except FileNotFoundError:
return {}

def _setup_py_python_requires(self) -> Optional[str]:
try:
with open(self.setup_py_path) as f:
tree = ast.parse(f.read())

analyzer = Analyzer()
analyzer.visit(tree)

return analyzer.requires_python or None
except Exception:
return None

def exists(self) -> bool:
"Returns True if any project file exists"

return self.pyproject_toml_path.exists() or self.setup_cfg_path.exists() or self.setup_py_path.exists()

def get_requires_python_str(self) -> Optional[str]:
"Return the python requires string from the most cannonical source available, or None"
return (
dig(self.pyproject_toml, 'project', 'requires-python')
or dig(self.setup_cfg, 'options', 'python_requires')
or self._setup_py_python_requires()
)
10 changes: 5 additions & 5 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ CIBW_ARCHS_LINUX: "auto aarch64"


### `CIBW_PROJECT_REQUIRES_PYTHON` {: #requires-python}
> Limit the Python versions to build
> 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/)
Expand All @@ -213,7 +213,7 @@ Platform-specific variants also available:<br/>

Example `pyproject.toml`:

```toml
~~~toml
[project]
requires-python = ">=3.6"

Expand All @@ -223,15 +223,15 @@ Platform-specific variants also available:<br/>
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
```
~~~

If you don't want to use this yet, you can also use the currently
recommended location for setuptools in `setup.cfg`. Example `setup.cfg`:

```ini
~~~ini
[options]
python_requires = ">=3.6"
```
~~~

#### Examples

Expand Down
21 changes: 21 additions & 0 deletions unit_test/main_tests/main_requires_python_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,24 @@ def test_override_pyproject_toml(platform, monkeypatch, intercepted_build_args,

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')
Loading

0 comments on commit b1cec95

Please sign in to comment.