From 32ad26f8ff9f86cad2ba2602c6b1ef631ed2a654 Mon Sep 17 00:00:00 2001 From: Shubham Padkonde Date: Mon, 28 Sep 2026 10:13:49 +0530 Subject: [PATCH] fix: preserve list-valued filters Fixes #542 Signed-off-by: Shubham Padkonde --- podman/api/http_utils.py | 10 ++++----- podman/tests/unit/test_api_utils.py | 25 +++++++++++++++++++++ podman/tests/unit/test_containersmanager.py | 14 ++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/podman/api/http_utils.py b/podman/api/http_utils.py index 37d895ad..47750cf3 100644 --- a/podman/api/http_utils.py +++ b/podman/api/http_utils.py @@ -45,12 +45,10 @@ def _format_dict(filters, criteria): for key, value in filters.items(): if value is None: continue - str_value = str(value) - - if key in criteria: - criteria[key].append(str_value) - else: - criteria[key] = [str_value] + values = value if isinstance(value, (list, tuple)) else [value] + for item in values: + if item is not None: + criteria.setdefault(key, []).append(str(item)) def _format_string(filters, criteria): diff --git a/podman/tests/unit/test_api_utils.py b/podman/tests/unit/test_api_utils.py index 4cfe95c4..cbbfd8c2 100644 --- a/podman/tests/unit/test_api_utils.py +++ b/podman/tests/unit/test_api_utils.py @@ -42,6 +42,31 @@ class TestCase: if actual is not None: self.assertIsInstance(actual, str) + def test_format_list_valued_filters(self) -> None: + cases: list[tuple[dict[str, Any], Optional[dict[str, list[str]]]]] = [ + ({"label": ["team=platform", "env=test"]}, {"label": ["team=platform", "env=test"]}), + ({"label": ["enabled"], "exited": 0}, {"label": ["enabled"], "exited": ["0"]}), + ({"exited": [0, 1]}, {"exited": ["0", "1"]}), + ({"label": [], "status": "running"}, {"status": ["running"]}), + ({"label": [None, "enabled"], "status": None}, {"label": ["enabled"]}), + ({"label": []}, None), + ({"label": [None]}, None), + ] + cases.extend( + ( + { + key: tuple(value) if isinstance(value, list) else value + for key, value in filters.items() + }, + expected, + ) + for filters, expected in list(cases) + ) + for filters, expected in cases: + with self.subTest(filters=filters): + actual = api.prepare_filters(filters) + self.assertEqual(json.loads(actual) if actual is not None else None, expected) + def test_containerignore_404(self) -> None: actual = api.prepare_containerignore("/does/not/exists") self.assertListEqual([], actual) diff --git a/podman/tests/unit/test_containersmanager.py b/podman/tests/unit/test_containersmanager.py index f1c1b1a3..21943fbe 100644 --- a/podman/tests/unit/test_containersmanager.py +++ b/podman/tests/unit/test_containersmanager.py @@ -144,6 +144,20 @@ def test_list_filtered(self, mock): "6dc84cc0a46747da94e4c1571efcc01a756b4017261440b4b8985d37203c3c03", ) + @requests_mock.Mocker() + def test_list_multiple_labels(self, mock): + filters = {"label": ["team=platform", "env=test"], "status": "running"} + mock.get(tests.LIBPOD_URL + "/containers/json", json=[FIRST_CONTAINER]) + + actual = self.client.containers.list(filters=filters) + + self.assertEqual([container.id for container in actual], [FIRST_CONTAINER["Id"]]) + self.assertEqual( + json.loads(mock.last_request.qs["filters"][0]), + {"label": ["team=platform", "env=test"], "status": ["running"]}, + ) + self.assertEqual(filters, {"label": ["team=platform", "env=test"], "status": "running"}) + @requests_mock.Mocker() def test_list_no_filters(self, mock): mock.get(