From 98d04651022d8ad5e8e13e285fbb8520121bf030 Mon Sep 17 00:00:00 2001 From: horihiro Date: Sun, 19 Jan 2025 09:08:54 +0000 Subject: [PATCH 1/8] Add tests --- .../test_environment_variables_braces.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index 50334d5..c6a5d1e 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -88,3 +88,62 @@ def test_service_with_double_dollar_sign_env_vars(self): compose_file = ComposeGenerator.get_compose_with_double_dollar_sign_env_vars() self.assertEqual(compose_file.services["frontend"].environment["ENVIRONMENT"], "$ENVIRONMENT") + + def test_uppercase_with_asterisk_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-*}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + + def test_uppercase_with_plus_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-+}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + + def test_uppercase_with_question_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-?}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + + def test_lowercase_with_asterisk_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-*}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + + def test_lowercase_with_plus_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-+}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + + def test_lowercase_with_question_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-?}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + + def test_uppercase_two_variables_with_default_in_string_value(self): + braced_first_env_var = "{HOSTPORT:-8080}" + braced_second_env_var = "{CONTAINERPORT:-80}" + compose_file = ComposeGenerator.get_with_two_environment_variables_in_string_value(braced_first_env_var, braced_second_env_var) + self.assertEqual(f"{compose_file.services['frontend'].ports[0]}", "8080:80/tcp") + self.assertEqual(compose_file.services['frontend'].ports[0].published, "8080") + self.assertEqual(compose_file.services['frontend'].ports[0].target, "80") + + def test_lowercase_two_variables_with_default_in_string_value(self): + braced_first_env_var = "{httpport:-8080}" + braced_second_env_var = "{containerport:-80}" + compose_file = ComposeGenerator.get_with_two_environment_variables_in_string_value(braced_first_env_var, braced_second_env_var) + self.assertEqual(f"{compose_file.services['frontend'].ports[0]}", "8080:80/tcp") + self.assertEqual(compose_file.services['frontend'].ports[0].published, "8080") + self.assertEqual(compose_file.services['frontend'].ports[0].target, "80") + From 3d0af8b65a7d4d92ae444792269e6dbd20fe7dbc Mon Sep 17 00:00:00 2001 From: horihiro Date: Sun, 19 Jan 2025 09:13:50 +0000 Subject: [PATCH 2/8] Add `timeout` decorator --- .../test_environment_variables_braces.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index c6a5d1e..6d8bf7e 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -2,7 +2,7 @@ import os from ..compose_generator import ComposeGenerator from pycomposefile.compose_element import EmptyOrUnsetException - +import timeout_decorator class TestBracesNoUnderscoreNoDigitVariableInterpolation(TestCase): @@ -89,6 +89,7 @@ def test_service_with_double_dollar_sign_env_vars(self): self.assertEqual(compose_file.services["frontend"].environment["ENVIRONMENT"], "$ENVIRONMENT") + @timeout_decorator.timeout(5) def test_uppercase_with_asterisk_as_default(self): env_var = "DEFAULTUNSET" os.unsetenv(env_var) @@ -96,6 +97,7 @@ def test_uppercase_with_asterisk_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + @timeout_decorator.timeout(5) def test_uppercase_with_plus_as_default(self): env_var = "DEFAULTUNSET" os.unsetenv(env_var) @@ -103,6 +105,7 @@ def test_uppercase_with_plus_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + @timeout_decorator.timeout(5) def test_uppercase_with_question_as_default(self): env_var = "DEFAULTUNSET" os.unsetenv(env_var) @@ -110,6 +113,7 @@ def test_uppercase_with_question_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + @timeout_decorator.timeout(5) def test_lowercase_with_asterisk_as_default(self): env_var = "defaultunset" os.unsetenv(env_var) @@ -117,6 +121,7 @@ def test_lowercase_with_asterisk_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/*") + @timeout_decorator.timeout(5) def test_lowercase_with_plus_as_default(self): env_var = "defaultunset" os.unsetenv(env_var) @@ -124,6 +129,7 @@ def test_lowercase_with_plus_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/+") + @timeout_decorator.timeout(5) def test_lowercase_with_question_as_default(self): env_var = "defaultunset" os.unsetenv(env_var) @@ -131,6 +137,7 @@ def test_lowercase_with_question_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + @timeout_decorator.timeout(5) def test_uppercase_two_variables_with_default_in_string_value(self): braced_first_env_var = "{HOSTPORT:-8080}" braced_second_env_var = "{CONTAINERPORT:-80}" @@ -139,6 +146,7 @@ def test_uppercase_two_variables_with_default_in_string_value(self): self.assertEqual(compose_file.services['frontend'].ports[0].published, "8080") self.assertEqual(compose_file.services['frontend'].ports[0].target, "80") + @timeout_decorator.timeout(5) def test_lowercase_two_variables_with_default_in_string_value(self): braced_first_env_var = "{httpport:-8080}" braced_second_env_var = "{containerport:-80}" From e1ed09836af2b6ce0098b334fde2dd1473d79f06 Mon Sep 17 00:00:00 2001 From: horihiro Date: Mon, 20 Jan 2025 23:46:31 +0000 Subject: [PATCH 3/8] Add a package for tests --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 32b07ed..52f182a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ pyyaml>=6.0 twine>=3.8 build>=0.7 flake8==7.1.1 -pylint==3.3.1 \ No newline at end of file +pylint==3.3.1 +timeout-decorator==0.5.0 \ No newline at end of file From 033caf8c805d5aa8ad2b1298bf9b092a473a880d Mon Sep 17 00:00:00 2001 From: horihiro Date: Tue, 21 Jan 2025 10:38:15 +0000 Subject: [PATCH 4/8] Add test for right brace as default value --- .../test_environment_variables_braces.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index 6d8bf7e..659ced8 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -113,6 +113,14 @@ def test_uppercase_with_question_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + @timeout_decorator.timeout(5) + def test_uppercase_with_rightbrace_as_default(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/}") + @timeout_decorator.timeout(5) def test_lowercase_with_asterisk_as_default(self): env_var = "defaultunset" @@ -137,6 +145,14 @@ def test_lowercase_with_question_as_default(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/?") + @timeout_decorator.timeout(5) + def test_lowercase_with_rightbrace_as_default(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/}") + @timeout_decorator.timeout(5) def test_uppercase_two_variables_with_default_in_string_value(self): braced_first_env_var = "{HOSTPORT:-8080}" From 5fcbae602d82756fc59592cef8b25ceba76a61bf Mon Sep 17 00:00:00 2001 From: horihiro Date: Tue, 21 Jan 2025 10:38:41 +0000 Subject: [PATCH 5/8] Fix regex to correctly capture default values in environment variable replacement --- .../compose_element/compose_datatype_transformer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pycomposefile/compose_element/compose_datatype_transformer.py b/src/pycomposefile/compose_element/compose_datatype_transformer.py index d0915e0..fe4f5cd 100644 --- a/src/pycomposefile/compose_element/compose_datatype_transformer.py +++ b/src/pycomposefile/compose_element/compose_datatype_transformer.py @@ -59,7 +59,7 @@ def update_value_with_resolved_environment(self, env_variable_name, env_variable return re.sub(env_variable_name, env_variable_value, source_string) def replace_environment_variables_with_empty_unset(self, value): - capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P.+)\}") + capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P.+?)\}") matches = capture.search(value) if matches is not None and matches[1] == "$$": return value @@ -68,7 +68,8 @@ def replace_environment_variables_with_empty_unset(self, value): default_value = matches.group("defaultvalue") if env_var is None or len(env_var) == 0: env_var = default_value - value = self.update_value_with_resolved_environment(f"\\{matches[0]}", env_var, value) + escaped = re.sub(r"([*+?{}])", r"\\\1", matches[0]) # Escaping `}` character + value = self.update_value_with_resolved_environment(f"\\{escaped}", env_var, value) matches = capture.search(value) return value From 701ba46ed98d8ac2b351340207b38618a738c2c1 Mon Sep 17 00:00:00 2001 From: horihiro Date: Fri, 24 Jan 2025 23:51:29 +0000 Subject: [PATCH 6/8] Add tests for unset environment variables without default value --- .../test_environment_variables_braces.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index 659ced8..b95aa8f 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -19,6 +19,13 @@ def test_uppercase_with_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + def test_uppercase_without_default_when_unset_in_string_value(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/") + @mock.patch.dict(os.environ, {"TESTCPUCOUNT": "1.5"}) def test_uppercase_in_decimal_value(self): braced_env_var = "{TESTCPUCOUNT}" @@ -48,6 +55,13 @@ def test_lowercase_with_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + def test_lowercase_without_default_when_unset_in_string_value(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/") + @mock.patch.dict(os.environ, {"testcpucount": "1.5"}) def test_lowercase_in_decimal_value(self): braced_env_var = "{testcpucount}" From d14db3b0f50b43b7d33f7c388ff8619ff8d07aa1 Mon Sep 17 00:00:00 2001 From: horihiro Date: Sat, 25 Jan 2025 04:23:33 +0000 Subject: [PATCH 7/8] Add tests for nested default values in environment variable interpolation --- .../test_environment_variables_braces.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py index b95aa8f..99c018e 100644 --- a/src/tests/environment_variable_evaluation/test_environment_variables_braces.py +++ b/src/tests/environment_variable_evaluation/test_environment_variables_braces.py @@ -26,6 +26,14 @@ def test_uppercase_without_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/") + @timeout_decorator.timeout(5) + def test_uppercase_with_nested_default_when_unset_in_string_value(self): + env_var = "DEFAULTUNSET" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-${CHILDUNSET:-bob}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + @mock.patch.dict(os.environ, {"TESTCPUCOUNT": "1.5"}) def test_uppercase_in_decimal_value(self): braced_env_var = "{TESTCPUCOUNT}" @@ -62,6 +70,14 @@ def test_lowercase_without_default_when_unset_in_string_value(self): compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) self.assertEqual(compose_file.services["frontend"].image, "awesome/") + @timeout_decorator.timeout(5) + def test_lowercase_with_nested_default_when_unset_in_string_value(self): + env_var = "defaultunset" + os.unsetenv(env_var) + braced_env_with_default_unset = "{" + env_var + ":-${childunset:-bob}}" + compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) + self.assertEqual(compose_file.services["frontend"].image, "awesome/bob") + @mock.patch.dict(os.environ, {"testcpucount": "1.5"}) def test_lowercase_in_decimal_value(self): braced_env_var = "{testcpucount}" @@ -131,9 +147,9 @@ def test_uppercase_with_question_as_default(self): def test_uppercase_with_rightbrace_as_default(self): env_var = "DEFAULTUNSET" os.unsetenv(env_var) - braced_env_with_default_unset = "{" + env_var + ":-}}" + braced_env_with_default_unset = "{" + env_var + ":-a}-}" compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset) - self.assertEqual(compose_file.services["frontend"].image, "awesome/}") + self.assertEqual(compose_file.services["frontend"].image, "awesome/a-}") # @timeout_decorator.timeout(5) def test_lowercase_with_asterisk_as_default(self): From 15fc66f3fd9c8bdf98204d88f99ca086baf008f3 Mon Sep 17 00:00:00 2001 From: horihiro Date: Sat, 25 Jan 2025 04:33:59 +0000 Subject: [PATCH 8/8] Fix regex to correctly capture default values without dollar signs in environment variable replacement --- .../compose_element/compose_datatype_transformer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pycomposefile/compose_element/compose_datatype_transformer.py b/src/pycomposefile/compose_element/compose_datatype_transformer.py index fe4f5cd..2baf4ac 100644 --- a/src/pycomposefile/compose_element/compose_datatype_transformer.py +++ b/src/pycomposefile/compose_element/compose_datatype_transformer.py @@ -59,7 +59,7 @@ def update_value_with_resolved_environment(self, env_variable_name, env_variable return re.sub(env_variable_name, env_variable_value, source_string) def replace_environment_variables_with_empty_unset(self, value): - capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P.+?)\}") + capture = re.compile(r"(\$+)\{(?P\w+)\:-(?P[^$]*?)\}") matches = capture.search(value) if matches is not None and matches[1] == "$$": return value @@ -68,7 +68,7 @@ def replace_environment_variables_with_empty_unset(self, value): default_value = matches.group("defaultvalue") if env_var is None or len(env_var) == 0: env_var = default_value - escaped = re.sub(r"([*+?{}])", r"\\\1", matches[0]) # Escaping `}` character + escaped = re.sub(r"([*+?{}])", r"\\\1", matches[0]) # escape special characters value = self.update_value_with_resolved_environment(f"\\{escaped}", env_var, value) matches = capture.search(value) return value