From 8eb95274d636bdac09572f0110d1dc6c97abbc84 Mon Sep 17 00:00:00 2001 From: Anton Petrov Date: Mon, 3 Nov 2025 19:50:36 +0200 Subject: [PATCH 1/2] check include block type Check if include is of type dict or list, throw error if it is dict but 'path' is not the key Fixes https://github.com/containers/podman-compose/issues/1320 Signed-off-by: Anton Petrov --- podman_compose.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/podman_compose.py b/podman_compose.py index 46725ffd..3826953d 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -62,9 +62,13 @@ def filteri(a: list[str]) -> list[str]: @overload -def try_int(i: int | str, fallback: int) -> int: ... +def try_int(i: int | str, fallback: int) -> int: + ... + + @overload -def try_int(i: int | str, fallback: None) -> int | None: ... +def try_int(i: int | str, fallback: None) -> int | None: + ... def try_int(i: int | str, fallback: int | None = None) -> int | None: @@ -271,11 +275,18 @@ def fix_mount_dict( @overload -def rec_subs(value: dict, subs_dict: dict[str, Any]) -> dict: ... +def rec_subs(value: dict, subs_dict: dict[str, Any]) -> dict: + ... + + @overload -def rec_subs(value: str, subs_dict: dict[str, Any]) -> str: ... +def rec_subs(value: str, subs_dict: dict[str, Any]) -> str: + ... + + @overload -def rec_subs(value: Iterable, subs_dict: dict[str, Any]) -> Iterable: ... +def rec_subs(value: Iterable, subs_dict: dict[str, Any]) -> Iterable: + ... def rec_subs(value: dict | str | Iterable, subs_dict: dict[str, Any]) -> dict | str | Iterable: @@ -2376,7 +2387,18 @@ def _parse_compose_file(self) -> None: # If `include` is used, append included files to files include = compose.get("include") if include: - files.extend([os.path.join(os.path.dirname(filename), i) for i in include]) + # Check if 'include' block is dict or list of strings + for i in include: + if isinstance(i, str): + files.append(os.path.join(os.path.dirname(filename), i)) + elif isinstance(i, dict): + path = i.get("path") + if path: + # Extend files list with values from path key + files.extend([os.path.join(os.path.dirname(filename), p) for p in path]) + elif not path: + # Raise error if path is missing + raise RuntimeError("Please use 'path' as key in include block") # As compose obj is updated and tested with every loop, not deleting `include` # from it, results in it being tested again and again, original values for # `include` be appended to `files`, and, included files be processed for ever. @@ -2569,7 +2591,11 @@ def _parse_args(self, argv: list[str] | None = None) -> argparse.Namespace: subparsers = parser.add_subparsers(title="command", dest="command") _ = subparsers.add_parser("help", help="show help") for cmd_name, cmd in self.commands.items(): - subparser = subparsers.add_parser(cmd_name, help=cmd.help, description=cmd.desc) # pylint: disable=protected-access + subparser = subparsers.add_parser( + cmd_name, + help=cmd.help, + description=cmd.desc + ) # pylint: disable=protected-access for cmd_parser in cmd._parse_args: # pylint: disable=protected-access cmd_parser(subparser) self.global_args = parser.parse_args(argv) From 70ea9336e8b073a4ddb11392fc78d63ef50712fb Mon Sep 17 00:00:00 2001 From: Anton Petrov Date: Sun, 30 Nov 2025 12:38:59 +0200 Subject: [PATCH 2/2] add intergration test, fix ruff formatting --- podman_compose.py | 21 ++----- .../include/docker-compose.include-dict.yaml | 6 ++ .../include/test_podman_compose_include.py | 60 +++++++++++++++++++ 3 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 tests/integration/include/docker-compose.include-dict.yaml diff --git a/podman_compose.py b/podman_compose.py index 3826953d..a24aa612 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -62,13 +62,11 @@ def filteri(a: list[str]) -> list[str]: @overload -def try_int(i: int | str, fallback: int) -> int: - ... +def try_int(i: int | str, fallback: int) -> int: ... @overload -def try_int(i: int | str, fallback: None) -> int | None: - ... +def try_int(i: int | str, fallback: None) -> int | None: ... def try_int(i: int | str, fallback: int | None = None) -> int | None: @@ -275,18 +273,15 @@ def fix_mount_dict( @overload -def rec_subs(value: dict, subs_dict: dict[str, Any]) -> dict: - ... +def rec_subs(value: dict, subs_dict: dict[str, Any]) -> dict: ... @overload -def rec_subs(value: str, subs_dict: dict[str, Any]) -> str: - ... +def rec_subs(value: str, subs_dict: dict[str, Any]) -> str: ... @overload -def rec_subs(value: Iterable, subs_dict: dict[str, Any]) -> Iterable: - ... +def rec_subs(value: Iterable, subs_dict: dict[str, Any]) -> Iterable: ... def rec_subs(value: dict | str | Iterable, subs_dict: dict[str, Any]) -> dict | str | Iterable: @@ -2591,11 +2586,7 @@ def _parse_args(self, argv: list[str] | None = None) -> argparse.Namespace: subparsers = parser.add_subparsers(title="command", dest="command") _ = subparsers.add_parser("help", help="show help") for cmd_name, cmd in self.commands.items(): - subparser = subparsers.add_parser( - cmd_name, - help=cmd.help, - description=cmd.desc - ) # pylint: disable=protected-access + subparser = subparsers.add_parser(cmd_name, help=cmd.help, description=cmd.desc) # pylint: disable=protected-access for cmd_parser in cmd._parse_args: # pylint: disable=protected-access cmd_parser(subparser) self.global_args = parser.parse_args(argv) diff --git a/tests/integration/include/docker-compose.include-dict.yaml b/tests/integration/include/docker-compose.include-dict.yaml new file mode 100644 index 00000000..40826190 --- /dev/null +++ b/tests/integration/include/docker-compose.include-dict.yaml @@ -0,0 +1,6 @@ +version: '3.6' + +include: + - path: + - docker-compose.base.yaml + - docker-compose.extend.yaml diff --git a/tests/integration/include/test_podman_compose_include.py b/tests/integration/include/test_podman_compose_include.py index bdd1857e..f8d1731e 100644 --- a/tests/integration/include/test_podman_compose_include.py +++ b/tests/integration/include/test_podman_compose_include.py @@ -62,3 +62,63 @@ def test_podman_compose_include(self) -> None: # check container did not exists anymore out, _ = self.run_subprocess_assert_returncode(command_check_container) self.assertEqual(out, b"") + + def test_podman_compose_include_dict(self) -> None: + """ + Test that podman-compose can execute podman-compose -f up with include + :return: + """ + main_path = Path(__file__).parent.parent.parent.parent + + command_up = [ + "coverage", + "run", + str(main_path.joinpath("podman_compose.py")), + "-f", + str( + main_path.joinpath( + "tests", "integration", "include", "docker-compose.include-dict.yaml" + ) + ), + "up", + "-d", + ] + + command_check_container = [ + "podman", + "ps", + "-a", + "--filter", + "label=io.podman.compose.project=include", + "--format", + '"{{.Image}}"', + ] + + command_container_id = [ + "podman", + "ps", + "-a", + "--filter", + "label=io.podman.compose.project=include", + "--format", + '"{{.ID}}"', + ] + + command_down = ["podman", "rm", "--force"] + + self.run_subprocess_assert_returncode(command_up) + out, _ = self.run_subprocess_assert_returncode(command_check_container) + expected_output = b'"localhost/nopush/podman-compose-test:latest"\n' * 2 + self.assertEqual(out, expected_output) + # Get container ID to remove it + out, _ = self.run_subprocess_assert_returncode(command_container_id) + self.assertNotEqual(out, b"") + container_ids = out.decode().strip().split("\n") + container_ids = [container_id.replace('"', "") for container_id in container_ids] + command_down.extend(container_ids) + out, _ = self.run_subprocess_assert_returncode(command_down) + # cleanup test image(tags) + self.assertNotEqual(out, b"") + # check container did not exists anymore + out, _ = self.run_subprocess_assert_returncode(command_check_container) + self.assertEqual(out, b"")