Add string and TOML-dict configuration of this param
This commit is contained in:
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user