feat: add schema and validate-pyproject support (#1622)

* feat: add schema and validate-pyproject support

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: address review comments, fix bug, add tests

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: add test of examples in docs and fix

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* fix: nicer titles

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
This commit is contained in:
Henry Schreiner
2023-09-26 11:23:20 -05:00
committed by GitHub
parent 7da7df1efc
commit d4af0ac229
11 changed files with 1864 additions and 10 deletions
+3 -2
View File
@@ -35,9 +35,10 @@ repos:
hooks:
- id: mypy
name: mypy 3.8 on cibuildwheel/
exclude: ^cibuildwheel/resources/.*py$
exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
args: ["--python-version=3.8"]
additional_dependencies: &mypy-dependencies
- bracex
- nox
- packaging
- pygithub
@@ -49,7 +50,7 @@ repos:
- types-jinja2
- types-pyyaml
- types-requests
- bracex
- validate-pyproject
- id: mypy
name: mypy 3.11
exclude: ^cibuildwheel/resources/.*py$
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python
import copy
import json
from typing import Any
import yaml
starter = """
$id: https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/resources/cibuildwheel.schema.json
$schema: http://json-schema.org/draft-07/schema
additionalProperties: false
description: cibuildwheel's settings.
type: object
properties:
archs:
description: Change the architectures built on your machine by default.
type: string_array
before-all:
description: Execute a shell command on the build system before any wheels are built.
type: string_array
before-build:
description: Execute a shell command preparing each wheel's build.
type: string_array
before-test:
description: Execute a shell command before testing each wheel.
type: string_array
build:
default: ['*']
description: Choose the Python versions to build.
type: string_array
build-frontend:
default: default
description: Set the tool to use to build, either "pip" (default for now) or "build"
oneOf:
- enum: [pip, build, default]
- type: string
pattern: '^pip; ?args:'
- type: string
pattern: '^build; ?args:'
- type: object
additionalProperties: false
required: [name]
properties:
name:
enum: [pip, build]
args:
type: array
items:
type: string
build-verbosity:
type: integer
minimum: -3
maximum: 3
default: 0
description: Increase/decrease the output of pip wheel.
config-settings:
description: Specify config-settings for the build backend.
type: string_table_array
container-engine:
oneOf:
- enum: [docker, podman]
- type: string
pattern: '^docker; ?create_args:'
- type: string
pattern: '^podman; ?create_args:'
- type: object
additionalProperties: false
required: [name]
properties:
name:
enum: [docker, podman]
create-args:
type: array
items:
type: string
dependency-versions:
default: pinned
description: Specify how cibuildwheel controls the versions of the tools it uses
type: string
environment:
description: Set environment variables needed during the build.
type: string_table
environment-pass:
description: Set environment variables on the host to pass-through to the container
during the build.
type: string_array
manylinux-aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-i686-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-ppc64le-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-pypy_aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-pypy_i686-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-pypy_x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
manylinux-x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-aarch64-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-i686-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-ppc64le-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-s390x-image:
type: string
description: Specify alternative manylinux / musllinux container images
musllinux-x86_64-image:
type: string
description: Specify alternative manylinux / musllinux container images
repair-wheel-command:
type: string_array
description: Execute a shell command to repair each built wheel.
skip:
description: Choose the Python versions to skip.
type: string_array
test-command:
description: Execute a shell command to test each built wheel.
type: string_array
test-extras:
description: Install your wheel for testing using `extras_require`
type: string_array
test-requires:
description: Install Python dependencies before running the tests
type: string_array
test-skip:
description: Skip running tests on some builds.
type: string_array
"""
schema = yaml.safe_load(starter)
string_array = yaml.safe_load(
"""
- type: string
- type: array
items:
type: string
"""
)
string_table_array = yaml.safe_load(
"""
- type: string
- type: object
additionalProperties: false
patternProperties:
.+:
oneOf:
- type: string
- type: array
items:
type: string
"""
)
string_table = yaml.safe_load(
"""
- type: string
- type: object
additionalProperties: false
patternProperties:
.+:
- type: string
"""
)
for value in schema["properties"].values():
match value:
case {"type": "string_array"}:
del value["type"]
value["oneOf"] = string_array
case {"type": "string_table"}:
del value["type"]
value["oneOf"] = string_table
case {"type": "string_table_array"}:
del value["type"]
value["oneOf"] = string_table_array
overrides = yaml.safe_load(
"""
type: array
description: An overrides array
items:
type: object
required: ["select"]
additionalProperties: false
properties:
select: {}
"""
)
non_global_options = copy.deepcopy(schema["properties"])
del non_global_options["build"]
del non_global_options["skip"]
del non_global_options["container-engine"]
del non_global_options["test-skip"]
overrides["items"]["properties"]["select"]["oneOf"] = string_array
overrides["items"]["properties"] |= non_global_options.copy()
del overrides["items"]["properties"]["archs"]
not_linux = non_global_options.copy()
del not_linux["environment-pass"]
for key in list(not_linux):
if "linux-" in key:
del not_linux[key]
def as_object(d: dict[str, Any]) -> dict[str, Any]:
return {
"type": "object",
"additionalProperties": False,
"properties": copy.deepcopy(d),
}
oses = {
"linux": as_object(non_global_options),
"windows": as_object(not_linux),
"macos": as_object(not_linux),
}
oses["linux"]["properties"]["repair-wheel-command"][
"default"
] = "auditwheel repair -w {dest_dir} {wheel}"
oses["macos"]["properties"]["repair-wheel-command"][
"default"
] = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
del oses["linux"]["properties"]["dependency-versions"]
schema["properties"]["overrides"] = overrides
schema["properties"] |= oses
for key, value in schema["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}'
for key, value in schema["properties"]["linux"]["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}_LINUX'
for key, value in schema["properties"]["macos"]["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}_MACOS'
for key, value in schema["properties"]["windows"]["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}_WINDOWS'
print(json.dumps(schema, indent=2))
+3 -3
View File
@@ -3,8 +3,8 @@ from __future__ import annotations
import sys
if sys.version_info >= (3, 11):
from tomllib import load
from tomllib import load, loads
else:
from tomli import load
from tomli import load, loads
__all__ = ("load",)
__all__ = ["load", "loads"]
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ config-settings = {}
dependency-versions = "pinned"
environment = {}
environment-pass = []
build-verbosity = ""
build-verbosity = 0
before-all = ""
before-build = ""
+15
View File
@@ -0,0 +1,15 @@
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
DIR = Path(__file__).parent.resolve()
def get_schema(tool_name: str = "cibuildwheel") -> dict[str, Any]:
"Get the stored complete schema for cibuildwheel settings."
assert tool_name == "cibuildwheel", "Only cibuildwheel is supported."
with DIR.joinpath("resources/cibuildwheel.schema.json").open(encoding="utf-8") as f:
return json.load(f) # type: ignore[no-any-return]
-4
View File
@@ -917,9 +917,7 @@ Platform-specific environment variables are also available:<br/>
CIBW_REPAIR_WHEEL_COMMAND: >
python scripts/repair_wheel.py -w {dest_dir} {wheel} &&
python scripts/check_repaired_wheel.py -w {dest_dir} {wheel}
```
```yaml
# Use abi3audit to catch issues with Limited API wheels
CIBW_REPAIR_WHEEL_COMMAND_LINUX: >
auditwheel repair -w {dest_dir} {wheel} &&
@@ -953,9 +951,7 @@ Platform-specific environment variables are also available:<br/>
'python scripts/repair_wheel.py -w {dest_dir} {wheel}',
'python scripts/check_repaired_wheel.py -w {dest_dir} {wheel}',
]
```
```toml
# Use abi3audit to catch issues with Limited API wheels
[tool.cibuildwheel.linux]
repair-wheel-command = [
+8
View File
@@ -113,6 +113,14 @@ def update_proj(session: nox.Session) -> None:
)
@nox.session(reuse_venv=True)
def generate_schema(session: nox.Session) -> None:
session.install("pyyaml")
output = session.run("python", "bin/generate_schema.py", silent=True)
assert isinstance(output, str)
DIR.joinpath("cibuildwheel/resources/cibuildwheel.schema.json").write_text(output)
@nox.session(python="3.9")
def docs(session: nox.Session) -> None:
"""
+2
View File
@@ -49,6 +49,8 @@ include =
[options.entry_points]
console_scripts =
cibuildwheel = cibuildwheel.__main__:main
validate_pyproject.tool_schema =
cibuildwheel = cibuildwheel.schema:get_schema
[options.package_data]
cibuildwheel = resources/*
+1
View File
@@ -17,6 +17,7 @@ extras = {
"pytest-xdist",
"build",
"tomli_w",
"validate-pyproject",
],
"bin": [
"click",
+103
View File
@@ -0,0 +1,103 @@
from __future__ import annotations
import re
from pathlib import Path
import pytest
import validate_pyproject.api
from cibuildwheel._compat import tomllib
DIR = Path(__file__).parent.resolve()
def test_validate_default_schema():
filepath = DIR.parent / "cibuildwheel/resources/defaults.toml"
with filepath.open("rb") as f:
example = tomllib.load(f)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None
def test_validate_bad_container_engine():
example = tomllib.loads(
"""
[tool.cibuildwheel.linux]
container-engine = "docker"
"""
)
validator = validate_pyproject.api.Validator()
with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example)
def test_overrides_select():
example = tomllib.loads(
"""
[[tool.cibuildwheel.overrides]]
select = "somestring"
repair-wheel-command = "something"
"""
)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None
def test_overrides_no_select():
example = tomllib.loads(
"""
[[tool.cibuildwheel.overrides]]
repair-wheel-command = "something"
"""
)
validator = validate_pyproject.api.Validator()
with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example)
def test_docs_examples():
"""
Parse out all the configuration examples, build valid TOML out of them, and
make sure they pass.
"""
expr = re.compile(
r"""
!!! tab examples "pyproject.toml"
\s*
\s*```toml
(.*?)```""",
re.MULTILINE | re.DOTALL,
)
txt = DIR.parent.joinpath("docs/options.md").read_text()
blocks: list[str] = []
for match in expr.finditer(txt):
lines = (line.strip() for line in match.group(1).strip().splitlines() if line.strip())
block: list[str] = []
header = ""
for line in lines:
if line.startswith(("[tool.cibuildwheel", "[[tool.cibuildwheel")):
header = line
elif line.startswith("#"):
if block:
blocks.append("\n".join([header, *block]))
block = []
elif " = " in line and any(x.startswith(line.partition(" = ")[0]) for x in block):
blocks.append("\n".join([header, *block]))
block = [line]
else:
block.append(line)
blocks.append("\n".join([header, *block]))
for example_txt in blocks:
print(example_txt)
print()
example = tomllib.loads(example_txt)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None