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.
type: object
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:
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
@@ -315,28 +337,6 @@ items:
additionalProperties: false
properties:
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.typing import PLATFORMS, PlatformName
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
TYPE_CHECKING = False
@@ -436,6 +442,28 @@ def _stringify_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:
"""
Gets options from the environment, config or defaults, optionally scoped
@@ -484,45 +512,18 @@ class OptionsReader:
self._validate_platform_option(option_name)
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_inherit = parse_inherit(config_platform_options.get("inherit"))
self.overrides: list[Override] = []
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:
"""
Raises an error if an option with this name is not allowed in the
[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:
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}"
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:
msg = f"Option {name!r} not supported in the {self.platform!r} section"
@@ -562,6 +565,56 @@ class OptionsReader:
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
def active_config_overrides(self) -> list[Override]:
if self.current_identifier is None:
@@ -585,7 +638,7 @@ class OptionsReader:
env_plat: bool = True,
option_format: OptionFormat | None = None,
ignore_empty: bool = False,
env_rule: InheritRule = InheritRule.NONE,
default_env_rule: InheritRule = InheritRule.NONE,
) -> str:
"""
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.
# platform-specific options are preferred, if they're allowed.
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),
(
self.default_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
],
(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,
option_format=option_format,
)
@@ -691,7 +765,10 @@ class Options:
allow_empty = args.allow_empty or strtobool(self.env.get("CIBW_ALLOW_EMPTY", "0"))
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:
enable = {
@@ -797,12 +874,11 @@ class Options:
if xbuild_tools == ["\u0000"]:
xbuild_tools = None
xbuild_files = parse_key_value_string(
xbuild_files = parse_arbitrary_key_value_string(
self.reader.get(
"xbuild-files",
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
),
kw_arg_names=["*"],
)
test_sources = shlex.split(
+80 -58
View File
@@ -26,6 +26,67 @@
"description": "cibuildwheel's settings.",
"type": "object",
"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": {
"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": [
@@ -689,64 +750,7 @@
]
},
"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"
}
}
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
@@ -875,6 +879,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
},
@@ -1014,6 +1021,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
},
@@ -1099,6 +1109,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
},
@@ -1184,6 +1197,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
},
@@ -1256,6 +1272,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"audit-command": {
"$ref": "#/properties/audit-command"
},
@@ -1341,6 +1360,9 @@
"type": "object",
"additionalProperties": false,
"properties": {
"inherit": {
"$ref": "#/properties/inherit"
},
"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:
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.commenters = ""
@@ -128,7 +128,7 @@ def parse_key_value_string(
# check to see if the option name is specified
field_name, sep, first_value = field[0].partition(":")
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}"
raise ValueError(msg)
@@ -147,6 +147,51 @@ def parse_key_value_string(
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)
class FlexibleVersion:
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
named after a platform will only affect those platforms. Platform-specific
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 naturally a list. Multiline strings also work just like in the environment
variables. Environment variable overrides, such as `CIBW_TEST_COMMAND` and
`CIBW_TEST_COMMAND_LINUX`, will take precedence if defined.
are not merged key by key unless you configure [inheritance](#inherit). Lists can
be used instead of strings for items that are naturally a list. Multiline strings
also work just like in the environment variables. Environment variable overrides,
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:
@@ -113,9 +114,9 @@ trigger new containers, one per image.
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
list will inherit from previous overrides or the main configuration. The valid
options are `"none"` (the default), `"append"`, and `"prepend"`.
By default, values in an override replace values from the main configuration or
earlier overrides. You can instead [extend a list or table option](#inherit) by
setting an `inherit` rule for it.
#### 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"}`.
## Extending existing options {: #inherit }
## Option inheritance {: #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:
As cibuildwheel reads its configuration, each layer normally replaces the value
from the previous layer. The layers, from lowest to highest precedence, are:
1. cibuildwheel's defaults
2. `[tool.cibuildwheel]`
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
[[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
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.
to bottom, with the config being accumulated.
For environment variables, specify the rules in `CIBW_INHERIT`. Rules are
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-command = "twine check {wheel}"
# Add an additional audit command using overrides, keeping the default abi3audit check
[[tool.cibuildwheel.overrides]]
select = "*"
# Add an additional audit command, keeping the default abi3audit check
[tool.cibuildwheel]
inherit.audit-requires = "append"
inherit.audit-command = "append"
audit-requires = ["twine"]
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
CIBW_AUDIT_REQUIRES: "twine"
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
import shlex
@@ -5,6 +7,7 @@ from typing import Any, cast
import pytest
from cibuildwheel import errors
from cibuildwheel.options import (
EnvironmentFormat,
InheritRule,
@@ -472,7 +475,11 @@ test-command = "pyproject-override"
)
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:
@@ -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:
pyproject_toml: Path = tmp_path / "pyproject.toml"
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 (
FlexibleVersion,
format_safe,
parse_arbitrary_key_value_string,
parse_key_value_string,
prepare_command,
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'"):
parse_key_value_string("key: value")
# Unknown fields can be enabled by passing "*".
assert parse_key_value_string(
"key: value",
kw_arg_names=["*"],
) == {
"key": ["value"],
def test_parse_arbitrary_key_value_string_basic() -> None:
assert parse_arbitrary_key_value_string("before-test: append; after-test: prepend") == {
"before-test": ["append"],
"after-test": ["prepend"],
}
assert parse_key_value_string(
"key1: value1a value1b; key2: value2",
kw_arg_names=["*"],
def test_parse_arbitrary_key_value_string_multiple_values() -> None:
assert parse_arbitrary_key_value_string(
"package1: some/header.h some/library.a; package2: other/header.h"
) == {
"key1": ["value1a", "value1b"],
"key2": ["value2"],
"package1": ["some/header.h", "some/library.a"],
"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"],
}