Fix for #1803, where the method used for merging the config-settings option - string concatenation - was of little use. I broke out the TableFmt object into a protocol-like object called OptionFormat, where the formatting and merge rules can be customised more deeply. This is more explicit, and lets us be a bit more precise with how TOML object get converted into the options. I intend to use the flexibility of the OptionsFormat structure to tackle the remaining abiguity around environment variable quoting, as seen in the xfails in the options_test.py unit test and discussed in #1271. Co-authored-by: Matthieu Darbois <mayeut@users.noreply.github.com>
This commit is contained in:
co-authored by
Matthieu Darbois
parent
229029e112
commit
ca08f5fbcf
+179
-93
@@ -9,15 +9,15 @@ import enum
|
|||||||
import functools
|
import functools
|
||||||
import shlex
|
import shlex
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections.abc import Callable, Generator, Iterable, Iterator, Set
|
from collections.abc import Generator, Iterable, Set
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, Mapping, Sequence, TypedDict, Union # noqa: TID251
|
from typing import Any, Literal, Mapping, Sequence, Union # noqa: TID251
|
||||||
|
|
||||||
from packaging.specifiers import SpecifierSet
|
from packaging.specifiers import SpecifierSet
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from ._compat import tomllib
|
from ._compat import tomllib
|
||||||
from ._compat.typing import NotRequired, assert_never
|
from ._compat.typing import assert_never
|
||||||
from .architecture import Architecture
|
from .architecture import Architecture
|
||||||
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
|
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
|
||||||
from .logger import log
|
from .logger import log
|
||||||
@@ -118,13 +118,16 @@ class BuildOptions:
|
|||||||
return self.globals.architectures
|
return self.globals.architectures
|
||||||
|
|
||||||
|
|
||||||
Setting = Union[Mapping[str, str], Sequence[str], str, int, bool]
|
SettingLeaf = Union[str, int, bool]
|
||||||
|
SettingList = Sequence[SettingLeaf]
|
||||||
|
SettingTable = Mapping[str, Union[SettingLeaf, SettingList]]
|
||||||
|
SettingValue = Union[SettingTable, SettingList, SettingLeaf]
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class Override:
|
class Override:
|
||||||
select_pattern: str
|
select_pattern: str
|
||||||
options: dict[str, Setting]
|
options: dict[str, SettingValue]
|
||||||
inherit: dict[str, InheritRule]
|
inherit: dict[str, InheritRule]
|
||||||
|
|
||||||
|
|
||||||
@@ -137,22 +140,123 @@ DISALLOWED_OPTIONS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TableFmt(TypedDict):
|
class OptionsReaderError(errors.ConfigurationError):
|
||||||
# a format string, used with '.format', with `k` and `v` parameters
|
|
||||||
# e.g. "{k}={v}"
|
|
||||||
item: str
|
|
||||||
# the string that is inserted between items
|
|
||||||
# e.g. " "
|
|
||||||
sep: str
|
|
||||||
# a quoting function that, if supplied, is called to quote each value
|
|
||||||
# e.g. shlex.quote
|
|
||||||
quote: NotRequired[Callable[[str], str]]
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigOptionError(KeyError):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OptionFormat:
|
||||||
|
"""
|
||||||
|
Base class for option format specifiers. These objects describe how values
|
||||||
|
can be parsed from rich TOML values and how they're merged together.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class NotSupported(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def format_list(self, value: SettingList) -> str: # noqa: ARG002
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
|
||||||
|
def format_table(self, table: SettingTable) -> str: # noqa: ARG002
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
|
||||||
|
def merge_values(self, before: str, after: str) -> str: # noqa: ARG002
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
|
||||||
|
|
||||||
|
class ListFormat(OptionFormat):
|
||||||
|
"""
|
||||||
|
A format that joins lists with a separator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sep: str) -> None:
|
||||||
|
self.sep = sep
|
||||||
|
|
||||||
|
def format_list(self, value: SettingList) -> str:
|
||||||
|
return self.sep.join(str(v) for v in value)
|
||||||
|
|
||||||
|
def merge_values(self, before: str, after: str) -> str:
|
||||||
|
return f"{before}{self.sep}{after}"
|
||||||
|
|
||||||
|
|
||||||
|
class ShlexTableFormat(OptionFormat):
|
||||||
|
"""
|
||||||
|
The standard table format uses shlex.quote to quote values and shlex.split
|
||||||
|
to unquote and split them. When merging values, keys in before are
|
||||||
|
replaced by keys in after.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sep: str = " ", pair_sep: str = "=", allow_merge: bool = True) -> None:
|
||||||
|
self.sep = sep
|
||||||
|
self.pair_sep = pair_sep
|
||||||
|
self.allow_merge = allow_merge
|
||||||
|
|
||||||
|
def format_table(self, table: SettingTable) -> str:
|
||||||
|
assignments: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
for k, v in table.items():
|
||||||
|
if shlex.split(k) != [k]:
|
||||||
|
msg = f"Invalid table key: {k}"
|
||||||
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
|
if isinstance(v, str):
|
||||||
|
assignments.append((k, v))
|
||||||
|
elif isinstance(v, Sequence):
|
||||||
|
for inner_v in v:
|
||||||
|
assignments.append((k, str(inner_v)))
|
||||||
|
else:
|
||||||
|
assignments.append((k, str(v)))
|
||||||
|
|
||||||
|
return self.sep.join(f"{k}{self.pair_sep}{shlex.quote(v)}" for k, v in assignments)
|
||||||
|
|
||||||
|
def merge_values(self, before: str, after: str) -> str:
|
||||||
|
if not self.allow_merge:
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
|
||||||
|
before_dict = self.parse_table(before)
|
||||||
|
after_dict = self.parse_table(after)
|
||||||
|
|
||||||
|
return self.format_table({**before_dict, **after_dict})
|
||||||
|
|
||||||
|
def parse_table(self, table: str) -> Mapping[str, str | Sequence[str]]:
|
||||||
|
assignments: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
for assignment_str in shlex.split(table):
|
||||||
|
key, sep, value = assignment_str.partition(self.pair_sep)
|
||||||
|
|
||||||
|
if not sep:
|
||||||
|
msg = f"malformed option with value {assignment_str!r}"
|
||||||
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
|
assignments.append((key, value))
|
||||||
|
|
||||||
|
result: dict[str, str | list[str]] = {}
|
||||||
|
|
||||||
|
for key, value in assignments:
|
||||||
|
if key in result:
|
||||||
|
existing_value = result[key]
|
||||||
|
if isinstance(existing_value, list):
|
||||||
|
result[key] = [*existing_value, value]
|
||||||
|
else:
|
||||||
|
result[key] = [existing_value, value]
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentFormat(OptionFormat):
|
||||||
|
"""
|
||||||
|
The environment format accepts a table of environment variables, where the
|
||||||
|
values may contain variables or command substitutions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def format_table(self, table: SettingTable) -> str:
|
||||||
|
return " ".join(f'{k}="{v}"' for k, v in table.items())
|
||||||
|
|
||||||
|
def merge_values(self, before: str, after: str) -> str:
|
||||||
|
return f"{before} {after}"
|
||||||
|
|
||||||
|
|
||||||
class InheritRule(enum.Enum):
|
class InheritRule(enum.Enum):
|
||||||
NONE = enum.auto()
|
NONE = enum.auto()
|
||||||
APPEND = enum.auto()
|
APPEND = enum.auto()
|
||||||
@@ -160,10 +264,9 @@ class InheritRule(enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
def _resolve_cascade(
|
def _resolve_cascade(
|
||||||
*pairs: tuple[Setting | None, InheritRule],
|
*pairs: tuple[SettingValue | None, InheritRule],
|
||||||
ignore_empty: bool = False,
|
ignore_empty: bool = False,
|
||||||
list_sep: str | None = None,
|
option_format: OptionFormat | None = None,
|
||||||
table_format: TableFmt | None = None,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Given a cascade of values with inherit rules, resolve them into a single
|
Given a cascade of values with inherit rules, resolve them into a single
|
||||||
@@ -186,13 +289,6 @@ def _resolve_cascade(
|
|||||||
|
|
||||||
result: str | None = None
|
result: str | None = None
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
for value, rule in pairs:
|
for value, rule in pairs:
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
@@ -200,14 +296,9 @@ def _resolve_cascade(
|
|||||||
if ignore_empty and not value and value is not False:
|
if ignore_empty and not value and value is not False:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
value_string = _stringify_setting(value, list_sep, table_format)
|
value_string = _stringify_setting(value, option_format=option_format)
|
||||||
|
|
||||||
result = _merge_values(
|
result = _apply_inherit_rule(result, value_string, rule=rule, option_format=option_format)
|
||||||
result,
|
|
||||||
value_string,
|
|
||||||
rule=rule,
|
|
||||||
merge_sep=merge_sep,
|
|
||||||
)
|
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
msg = "a setting should at least have a default value"
|
msg = "a setting should at least have a default value"
|
||||||
@@ -217,7 +308,9 @@ def _resolve_cascade(
|
|||||||
|
|
||||||
|
|
||||||
# pylint: disable-next=inconsistent-return-statements
|
# pylint: disable-next=inconsistent-return-statements
|
||||||
def _merge_values(before: str | None, after: str, rule: InheritRule, merge_sep: str | None) -> str:
|
def _apply_inherit_rule(
|
||||||
|
before: str | None, after: str, rule: InheritRule, option_format: OptionFormat | None
|
||||||
|
) -> str:
|
||||||
if rule == InheritRule.NONE:
|
if rule == InheritRule.NONE:
|
||||||
return after
|
return after
|
||||||
|
|
||||||
@@ -230,34 +323,39 @@ def _merge_values(before: str | None, after: str, rule: InheritRule, merge_sep:
|
|||||||
# if after is an empty string, we shouldn't add any separator
|
# if after is an empty string, we shouldn't add any separator
|
||||||
return before
|
return before
|
||||||
|
|
||||||
if not merge_sep:
|
if not option_format:
|
||||||
msg = f"Don't know how to merge {before!r} and {after!r} with {rule}"
|
msg = f"Don't know how to merge {before!r} and {after!r} with {rule}"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
if rule == InheritRule.APPEND:
|
if rule == InheritRule.APPEND:
|
||||||
return f"{before}{merge_sep}{after}"
|
return option_format.merge_values(before, after)
|
||||||
elif rule == InheritRule.PREPEND:
|
elif rule == InheritRule.PREPEND:
|
||||||
return f"{after}{merge_sep}{before}"
|
return option_format.merge_values(after, before)
|
||||||
else:
|
else:
|
||||||
assert_never(rule)
|
assert_never(rule)
|
||||||
|
|
||||||
|
|
||||||
def _stringify_setting(
|
def _stringify_setting(
|
||||||
setting: Setting, list_sep: str | None, table_format: TableFmt | None
|
setting: SettingValue,
|
||||||
|
option_format: OptionFormat | None,
|
||||||
) -> str:
|
) -> str:
|
||||||
if isinstance(setting, Mapping):
|
if isinstance(setting, Mapping):
|
||||||
if table_format is None:
|
try:
|
||||||
|
if option_format is None:
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
return option_format.format_table(setting)
|
||||||
|
except OptionFormat.NotSupported:
|
||||||
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a table"
|
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a table"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg) from None
|
||||||
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 not isinstance(setting, str) and isinstance(setting, Sequence):
|
||||||
if list_sep is None:
|
try:
|
||||||
|
if option_format is None:
|
||||||
|
raise OptionFormat.NotSupported
|
||||||
|
return option_format.format_list(setting)
|
||||||
|
except OptionFormat.NotSupported:
|
||||||
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a list"
|
msg = f"Error converting {setting!r} to a string: this setting doesn't accept a list"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg) from None
|
||||||
return list_sep.join(setting)
|
|
||||||
|
|
||||||
if isinstance(setting, (bool, int)):
|
if isinstance(setting, (bool, int)):
|
||||||
return str(setting)
|
return str(setting)
|
||||||
@@ -323,14 +421,14 @@ class OptionsReader:
|
|||||||
if config_overrides is not None:
|
if config_overrides is not None:
|
||||||
if not isinstance(config_overrides, list):
|
if not isinstance(config_overrides, list):
|
||||||
msg = "'tool.cibuildwheel.overrides' must be a list"
|
msg = "'tool.cibuildwheel.overrides' must be a list"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
for config_override in config_overrides:
|
for config_override in config_overrides:
|
||||||
select = config_override.pop("select", None)
|
select = config_override.pop("select", None)
|
||||||
|
|
||||||
if not select:
|
if not select:
|
||||||
msg = "'select' must be set in an override"
|
msg = "'select' must be set in an override"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
if isinstance(select, list):
|
if isinstance(select, list):
|
||||||
select = " ".join(select)
|
select = " ".join(select)
|
||||||
@@ -340,7 +438,7 @@ class OptionsReader:
|
|||||||
i in {"none", "append", "prepend"} for i in inherit.values()
|
i in {"none", "append", "prepend"} for i in inherit.values()
|
||||||
):
|
):
|
||||||
msg = "'inherit' must be a dict containing only {'none', 'append', 'prepend'} values"
|
msg = "'inherit' must be a dict containing only {'none', 'append', 'prepend'} values"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
inherit_enum = {k: InheritRule[v.upper()] for k, v in inherit.items()}
|
inherit_enum = {k: InheritRule[v.upper()] for k, v in inherit.items()}
|
||||||
|
|
||||||
@@ -358,7 +456,7 @@ class OptionsReader:
|
|||||||
matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7)
|
matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7)
|
||||||
if matches:
|
if matches:
|
||||||
msg += f" Perhaps you meant {matches[0]!r}?"
|
msg += f" Perhaps you meant {matches[0]!r}?"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
def _validate_platform_option(self, name: str) -> None:
|
def _validate_platform_option(self, name: str) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -368,7 +466,7 @@ class OptionsReader:
|
|||||||
disallowed_platform_options = self.disallow.get(self.platform, set())
|
disallowed_platform_options = self.disallow.get(self.platform, set())
|
||||||
if name in disallowed_platform_options:
|
if name in disallowed_platform_options:
|
||||||
msg = f"{name!r} is not allowed in {disallowed_platform_options}"
|
msg = f"{name!r} is not allowed in {disallowed_platform_options}"
|
||||||
raise ConfigOptionError(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()
|
||||||
|
|
||||||
@@ -377,7 +475,7 @@ class OptionsReader:
|
|||||||
matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7)
|
matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7)
|
||||||
if matches:
|
if matches:
|
||||||
msg += f" Perhaps you meant {matches[0]!r}?"
|
msg += f" Perhaps you meant {matches[0]!r}?"
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
def _load_file(self, filename: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
def _load_file(self, filename: Path) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
@@ -412,8 +510,7 @@ class OptionsReader:
|
|||||||
name: str,
|
name: str,
|
||||||
*,
|
*,
|
||||||
env_plat: bool = True,
|
env_plat: bool = True,
|
||||||
list_sep: str | None = None,
|
option_format: OptionFormat | None = None,
|
||||||
table_format: TableFmt | None = None,
|
|
||||||
ignore_empty: bool = False,
|
ignore_empty: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -429,7 +526,7 @@ class OptionsReader:
|
|||||||
|
|
||||||
if name not in self.default_options and name not in self.default_platform_options:
|
if name not in self.default_options and name not in self.default_platform_options:
|
||||||
msg = f"{name!r} must be in cibuildwheel/resources/defaults.toml file to be accessed."
|
msg = f"{name!r} must be in cibuildwheel/resources/defaults.toml file to be accessed."
|
||||||
raise ConfigOptionError(msg)
|
raise OptionsReaderError(msg)
|
||||||
|
|
||||||
# Environment variable form
|
# Environment variable form
|
||||||
envvar = f"CIBW_{name.upper().replace('-', '_')}"
|
envvar = f"CIBW_{name.upper().replace('-', '_')}"
|
||||||
@@ -449,26 +546,10 @@ class OptionsReader:
|
|||||||
(self.env.get(envvar), InheritRule.NONE),
|
(self.env.get(envvar), InheritRule.NONE),
|
||||||
(self.env.get(plat_envvar) if env_plat else None, InheritRule.NONE),
|
(self.env.get(plat_envvar) if env_plat else None, InheritRule.NONE),
|
||||||
ignore_empty=ignore_empty,
|
ignore_empty=ignore_empty,
|
||||||
list_sep=list_sep,
|
option_format=option_format,
|
||||||
table_format=table_format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _inner_fmt(k: str, v: Any, table: TableFmt) -> Iterator[str]:
|
|
||||||
quote_function = table.get("quote", lambda a: a)
|
|
||||||
|
|
||||||
if isinstance(v, list):
|
|
||||||
for inner_v in v:
|
|
||||||
qv = quote_function(inner_v)
|
|
||||||
yield table["item"].format(k=k, v=qv)
|
|
||||||
elif isinstance(v, bool):
|
|
||||||
qv = quote_function(str(v))
|
|
||||||
yield table["item"].format(k=k, v=qv)
|
|
||||||
else:
|
|
||||||
qv = quote_function(v)
|
|
||||||
yield table["item"].format(k=k, v=qv)
|
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
class Options:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -513,9 +594,11 @@ class Options:
|
|||||||
package_dir = args.package_dir
|
package_dir = args.package_dir
|
||||||
output_dir = args.output_dir
|
output_dir = args.output_dir
|
||||||
|
|
||||||
build_config = self.reader.get("build", env_plat=False, list_sep=" ") or "*"
|
build_config = (
|
||||||
skip_config = self.reader.get("skip", env_plat=False, list_sep=" ")
|
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "*"
|
||||||
test_skip = self.reader.get("test-skip", env_plat=False, list_sep=" ")
|
)
|
||||||
|
skip_config = self.reader.get("skip", env_plat=False, option_format=ListFormat(sep=" "))
|
||||||
|
test_skip = self.reader.get("test-skip", env_plat=False, option_format=ListFormat(sep=" "))
|
||||||
|
|
||||||
free_threaded_support = strtobool(
|
free_threaded_support = strtobool(
|
||||||
self.reader.get("free-threaded-support", env_plat=False, ignore_empty=True)
|
self.reader.get("free-threaded-support", env_plat=False, ignore_empty=True)
|
||||||
@@ -534,7 +617,7 @@ class Options:
|
|||||||
)
|
)
|
||||||
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
|
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
|
||||||
|
|
||||||
archs_config_str = args.archs or self.reader.get("archs", list_sep=" ")
|
archs_config_str = args.archs or self.reader.get("archs", option_format=ListFormat(sep=" "))
|
||||||
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
|
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
|
||||||
|
|
||||||
# Process `--only`
|
# Process `--only`
|
||||||
@@ -569,30 +652,33 @@ class Options:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with self.reader.identifier(identifier):
|
with self.reader.identifier(identifier):
|
||||||
before_all = self.reader.get("before-all", list_sep=" && ")
|
before_all = self.reader.get("before-all", option_format=ListFormat(sep=" && "))
|
||||||
|
|
||||||
environment_config = self.reader.get(
|
environment_config = self.reader.get("environment", option_format=EnvironmentFormat())
|
||||||
"environment", table_format={"item": '{k}="{v}"', "sep": " "}
|
environment_pass = self.reader.get(
|
||||||
|
"environment-pass", option_format=ListFormat(sep=" ")
|
||||||
|
).split()
|
||||||
|
before_build = self.reader.get("before-build", option_format=ListFormat(sep=" && "))
|
||||||
|
repair_command = self.reader.get(
|
||||||
|
"repair-wheel-command", option_format=ListFormat(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 = self.reader.get(
|
||||||
"config-settings",
|
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
|
||||||
table_format={"item": "{k}={v}", "sep": " ", "quote": shlex.quote},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
dependency_versions = self.reader.get("dependency-versions")
|
dependency_versions = self.reader.get("dependency-versions")
|
||||||
test_command = self.reader.get("test-command", list_sep=" && ")
|
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
||||||
before_test = self.reader.get("before-test", list_sep=" && ")
|
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
||||||
test_requires = self.reader.get("test-requires", list_sep=" ").split()
|
test_requires = self.reader.get(
|
||||||
test_extras = self.reader.get("test-extras", list_sep=",")
|
"test-requires", option_format=ListFormat(sep=" ")
|
||||||
|
).split()
|
||||||
|
test_extras = self.reader.get("test-extras", option_format=ListFormat(sep=","))
|
||||||
build_verbosity_str = self.reader.get("build-verbosity")
|
build_verbosity_str = self.reader.get("build-verbosity")
|
||||||
|
|
||||||
build_frontend_str = self.reader.get(
|
build_frontend_str = self.reader.get(
|
||||||
"build-frontend",
|
"build-frontend",
|
||||||
env_plat=False,
|
env_plat=False,
|
||||||
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
|
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||||
)
|
)
|
||||||
build_frontend: BuildFrontendConfig | None
|
build_frontend: BuildFrontendConfig | None
|
||||||
if not build_frontend_str or build_frontend_str == "default":
|
if not build_frontend_str or build_frontend_str == "default":
|
||||||
@@ -674,7 +760,7 @@ class Options:
|
|||||||
|
|
||||||
container_engine_str = self.reader.get(
|
container_engine_str = self.reader.get(
|
||||||
"container-engine",
|
"container-engine",
|
||||||
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
|
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import pytest
|
|||||||
from cibuildwheel import errors
|
from cibuildwheel import errors
|
||||||
from cibuildwheel.__main__ import get_build_identifiers, get_platform_module
|
from cibuildwheel.__main__ import get_build_identifiers, get_platform_module
|
||||||
from cibuildwheel.bashlex_eval import local_environment_executor
|
from cibuildwheel.bashlex_eval import local_environment_executor
|
||||||
from cibuildwheel.environment import parse_environment
|
|
||||||
from cibuildwheel.options import (
|
from cibuildwheel.options import (
|
||||||
CommandLineArguments,
|
CommandLineArguments,
|
||||||
Options,
|
Options,
|
||||||
@@ -66,7 +65,7 @@ def test_options_1(tmp_path, monkeypatch):
|
|||||||
|
|
||||||
default_build_options = options.build_options(identifier=None)
|
default_build_options = options.build_options(identifier=None)
|
||||||
|
|
||||||
assert default_build_options.environment == parse_environment('FOO="BAR"')
|
assert default_build_options.environment.as_dictionary(prev_environment={}) == {"FOO": "BAR"}
|
||||||
|
|
||||||
all_pinned_container_images = _get_pinned_container_images()
|
all_pinned_container_images = _get_pinned_container_images()
|
||||||
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
|
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
|
||||||
|
|||||||
@@ -4,7 +4,15 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel.options import ConfigOptionError, InheritRule, OptionsReader, _resolve_cascade
|
from cibuildwheel.options import (
|
||||||
|
EnvironmentFormat,
|
||||||
|
InheritRule,
|
||||||
|
ListFormat,
|
||||||
|
OptionsReader,
|
||||||
|
OptionsReaderError,
|
||||||
|
ShlexTableFormat,
|
||||||
|
_resolve_cascade,
|
||||||
|
)
|
||||||
|
|
||||||
PYPROJECT_1 = """
|
PYPROJECT_1 = """
|
||||||
[tool.cibuildwheel]
|
[tool.cibuildwheel]
|
||||||
@@ -37,42 +45,32 @@ def test_simple_settings(tmp_path, platform, fname):
|
|||||||
|
|
||||||
options_reader = OptionsReader(config_file_path, platform=platform, env={})
|
options_reader = OptionsReader(config_file_path, platform=platform, env={})
|
||||||
|
|
||||||
assert options_reader.get("build", env_plat=False, list_sep=" ") == "cp39*"
|
assert options_reader.get("build", option_format=ListFormat(" "), env_plat=False) == "cp39*"
|
||||||
|
|
||||||
assert options_reader.get("test-command") == "pyproject"
|
assert options_reader.get("test-command") == "pyproject"
|
||||||
assert options_reader.get("archs", list_sep=" ") == "auto"
|
assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto"
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-requires", list_sep=" ")
|
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||||
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Also testing options for support for both lists and tables
|
# Also testing options for support for both lists and tables
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
options_reader.get("environment", option_format=EnvironmentFormat())
|
||||||
== 'THING="OTHER" FOO="BAR"'
|
== 'THING="OTHER" FOO="BAR"'
|
||||||
)
|
)
|
||||||
assert (
|
assert options_reader.get("test-extras", option_format=ListFormat(",")) == "one,two"
|
||||||
options_reader.get(
|
|
||||||
"environment", list_sep="x", table_format={"item": '{k}="{v}"', "sep": " "}
|
|
||||||
)
|
|
||||||
== 'THING="OTHER" FOO="BAR"'
|
|
||||||
)
|
|
||||||
assert options_reader.get("test-extras", list_sep=",") == "one,two"
|
|
||||||
assert (
|
|
||||||
options_reader.get(
|
|
||||||
"test-extras", list_sep=",", table_format={"item": '{k}="{v}"', "sep": " "}
|
|
||||||
)
|
|
||||||
== "one,two"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert options_reader.get("manylinux-x86_64-image") == "manylinux1"
|
assert options_reader.get("manylinux-x86_64-image") == "manylinux1"
|
||||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||||
|
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
options_reader.get("environment", list_sep=" ")
|
# fails because the option is a table and the option_format only works with lists
|
||||||
|
options_reader.get("environment", option_format=ListFormat(" "))
|
||||||
|
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
options_reader.get("test-extras", table_format={"item": '{k}="{v}"', "sep": " "})
|
# fails because the option is a list and the option_format only works with tables
|
||||||
|
options_reader.get("test-extras", option_format=ShlexTableFormat())
|
||||||
|
|
||||||
|
|
||||||
def test_envvar_override(tmp_path, platform):
|
def test_envvar_override(tmp_path, platform):
|
||||||
@@ -91,14 +89,14 @@ def test_envvar_override(tmp_path, platform):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert options_reader.get("archs", list_sep=" ") == "auto"
|
assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto"
|
||||||
|
|
||||||
assert options_reader.get("build", list_sep=" ") == "cp38*"
|
assert options_reader.get("build") == "cp38*"
|
||||||
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
|
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
|
||||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-requires", list_sep=" ")
|
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||||
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
||||||
)
|
)
|
||||||
assert options_reader.get("test-command") == "mytest"
|
assert options_reader.get("test-command") == "mytest"
|
||||||
@@ -174,7 +172,7 @@ repairs-wheel-command = "repair-project-linux"
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ConfigOptionError) as excinfo:
|
with pytest.raises(OptionsReaderError) as excinfo:
|
||||||
OptionsReader(pyproject_toml, platform="linux", env={})
|
OptionsReader(pyproject_toml, platform="linux", env={})
|
||||||
|
|
||||||
assert "repair-wheel-command" in str(excinfo.value)
|
assert "repair-wheel-command" in str(excinfo.value)
|
||||||
@@ -191,7 +189,7 @@ repair_wheel_command = "repair-project-linux"
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ConfigOptionError) as excinfo:
|
with pytest.raises(OptionsReaderError) as excinfo:
|
||||||
OptionsReader(pyproject_toml, platform="linux", env={})
|
OptionsReader(pyproject_toml, platform="linux", env={})
|
||||||
|
|
||||||
assert "repair-wheel-command" in str(excinfo.value)
|
assert "repair-wheel-command" in str(excinfo.value)
|
||||||
@@ -205,7 +203,7 @@ def test_unexpected_table(tmp_path):
|
|||||||
repair-wheel-command = "repair-project-linux"
|
repair-wheel-command = "repair-project-linux"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
OptionsReader(pyproject_toml, platform="linux", env={})
|
OptionsReader(pyproject_toml, platform="linux", env={})
|
||||||
|
|
||||||
|
|
||||||
@@ -219,8 +217,8 @@ build = ["1", "2"]
|
|||||||
)
|
)
|
||||||
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
|
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
|
||||||
|
|
||||||
assert options_reader.get("build", list_sep=", ") == "1, 2"
|
assert options_reader.get("build", option_format=ListFormat(", ")) == "1, 2"
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
options_reader.get("build")
|
options_reader.get("build")
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +232,7 @@ manylinux-x86_64-image = "manylinux1"
|
|||||||
)
|
)
|
||||||
disallow = {"windows": {"manylinux-x86_64-image"}}
|
disallow = {"windows": {"manylinux-x86_64-image"}}
|
||||||
OptionsReader(pyproject_toml, platform="linux", disallow=disallow, env={})
|
OptionsReader(pyproject_toml, platform="linux", disallow=disallow, env={})
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
OptionsReader(pyproject_toml, platform="windows", disallow=disallow, env={})
|
OptionsReader(pyproject_toml, platform="windows", disallow=disallow, env={})
|
||||||
|
|
||||||
|
|
||||||
@@ -305,7 +303,7 @@ def test_resolve_cascade_merge_list(ignore_empty, rule):
|
|||||||
(["b1", "b2"], rule),
|
(["b1", "b2"], rule),
|
||||||
(None, InheritRule.NONE),
|
(None, InheritRule.NONE),
|
||||||
ignore_empty=ignore_empty,
|
ignore_empty=ignore_empty,
|
||||||
list_sep=" ",
|
option_format=ListFormat(" "),
|
||||||
)
|
)
|
||||||
|
|
||||||
if not ignore_empty:
|
if not ignore_empty:
|
||||||
@@ -326,24 +324,34 @@ def test_resolve_cascade_merge_dict(rule):
|
|||||||
(None, InheritRule.NONE),
|
(None, InheritRule.NONE),
|
||||||
({"value": "override"}, rule),
|
({"value": "override"}, rule),
|
||||||
(None, InheritRule.NONE),
|
(None, InheritRule.NONE),
|
||||||
table_format={"item": "{k}={v}", "sep": " "},
|
option_format=ShlexTableFormat(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if rule == InheritRule.PREPEND:
|
if rule == InheritRule.PREPEND:
|
||||||
assert answer == "value=override value=a1 base=b1"
|
assert answer == "value=a1 base=b1"
|
||||||
elif rule == InheritRule.NONE:
|
elif rule == InheritRule.NONE:
|
||||||
assert answer == "value=override"
|
assert answer == "value=override"
|
||||||
elif rule == InheritRule.APPEND:
|
elif rule == InheritRule.APPEND:
|
||||||
assert answer == "value=a1 base=b1 value=override"
|
assert answer == "value=override base=b1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_cascade_merge_strings():
|
||||||
|
answer = _resolve_cascade(
|
||||||
|
("value=a1 base=b1", InheritRule.NONE),
|
||||||
|
("value=override", InheritRule.APPEND),
|
||||||
|
option_format=ShlexTableFormat(),
|
||||||
|
)
|
||||||
|
assert answer == "value=override base=b1"
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_cascade_merge_different_types():
|
def test_resolve_cascade_merge_different_types():
|
||||||
answer = _resolve_cascade(
|
answer = _resolve_cascade(
|
||||||
({"value": "a1", "base": "b1"}, InheritRule.NONE),
|
("value=a1 base=b1", InheritRule.NONE),
|
||||||
({"value": "override"}, InheritRule.APPEND),
|
({"value": "override"}, InheritRule.APPEND),
|
||||||
table_format={"item": "{k}={v}", "sep": " "},
|
("extra_string_var=c1", InheritRule.APPEND),
|
||||||
|
option_format=ShlexTableFormat(),
|
||||||
)
|
)
|
||||||
assert answer == "value=a1 base=b1 value=override"
|
assert answer == "value=override base=b1 extra_string_var=c1"
|
||||||
|
|
||||||
|
|
||||||
PYPROJECT_2 = """
|
PYPROJECT_2 = """
|
||||||
@@ -387,28 +395,28 @@ def test_pyproject_2(tmp_path, platform):
|
|||||||
pyproject_toml.write_text(PYPROJECT_2)
|
pyproject_toml.write_text(PYPROJECT_2)
|
||||||
|
|
||||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
options_reader = OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
||||||
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
|
assert options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject"
|
||||||
|
|
||||||
with options_reader.identifier("random"):
|
with options_reader.identifier("random"):
|
||||||
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
|
assert options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject"
|
||||||
|
|
||||||
with options_reader.identifier("cp37-something"):
|
with options_reader.identifier("cp37-something"):
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-command", list_sep=" && ")
|
options_reader.get("test-command", option_format=ListFormat(" && "))
|
||||||
== "pyproject-override && override2 && pyproject"
|
== "pyproject-override && override2 && pyproject"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
options_reader.get("environment", option_format=EnvironmentFormat())
|
||||||
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
||||||
)
|
)
|
||||||
|
|
||||||
with options_reader.identifier("cp37-final"):
|
with options_reader.identifier("cp37-final"):
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-command", list_sep=" && ")
|
options_reader.get("test-command", option_format=ListFormat(" && "))
|
||||||
== "extra-prepend && pyproject-override && override2 && pyproject && pyproject-finalize && finalize2 && extra-finalize"
|
== "extra-prepend && pyproject-override && override2 && pyproject && pyproject-finalize && finalize2 && extra-finalize"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
options_reader.get("environment", option_format=EnvironmentFormat())
|
||||||
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -426,7 +434,7 @@ test-command = "pyproject-override"
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(ConfigOptionError):
|
with pytest.raises(OptionsReaderError):
|
||||||
OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
||||||
|
|
||||||
|
|
||||||
@@ -442,8 +450,8 @@ other = ["two", "three"]
|
|||||||
|
|
||||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("config-settings", table_format={"item": '{k}="{v}"', "sep": " "})
|
options_reader.get("config-settings", option_format=ShlexTableFormat(pair_sep="=", sep=" "))
|
||||||
== 'example="one" other="two" other="three"'
|
== "example=one other=two other=three"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -458,10 +466,8 @@ def test_pip_config_settings(tmp_path):
|
|||||||
|
|
||||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||||
assert (
|
assert (
|
||||||
options_reader.get(
|
options_reader.get("config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="="))
|
||||||
"config-settings", table_format={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
|
== "--build-option=--use-mypyc"
|
||||||
)
|
|
||||||
== "--config-settings='--build-option=\"--use-mypyc\"'"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -471,12 +477,16 @@ def test_overrides_inherit(tmp_path):
|
|||||||
"""\
|
"""\
|
||||||
[tool.cibuildwheel]
|
[tool.cibuildwheel]
|
||||||
before-all = ["before-all"]
|
before-all = ["before-all"]
|
||||||
|
config-settings = {key1="value1", key2="value2", empty=""}
|
||||||
|
|
||||||
[[tool.cibuildwheel.overrides]]
|
[[tool.cibuildwheel.overrides]]
|
||||||
select = "cp37*"
|
select = "cp37*"
|
||||||
inherit.before-all = "append"
|
inherit.before-all = "append"
|
||||||
before-all = ["override1"]
|
before-all = ["override1"]
|
||||||
|
|
||||||
|
inherit.config-settings = "append"
|
||||||
|
config-settings = {key3="value3", key2="override2"}
|
||||||
|
|
||||||
[[tool.cibuildwheel.overrides]]
|
[[tool.cibuildwheel.overrides]]
|
||||||
select = "cp37*"
|
select = "cp37*"
|
||||||
inherit.before-all = "prepend"
|
inherit.before-all = "prepend"
|
||||||
@@ -486,9 +496,17 @@ before-all = ["override2"]
|
|||||||
|
|
||||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||||
with options_reader.identifier("cp38-something"):
|
with options_reader.identifier("cp38-something"):
|
||||||
assert options_reader.get("before-all", list_sep=" && ") == "before-all"
|
assert options_reader.get("before-all", option_format=ListFormat(" && ")) == "before-all"
|
||||||
|
assert (
|
||||||
|
options_reader.get("config-settings", option_format=ShlexTableFormat())
|
||||||
|
== "key1=value1 key2=value2 empty=''"
|
||||||
|
)
|
||||||
with options_reader.identifier("cp37-something"):
|
with options_reader.identifier("cp37-something"):
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("before-all", list_sep=" && ")
|
options_reader.get("before-all", option_format=ListFormat(" && "))
|
||||||
== "override2 && before-all && override1"
|
== "override2 && before-all && override1"
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
options_reader.get("config-settings", option_format=ShlexTableFormat())
|
||||||
|
== "key1=value1 key2=override2 empty='' key3=value3"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user