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

Issue:
`docker.system.prune` doesn't output anything
the command works but there's no output shown to indicate what was cleared.

the following is the command I used in my script
`docker.system.prune(all=True, volumes=True)`


Discussion / hints:
Currently, we don't return anything with this function. One cool feature would be to parse the output and return what was removed. I'm open to contributions :) 
Would love to contribute but I honestly have no clue where to begin with what little experience I have with Python.

What I've gathered is that the code for the actual prune command is here but I'm not sure where the output goes.

https://github.com/gabrieldemarmiesse/python-on-whales/blob/bc9a3cb399ed377b45ad4082aa00a95e5bdbc3de/python_on_whales/components/system/cli_wrapper.py#L156-L172

Fix patch:
diff --git a/python_on_whales/components/container/cli_wrapper.py b/python_on_whales/components/container/cli_wrapper.py
index e228ca1d..0ed58ef6 100644
--- a/python_on_whales/components/container/cli_wrapper.py
+++ b/python_on_whales/components/container/cli_wrapper.py
@@ -1159,11 +1159,35 @@ def pause(self, containers: Union[ValidContainer, List[ValidContainer]]) -> None
 
         run(full_cmd)
 
-    def prune(self, filters: Dict[str, str] = {}) -> None:
+    @overload
+    def prune(
+        self,
+        filters: Dict[str, str] = {},
+        stream_logs: Literal[True] = ...,
+    ) -> Iterable[Tuple[str, bytes]]:
+        ...
+
+    @overload
+    def prune(
+        self,
+        filters: Dict[str, str] = {},
+        stream_logs: Literal[False] = ...,
+    ) -> None:
+        ...
+
+    def prune(
+        self,
+        filters: Dict[str, str] = {},
+        stream_logs: bool = False,
+    ):
         """Remove containers that are not running.
 
         Parameters:
             filters: Filters as strings or list of strings
+            stream_logs: If `True` this function will return an iterator of strings.
+                You can then read the logs as they arrive. If `False` (the default value), then
+                the function returns `None`, but when it returns, then the prune operation has already been
+                done.
         """
         if isinstance(filter, list):
             raise TypeError(
@@ -1173,6 +1197,8 @@ def prune(self, filters: Dict[str, str] = {}) -> None:
             )
         full_cmd = self.docker_cmd + ["container", "prune", "--force"]
         full_cmd.add_args_list("--filter", format_dict_for_cli(filters))
+        if stream_logs:
+            return stream_stdout_and_stderr(full_cmd)
         run(full_cmd)
 
     def rename(self, container: ValidContainer, new_name: str) -> None:


Test patch:
diff --git a/tests/python_on_whales/components/test_container.py b/tests/python_on_whales/components/test_container.py
index fded0d61..f2f220b6 100644
--- a/tests/python_on_whales/components/test_container.py
+++ b/tests/python_on_whales/components/test_container.py
@@ -924,6 +924,86 @@ def test_prune(ctr_client: DockerClient):
     assert container not in ctr_client.container.list(all=True)
 
 
+@pytest.mark.parametrize("ctr_client", ["docker", "podman"], indirect=True)
+def test_prune_streaming(ctr_client: DockerClient):
+    for container in ctr_client.container.list(filters={"name": "test-container"}):
+        ctr_client.container.remove(container, force=True)
+    container = ctr_client.container.create("busybox")
+    assert container in ctr_client.container.list(all=True)
+
+    # container not pruned because it is not old enough
+    # podman does not provide logs when not pruned
+    logs = list(ctr_client.container.prune(filters={"until": "100h"}, stream_logs=True))
+    assert container in ctr_client.container.list(all=True)
+
+    if ctr_client.client_config.client_type == "docker":
+        assert len(logs) >= 1
+    logs_as_big_binary = b""
+    for log_type, log_value in logs:
+        if ctr_client.client_config.client_type == "docker":
+            assert log_type in ("stdout", "stderr")
+            logs_as_big_binary += log_value
+
+    if ctr_client.client_config.client_type == "docker":
+        assert b"Total reclaimed space:" in logs_as_big_binary
+
+    # container not pruned because it is does not have label "dne"
+    # podman does not provide logs when not pruned
+    logs = list(ctr_client.container.prune(filters={"label": "dne"}, stream_logs=True))
+    assert container in ctr_client.container.list(all=True)
+
+    if ctr_client.client_config.client_type == "docker":
+        assert len(logs) >= 1
+    logs_as_big_binary = b""
+    for log_type, log_value in logs:
+        if ctr_client.client_config.client_type == "docker":
+            assert log_type in ("stdout", "stderr")
+            logs_as_big_binary += log_value
+
+    if ctr_client.client_config.client_type == "docker":
+        assert b"Total reclaimed space:" in logs_as_big_binary
+
+    # container not pruned because it is not old enough and does not have label "dne"
+    # podman does not provide logs when not pruned
+    logs = list(
+        ctr_client.container.prune(
+            filters={"until": "100h", "label": "dne"}, stream_logs=True
+        )
+    )
+    assert container in ctr_client.container.list(all=True)
+
+    if ctr_client.client_config.client_type == "docker":
+        assert len(logs) >= 1
+    logs_as_big_binary = b""
+    for log_type, log_value in logs:
+        if ctr_client.client_config.client_type == "docker":
+            assert log_type in ("stdout", "stderr")
+        logs_as_big_binary += log_value
+
+    if ctr_client.client_config.client_type == "docker":
+        assert b"Total reclaimed space:" in logs_as_big_binary
+
+    # container pruned
+    # podman does provide logs when pruned
+    logs = list(ctr_client.container.prune(stream_logs=True))
+    assert container not in ctr_client.container.list(all=True)
+
+    if ctr_client.client_config.client_type == "docker":
+        len(logs) >= 3
+
+    if ctr_client.client_config.client_type == "podman":
+        len(logs) >= 1
+
+    logs_as_big_binary = b""
+    for log_type, log_value in logs:
+        if ctr_client.client_config.client_type == "docker":
+            assert log_type in ("stdout", "stderr")
+        logs_as_big_binary += log_value
+
+    if ctr_client.client_config.client_type == "docker":
+        assert b"Total reclaimed space:" in logs_as_big_binary
+
+
 @pytest.mark.parametrize("ctr_client", ["docker", "podman"], indirect=True)
 def test_run_detached_interactive(ctr_client: DockerClient):
     with ctr_client.run("ubuntu", interactive=True, detach=True, tty=False) as c: