From d3a0e476990a6fe5d83d4a5c59ea6a58abe41e3a Mon Sep 17 00:00:00 2001 From: Nicola Sella Date: Mon, 14 Apr 2025 15:42:11 +0200 Subject: [PATCH] Implement sparse keyword for containers.list() Defaults to True for Libpod calls and False for Docker Compat calls This ensures: 1. Docker API compatibility 2. No breaking changes with Libpod It also providest 1. Possibility to inspect containers on demand for list calls 2. Safer behavior if container hangs 3. Fewer expensive calls to the API by default Fixes: https://github.com/containers/podman-py/issues/459 Fixes: https://github.com/containers/podman-py/issues/446 Signed-off-by: Nicola Sella --- podman/domain/containers_manager.py | 27 ++++++++++++++++++--- podman/tests/unit/test_containersmanager.py | 16 ++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/podman/domain/containers_manager.py b/podman/domain/containers_manager.py index f41585a7..2e4f5e08 100644 --- a/podman/domain/containers_manager.py +++ b/podman/domain/containers_manager.py @@ -67,16 +67,26 @@ def list(self, **kwargs) -> list[Container]: Give the container name or id. - since (str): Only containers created after a particular container. Give container name or id. - sparse: Ignored + sparse: If False, return basic container information without additional + inspection requests. This improves performance when listing many containers + but might provide less detail. You can call Container.reload() on individual + containers later to retrieve complete attributes. Default: True. + When Docker compatibility is enabled with `compatible=True`: Default: False. ignore_removed: If True, ignore failures due to missing containers. Raises: APIError: when service returns an error """ + compatible = kwargs.get("compatible", False) + # Libpod behavior: default is sparse=True and containers require a reload call + # to get full details + # Docker behavior: default is sparse=False and containers are inspected during + # list calls params = { "all": kwargs.get("all"), "filters": kwargs.get("filters", {}), "limit": kwargs.get("limit"), + "sparse": kwargs.get("sparse", not compatible), } if "before" in kwargs: params["filters"]["before"] = kwargs.get("before") @@ -86,10 +96,21 @@ def list(self, **kwargs) -> list[Container]: # filters formatted last because some kwargs may need to be mapped into filters params["filters"] = api.prepare_filters(params["filters"]) - response = self.client.get("/containers/json", params=params) + response = self.client.get("/containers/json", params=params, compatible=compatible) response.raise_for_status() - return [self.prepare_model(attrs=i) for i in response.json()] + containers: list[Container] = [self.prepare_model(attrs=i) for i in response.json()] + + # If sparse is False (default), reload each container to get full details + if not kwargs.get("sparse", False): + for container in containers: + try: + container.reload() + except APIError: + # Skip containers that might have been removed + pass + + return containers def prune(self, filters: Mapping[str, str] = None) -> dict[str, Any]: """Delete stopped containers. diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index 47b30482..47c01d0f 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -154,6 +154,22 @@ def test_list_no_filters(self, mock): actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03" ) + @requests_mock.Mocker() + def test_list_sparse_with_compat(self, mock): + mock.get( + tests.COMPATIBLE_URL + "/containers/json?sparse=False", + json=[FIRST_CONTAINER, SECOND_CONTAINER], + ) + actual = self.client.containers.list(compatible=True) + self.assertIsInstance(actual, list) + + self.assertEqual( + actual[0].id, "87e1325c82424e49a00abdd4de08009eb76c7de8d228426a9b8af9318ced5ecd" + ) + self.assertEqual( + actual[1].id, "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03" + ) + @requests_mock.Mocker() def test_prune(self, mock): mock.post(