feat: add inherit to override (#1730)

* feat: add inherit to override

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

* refactor: use dict and support prepend

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

* Refactor: simplifying by splitting the responsibilities of _dig_first

* Refactor to allow merging of string settings, and preserve table cascades

* docs: add some docs for inherit

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

* Apply suggestions from code review

Co-authored-by: Joe Rickerby <joerick@mac.com>

---------

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
Henry Schreiner
2024-03-02 16:58:18 -05:00
committed by GitHub
co-authored by Joe Rickerby
parent ae2451a199
commit 8f58f71c2e
7 changed files with 567 additions and 145 deletions
+31 -33
View File
@@ -17,6 +17,14 @@ $schema: http://json-schema.org/draft-07/schema
additionalProperties: false
description: cibuildwheel's settings.
type: object
defines:
inherit:
enum:
- none
- prepend
- append
default: none
description: How to inherit the parent's value.
properties:
archs:
description: Change the architectures built on your machine by default.
@@ -153,9 +161,6 @@ properties:
schema = yaml.safe_load(starter)
if args.schemastore:
schema["$id"] += "#"
string_array = yaml.safe_load(
"""
- type: string
@@ -214,19 +219,28 @@ items:
additionalProperties: false
properties:
select: {}
inherit:
type: object
additionalProperties: false
properties:
before-all: {"$ref": "#/defines/inherit"}
before-build: {"$ref": "#/defines/inherit"}
before-test: {"$ref": "#/defines/inherit"}
config-settings: {"$ref": "#/defines/inherit"}
container-engine: {"$ref": "#/defines/inherit"}
environment: {"$ref": "#/defines/inherit"}
environment-pass: {"$ref": "#/defines/inherit"}
repair-wheel-command: {"$ref": "#/defines/inherit"}
test-command: {"$ref": "#/defines/inherit"}
test-extras: {"$ref": "#/defines/inherit"}
test-requires: {"$ref": "#/defines/inherit"}
"""
)
for key, value in schema["properties"].items():
value["title"] = f'CIBW_{key.replace("-", "_").upper()}'
if args.schemastore:
non_global_options = {
k: {"$ref": f"#/properties/tool/properties/cibuildwheel/properties/{k}"}
for k in schema["properties"]
}
else:
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]}
del non_global_options["build"]
del non_global_options["skip"]
del non_global_options["container-engine"]
@@ -273,27 +287,11 @@ del oses["linux"]["properties"]["dependency-versions"]
schema["properties"]["overrides"] = overrides
schema["properties"] |= oses
if not args.schemastore:
print(json.dumps(schema, indent=2))
raise SystemExit(0)
if args.schemastore:
schema["$id"] = "https://json.schemastore.org/partial-cibuildwheel.json"
schema["$schema"] = "http://json-schema.org/draft-07/schema#"
schema[
"description"
] = "cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel."
schema_store_txt = """
$id: https://json.schemastore.org/cibuildwheel.json
$schema: http://json-schema.org/draft-07/schema#
additionalProperties: false
description: cibuildwheel's toml file, generated with ./bin/generate_schema.py --schemastore from cibuildwheel.
type: object
properties:
tool:
type: object
properties:
cibuildwheel:
type: object
"""
schema_store = yaml.safe_load(schema_store_txt)
schema_store["properties"]["tool"]["properties"]["cibuildwheel"]["properties"] = schema[
"properties"
]
print(json.dumps(schema_store, indent=2))
print(json.dumps(schema, indent=2))
+150 -67
View File
@@ -5,19 +5,20 @@ import configparser
import contextlib
import dataclasses
import difflib
import enum
import functools
import shlex
import sys
import textwrap
import traceback
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Set
from collections.abc import Callable, Generator, Iterable, Iterator, Set
from pathlib import Path
from typing import Any, Dict, List, Literal, TypedDict, Union
from typing import Any, Literal, Mapping, Sequence, TypedDict, Union # noqa: TID251
from packaging.specifiers import SpecifierSet
from ._compat import tomllib
from ._compat.typing import NotRequired
from ._compat.typing import NotRequired, assert_never
from .architecture import Architecture
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
from .logger import log
@@ -116,13 +117,14 @@ class BuildOptions:
return self.globals.architectures
Setting = Union[Dict[str, str], List[str], str, int]
Setting = Union[Mapping[str, str], Sequence[str], str, int]
@dataclasses.dataclass(frozen=True)
class Override:
select_pattern: str
options: dict[str, Setting]
inherit: dict[str, InheritRule]
MANYLINUX_OPTIONS = {f"manylinux-{build_platform}-image" for build_platform in MANYLINUX_ARCHS}
@@ -150,28 +152,115 @@ class ConfigOptionError(KeyError):
pass
def _dig_first(*pairs: tuple[Mapping[str, Setting], str], ignore_empty: bool = False) -> Setting:
"""
Return the first dict item that matches from pairs of dicts and keys.
Will throw a KeyError if missing.
class InheritRule(enum.Enum):
NONE = enum.auto()
APPEND = enum.auto()
PREPEND = enum.auto()
_dig_first((dict1, "key1"), (dict2, "key2"), ...)
def _resolve_cascade(
*pairs: tuple[Setting | None, InheritRule],
ignore_empty: bool = False,
list_sep: str | None = None,
table_format: TableFmt | None = None,
) -> str:
"""
Given a cascade of values with inherit rules, resolve them into a single
value.
'None' values mean that the option was not set at that level, and are
ignored. If `ignore_empty` is True, empty values are ignored too.
Values start with defaults, followed by more specific rules. If rules are
NONE, the last non-null value is returned. If a rule is APPEND or PREPEND,
the value is concatenated with the previous value.
The following idiom can be used to get the first matching value:
_resolve_cascade(("value1", Inherit.NONE), ("value2", Inherit.NONE), ...)))
"""
if not pairs:
msg = "pairs cannot be empty"
raise ValueError(msg)
for dict_like, key in pairs:
if key in dict_like:
value = dict_like[key]
result: str | None = None
if ignore_empty and value == "":
continue
if table_format is not None:
merge_sep = table_format["sep"]
elif list_sep is not None:
merge_sep = list_sep
else:
merge_sep = None
return value
for value, rule in pairs:
if value is None:
continue
last_key = pairs[-1][1]
raise KeyError(last_key)
if ignore_empty and not value:
continue
value_string = _stringify_setting(value, list_sep, table_format)
result = _merge_values(
result,
value_string,
rule=rule,
merge_sep=merge_sep,
)
if result is None:
msg = "a setting should at least have a default value"
raise ValueError(msg)
return result
def _merge_values(before: str | None, after: str, rule: InheritRule, merge_sep: str | None) -> str:
if rule == InheritRule.NONE:
return after
if not before:
# if before is None, we can just return after
# if before is an empty string, we shouldn't add any separator
return after
if not after:
# if after is an empty string, we shouldn't add any separator
return before
if not merge_sep:
msg = f"Don't know how to merge {before!r} and {after!r} with {rule}"
raise ConfigOptionError(msg)
if rule == InheritRule.APPEND:
return f"{before}{merge_sep}{after}"
elif rule == InheritRule.PREPEND:
return f"{after}{merge_sep}{before}"
else:
assert_never(rule)
def _stringify_setting(
setting: Setting, list_sep: str | None, table_format: TableFmt | None
) -> str:
if isinstance(setting, Mapping):
if table_format is None:
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a table"
raise ConfigOptionError(msg)
return table_format["sep"].join(
item for k, v in setting.items() for item in _inner_fmt(k, v, table_format)
)
if not isinstance(setting, str) and isinstance(setting, Sequence):
if list_sep is None:
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a list"
raise ConfigOptionError(msg)
return list_sep.join(setting)
if isinstance(setting, int):
return str(setting)
return setting
class OptionsReader:
@@ -244,7 +333,16 @@ class OptionsReader:
if isinstance(select, list):
select = " ".join(select)
self.overrides.append(Override(select, config_override))
inherit = config_override.pop("inherit", {})
if not isinstance(inherit, dict) or not all(
i in {"none", "append", "prepend"} for i in inherit.values()
):
msg = "'inherit' must be a dict containing only {'none', 'append', 'prepend'} values"
raise ConfigOptionError(msg)
inherit_enum = {k: InheritRule[v.upper()] for k, v in inherit.items()}
self.overrides.append(Override(select, config_override, inherit_enum))
def _validate_global_option(self, name: str) -> None:
"""
@@ -312,8 +410,8 @@ class OptionsReader:
name: str,
*,
env_plat: bool = True,
sep: str | None = None,
table: TableFmt | None = None,
list_sep: str | None = None,
table_format: TableFmt | None = None,
ignore_empty: bool = False,
) -> str:
"""
@@ -335,41 +433,24 @@ class OptionsReader:
envvar = f"CIBW_{name.upper().replace('-', '_')}"
plat_envvar = f"{envvar}_{self.platform.upper()}"
# later overrides take precedence over earlier ones, so reverse the list
active_config_overrides = reversed(self.active_config_overrides)
# get the option from the environment, then the config file, then finally the default.
# get the option from the default, then the config file, then finally the environment.
# platform-specific options are preferred, if they're allowed.
result = _dig_first(
(self.env if env_plat else {}, plat_envvar),
(self.env, envvar),
*[(o.options, name) for o in active_config_overrides],
(self.config_platform_options, name),
(self.config_options, name),
(self.default_platform_options, name),
(self.default_options, name),
return _resolve_cascade(
(self.default_options.get(name), InheritRule.NONE),
(self.default_platform_options.get(name), InheritRule.NONE),
(self.config_options.get(name), InheritRule.NONE),
(self.config_platform_options.get(name), InheritRule.NONE),
*[
(o.options.get(name), o.inherit.get(name, InheritRule.NONE))
for o in self.active_config_overrides
],
(self.env.get(envvar), InheritRule.NONE),
(self.env.get(plat_envvar) if env_plat else None, InheritRule.NONE),
ignore_empty=ignore_empty,
list_sep=list_sep,
table_format=table_format,
)
if isinstance(result, dict):
if table is None:
msg = f"{name!r} does not accept a table"
raise ConfigOptionError(msg)
return table["sep"].join(
item for k, v in result.items() for item in _inner_fmt(k, v, table)
)
if isinstance(result, list):
if sep is None:
msg = f"{name!r} does not accept a list"
raise ConfigOptionError(msg)
return sep.join(result)
if isinstance(result, int):
return str(result)
return result
def _inner_fmt(k: str, v: Any, table: TableFmt) -> Iterator[str]:
quote_function = table.get("quote", lambda a: a)
@@ -430,9 +511,9 @@ class Options:
package_dir = args.package_dir
output_dir = args.output_dir
build_config = self.reader.get("build", env_plat=False, sep=" ") or "*"
skip_config = self.reader.get("skip", env_plat=False, sep=" ")
test_skip = self.reader.get("test-skip", env_plat=False, sep=" ")
build_config = self.reader.get("build", env_plat=False, list_sep=" ") or "*"
skip_config = self.reader.get("skip", env_plat=False, list_sep=" ")
test_skip = self.reader.get("test-skip", env_plat=False, list_sep=" ")
prerelease_pythons = args.prerelease_pythons or strtobool(
self.env.get("CIBW_PRERELEASE_PYTHONS", "0")
@@ -445,7 +526,7 @@ class Options:
)
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
archs_config_str = args.archs or self.reader.get("archs", list_sep=" ")
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
# Process `--only`
@@ -464,7 +545,8 @@ class Options:
test_selector = TestSelector(skip_config=test_skip)
container_engine_str = self.reader.get(
"container-engine", table={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote}
"container-engine",
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
)
try:
@@ -489,29 +571,30 @@ class Options:
"""
with self.reader.identifier(identifier):
before_all = self.reader.get("before-all", sep=" && ")
before_all = self.reader.get("before-all", list_sep=" && ")
environment_config = self.reader.get(
"environment", table={"item": '{k}="{v}"', "sep": " "}
"environment", table_format={"item": '{k}="{v}"', "sep": " "}
)
environment_pass = self.reader.get("environment-pass", sep=" ").split()
before_build = self.reader.get("before-build", sep=" && ")
repair_command = self.reader.get("repair-wheel-command", sep=" && ")
environment_pass = self.reader.get("environment-pass", list_sep=" ").split()
before_build = self.reader.get("before-build", list_sep=" && ")
repair_command = self.reader.get("repair-wheel-command", list_sep=" && ")
config_settings = self.reader.get(
"config-settings", table={"item": "{k}={v}", "sep": " ", "quote": shlex.quote}
"config-settings",
table_format={"item": "{k}={v}", "sep": " ", "quote": shlex.quote},
)
dependency_versions = self.reader.get("dependency-versions")
test_command = self.reader.get("test-command", sep=" && ")
before_test = self.reader.get("before-test", sep=" && ")
test_requires = self.reader.get("test-requires", sep=" ").split()
test_extras = self.reader.get("test-extras", sep=",")
test_command = self.reader.get("test-command", list_sep=" && ")
before_test = self.reader.get("before-test", list_sep=" && ")
test_requires = self.reader.get("test-requires", list_sep=" ").split()
test_extras = self.reader.get("test-extras", list_sep=",")
build_verbosity_str = self.reader.get("build-verbosity")
build_frontend_str = self.reader.get(
"build-frontend",
env_plat=False,
table={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
)
build_frontend: BuildFrontendConfig | None
if not build_frontend_str or build_frontend_str == "default":
@@ -4,6 +4,17 @@
"additionalProperties": false,
"description": "cibuildwheel's settings.",
"type": "object",
"defines": {
"inherit": {
"enum": [
"none",
"prepend",
"append"
],
"default": "none",
"description": "How to inherit the parent's value."
}
},
"properties": {
"archs": {
"description": "Change the architectures built on your machine by default.",
@@ -423,6 +434,45 @@
}
]
},
"inherit": {
"type": "object",
"additionalProperties": false,
"properties": {
"before-all": {
"$ref": "#/defines/inherit"
},
"before-build": {
"$ref": "#/defines/inherit"
},
"before-test": {
"$ref": "#/defines/inherit"
},
"config-settings": {
"$ref": "#/defines/inherit"
},
"container-engine": {
"$ref": "#/defines/inherit"
},
"environment": {
"$ref": "#/defines/inherit"
},
"environment-pass": {
"$ref": "#/defines/inherit"
},
"repair-wheel-command": {
"$ref": "#/defines/inherit"
},
"test-command": {
"$ref": "#/defines/inherit"
},
"test-extras": {
"$ref": "#/defines/inherit"
},
"test-requires": {
"$ref": "#/defines/inherit"
}
}
},
"before-all": {
"$ref": "#/properties/before-all"
},
+67
View File
@@ -135,6 +135,10 @@ trigger new containers, one per image. Some commands are not supported;
`output-dir`, build/skip/test_skip selectors, and architectures cannot be
overridden.
You can specify a table of overrides in `inherit={}`, any list or table in this
list will inherit from previous overrides or the main configuration. The valid
options are `"none"` (the default), `"append"`, and `"prepend"`.
##### Examples:
```toml
@@ -169,6 +173,69 @@ This example will build CPython 3.6 wheels on manylinux1, CPython 3.7-3.9
wheels on manylinux2010, and manylinux2014 wheels for any newer Python
(like 3.10).
```toml
[tool.cibuildwheel]
environment = {FOO="BAR", "HAM"="EGGS"}
test-command = ["pyproject"]
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.test-command = "prepend"
test-command = ["pyproject-before"]
inherit.environment="append"
environment = {FOO="BAZ", "PYTHON"="MONTY"}
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.test-command = "append"
test-command = ["pyproject-after"]
```
This example will provide the command `"pyproject-before && pyproject && pyproject-after"`
on Python 3.11, and will have `environment = {FOO="BAZ", "PYTHON"="MONTY", "HAM"="EGGS"}`.
### Extending existing options {: #inherit }
In the TOML configuration, you can choose how tables and lists are inherited.
By default, all values are overridden completely (`"none"`) but sometimes you'd
rather `"append"` or `"prepend"` to an existing list or table. You can do this
with the `inherit` table in overrides. For example, if you want to add an environment
variable for CPython 3.11, without `inherit` you'd have to repeat all the
original environment variables in the override. With `inherit`, it's just:
```toml
[[tool.cibuildwheel.overrides]]
select = "cp311*"
inherit.environment = "append"
environment.NEWVAR = "Added!"
```
For a table, `"append"` will replace a key if it exists, while `"prepend"` will
only add a new key, older keys take precedence.
Lists are also supported (and keep in mind that commands are lists). For
example, you can print a message before and after a wheel is repaired:
```toml
[[tool.cibuildwheel.overrides]]
select = "*"
inherit.repair-wheel-command = "prepend"
repair-wheel-command = "echo 'Before repair'"
[[tool.cibuildwheel.overrides]]
select = "*"
inherit.repair-wheel-command = "append"
repair-wheel-command = "echo 'After repair'"
```
As seen in this example, you can have multiple overrides match - they match top
to bottom, with the config being accumulated. If you need platform-specific
inheritance, you can use `select = "*-????linux_*"` for Linux, `select =
"*-win_*"` for Windows, and `select = "*-macosx_*"` for macOS. As always,
environment variables will completely override any TOML configuration.
## Options summary
+68
View File
@@ -347,3 +347,71 @@ def test_build_frontend_option(tmp_path: Path, toml_assignment, result_name, res
assert parsed_build_frontend.args == result_args
else:
assert parsed_build_frontend is None
def test_override_inherit_environment(tmp_path: Path):
args = CommandLineArguments.defaults()
args.package_dir = tmp_path
pyproject_toml: Path = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
textwrap.dedent(
"""\
[tool.cibuildwheel]
environment = {FOO="BAR", "HAM"="EGGS"}
[[tool.cibuildwheel.overrides]]
select = "cp37*"
inherit.environment = "append"
environment = {FOO="BAZ", "PYTHON"="MONTY"}
"""
)
)
options = Options(platform="linux", command_line_arguments=args, env={})
parsed_environment = options.build_options(identifier=None).environment
assert parsed_environment.as_dictionary(prev_environment={}) == {
"FOO": "BAR",
"HAM": "EGGS",
}
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
prev_environment={}
) == {
"FOO": "BAZ",
"HAM": "EGGS",
"PYTHON": "MONTY",
}
def test_override_inherit_environment_with_references(tmp_path: Path):
args = CommandLineArguments.defaults()
args.package_dir = tmp_path
pyproject_toml: Path = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
textwrap.dedent(
"""\
[tool.cibuildwheel]
environment = {PATH="/opt/bin:$PATH"}
[[tool.cibuildwheel.overrides]]
select = "cp37*"
inherit.environment = "append"
environment = {PATH="/opt/local/bin:$PATH"}
"""
)
)
options = Options(platform="linux", command_line_arguments=args, env={"MONTY": "PYTHON"})
parsed_environment = options.build_options(identifier=None).environment
prev_environment = {"PATH": "/usr/bin:/bin"}
assert parsed_environment.as_dictionary(prev_environment=prev_environment) == {
"PATH": "/opt/bin:/usr/bin:/bin",
}
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
prev_environment=prev_environment
) == {
"PATH": "/opt/local/bin:/opt/bin:/usr/bin:/bin",
}
+157 -45
View File
@@ -4,7 +4,7 @@ from pathlib import Path
import pytest
from cibuildwheel.options import ConfigOptionError, OptionsReader, _dig_first
from cibuildwheel.options import ConfigOptionError, InheritRule, OptionsReader, _resolve_cascade
PYPROJECT_1 = """
[tool.cibuildwheel]
@@ -37,27 +37,31 @@ def test_simple_settings(tmp_path, platform, fname):
options_reader = OptionsReader(config_file_path, platform=platform, env={})
assert options_reader.get("build", env_plat=False, sep=" ") == "cp39*"
assert options_reader.get("build", env_plat=False, list_sep=" ") == "cp39*"
assert options_reader.get("test-command") == "pyproject"
assert options_reader.get("archs", sep=" ") == "auto"
assert options_reader.get("archs", list_sep=" ") == "auto"
assert (
options_reader.get("test-requires", sep=" ")
options_reader.get("test-requires", list_sep=" ")
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
)
# Also testing options for support for both lists and tables
assert (
options_reader.get("environment", table={"item": '{k}="{v}"', "sep": " "})
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
== 'THING="OTHER" FOO="BAR"'
)
assert (
options_reader.get("environment", sep="x", table={"item": '{k}="{v}"', "sep": " "})
options_reader.get(
"environment", list_sep="x", table_format={"item": '{k}="{v}"', "sep": " "}
)
== 'THING="OTHER" FOO="BAR"'
)
assert options_reader.get("test-extras", sep=",") == "one,two"
assert options_reader.get("test-extras", list_sep=",") == "one,two"
assert (
options_reader.get("test-extras", sep=",", table={"item": '{k}="{v}"', "sep": " "})
options_reader.get(
"test-extras", list_sep=",", table_format={"item": '{k}="{v}"', "sep": " "}
)
== "one,two"
)
@@ -65,10 +69,10 @@ def test_simple_settings(tmp_path, platform, fname):
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
with pytest.raises(ConfigOptionError):
options_reader.get("environment", sep=" ")
options_reader.get("environment", list_sep=" ")
with pytest.raises(ConfigOptionError):
options_reader.get("test-extras", table={"item": '{k}="{v}"', "sep": " "})
options_reader.get("test-extras", table_format={"item": '{k}="{v}"', "sep": " "})
def test_envvar_override(tmp_path, platform):
@@ -87,14 +91,14 @@ def test_envvar_override(tmp_path, platform):
},
)
assert options_reader.get("archs", sep=" ") == "auto"
assert options_reader.get("archs", list_sep=" ") == "auto"
assert options_reader.get("build", sep=" ") == "cp38*"
assert options_reader.get("build", list_sep=" ") == "cp38*"
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
assert (
options_reader.get("test-requires", sep=" ")
options_reader.get("test-requires", list_sep=" ")
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
)
assert options_reader.get("test-command") == "mytest"
@@ -215,7 +219,7 @@ build = ["1", "2"]
)
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
assert options_reader.get("build", sep=", ") == "1, 2"
assert options_reader.get("build", list_sep=", ") == "1, 2"
with pytest.raises(ConfigOptionError):
options_reader.get("build")
@@ -262,47 +266,92 @@ manylinux-x86_64-image = ""
assert options_reader.get("manylinux-aarch64-image", ignore_empty=True) == "manylinux1"
@pytest.mark.parametrize("ignore_empty", [True, False])
def test_dig_first(ignore_empty):
d1 = {"random": "thing"}
d2 = {"this": "that", "empty": ""}
d3 = {"other": "hi"}
d4 = {"this": "d4", "empty": "not"}
answer = _dig_first(
(d1, "empty"),
(d2, "empty"),
(d3, "empty"),
(d4, "empty"),
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
def test_resolve_cascade(ignore_empty):
answer = _resolve_cascade(
("not", InheritRule.NONE),
(None, InheritRule.NONE),
("", InheritRule.NONE),
(None, InheritRule.NONE),
ignore_empty=ignore_empty,
)
assert answer == ("not" if ignore_empty else "")
answer = _dig_first(
(d1, "this"),
(d2, "this"),
(d3, "this"),
(d4, "this"),
answer = _resolve_cascade(
("d4", InheritRule.NONE),
(None, InheritRule.NONE),
("that", InheritRule.NONE),
(None, InheritRule.NONE),
ignore_empty=ignore_empty,
)
assert answer == "that"
with pytest.raises(KeyError):
_dig_first(
(d1, "this"),
(d2, "other"),
(d3, "this"),
(d4, "other"),
with pytest.raises(ValueError, match="a setting should at least have a default value"):
_resolve_cascade(
(None, InheritRule.NONE),
(None, InheritRule.NONE),
(None, InheritRule.NONE),
(None, InheritRule.NONE),
ignore_empty=ignore_empty,
)
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
@pytest.mark.parametrize("rule", [InheritRule.PREPEND, InheritRule.NONE, InheritRule.APPEND])
def test_resolve_cascade_merge_list(ignore_empty, rule):
answer = _resolve_cascade(
(["a1", "a2"], InheritRule.NONE),
([], InheritRule.NONE),
(["b1", "b2"], rule),
(None, InheritRule.NONE),
ignore_empty=ignore_empty,
list_sep=" ",
)
if not ignore_empty:
assert answer == "b1 b2"
else:
if rule == InheritRule.PREPEND:
assert answer == "b1 b2 a1 a2"
elif rule == InheritRule.NONE:
assert answer == "b1 b2"
elif rule == InheritRule.APPEND:
assert answer == "a1 a2 b1 b2"
@pytest.mark.parametrize("rule", [InheritRule.PREPEND, InheritRule.NONE, InheritRule.APPEND])
def test_resolve_cascade_merge_dict(rule):
answer = _resolve_cascade(
({"value": "a1", "base": "b1"}, InheritRule.NONE),
(None, InheritRule.NONE),
({"value": "override"}, rule),
(None, InheritRule.NONE),
table_format={"item": "{k}={v}", "sep": " "},
)
if rule == InheritRule.PREPEND:
assert answer == "value=override value=a1 base=b1"
elif rule == InheritRule.NONE:
assert answer == "value=override"
elif rule == InheritRule.APPEND:
assert answer == "value=a1 base=b1 value=override"
def test_resolve_cascade_merge_different_types():
answer = _resolve_cascade(
({"value": "a1", "base": "b1"}, InheritRule.NONE),
({"value": "override"}, InheritRule.APPEND),
table_format={"item": "{k}={v}", "sep": " "},
)
assert answer == "value=a1 base=b1 value=override"
PYPROJECT_2 = """
[tool.cibuildwheel]
build = ["cp38*", "cp37*"]
environment = {FOO="BAR"}
environment = {FOO="BAR", "HAM"="EGGS"}
test-command = "pyproject"
test-command = ["pyproject"]
manylinux-x86_64-image = "manylinux1"
@@ -311,8 +360,25 @@ test-requires = "else"
[[tool.cibuildwheel.overrides]]
select = "cp37*"
test-command = "pyproject-override"
inherit = {test-command="prepend", environment="append"}
test-command = ["pyproject-override", "override2"]
manylinux-x86_64-image = "manylinux2014"
environment = {FOO="BAZ", "PYTHON"="MONTY"}
[[tool.cibuildwheel.overrides]]
select = "*-final"
inherit = {test-command="append"}
test-command = ["pyproject-finalize", "finalize2"]
[[tool.cibuildwheel.overrides]]
select = "*-final"
inherit = {test-command="append"}
test-command = ["extra-finalize"]
[[tool.cibuildwheel.overrides]]
select = "*-final"
inherit = {test-command="prepend"}
test-command = ["extra-prepend"]
"""
@@ -321,13 +387,30 @@ def test_pyproject_2(tmp_path, platform):
pyproject_toml.write_text(PYPROJECT_2)
options_reader = OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
assert options_reader.get("test-command") == "pyproject"
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
with options_reader.identifier("random"):
assert options_reader.get("test-command") == "pyproject"
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
with options_reader.identifier("cp37-something"):
assert options_reader.get("test-command") == "pyproject-override"
assert (
options_reader.get("test-command", list_sep=" && ")
== "pyproject-override && override2 && pyproject"
)
assert (
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
)
with options_reader.identifier("cp37-final"):
assert (
options_reader.get("test-command", list_sep=" && ")
== "extra-prepend && pyproject-override && override2 && pyproject && pyproject-finalize && finalize2 && extra-finalize"
)
assert (
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
)
def test_overrides_not_a_list(tmp_path, platform):
@@ -359,7 +442,7 @@ other = ["two", "three"]
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
assert (
options_reader.get("config-settings", table={"item": '{k}="{v}"', "sep": " "})
options_reader.get("config-settings", table_format={"item": '{k}="{v}"', "sep": " "})
== 'example="one" other="two" other="three"'
)
@@ -376,7 +459,36 @@ def test_pip_config_settings(tmp_path):
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
assert (
options_reader.get(
"config-settings", table={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
"config-settings", table_format={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
)
== "--config-settings='--build-option=\"--use-mypyc\"'"
)
def test_overrides_inherit(tmp_path):
pyproject_toml: Path = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
before-all = ["before-all"]
[[tool.cibuildwheel.overrides]]
select = "cp37*"
inherit.before-all = "append"
before-all = ["override1"]
[[tool.cibuildwheel.overrides]]
select = "cp37*"
inherit.before-all = "prepend"
before-all = ["override2"]
"""
)
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
with options_reader.identifier("cp38-something"):
assert options_reader.get("before-all", list_sep=" && ") == "before-all"
with options_reader.identifier("cp37-something"):
assert (
options_reader.get("before-all", list_sep=" && ")
== "override2 && before-all && override1"
)
+44
View File
@@ -72,6 +72,50 @@ def test_overrides_only_select():
validator(example)
def test_overrides_valid_inherit():
example = tomllib.loads(
"""
[[tool.cibuildwheel.overrides]]
inherit.repair-wheel-command = "append"
select = "somestring"
repair-wheel-command = ["something"]
"""
)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None
def test_overrides_invalid_inherit():
example = tomllib.loads(
"""
[[tool.cibuildwheel.overrides]]
inherit.something = "append"
select = "somestring"
repair-wheel-command = "something"
"""
)
validator = validate_pyproject.api.Validator()
with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example)
def test_overrides_invalid_inherit_value():
example = tomllib.loads(
"""
[[tool.cibuildwheel.overrides]]
inherit.repair-wheel-command = "nothing"
select = "somestring"
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