diff --git a/python_on_whales/client_config.py b/python_on_whales/client_config.py index 207961c1..6709f4ab 100644 --- a/python_on_whales/client_config.py +++ b/python_on_whales/client_config.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Union +from typing import Any, Dict, Iterable, List, Literal, Mapping, Optional, Union import pydantic @@ -13,9 +13,9 @@ download_docker_cli, get_docker_binary_path_in_cache, ) -from python_on_whales.utils import to_list -from .utils import ValidPath, run +from . import utils +from .utils import ValidPath, run, to_list CACHE_VALIDITY_PERIOD = 0.01 @@ -37,10 +37,23 @@ def add_flag(self, name: str, value: bool): if value: self.append(name) - def add_args_list(self, arg_name: str, list_values: list): - for value in to_list(list_values): + def add_args_iterable(self, arg_name: str, values: Iterable[Any]): + for value in values: self.extend([arg_name, value]) + def add_args_iterable_or_single( + self, arg_name: str, iterable_or_single: Union[Iterable[Any], Any] + ): + for value in to_list(iterable_or_single): + self.extend([arg_name, value]) + + def add_args_mapping( + self, arg_name: str, mapping: Mapping[Any, Any], *, separator="=" + ): + self.add_args_iterable( + arg_name, utils.format_mapping_for_cli(mapping, separator) + ) + def __add__(self, other) -> "Command": return Command(super().__add__(other)) @@ -144,8 +157,8 @@ def docker_cmd(self) -> Command: @property def docker_compose_cmd(self) -> Command: base_cmd = self.docker_cmd + ["compose"] - base_cmd.add_args_list("--file", self.compose_files) - base_cmd.add_args_list("--profile", self.compose_profiles) + base_cmd.add_args_iterable_or_single("--file", self.compose_files) + base_cmd.add_args_iterable_or_single("--profile", self.compose_profiles) base_cmd.add_simple_arg("--env-file", self.compose_env_file) base_cmd.add_simple_arg("--project-name", self.compose_project_name) base_cmd.add_simple_arg("--project-directory", self.compose_project_directory) diff --git a/python_on_whales/components/buildx/cli_wrapper.py b/python_on_whales/components/buildx/cli_wrapper.py index 9c54b33e..be837240 100644 --- a/python_on_whales/components/buildx/cli_wrapper.py +++ b/python_on_whales/components/buildx/cli_wrapper.py @@ -27,7 +27,7 @@ from python_on_whales.components.buildx.models import BuilderInspectResult from python_on_whales.utils import ( ValidPath, - format_dict_for_cli, + format_mapping_for_cli, run, stream_stdout_and_stderr, to_list, @@ -196,7 +196,7 @@ def bake( full_cmd += ["--progress", progress] for file in to_list(files): full_cmd.add_simple_arg("--file", file) - full_cmd.add_args_list("--set", format_dict_for_cli(set)) + full_cmd.add_args_iterable_or_single("--set", format_mapping_for_cli(set)) targets = to_list(targets) env = dict(variables) if print: @@ -326,16 +326,20 @@ def build( if progress != "auto" and isinstance(progress, str): full_cmd += ["--progress", progress] - full_cmd.add_args_list( - "--add-host", format_dict_for_cli(add_hosts, separator=":") + full_cmd.add_args_iterable_or_single( + "--add-host", format_mapping_for_cli(add_hosts, separator=":") ) - full_cmd.add_args_list("--allow", allow) + full_cmd.add_args_iterable_or_single("--allow", allow) if isinstance(attest, dict): full_cmd.add_simple_arg("--attest", format_dict_for_buildx(attest)) - full_cmd.add_args_list("--build-arg", format_dict_for_cli(build_args)) - full_cmd.add_args_list("--build-context", format_dict_for_cli(build_contexts)) + full_cmd.add_args_iterable_or_single( + "--build-arg", format_mapping_for_cli(build_args) + ) + full_cmd.add_args_iterable_or_single( + "--build-context", format_mapping_for_cli(build_contexts) + ) full_cmd.add_simple_arg("--builder", builder) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.add_simple_arg("--ssh", ssh) @@ -363,14 +367,14 @@ def build( full_cmd.add_simple_arg("--cache-to", format_dict_for_buildx(cache_to)) else: full_cmd.add_simple_arg("--cache-to", cache_to) - full_cmd.add_args_list("--secret", to_list(secrets)) + full_cmd.add_args_iterable_or_single("--secret", to_list(secrets)) if output != {}: full_cmd += ["--output", format_dict_for_buildx(output)] if platforms is not None: full_cmd += ["--platform", ",".join(platforms)] full_cmd.add_simple_arg("--network", network) full_cmd.add_flag("--no-cache", not cache) - full_cmd.add_args_list("--tag", tags) + full_cmd.add_args_iterable_or_single("--tag", tags) if stream_logs: if progress in (False, "tty"): @@ -558,7 +562,9 @@ def prune( """ full_cmd = self.docker_cmd + ["buildx", "prune", "--force"] full_cmd.add_flag("--all", all) - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) if stream_logs: return stream_buildx_logs(full_cmd) run(full_cmd) @@ -640,7 +646,7 @@ def removesuffix(base_string: str, suffix: str) -> str: def format_dict_for_buildx(options: Dict[str, str]) -> str: - return ",".join(format_dict_for_cli(options, separator="=")) + return ",".join(format_mapping_for_cli(options, separator="=")) def stream_buildx_logs(full_cmd: list, env: Dict[str, str] = None) -> Iterator[str]: diff --git a/python_on_whales/components/compose/cli_wrapper.py b/python_on_whales/components/compose/cli_wrapper.py index 843acf1c..3940bc01 100644 --- a/python_on_whales/components/compose/cli_wrapper.py +++ b/python_on_whales/components/compose/cli_wrapper.py @@ -11,7 +11,7 @@ from python_on_whales.client_config import DockerCLICaller from python_on_whales.components.compose.models import ComposeConfig, ComposeProject from python_on_whales.utils import ( - format_dict_for_cli, + format_mapping_for_cli, format_signal_arg, parse_ls_status_count, run, @@ -89,7 +89,9 @@ def build( ) full_cmd = self.docker_compose_cmd + ["build"] - full_cmd.add_args_list("--build-arg", format_dict_for_cli(build_args)) + full_cmd.add_args_iterable_or_single( + "--build-arg", format_mapping_for_cli(build_args) + ) full_cmd.add_flag("--no-cache", not cache) full_cmd.add_simple_arg("--progress", progress) full_cmd.add_flag("--pull", pull) @@ -500,7 +502,9 @@ def ls( """ full_cmd = self.docker_compose_cmd + ["ls", "--format", "json"] full_cmd.add_flag("--all", all) - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) return [ ComposeProject( @@ -751,7 +755,7 @@ def run( full_cmd.add_flag("--use-aliases", use_aliases) full_cmd.add_simple_arg("--user", user) full_cmd.add_simple_arg("--workdir", workdir) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.append(service) full_cmd += command diff --git a/python_on_whales/components/config/cli_wrapper.py b/python_on_whales/components/config/cli_wrapper.py index 19bad0e9..330cd643 100644 --- a/python_on_whales/components/config/cli_wrapper.py +++ b/python_on_whales/components/config/cli_wrapper.py @@ -15,7 +15,7 @@ ConfigSpec, DockerObjectVersion, ) -from python_on_whales.utils import format_dict_for_cli, run, to_list +from python_on_whales.utils import format_mapping_for_cli, run, to_list class Config(ReloadableObjectFromJson): @@ -96,7 +96,7 @@ def create( A `python_on_whales.Config` object. """ full_cmd = self.docker_cmd + ["config", "create"] - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.add_simple_arg("--template-driver", template_driver) full_cmd += [name, file] return Config(self.client_config, run(full_cmd), is_immutable_id=True) @@ -135,7 +135,9 @@ def list(self, filters: Dict[str, str] = {}) -> List[Config]: A `List[python_on_whales.Config]`. """ full_cmd = self.docker_cmd + ["config", "list", "--quiet"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) output = run(full_cmd) ids = output.splitlines() return [Config(self.client_config, id_, is_immutable_id=True) for id_ in ids] diff --git a/python_on_whales/components/container/cli_wrapper.py b/python_on_whales/components/container/cli_wrapper.py index 81ffc35b..9cb1e91d 100644 --- a/python_on_whales/components/container/cli_wrapper.py +++ b/python_on_whales/components/container/cli_wrapper.py @@ -41,7 +41,7 @@ ValidPath, ValidPortMapping, custom_parse_object_as, - format_dict_for_cli, + format_mapping_for_cli, format_port_arg, format_signal_arg, format_time_arg, @@ -665,13 +665,15 @@ def create( full_cmd = self.docker_cmd + ["create"] add_hosts = [f"{host}:{ip}" for host, ip in add_hosts] - full_cmd.add_args_list("--add-host", add_hosts) + full_cmd.add_args_iterable_or_single("--add-host", add_hosts) full_cmd.add_simple_arg("--blkio-weight", blkio_weight) - full_cmd.add_args_list("--blkio-weight-device", blkio_weight_device) + full_cmd.add_args_iterable_or_single( + "--blkio-weight-device", blkio_weight_device + ) - full_cmd.add_args_list("--cap-add", cap_add) - full_cmd.add_args_list("--cap-drop", cap_drop) + full_cmd.add_args_iterable_or_single("--cap-add", cap_add) + full_cmd.add_args_iterable_or_single("--cap-drop", cap_drop) full_cmd.add_simple_arg("--cgroup-parent", cgroup_parent) full_cmd.add_simple_arg("--cgroupns", cgroupns) @@ -688,32 +690,34 @@ def create( full_cmd.add_flag("--detach", detach) - full_cmd.add_args_list("--device", devices) - full_cmd.add_args_list("--device-cgroup-rule", device_cgroup_rules) - full_cmd.add_args_list("--device-read-bps", device_read_bps) - full_cmd.add_args_list("--device-read-iops", device_read_iops) - full_cmd.add_args_list("--device-write-bps", device_write_bps) - full_cmd.add_args_list("--device-write-iops", device_write_iops) + full_cmd.add_args_iterable_or_single("--device", devices) + full_cmd.add_args_iterable_or_single( + "--device-cgroup-rule", device_cgroup_rules + ) + full_cmd.add_args_iterable_or_single("--device-read-bps", device_read_bps) + full_cmd.add_args_iterable_or_single("--device-read-iops", device_read_iops) + full_cmd.add_args_iterable_or_single("--device-write-bps", device_write_bps) + full_cmd.add_args_iterable_or_single("--device-write-iops", device_write_iops) if content_trust: full_cmd += ["--disable-content-trust", "false"] - full_cmd.add_args_list("--dns", dns) - full_cmd.add_args_list("--dns-option", dns_options) - full_cmd.add_args_list("--dns-search", dns_search) + full_cmd.add_args_iterable_or_single("--dns", dns) + full_cmd.add_args_iterable_or_single("--dns-option", dns_options) + full_cmd.add_args_iterable_or_single("--dns-search", dns_search) full_cmd.add_simple_arg("--domainname", domainname) full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_list("--env", format_dict_for_cli(envs)) - full_cmd.add_args_list("--env-file", env_files) + full_cmd.add_args_iterable_or_single("--env", format_mapping_for_cli(envs)) + full_cmd.add_args_iterable_or_single("--env-file", env_files) full_cmd.add_flag("--env-host", env_host) - full_cmd.add_args_list("--expose", expose) + full_cmd.add_args_iterable_or_single("--expose", expose) full_cmd.add_simple_arg("--gpus", gpus) - full_cmd.add_args_list("--group-add", groups_add) + full_cmd.add_args_iterable_or_single("--group-add", groups_add) full_cmd.add_flag("--no-healthcheck", not healthcheck) full_cmd.add_simple_arg("--health-cmd", health_cmd) @@ -736,14 +740,14 @@ def create( full_cmd.add_simple_arg("--isolation", isolation) full_cmd.add_simple_arg("--kernel-memory", kernel_memory) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) - full_cmd.add_args_list("--label-file", label_files) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label-file", label_files) - full_cmd.add_args_list("--link", link) - full_cmd.add_args_list("--link-local-ip", link_local_ip) + full_cmd.add_args_iterable_or_single("--link", link) + full_cmd.add_args_iterable_or_single("--link-local-ip", link_local_ip) full_cmd.add_simple_arg("--log-driver", log_driver) - full_cmd.add_args_list("--log-opt", log_options) + full_cmd.add_args_iterable_or_single("--log-opt", log_options) full_cmd.add_simple_arg("--mac-address", mac_address) @@ -752,11 +756,11 @@ def create( full_cmd.add_simple_arg("--memory-swap", memory_swap) full_cmd.add_simple_arg("--memory-swappiness", memory_swappiness) - full_cmd.add_args_list("--mount", [",".join(x) for x in mounts]) + full_cmd.add_args_iterable_or_single("--mount", [",".join(x) for x in mounts]) full_cmd.add_simple_arg("--name", name) - full_cmd.add_args_list("--network", networks) - full_cmd.add_args_list("--network-alias", network_aliases) + full_cmd.add_args_iterable_or_single("--network", networks) + full_cmd.add_args_iterable_or_single("--network-alias", network_aliases) full_cmd.add_flag("--oom-kill-disable", not oom_kill) full_cmd.add_simple_arg("--oom-score-adj", oom_score_adj) @@ -767,7 +771,9 @@ def create( full_cmd.add_simple_arg("--platform", platform) full_cmd.add_flag("--privileged", privileged) - full_cmd.add_args_list("-p", [format_port_arg(p) for p in publish]) + full_cmd.add_args_iterable_or_single( + "-p", [format_port_arg(p) for p in publish] + ) full_cmd.add_flag("--publish-all", publish_all) if pull == "never": @@ -778,7 +784,7 @@ def create( full_cmd.add_flag("--rm", remove) full_cmd.add_simple_arg("--runtime", runtime) - full_cmd.add_args_list("--security-opt", security_options) + full_cmd.add_args_iterable_or_single("--security-opt", security_options) full_cmd.add_simple_arg("--shm-size", shm_size) if sig_proxy is False: @@ -787,13 +793,13 @@ def create( full_cmd.add_simple_arg("--stop-signal", format_signal_arg(stop_signal)) full_cmd.add_simple_arg("--stop-timeout", stop_timeout) - full_cmd.add_args_list("--storage-opt", storage_options) - full_cmd.add_args_list("--sysctl", format_dict_for_cli(sysctl)) + full_cmd.add_args_iterable_or_single("--storage-opt", storage_options) + full_cmd.add_args_iterable_or_single("--sysctl", format_mapping_for_cli(sysctl)) full_cmd.add_simple_arg("--systemd", systemd) - full_cmd.add_args_list("--tmpfs", tmpfs) + full_cmd.add_args_iterable_or_single("--tmpfs", tmpfs) full_cmd.add_flag("--tty", tty) full_cmd.add_simple_arg("--tz", tz) - full_cmd.add_args_list("--ulimit", ulimit) + full_cmd.add_args_iterable_or_single("--ulimit", ulimit) full_cmd.add_simple_arg("--user", user) full_cmd.add_simple_arg("--userns", userns) @@ -803,7 +809,7 @@ def create( volume_definition = tuple(str(x) for x in volume_definition) full_cmd += ["--volume", ":".join(volume_definition)] full_cmd.add_simple_arg("--volume-driver", volume_driver) - full_cmd.add_args_list("--volumes-from", volumes_from) + full_cmd.add_args_iterable_or_single("--volumes-from", volumes_from) full_cmd.add_simple_arg("--workdir", workdir) @@ -901,8 +907,8 @@ def execute( full_cmd.add_flag("--detach", detach) full_cmd.add_simple_arg("--detach-keys", detach_keys) - full_cmd.add_args_list("--env", format_dict_for_cli(envs)) - full_cmd.add_args_list("--env-file", env_files) + full_cmd.add_args_iterable_or_single("--env", format_mapping_for_cli(envs)) + full_cmd.add_args_iterable_or_single("--env-file", env_files) if interactive and stream: raise ValueError( @@ -1136,7 +1142,9 @@ def list( """ full_cmd = self.docker_cmd full_cmd += ["container", "list", "-q", "--no-trunc"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) full_cmd.add_flag("--all", all) # TODO: add a test for the fix of is_immutable_id, without it, we get @@ -1203,7 +1211,9 @@ def prune( "docker.container.prune(filters={...})" ) full_cmd = self.docker_cmd + ["container", "prune", "--force"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) if stream_logs: return stream_stdout_and_stderr(full_cmd) run(full_cmd) @@ -1587,13 +1597,15 @@ def run( full_cmd = self.docker_cmd + ["container", "run"] add_hosts = [f"{host}:{ip}" for host, ip in add_hosts] - full_cmd.add_args_list("--add-host", add_hosts) + full_cmd.add_args_iterable_or_single("--add-host", add_hosts) full_cmd.add_simple_arg("--blkio-weight", blkio_weight) - full_cmd.add_args_list("--blkio-weight-device", blkio_weight_device) + full_cmd.add_args_iterable_or_single( + "--blkio-weight-device", blkio_weight_device + ) - full_cmd.add_args_list("--cap-add", cap_add) - full_cmd.add_args_list("--cap-drop", cap_drop) + full_cmd.add_args_iterable_or_single("--cap-add", cap_add) + full_cmd.add_args_iterable_or_single("--cap-drop", cap_drop) full_cmd.add_simple_arg("--cgroup-parent", cgroup_parent) full_cmd.add_simple_arg("--cgroupns", cgroupns) @@ -1610,32 +1622,34 @@ def run( full_cmd.add_flag("--detach", detach) - full_cmd.add_args_list("--device", devices) - full_cmd.add_args_list("--device-cgroup-rule", device_cgroup_rules) - full_cmd.add_args_list("--device-read-bps", device_read_bps) - full_cmd.add_args_list("--device-read-iops", device_read_iops) - full_cmd.add_args_list("--device-write-bps", device_write_bps) - full_cmd.add_args_list("--device-write-iops", device_write_iops) + full_cmd.add_args_iterable_or_single("--device", devices) + full_cmd.add_args_iterable_or_single( + "--device-cgroup-rule", device_cgroup_rules + ) + full_cmd.add_args_iterable_or_single("--device-read-bps", device_read_bps) + full_cmd.add_args_iterable_or_single("--device-read-iops", device_read_iops) + full_cmd.add_args_iterable_or_single("--device-write-bps", device_write_bps) + full_cmd.add_args_iterable_or_single("--device-write-iops", device_write_iops) if content_trust: full_cmd += ["--disable-content-trust", "false"] - full_cmd.add_args_list("--dns", dns) - full_cmd.add_args_list("--dns-option", dns_options) - full_cmd.add_args_list("--dns-search", dns_search) + full_cmd.add_args_iterable_or_single("--dns", dns) + full_cmd.add_args_iterable_or_single("--dns-option", dns_options) + full_cmd.add_args_iterable_or_single("--dns-search", dns_search) full_cmd.add_simple_arg("--domainname", domainname) full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_list("--env", format_dict_for_cli(envs)) - full_cmd.add_args_list("--env-file", env_files) + full_cmd.add_args_iterable_or_single("--env", format_mapping_for_cli(envs)) + full_cmd.add_args_iterable_or_single("--env-file", env_files) full_cmd.add_flag("--env-host", env_host) - full_cmd.add_args_list("--expose", expose) + full_cmd.add_args_iterable_or_single("--expose", expose) full_cmd.add_simple_arg("--gpus", gpus) - full_cmd.add_args_list("--group-add", groups_add) + full_cmd.add_args_iterable_or_single("--group-add", groups_add) full_cmd.add_flag("--no-healthcheck", not healthcheck) full_cmd.add_simple_arg("--health-cmd", health_cmd) @@ -1659,14 +1673,14 @@ def run( full_cmd.add_simple_arg("--isolation", isolation) full_cmd.add_simple_arg("--kernel-memory", kernel_memory) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) - full_cmd.add_args_list("--label-file", label_files) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label-file", label_files) - full_cmd.add_args_list("--link", link) - full_cmd.add_args_list("--link-local-ip", link_local_ip) + full_cmd.add_args_iterable_or_single("--link", link) + full_cmd.add_args_iterable_or_single("--link-local-ip", link_local_ip) full_cmd.add_simple_arg("--log-driver", log_driver) - full_cmd.add_args_list("--log-opt", log_options) + full_cmd.add_args_iterable_or_single("--log-opt", log_options) full_cmd.add_simple_arg("--mac-address", mac_address) @@ -1676,11 +1690,11 @@ def run( full_cmd.add_simple_arg("--memory-swappiness", memory_swappiness) mounts = [",".join(x) for x in mounts] - full_cmd.add_args_list("--mount", mounts) + full_cmd.add_args_iterable_or_single("--mount", mounts) full_cmd.add_simple_arg("--name", name) - full_cmd.add_args_list("--network", networks) - full_cmd.add_args_list("--network-alias", network_aliases) + full_cmd.add_args_iterable_or_single("--network", networks) + full_cmd.add_args_iterable_or_single("--network-alias", network_aliases) full_cmd.add_flag("--oom-kill-disable", not oom_kill) full_cmd.add_simple_arg("--oom-score-adj", oom_score_adj) @@ -1692,7 +1706,9 @@ def run( full_cmd.add_simple_arg("--preserve-fds", preserve_fds) full_cmd.add_flag("--privileged", privileged) - full_cmd.add_args_list("-p", [format_port_arg(p) for p in publish]) + full_cmd.add_args_iterable_or_single( + "-p", [format_port_arg(p) for p in publish] + ) full_cmd.add_flag("--publish-all", publish_all) if pull == "never": @@ -1703,7 +1719,7 @@ def run( full_cmd.add_flag("--rm", remove) full_cmd.add_simple_arg("--runtime", runtime) - full_cmd.add_args_list("--security-opt", security_options) + full_cmd.add_args_iterable_or_single("--security-opt", security_options) full_cmd.add_simple_arg("--shm-size", shm_size) if sig_proxy is False: @@ -1712,13 +1728,13 @@ def run( full_cmd.add_simple_arg("--stop-signal", format_signal_arg(stop_signal)) full_cmd.add_simple_arg("--stop-timeout", stop_timeout) - full_cmd.add_args_list("--storage-opt", storage_options) - full_cmd.add_args_list("--sysctl", format_dict_for_cli(sysctl)) + full_cmd.add_args_iterable_or_single("--storage-opt", storage_options) + full_cmd.add_args_iterable_or_single("--sysctl", format_mapping_for_cli(sysctl)) full_cmd.add_simple_arg("--systemd", systemd) - full_cmd.add_args_list("--tmpfs", tmpfs) + full_cmd.add_args_iterable_or_single("--tmpfs", tmpfs) full_cmd.add_flag("--tty", tty) full_cmd.add_simple_arg("--tz", tz) - full_cmd.add_args_list("--ulimit", ulimit) + full_cmd.add_args_iterable_or_single("--ulimit", ulimit) full_cmd.add_simple_arg("--user", user) full_cmd.add_simple_arg("--userns", userns) @@ -1728,7 +1744,7 @@ def run( volume_definition = tuple(str(x) for x in volume_definition) full_cmd += ["--volume", ":".join(volume_definition)] full_cmd.add_simple_arg("--volume-driver", volume_driver) - full_cmd.add_args_list("--volumes-from", volumes_from) + full_cmd.add_args_iterable_or_single("--volumes-from", volumes_from) full_cmd.add_simple_arg("--workdir", workdir) diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py index 275a565c..4cd21d31 100644 --- a/python_on_whales/components/image/cli_wrapper.py +++ b/python_on_whales/components/image/cli_wrapper.py @@ -26,7 +26,7 @@ from python_on_whales.exceptions import DockerException, NoSuchImage from python_on_whales.utils import ( ValidPath, - format_dict_for_cli, + format_mapping_for_cli, run, stream_stdout_and_stderr, to_list, @@ -269,17 +269,19 @@ def legacy_build( tags = to_list(tags) full_cmd = self.docker_cmd + ["build", "--quiet"] - full_cmd.add_args_list( - "--add-host", format_dict_for_cli(add_hosts, separator=":") + full_cmd.add_args_iterable_or_single( + "--add-host", format_mapping_for_cli(add_hosts, separator=":") ) - full_cmd.add_args_list("--build-arg", format_dict_for_cli(build_args)) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single( + "--build-arg", format_mapping_for_cli(build_args) + ) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.add_flag("--pull", pull) full_cmd.add_simple_arg("--file", file) full_cmd.add_simple_arg("--target", target) full_cmd.add_simple_arg("--network", network) full_cmd.add_flag("--no-cache", not cache) - full_cmd.add_args_list("--tag", tags) + full_cmd.add_args_iterable_or_single("--tag", tags) docker_image = python_on_whales.components.image.cli_wrapper.ImageCLI( self.client_config @@ -310,7 +312,7 @@ def import_( platform: Set platform if server is multi-platform capable """ full_cmd = self.docker_cmd + ["image", "import"] - full_cmd.add_args_list("--change", changes) + full_cmd.add_args_iterable_or_single("--change", changes) full_cmd.add_simple_arg("--message", message) full_cmd.add_simple_arg("--platform", platform) full_cmd.append(source) @@ -450,7 +452,9 @@ def list( "--quiet", "--no-trunc", ] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) full_cmd.add_flag("--all", all) if repository_or_tag is not None: @@ -474,7 +478,7 @@ def prune(self, all: bool = False, filter: Dict[str, str] = {}) -> str: """ full_cmd = self.docker_cmd + ["image", "prune", "--force"] full_cmd.add_flag("--all", all) - full_cmd.add_args_list("--filter", format_dict_for_cli(filter)) + full_cmd.add_args_iterable_or_single("--filter", format_mapping_for_cli(filter)) return run(full_cmd) def pull( diff --git a/python_on_whales/components/network/cli_wrapper.py b/python_on_whales/components/network/cli_wrapper.py index 750d3287..5beabf6a 100644 --- a/python_on_whales/components/network/cli_wrapper.py +++ b/python_on_whales/components/network/cli_wrapper.py @@ -15,7 +15,7 @@ NetworkInspectResult, NetworkIPAM, ) -from python_on_whales.utils import format_dict_for_cli, run, to_list +from python_on_whales.utils import format_mapping_for_cli, run, to_list class Network(ReloadableObjectFromJson): @@ -157,10 +157,10 @@ def connect( """ full_cmd = self.docker_cmd + ["network", "connect"] full_cmd.add_simple_arg("--alias", alias) - full_cmd.add_args_list("--driver-opt", driver_options) + full_cmd.add_args_iterable_or_single("--driver-opt", driver_options) full_cmd.add_simple_arg("--ip", ip) full_cmd.add_simple_arg("--ip6", ip6) - full_cmd.add_args_list("--link", links) + full_cmd.add_args_iterable_or_single("--link", links) full_cmd += [network, container] run(full_cmd) @@ -187,8 +187,8 @@ def create( full_cmd.add_simple_arg("--driver", driver) full_cmd.add_simple_arg("--gateway", gateway) full_cmd.add_simple_arg("--subnet", subnet) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) - full_cmd.add_args_list("--opt", options) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--opt", options) full_cmd.append(name) return Network(self.client_config, run(full_cmd), is_immutable_id=True) @@ -225,14 +225,18 @@ def inspect(self, x: Union[str, List[str]]) -> Union[Network, List[Network]]: def list(self, filters: Dict[str, str] = {}) -> List[Network]: full_cmd = self.docker_cmd + ["network", "ls", "--no-trunc", "--quiet"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) ids = run(full_cmd).splitlines() return [Network(self.client_config, id_, is_immutable_id=True) for id_ in ids] def prune(self, filters: Dict[str, str] = {}): full_cmd = self.docker_cmd + ["network", "prune", "--force"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) run(full_cmd) def remove(self, networks: Union[ValidNetwork, List[ValidNetwork]]): diff --git a/python_on_whales/components/secret/cli_wrapper.py b/python_on_whales/components/secret/cli_wrapper.py index 39e2a6a6..c2083fe6 100644 --- a/python_on_whales/components/secret/cli_wrapper.py +++ b/python_on_whales/components/secret/cli_wrapper.py @@ -7,7 +7,7 @@ ReloadableObjectFromJson, ) from python_on_whales.components.secret.models import SecretInspectResult -from python_on_whales.utils import ValidPath, format_dict_for_cli, run, to_list +from python_on_whales.utils import ValidPath, format_mapping_for_cli, run, to_list class Secret(ReloadableObjectFromJson): @@ -65,7 +65,7 @@ def create( """ full_cmd = self.docker_cmd + ["secret", "create"] full_cmd.add_simple_arg("--driver", driver) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.add_simple_arg("--template-driver", template_driver) full_cmd += [name, file] return Secret(self.client_config, run(full_cmd), is_immutable_id=True) @@ -84,7 +84,9 @@ def inspect(self, x: Union[str, List[str]]) -> Union[Secret, List[Secret]]: def list(self, filters: Dict[str, str] = {}) -> List[Secret]: """Returns all secrets as a `List[python_on_whales.Secret]`.""" full_cmd = self.docker_cmd + ["secret", "list", "--quiet"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) ids = run(full_cmd).splitlines() return [Secret(self.client_config, id_, is_immutable_id=True) for id_ in ids] diff --git a/python_on_whales/components/service/cli_wrapper.py b/python_on_whales/components/service/cli_wrapper.py index da4fa6b0..0f7a4e83 100644 --- a/python_on_whales/components/service/cli_wrapper.py +++ b/python_on_whales/components/service/cli_wrapper.py @@ -21,7 +21,7 @@ from python_on_whales.exceptions import NoSuchService from python_on_whales.utils import ( ValidPath, - format_dict_for_cli, + format_mapping_for_cli, format_time_arg, run, stream_stdout_and_stderr, @@ -195,22 +195,22 @@ def create( """ full_cmd = self.docker_cmd + ["service", "create", "--quiet"] - full_cmd.add_args_list("--cap-add", cap_add) - full_cmd.add_args_list("--cap-drop", cap_drop) - full_cmd.add_args_list("--constraint", constraints) + full_cmd.add_args_iterable_or_single("--cap-add", cap_add) + full_cmd.add_args_iterable_or_single("--cap-drop", cap_drop) + full_cmd.add_args_iterable_or_single("--constraint", constraints) full_cmd.add_flag("--detach", detach) - full_cmd.add_args_list("--dns", dns) - full_cmd.add_args_list("--dns-option", dns_options) - full_cmd.add_args_list("--dns-search", dns_search) + full_cmd.add_args_iterable_or_single("--dns", dns) + full_cmd.add_args_iterable_or_single("--dns-option", dns_options) + full_cmd.add_args_iterable_or_single("--dns-search", dns_search) full_cmd.add_simple_arg("--endpoint-mode", endpoint_mode) full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_list("--env", format_dict_for_cli(envs)) - full_cmd.add_args_list("--env-file", env_files) + full_cmd.add_args_iterable_or_single("--env", format_mapping_for_cli(envs)) + full_cmd.add_args_iterable_or_single("--env-file", env_files) - full_cmd.add_args_list("--generic-resource", generic_resources) - full_cmd.add_args_list("--group", groups) + full_cmd.add_args_iterable_or_single("--generic-resource", generic_resources) + full_cmd.add_args_iterable_or_single("--group", groups) full_cmd.add_flag("--no-healthcheck", not healthcheck) full_cmd.add_simple_arg("--health-cmd", health_cmd) @@ -227,19 +227,19 @@ def create( full_cmd.add_flag("--init", init) full_cmd.add_simple_arg("--isolation", isolation) - full_cmd.add_args_list("--label", format_dict_for_cli(labels)) + full_cmd.add_args_iterable_or_single("--label", format_mapping_for_cli(labels)) full_cmd.add_simple_arg("--limit-cpu", limit_cpu) full_cmd.add_simple_arg("--limit-memory", limit_memory) full_cmd.add_simple_arg("--limit-pids", limit_pids) full_cmd.add_simple_arg("--log-driver", log_driver) full_cmd.add_simple_arg("--restart-condition", restart_condition) full_cmd.add_simple_arg("--restart-max-attempts", restart_max_attempts) - full_cmd.add_args_list( - "--mount", [",".join(format_dict_for_cli(s)) for s in mounts or []] + full_cmd.add_args_iterable_or_single( + "--mount", [",".join(format_mapping_for_cli(s)) for s in mounts or []] ) full_cmd.add_simple_arg("--network", network) - full_cmd.add_args_list( - "--secret", [",".join(format_dict_for_cli(s)) for s in secrets or []] + full_cmd.add_args_iterable_or_single( + "--secret", [",".join(format_mapping_for_cli(s)) for s in secrets or []] ) full_cmd.append(image) @@ -364,7 +364,9 @@ def list(self, filters: Dict[str, str] = {}) -> List[Service]: A `List[python_on_whales.Services]` """ full_cmd = self.docker_cmd + ["service", "list", "--quiet"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) ids_truncated = run(full_cmd).splitlines() diff --git a/python_on_whales/components/stack/cli_wrapper.py b/python_on_whales/components/stack/cli_wrapper.py index 294a24e2..2011985b 100644 --- a/python_on_whales/components/stack/cli_wrapper.py +++ b/python_on_whales/components/stack/cli_wrapper.py @@ -79,7 +79,7 @@ def deploy( """ full_cmd = self.docker_cmd + ["stack", "deploy"] - full_cmd.add_args_list("--compose-file", compose_files) + full_cmd.add_args_iterable_or_single("--compose-file", compose_files) full_cmd.add_simple_arg("--orchestrator", orchestrator) full_cmd.add_flag("--prune", prune) full_cmd.add_simple_arg("--resolve-image", resolve_image) diff --git a/python_on_whales/components/system/cli_wrapper.py b/python_on_whales/components/system/cli_wrapper.py index 627001bf..b9c84790 100644 --- a/python_on_whales/components/system/cli_wrapper.py +++ b/python_on_whales/components/system/cli_wrapper.py @@ -9,7 +9,7 @@ SystemInfo, ) from python_on_whales.utils import ( - format_dict_for_cli, + format_mapping_for_cli, format_time_arg, run, stream_stdout_and_stderr, @@ -122,7 +122,9 @@ def events( ] full_cmd.add_simple_arg("--since", format_time_arg(since)) full_cmd.add_simple_arg("--until", format_time_arg(until)) - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) iterator = stream_stdout_and_stderr(full_cmd) for stream_origin, stream_content in iterator: if stream_origin == "stdout": @@ -168,5 +170,7 @@ def prune( full_cmd = self.docker_cmd + ["system", "prune", "--force"] full_cmd.add_flag("--all", all) full_cmd.add_flag("--volumes", volumes) - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) run(full_cmd) diff --git a/python_on_whales/components/volume/cli_wrapper.py b/python_on_whales/components/volume/cli_wrapper.py index 7cb647ed..31f78b4b 100644 --- a/python_on_whales/components/volume/cli_wrapper.py +++ b/python_on_whales/components/volume/cli_wrapper.py @@ -18,7 +18,7 @@ from python_on_whales.components.volume.models import VolumeInspectResult from python_on_whales.exceptions import NoSuchVolume from python_on_whales.test_utils import random_name -from python_on_whales.utils import ValidPath, format_dict_for_cli, run, to_list +from python_on_whales.utils import ValidPath, format_mapping_for_cli, run, to_list class Volume(ReloadableObjectFromJson): @@ -186,7 +186,9 @@ def list(self, filters: Dict[str, Union[str, int]] = {}) -> List[Volume]: """ full_cmd = self.docker_cmd + ["volume", "list", "--quiet"] - full_cmd.add_args_list("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) volumes_names = run(full_cmd).splitlines() diff --git a/python_on_whales/utils.py b/python_on_whales/utils.py index fec99672..19fcddb2 100644 --- a/python_on_whales/utils.py +++ b/python_on_whales/utils.py @@ -8,7 +8,18 @@ from queue import Queue from subprocess import PIPE, Popen from threading import Thread -from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union, overload +from typing import ( + Any, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Union, + overload, +) import pydantic from typing_extensions import Literal @@ -236,6 +247,10 @@ def post_process_stream(stream: Optional[bytes]): def to_list(x) -> list: if isinstance(x, list): return x + elif isinstance(x, (str, bytes)): + return [x] + elif isinstance(x, Iterable): + return list(x) else: return [x] @@ -297,8 +312,8 @@ def stream_stdout_and_stderr( raise DockerException(full_cmd, exit_code, stderr=full_stderr) -def format_dict_for_cli(dictionary: Dict[str, str], separator="="): - return [f"{key}{separator}{value}" for key, value in dictionary.items()] +def format_mapping_for_cli(mapping: Mapping[str, str], separator="="): + return [f"{key}{separator}{value}" for key, value in mapping.items()] def read_env_file(env_file: Path) -> Dict[str, str]: