Repository: chimpler/pyhocon

Issue:
support for "+=" field separator?
I get an error when parsing a hocon file:

```
[ec2-user@ip-172-16-2-117 ~]$ pyhocon -i /opt/mapr/drill/drill-1.1.0/conf/drill-override-example.conf
Traceback (most recent call last):
  File "/usr/local/bin/pyhocon", line 9, in <module>
    load_entry_point('pyhocon==0.3.4', 'console_scripts', 'pyhocon')()
  File "/usr/local/lib/python2.7/site-packages/pyhocon/tool.py", line 188, in main
    HOCONConverter.convert(args.input, args.output, args.format)
  File "/usr/local/lib/python2.7/site-packages/pyhocon/tool.py", line 145, in convert
    config = ConfigFactory.parse_file(input_file)
  File "/usr/local/lib/python2.7/site-packages/pyhocon/__init__.py", line 40, in parse_file
    return ConfigFactory.parse_string(content, os.path.dirname(filename))
  File "/usr/local/lib/python2.7/site-packages/pyhocon/__init__.py", line 75, in parse_string
    return ConfigParser().parse(content, basedir)
  File "/usr/local/lib/python2.7/site-packages/pyhocon/__init__.py", line 216, in parse
    config = config_expr.parseString(content, parseAll=True)[0]
  File "/usr/local/lib/python2.7/site-packages/pyparsing.py", line 1125, in parseString
    raise exc
pyparsing.ParseSyntaxException: Expected "{" (at char 1087), (line:20, col:33)
```

Line 20 in the above looks like this:

```
drill.logical.function.packages += "org.apache.drill.exec.expr.fn.impl"
```

And column 33 implicates the "+=" field separator.

