Add string and TOML-dict configuration of this param

This commit is contained in:
Joe Rickerby
2023-05-12 14:18:08 +01:00
parent 5cb0964111
commit 290a55097d
5 changed files with 176 additions and 8 deletions
+20 -1
View File
@@ -18,7 +18,7 @@ from typing import IO, Dict
from ._compat.typing import Literal from ._compat.typing import Literal
from .typing import PathOrStr, PopenBytes from .typing import PathOrStr, PopenBytes
from .util import CIProvider, detect_ci_provider from .util import CIProvider, detect_ci_provider, parse_key_value_string
ContainerEngineName = Literal["docker", "podman"] ContainerEngineName = Literal["docker", "podman"]
@@ -28,6 +28,25 @@ class OCIContainerEngineConfig:
name: ContainerEngineName name: ContainerEngineName
create_args: Sequence[str] = () create_args: Sequence[str] = ()
@staticmethod
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
config_dict = parse_key_value_string(config_string, ["name"])
name = " ".join(config_dict["name"])
if name not in ["docker", "podman"]:
msg = f"unknown container engine {name}"
raise ValueError(msg)
name = typing.cast(ContainerEngineName, name)
# some flexibility in the option name to cope with TOML conventions
create_args = config_dict.get("create_args") or config_dict.get("create-args") or []
return OCIContainerEngineConfig(name=name, create_args=create_args)
def options_summary(self) -> str | dict[str, str]:
if not self.create_args:
return self.name
else:
return {"name": self.name, "create_args": repr(self.create_args)}
DEFAULT_ENGINE = OCIContainerEngineConfig("docker") DEFAULT_ENGINE = OCIContainerEngineConfig("docker")
+13 -6
View File
@@ -10,7 +10,6 @@ import shlex
import sys import sys
import textwrap import textwrap
import traceback import traceback
import typing
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Set from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Set
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Union from typing import Any, Dict, List, Union
@@ -136,8 +135,14 @@ DISALLOWED_OPTIONS = {
class TableFmt(TypedDict): class TableFmt(TypedDict):
# a format string, used with '.format', with `k` and `v` parameters
# e.g. "{k}={v}"
item: str item: str
# the string that is inserted between items
# e.g. " "
sep: str sep: str
# a quoting function that, if supplied, is called to quote each value
# e.g. shlex.quote
quote: NotRequired[Callable[[str], str]] quote: NotRequired[Callable[[str], str]]
@@ -454,15 +459,17 @@ class Options:
) )
test_selector = TestSelector(skip_config=test_skip) test_selector = TestSelector(skip_config=test_skip)
container_engine_str = self.reader.get("container-engine") container_engine_str = self.reader.get(
"container-engine", table={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote}
)
if container_engine_str not in ["docker", "podman"]: try:
msg = f"cibuildwheel: Unrecognised container_engine {container_engine_str!r}, only 'docker' and 'podman' are supported" container_engine = OCIContainerEngineConfig.from_config_string(container_engine_str)
except ValueError as e:
msg = f"cibuildwheel: Failed to parse container config. {e}"
print(msg, file=sys.stderr) print(msg, file=sys.stderr)
sys.exit(2) sys.exit(2)
container_engine = typing.cast(ContainerEngine, container_engine_str)
return GlobalOptions( return GlobalOptions(
package_dir=package_dir, package_dir=package_dir,
output_dir=output_dir, output_dir=output_dir,
+39
View File
@@ -13,6 +13,7 @@ import textwrap
import time import time
import typing import typing
import urllib.request import urllib.request
from collections import defaultdict
from collections.abc import Generator, Iterable, Mapping, Sequence from collections.abc import Generator, Iterable, Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
@@ -697,3 +698,41 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
ansi_codes.append(code) ansi_codes.append(code)
return output return output
def parse_key_value_string(
key_value_string: str, positional_arg_names: list[str] | None = None
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";:")
shlexer.commenters = ""
shlexer.whitespace_split = True
parts = list(shlexer)
# parts now looks like
# ['docker', ';', 'create_args',':', '--some-option=value', 'another-option']
# split by semicolon
fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k]
result: dict[str, list[str]] = defaultdict(list)
for field_i, field in enumerate(fields):
if len(field) > 1 and field[1] == ":":
field_name = field[0]
values = field[2:]
else:
try:
field_name = positional_arg_names[field_i]
except IndexError:
msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}"
raise ValueError(msg) from None
values = field
result[field_name] += values
return result
+49 -1
View File
@@ -30,6 +30,8 @@ elif pm == "s390x":
else: else:
DEFAULT_IMAGE = "" DEFAULT_IMAGE = ""
PODMAN = OCIContainerEngineConfig(name="podman")
@pytest.fixture(params=["docker", "podman"]) @pytest.fixture(params=["docker", "podman"])
def container_engine(request): def container_engine(request):
@@ -280,7 +282,7 @@ def test_podman_vfs(tmp_path: Path, monkeypatch, request):
monkeypatch.setenv("CONTAINERS_CONF", str(vfs_containers_conf_fpath)) monkeypatch.setenv("CONTAINERS_CONF", str(vfs_containers_conf_fpath))
monkeypatch.setenv("CONTAINERS_STORAGE_CONF", str(vfs_containers_storage_conf_fpath)) monkeypatch.setenv("CONTAINERS_STORAGE_CONF", str(vfs_containers_storage_conf_fpath))
with OCIContainer(engine="podman", image=DEFAULT_IMAGE) as container: with OCIContainer(engine=PODMAN, image=DEFAULT_IMAGE) as container:
# test running a command # test running a command
assert container.call(["echo", "hello"], capture_output=True) == "hello\n" assert container.call(["echo", "hello"], capture_output=True) == "hello\n"
@@ -311,3 +313,49 @@ def test_create_args(tmp_path: Path):
image=DEFAULT_IMAGE, image=DEFAULT_IMAGE,
) as container: ) as container:
assert container.call(["cat", "/test_mount/test_file.txt"], capture_output=True) == "1234" assert container.call(["cat", "/test_mount/test_file.txt"], capture_output=True) == "1234"
@pytest.mark.parametrize(
("config", "name", "create_args"),
[
(
"docker",
"docker",
[],
),
(
"docker;create_args:",
"docker",
[],
),
(
"docker;create_args:--abc --def",
"docker",
["--abc", "--def"],
),
(
"docker; create_args: --abc --def",
"docker",
["--abc", "--def"],
),
(
"name:docker; create_args: --abc --def",
"docker",
["--abc", "--def"],
),
(
'docker; create_args: --some-option="value with spaces"',
"docker",
["--some-option=value with spaces"],
),
(
'docker; create_args: --some-option="value; with; semicolons" --another-option',
"docker",
["--some-option=value; with; semicolons", "--another-option"],
),
],
)
def test_parse_engine_config(config, name, create_args):
engine_config = OCIContainerEngineConfig.from_config_string(config)
assert engine_config.name == name
assert engine_config.create_args == create_args
+55
View File
@@ -198,3 +198,58 @@ def test_toml_environment_quoting(tmp_path: Path, toml_assignment, result_value)
) )
assert environment_values["TEST_VAR"] == result_value assert environment_values["TEST_VAR"] == result_value
@pytest.mark.parametrize(
("toml_assignment", "result_name", "result_create_args"),
[
(
'container-engine = "podman"',
"podman",
[],
),
(
'container-engine = {name = "podman"}',
"podman",
[],
),
(
'container-engine = "docker; create_args: --some-option"',
"docker",
["--some-option"],
),
(
'container-engine = {name = "docker", create-args = ["--some-option"]}',
"docker",
["--some-option"],
),
(
'container-engine = {name = "docker", create-args = ["--some-option", "value that contains spaces"]}',
"docker",
["--some-option", "value that contains spaces"],
),
(
'container-engine = {name = "docker", create-args = ["--some-option", "value;that;contains;semicolons"]}',
"docker",
["--some-option", "value;that;contains;semicolons"],
),
],
)
def test_container_engine_option(tmp_path: Path, toml_assignment, result_name, result_create_args):
args = CommandLineArguments.defaults()
args.package_dir = tmp_path
tmp_path.joinpath("pyproject.toml").write_text(
textwrap.dedent(
f"""\
[tool.cibuildwheel]
{toml_assignment}
"""
)
)
options = Options(platform="linux", command_line_arguments=args, env={})
parsed_container_engine = options.globals.container_engine
assert parsed_container_engine.name == result_name
assert parsed_container_engine.create_args == result_create_args