Repository: gabrieldemarmiesse/python-on-whales (language: python)

Issue:
Support sudo or list for client_binary
For podman I usually need running it with sudo.
Temporary workaround:
```python
class SudoWhalesClientConfig(python_on_whales.client_config.ClientConfig):
    sudo_binary: str = "sudo"
    @property
    def docker_cmd(self):
        result = super().docker_cmd
        result.insert(0, pathlib.Path(shutil.which(self.sudo_binary)))
        return result

podman = python_on_whales.DockerClient(client_config=SudoWhalesClientConfig(client_binary='podman'))
```

But I think, we need `client_binary` to support list of strings.


Discussion / hints:
I'm unsure about supporting sudo. I'll have to think about the implications. Of course, since users might need it, I'll provide a "best effort" support of it
Would it be possible for you to start your whole program with sudo? Sometying like `sudo python my_file.py`. I'm trying to understand the use case a bit better
Maybe something like `client_binary=['sudo','python','my_file.py']`?

Fix patch:
diff --git a/docs/autogen.py b/docs/autogen.py
index 97b0251c..84e5bc93 100644
--- a/docs/autogen.py
+++ b/docs/autogen.py
@@ -11,8 +11,7 @@
 print(a)
 pages = {
     "docker_client.md": ["python_on_whales.DockerClient"]
-    + get_methods("python_on_whales.docker_client.DockerClient")
-    + ["python_on_whales.get_docker_client_binary_path"],
+    + get_methods("python_on_whales.docker_client.DockerClient"),
     "sub-commands/buildx.md": get_methods(
         "python_on_whales.components.buildx.cli_wrapper.BuildxCLI"
     )
diff --git a/docs/template/docker_client.md b/docs/template/docker_client.md
index bfc0c414..85e4fa9c 100644
--- a/docs/template/docker_client.md
+++ b/docs/template/docker_client.md
@@ -114,3 +114,8 @@ you can run from your shell:
 python-on-whales download-cli
 ```
 
+# Handling an unavailable client
+
+Trying to use Python-on-whales when it cannot find or download a Docker client binary
+will trigger a `python_on_whales.ClientNotFoundError`. You can use a try-except around 
+a first `docker.ps()` call to handle the case when Python-on-whales won't work.
diff --git a/python_on_whales/client_config.py b/python_on_whales/client_config.py
index 5042ecf0..bb767cd2 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, Optional
+from typing import Any, Dict, List, Optional, Union
 
 import pydantic
 
@@ -64,19 +64,22 @@ class ClientConfig:
     compose_project_name: Optional[str] = None
     compose_project_directory: Optional[ValidPath] = None
     compose_compatibility: Optional[bool] = None
-    client_binary: str = "docker"
+    client_call: List[str] = field(default_factory=lambda: ["docker"])
+    _client_call_with_path: Optional[List[Union[Path, str]]] = None
 
-    def get_docker_path(self) -> ValidPath:
-        if self.client_binary_path is None:
-            self.client_binary_path = Path(self._get_docker_path())
+    def get_client_call_with_path(self) -> List[Union[Path, str]]:
+        if self._client_call_with_path is None:
+            self._client_call_with_path = [
+                Path(self._get_docker_path())
+            ] + self.client_call[1:]
 
-        return self.client_binary_path
+        return self._client_call_with_path
 
     def _get_docker_path(self) -> str:
-        which_result = shutil.which(self.client_binary)
+        which_result = shutil.which(self.client_call[0])
         if which_result is not None:
             return which_result
-        if self.client_binary == "docker":
+        if self.client_call[0] == "docker":
             if not get_docker_binary_path_in_cache().exists():
                 warnings.warn(
                     "The docker client binary file was not found on your system. \n"
@@ -96,7 +99,7 @@ def _get_docker_path(self) -> str:
             return get_docker_binary_path_in_cache()
 
         raise ClientNotFoundError(
-            f"The binary '{self.client_binary}' could not be found on your PATH. "
+            f"The binary '{self.client_call[0]}' could not be found on your PATH. "
             f"Please ensure that your PATH is has the directory of the binary you're looking for. "
             f"You can use `print(os.environ['PATH'])` to verify what directories are in your PATH."
         )
@@ -104,7 +107,7 @@ def _get_docker_path(self) -> str:
     @property
     def docker_cmd(self) -> Command:
 
-        result = Command([self.get_docker_path()])
+        result = Command(self.get_client_call_with_path())
 
         if self.config is not None:
             result += ["--config", self.config]
diff --git a/python_on_whales/docker_client.py b/python_on_whales/docker_client.py
index 632e7552..364a94e4 100644
--- a/python_on_whales/docker_client.py
+++ b/python_on_whales/docker_client.py
@@ -1,4 +1,5 @@
 import base64
+import warnings
 from typing import List, Optional
 
 from python_on_whales.client_config import ClientConfig, DockerCLICaller
@@ -60,6 +61,24 @@ class DockerClient(DockerCLICaller):
         compose_project_directory: Use an alternate working directory. By default, it
             uses the path of the compose file.
         compose_compatibility: Use docker compose in compatibility mode.
+        client_call: Client binary to use and how to call it. Default is `["docker"]`. You can try with
+            for example `["podman"]` or `["nerdctl"]`. The client must have the same commands and
+            outputs as Docker to work. Some best effort support is done in case of divergences,
+            meaning you can report issues occuring on some other binary than Docker, but
+            we don't guarantee that it will be fixed.
+            This option is a list because you can provide a list of command line arguments to be placed after `"docker"`.
+            For exemple `host="ssh://my_user@host.com"` is equivalent
+            to `client_call=["docker", "--host=ssh://my_user@host.com"]`.
+            This will allow you to use some exotic options that are not explicitly supported by Python-on-whales.
+            Let's say you want to use estargz to run a container immediately, without waiting for the "pull"
+            to finish (yes it's possible!), you can
+            do `nerdctl = DockerClient(client_call=["nerdctl", "--snapshotter=stargz"])`
+            and then `nerdctl.run("ghcr.io/stargz-containers/python:3.7-org", ["-c", "print('hi')"])`.
+            You can also use this system to call Docker with sudo with `client_call=["sudo", "docker"]`
+            (note that it won't ask for your password, so sudo should be passwordless during the python
+            program execution).
+        client_binary: Deprecated, use `client_call`. If you used before `client_binary="podman"`, now use
+            `client_call=["podman"]`.
     """
 
     def __init__(
@@ -82,7 +101,14 @@ def __init__(
         compose_project_directory: Optional[ValidPath] = None,
         compose_compatibility: Optional[bool] = None,
         client_binary: str = "docker",
+        client_call: List[str] = ["docker"],
     ):
+        if client_binary != "docker":
+            warnings.warn(
+                "The usage of client_binary is deprecated, use `client_call` instead. For "
+                "example if you used `client_binary='podman'`, now you should use `client_call=['podman']`."
+            )
+            client_call = [client_binary]
 
         if client_config is None:
             client_config = ClientConfig(
@@ -102,7 +128,7 @@ def __init__(
                 compose_project_name=compose_project_name,
                 compose_project_directory=compose_project_directory,
                 compose_compatibility=compose_compatibility,
-                client_binary=client_binary,
+                client_call=client_call,
             )
         super().__init__(client_config)
 


Test patch:
diff --git a/tests/python_on_whales/test_docker_client.py b/tests/python_on_whales/test_docker_client.py
index 28487992..e76cf34b 100644
--- a/tests/python_on_whales/test_docker_client.py
+++ b/tests/python_on_whales/test_docker_client.py
@@ -1,8 +1,9 @@
+import time
 from pathlib import Path
 
 import pytest
 
-from python_on_whales import docker
+from python_on_whales import DockerClient, docker
 from python_on_whales.exceptions import DockerException
 
 
@@ -26,3 +27,28 @@ def test_login_logout(docker_registry_without_login):
         docker_registry_without_login
         not in (Path.home() / ".docker" / "config.json").read_text()
     )
+
+
+@pytest.mark.skipif(True, reason="It doesn't work in the ci")
+def test_docker_client_options():
+    if docker.container.exists("test_dind_container"):
+        docker.container.remove("test_dind_container", force=True, volumes=True)
+
+    with docker.run(
+        "docker:20.10.16-dind",
+        ["dockerd", "--host=tcp://0.0.0.0:2375", "--tls=false"],
+        remove=True,
+        privileged=True,
+        publish=[(2380, 2375)],
+        detach=True,
+        name="test_dind_container",
+    ):
+        time.sleep(10)
+        dind_client = DockerClient(host="tcp://localhost:2380")
+        assert dind_client.image.list() == []
+        assert "Hello from Docker!" in dind_client.run("hello-world")
+
+        dind_client = DockerClient(
+            client_call=["docker", "--host=tcp://localhost:2380"]
+        )
+        assert "Hello from Docker!" in dind_client.run("hello-world")