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

Issue:
Display better representation of docker objects
```
In [3]: docker.ps()
Out[3]:
[<python_on_whales.components.container.cli_wrapper.Container at 0x7f144b354e80>,
 <python_on_whales.components.container.cli_wrapper.Container at 0x7f1450026d60>]
```

We should display the type (`python_on_whales.Container`), the id and the name. Should use `__repr__`. This shouldn't be hard to do.


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index c93d71a4..7ce87297 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,9 @@ e6ca3592b144: Downloading [=============>                                     ]
 >>> print(my_docker_image.repo_tags)
 ['ubuntu:20.04']
 
+>>> docker.image.list()
+[python_on_whales.Image(id='sha256:1a437e363abfa', tags=['ubuntu:20.04'])]
+
 >>> my_docker_image.remove()
 ```
 
@@ -128,6 +131,8 @@ docker.run(
     envs={"POSTGRES_PASSWORD": "mysecretpassword"},
     detach=True,
 )
+print(docker.ps())
+# [python_on_whales.Container(id='f5fb939c409d', name='some-postgres')]
 ```
 
 -----
diff --git a/python_on_whales/components/buildx/cli_wrapper.py b/python_on_whales/components/buildx/cli_wrapper.py
index d0dc89cc..fa88feb8 100644
--- a/python_on_whales/components/buildx/cli_wrapper.py
+++ b/python_on_whales/components/buildx/cli_wrapper.py
@@ -60,6 +60,9 @@ def name(self) -> str:
     def driver(self) -> str:
         return self._get_inspect_result().driver
 
+    def __repr__(self):
+        return f"python_on_whales.Builder(name='{self.name}', driver='{self.driver}')"
+
     def remove(self):
         """Removes this builder. After this operation the builder cannot be used anymore.
 
diff --git a/python_on_whales/components/container/cli_wrapper.py b/python_on_whales/components/container/cli_wrapper.py
index b62c2b2b..f62ed95f 100644
--- a/python_on_whales/components/container/cli_wrapper.py
+++ b/python_on_whales/components/container/cli_wrapper.py
@@ -169,6 +169,9 @@ def config(self) -> ContainerConfig:
     def network_settings(self) -> NetworkSettings:
         return self._get_inspect_result().network_settings
 
+    def __repr__(self):
+        return f"python_on_whales.Container(id='{self.id[:12]}', name='{self.name}')"
+
     # --------------------------------------------------------------------
     # public methods
 
diff --git a/python_on_whales/components/image/cli_wrapper.py b/python_on_whales/components/image/cli_wrapper.py
index 55b41490..003cea6f 100644
--- a/python_on_whales/components/image/cli_wrapper.py
+++ b/python_on_whales/components/image/cli_wrapper.py
@@ -130,6 +130,9 @@ def root_fs(self) -> ImageRootFS:
     def metadata(self) -> Optional[Dict[str, str]]:
         return self._get_inspect_result().metadata
 
+    def __repr__(self):
+        return f"python_on_whales.Image(id='{self.id[:20]}', tags={self.repo_tags})"
+
     def remove(self, force: bool = False, prune: bool = True):
         """Remove this Docker image.
 
diff --git a/python_on_whales/components/network/cli_wrapper.py b/python_on_whales/components/network/cli_wrapper.py
index 9e809298..6b9535cc 100644
--- a/python_on_whales/components/network/cli_wrapper.py
+++ b/python_on_whales/components/network/cli_wrapper.py
@@ -99,6 +99,9 @@ def config_from(self) -> dict:
     def config_only(self) -> bool:
         return self._get_inspect_result().config_only
 
