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

Issue:
image list command won't take image name as argument
In docker CLI one can use the following command:
```
$ docker image ls ubuntu
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
ubuntu       20.04     597ce1600cf4   10 months ago   72.8MB
```
I couldn't find any way to replicate this with `python_on_whales`. Here are my attempts:
```
❯ ipython
Python 3.10.2 (main, Jan 15 2022, 19:56:27) [GCC 11.1.0]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.1.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from python_on_whales import docker

In [2]: docker.image.list('ubuntu')
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [2], in <cell line: 1>()
----> 1 docker.image.list('ubuntu')

File ~/.local/lib/python3.10/site-packages/python_on_whales/components/image/cli_wrapper.py:418, in ImageCLI.list(self, filters)
    403 """Returns the list of Docker images present on the machine.
    404 
    405 Alias: `docker.images()`
   (...)
    410     A `List[python_on_whales.Image]` object.
    411 """
    412 full_cmd = self.docker_cmd + [
    413     "image",
    414     "list",
    415     "--quiet",
    416     "--no-trunc",
    417 ]
--> 418 full_cmd.add_args_list("--filter", format_dict_for_cli(filters))
    420 ids = run(full_cmd).splitlines()
    421 # the list of tags is bigger than the number of images. We uniquify

File ~/.local/lib/python3.10/site-packages/python_on_whales/utils.py:268, in format_dict_for_cli(dictionary, separator)
    267 def format_dict_for_cli(dictionary: Dict[str, str], separator="="):
--> 268     return [f"{key}{separator}{value}" for key, value in dictionary.items()]

AttributeError: 'str' object has no attribute 'items'
```
Using the `filters` argument seems to provide a workaround (`docker.image.list({'reference':'ubuntu'})`, but this seems to be against `python_on_whales` promise to be "a 1-to-1 mapping between the Docker CLI and the Python library".


Discussion / hints:
Thank you for the bug report, I will look it up :) 
I seems that I overlooked that option when I implemented the `docker.image.list()` you are right.

I should be able to fix this quite easily. I'll keep you updated :) 

Fix patch:
diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py
index a6dd1383..fb8805d2 100644
--- a/python_on_whales/components/image/cli_wrapper.py
+++ b/python_on_whales/components/image/cli_wrapper.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import warnings
 from datetime import datetime
 from multiprocessing.pool import ThreadPool
 from pathlib import Path
@@ -399,7 +400,12 @@ def _load_from_generator(self, full_cmd: List[str], input: Iterator[bytes]):
             raise DockerException(full_cmd, exit_code)
         return stdout.decode().splitlines()
 
-    def list(self, filters: Dict[str, str] = {}) -> List[Image]:
+    def list(
+        self,
+        repository_or_tag: Optional[str] = None,
+        filters: Dict[str, str] = {},
+        all: bool = False,
+    ) -> List[Image]:
         """Returns the list of Docker images present on the machine.
 
         Alias: `docker.images()`
@@ -409,6 +415,21 @@ def list(self, filters: Dict[str, str] = {}) -> List[Image]:
         # Returns
             A `List[python_on_whales.Image]` object.
         """
+        # previously the signature was
+        # def list(self,filters: Dict[str, str] = {}) -> List[Image]:
+        # so to avoid breakages when people used positional arguments, we can check the types and send a warning
+        if isinstance(repository_or_tag, dict):
+            # after a while, we can convert that to an error. No hurry though.
+            warnings.warn(
+                f"You are calling docker.image.list({repository_or_tag}) with the filter as the first argument."
+                f"Since Python-on-whales v0.51.0, the first argument has be changed to `repository_or_tag`."
+                f"To fix this warning, please add the filters keyword argument, "
+                f"like so: docker.image.list(filters={repository_or_tag}) ",
+                DeprecationWarning,
+            )
+            filters = repository_or_tag
+            repository_or_tag = None
+
         full_cmd = self.docker_cmd + [
             "image",
             "list",
@@ -416,6 +437,10 @@ def list(self, filters: Dict[str, str] = {}) -> List[Image]:
             "--no-trunc",
         ]
         full_cmd.add_args_list("--filter", format_dict_for_cli(filters))
+        full_cmd.add_flag("--all", all)
+
+        if repository_or_tag is not None:
+            full_cmd.append(repository_or_tag)
 
         ids = run(full_cmd).splitlines()
         # the list of tags is bigger than the number of images. We uniquify


Test patch:
diff --git a/tests/python_on_whales/components/test_image.py b/tests/python_on_whales/components/test_image.py
index 5196541b..f8a8c7af 100644
--- a/tests/python_on_whales/components/test_image.py
+++ b/tests/python_on_whales/components/test_image.py
@@ -47,7 +47,7 @@ def test_save_iterator_bytes():
 
 
 def test_filter_when_listing():
-    docker.pull("hello-world")
+    docker.pull(["hello-world", "busybox"])
     images_listed = docker.image.list(filters=dict(reference="hello-world"))
     tags = set()
     for image in images_listed:
@@ -56,6 +56,33 @@ def test_filter_when_listing():
     assert tags == {"hello-world:latest"}
 
 
+def test_filter_when_listing_old_signature():
+    """Check backward compatibility"""
+    docker.pull(["hello-world", "busybox"])
+    with pytest.warns(DeprecationWarning) as warnings_emmitted:
+        images_listed = docker.image.list({"reference": "hello-world"})
+
+    warning_message = str(warnings_emmitted.list[0].message)
+    assert "docker.image.list({'reference': 'hello-world'}" in warning_message
+    assert "docker.image.list(filters={'reference': 'hello-world'}" in warning_message
+    tags = set()
+    for image in images_listed:
+        for tag in image.repo_tags:
+            tags.add(tag)
+    assert tags == {"hello-world:latest"}
+
+
+def test_use_first_argument_to_filter():
+    """Check backward compatibility"""
+    docker.pull(["hello-world", "busybox"])
+    images_listed = docker.image.list("hello-world")
+    tags = set()
+    for image in images_listed:
+        for tag in image.repo_tags:
+            tags.add(tag)
+    assert tags == {"hello-world:latest"}
+
+
 def test_save_iterator_bytes_and_load():
     image_name = "busybox:1"
     docker.image.pull(image_name, quiet=True)