feat: add inherit to override (#1730)
* feat: add inherit to override Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * refactor: use dict and support prepend Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Refactor: simplifying by splitting the responsibilities of _dig_first * Refactor to allow merging of string settings, and preserve table cascades * docs: add some docs for inherit Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Apply suggestions from code review Co-authored-by: Joe Rickerby <joerick@mac.com> --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
parent
ae2451a199
commit
8f58f71c2e
@@ -347,3 +347,71 @@ def test_build_frontend_option(tmp_path: Path, toml_assignment, result_name, res
|
||||
assert parsed_build_frontend.args == result_args
|
||||
else:
|
||||
assert parsed_build_frontend is None
|
||||
|
||||
|
||||
def test_override_inherit_environment(tmp_path: Path):
|
||||
args = CommandLineArguments.defaults()
|
||||
args.package_dir = tmp_path
|
||||
|
||||
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||
pyproject_toml.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
[tool.cibuildwheel]
|
||||
environment = {FOO="BAR", "HAM"="EGGS"}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.environment = "append"
|
||||
environment = {FOO="BAZ", "PYTHON"="MONTY"}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
options = Options(platform="linux", command_line_arguments=args, env={})
|
||||
parsed_environment = options.build_options(identifier=None).environment
|
||||
assert parsed_environment.as_dictionary(prev_environment={}) == {
|
||||
"FOO": "BAR",
|
||||
"HAM": "EGGS",
|
||||
}
|
||||
|
||||
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
|
||||
prev_environment={}
|
||||
) == {
|
||||
"FOO": "BAZ",
|
||||
"HAM": "EGGS",
|
||||
"PYTHON": "MONTY",
|
||||
}
|
||||
|
||||
|
||||
def test_override_inherit_environment_with_references(tmp_path: Path):
|
||||
args = CommandLineArguments.defaults()
|
||||
args.package_dir = tmp_path
|
||||
|
||||
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||
pyproject_toml.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
[tool.cibuildwheel]
|
||||
environment = {PATH="/opt/bin:$PATH"}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.environment = "append"
|
||||
environment = {PATH="/opt/local/bin:$PATH"}
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
options = Options(platform="linux", command_line_arguments=args, env={"MONTY": "PYTHON"})
|
||||
parsed_environment = options.build_options(identifier=None).environment
|
||||
prev_environment = {"PATH": "/usr/bin:/bin"}
|
||||
assert parsed_environment.as_dictionary(prev_environment=prev_environment) == {
|
||||
"PATH": "/opt/bin:/usr/bin:/bin",
|
||||
}
|
||||
|
||||
assert options.build_options("cp37-manylinux_x86_64").environment.as_dictionary(
|
||||
prev_environment=prev_environment
|
||||
) == {
|
||||
"PATH": "/opt/local/bin:/opt/bin:/usr/bin:/bin",
|
||||
}
|
||||
|
||||
+157
-45
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from cibuildwheel.options import ConfigOptionError, OptionsReader, _dig_first
|
||||
from cibuildwheel.options import ConfigOptionError, InheritRule, OptionsReader, _resolve_cascade
|
||||
|
||||
PYPROJECT_1 = """
|
||||
[tool.cibuildwheel]
|
||||
@@ -37,27 +37,31 @@ def test_simple_settings(tmp_path, platform, fname):
|
||||
|
||||
options_reader = OptionsReader(config_file_path, platform=platform, env={})
|
||||
|
||||
assert options_reader.get("build", env_plat=False, sep=" ") == "cp39*"
|
||||
assert options_reader.get("build", env_plat=False, list_sep=" ") == "cp39*"
|
||||
|
||||
assert options_reader.get("test-command") == "pyproject"
|
||||
assert options_reader.get("archs", sep=" ") == "auto"
|
||||
assert options_reader.get("archs", list_sep=" ") == "auto"
|
||||
assert (
|
||||
options_reader.get("test-requires", sep=" ")
|
||||
options_reader.get("test-requires", list_sep=" ")
|
||||
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
||||
)
|
||||
|
||||
# Also testing options for support for both lists and tables
|
||||
assert (
|
||||
options_reader.get("environment", table={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
== 'THING="OTHER" FOO="BAR"'
|
||||
)
|
||||
assert (
|
||||
options_reader.get("environment", sep="x", table={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get(
|
||||
"environment", list_sep="x", table_format={"item": '{k}="{v}"', "sep": " "}
|
||||
)
|
||||
== 'THING="OTHER" FOO="BAR"'
|
||||
)
|
||||
assert options_reader.get("test-extras", sep=",") == "one,two"
|
||||
assert options_reader.get("test-extras", list_sep=",") == "one,two"
|
||||
assert (
|
||||
options_reader.get("test-extras", sep=",", table={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get(
|
||||
"test-extras", list_sep=",", table_format={"item": '{k}="{v}"', "sep": " "}
|
||||
)
|
||||
== "one,two"
|
||||
)
|
||||
|
||||
@@ -65,10 +69,10 @@ def test_simple_settings(tmp_path, platform, fname):
|
||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||
|
||||
with pytest.raises(ConfigOptionError):
|
||||
options_reader.get("environment", sep=" ")
|
||||
options_reader.get("environment", list_sep=" ")
|
||||
|
||||
with pytest.raises(ConfigOptionError):
|
||||
options_reader.get("test-extras", table={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get("test-extras", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
|
||||
|
||||
def test_envvar_override(tmp_path, platform):
|
||||
@@ -87,14 +91,14 @@ def test_envvar_override(tmp_path, platform):
|
||||
},
|
||||
)
|
||||
|
||||
assert options_reader.get("archs", sep=" ") == "auto"
|
||||
assert options_reader.get("archs", list_sep=" ") == "auto"
|
||||
|
||||
assert options_reader.get("build", sep=" ") == "cp38*"
|
||||
assert options_reader.get("build", list_sep=" ") == "cp38*"
|
||||
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
|
||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||
|
||||
assert (
|
||||
options_reader.get("test-requires", sep=" ")
|
||||
options_reader.get("test-requires", list_sep=" ")
|
||||
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
||||
)
|
||||
assert options_reader.get("test-command") == "mytest"
|
||||
@@ -215,7 +219,7 @@ build = ["1", "2"]
|
||||
)
|
||||
options_reader = OptionsReader(pyproject_toml, platform="linux", env={})
|
||||
|
||||
assert options_reader.get("build", sep=", ") == "1, 2"
|
||||
assert options_reader.get("build", list_sep=", ") == "1, 2"
|
||||
with pytest.raises(ConfigOptionError):
|
||||
options_reader.get("build")
|
||||
|
||||
@@ -262,47 +266,92 @@ manylinux-x86_64-image = ""
|
||||
assert options_reader.get("manylinux-aarch64-image", ignore_empty=True) == "manylinux1"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_empty", [True, False])
|
||||
def test_dig_first(ignore_empty):
|
||||
d1 = {"random": "thing"}
|
||||
d2 = {"this": "that", "empty": ""}
|
||||
d3 = {"other": "hi"}
|
||||
d4 = {"this": "d4", "empty": "not"}
|
||||
|
||||
answer = _dig_first(
|
||||
(d1, "empty"),
|
||||
(d2, "empty"),
|
||||
(d3, "empty"),
|
||||
(d4, "empty"),
|
||||
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
|
||||
def test_resolve_cascade(ignore_empty):
|
||||
answer = _resolve_cascade(
|
||||
("not", InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
("", InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
)
|
||||
assert answer == ("not" if ignore_empty else "")
|
||||
|
||||
answer = _dig_first(
|
||||
(d1, "this"),
|
||||
(d2, "this"),
|
||||
(d3, "this"),
|
||||
(d4, "this"),
|
||||
answer = _resolve_cascade(
|
||||
("d4", InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
("that", InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
)
|
||||
assert answer == "that"
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_dig_first(
|
||||
(d1, "this"),
|
||||
(d2, "other"),
|
||||
(d3, "this"),
|
||||
(d4, "other"),
|
||||
with pytest.raises(ValueError, match="a setting should at least have a default value"):
|
||||
_resolve_cascade(
|
||||
(None, InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"])
|
||||
@pytest.mark.parametrize("rule", [InheritRule.PREPEND, InheritRule.NONE, InheritRule.APPEND])
|
||||
def test_resolve_cascade_merge_list(ignore_empty, rule):
|
||||
answer = _resolve_cascade(
|
||||
(["a1", "a2"], InheritRule.NONE),
|
||||
([], InheritRule.NONE),
|
||||
(["b1", "b2"], rule),
|
||||
(None, InheritRule.NONE),
|
||||
ignore_empty=ignore_empty,
|
||||
list_sep=" ",
|
||||
)
|
||||
|
||||
if not ignore_empty:
|
||||
assert answer == "b1 b2"
|
||||
else:
|
||||
if rule == InheritRule.PREPEND:
|
||||
assert answer == "b1 b2 a1 a2"
|
||||
elif rule == InheritRule.NONE:
|
||||
assert answer == "b1 b2"
|
||||
elif rule == InheritRule.APPEND:
|
||||
assert answer == "a1 a2 b1 b2"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rule", [InheritRule.PREPEND, InheritRule.NONE, InheritRule.APPEND])
|
||||
def test_resolve_cascade_merge_dict(rule):
|
||||
answer = _resolve_cascade(
|
||||
({"value": "a1", "base": "b1"}, InheritRule.NONE),
|
||||
(None, InheritRule.NONE),
|
||||
({"value": "override"}, rule),
|
||||
(None, InheritRule.NONE),
|
||||
table_format={"item": "{k}={v}", "sep": " "},
|
||||
)
|
||||
|
||||
if rule == InheritRule.PREPEND:
|
||||
assert answer == "value=override value=a1 base=b1"
|
||||
elif rule == InheritRule.NONE:
|
||||
assert answer == "value=override"
|
||||
elif rule == InheritRule.APPEND:
|
||||
assert answer == "value=a1 base=b1 value=override"
|
||||
|
||||
|
||||
def test_resolve_cascade_merge_different_types():
|
||||
answer = _resolve_cascade(
|
||||
({"value": "a1", "base": "b1"}, InheritRule.NONE),
|
||||
({"value": "override"}, InheritRule.APPEND),
|
||||
table_format={"item": "{k}={v}", "sep": " "},
|
||||
)
|
||||
assert answer == "value=a1 base=b1 value=override"
|
||||
|
||||
|
||||
PYPROJECT_2 = """
|
||||
[tool.cibuildwheel]
|
||||
build = ["cp38*", "cp37*"]
|
||||
environment = {FOO="BAR"}
|
||||
environment = {FOO="BAR", "HAM"="EGGS"}
|
||||
|
||||
test-command = "pyproject"
|
||||
test-command = ["pyproject"]
|
||||
|
||||
manylinux-x86_64-image = "manylinux1"
|
||||
|
||||
@@ -311,8 +360,25 @@ test-requires = "else"
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
test-command = "pyproject-override"
|
||||
inherit = {test-command="prepend", environment="append"}
|
||||
test-command = ["pyproject-override", "override2"]
|
||||
manylinux-x86_64-image = "manylinux2014"
|
||||
environment = {FOO="BAZ", "PYTHON"="MONTY"}
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*-final"
|
||||
inherit = {test-command="append"}
|
||||
test-command = ["pyproject-finalize", "finalize2"]
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*-final"
|
||||
inherit = {test-command="append"}
|
||||
test-command = ["extra-finalize"]
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "*-final"
|
||||
inherit = {test-command="prepend"}
|
||||
test-command = ["extra-prepend"]
|
||||
"""
|
||||
|
||||
|
||||
@@ -321,13 +387,30 @@ def test_pyproject_2(tmp_path, platform):
|
||||
pyproject_toml.write_text(PYPROJECT_2)
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform=platform, env={})
|
||||
assert options_reader.get("test-command") == "pyproject"
|
||||
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
|
||||
|
||||
with options_reader.identifier("random"):
|
||||
assert options_reader.get("test-command") == "pyproject"
|
||||
assert options_reader.get("test-command", list_sep=" && ") == "pyproject"
|
||||
|
||||
with options_reader.identifier("cp37-something"):
|
||||
assert options_reader.get("test-command") == "pyproject-override"
|
||||
assert (
|
||||
options_reader.get("test-command", list_sep=" && ")
|
||||
== "pyproject-override && override2 && pyproject"
|
||||
)
|
||||
assert (
|
||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
||||
)
|
||||
|
||||
with options_reader.identifier("cp37-final"):
|
||||
assert (
|
||||
options_reader.get("test-command", list_sep=" && ")
|
||||
== "extra-prepend && pyproject-override && override2 && pyproject && pyproject-finalize && finalize2 && extra-finalize"
|
||||
)
|
||||
assert (
|
||||
options_reader.get("environment", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
== 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"'
|
||||
)
|
||||
|
||||
|
||||
def test_overrides_not_a_list(tmp_path, platform):
|
||||
@@ -359,7 +442,7 @@ other = ["two", "three"]
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||
assert (
|
||||
options_reader.get("config-settings", table={"item": '{k}="{v}"', "sep": " "})
|
||||
options_reader.get("config-settings", table_format={"item": '{k}="{v}"', "sep": " "})
|
||||
== 'example="one" other="two" other="three"'
|
||||
)
|
||||
|
||||
@@ -376,7 +459,36 @@ def test_pip_config_settings(tmp_path):
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||
assert (
|
||||
options_reader.get(
|
||||
"config-settings", table={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
|
||||
"config-settings", table_format={"item": "--config-settings='{k}=\"{v}\"'", "sep": " "}
|
||||
)
|
||||
== "--config-settings='--build-option=\"--use-mypyc\"'"
|
||||
)
|
||||
|
||||
|
||||
def test_overrides_inherit(tmp_path):
|
||||
pyproject_toml: Path = tmp_path / "pyproject.toml"
|
||||
pyproject_toml.write_text(
|
||||
"""\
|
||||
[tool.cibuildwheel]
|
||||
before-all = ["before-all"]
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.before-all = "append"
|
||||
before-all = ["override1"]
|
||||
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
select = "cp37*"
|
||||
inherit.before-all = "prepend"
|
||||
before-all = ["override2"]
|
||||
"""
|
||||
)
|
||||
|
||||
options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={})
|
||||
with options_reader.identifier("cp38-something"):
|
||||
assert options_reader.get("before-all", list_sep=" && ") == "before-all"
|
||||
with options_reader.identifier("cp37-something"):
|
||||
assert (
|
||||
options_reader.get("before-all", list_sep=" && ")
|
||||
== "override2 && before-all && override1"
|
||||
)
|
||||
|
||||
@@ -72,6 +72,50 @@ def test_overrides_only_select():
|
||||
validator(example)
|
||||
|
||||
|
||||
def test_overrides_valid_inherit():
|
||||
example = tomllib.loads(
|
||||
"""
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
inherit.repair-wheel-command = "append"
|
||||
select = "somestring"
|
||||
repair-wheel-command = ["something"]
|
||||
"""
|
||||
)
|
||||
|
||||
validator = validate_pyproject.api.Validator()
|
||||
assert validator(example) is not None
|
||||
|
||||
|
||||
def test_overrides_invalid_inherit():
|
||||
example = tomllib.loads(
|
||||
"""
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
inherit.something = "append"
|
||||
select = "somestring"
|
||||
repair-wheel-command = "something"
|
||||
"""
|
||||
)
|
||||
|
||||
validator = validate_pyproject.api.Validator()
|
||||
with pytest.raises(validate_pyproject.error_reporting.ValidationError):
|
||||
validator(example)
|
||||
|
||||
|
||||
def test_overrides_invalid_inherit_value():
|
||||
example = tomllib.loads(
|
||||
"""
|
||||
[[tool.cibuildwheel.overrides]]
|
||||
inherit.repair-wheel-command = "nothing"
|
||||
select = "somestring"
|
||||
repair-wheel-command = "something"
|
||||
"""
|
||||
)
|
||||
|
||||
validator = validate_pyproject.api.Validator()
|
||||
with pytest.raises(validate_pyproject.error_reporting.ValidationError):
|
||||
validator(example)
|
||||
|
||||
|
||||
def test_docs_examples():
|
||||
"""
|
||||
Parse out all the configuration examples, build valid TOML out of them, and
|
||||
|
||||
Reference in New Issue
Block a user