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:
@@ -35,9 +35,10 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: mypy 3.8 on cibuildwheel/
|
name: mypy 3.8 on cibuildwheel/
|
||||||
exclude: ^cibuildwheel/resources/.*py$
|
exclude: ^cibuildwheel/resources/.*py|bin/generate_schema.py$
|
||||||
args: ["--python-version=3.8"]
|
args: ["--python-version=3.8"]
|
||||||
additional_dependencies: &mypy-dependencies
|
additional_dependencies: &mypy-dependencies
|
||||||
|
- bracex
|
||||||
- nox
|
- nox
|
||||||
- packaging
|
- packaging
|
||||||
- pygithub
|
- pygithub
|
||||||
@@ -49,7 +50,7 @@ repos:
|
|||||||
- types-jinja2
|
- types-jinja2
|
||||||
- types-pyyaml
|
- types-pyyaml
|
||||||
- types-requests
|
- types-requests
|
||||||
- bracex
|
- validate-pyproject
|
||||||
- id: mypy
|
- id: mypy
|
||||||
name: mypy 3.11
|
name: mypy 3.11
|
||||||
exclude: ^cibuildwheel/resources/.*py$
|
exclude: ^cibuildwheel/resources/.*py$
|
||||||
|
|||||||
Executable
+263
@@ -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,8 +3,8 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info >= (3, 11):
|
if sys.version_info >= (3, 11):
|
||||||
from tomllib import load
|
from tomllib import load, loads
|
||||||
else:
|
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
@@ -9,7 +9,7 @@ config-settings = {}
|
|||||||
dependency-versions = "pinned"
|
dependency-versions = "pinned"
|
||||||
environment = {}
|
environment = {}
|
||||||
environment-pass = []
|
environment-pass = []
|
||||||
build-verbosity = ""
|
build-verbosity = 0
|
||||||
|
|
||||||
before-all = ""
|
before-all = ""
|
||||||
before-build = ""
|
before-build = ""
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -917,9 +917,7 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
CIBW_REPAIR_WHEEL_COMMAND: >
|
CIBW_REPAIR_WHEEL_COMMAND: >
|
||||||
python scripts/repair_wheel.py -w {dest_dir} {wheel} &&
|
python scripts/repair_wheel.py -w {dest_dir} {wheel} &&
|
||||||
python scripts/check_repaired_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
|
# Use abi3audit to catch issues with Limited API wheels
|
||||||
CIBW_REPAIR_WHEEL_COMMAND_LINUX: >
|
CIBW_REPAIR_WHEEL_COMMAND_LINUX: >
|
||||||
auditwheel repair -w {dest_dir} {wheel} &&
|
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/repair_wheel.py -w {dest_dir} {wheel}',
|
||||||
'python scripts/check_repaired_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
|
# Use abi3audit to catch issues with Limited API wheels
|
||||||
[tool.cibuildwheel.linux]
|
[tool.cibuildwheel.linux]
|
||||||
repair-wheel-command = [
|
repair-wheel-command = [
|
||||||
|
|||||||
@@ -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")
|
@nox.session(python="3.9")
|
||||||
def docs(session: nox.Session) -> None:
|
def docs(session: nox.Session) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ include =
|
|||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
cibuildwheel = cibuildwheel.__main__:main
|
cibuildwheel = cibuildwheel.__main__:main
|
||||||
|
validate_pyproject.tool_schema =
|
||||||
|
cibuildwheel = cibuildwheel.schema:get_schema
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
cibuildwheel = resources/*
|
cibuildwheel = resources/*
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ extras = {
|
|||||||
"pytest-xdist",
|
"pytest-xdist",
|
||||||
"build",
|
"build",
|
||||||
"tomli_w",
|
"tomli_w",
|
||||||
|
"validate-pyproject",
|
||||||
],
|
],
|
||||||
"bin": [
|
"bin": [
|
||||||
"click",
|
"click",
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user