+    def __repr__(self):
+        return f"python_on_whales.Network(id='{self.id[:12]}', name={self.name})"
+
     def remove(self) -> None:
         """Removes this Docker network.
 
diff --git a/python_on_whales/components/node/cli_wrapper.py b/python_on_whales/components/node/cli_wrapper.py
index de08d85b..488bb472 100644
--- a/python_on_whales/components/node/cli_wrapper.py
+++ b/python_on_whales/components/node/cli_wrapper.py
@@ -68,6 +68,9 @@ def status(self) -> NodeStatus:
     def manager_status(self) -> Optional[NodeManagerStatus]:
         return self._get_inspect_result().manager_status
 
+    def __repr__(self):
+        return f"python_on_whales.Node(id='{self.id[:12]}', hostname='{self.description.hostname}', role='{self.spec.role}')"
+
     def update(
         self,
         availability: Optional[str] = None,
diff --git a/python_on_whales/components/plugin/cli_wrapper.py b/python_on_whales/components/plugin/cli_wrapper.py
index 603ea3dd..c5133ad2 100644
--- a/python_on_whales/components/plugin/cli_wrapper.py
+++ b/python_on_whales/components/plugin/cli_wrapper.py
@@ -65,6 +65,9 @@ def config(self) -> PluginConfig:
     def _docker_plugin(self) -> PluginCLI:
         return PluginCLI(self.client_config)
 
+    def __repr__(self):
+        return f"python_on_whales.Plugin(id='{self.id[:12]}', name='{self.name}')"
+
     def disable(self, force: bool = False) -> None:
         """Disable this plugin"""
         self._docker_plugin.disable(self, force=force)
@@ -203,7 +206,6 @@ def install(
     def list(self) -> List[Plugin]:
         """Returns a `List[python_on_whales.Plugin` that are installed on the daemon."""
         full_cmd = self.docker_cmd + [
-            "docker",
             "plugin",
             "list",
             "--no-trunc",
diff --git a/python_on_whales/components/stack/cli_wrapper.py b/python_on_whales/components/stack/cli_wrapper.py
index 64db3c96..a922516c 100644
--- a/python_on_whales/components/stack/cli_wrapper.py
+++ b/python_on_whales/components/stack/cli_wrapper.py
@@ -20,6 +20,9 @@ def __str__(self):
     def __eq__(self, other: Stack):
         return self.client_config == other.client_config and self.name == other.name
 
+    def __repr__(self):
+        return f"python_on_whales.Stack(name='{self.name}')"
+
     def remove(self) -> None:
         StackCLI(self.client_config).remove(self)
 
diff --git a/python_on_whales/components/task/cli_wrapper.py b/python_on_whales/components/task/cli_wrapper.py
index a37bb6a3..c6592e57 100644
--- a/python_on_whales/components/task/cli_wrapper.py
+++ b/python_on_whales/components/task/cli_wrapper.py
@@ -87,6 +87,9 @@ def status(self) -> TaskStatus:
     def desired_state(self) -> str:
         return self._get_inspect_result().desired_state
 
+    def __repr__(self):
+        return f"python_on_whales.Image(id='{self.id[:12]}', name={self.name})"
+
 
 class TaskCLI(DockerCLICaller):
     def list(self) -> List[Task]:
diff --git a/python_on_whales/components/volume/cli_wrapper.py b/python_on_whales/components/volume/cli_wrapper.py
index f4839e42..b285bdcb 100644
--- a/python_on_whales/components/volume/cli_wrapper.py
+++ b/python_on_whales/components/volume/cli_wrapper.py
@@ -74,6 +74,9 @@ def scope(self) -> str:
     def options(self) -> Optional[Dict[str, str]]:
         return self._get_inspect_result().options
 
+    def __repr__(self):
+        return f"python_on_whales.Volume(name='{self.name}', driver={self.driver})"
+
     def remove(self) -> None:
         """Removes this volume"""
         VolumeCLI(self.client_config).remove(self)


Test patch:
diff --git a/tests/python_on_whales/components/test_config.py b/tests/python_on_whales/components/test_config.py
index 95846bac..3beb38b2 100644
--- a/tests/python_on_whales/components/test_config.py
+++ b/tests/python_on_whales/components/test_config.py
@@ -17,6 +17,7 @@ def test_create_delete_config(tmp_path):
         assert my_conf.spec.name == "my_conf"
         assert docker.config.list() == [my_conf]
         assert docker.config.inspect("my_conf") == my_conf
+        repr(docker.config.list())
 
 
 @pytest.mark.usefixtures("swarm_mode")
diff --git a/tests/python_on_whales/components/test_container.py b/tests/python_on_whales/components/test_container.py
index 734d066d..979537fb 100644
--- a/tests/python_on_whales/components/test_container.py
+++ b/tests/python_on_whales/components/test_container.py
@@ -109,6 +109,17 @@ def test_fails_correctly():
         assert "Something is wrong!" in str(err.value)
 
 
+def test_container_repr():
+    with docker.run(
+        "ubuntu",
+        ["sleep", "infinity"],
+        remove=True,
+        detach=True,
+        stop_timeout=1,
+    ) as container:
+        assert container.id[:12] in repr(docker.container.list())
+
+
 def test_container_run_with_random_port():
     with docker.run(
         "ubuntu",
@@ -119,6 +130,7 @@ def test_container_run_with_random_port():
         stop_timeout=1,
     ) as container:
         assert container.network_settings.ports["90/tcp"][0]["HostPort"] is not None
+        assert container.id[:12] in repr(docker.container.list())
 
 
 def test_container_create_with_random_ports():
diff --git a/tests/python_on_whales/components/test_image.py b/tests/python_on_whales/components/test_image.py
index 91f3b143..1628dbfc 100644
--- a/tests/python_on_whales/components/test_image.py
+++ b/tests/python_on_whales/components/test_image.py
@@ -13,6 +13,14 @@ def test_load_json(json_file):
     # we could do more checks here if needed
 
 
+def test_image_repr():
+    docker.image.pull("busybox:1", quiet=True)
+    docker.image.pull("busybox:1.32", quiet=True)
+    assert "busybox:1" in repr(docker.image.list())
+    assert "busybox:1.32" in repr(docker.image.list())
+    docker.image.remove(["busybox:1", "busybox:1.32"])
+
+
 def test_image_remove():
     docker.image.pull("busybox:1", quiet=True)
     docker.image.pull("busybox:1.32", quiet=True)
diff --git a/tests/python_on_whales/components/test_node.py b/tests/python_on_whales/components/test_node.py
index 79412fe7..48f259df 100644
--- a/tests/python_on_whales/components/test_node.py
+++ b/tests/python_on_whales/components/test_node.py
@@ -24,6 +24,7 @@ def test_load_json(json_file):
 @pytest.mark.usefixtures("swarm_mode")
 def test_list_nodes():
     nodes = docker.node.list()
+    assert nodes[0].id[:12] in repr(nodes)
     assert len(nodes) == 1
 
 
diff --git a/tests/python_on_whales/components/test_plugin.py b/tests/python_on_whales/components/test_plugin.py
index 1bb2b262..761f1efd 100644
--- a/tests/python_on_whales/components/test_plugin.py
+++ b/tests/python_on_whales/components/test_plugin.py
@@ -26,6 +26,8 @@ def test_install_plugin_disable_enable():
     with docker.plugin.install(test_plugin_name) as my_plugin:
         my_plugin.disable()
         my_plugin.enable()
+        assert my_plugin in docker.plugin.list()
+        assert "name='vieux/sshfs:latest'" in repr(my_plugin)
 
 
 def test_plugin_upgrade():
diff --git a/tests/python_on_whales/components/test_stack.py b/tests/python_on_whales/components/test_stack.py
index fd0377cb..bbf5bc18 100644
--- a/tests/python_on_whales/components/test_stack.py
+++ b/tests/python_on_whales/components/test_stack.py
@@ -26,6 +26,7 @@ def test_services_inspect():
     all_services = docker.service.list()
     assert len(all_services) == 4
     assert set(all_services) == set(docker.stack.services("some_stack"))
+    assert "name='some_stack'" in repr(docker.stack.list())
 
 
 def test_stack_ps_and_services(with_test_stack):
diff --git a/tests/python_on_whales/components/test_task.py b/tests/python_on_whales/components/test_task.py
index 6b0c3727..97acdd82 100644
--- a/tests/python_on_whales/components/test_task.py
+++ b/tests/python_on_whales/components/test_task.py
@@ -18,6 +18,7 @@ def test_list_tasks():
 
     # Todo: use a context manager
     tasks = docker.task.list()
+    repr(tasks)
     assert len(tasks) >= 1
     assert tasks[0].desired_state == "running"
     assert tasks[0].service_id == service.id
diff --git a/tests/python_on_whales/components/test_volume.py b/tests/python_on_whales/components/test_volume.py
index b93bbb5d..0c9aa5db 100644
--- a/tests/python_on_whales/components/test_volume.py
+++ b/tests/python_on_whales/components/test_volume.py
@@ -31,6 +31,9 @@ def test_load_json(json_file):
 
 def test_simple_volume():
     some_volume = docker.volume.create()
+    assert some_volume.exists()
+    assert some_volume.name in repr(docker.volume.list())
+    assert some_volume.driver in repr(docker.volume.list())
     docker.volume.remove(some_volume)