diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py index cc1c1de..2e226e9 100644 --- a/pyhocon/config_parser.py +++ b/pyhocon/config_parser.py @@ -167,7 +167,10 @@ def create_tree(value): if isinstance(value, dict): res = ConfigTree(root=root) for key, child_value in value.items(): - res.put(key, create_tree(child_value)) + # Use _put with a single-element path to preserve the literal key. + # Using put() would split the key on dots (e.g. "a.b" → path ["a","b"]), + # truncating or nesting keys that contain HOCON path-separator characters. + res._put([key], create_tree(child_value)) return res if isinstance(value, list): return [create_tree(v) for v in value] diff --git a/pyhocon/config_tree.py b/pyhocon/config_tree.py index 5ca1657..8e27295 100644 --- a/pyhocon/config_tree.py +++ b/pyhocon/config_tree.py @@ -227,6 +227,16 @@ def get(self, key, default=UndefinedKey): :type default: object :return: value in the tree located at key """ + # Try an exact (literal) key lookup first so that keys containing dots + # that were stored verbatim (e.g. via from_dict()) are found before the + # dotted-path traversal interprets those dots as path separators. + exact = super(ConfigTree, self).get(key, UndefinedKey) + if exact is not UndefinedKey: + if isinstance(exact, NoneValue): + return None + if isinstance(exact, list): + return [None if isinstance(x, NoneValue) else x for x in exact] + return exact return self._get(ConfigTree.parse_key(key), 0, default) def get_string(self, key, default=UndefinedKey): diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index 6e0cae2..4555252 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -2310,6 +2310,24 @@ def test_from_dict_with_nested_dict(self): config = ConfigFactory.from_dict(d) assert config == d + def test_from_dict_preserves_dotted_keys(self): + """Keys containing dots must be stored as literal keys, not split into nested paths.""" + d = { + 'myStuff': { + 'myThing_r0.1': ['someVal'], + } + } + config = ConfigFactory.from_dict(d) + # The key 'myThing_r0.1' must appear verbatim, not be truncated to 'myThing_r0'. + assert list(config['myStuff'].keys()) == ['myThing_r0.1'] + # Iterating items() must also yield the original key. + items = list(config['myStuff'].items()) + assert items == [('myThing_r0.1', ['someVal'])] + # Direct get() must find the value. + assert config['myStuff'].get('myThing_r0.1') == ['someVal'] + # Round-trip equality with the original dict. + assert config == d + def test_object_concat(self): config = ConfigFactory.parse_string( """o1 = {