From cbbd6b4f7eda162869854689022ebb9a544f2936 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 25 May 2024 14:09:30 +0100 Subject: [PATCH 1/6] Change arg list handling to support iterables --- python_on_whales/client_config.py | 12 +- .../components/buildx/cli_wrapper.py | 22 +-- .../components/compose/cli_wrapper.py | 8 +- .../components/config/cli_wrapper.py | 4 +- .../components/container/cli_wrapper.py | 148 ++++++++++-------- .../components/image/cli_wrapper.py | 16 +- .../components/network/cli_wrapper.py | 12 +- .../components/secret/cli_wrapper.py | 4 +- .../components/service/cli_wrapper.py | 28 ++-- .../components/stack/cli_wrapper.py | 2 +- .../components/system/cli_wrapper.py | 4 +- .../components/volume/cli_wrapper.py | 2 +- python_on_whales/utils.py | 4 +- 13 files changed, 144 insertions(+), 122 deletions(-) diff --git a/python_on_whales/client_config.py b/python_on_whales/client_config.py index 207961c1..ca425f92 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, Optional, Union import pydantic @@ -37,8 +37,10 @@ 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_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__(self, other) -> "Command": @@ -144,8 +146,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..63edb851 100644 --- a/python_on_whales/components/buildx/cli_wrapper.py +++ b/python_on_whales/components/buildx/cli_wrapper.py @@ -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_dict_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( + full_cmd.add_args_iterable_or_single( "--add-host", format_dict_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_dict_for_cli(build_args) + ) + full_cmd.add_args_iterable_or_single( + "--build-context", format_dict_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_dict_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,7 @@ 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_dict_for_cli(filters)) if stream_logs: return stream_buildx_logs(full_cmd) run(full_cmd) diff --git a/python_on_whales/components/compose/cli_wrapper.py b/python_on_whales/components/compose/cli_wrapper.py index 843acf1c..2362d076 100644 --- a/python_on_whales/components/compose/cli_wrapper.py +++ b/python_on_whales/components/compose/cli_wrapper.py @@ -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_dict_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,7 @@ 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_dict_for_cli(filters)) return [ ComposeProject( @@ -751,7 +753,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_dict_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..3d0c5110 100644 --- a/python_on_whales/components/config/cli_wrapper.py +++ b/python_on_whales/components/config/cli_wrapper.py @@ -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_dict_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,7 @@ 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_dict_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..14be5e00 100644 --- a/python_on_whales/components/container/cli_wrapper.py +++ b/python_on_whales/components/container/cli_wrapper.py @@ -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_dict_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_dict_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_dict_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_dict_for_cli(envs)) + full_cmd.add_args_iterable_or_single("--env-file", env_files) if interactive and stream: raise ValueError( @@ -1136,7 +1142,7 @@ 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_dict_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 +1209,7 @@ 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_dict_for_cli(filters)) if stream_logs: return stream_stdout_and_stderr(full_cmd) run(full_cmd) @@ -1587,13 +1593,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 +1618,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_dict_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 +1669,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_dict_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 +1686,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 +1702,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 +1715,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 +1724,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_dict_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 +1740,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..912f83c4 100644 --- a/python_on_whales/components/image/cli_wrapper.py +++ b/python_on_whales/components/image/cli_wrapper.py @@ -269,17 +269,19 @@ def legacy_build( tags = to_list(tags) full_cmd = self.docker_cmd + ["build", "--quiet"] - full_cmd.add_args_list( + full_cmd.add_args_iterable_or_single( "--add-host", format_dict_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_dict_for_cli(build_args) + ) + full_cmd.add_args_iterable_or_single("--label", format_dict_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,7 @@ 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_dict_for_cli(filters)) full_cmd.add_flag("--all", all) if repository_or_tag is not None: @@ -474,7 +476,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_dict_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..83b9e1b5 100644 --- a/python_on_whales/components/network/cli_wrapper.py +++ b/python_on_whales/components/network/cli_wrapper.py @@ -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_dict_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,14 @@ 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_dict_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_dict_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..7d038d58 100644 --- a/python_on_whales/components/secret/cli_wrapper.py +++ b/python_on_whales/components/secret/cli_wrapper.py @@ -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_dict_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,7 @@ 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_dict_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..c1b39cfe 100644 --- a/python_on_whales/components/service/cli_wrapper.py +++ b/python_on_whales/components/service/cli_wrapper.py @@ -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_dict_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,18 +227,18 @@ 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_dict_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( + full_cmd.add_args_iterable_or_single( "--mount", [",".join(format_dict_for_cli(s)) for s in mounts or []] ) full_cmd.add_simple_arg("--network", network) - full_cmd.add_args_list( + full_cmd.add_args_iterable_or_single( "--secret", [",".join(format_dict_for_cli(s)) for s in secrets or []] ) @@ -364,7 +364,7 @@ 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_dict_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..dccb5318 100644 --- a/python_on_whales/components/system/cli_wrapper.py +++ b/python_on_whales/components/system/cli_wrapper.py @@ -122,7 +122,7 @@ 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_dict_for_cli(filters)) iterator = stream_stdout_and_stderr(full_cmd) for stream_origin, stream_content in iterator: if stream_origin == "stdout": @@ -168,5 +168,5 @@ 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_dict_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..84821af4 100644 --- a/python_on_whales/components/volume/cli_wrapper.py +++ b/python_on_whales/components/volume/cli_wrapper.py @@ -186,7 +186,7 @@ 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_dict_for_cli(filters)) volumes_names = run(full_cmd).splitlines() diff --git a/python_on_whales/utils.py b/python_on_whales/utils.py index 7643adfe..99484674 100644 --- a/python_on_whales/utils.py +++ b/python_on_whales/utils.py @@ -232,8 +232,8 @@ def post_process_stream(stream: Optional[bytes]): def to_list(x) -> list: - if isinstance(x, list): - return x + if isinstance(x, Iterable): + return list(x) else: return [x] From e35c740c460c77c279bb8f605d5e92561e142bbe Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 25 May 2024 14:20:01 +0100 Subject: [PATCH 2/6] Rename 'format_dict_for_cli()' to target generic mappings --- .../components/buildx/cli_wrapper.py | 18 +++++++------- .../components/compose/cli_wrapper.py | 10 ++++---- .../components/config/cli_wrapper.py | 8 ++++--- .../components/container/cli_wrapper.py | 24 +++++++++++-------- .../components/image/cli_wrapper.py | 14 ++++++----- .../components/network/cli_wrapper.py | 12 ++++++---- .../components/secret/cli_wrapper.py | 8 ++++--- .../components/service/cli_wrapper.py | 14 ++++++----- .../components/system/cli_wrapper.py | 10 +++++--- .../components/volume/cli_wrapper.py | 6 +++-- python_on_whales/utils.py | 17 ++++++++++--- 11 files changed, 89 insertions(+), 52 deletions(-) diff --git a/python_on_whales/components/buildx/cli_wrapper.py b/python_on_whales/components/buildx/cli_wrapper.py index 63edb851..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_iterable_or_single("--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: @@ -327,19 +327,19 @@ def build( full_cmd += ["--progress", progress] full_cmd.add_args_iterable_or_single( - "--add-host", format_dict_for_cli(add_hosts, separator=":") + "--add-host", format_mapping_for_cli(add_hosts, separator=":") ) 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_iterable_or_single( - "--build-arg", format_dict_for_cli(build_args) + "--build-arg", format_mapping_for_cli(build_args) ) full_cmd.add_args_iterable_or_single( - "--build-context", format_dict_for_cli(build_contexts) + "--build-context", format_mapping_for_cli(build_contexts) ) full_cmd.add_simple_arg("--builder", builder) - full_cmd.add_args_iterable_or_single("--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) @@ -562,7 +562,9 @@ def prune( """ full_cmd = self.docker_cmd + ["buildx", "prune", "--force"] full_cmd.add_flag("--all", all) - full_cmd.add_args_iterable_or_single("--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) @@ -644,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 2362d076..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, @@ -90,7 +90,7 @@ def build( full_cmd = self.docker_compose_cmd + ["build"] full_cmd.add_args_iterable_or_single( - "--build-arg", format_dict_for_cli(build_args) + "--build-arg", format_mapping_for_cli(build_args) ) full_cmd.add_flag("--no-cache", not cache) full_cmd.add_simple_arg("--progress", progress) @@ -502,7 +502,9 @@ def ls( """ full_cmd = self.docker_compose_cmd + ["ls", "--format", "json"] full_cmd.add_flag("--all", all) - full_cmd.add_args_iterable_or_single("--filter", format_dict_for_cli(filters)) + full_cmd.add_args_iterable_or_single( + "--filter", format_mapping_for_cli(filters) + ) return [ ComposeProject( @@ -753,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_iterable_or_single("--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 3d0c5110..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_iterable_or_single("--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_iterable_or_single("--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 14be5e00..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, @@ -709,7 +709,7 @@ def create( full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_iterable_or_single("--env", format_dict_for_cli(envs)) + 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) @@ -740,7 +740,7 @@ def create( full_cmd.add_simple_arg("--isolation", isolation) full_cmd.add_simple_arg("--kernel-memory", kernel_memory) - full_cmd.add_args_iterable_or_single("--label", format_dict_for_cli(labels)) + 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_iterable_or_single("--link", link) @@ -794,7 +794,7 @@ def create( full_cmd.add_simple_arg("--stop-timeout", stop_timeout) full_cmd.add_args_iterable_or_single("--storage-opt", storage_options) - full_cmd.add_args_iterable_or_single("--sysctl", format_dict_for_cli(sysctl)) + full_cmd.add_args_iterable_or_single("--sysctl", format_mapping_for_cli(sysctl)) full_cmd.add_simple_arg("--systemd", systemd) full_cmd.add_args_iterable_or_single("--tmpfs", tmpfs) full_cmd.add_flag("--tty", tty) @@ -907,7 +907,7 @@ def execute( full_cmd.add_flag("--detach", detach) full_cmd.add_simple_arg("--detach-keys", detach_keys) - full_cmd.add_args_iterable_or_single("--env", format_dict_for_cli(envs)) + 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: @@ -1142,7 +1142,9 @@ def list( """ full_cmd = self.docker_cmd full_cmd += ["container", "list", "-q", "--no-trunc"] - full_cmd.add_args_iterable_or_single("--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 @@ -1209,7 +1211,9 @@ def prune( "docker.container.prune(filters={...})" ) full_cmd = self.docker_cmd + ["container", "prune", "--force"] - full_cmd.add_args_iterable_or_single("--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) @@ -1637,7 +1641,7 @@ def run( full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_iterable_or_single("--env", format_dict_for_cli(envs)) + 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) @@ -1669,7 +1673,7 @@ def run( full_cmd.add_simple_arg("--isolation", isolation) full_cmd.add_simple_arg("--kernel-memory", kernel_memory) - full_cmd.add_args_iterable_or_single("--label", format_dict_for_cli(labels)) + 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_iterable_or_single("--link", link) @@ -1725,7 +1729,7 @@ def run( full_cmd.add_simple_arg("--stop-timeout", stop_timeout) full_cmd.add_args_iterable_or_single("--storage-opt", storage_options) - full_cmd.add_args_iterable_or_single("--sysctl", format_dict_for_cli(sysctl)) + full_cmd.add_args_iterable_or_single("--sysctl", format_mapping_for_cli(sysctl)) full_cmd.add_simple_arg("--systemd", systemd) full_cmd.add_args_iterable_or_single("--tmpfs", tmpfs) full_cmd.add_flag("--tty", tty) diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py index 912f83c4..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, @@ -270,12 +270,12 @@ def legacy_build( full_cmd = self.docker_cmd + ["build", "--quiet"] full_cmd.add_args_iterable_or_single( - "--add-host", format_dict_for_cli(add_hosts, separator=":") + "--add-host", format_mapping_for_cli(add_hosts, separator=":") ) full_cmd.add_args_iterable_or_single( - "--build-arg", format_dict_for_cli(build_args) + "--build-arg", format_mapping_for_cli(build_args) ) - full_cmd.add_args_iterable_or_single("--label", format_dict_for_cli(labels)) + 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) @@ -452,7 +452,9 @@ def list( "--quiet", "--no-trunc", ] - full_cmd.add_args_iterable_or_single("--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: @@ -476,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_iterable_or_single("--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 83b9e1b5..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): @@ -187,7 +187,7 @@ 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_iterable_or_single("--label", format_dict_for_cli(labels)) + 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_iterable_or_single("--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_iterable_or_single("--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 7d038d58..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_iterable_or_single("--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_iterable_or_single("--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 c1b39cfe..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, @@ -206,7 +206,7 @@ def create( full_cmd.add_simple_arg("--endpoint-mode", endpoint_mode) full_cmd.add_simple_arg("--entrypoint", entrypoint) - full_cmd.add_args_iterable_or_single("--env", format_dict_for_cli(envs)) + 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_iterable_or_single("--generic-resource", generic_resources) @@ -227,7 +227,7 @@ def create( full_cmd.add_flag("--init", init) full_cmd.add_simple_arg("--isolation", isolation) - full_cmd.add_args_iterable_or_single("--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) @@ -235,11 +235,11 @@ def create( full_cmd.add_simple_arg("--restart-condition", restart_condition) full_cmd.add_simple_arg("--restart-max-attempts", restart_max_attempts) full_cmd.add_args_iterable_or_single( - "--mount", [",".join(format_dict_for_cli(s)) for s in mounts or []] + "--mount", [",".join(format_mapping_for_cli(s)) for s in mounts or []] ) full_cmd.add_simple_arg("--network", network) full_cmd.add_args_iterable_or_single( - "--secret", [",".join(format_dict_for_cli(s)) for s in secrets or []] + "--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_iterable_or_single("--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/system/cli_wrapper.py b/python_on_whales/components/system/cli_wrapper.py index dccb5318..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_iterable_or_single("--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_iterable_or_single("--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 84821af4..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_iterable_or_single("--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 99484674..b42dda91 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 @@ -295,8 +306,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]: From ac889907f329a14336d9bde446dc614f1e601ef1 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 25 May 2024 14:16:28 +0100 Subject: [PATCH 3/6] Add Command.add_args_iterable() and Command.add_args_mapping() --- python_on_whales/client_config.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/python_on_whales/client_config.py b/python_on_whales/client_config.py index ca425f92..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, Iterable, 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,12 +37,23 @@ def add_flag(self, name: str, value: bool): if value: self.append(name) + 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)) From 083d9ae009d7f17882de6d7e26c55e5a291c3c14 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 25 May 2024 14:47:51 +0100 Subject: [PATCH 4/6] Don't treat strings/bytes as iterables to be converted to lists --- python_on_whales/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/python_on_whales/utils.py b/python_on_whales/utils.py index b42dda91..00545478 100644 --- a/python_on_whales/utils.py +++ b/python_on_whales/utils.py @@ -243,7 +243,11 @@ def post_process_stream(stream: Optional[bytes]): def to_list(x) -> list: - if isinstance(x, Iterable): + if isinstance(x, list): + return x + elif isinstance(x, (str, bytes)): + return [x] + elif isinstance(x, Iterable): return list(x) else: return [x] From 375b001e5c07cee65f46bb4c944da94dfd0f35d9 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 25 May 2024 14:41:59 +0100 Subject: [PATCH 5/6] Update image component to use generic types with new command construction --- python_on_whales/client_config.py | 8 +- .../components/image/cli_wrapper.py | 151 ++++++++---------- 2 files changed, 70 insertions(+), 89 deletions(-) diff --git a/python_on_whales/client_config.py b/python_on_whales/client_config.py index 6709f4ab..93296576 100644 --- a/python_on_whales/client_config.py +++ b/python_on_whales/client_config.py @@ -15,7 +15,7 @@ ) from . import utils -from .utils import ValidPath, run, to_list +from .utils import ValidPath, run CACHE_VALIDITY_PERIOD = 0.01 @@ -44,7 +44,11 @@ def add_args_iterable(self, arg_name: str, values: Iterable[Any]): 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): + if isinstance(iterable_or_single, Iterable): + arg_values = iterable_or_single + else: + arg_values = [iterable_or_single] + for value in arg_values: self.extend([arg_name, value]) def add_args_mapping( diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py index 4cd21d31..996e0235 100644 --- a/python_on_whales/components/image/cli_wrapper.py +++ b/python_on_whales/components/image/cli_wrapper.py @@ -6,7 +6,7 @@ from multiprocessing.pool import ThreadPool from pathlib import Path from subprocess import PIPE, Popen -from typing import Any, Dict, Iterator, List, Optional, Union, overload +from typing import Any, Iterable, Iterator, List, Mapping, Optional, Union, overload import python_on_whales.components.buildx.cli_wrapper from python_on_whales.client_config import ( @@ -24,13 +24,7 @@ ImageRootFS, ) from python_on_whales.exceptions import DockerException, NoSuchImage -from python_on_whales.utils import ( - ValidPath, - format_mapping_for_cli, - run, - stream_stdout_and_stderr, - to_list, -) +from python_on_whales.utils import ValidPath, run, stream_stdout_and_stderr, to_list class Image(ReloadableObjectFromJson): @@ -49,7 +43,7 @@ def _fetch_inspect_result_json(self, reference): json_str = run(self.docker_cmd + ["image", "inspect", reference]) return json.loads(json_str)[0] - def _parse_json_object(self, json_object: Dict[str, Any]) -> ImageInspectResult: + def _parse_json_object(self, json_object: Mapping[str, Any]) -> ImageInspectResult: return ImageInspectResult(**json_object) def _get_inspect_result(self) -> ImageInspectResult: @@ -137,7 +131,7 @@ def root_fs(self) -> ImageRootFS: return self._get_inspect_result().root_fs @property - def metadata(self) -> Optional[Dict[str, str]]: + def metadata(self) -> Optional[Mapping[str, str]]: return self._get_inspect_result().metadata def __repr__(self): @@ -220,14 +214,14 @@ def __init__(self, client_config: ClientConfig): def legacy_build( self, context_path: ValidPath, - add_hosts: Dict[str, str] = {}, - build_args: Dict[str, str] = {}, + add_hosts: Mapping[str, str] = {}, + build_args: Mapping[str, str] = {}, cache: bool = True, file: Optional[ValidPath] = None, - labels: Dict[str, str] = {}, + labels: Mapping[str, str] = {}, network: Optional[str] = None, pull: bool = False, - tags: Union[str, List[str]] = [], + tags: Union[str, Iterable[str]] = (), target: Optional[str] = None, ) -> python_on_whales.components.image.cli_wrapper.Image: """Build a Docker image with the old Docker builder (meaning not using buildx/buildkit) @@ -254,7 +248,7 @@ def legacy_build( ex `build_args={"PY_VERSION": "3.7.8", "UBUNTU_VERSION": "20.04"}`. cache: Whether or not to use the cache, defaults to True file: The path of the Dockerfile, defaults to `context_path/Dockerfile` - labels: Dict of labels to add to the image. + labels: Mapping of labels to add to the image. `labels={"very-secure": "1", "needs-gpu": "0"}` for example. network: which network to use when building the Docker image pull: Always attempt to pull a newer version of the image @@ -266,16 +260,11 @@ def legacy_build( """ # to make it easier to write and read tests, the tests of this function # are also grouped with the tests of "docker.build()". - tags = to_list(tags) full_cmd = self.docker_cmd + ["build", "--quiet"] - full_cmd.add_args_iterable_or_single( - "--add-host", format_mapping_for_cli(add_hosts, separator=":") - ) - 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_args_mapping("--add-host", add_hosts, separator=":") + full_cmd.add_args_mapping("--build-arg", build_args) + full_cmd.add_args_mapping("--label", labels) full_cmd.add_flag("--pull", pull) full_cmd.add_simple_arg("--file", file) full_cmd.add_simple_arg("--target", target) @@ -298,7 +287,7 @@ def import_( self, source: ValidPath, tag: Optional[str] = None, - changes: List[str] = [], + changes: Iterable[str] = (), message: Optional[str] = None, platform: Optional[str] = None, ) -> Image: @@ -312,7 +301,7 @@ def import_( platform: Set platform if server is multi-platform capable """ full_cmd = self.docker_cmd + ["image", "import"] - full_cmd.add_args_iterable_or_single("--change", changes) + full_cmd.add_args_iterable("--change", changes) full_cmd.add_simple_arg("--message", message) full_cmd.add_simple_arg("--platform", platform) full_cmd.append(source) @@ -325,10 +314,10 @@ def inspect(self, x: str) -> Image: ... @overload - def inspect(self, x: List[str]) -> List[Image]: + def inspect(self, x: Iterable[str]) -> List[Image]: ... - def inspect(self, x: Union[str, List[str]]) -> Union[Image, List[Image]]: + def inspect(self, x: Union[str, Iterable[str]]) -> Union[Image, List[Image]]: """Creates a `python_on_whales.Image` object. # Returns @@ -339,10 +328,10 @@ def inspect(self, x: Union[str, List[str]]) -> Union[Image, List[Image]]: `python_on_whales.exceptions.NoSuchImage` if one of the images does not exists. """ - if isinstance(x, list): - return [Image(self.client_config, identifier) for identifier in x] - else: + if isinstance(x, str): return Image(self.client_config, x) + else: + return [Image(self.client_config, identifier) for identifier in x] def exists(self, x: str) -> bool: """Returns `True` if the image exists. `False` otherwise. @@ -419,7 +408,7 @@ def _load_from_generator(self, full_cmd: List[str], input: Iterator[bytes]): def list( self, repository_or_tag: Optional[str] = None, - filters: Dict[str, str] = {}, + filters: Mapping[str, str] = {}, all: bool = False, ) -> List[Image]: """Returns the list of Docker images present on the machine. @@ -452,9 +441,7 @@ def list( "--quiet", "--no-trunc", ] - full_cmd.add_args_iterable_or_single( - "--filter", format_mapping_for_cli(filters) - ) + full_cmd.add_args_mapping("--filter", filters) full_cmd.add_flag("--all", all) if repository_or_tag is not None: @@ -466,7 +453,7 @@ def list( return [Image(self.client_config, x, is_immutable_id=True) for x in ids] - def prune(self, all: bool = False, filter: Dict[str, str] = {}) -> str: + def prune(self, all: bool = False, filter: Mapping[str, str] = {}) -> str: """Remove unused images Parameters: @@ -478,12 +465,12 @@ 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_iterable_or_single("--filter", format_mapping_for_cli(filter)) + full_cmd.add_args_mapping("--filter", filter) return run(full_cmd) def pull( self, - x: Union[str, List[str]], + x: Union[str, Iterable[str]], quiet: bool = False, platform: Optional[str] = None, ) -> Union[Image, List[Image]]: @@ -504,14 +491,14 @@ def pull( If a list was passed as input, then a `List[python_on_whales.Image]` will be returned. """ - - if x == []: - return [] - elif isinstance(x, str): + if isinstance(x, str): return self._pull_single_tag(x, quiet=quiet, platform=platform) - elif isinstance(x, list) and len(x) == 1: + x = list(x) + if not x: + return [] + if len(x) == 1: return [self._pull_single_tag(x[0], quiet=quiet, platform=platform)] - elif len(x) >= 2: + else: pool = ThreadPool(4) generator = self._generate_args_pull(x, quiet, platform) all_images = pool.starmap(self._pull_single_tag, generator) @@ -520,7 +507,7 @@ def pull( return all_images def _generate_args_pull( - self, _list: List[str], quiet: bool, platform: Optional[str] = None + self, _list: Iterable[str], quiet: bool, platform: Optional[str] = None ): for tag in _list: yield tag, quiet, platform @@ -540,82 +527,72 @@ def _pull_single_tag( run(full_cmd, capture_stdout=quiet, capture_stderr=quiet) return Image(self.client_config, image_name) - def push(self, x: Union[str, List[str]], quiet: bool = False): + def push(self, x: Union[str, Iterable[str]], quiet: bool = False) -> None: """Push a tag or a repository to a registry Alias: `docker.push(...)` Parameters: - x: Tag(s) or repo(s) to push. Can be a string or a list of strings. - If it's a list of string, python-on-whales will push all the images with + x: Tag(s) or repo(s) to push. Can be a string or an iterable of strings. + If it's an iterable, python-on-whales will push all the images with multiple threads. The progress bars might look strange as multiple processes are drawing on the terminal at the same time. quiet: If you don't want to see the progress bars. # Raises - `python_on_whales.exceptions.NoSuchImage` if one of the images does not exists. + `python_on_whales.exceptions.NoSuchImage` if one of the images does not exist. """ - x = to_list(x) + images = to_list(x) # this is just to raise a correct exception if the images don't exist - self.inspect(x) + self.inspect(images) - if x == []: + if images == []: return - elif len(x) == 1: - self._push_single_tag(x[0], quiet=quiet) - elif len(x) >= 2: + elif len(images) == 1: + self._push_single_tag(images[0], quiet) + elif len(images) >= 2: pool = ThreadPool(4) - generator = self._generate_args_push(x, quiet) - pool.starmap(self._push_single_tag, generator) + pool.starmap(self._push_single_tag, ((img, quiet) for img in images)) pool.close() pool.join() - def _generate_args_push(self, _list: List[str], quiet: bool): - for tag in _list: - yield tag, quiet - def _push_single_tag(self, tag_or_repo: str, quiet: bool): full_cmd = self.docker_cmd + ["image", "push"] - - if quiet: - full_cmd.append("--quiet") - + full_cmd.add_flag(quiet) full_cmd.append(tag_or_repo) run(full_cmd, capture_stdout=quiet, capture_stderr=quiet) def remove( self, - x: Union[ValidImage, List[ValidImage]], + x: Union[ValidImage, Iterable[ValidImage]], force: bool = False, prune: bool = True, ): """Remove one or more docker images. Parameters: - x: Single image or list of Docker images to remove. You can use tags or + x: Single image or iterable of Docker images to remove. You can use tags or `python_on_whales.Image` objects. - force: Force removal of the image - prune: Delete untagged parents + force: Force removal of the image(s). + prune: Delete untagged parents. # Raises - `python_on_whales.exceptions.NoSuchImage` if one of the images does not exists. + `python_on_whales.exceptions.NoSuchImage` if one of the images does not exist. """ - + images = to_list(x) + if x == []: + return full_cmd = self.docker_cmd + ["image", "rm"] full_cmd.add_flag("--force", force) full_cmd.add_flag("--no-prune", not prune) - if x == []: - return - for image in to_list(x): - full_cmd.append(image) - + full_cmd.extend(images) run(full_cmd) def save( self, - images: Union[ValidImage, List[ValidImage]], + images: Union[ValidImage, Iterable[ValidImage]], output: Optional[ValidPath] = None, ) -> Optional[Iterator[bytes]]: """Save one or more images to a tar archive. Returns a stream if output is `None` @@ -623,7 +600,7 @@ def save( Alias: `docker.save(...)` Parameters: - images: Single docker image or list of docker images to save + images: Single image or non-empty iterable of images to save. output: Path of the tar archive to produce. If `output` is None, a generator of bytes is produced. It can be used to stream those bytes elsewhere, to another Docker daemon for example. @@ -632,7 +609,7 @@ def save( `Optional[Iterator[bytes]]`. If output is a path, nothing is returned. # Raises - `python_on_whales.exceptions.NoSuchImage` if one of the images does not exists. + `python_on_whales.exceptions.NoSuchImage` if one of the images does not exist. # Example @@ -652,26 +629,26 @@ def save( remote_docker.image.load(bytes_iterator) ``` - Of course the best solution is to use a registry to transfer image but + Of course the best solution is to use a registry to transfer images, but it's a cool example nonetheless. """ - full_cmd = self.docker_cmd + ["image", "save"] images = to_list(images) + if len(images) == 0: + raise ValueError("One or more images must be provided") - # trigger an exception early + # Trigger an exception early if an image doesn't exist. self.inspect(images) - if output is not None: - full_cmd += ["--output", str(output)] - - full_cmd += images + full_cmd = self.docker_cmd + ["image", "save"] + full_cmd.add_simple_arg("--output", output) + full_cmd.extend(images) if output is None: # we stream the bytes return self._save_generator(full_cmd) else: run(full_cmd) - def _save_generator(self, full_cmd) -> Iterator[bytes]: + def _save_generator(self, full_cmd: List[Any]) -> Iterator[bytes]: full_cmd = [str(x) for x in full_cmd] p = Popen(full_cmd, stdout=PIPE, stderr=PIPE) for line in p.stdout: @@ -693,7 +670,7 @@ def tag(self, source_image: Union[Image, str], new_tag: str): new_tag: The tag to add to the Docker image. # Raises - `python_on_whales.exceptions.NoSuchImage` if the image does not exists. + `python_on_whales.exceptions.NoSuchImage` if the image does not exist. """ full_cmd = self.docker_cmd + [ "image", From 8b18b73a3653db45f90ecd4a51678469ccf745e8 Mon Sep 17 00:00:00 2001 From: Lewis Gaul Date: Sat, 1 Jun 2024 16:40:35 +0100 Subject: [PATCH 6/6] Fix use of quiet flag --- python_on_whales/components/image/cli_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py index 996e0235..31f285fa 100644 --- a/python_on_whales/components/image/cli_wrapper.py +++ b/python_on_whales/components/image/cli_wrapper.py @@ -559,7 +559,7 @@ def push(self, x: Union[str, Iterable[str]], quiet: bool = False) -> None: def _push_single_tag(self, tag_or_repo: str, quiet: bool): full_cmd = self.docker_cmd + ["image", "push"] - full_cmd.add_flag(quiet) + full_cmd.add_flag("--quiet", quiet) full_cmd.append(tag_or_repo) run(full_cmd, capture_stdout=quiet, capture_stderr=quiet)