Repository: qutebrowser/qutebrowser

Issue:
New config: url.searchengines retains only most recently-updated key
On the latest new-config branch, using a `config.py` (a truncated variant of mine for brevity, but the problem is the same for any more than one update) with
```
c.url.searchengines['arch'] = 'https://wiki.archlinux.org/index.php?title=Special%3ASearch&search={}' 
c.url.searchengines['aur'] = 'https://aur.archlinux.org/packages/?O=0&K={}'     
```
only `:open aur somesearchterm` works as expected; `:open arch somesearchterm` falls back on `DEFAULT` and opens a DDG search for `arch somesearchterm` instead. The debug log gives
```
02:54:00 DEBUG    init       app:_init_modules:422 Initializing config...
02:54:00 DEBUG    config     config:update_mutables:475 url.searchengines was mutated, updating
02:54:00 DEBUG    config     config:_set_value:396 Config option changed: url.searchengines = {'DEFAULT': 'https://duckduckgo.com/?q={}', 'arch': 'https://wiki.archlinux.org/index.php?title=Special%3ASearch&search={}'}
02:54:00 DEBUG    config     config:update_mutables:475 url.searchengines was mutated, updating
02:54:00 DEBUG    config     config:_set_value:396 Config option changed: url.searchengines = {'DEFAULT': 'https://duckduckgo.com/?q={}', 'aur': 'https://aur.archlinux.org/packages/?O=0&K={}'}
```

Adding some additional logging to `Config.update_mutables` shows that -- while `self._mutables` contains the updated value:
```
[('url.searchengines', {'DEFAULT': 'https://duckduckgo.com/?q={}'}, {'DEFAULT': 'https://duckduckgo.com/?q={}', 'arch': 'https://wiki.archlinux.org/index.php?title=Special%3ASearch&search={}'}), ('url.searchengines', {'DEFAULT': 'https://duckduckgo.com/?q={}'}, {'DEFAULT': 'https://duckduckgo.com/?q={}', 'aur': 'https://aur.archlinux.org/packages/?O=0&K={}'})]
```
the actual mutation only ever occurs on the first entry in the list:
```
02:57:26 DEBUG    config     config:update_mutables:476 OLD VALUE: {'DEFAULT': 'https://duckduckgo.com/?q={}'}
02:57:26 DEBUG    config     config:update_mutables:478 url.searchengines was mutated, updating
02:57:26 DEBUG    config     config:_set_value:397 Config option changed: url.searchengines = {'DEFAULT': 'https://duckduckgo.com/?q={}', 'arch': 'https://wiki.archlinux.org/index.php?title=Special%3ASearch&search={}'}
02:57:26 DEBUG    config     config:update_mutables:476 OLD VALUE: {'DEFAULT': 'https://duckduckgo.com/?q={}'}
02:57:26 DEBUG    config     config:update_mutables:478 url.searchengines was mutated, updating
02:57:26 DEBUG    config     config:_set_value:397 Config option changed: url.searchengines = {'DEFAULT': 'https://duckduckgo.com/?q={}', 'aur': 'https://aur.archlinux.org/packages/?O=0&K={}'}
```


Discussion:
Note that you should use the latest `master` now that `new-config` has been merged, but this is probably happening there as well.

Resolving patch:
diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py
--- a/qutebrowser/config/config.py
+++ b/qutebrowser/config/config.py
@@ -368,7 +368,7 @@ class Config(QObject):
 
     Attributes:
         _values: A dict mapping setting names to their values.
-        _mutables: A list of mutable objects to be checked for changes.
+        _mutables: A dictionary of mutable objects to be checked for changes.
         _yaml: A YamlConfig object or None.
 
     Signals:
@@ -381,7 +381,7 @@ def __init__(self, yaml_config, parent=None):
         super().__init__(parent)
         self.changed.connect(_render_stylesheet.cache_clear)
         self._values = {}
-        self._mutables = []
+        self._mutables = {}
         self._yaml = yaml_config
 
     def init_save_manager(self, save_manager):
@@ -430,16 +430,23 @@ def get_obj(self, name, *, mutable=True):
         If mutable=True is set, watch the returned object for mutations.
         """
         opt = self.get_opt(name)
-        # We always return a copy of the value stored internally, so the
+        obj = None
+        # If we allow mutation, there is a chance that prior mutations already
+        # entered the mutable dictionary and thus further copies are unneeded
+        # until update_mutables() is called
+        if name in self._mutables and mutable:
+            obj = self._mutables[name][1]
+        # Otherwise, we return a copy of the value stored internally, so the
         # internal value can never be changed by mutating the object returned.
-        obj = copy.deepcopy(self._values.get(name, opt.default))
-        # Then we watch the returned object for changes.
-        if isinstance(obj, (dict, list)):
-            if mutable:
-                self._mutables.append((name, copy.deepcopy(obj), obj))
         else:
-            # Shouldn't be mutable (and thus hashable)
-            assert obj.__hash__ is not None, obj
+            obj = copy.deepcopy(self._values.get(name, opt.default))
+            # Then we watch the returned object for changes.
+            if isinstance(obj, (dict, list)):
+                if mutable:
+                    self._mutables[name] = (copy.deepcopy(obj), obj)
+            else:
+                # Shouldn't be mutable (and thus hashable)
+                assert obj.__hash__ is not None, obj
         return obj
 
     def get_str(self, name):
@@ -480,11 +487,13 @@ def update_mutables(self, *, save_yaml=False):
         Here, we check all those saved copies for mutations, and if something
         mutated, we call set_obj again so we save the new value.
         """
-        for name, old_value, new_value in self._mutables:
+        for name in self._mutables:
+            old_value = self._mutables[name][0]
+            new_value = self._mutables[name][1]
             if old_value != new_value:
                 log.config.debug("{} was mutated, updating".format(name))
                 self.set_obj(name, new_value, save_yaml=save_yaml)
-        self._mutables = []
+        self._mutables = {}
 
     def dump_userconfig(self):
         """Get the part of the config which was changed by the user.


Test patch:
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -706,7 +706,7 @@ def test_get_obj_mutable(self, conf, config_stub, qtbot, caplog,
                         assert obj == new
 
         if mutable:
-            assert conf._mutables == [(option, old, new)]
+            assert conf._mutables[option] == (old, new)
 
         if mutable and mutated:
             # Now let's update
diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py
--- a/tests/unit/config/test_configfiles.py
+++ b/tests/unit/config/test_configfiles.py
@@ -217,9 +217,11 @@ def test_unbind(self, confpy, line, key, mode):
         assert config.instance._values['bindings.commands'] == expected
 
     def test_mutating(self, confpy):
-        confpy.write('c.aliases["foo"] = "message-info foo"')
+        confpy.write('c.aliases["foo"] = "message-info foo"',
+                     'c.aliases["bar"] = "message-info bar"')
         confpy.read()
         assert config.instance._values['aliases']['foo'] == 'message-info foo'
+        assert config.instance._values['aliases']['bar'] == 'message-info bar'
 
     def test_reading_default_location(self, config_tmpdir):
         (config_tmpdir / 'config.py').write_text(