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 shlex
|
||||
import textwrap
|
||||
from collections.abc import Callable, Generator, Iterable, Iterator, Set
|
||||
from collections.abc import Generator, Iterable, Set
|
||||
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 . import errors
|
||||
from ._compat import tomllib
|
||||
from ._compat.typing import NotRequired, assert_never
|
||||
from ._compat.typing import assert_never
|
||||
from .architecture import Architecture
|
||||
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
|
||||
from .logger import log
|
||||
@@ -118,13 +118,16 @@ class BuildOptions:
|
||||
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)
|
||||
class Override:
|
||||
select_pattern: str
|
||||
options: dict[str, Setting]
|
||||
options: dict[str, SettingValue]
|
||||
inherit: dict[str, InheritRule]
|
||||
|
||||
|
||||
@@ -137,22 +140,123 @@ DISALLOWED_OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
class TableFmt(TypedDict):
|
||||
# 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):
|
||||
class OptionsReaderError(errors.ConfigurationError):
|
||||
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):
|
||||
NONE = enum.auto()
|
||||
APPEND = enum.auto()
|
||||
@@ -160,10 +264,9 @@ class InheritRule(enum.Enum):
|
||||
|
||||
|
||||
def _resolve_cascade(
|
||||
*pairs: tuple[Setting | None, InheritRule],
|
||||
*pairs: tuple[SettingValue | None, InheritRule],
|
||||
ignore_empty: bool = False,
|
||||
list_sep: str | None = None,
|
||||
table_format: TableFmt | None = None,
|
||||
option_format: OptionFormat | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Given a cascade of values with inherit rules, resolve them into a single
|
||||
@@ -186,13 +289,6 @@ def _resolve_cascade(
|
||||
|
||||
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:
|
||||
if value is None:
|
||||
continue
|
||||
@@ -200,14 +296,9 @@ def _resolve_cascade(
|
||||
if ignore_empty and not value and value is not False:
|
||||
continue
|
||||
|
||||
value_string = _stringify_setting(value, list_sep, table_format)
|
||||
value_string = _stringify_setting(value, option_format=option_format)
|
||||
|
||||
result = _merge_values(
|
||||
result,
|
||||
value_string,
|
||||
rule=rule,
|
||||
merge_sep=merge_sep,
|
||||
)
|
||||
result = _apply_inherit_rule(result, value_string, rule=rule, option_format=option_format)
|
||||
|
||||
if result is None:
|
||||
msg = "a setting should at least have a default value"
|
||||
@@ -217,7 +308,9 @@ def _resolve_cascade(
|
||||
|
||||
|
||||
# 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:
|
||||
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
|
||||
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}"
|
||||
raise ConfigOptionError(msg)
|
||||
raise OptionsReaderError(msg)
|
||||
|
||||
if rule == InheritRule.APPEND:
|
||||
return f"{before}{merge_sep}{after}"
|
||||
return option_format.merge_values(before, after)
|
||||
elif rule == InheritRule.PREPEND:
|
||||
return f"{after}{merge_sep}{before}"
|
||||
return option_format.merge_values(after, before)
|
||||
else:
|
||||
assert_never(rule)
|
||||
|
||||
|
||||
def _stringify_setting(
|
||||
setting: Setting, list_sep: str | None, table_format: TableFmt | None
|
||||
setting: SettingValue,
|
||||
option_format: OptionFormat | None,
|
||||
) -> str:
|
||||
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"
|
||||
raise ConfigOptionError(msg)
|
||||
return table_format["sep"].join(
|
||||
item for k, v in setting.items() for item in _inner_fmt(k, v, table_format)
|
||||
)
|
||||
raise OptionsReaderError(msg) from None
|
||||
|
||||
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"
|
||||
raise ConfigOptionError(msg)
|
||||
return list_sep.join(setting)
|
||||
raise OptionsReaderError(msg) from None
|
||||
|
||||
if isinstance(setting, (bool, int)):
|
||||
return str(setting)
|
||||
@@ -323,14 +421,14 @@ class OptionsReader:
|
||||
if config_overrides is not None:
|
||||
if not isinstance(config_overrides, list):
|
||||
msg = "'tool.cibuildwheel.overrides' must be a list"
|
||||
raise ConfigOptionError(msg)
|
||||
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 ConfigOptionError(msg)
|
||||
raise OptionsReaderError(msg)
|
||||
|
||||
if isinstance(select, list):
|
||||
select = " ".join(select)
|
||||
@@ -340,7 +438,7 @@ class OptionsReader:
|
||||
i in {"none", "append", "prepend"} for i in inherit.values()
|
||||
):
|
||||
msg = "'inherit' must be a dict containing only {'none', 'append', 'prepend'} values"
|
||||
raise ConfigOptionError(msg)
|
||||
raise OptionsReaderError(msg)
|
||||
|
||||
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)
|
||||
if matches:
|
||||
msg += f" Perhaps you meant {matches[0]!r}?"
|
||||
raise ConfigOptionError(msg)
|
||||
raise OptionsReaderError(msg)
|
||||
|
||||
def _validate_platform_option(self, name: str) -> None:
|
||||
"""
|
||||
@@ -368,7 +466,7 @@ class OptionsReader:
|
||||
disallowed_platform_options = self.disallow.get(self.platform, set())
|
||||
if name 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()
|
||||
|
||||
@@ -377,7 +475,7 @@ class OptionsReader:
|
||||
matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7)
|
||||
if matches:
|
||||
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]]:
|
||||
"""
|
||||
@@ -412,8 +510,7 @@ class OptionsReader:
|
||||
name: str,
|
||||
*,
|
||||
env_plat: bool = True,
|
||||
list_sep: str | None = None,
|
||||
table_format: TableFmt | None = None,
|
||||
option_format: OptionFormat | None = None,
|
||||
ignore_empty: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
@@ -429,7 +526,7 @@ class OptionsReader:
|
||||
|
||||
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."
|
||||
raise ConfigOptionError(msg)
|
||||
raise OptionsReaderError(msg)
|
||||
|
||||
# Environment variable form
|
||||
envvar = f"CIBW_{name.upper().replace('-', '_')}"
|
||||
@@ -449,26 +546,10 @@ class OptionsReader:
|
||||
(self.env.get(envvar), InheritRule.NONE),
|
||||
(self.env.get(plat_envvar) if env_plat else None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
list_sep=list_sep,
|
||||
table_format=table_format,
|
||||
option_format=option_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:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -513,9 +594,11 @@ class Options:
|
||||
package_dir = args.package_dir
|
||||
output_dir = args.output_dir
|
||||
|
||||
build_config = self.reader.get("build", env_plat=False, list_sep=" ") or "*"
|
||||
skip_config = self.reader.get("skip", env_plat=False, list_sep=" ")
|
||||
test_skip = self.reader.get("test-skip", env_plat=False, list_sep=" ")
|
||||
build_config = (
|
||||
self.reader.get("build", env_plat=False, option_format=ListFormat(sep=" ")) or "*"
|
||||
)
|
||||
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(
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# Process `--only`
|
||||
@@ -569,30 +652,33 @@ class Options:
|
||||
"""
|
||||
|
||||
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", table_format={"item": '{k}="{v}"', "sep": " "}
|
||||
environment_config = self.reader.get("environment", option_format=EnvironmentFormat())
|
||||
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",
|
||||
table_format={"item": "{k}={v}", "sep": " ", "quote": shlex.quote},
|
||||
"config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")
|
||||
)
|
||||
|
||||
dependency_versions = self.reader.get("dependency-versions")
|
||||
test_command = self.reader.get("test-command", list_sep=" && ")
|
||||
before_test = self.reader.get("before-test", list_sep=" && ")
|
||||
test_requires = self.reader.get("test-requires", list_sep=" ").split()
|
||||
test_extras = self.reader.get("test-extras", list_sep=",")
|
||||
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
||||
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
||||
test_requires = self.reader.get(
|
||||
"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_frontend_str = self.reader.get(
|
||||
"build-frontend",
|
||||
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
|
||||
if not build_frontend_str or build_frontend_str == "default":
|
||||
@@ -674,7 +760,7 @@ class Options:
|
||||
|
||||
container_engine_str = self.reader.get(
|
||||
"container-engine",
|
||||
table_format={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
|
||||
option_format=ShlexTableFormat(sep="; ", pair_sep=":", allow_merge=False),
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -10,7 +10,6 @@ import pytest
|
||||
from cibuildwheel import errors
|
||||
from cibuildwheel.__main__ import get_build_identifiers, get_platform_module
|
||||
from cibuildwheel.bashlex_eval import local_environment_executor
|
||||
from cibuildwheel.environment import parse_environment
|
||||
from cibuildwheel.options import (
|
||||
CommandLineArguments,
|
||||
Options,
|
||||
@@ -66,7 +65,7 @@ def test_options_1(tmp_path, monkeypatch):
|
||||
|
||||
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()
|
||||
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
|
||||
|
||||
@@ -4,7 +4,15 @@ from pathlib import Path
|
||||
|
||||
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 = """
|
||||
[tool.cibuildwheel]
|
||||
@@ -37,42 +45,32 @@ def test_simple_settings(tmp_path, platform, fname):
|
||||
|
||||
options_reader = OptionsReader(config_file_path, platform=platform, env={})
|
||||
|
||||
assert options_reader.get("build", env_plat=False, 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("archs", list_sep=" ") == "auto"
|
||||
assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto"
|
||||
assert (
|
||||
options_reader.get("test-requires", list_sep=" ")
|
||||
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
||||
)
|
||||
|
||||
# Also testing options for support for both lists and tables
|
||||
assert (
|
||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get("environment", option_format=EnvironmentFormat())
|
||||
== 'THING="OTHER" FOO="BAR"'
|
||||
)
|
||||
assert (
|
||||
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("test-extras", option_format=ListFormat(",")) == "one,two"
|
||||
|
||||
assert options_reader.get("manylinux-x86_64-image") == "manylinux1"
|
||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||
|
||||
with pytest.raises(ConfigOptionError):
|
||||
options_reader.get("environment", list_sep=" ")
|
||||
with pytest.raises(OptionsReaderError):
|
||||
# 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):
|
||||
options_reader.get("test-extras", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
with pytest.raises(OptionsReaderError):
|
||||
# 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):
|
||||
@@ -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-i686-image") == "manylinux2014"
|
||||
|
||||
assert (
|
||||
options_reader.get("test-requires", list_sep=" ")
|
||||
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
||||
)
|
||||
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={})
|
||||
|
||||
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={})
|
||||
|
||||
assert "repair-wheel-command" in str(excinfo.value)
|
||||
@@ -205,7 +203,7 @@ def test_unexpected_table(tmp_path):
|
||||
repair-wheel-command = "repair-project-linux"
|
||||
"""
|
||||
)
|
||||
with pytest.raises(ConfigOptionError):
|
||||
with pytest.raises(OptionsReaderError):
|
||||
OptionsReader(pyproject_toml, platform="linux", env={})
|
||||
|
||||
|
||||
@@ -219,8 +217,8 @@ build = ["1", "2"]
|
||||
)
|
||||
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
|
||||
|
||||
assert options_reader.get("build", list_sep=", ") == "1, 2"
|
||||
with pytest.raises(ConfigOptionError):
|
||||
assert options_reader.get("build", option_format=ListFormat(", ")) == "1, 2"
|
||||
with pytest.raises(OptionsReaderError):
|
||||
options_reader.get("build")
|
||||
|
||||
|
||||
@@ -234,7 +232,7 @@ manylinux-x86_64-image = "manylinux1"
|
||||
)
|
||||
disallow = {"windows": {"manylinux-x86_64-image"}}
|
||||
OptionsReader(pyproject_toml, platform="linux", disallow=disallow, env={})
|
||||
with pytest.raises(ConfigOptionError):
|
||||
with pytest.raises(OptionsReaderError):
|
||||
OptionsReader(pyproject_toml, platform="windows", disallow=disallow, env={})
|
||||
|
||||
|
||||
@@ -305,7 +303,7 @@ def test_resolve_cascade_merge_list(ignore_empty, rule):
|
||||
(["b1", "b2"], rule),
|
||||
(None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
list_sep=" ",
|
||||
option_format=ListFormat(" "),
|
||||
)
|
||||
|
||||
if not ignore_empty:
|
||||
@@ -326,24 +324,34 @@ def test_resolve_cascade_merge_dict(rule):
|
||||
(None, InheritRule.NONE),
|
||||
({"value": "override"}, rule),
|
||||
(None, InheritRule.NONE),
|
||||
table_format={"item": "{k}={v}", "sep": " "},
|
||||
option_format=ShlexTableFormat(),
|
||||
)
|
||||
|
||||
if rule == InheritRule.PREPEND:
|
||||
assert answer == "value=override value=a1 base=b1"
|
||||
assert answer == "value=a1 base=b1"
|
||||
elif rule == InheritRule.NONE:
|
||||
assert answer == "value=override"
|
||||
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():
|
||||
answer = _resolve_cascade(
|
||||
({"value": "a1", "base": "b1"}, InheritRule.NONE),
|
||||
("value=a1 base=b1", InheritRule.NONE),
|
||||
({"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 = """
|
||||
@@ -387,28 +395,28 @@ def test_pyproject_2(tmp_path, platform):
|
||||
pyproject_toml.write_text(PYPROJECT_2)
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
||||
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
|
||||
assert options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject"
|
||||
|
||||
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"):
|
||||
assert (
|
||||
options_reader.get("test-command", list_sep=" && ")
|
||||
options_reader.get("test-command", option_format=ListFormat(" && "))
|
||||
== "pyproject-override && override2 && pyproject"
|
||||
)
|
||||
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"'
|
||||
)
|
||||
|
||||
with options_reader.identifier("cp37-final"):
|
||||
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"
|
||||
)
|
||||
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"'
|
||||
)
|
||||
|
||||
@@ -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={})
|
||||
|
||||
|
||||
@@ -442,8 +450,8 @@ other = ["two", "three"]
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||
assert (
|
||||
options_reader.get("config-settings", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
== 'example="one" other="two" other="three"'
|
||||
options_reader.get("config-settings", option_format=ShlexTableFormat(pair_sep="=", sep=" "))
|
||||
== "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={})
|
||||
assert (
|
||||
options_reader.get(
|
||||
"config-settings", table_format={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
|
||||
)
|
||||
== "--config-settings='--build-option=\"--use-mypyc\"'"
|
||||
options_reader.get("config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="="))
|
||||
== "--build-option=--use-mypyc"
|
||||
)
|
||||
|
||||
|
||||
@@ -471,12 +477,16 @@ def test_overrides_inherit(tmp_path):
|
||||
"""\
|
||||
[tool.cibuildwheel]
|
||||
before-all = ["before-all"]
|
||||
config-settings = {key1="value1", key2="value2", empty=""}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.before-all = "append"
|
||||
before-all = ["override1"]
|
||||
|
||||
inherit.config-settings = "append"
|
||||
config-settings = {key3="value3", key2="override2"}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.before-all = "prepend"
|
||||
@@ -486,9 +496,17 @@ before-all = ["override2"]
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||
with options_reader.identifier("cp38-something"):
|
||||
assert options_reader.get("before-all", list_sep=" && ") == "before-all"
|
||||
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"):
|
||||
assert (
|
||||
options_reader.get("before-all", list_sep=" && ")
|
||||
options_reader.get("before-all", option_format=ListFormat(" && "))
|
||||
== "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