Repository: ipython/traitlets
Pull request #558 (2020-05-30 21:15:40)

Allow compounding of Lazy traitlets across configuration files.
This is mostly intesting in the case of systemwide configuration in
/etc/local... to provide default configuration to user and still allow
them to extend their configuration.

Current traitlets behavior is as follow.

   /etc/local/config.py ::  c.Foo.value = ['a']
   ~/.jupyter/config.py ::  c.Foo.value.append('b')

Final seen value will be `['b']`

With the changes included here, you would get `['a', 'b']`

It also become possible to do:

   /etc/local/config.py ::  c.Foo.value.append('a')
   ~/.jupyter/config.py ::  c.Foo.value.append('b')

In which case the end result will be `['default', 'a', 'b']`.

This feel like a more reasonable (and requested) behavior than the
current.


Patch:
diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py
index 12d4592..62c26e3 100644
--- a/traitlets/config/loader.py
+++ b/traitlets/config/loader.py
@@ -77,6 +77,7 @@ class LazyConfigValue(HasTraits):
     # list methods
     _extend = List()
     _prepend = List()
+    _inserts = List()
 
     def append(self, obj):
         self._extend.append(obj)
@@ -88,7 +89,42 @@ class LazyConfigValue(HasTraits):
         """like list.extend, but for the front"""
         self._prepend[:0] = other
 
-    _inserts = List()
+
+    def merge_into(self, other):
+        """
+        Merge with another  earlier LazyConfig Value or an earlier container.
+        This is used when having global systemwide configuration file.
+
+        Self is expected to have higher precedence.
+
+        Parameters:
+        -----------
+
+        other: LazyConfigValue or container
+
+
+        Return: LazyConfigValue if ``other`` is also lazy, a reified container
+        otherwise.
+        """
+        if isinstance(other, LazyConfigValue):
+            other._extend.extend(self._extend)
+            self._extend = other._extend
+
+            self._prepend.extend(other._prepend)
+
+            other._inserts.extend(self._inserts)
+            self._inserts = other._inserts
+
+            if self._update:
+                other.update(self._update)
+                self._update = other._update
+            return self
+        else:
+            # other is a container, reify now.
+            return self.get_value(other)
+
+
+
     def insert(self, index, other):
         if not isinstance(index, int):
             raise TypeError("An integer is required")
@@ -197,6 +233,8 @@ class Config(dict):
                 if isinstance(v, Config) and isinstance(self[k], Config):
                     # Recursively merge common sub Configs
                     self[k].merge(v)
+                elif isinstance(v, LazyConfigValue):
+                    self[k] = v.merge_into(self[k])
                 else:
                     # Plain updates for non-Configs
                     to_update[k] = v


Test patch:
diff --git a/traitlets/config/tests/test_loader.py b/traitlets/config/tests/test_loader.py
index 3fc0fb3..4263fb6 100644
--- a/traitlets/config/tests/test_loader.py
+++ b/traitlets/config/tests/test_loader.py
@@ -526,3 +526,132 @@ class TestConfig(TestCase):
         self.assertEqual(c.Foo.trait, [1])
         self.assertEqual(c2.Foo.trait, [1])
 
+    
+    def test_merge_multi_lazy(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        If systemwide overwirte and user append, we want both in the right
+        order.
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait = [1]
+        c2.Foo.trait.append(2)
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait, [1,2] )
+
+
+    
+    def test_merge_multi_lazyII(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        If both are lazy we still want a lazy config.
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait.append(1)
+        c2.Foo.trait.append(2)
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait._extend, [1,2] )
+
+    def test_merge_multi_lazy_III(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        Prepend should prepend in the right order.
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait = [1]
+        c2.Foo.trait.prepend([0])
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait, [0, 1] )
+
+    def test_merge_multi_lazy_IV(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        Both prepending should be lazy
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait.prepend([1])
+        c2.Foo.trait.prepend([0])
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait._prepend, [0, 1])
+
+    def test_merge_multi_lazy_update_I(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        dict update shoudl be in the right order.
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait = {"a": 1, "z": 26}
+        c2.Foo.trait.update({"a": 0, "b": 1})
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait, {"a": 0, "b": 1, "z": 26})
+
+    def test_merge_multi_lazy_update_II(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        Later dict overwrite lazyness
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait.update({"a": 0, "b": 1})
+        c2.Foo.trait = {"a": 1, "z": 26}
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait, {"a": 1, "z": 26})
+
+    def test_merge_multi_lazy_update_III(self):
+        """
+        With multiple config files (systemwide and users), we want compounding.
+
+        Later dict overwrite lazyness
+        """
+        c1 = Config()
+        c2 = Config()
+
+        c1.Foo.trait.update({"a": 0, "b": 1})
+        c2.Foo.trait.update({"a": 1, "z": 26})
+
+        c = Config()
+        c.merge(c1)
+        c.merge(c2)
+
+        self.assertEqual(c.Foo.trait._update, {"a": 1, "z": 26, "b": 1})