Compare commits

...
5 Commits
8 changed files with 515 additions and 158 deletions
+22 -22
View File
@@ -40,6 +40,28 @@ additionalProperties: false
description: cibuildwheel's settings. description: cibuildwheel's settings.
type: object type: object
properties: properties:
inherit:
type: object
additionalProperties: false
properties:
audit-command: {"$ref": "#/$defs/inherit"}
audit-requires: {"$ref": "#/$defs/inherit"}
before-all: {"$ref": "#/$defs/inherit"}
before-build: {"$ref": "#/$defs/inherit"}
xbuild-tools: {"$ref": "#/$defs/inherit"}
xbuild-files: {"$ref": "#/$defs/inherit"}
before-test: {"$ref": "#/$defs/inherit"}
config-settings: {"$ref": "#/$defs/inherit"}
container-engine: {"$ref": "#/$defs/inherit"}
environment: {"$ref": "#/$defs/inherit"}
environment-pass: {"$ref": "#/$defs/inherit"}
repair-wheel-command: {"$ref": "#/$defs/inherit"}
test-command: {"$ref": "#/$defs/inherit"}
test-extras: {"$ref": "#/$defs/inherit"}
test-sources: {"$ref": "#/$defs/inherit"}
test-requires: {"$ref": "#/$defs/inherit"}
test-environment: {"$ref": "#/$defs/inherit"}
test-runtime: {"$ref": "#/$defs/inherit"}
audit-command: audit-command:
description: Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels. description: Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.
type: string_array type: string_array
@@ -315,28 +337,6 @@ items:
additionalProperties: false additionalProperties: false
properties: properties:
select: {} select: {}
inherit:
type: object
additionalProperties: false
properties:
audit-command: {"$ref": "#/$defs/inherit"}
audit-requires: {"$ref": "#/$defs/inherit"}
before-all: {"$ref": "#/$defs/inherit"}
before-build: {"$ref": "#/$defs/inherit"}
xbuild-tools: {"$ref": "#/$defs/inherit"}
xbuild-files: {"$ref": "#/$defs/inherit"}
before-test: {"$ref": "#/$defs/inherit"}
config-settings: {"$ref": "#/$defs/inherit"}
container-engine: {"$ref": "#/$defs/inherit"}
environment: {"$ref": "#/$defs/inherit"}
environment-pass: {"$ref": "#/$defs/inherit"}
repair-wheel-command: {"$ref": "#/$defs/inherit"}
test-command: {"$ref": "#/$defs/inherit"}
test-extras: {"$ref": "#/$defs/inherit"}
test-sources: {"$ref": "#/$defs/inherit"}
test-requires: {"$ref": "#/$defs/inherit"}
test-environment: {"$ref": "#/$defs/inherit"}
test-runtime: {"$ref": "#/$defs/inherit"}
""" """
) )
+119 -43
View File
@@ -50,7 +50,13 @@ from cibuildwheel.projectfiles import get_requires_python_str, resolve_dependenc
from cibuildwheel.selector import BuildSelector, EnableGroup, TestSelector, selector_matches from cibuildwheel.selector import BuildSelector, EnableGroup, TestSelector, selector_matches
from cibuildwheel.typing import PLATFORMS, PlatformName from cibuildwheel.typing import PLATFORMS, PlatformName
from cibuildwheel.util import resources from cibuildwheel.util import resources
from cibuildwheel.util.helpers import format_safe, parse_key_value_string, strtobool, unwrap from cibuildwheel.util.helpers import (
format_safe,
parse_arbitrary_key_value_string,
parse_key_value_string,
strtobool,
unwrap,
)
from cibuildwheel.util.packaging import DependencyConstraints from cibuildwheel.util.packaging import DependencyConstraints
TYPE_CHECKING = False TYPE_CHECKING = False
@@ -436,6 +442,28 @@ def _stringify_setting(
return setting return setting
def parse_inherit(config: str | dict[str, str] | None) -> dict[str, InheritRule]:
inherit_dict: dict[str, str]
if config is None:
return {}
if isinstance(config, str):
parsed = parse_arbitrary_key_value_string(config, default_value="append")
inherit_dict = {k: "".join(v) for k, v in parsed.items()}
elif isinstance(config, dict):
inherit_dict = config
else:
msg = "'inherit' must be a string or a table"
raise OptionsReaderError(msg)
if not all(v in {"none", "append", "prepend"} for v in inherit_dict.values()):
msg = "'inherit' must contain only {'none', 'append', 'prepend'} values"
raise OptionsReaderError(msg)
return {k: InheritRule[v.upper()] for k, v in inherit_dict.items()}
class OptionsReader: class OptionsReader:
""" """
Gets options from the environment, config or defaults, optionally scoped Gets options from the environment, config or defaults, optionally scoped
@@ -484,45 +512,18 @@ class OptionsReader:
self._validate_platform_option(option_name) self._validate_platform_option(option_name)
self.config_options = config_options self.config_options = config_options
self.config_options_inherit = parse_inherit(config_options.get("inherit"))
self.config_platform_options = config_platform_options self.config_platform_options = config_platform_options
self.config_platform_options_inherit = parse_inherit(config_platform_options.get("inherit"))
self.overrides: list[Override] = []
self.current_identifier: str | None = None self.current_identifier: str | None = None
config_overrides = self.config_options.get("overrides")
if config_overrides is not None:
if not isinstance(config_overrides, list):
msg = "'tool.cibuildwheel.overrides' must be a list"
raise OptionsReaderError(msg)
for config_override in config_overrides:
select = config_override.pop("select", None)
if not select:
msg = "'select' must be set in an override"
raise OptionsReaderError(msg)
if isinstance(select, list):
select = " ".join(select)
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 OptionsReaderError(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: def _validate_global_option(self, name: str) -> None:
""" """
Raises an error if an option with this name is not allowed in the Raises an error if an option with this name is not allowed in the
[tool.cibuildwheel] section of a config file. [tool.cibuildwheel] section of a config file.
""" """
allowed_option_names = self.default_options.keys() | PLATFORMS | {"overrides"} allowed_option_names = self.default_options.keys() | PLATFORMS | {"inherit", "overrides"}
if name not in allowed_option_names: if name not in allowed_option_names:
msg = f"Option {name!r} not supported in a config file." msg = f"Option {name!r} not supported in a config file."
@@ -541,7 +542,9 @@ class OptionsReader:
msg = f"{name!r} is not allowed in {disallowed_platform_options}" msg = f"{name!r} is not allowed in {disallowed_platform_options}"
raise OptionsReaderError(msg) raise OptionsReaderError(msg)
allowed_option_names = self.default_options.keys() | self.default_platform_options.keys() allowed_option_names = (
self.default_options.keys() | self.default_platform_options.keys() | {"inherit"}
)
if name not in allowed_option_names: if name not in allowed_option_names:
msg = f"Option {name!r} not supported in the {self.platform!r} section" msg = f"Option {name!r} not supported in the {self.platform!r} section"
@@ -562,6 +565,56 @@ class OptionsReader:
return global_options, platform_options return global_options, platform_options
@functools.cached_property
def overrides(self) -> list[Override]:
config_overrides = self.config_options.get("overrides")
overrides: list[Override] = []
if config_overrides is not None:
if not isinstance(config_overrides, list):
msg = "'tool.cibuildwheel.overrides' must be a list"
raise OptionsReaderError(msg)
for config_override in config_overrides:
select = config_override.pop("select", None)
if not select:
msg = "'select' must be set in an override"
raise OptionsReaderError(msg)
if isinstance(select, list):
select = " ".join(select)
inherit = config_override.pop("inherit", {})
overrides.append(Override(select, config_override, parse_inherit(inherit)))
return overrides
@functools.cached_property
def env_inherit(self) -> dict[str, InheritRule]:
env_inherit_str = self.env.get("CIBW_INHERIT", "")
try:
return parse_inherit(env_inherit_str)
except OptionsReaderError as e:
msg = f"Failed to parse CIBW_INHERIT environment variable. {e}"
raise errors.ConfigurationError(msg) from e
@functools.cached_property
def env_platform_inherit(self) -> dict[str, InheritRule]:
env_inherit = self.env_inherit
# find the rules which have -{platform} on the end of their key,
# remove the platform suffix from the key and return the resulting
# rule.
platform_suffix = f"-{self.platform}"
return {
key.removesuffix(platform_suffix): value
for key, value in env_inherit.items()
if key.endswith(platform_suffix)
}
@property @property
def active_config_overrides(self) -> list[Override]: def active_config_overrides(self) -> list[Override]:
if self.current_identifier is None: if self.current_identifier is None:
@@ -585,7 +638,7 @@ class OptionsReader:
env_plat: bool = True, env_plat: bool = True,
option_format: OptionFormat | None = None, option_format: OptionFormat | None = None,
ignore_empty: bool = False, ignore_empty: bool = False,
env_rule: InheritRule = InheritRule.NONE, default_env_rule: InheritRule = InheritRule.NONE,
) -> str: ) -> str:
""" """
Get and return the value for the named option from environment, Get and return the value for the named option from environment,
@@ -609,16 +662,37 @@ class OptionsReader:
# get the option from the default, then the config file, then finally the environment. # get the option from the default, then the config file, then finally the environment.
# platform-specific options are preferred, if they're allowed. # platform-specific options are preferred, if they're allowed.
return _resolve_cascade( return _resolve_cascade(
(self.default_options.get(name), InheritRule.NONE), (
(self.default_platform_options.get(name), InheritRule.NONE), self.default_options.get(name),
(self.config_options.get(name), InheritRule.NONE), InheritRule.NONE,
(self.config_platform_options.get(name), InheritRule.NONE), ),
(
self.default_platform_options.get(name),
InheritRule.NONE,
),
(
self.config_options.get(name),
self.config_options_inherit.get(name, InheritRule.NONE),
),
(
self.config_platform_options.get(name),
self.config_platform_options_inherit.get(name, InheritRule.NONE),
),
*[ *[
(o.options.get(name), o.inherit.get(name, InheritRule.NONE)) (
o.options.get(name),
o.inherit.get(name, InheritRule.NONE),
)
for o in self.active_config_overrides for o in self.active_config_overrides
], ],
(self.env.get(envvar), env_rule), (
(self.env.get(plat_envvar) if env_plat else None, env_rule), self.env.get(envvar),
self.env_inherit.get(name, default_env_rule),
),
(
self.env.get(plat_envvar) if env_plat else None,
self.env_platform_inherit.get(name, default_env_rule),
),
ignore_empty=ignore_empty, ignore_empty=ignore_empty,
option_format=option_format, option_format=option_format,
) )
@@ -691,7 +765,10 @@ class Options:
allow_empty = args.allow_empty or strtobool(self.env.get("CIBW_ALLOW_EMPTY", "0")) allow_empty = args.allow_empty or strtobool(self.env.get("CIBW_ALLOW_EMPTY", "0"))
enable_groups = self.reader.get( enable_groups = self.reader.get(
"enable", env_plat=False, option_format=ListFormat(sep=" "), env_rule=InheritRule.APPEND "enable",
env_plat=False,
option_format=ListFormat(sep=" "),
default_env_rule=InheritRule.APPEND,
) )
try: try:
enable = { enable = {
@@ -797,12 +874,11 @@ class Options:
if xbuild_tools == ["\u0000"]: if xbuild_tools == ["\u0000"]:
xbuild_tools = None xbuild_tools = None
xbuild_files = parse_key_value_string( xbuild_files = parse_arbitrary_key_value_string(
self.reader.get( self.reader.get(
"xbuild-files", "xbuild-files",
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False), option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
), ),
kw_arg_names=["*"],
) )
test_sources = shlex.split( test_sources = shlex.split(
+80 -58
View File
@@ -26,6 +26,67 @@
"description": "cibuildwheel's settings.", "description": "cibuildwheel's settings.",
"type": "object", "type": "object",
"properties": { "properties": {
"inherit": {
"type": "object",
"additionalProperties": false,
"properties": {
"audit-command": {
"$ref": "#/$defs/inherit"
},
"audit-requires": {
"$ref": "#/$defs/inherit"
},
"before-all": {
"$ref": "#/$defs/inherit"
},
"before-build": {
"$ref": "#/$defs/inherit"
},
"xbuild-tools": {
"$ref": "#/$defs/inherit"
},
"xbuild-files": {
"$ref": "#/$defs/inherit"
},
"before-test": {
"$ref": "#/$defs/inherit"
},
"config-settings": {
"$ref": "#/$defs/inherit"
},
"container-engine": {
"$ref": "#/$defs/inherit"
},
"environment": {
"$ref": "#/$defs/inherit"
},
"environment-pass": {
"$ref": "#/$defs/inherit"
},
"repair-wheel-command": {
"$ref": "#/$defs/inherit"
},
"test-command": {
"$ref": "#/$defs/inherit"
},
"test-extras": {
"$ref": "#/$defs/inherit"
},
"test-sources": {
"$ref": "#/$defs/inherit"
},
"test-requires": {
"$ref": "#/$defs/inherit"
},
"test-environment": {
"$ref": "#/$defs/inherit"
},
"test-runtime": {
"$ref": "#/$defs/inherit"
}
},
"title": "CIBW_INHERIT"
},
"audit-command": { "audit-command": {
"description": "Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.", "description": "Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels.",
"oneOf": [ "oneOf": [
@@ -689,64 +750,7 @@
] ]
}, },
"inherit": { "inherit": {
"type": "object", "$ref": "#/properties/inherit"
"additionalProperties": false,
"properties": {
"audit-command": {
"$ref": "#/$defs/inherit"
},
"audit-requires": {
"$ref": "#/$defs/inherit"
},
"before-all": {
"$ref": "#/$defs/inherit"
},
"before-build": {
"$ref": "#/$defs/inherit"
},
"xbuild-tools": {
"$ref": "#/$defs/inherit"
},
"xbuild-files": {
"$ref": "#/$defs/inherit"
},
"before-test": {
"$ref": "#/$defs/inherit"
},
"config-settings": {
"$ref": "#/$defs/inherit"
},
"container-engine": {
"$ref": "#/$defs/inherit"
},
"environment": {
"$ref": "#/$defs/inherit"
},
"environment-pass": {
"$ref": "#/$defs/inherit"
},
"repair-wheel-command": {
"$ref": "#/$defs/inherit"
},
"test-command": {
"$ref": "#/$defs/inherit"
},
"test-extras": {
"$ref": "#/$defs/inherit"
},
"test-sources": {
"$ref": "#/$defs/inherit"
},
"test-requires": {
"$ref": "#/$defs/inherit"
},
"test-environment": {
"$ref": "#/$defs/inherit"
},
"test-runtime": {
"$ref": "#/$defs/inherit"
}
}
}, },
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
@@ -875,6 +879,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
@@ -1014,6 +1021,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
@@ -1099,6 +1109,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
@@ -1184,6 +1197,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
@@ -1256,6 +1272,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
@@ -1341,6 +1360,9 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": { "audit-command": {
"$ref": "#/properties/audit-command" "$ref": "#/properties/audit-command"
}, },
+47 -2
View File
@@ -111,7 +111,7 @@ def parse_key_value_string(
if kw_arg_names is None: if kw_arg_names is None:
kw_arg_names = [] kw_arg_names = []
all_field_names = None if ("*" in kw_arg_names) else [*positional_arg_names, *kw_arg_names] all_field_names = [*positional_arg_names, *kw_arg_names]
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";") shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = "" shlexer.commenters = ""
@@ -128,7 +128,7 @@ def parse_key_value_string(
# check to see if the option name is specified # check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":") field_name, sep, first_value = field[0].partition(":")
if sep: if sep:
if (all_field_names is not None) and (field_name not in all_field_names): if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}" msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg) raise ValueError(msg)
@@ -147,6 +147,51 @@ def parse_key_value_string(
return dict(result) return dict(result)
def parse_arbitrary_key_value_string(
key_value_string: str, default_value: str | None = None
) -> dict[str, list[str]]:
"""
Parses a string like
"before-build; before-test: append; after-test: prepend"
or
"package1: some/header.h some/library.a; package2: other/header.h"
There are no restrictions on the keys than can be set.
No positional arguments are allowed. Words without a colon attached are
interpreted as keys. Keys without a value will be assigned the
default_value if provided, otherwise throw an error.
"""
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['before-build', ';', 'before-test:', 'append', ';', 'after-test:', 'prepend']
# split by semicolon
result: defaultdict[str, list[str]] = defaultdict(list)
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
for field in fields:
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
if sep:
# the colon was present, so the first value is the value after the colon
values = ([first_value] if first_value else []) + field[1:]
result[field_name] += values
else:
# no colon, so it's a key (or set of keys) without values
if default_value is None:
msg = f"Failed to parse {key_value_string!r}. No value specified for {field_name!r}. Expected ':' followed by a value."
raise ValueError(msg)
for key in field:
result[key].append(default_value)
return dict(result)
@dataclasses.dataclass(order=True) @dataclasses.dataclass(order=True)
class FlexibleVersion: class FlexibleVersion:
version_parts: tuple[int, ...] = dataclasses.field(init=False, repr=False) version_parts: tuple[int, ...] = dataclasses.field(init=False, repr=False)
+71 -18
View File
@@ -66,10 +66,11 @@ placed in `[tool.cibuildwheel]` and are lower case, with dashes, following
common [TOML](https://toml.io) practice. Anything placed in subsections common [TOML](https://toml.io) practice. Anything placed in subsections
named after a platform will only affect those platforms. Platform-specific named after a platform will only affect those platforms. Platform-specific
values replace the corresponding global value for that platform; table options values replace the corresponding global value for that platform; table options
are not merged key by key. Lists can be used instead of strings for items that are not merged key by key unless you configure [inheritance](#inherit). Lists can
are naturally a list. Multiline strings also work just like in the environment be used instead of strings for items that are naturally a list. Multiline strings
variables. Environment variable overrides, such as `CIBW_TEST_COMMAND` and also work just like in the environment variables. Environment variable overrides,
`CIBW_TEST_COMMAND_LINUX`, will take precedence if defined. such as `CIBW_TEST_COMMAND` and `CIBW_TEST_COMMAND_LINUX`, take precedence if
defined.
The example above using environment variables could have been written like this: The example above using environment variables could have been written like this:
@@ -113,9 +114,9 @@ trigger new containers, one per image.
The ``output-dir``, ``build``, ``skip``, ``test_skip`` selectors, and architectures cannot be overridden. The ``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 By default, values in an override replace values from the main configuration or
list will inherit from previous overrides or the main configuration. The valid earlier overrides. You can instead [extend a list or table option](#inherit) by
options are `"none"` (the default), `"append"`, and `"prepend"`. setting an `inherit` rule for it.
#### Examples: #### Examples:
@@ -175,14 +176,50 @@ This example will provide the command `"pyproject-before && pyproject && pyproje
on Python 3.11, and will have `environment = {FOO="BAZ", "PYTHON"="MONTY", "HAM"="EGGS"}`. on Python 3.11, and will have `environment = {FOO="BAZ", "PYTHON"="MONTY", "HAM"="EGGS"}`.
## Extending existing options {: #inherit } ## Option inheritance {: #inherit }
In the TOML configuration, you can choose how tables and lists are inherited. As cibuildwheel reads its configuration, each layer normally replaces the value
By default, all values are overridden completely (`"none"`) but sometimes you'd from the previous layer. The layers, from lowest to highest precedence, are:
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 1. cibuildwheel's defaults
variable for CPython 3.11, without `inherit` you'd have to repeat all the 2. `[tool.cibuildwheel]`
original environment variables in the override. With `inherit`, it's just: 3. `[tool.cibuildwheel.<platform>]`
4. matching `[[tool.cibuildwheel.overrides]]` entries, in order
5. `CIBW_<OPTION>`
6. `CIBW_<OPTION>_<PLATFORM>`
For list and table options, you can use an `inherit` rule to merge a value with
the value accumulated from the preceding layers instead. The available rules
are `"none"` (replace the previous value, the default), `"append"`, and
`"prepend"`.
In `pyproject.toml`, set the rule in the same table as the value it applies to.
For example, this adds Twine checks to the default audit configuration:
```toml
[tool.cibuildwheel]
inherit.audit-requires = "append"
inherit.audit-command = "append"
audit-requires = ["twine"]
audit-command = "twine check {wheel}"
```
Inheritance can also combine global and platform-specific configuration. This
example runs a Linux-specific setup command before the global command:
```toml
[tool.cibuildwheel]
before-all = "make -C third_party_lib"
[tool.cibuildwheel.linux]
inherit.before-all = "prepend"
before-all = "yum install -y libffi-devel"
```
The same mechanism remains available 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 ```toml
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
@@ -210,7 +247,23 @@ repair-wheel-command = "echo 'After repair'"
``` ```
As seen in this example, you can have multiple overrides match - they match top 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 to bottom, with the config being accumulated.
inheritance, you can use `select = "*-????linux_*"` for Linux, `select =
"*-win_*"` for Windows, and `select = "*-macosx_*"` for macOS. As always, For environment variables, specify the rules in `CIBW_INHERIT`. Rules are
environment variables will completely override any TOML configuration. separated by semicolons and use lowercase option names. A rule without an
explicit value defaults to `append`:
```yaml
CIBW_AUDIT_REQUIRES: twine
CIBW_AUDIT_COMMAND: "twine check {wheel}"
CIBW_INHERIT: "audit-requires; audit-command"
```
To control a platform-specific environment variable, add the lowercase platform
suffix to the option name. For example, this prepends `CIBW_BEFORE_ALL_LINUX` to
the value accumulated from the lower-precedence layers:
```yaml
CIBW_BEFORE_ALL_LINUX: yum install -y libffi-devel
CIBW_INHERIT: "before-all-linux: prepend"
```
+9 -3
View File
@@ -1444,10 +1444,11 @@ Run shell commands to verify your wheels once they are built. Multiple commands
audit-requires = ["twine"] audit-requires = ["twine"]
audit-command = "twine check {wheel}" audit-command = "twine check {wheel}"
# Add an additional audit command using overrides, keeping the default abi3audit check # Add an additional audit command, keeping the default abi3audit check
[[tool.cibuildwheel.overrides]] [tool.cibuildwheel]
select = "*" inherit.audit-requires = "append"
inherit.audit-command = "append" inherit.audit-command = "append"
audit-requires = ["twine"]
audit-command = "twine check {wheel}" audit-command = "twine check {wheel}"
``` ```
@@ -1463,6 +1464,11 @@ Run shell commands to verify your wheels once they are built. Multiple commands
# Use twine check to validate wheel metadata # Use twine check to validate wheel metadata
CIBW_AUDIT_REQUIRES: "twine" CIBW_AUDIT_REQUIRES: "twine"
CIBW_AUDIT_COMMAND: "twine check {wheel}" CIBW_AUDIT_COMMAND: "twine check {wheel}"
# Add twine check to the default audit configuration
CIBW_AUDIT_REQUIRES: "twine"
CIBW_AUDIT_COMMAND: "twine check {wheel}"
CIBW_INHERIT: "audit-requires; audit-command"
``` ```
@@ -1,3 +1,5 @@
"""Tests for reading and resolving configuration options."""
from __future__ import annotations from __future__ import annotations
import shlex import shlex
@@ -5,6 +7,7 @@ from typing import Any, cast
import pytest import pytest
from cibuildwheel import errors
from cibuildwheel.options import ( from cibuildwheel.options import (
EnvironmentFormat, EnvironmentFormat,
InheritRule, InheritRule,
@@ -472,7 +475,11 @@ test-command = "pyproject-override"
) )
with pytest.raises(OptionsReaderError): with pytest.raises(OptionsReaderError):
OptionsReader(config_file_path=pyproject_toml, platform=cast("Any", platform), env={}) print(
OptionsReader(
config_file_path=pyproject_toml, platform=cast("Any", platform), env={}
).overrides
)
def test_config_settings(tmp_path: Path) -> None: def test_config_settings(tmp_path: Path) -> None:
@@ -549,6 +556,112 @@ before-all = ["override2"]
) )
def test_global_inherit(tmp_path: Path) -> None:
pyproject_toml = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
inherit = {audit-command = "append"}
audit-command = "twine check {wheel}"
"""
)
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
assert (
options_reader.get("audit-command", option_format=ListFormat(" && "))
== "abi3audit --strict --report {abi3_wheel} && twine check {wheel}"
)
def test_platform_inherit(tmp_path: Path) -> None:
pyproject_toml = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
before-all = "global"
[tool.cibuildwheel.linux]
inherit = {before-all = "prepend"}
before-all = "linux"
"""
)
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
assert options_reader.get("before-all", option_format=ListFormat(" && ")) == "linux && global"
def test_environment_inherit(tmp_path: Path) -> None:
pyproject_toml = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
before-all = "config"
"""
)
options_reader = OptionsReader(
pyproject_toml,
platform="linux",
env={
"CIBW_BEFORE_ALL": "env",
"CIBW_BEFORE_ALL_LINUX": "linux-env",
# A key without a value defaults to append.
"CIBW_INHERIT": "before-all; before-all-linux: prepend",
},
)
assert (
options_reader.get("before-all", option_format=ListFormat(" && "))
== "linux-env && config && env"
)
def test_environment_inherit_none_overrides_default_rule(tmp_path: Path) -> None:
pyproject_toml = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
enable = ["cpython-freethreading"]
"""
)
options_reader = OptionsReader(
pyproject_toml,
platform="linux",
env={"CIBW_ENABLE": "pypy", "CIBW_INHERIT": "enable: none"},
)
assert (
options_reader.get(
"enable", option_format=ListFormat(" "), default_env_rule=InheritRule.APPEND
)
== "pypy"
)
def test_invalid_config_inherit_rule(tmp_path: Path) -> None:
pyproject_toml = tmp_path / "pyproject.toml"
pyproject_toml.write_text(
"""\
[tool.cibuildwheel]
inherit = {before-all = "invalid"}
"""
)
with pytest.raises(OptionsReaderError, match="must contain only"):
OptionsReader(pyproject_toml, platform="linux", env={})
def test_invalid_environment_inherit_rule() -> None:
options_reader = OptionsReader(platform="linux", env={"CIBW_INHERIT": "before-all: invalid"})
with pytest.raises(
errors.ConfigurationError, match="Failed to parse CIBW_INHERIT environment variable"
):
options_reader.get("before-all", option_format=ListFormat(" && "))
def test_audit_command_option(tmp_path: Path, platform: PlatformName) -> None: def test_audit_command_option(tmp_path: Path, platform: PlatformName) -> None:
pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml: Path = tmp_path / "pyproject.toml"
pyproject_toml.write_text( pyproject_toml.write_text(
+53 -11
View File
@@ -11,6 +11,7 @@ from cibuildwheel.util.file import copy_test_sources, remove_on_error
from cibuildwheel.util.helpers import ( from cibuildwheel.util.helpers import (
FlexibleVersion, FlexibleVersion,
format_safe, format_safe,
parse_arbitrary_key_value_string,
parse_key_value_string, parse_key_value_string,
prepare_command, prepare_command,
unwrap, unwrap,
@@ -218,20 +219,61 @@ def test_parse_key_value_string_unknown_name() -> None:
with pytest.raises(ValueError, match=r"Failed to parse 'key: value'. Unknown field name 'key'"): with pytest.raises(ValueError, match=r"Failed to parse 'key: value'. Unknown field name 'key'"):
parse_key_value_string("key: value") parse_key_value_string("key: value")
# Unknown fields can be enabled by passing "*".
assert parse_key_value_string( def test_parse_arbitrary_key_value_string_basic() -> None:
"key: value", assert parse_arbitrary_key_value_string("before-test: append; after-test: prepend") == {
kw_arg_names=["*"], "before-test": ["append"],
) == { "after-test": ["prepend"],
"key": ["value"],
} }
assert parse_key_value_string(
"key1: value1a value1b; key2: value2", def test_parse_arbitrary_key_value_string_multiple_values() -> None:
kw_arg_names=["*"], assert parse_arbitrary_key_value_string(
"package1: some/header.h some/library.a; package2: other/header.h"
) == { ) == {
"key1": ["value1a", "value1b"], "package1": ["some/header.h", "some/library.a"],
"key2": ["value2"], "package2": ["other/header.h"],
}
def test_parse_arbitrary_key_value_string_keys_without_values_default() -> None:
assert parse_arbitrary_key_value_string(
"before-build; before-test: prepend", default_value="append"
) == {
"before-build": ["append"],
"before-test": ["prepend"],
}
def test_parse_arbitrary_key_value_string_keys_without_values_no_default() -> None:
with pytest.raises(ValueError, match="No value specified"):
parse_arbitrary_key_value_string("before-build")
def test_parse_arbitrary_key_value_string_empty() -> None:
assert parse_arbitrary_key_value_string("") == {}
def test_parse_arbitrary_key_value_string_duplicate_keys() -> None:
assert parse_arbitrary_key_value_string("key: val1; key: val2") == {
"key": ["val1", "val2"],
}
def test_parse_arbitrary_key_value_string_key_only_with_colon() -> None:
assert parse_arbitrary_key_value_string("key:") == {"key": []}
def test_parse_arbitrary_key_value_string_quoted_values() -> None:
assert parse_arbitrary_key_value_string('key: "hello world"') == {"key": ["hello world"]}
def test_parse_arbitrary_key_value_string_multiple_bare_keys_with_default() -> None:
"""works, but should remain undocumented"""
assert parse_arbitrary_key_value_string("a b c", default_value="yes") == {
"a": ["yes"],
"b": ["yes"],
"c": ["yes"],
} }