Skip to content

Commit 1b0545f

Browse files
authored
Fix dotted dict processing dots in second+ level keys (#2745)
1 parent dfd3a91 commit 1b0545f

File tree

2 files changed

+58
-13
lines changed

2 files changed

+58
-13
lines changed

plugin/core/collections.py

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
import sublime
88

99

10+
def deep_merge(base: dict[str, Any], update: dict[str, Any]) -> None:
11+
"""Recursively merge update dict with base dict."""
12+
for key, value in update.items():
13+
if isinstance(value, dict) and key in base and isinstance(base[key], dict):
14+
deep_merge(base[key], value)
15+
else:
16+
base[key] = deepcopy(value)
17+
18+
1019
class DottedDict:
1120

1221
__slots__ = ('_d',)
@@ -15,6 +24,9 @@ def __init__(self, d: dict[str, Any] | None = None) -> None:
1524
"""
1625
Construct a DottedDict, optionally from an existing dictionary.
1726
27+
The dots within the first-level keys (only) of the passed dict will be interpreted as nesting triggers and the
28+
resulting dict will have those transformed into nested keys.
29+
1830
:param d: An existing dictionary.
1931
"""
2032
self._d: dict[str, Any] = {}
@@ -138,13 +150,13 @@ def update(self, d: dict[str, Any]) -> None:
138150
"""
139151
Overwrite and/or add new key-value pairs to the collection.
140152
153+
The dots within the first-level keys (only) of the passed dict will be interpreted as nesting triggers and the
154+
resulting dict will have those transformed into nested keys.
155+
141156
:param d: The overriding dictionary. Can contain nested dictionaries.
142157
"""
143158
for key, value in d.items():
144-
if isinstance(value, dict):
145-
self._update_recursive(value, key)
146-
else:
147-
self.set(key, value)
159+
self._merge(key, value)
148160

149161
def get_resolved(self, variables: dict[str, str]) -> dict[str, Any]:
150162
"""
@@ -156,15 +168,29 @@ def get_resolved(self, variables: dict[str, str]) -> dict[str, Any]:
156168
"""
157169
return sublime.expand_variables(self._d, variables)
158170

159-
def _update_recursive(self, current: dict[str, Any], prefix: str) -> None:
160-
if not current or any(filter(lambda key: isinstance(key, str) and (":" in key or "/" in key), current.keys())):
161-
return self.set(prefix, current)
162-
for key, value in current.items():
163-
path = f"{prefix}.{key}"
164-
if isinstance(value, dict):
165-
self._update_recursive(value, path)
166-
else:
167-
self.set(path, value)
171+
def _merge(self, path: str, value: Any) -> None:
172+
"""
173+
Update a value in the dictionary (merge if value is a dict).
174+
175+
:param path: The path, e.g. foo.bar.baz
176+
:param value: The value
177+
"""
178+
current = self._d
179+
keys = path.split('.')
180+
for i in range(0, len(keys) - 1):
181+
key = keys[i]
182+
next_current = current.get(key)
183+
if not isinstance(next_current, dict):
184+
next_current = {}
185+
current[key] = next_current
186+
current = next_current
187+
last_key = keys[-1]
188+
if isinstance(value, dict):
189+
if not isinstance(current.get(last_key), dict):
190+
current[last_key] = {}
191+
deep_merge(current[last_key], value)
192+
else:
193+
current[last_key] = value
168194

169195
def __repr__(self) -> str:
170196
return f"{self.__class__.__name__}({repr(self._d)})"

tests/test_collections.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ def test_set_and_get(self) -> None:
2525
d.set("foo.bar.baz", {"some": "dict"})
2626
self.verify(d, "foo.bar.baz.some", "dict")
2727

28+
def test_does_not_expand_at_second_nesting_level(self) -> None:
29+
d = DottedDict({"editor.codeActionsOnSave": {"source.fixAll": "explicit"}})
30+
self.verify(d, "editor.codeActionsOnSave", {"source.fixAll": "explicit"})
31+
self.assertIsNone(d.get("editor.codeActionsOnSave.source"))
32+
33+
def test_overwrite_int_with_dict(self) -> None:
34+
d = DottedDict({'foo.bar': 1})
35+
d.set('foo.bar', {
36+
"a": "a",
37+
})
38+
self.verify(d, "foo.bar", {"a": "a"})
39+
40+
def test_overwrite_dict_with_dict(self) -> None:
41+
d = DottedDict({'foo.bar': {'a': 'a'}})
42+
d.set('foo.bar', {
43+
"b": "b",
44+
})
45+
self.verify(d, "foo.bar", {"b": "b"})
46+
2847
def test_remove(self) -> None:
2948
d = DottedDict()
3049
d.set("foo", "asdf")

0 commit comments

Comments
 (0)