diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 85de488..957bd98 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/podman_compose.py b/podman_compose.py index e53a7cd..3baeffb 100755 --- a/podman_compose.py +++ b/podman_compose.py @@ -1253,6 +1253,10 @@ def normalize_service(service, sub_dir=""): if not context: context = "." service["build"]["context"] = context + for key in ("command", "entrypoint"): + if key in service: + if is_str(service[key]): + service[key] = shlex.split(service[key]) for key in ("env_file", "security_opt", "volumes"): if key not in service: continue @@ -1305,14 +1309,14 @@ def rec_merge_one(target, source): if key not in source: continue value2 = source[key] - if key == "command": + if key in ("command", "entrypoint"): target[key] = clone(value2) continue if not isinstance(value2, type(value)): value_type = type(value) value2_type = type(value2) raise ValueError( - f"can't merge value of {key} of type {value_type} and {value2_type}" + f"can't merge value of [{key}] of type {value_type} and {value2_type}" ) if is_list(value2): if key == "volumes": diff --git a/pytests/test_build_can_merge_str_and_dict.py b/pytests/test_can_merge_build.py similarity index 100% rename from pytests/test_build_can_merge_str_and_dict.py rename to pytests/test_can_merge_build.py diff --git a/pytests/test_can_merge_cmd_ent.py b/pytests/test_can_merge_cmd_ent.py new file mode 100644 index 0000000..5175673 --- /dev/null +++ b/pytests/test_can_merge_cmd_ent.py @@ -0,0 +1,121 @@ +import copy +import os +import argparse +import yaml +from podman_compose import normalize_service, PodmanCompose + +test_keys = ["command", "entrypoint"] + +test_cases_normalise_pre_merge = [ + ({"$$$": []}, {"$$$": []}), + ({"$$$": ["sh"]}, {"$$$": ["sh"]}), + ({"$$$": ["sh", "-c", "date"]}, {"$$$": ["sh", "-c", "date"]}), + ({"$$$": "sh"}, {"$$$": ["sh"]}), + ({"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}), + ( + {"$$$": "bash -c 'sleep infinity'"}, + {"$$$": ["bash", "-c", "sleep infinity"]}, + ), +] + +test_cases_merges = [ + ({}, {"$$$": []}, {"$$$": []}), + ({"$$$": []}, {}, {"$$$": []}), + ({"$$$": []}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), + ({"$$$": "sh-2"}, {"$$$": []}, {"$$$": []}), + ({}, {"$$$": "sh"}, {"$$$": ["sh"]}), + ({"$$$": "sh"}, {}, {"$$$": ["sh"]}), + ({"$$$": "sh-1"}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), + ({"$$$": ["sh-1"]}, {"$$$": "sh-2"}, {"$$$": ["sh-2"]}), + ({"$$$": "sh-1"}, {"$$$": ["sh-2"]}, {"$$$": ["sh-2"]}), + ({"$$$": "sh-1"}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}), + ({"$$$": ["sh-1"]}, {"$$$": ["sh-2", "sh-3"]}, {"$$$": ["sh-2", "sh-3"]}), + ({"$$$": ["sh-1", "sh-2"]}, {"$$$": ["sh-3", "sh-4"]}, {"$$$": ["sh-3", "sh-4"]}), + ({}, {"$$$": ["sh-3", "sh 4"]}, {"$$$": ["sh-3", "sh 4"]}), + ({"$$$": "sleep infinity"}, {"$$$": "sh"}, {"$$$": ["sh"]}), + ({"$$$": "sh"}, {"$$$": "sleep infinity"}, {"$$$": ["sleep", "infinity"]}), + ( + {}, + {"$$$": "bash -c 'sleep infinity'"}, + {"$$$": ["bash", "-c", "sleep infinity"]}, + ), +] + + +def template_to_expression(base, override, expected, key): + base_copy = copy.deepcopy(base) + override_copy = copy.deepcopy(override) + expected_copy = copy.deepcopy(expected) + + expected_copy[key] = expected_copy.pop("$$$") + if "$$$" in base: + base_copy[key] = base_copy.pop("$$$") + if "$$$" in override: + override_copy[key] = override_copy.pop("$$$") + return base_copy, override_copy, expected_copy + + +def test_normalize_service(): + for test_input_template, expected_template in test_cases_normalise_pre_merge: + for key in test_keys: + test_input, _, expected = template_to_expression( + test_input_template, {}, expected_template, key + ) + test_input = normalize_service(test_input) + test_result = expected == test_input + if not test_result: + print("base_template: ", test_input_template) + print("expected: ", expected) + print("actual: ", test_input) + assert test_result + + +def test__parse_compose_file_when_multiple_composes() -> None: + for base_template, override_template, expected_template in copy.deepcopy( + test_cases_merges + ): + for key in test_keys: + base, override, expected = template_to_expression( + base_template, override_template, expected_template, key + ) + compose_test_1 = {"services": {"test-service": base}} + compose_test_2 = {"services": {"test-service": override}} + dump_yaml(compose_test_1, "test-compose-1.yaml") + dump_yaml(compose_test_2, "test-compose-2.yaml") + + podman_compose = PodmanCompose() + set_args(podman_compose, ["test-compose-1.yaml", "test-compose-2.yaml"]) + + podman_compose._parse_compose_file() # pylint: disable=protected-access + + actual = {} + if podman_compose.services: + podman_compose.services["test-service"].pop("_deps") + actual = podman_compose.services["test-service"] + if actual != expected: + print("compose: ", base) + print("override: ", override) + print("result: ", expected) + + assert expected == actual + + +def set_args(podman_compose: PodmanCompose, file_names: list[str]) -> None: + podman_compose.global_args = argparse.Namespace() + podman_compose.global_args.file = file_names + podman_compose.global_args.project_name = None + podman_compose.global_args.env_file = None + podman_compose.global_args.profile = [] + podman_compose.global_args.in_pod = True + + +def dump_yaml(compose: dict, name: str) -> None: + with open(name, "w", encoding="utf-8") as outfile: + yaml.safe_dump(compose, outfile, default_flow_style=False) + + +def test_clean_test_yamls() -> None: + test_files = ["test-compose-1.yaml", "test-compose-2.yaml"] + for file in test_files: + if os.path.exists(file): + os.remove(file) diff --git a/setup.py b/setup.py index 770649e..5222c14 100644 --- a/setup.py +++ b/setup.py @@ -16,12 +16,11 @@ setup( classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Intended Audience :: Developers", "Operating System :: OS Independent", "Development Status :: 3 - Alpha",