Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pyyaml>=6.0
twine>=3.8
build>=0.7
flake8==7.1.1
pylint==3.3.1
pylint==3.3.1
timeout-decorator==0.5.0
Original file line number Diff line number Diff line change
Expand Up @@ -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<variablename>\w+)\:-(?P<defaultvalue>.+)\}")
capture = re.compile(r"(\$+)\{(?P<variablename>\w+)\:-(?P<defaultvalue>[^$]*?)\}")
matches = capture.search(value)
if matches is not None and matches[1] == "$$":
return value
Expand All @@ -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]) # escape special characters
Copy link

Copilot AI Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using re.escape(matches[0]) instead of a custom regex to escape special characters, as it improves readability and reduces the risk of missing additional special characters.

Suggested change
escaped = re.sub(r"([*+?{}])", r"\\\1", matches[0]) # escape special characters
escaped = re.escape(matches[0]) # escape special characters

Copilot uses AI. Check for mistakes.
value = self.update_value_with_resolved_environment(f"\\{escaped}", env_var, value)
matches = capture.search(value)
return value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
from ..compose_generator import ComposeGenerator
from pycomposefile.compose_element import EmptyOrUnsetException

import timeout_decorator

class TestBracesNoUnderscoreNoDigitVariableInterpolation(TestCase):

Expand All @@ -19,6 +19,21 @@ 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/")

@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}"
Expand Down Expand Up @@ -48,6 +63,21 @@ 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/")

@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}"
Expand Down Expand Up @@ -88,3 +118,86 @@ 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")

@timeout_decorator.timeout(5)
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/*")

@timeout_decorator.timeout(5)
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/+")

@timeout_decorator.timeout(5)
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/?")

@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 + ":-a}-}"
compose_file = ComposeGenerator.get_compose_with_string_value(braced_env_with_default_unset)
self.assertEqual(compose_file.services["frontend"].image, "awesome/a-}") #

@timeout_decorator.timeout(5)
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/*")

@timeout_decorator.timeout(5)
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/+")

@timeout_decorator.timeout(5)
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/?")

@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}"
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")

@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}"
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")