diff --git a/pyhocon/__init__.py b/pyhocon/__init__.py index 3c2bf132..939430ba 100644 --- a/pyhocon/__init__.py +++ b/pyhocon/__init__.py @@ -122,8 +122,6 @@ def convert_number(tokens): # ${path} or ${?path} for optional substitution SUBSTITUTION = "\$\{(?P\?)?(?P[^}]+)\}(?P\s*)" - substitutions = [] - def create_substitution(instring, loc, token): # remove the ${ and } match = re.match(SUBSTITUTION, token[0]) @@ -131,7 +129,6 @@ def create_substitution(instring, loc, token): ws = match.group('ws') optional = match.group('optional') == '?' substitution = ConfigSubstitution(variable, optional, ws, instring, loc) - substitutions.append(substitution) return substitution def include_config(token): @@ -217,7 +214,7 @@ def include_config(token): # the file can be { ... } where {} can be omitted or [] config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | dict_expr | inside_dict_expr) + ZeroOrMore(comment_eol | eol_comma) config = config_expr.parseString(content, parseAll=True)[0] - ConfigParser._resolve_substitutions(config, substitutions) + ConfigParser._resolve_substitutions(config) return config @staticmethod @@ -254,7 +251,29 @@ def _resolve_variable(config, substitution): return True, value @staticmethod - def _resolve_substitutions(config, substitutions): + def _resolve_substitutions(config): + # traverse config to find all the substitutions + def find_substitutions(item): + """Convert HOCON input into a JSON output + + :return: JSON string representation + :type return: basestring + """ + if isinstance(item, ConfigValues): + return item.get_substitutions() + + substitutions = [] + if isinstance(item, ConfigTree): + for key, child in item.items(): + substitutions += find_substitutions(child) + elif isinstance(item, list): + for child in item: + substitutions += find_substitutions(child) + + return substitutions + + substitutions = find_substitutions(config) + if len(substitutions) > 0: _substitutions = set(substitutions) for i in range(len(substitutions)): @@ -262,7 +281,7 @@ def _resolve_substitutions(config, substitutions): for substitution in list(_substitutions): is_optional_resolved, resolved_value = ConfigParser._resolve_variable(config, substitution) - # if the substitition is optional + # if the substitution is optional if not is_optional_resolved and substitution.optional: resolved_value = None diff --git a/pyhocon/config_tree.py b/pyhocon/config_tree.py index d045c7bb..21ef4333 100644 --- a/pyhocon/config_tree.py +++ b/pyhocon/config_tree.py @@ -264,7 +264,10 @@ def __init__(self, tokens, instring, loc): self.tokens[-1] = self.tokens[-1].rstrip() def has_substitution(self): - return next((True for token in self.tokens if isinstance(token, ConfigSubstitution)), False) + return len(self.get_substitutions()) > 0 + + def get_substitutions(self): + return [token for token in self.tokens if isinstance(token, ConfigSubstitution)] def transform(self): def determine_type(token): diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index c0dbb151..08a88f9b 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -1000,3 +1000,39 @@ def test_assign_dict_strings_no_equal_sign_with_eol(self): assert config['a'] == {'a': 1, 'b': 2} assert config['b'] == {'c': 3, 'd': 4} assert config['c'] == {'e': 5, 'f': 6} + + def test_substitutions_overwrite(self): + config1 = ConfigFactory.parse_string( + """ + a = 123 + a = ${?test} + a = 5 + """ + ) + + assert config1['a'] == 5 + + config2 = ConfigFactory.parse_string( + """ + { + database { + host = "localhost" + port = 8000 + url = ${database.host}":"${database.port} + } + + database { + host = ${?DB_HOST} + } + + database { + host = "other.host.net" + port = 433 + } + } + """ + ) + + assert config2['database']['host'] == 'other.host.net' + assert config2['database']['port'] == 433 + assert config2['database']['url'] == 'other.host.net:433'