from __future__ import annotations import shlex from typing import Any, cast import pytest from cibuildwheel.options import ( EnvironmentFormat, InheritRule, ListFormat, OptionsReader, OptionsReaderError, ShlexTableFormat, _resolve_cascade, ) TYPE_CHECKING = False if TYPE_CHECKING: from pathlib import Path from cibuildwheel.typing import PlatformName PYPROJECT_1 = """ [tool.cibuildwheel] build = "cp39*" environment = {THING = "OTHER", FOO="BAR"} xbuild-tools = ["first"] test-command = "pyproject" test-requires = "something" test-extras = ["one", "two"] test-groups = ["three", "four"] test-sources = ["five", "six and seven"] manylinux-x86_64-image = "manylinux_2_28" [tool.cibuildwheel.macos] test-requires = "else" [tool.cibuildwheel.linux] test-requires = ["other", "many"] """ @pytest.fixture(params=["linux", "macos", "windows"]) def platform(request: pytest.FixtureRequest) -> PlatformName: return cast("PlatformName", request.param) @pytest.mark.parametrize("fname", ["pyproject.toml", "cibuildwheel.toml"]) def test_simple_settings(tmp_path: Path, platform: PlatformName, fname: str) -> None: config_file_path: Path = tmp_path / fname config_file_path.write_text(PYPROJECT_1) options_reader = OptionsReader(config_file_path, platform=platform, env={}) assert options_reader.get("build", option_format=ListFormat(" "), env_plat=False) == "cp39*" assert options_reader.get("test-command") == "pyproject" assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto" assert ( options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote)) == "five 'six and seven'" ) assert ( 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", option_format=EnvironmentFormat()) == 'THING="OTHER" FOO="BAR"' ) assert options_reader.get("test-extras", option_format=ListFormat(",")) == "one,two" assert options_reader.get("test-groups", option_format=ListFormat(" ")) == "three four" assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_28" assert options_reader.get("manylinux-i686-image") == "manylinux_2_28" 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(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: Path, platform: PlatformName) -> None: config_file_path: Path = tmp_path / "pyproject.toml" config_file_path.write_text(PYPROJECT_1) options_reader = OptionsReader( config_file_path, platform=platform, env={ "CIBW_BUILD": "cp38*", "CIBW_MANYLINUX_X86_64_IMAGE": "manylinux_2_24", "CIBW_XBUILD_TOOLS": "cmake rustc", "CIBW_TEST_COMMAND": "mytest", "CIBW_TEST_REQUIRES": "docs", "CIBW_TEST_GROUPS": "mgroup two", "CIBW_TEST_REQUIRES_LINUX": "scod", "CIBW_TEST_GROUPS_LINUX": "lgroup", "CIBW_TEST_SOURCES": 'first "second third"', }, ) assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto" assert options_reader.get("build") == "cp38*" assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24" assert options_reader.get("manylinux-i686-image") == "manylinux_2_28" assert ( options_reader.get("xbuild-tools", option_format=ListFormat(" ", quote=shlex.quote)) == "cmake rustc" ) assert ( options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote)) == 'first "second third"' ) assert ( options_reader.get("test-requires", option_format=ListFormat(" ")) == {"windows": "docs", "macos": "docs", "linux": "scod"}[platform] ) assert ( options_reader.get("test-groups", option_format=ListFormat(" ")) == {"windows": "mgroup two", "macos": "mgroup two", "linux": "lgroup"}[platform] ) assert options_reader.get("test-command") == "mytest" def test_project_global_override_default_platform(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] repair-wheel-command = "repair-project-global" """ ) options_reader = OptionsReader(pyproject_toml, platform=platform, env={}) assert options_reader.get("repair-wheel-command") == "repair-project-global" def test_env_global_override_default_platform(platform: PlatformName) -> None: options_reader = OptionsReader( platform=platform, env={"CIBW_REPAIR_WHEEL_COMMAND": "repair-env-global"} ) assert options_reader.get("repair-wheel-command") == "repair-env-global" def test_env_global_override_project_platform(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel.linux] repair-wheel-command = "repair-project-linux" [tool.cibuildwheel.windows] repair-wheel-command = "repair-project-windows" [tool.cibuildwheel.macos] repair-wheel-command = "repair-project-macos" """ ) options_reader = OptionsReader( pyproject_toml, platform=cast("Any", platform), env={ "CIBW_REPAIR_WHEEL_COMMAND": "repair-env-global", }, ) assert options_reader.get("repair-wheel-command") == "repair-env-global" def test_global_platform_order(tmp_path: Path, platform: str) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel.linux] repair-wheel-command = "repair-project-linux" [tool.cibuildwheel.windows] repair-wheel-command = "repair-project-windows" [tool.cibuildwheel.macos] repair-wheel-command = "repair-project-macos" [tool.cibuildwheel] repair-wheel-command = "repair-project-global" """ ) options_reader = OptionsReader(pyproject_toml, platform=cast("Any", platform), env={}) assert options_reader.get("repair-wheel-command") == f"repair-project-{platform}" def test_unexpected_key(tmp_path: Path) -> None: # Note that platform contents are only checked when running # for that platform. pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] repairs-wheel-command = "repair-project-linux" """ ) with pytest.raises(OptionsReaderError) as excinfo: OptionsReader(pyproject_toml, platform="linux", env={}) assert "repair-wheel-command" in str(excinfo.value) def test_underscores_in_key(tmp_path: Path) -> None: # Note that platform contents are only checked when running # for that platform. pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] repair_wheel_command = "repair-project-linux" """ ) with pytest.raises(OptionsReaderError) as excinfo: OptionsReader(pyproject_toml, platform="linux", env={}) assert "repair-wheel-command" in str(excinfo.value) def test_unexpected_table(tmp_path: Path) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel.linus] repair-wheel-command = "repair-project-linux" """ ) with pytest.raises(OptionsReaderError): OptionsReader(pyproject_toml, platform="linux", env={}) def test_unsupported_join(tmp_path: Path) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] build = ["1", "2"] """ ) options_reader = OptionsReader(pyproject_toml, platform="linux", env={}) assert options_reader.get("build", option_format=ListFormat(", ")) == "1, 2" with pytest.raises(OptionsReaderError): options_reader.get("build") def test_disallowed_a(tmp_path: Path) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel.windows] manylinux-x86_64-image = "manylinux1" """ ) disallow = {"windows": {"manylinux-x86_64-image"}} OptionsReader(pyproject_toml, platform="linux", disallow=disallow, env={}) with pytest.raises(OptionsReaderError): OptionsReader(pyproject_toml, platform="windows", disallow=disallow, env={}) def test_environment_override_empty(tmp_path: Path) -> None: pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] manylinux-i686-image = "manylinux1" manylinux-x86_64-image = "" """ ) options_reader = OptionsReader( pyproject_toml, platform="linux", env={ "CIBW_MANYLINUX_I686_IMAGE": "", "CIBW_MANYLINUX_AARCH64_IMAGE": "manylinux1", "CIBW_XBUILD_TOOLS": "", }, ) assert options_reader.get("manylinux-x86_64-image") == "" assert options_reader.get("manylinux-i686-image") == "" assert options_reader.get("manylinux-aarch64-image") == "manylinux1" assert options_reader.get("manylinux-x86_64-image", ignore_empty=True) == "manylinux_2_28" assert options_reader.get("manylinux-i686-image", ignore_empty=True) == "manylinux1" assert options_reader.get("manylinux-aarch64-image", ignore_empty=True) == "manylinux1" assert ( options_reader.get("xbuild-tools", option_format=ListFormat(" ", quote=shlex.quote)) == "" ) @pytest.mark.parametrize("ignore_empty", [True, False], ids=["ignore_empty", "no_ignore_empty"]) def test_resolve_cascade(ignore_empty: bool) -> None: 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 = _resolve_cascade( ("d4", InheritRule.NONE), (None, InheritRule.NONE), ("that", InheritRule.NONE), (None, InheritRule.NONE), ignore_empty=ignore_empty, ) assert answer == "that" 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: bool, rule: InheritRule) -> None: answer = _resolve_cascade( (["a1", "a2"], InheritRule.NONE), ([], InheritRule.NONE), (["b1", "b2"], rule), (None, InheritRule.NONE), ignore_empty=ignore_empty, option_format=ListFormat(" "), ) if not ignore_empty: assert answer == "b1 b2" elif 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: InheritRule) -> None: answer = _resolve_cascade( ({"value": "a1", "base": "b1"}, InheritRule.NONE), (None, InheritRule.NONE), ({"value": "override"}, rule), (None, InheritRule.NONE), option_format=ShlexTableFormat(), ) if rule == InheritRule.PREPEND: assert answer == "value=a1 base=b1" elif rule == InheritRule.NONE: assert answer == "value=override" elif rule == InheritRule.APPEND: assert answer == "value=override base=b1" def test_resolve_cascade_merge_strings() -> None: 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() -> None: answer = _resolve_cascade( ("value=a1 base=b1", InheritRule.NONE), ({"value": "override"}, InheritRule.APPEND), ("extra_string_var=c1", InheritRule.APPEND), option_format=ShlexTableFormat(), ) assert answer == "value=override base=b1 extra_string_var=c1" PYPROJECT_2 = """ [tool.cibuildwheel] build = ["cp38*", "cp37*"] environment = {FOO="BAR", "HAM"="EGGS"} test-command = ["pyproject"] manylinux-x86_64-image = "manylinux1" [tool.cibuildwheel.macos] test-requires = "else" [[tool.cibuildwheel.overrides]] select = "cp37*" 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"] """ def test_pyproject_2(tmp_path: Path, platform: str) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text(PYPROJECT_2) options_reader = OptionsReader( config_file_path=pyproject_toml, platform=cast("Any", platform), env={} ) assert options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject" with options_reader.identifier("random"): assert options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject" with options_reader.identifier("cp37-something"): assert ( options_reader.get("test-command", option_format=ListFormat(" && ")) == "pyproject-override && override2 && pyproject" ) assert ( 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", option_format=ListFormat(" && ")) == "extra-prepend && pyproject-override && override2 && pyproject && pyproject-finalize && finalize2 && extra-finalize" ) assert ( options_reader.get("environment", option_format=EnvironmentFormat()) == 'FOO="BAR" HAM="EGGS" FOO="BAZ" PYTHON="MONTY"' ) def test_overrides_not_a_list(tmp_path: Path, platform: str) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """\ [tool.cibuildwheel] build = ["cp38*", "cp37*"] [tool.cibuildwheel.overrides] select = "cp37*" test-command = "pyproject-override" """ ) with pytest.raises(OptionsReaderError): OptionsReader(config_file_path=pyproject_toml, platform=cast("Any", platform), env={}) def test_config_settings(tmp_path: Path) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """\ [tool.cibuildwheel.config-settings] example = "one" other = ["two", "three"] """ ) options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={}) assert ( options_reader.get("config-settings", option_format=ShlexTableFormat(pair_sep="=", sep=" ")) == "example=one other=two other=three" ) def test_pip_config_settings(tmp_path: Path) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """\ [tool.cibuildwheel.config-settings] --build-option="--use-mypyc" """ ) options_reader = OptionsReader(config_file_path=pyproject_toml, platform="linux", env={}) assert ( options_reader.get("config-settings", option_format=ShlexTableFormat(sep=" ", pair_sep="=")) == "--build-option=--use-mypyc" ) def test_overrides_inherit(tmp_path: Path) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """\ [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" 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", 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", option_format=ListFormat(" && ")) == "override2 && before-all && override1" ) assert ( options_reader.get("config-settings", option_format=ShlexTableFormat()) == "key1=value1 key2=override2 empty='' key3=value3" ) def test_audit_command_option(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-command = "abi3audit {abi3_wheel}" """ ) options_reader = OptionsReader(pyproject_toml, platform=platform, env={}) assert ( options_reader.get("audit-command", option_format=ListFormat(" && ")) == "abi3audit {abi3_wheel}" ) def test_audit_command_option_list(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-command = ["first command", "second command"] """ ) options_reader = OptionsReader(pyproject_toml, platform=platform, env={}) assert ( options_reader.get("audit-command", option_format=ListFormat(" && ")) == "first command && second command" ) def test_audit_command_option_env(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] """ ) options_reader = OptionsReader( pyproject_toml, platform=platform, env={"CIBW_AUDIT_COMMAND": "my-audit-tool {wheel}"} ) assert ( options_reader.get("audit-command", option_format=ListFormat(" && ")) == "my-audit-tool {wheel}" ) def test_audit_requires_option(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-requires = "abi3audit" """ ) options_reader = OptionsReader(pyproject_toml, platform=platform, env={}) assert options_reader.get("audit-requires", option_format=ListFormat(" ")) == "abi3audit" def test_audit_requires_option_list(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-requires = ["abi3audit", "twine"] """ ) options_reader = OptionsReader(pyproject_toml, platform=platform, env={}) assert options_reader.get("audit-requires", option_format=ListFormat(" ")) == "abi3audit twine" def test_audit_requires_option_env(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] """ ) options_reader = OptionsReader( pyproject_toml, platform=platform, env={"CIBW_AUDIT_REQUIRES": "custom-audit-tool"} ) assert ( options_reader.get("audit-requires", option_format=ListFormat(" ")) == "custom-audit-tool" ) def test_audit_requires_option_env_override(tmp_path: Path, platform: PlatformName) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-requires = "abi3audit" """ ) options_reader = OptionsReader( pyproject_toml, platform=platform, env={"CIBW_AUDIT_REQUIRES": "custom-audit-tool"} ) assert ( options_reader.get("audit-requires", option_format=ListFormat(" ")) == "custom-audit-tool" ) def test_audit_requires_platform_specific(tmp_path: Path) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-requires = "abi3audit" [tool.cibuildwheel.linux] audit-requires = ["abi3audit", "check-wheel-contents"] # whatever [tool.cibuildwheel.macos] audit-requires = ["check-wheel-contents", "pydistcheck"] # whatever """ ) linux_reader = OptionsReader(pyproject_toml, platform="linux", env={}) assert ( linux_reader.get("audit-requires", option_format=ListFormat(" ")) == "abi3audit check-wheel-contents" ) macos_reader = OptionsReader(pyproject_toml, platform="macos", env={}) assert ( macos_reader.get("audit-requires", option_format=ListFormat(" ")) == "check-wheel-contents pydistcheck" ) windows_reader = OptionsReader(pyproject_toml, platform="windows", env={}) assert windows_reader.get("audit-requires", option_format=ListFormat(" ")) == "abi3audit" def test_audit_requires_platform_env_override(tmp_path: Path) -> None: pyproject_toml: Path = tmp_path / "pyproject.toml" pyproject_toml.write_text( """ [tool.cibuildwheel] audit-requires = "abi3audit" """ ) options_reader = OptionsReader( pyproject_toml, platform="linux", env={ "CIBW_AUDIT_REQUIRES": "some-fallback-tool", "CIBW_AUDIT_REQUIRES_LINUX": "linux-audit-tool", }, ) assert options_reader.get("audit-requires", option_format=ListFormat(" ")) == "linux-audit-tool"