Any plans to add support for this?
https://github.com/typesafehub/config/blob/master/HOCON.md#the--field-separator




Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 373df98..5f75182 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+# Version 0.3.xx (TBA)
+
+* Implemented self-referential subsitutions
+
 # Version 0.3.25
 
 * ConfigValue.transform: do not wrap lists. PR [#76]
diff --git a/README.md b/README.md
index a811af0..6153f4f 100644
--- a/README.md
+++ b/README.md
@@ -300,8 +300,8 @@ Arrays without commas                  | :white_check_mark:
 Path expressions                       | :white_check_mark:
 Paths as keys                          | :white_check_mark:
 Substitutions                          | :white_check_mark:
-Self-referential substitutions         | :x:
-The `+=` separator                     | :x:
+Self-referential substitutions         | :white_check_mark:
+The `+=` separator                     | :white_check_mark:
 Includes                               | :white_check_mark:
 Include semantics: merging             | :white_check_mark:
 Include semantics: substitution        | :white_check_mark:
@@ -335,6 +335,7 @@ Java properties mapping                                    | :x:
   - Virgil Palanciuc ([@virgil-palanciuc](https://github.com/virgil-palanciuc))
   - Douglas Simon ([@dougxc](https://github.com/dougxc))
   - Gilles Duboscq ([@gilles-duboscq](https://github.com/gilles-duboscq))
+  - Stefan Anzinger ([@sanzinger](https://github.com/sanzinger))
 
 ### Thanks
 
diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py
index 5f3d4bd..a029912 100644
--- a/pyhocon/config_parser.py
+++ b/pyhocon/config_parser.py
@@ -3,7 +3,7 @@ import os
 import socket
 import contextlib
 from pyparsing import Forward, Keyword, QuotedString, Word, Literal, Suppress, Regex, Optional, SkipTo, ZeroOrMore, \
-    Group, lineno, col, TokenConverter, replaceWith, alphanums
+    Group, lineno, col, TokenConverter, replaceWith, alphanums, ParseSyntaxException
 from pyparsing import ParserElement
 from pyhocon.config_tree import ConfigTree, ConfigSubstitution, ConfigList, ConfigValues, ConfigUnquotedString, \
     ConfigInclude, NoneValue
@@ -230,6 +230,7 @@ class ConfigParser(object):
                 (Keyword('url') | Keyword('file')) - Literal('(').suppress() - quoted_string - Literal(')').suppress()))) \
             .setParseAction(include_config)
 
+        root_dict_expr = Forward()
         dict_expr = Forward()
         list_expr = Forward()
         multi_value_expr = ZeroOrMore((Literal(
@@ -237,7 +238,9 @@ class ConfigParser(object):
         # for a dictionary : or = is optional
         # last zeroOrMore is because we can have t = {a:4} {b: 6} {c: 7} which is dictionary concatenation
         inside_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma))
+        inside_root_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma), root=True)
         dict_expr << Suppress('{') - inside_dict_expr - Suppress('}')
+        root_dict_expr << Suppress('{') - inside_root_dict_expr - Suppress('}')
         list_entry = ConcatenatedValueParser(multi_value_expr)
         list_expr << Suppress('[') - ListParser(list_entry - ZeroOrMore(eol_comma - list_entry)) - Suppress(']')
 
@@ -245,12 +248,12 @@ class ConfigParser(object):
         assign_expr << Group(
             key -
             ZeroOrMore(comment_no_comma_eol) -
-            (dict_expr | Suppress(Literal('=') | Literal(':')) - ZeroOrMore(
+            (dict_expr | (Literal('=') | Literal(':') | Literal('+=')) - ZeroOrMore(
                 comment_no_comma_eol) - ConcatenatedValueParser(multi_value_expr))
         )
 
         # the file can be { ... } where {} can be omitted or []
-        config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | dict_expr | inside_dict_expr) + ZeroOrMore(
+        config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore(
             comment_eol | eol_comma)
         config = config_expr.parseString(content, parseAll=True)[0]
         if resolve:
@@ -290,41 +293,106 @@ class ConfigParser(object):
                         col=col(substitution.loc, substitution.instring)))
             return True, value
 
+    @staticmethod
+    def _fixup_self_references(config):
+        if isinstance(config, ConfigTree) and config.root:
+            for key in config:  # Traverse history of element
+                history = config.history[key]
+                previous_item = history[0]
+                for current_item in history[1:]:
+                    for substitution in ConfigParser._find_substitutions(current_item):
+                        prop_path = ConfigTree.parse_key(substitution.variable)
+                        if len(prop_path) > 1 and config.get(substitution.variable, None) is not None:
+                            continue  # If value is present in latest version, don't do anything
+                        if prop_path[0] == key:
+                            if isinstance(previous_item, ConfigValues):  # We hit a dead end, we cannot evaluate
+                                raise ConfigSubstitutionException("Property {variable} cannot be substituted. Check for cycles.".format(
+                                    variable=substitution.variable))
+                            value = previous_item if len(prop_path) == 1 else previous_item.get(".".join(prop_path[1:]))
+                            (_, _, current_item) = ConfigParser._do_substitute(substitution, value)
+                    previous_item = current_item
+
+                if len(history) == 1:  # special case, when self optional referencing without existing
+                    for substitution in ConfigParser._find_substitutions(previous_item):
+                        prop_path = ConfigTree.parse_key(substitution.variable)
+                        if len(prop_path) > 1 and config.get(substitution.variable, None) is not None:
+                            continue  # If value is present in latest version, don't do anything
+                        if prop_path[0] == key and substitution.optional:
+                            ConfigParser._do_substitute(substitution, None)
+
+    # traverse config to find all the substitutions
+    @staticmethod
+    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 += ConfigParser._find_substitutions(child)
+        elif isinstance(item, list):
+            for child in item:
+                substitutions += ConfigParser._find_substitutions(child)
+        return substitutions
+
+    @staticmethod
+    def _do_substitute(substitution, resolved_value, is_optional_resolved=True):
+        unresolved = False
+        new_substitutions = []
+        if isinstance(resolved_value, ConfigValues):
+            resolved_value = resolved_value.transform()
+        if isinstance(resolved_value, ConfigValues):
+            unresolved = True
+            result = None
+        else:
+            # replace token by substitution
+            config_values = substitution.parent
+            # if it is a string, then add the extra ws that was present in the original string after the substitution
+            formatted_resolved_value = resolved_value \
+                if resolved_value is None \
+                or isinstance(resolved_value, (dict, list)) \
+                or substitution.index == len(config_values.tokens) - 1 \
+                else (str(resolved_value) + substitution.ws)
+            config_values.put(substitution.index, formatted_resolved_value)
+            transformation = config_values.transform()
+            result = None
+            if transformation is None and not is_optional_resolved:
+                result = config_values.overriden_value
+            else:
+                result = transformation
+
+            if result is None:
+                del config_values.parent[config_values.key]
+            else:
+                config_values.parent[config_values.key] = result
+                s = ConfigParser._find_substitutions(result)
+                if s:
+                    new_substitutions = s
+                    unresolved = True
+
+        return (unresolved, new_substitutions, result)
+
+    @staticmethod
+    def _final_fixup(item):
+        if isinstance(item, ConfigValues):
+            return item.transform()
+        elif isinstance(item, list):
+            return list([ConfigParser._final_fixup(child) for child in item])
+        elif isinstance(item, ConfigTree):
+            items = list(item.items())
+            for key, child in items:
+                item[key] = ConfigParser._final_fixup(child)
+        return item
+
     @staticmethod
     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
-
-        def final_fixup(item):
-            if isinstance(item, ConfigValues):
-                return item.transform()
-            elif isinstance(item, list):
-                return list([final_fixup(child) for child in item])
-            elif isinstance(item, ConfigTree):
-                items = list(item.items())
-                for key, child in items:
-                    item[key] = final_fixup(child)
-
-            return item
-
-        substitutions = find_substitutions(config)
+        ConfigParser._fixup_self_references(config)
+        substitutions = ConfigParser._find_substitutions(config)
 
         if len(substitutions) > 0:
             unresolved = True
@@ -340,42 +408,11 @@ class ConfigParser(object):
                     if not is_optional_resolved and substitution.optional:
                         resolved_value = None
 
-                    if isinstance(resolved_value, ConfigValues):
-                        resolved_value = resolved_value.transform()
-                    if isinstance(resolved_value, ConfigValues):
-                        unresolved = True
-                    else:
-                        # replace token by substitution
-                        config_values = substitution.parent
-                        # if it is a string, then add the extra ws that was present in the original string after the substitution
-                        formatted_resolved_value = resolved_value \
-                            if resolved_value is None \
-                            or isinstance(resolved_value, (dict, list)) \
-                            or substitution.index == len(config_values.tokens) - 1 \
-                            else (str(resolved_value) + substitution.ws)
-                        config_values.put(substitution.index, formatted_resolved_value)
-                        transformation = config_values.transform()
-                        if transformation is None and not is_optional_resolved:
-                            # if it does not override anything remove the key
-                            # otherwise put back old value that it was overriding
-                            if config_values.overriden_value is None:
-                                if config_values.key in config_values.parent:
-                                    del config_values.parent[config_values.key]
-                            else:
-                                config_values.parent[config_values.key] = config_values.overriden_value
-                                s = find_substitutions(config_values.overriden_value)
-                                if s:
-                                    substitutions.extend(s)
-                                    unresolved = True
-                        else:
-                            config_values.parent[config_values.key] = transformation
-                            s = find_substitutions(transformation)
-                            if s:
-                                substitutions.extend(s)
-                                unresolved = True
-                        substitutions.remove(substitution)
-
-            final_fixup(config)
+                    unresolved, new_subsitutions, _ = ConfigParser._do_substitute(substitution, resolved_value, is_optional_resolved)
+                    substitutions.extend(new_subsitutions)
+                    substitutions.remove(substitution)
+
+            ConfigParser._final_fixup(config)
             if unresolved:
                 raise ConfigSubstitutionException("Cannot resolve {variables}. Check for cycles.".format(
                     variables=', '.join('${{{variable}}}: (line: {line}, col: {col})'.format(
@@ -425,8 +462,9 @@ class ConfigTreeParser(TokenConverter):
     Parse a config tree from tokens
     """
 
-    def __init__(self, expr=None):
+    def __init__(self, expr=None, root=False):
         super(ConfigTreeParser, self).__init__(expr)
+        self.root = root
         self.saveAsList = True
 
     def postParse(self, instring, loc, token_list):
@@ -437,35 +475,47 @@ class ConfigTreeParser(TokenConverter):
         :param token_list:
         :return:
         """
-        config_tree = ConfigTree()
+        config_tree = ConfigTree(root=self.root)
         for element in token_list:
             expanded_tokens = element.tokens if isinstance(element, ConfigInclude) else [element]
 
             for tokens in expanded_tokens:
                 # key, value1 (optional), ...
                 key = tokens[0].strip()
-                values = tokens[1:]
-
+                operator = '='
+                if len(tokens) == 3 and tokens[1].strip() in [':', '=', '+=']:
+                    operator = tokens[1].strip()
+                    values = tokens[2:]
+                elif len(tokens) == 2:
+                    values = tokens[1:]
+                else:
+                    raise ParseSyntaxException("Unknown tokens {} received".format(tokens))
                 # empty string
                 if len(values) == 0:
                     config_tree.put(key, '')
                 else:
                     value = values[0]
-                    if isinstance(value, list):
+                    if isinstance(value, list) and operator == "+=":
+                        value = ConfigValues([ConfigSubstitution(key, True, '', False, loc), value], False, loc)
+                        config_tree.put(key, value, False)
+                    elif isinstance(value, str) and operator == "+=":
+                        value = ConfigValues([ConfigSubstitution(key, True, '', True, loc), ' ' + value], True, loc)
+                        config_tree.put(key, value, False)
+                    elif isinstance(value, list):
                         config_tree.put(key, value, False)
                     else:
-                        if isinstance(value, ConfigTree):
+                        existing_value = config_tree.get(key, None)
+                        if isinstance(value, ConfigTree) and not isinstance(existing_value, list):
+                            # Only Tree has to be merged with tree
                             config_tree.put(key, value, True)
                         elif isinstance(value, ConfigValues):
                             conf_value = value
                             value.parent = config_tree
                             value.key = key
-                            existing_value = config_tree.get(key, None)
                             if isinstance(existing_value, list) or isinstance(existing_value, ConfigTree):
                                 config_tree.put(key, conf_value, True)
                             else:
                                 config_tree.put(key, conf_value, False)
                         else:
-                            conf_value = value
-                            config_tree.put(key, conf_value, False)
+                            config_tree.put(key, value, False)
         return config_tree
diff --git a/pyhocon/config_tree.py b/pyhocon/config_tree.py
index 92a7afc..225e15f 100644
--- a/pyhocon/config_tree.py
+++ b/pyhocon/config_tree.py
@@ -26,8 +26,10 @@ class ConfigTree(OrderedDict):
     KEY_SEP = '.'
 
     def __init__(self, *args, **kwds):
+        self.root = kwds.pop('root') if 'root' in kwds else False
+        if self.root:
+            self.history = {}
         super(ConfigTree, self).__init__(*args, **kwds)
-
         for key, value in self.items():
             if isinstance(value, ConfigValues):
                 value.parent = self
@@ -55,6 +57,8 @@ class ConfigTree(OrderedDict):
                     value.key = key
                     value.overriden_value = a.get(key, None)
                 a[key] = value
+                if a.root:
+                    a.history[key] = (a.history.get(key) or []) + b.history.get(key)
 
         return a
 
@@ -65,7 +69,13 @@ class ConfigTree(OrderedDict):
             # if they are both configs then merge
             # if not then override
             if key_elt in self and isinstance(self[key_elt], ConfigTree) and isinstance(value, ConfigTree):
-                ConfigTree.merge_configs(self[key_elt], value)
+                if self.root:
+                    new_value = ConfigTree.merge_configs(ConfigTree(), self[key_elt], copy_trees=True)
+                    new_value = ConfigTree.merge_configs(new_value, value, copy_trees=True)
+                    self._push_history(key_elt, new_value)
+                    self[key_elt] = new_value
+                else:
+                    ConfigTree.merge_configs(self[key_elt], value)
             elif append:
                 # If we have t=1
                 # and we try to put t.a=5 then t is replaced by {a: 5}
@@ -76,10 +86,16 @@ class ConfigTree(OrderedDict):
                 elif isinstance(l, ConfigTree) and isinstance(value, ConfigValues):
                     value.tokens.append(l)
                     value.recompute()
+                    self._push_history(key_elt, value)
+                    self[key_elt] = value
+                elif isinstance(l, list) and isinstance(value, ConfigValues):
+                    self._push_history(key_elt, value)
                     self[key_elt] = value
                 elif isinstance(l, list):
                     l += value
+                    self._push_history(key_elt, l)
                 elif l is None:
+                    self._push_history(key_elt, value)
                     self[key_elt] = value
 
                 else:
@@ -96,15 +112,24 @@ class ConfigTree(OrderedDict):
                     value.parent = self
                     value.key = key_elt
                     value.overriden_value = self.get(key_elt, None)
-                super(ConfigTree, self).__setitem__(key_elt, value)
+                self._push_history(key_elt, value)
+                self[key_elt] = value
         else:
             next_config_tree = super(ConfigTree, self).get(key_elt)
             if not isinstance(next_config_tree, ConfigTree):
                 # create a new dictionary or overwrite a previous value
                 next_config_tree = ConfigTree()
+                self._push_history(key_elt, value)
                 self[key_elt] = next_config_tree
             next_config_tree._put(key_path[1:], value, append)
 
+    def _push_history(self, key, value):
+        if self.root:
+            hist = self.history.get(key)
+            if hist is None:
+                hist = self.history[key] = []
+            hist.append(value)
+
     def _get(self, key_path, key_index=0, default=UndefinedKey):
         key_elt = key_path[key_index]
         elt = super(ConfigTree, self).get(key_elt, UndefinedKey)
@@ -130,7 +155,8 @@ class ConfigTree(OrderedDict):
             else:
                 return default
 
-    def _parse_key(self, str):
+    @staticmethod
+    def parse_key(str):
         """
         Split a key into path elements:
         - a.b.c => a, b, c
@@ -150,7 +176,7 @@ class ConfigTree(OrderedDict):
         :type key: basestring
         :param value: value to put
         """
-        self._put(self._parse_key(key), value, append)
+        self._put(ConfigTree.parse_key(key), value, append)
 
     def get(self, key, default=UndefinedKey):
         """Get a value from the tree
@@ -161,7 +187,7 @@ class ConfigTree(OrderedDict):
         :type default: object
         :return: value in the tree located at key
         """
-        return self._get(self._parse_key(key), 0, default)
+        return self._get(ConfigTree.parse_key(key), 0, default)
 
     def get_string(self, key, default=UndefinedKey):
         """Return string representation of value found at key


Test patch:
diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py
index a639f89..2c93f13 100644
--- a/tests/test_config_parser.py
+++ b/tests/test_config_parser.py
@@ -706,6 +706,234 @@ class TestConfigParser(object):
                 """
             )
 
+    def test_self_ref_substitution_array(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = [1,2]
+            x = ${x} [3,4]
+            x = [-1, 0] ${x} [5, 6]
+            x = [-3, -2] ${x}
+            """
+        )
+        assert config.get("x") == [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6]
+
+    def test_self_append_array(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = [1,2]
+            x += [3,4]
+            """
+        )
+        assert config.get("x") == [1, 2, 3, 4]
+
+    def test_self_append_string(self):
+        '''
+        Should be equivalent to
+        x = abc
+        x = ${?x} def
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            x = abc
+            x += def
+            """
+        )
+        assert config.get("x") == "abc def"
+
+    def test_self_append_non_existent_string(self):
+        '''
+        Should be equivalent to x = ${?x} def
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            x += def
+            """
+        )
+        assert config.get("x") == " def"
+
+    def test_self_append_nonexistent_array(self):
+        config = ConfigFactory.parse_string(
+            """
+            x += [1,2]
+            """
+        )
+        assert config.get("x") == [1, 2]
+
+    def test_self_append_object(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = {a: 1}
+            x += {b: 2}
+            """
+        )
+        assert config.get("x") == {'a': 1, 'b': 2}
+
+    def test_self_append_nonexistent_object(self):
+        config = ConfigFactory.parse_string(
+            """
+            x += {a: 1}
+            """
+        )
+        assert config.get("x") == {'a': 1}
+
+    def test_self_ref_substitution_array_to_dict(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = [1,2]
+            x = {x: [3,4]}
+            x = {y: [5,6]}
+            x = {z: ${x}}
+            """
+        )
+        assert config.get("x.x") == [3, 4]
+        assert config.get("x.y") == [5, 6]
+        assert config.get("x.z") == {'x': [3, 4], 'y': [5, 6]}
+
+    def test_self_ref_substitiotion_dict_in_array(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = {x: [3,4]}
+            x = [${x}, 2, 3]
+            """
+        )
+        (one, two, three) = config.get("x")
+        assert one == {'x': [3, 4]}
+        assert two == 2
+        assert three == 3
+
+    def test_self_ref_substitution_dict_path(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = {y: {z: 1}}
+            x = ${x.y}
+            """
+        )
+        assert config.get("x.y") == {'z': 1}
+        assert config.get("x.z") == 1
+        assert set(config.get("x").keys()) == set(['y', 'z'])
+
+    def test_self_ref_substitution_dict_path_hide(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = {y: {y: 1}}
+            x = ${x.y}
+            """
+        )
+        assert config.get("x.y") == {'y': 1}
+        assert set(config.get("x").keys()) == set(['y'])
+
+    def test_self_ref_substitution_dict_recurse(self):
+        with pytest.raises(ConfigSubstitutionException):
+            ConfigFactory.parse_string(
+                """
+                x = ${x}
+                """
+            )
+
+    def test_self_ref_substitution_dict_recurse2(self):
+        with pytest.raises(ConfigSubstitutionException):
+            ConfigFactory.parse_string(
+                """
+                x = ${x}
+                x = ${x}
+                """
+            )
+
+    def test_self_ref_substitution_dict_merge(self):
+        '''
+        Example from HOCON spec
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            foo : { a : { c : 1 } }
+            foo : ${foo.a}
+            foo : { a : 2 }
+            """
+        )
+        assert config.get('foo') == {'a': 2, 'c': 1}
+        assert set(config.keys()) == set(['foo'])
+
+    def test_self_ref_substitution_dict_otherfield(self):
+        '''
+        Example from HOCON spec
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            bar : {
+              foo : 42,
+              baz : ${bar.foo}
+            }
+            """
+        )
+        assert config.get("bar") == {'foo': 42, 'baz': 42}
+        assert set(config.keys()) == set(['bar'])
+
+    def test_self_ref_substitution_dict_otherfield_merged_in(self):
+        '''
+        Example from HOCON spec
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            bar : {
+                foo : 42,
+                baz : ${bar.foo}
+            }
+            bar : { foo : 43 }
+            """
+        )
+        assert config.get("bar") == {'foo': 43, 'baz': 43}
+        assert set(config.keys()) == set(['bar'])
+
+    def test_self_ref_substitution_dict_otherfield_merged_in_mutual(self):
+        '''
+        Example from HOCON spec
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            // bar.a should end up as 4
+            bar : { a : ${foo.d}, b : 1 }
+            bar.b = 3
+            // foo.c should end up as 3
+            foo : { c : ${bar.b}, d : 2 }
+            foo.d = 4
+            """
+        )
+        assert config.get("bar") == {'a': 4, 'b': 3}
+        assert config.get("foo") == {'c': 3, 'd': 4}
+        assert set(config.keys()) == set(['bar', 'foo'])
+
+    def test_self_ref_substitution_string_opt_concat(self):
+        '''
+        Example from HOCON spec
+        '''
+        config = ConfigFactory.parse_string(
+            """
+            a = ${?a}foo
+            """
+        )
+        assert config.get("a") == 'foo'
+        assert set(config.keys()) == set(['a'])
+
+    def test_self_ref_substitution_dict_recurse_part(self):
+        with pytest.raises(ConfigSubstitutionException):
+            ConfigFactory.parse_string(
+                """
+                x = ${x} {y: 1}
+                x = ${x.y}
+                """
+            )
+
+    def test_self_ref_substitution_object(self):
+        config = ConfigFactory.parse_string(
+            """
+            x = {a: 1, b: 2}
+            x = ${x} {c: 3}
+            x = {z: 0} ${x}
+            x = {y: -1} ${x} {d: 4}
+            """
+        )
+        assert config.get("x") == {'a': 1, 'b': 2, 'c': 3, 'z': 0, 'y': -1, 'd': 4}
+
     def test_concat_multi_line_string(self):
         config = ConfigFactory.parse_string(
             """
@@ -1278,6 +1506,66 @@ class TestConfigParser(object):
             'large-jvm-opts': ['-XX:+UseParNewGC', '-Xm16g']
         }
 
+    def test_fallback_self_ref_substitutions_append(self):
+        config1 = ConfigFactory.parse_string(
+            """
+            list = [ 1, 2, 3 ]
+            """
+        )
+        config2 = ConfigFactory.parse_string(
+            """
+            list = ${list} [ 4, 5, 6 ]
+            """,
+            resolve=False
+        )
+        config2 = config2.with_fallback(config1)
+        assert config2.get("list") == [1, 2, 3, 4, 5, 6]
+
+    def test_fallback_self_ref_substitutions_append_plus_equals(self):
+        config1 = ConfigFactory.parse_string(
+            """
+            list = [ 1, 2, 3 ]
+            """
+        )
+        config2 = ConfigFactory.parse_string(
+            """
+            list += [ 4, 5, 6 ]
+            """,
+            resolve=False
+        )
+        config2 = config2.with_fallback(config1)
+        assert config2.get("list") == [1, 2, 3, 4, 5, 6]
+
+    def test_fallback_self_ref_substitutions_merge(self):
+        config1 = ConfigFactory.parse_string(
+            """
+            dict = { x: 1 }
+            """
+        )
+        config2 = ConfigFactory.parse_string(
+            """
+            dict = ${dict} { y: 2 }
+            """,
+            resolve=False
+        )
+        config2 = config2.with_fallback(config1)
+        assert config2.get("dict") == {'x': 1, 'y': 2}
+
+    def test_fallback_self_ref_substitutions_concat_string(self):
+        config1 = ConfigFactory.parse_string(
+            """
+            string = abc
+            """
+        )
+        config2 = ConfigFactory.parse_string(
+            """
+            string = ${string}def
+            """,
+            resolve=False
+        )
+        config2 = config2.with_fallback(config1)
+        assert config2.get("string") == 'abcdef'
+
     def test_object_field_substitution(self):
         config = ConfigFactory.parse_string(
             """