changeset 741:e069797f0e36

Implemented the new merge stragegies when merging lists: "extend" and "prepend"
author Franz Glasner <fzglas.hg@dom66.de>
date Sun, 29 Oct 2023 17:15:41 +0100
parents cc9dff5fe0ca
children 220a9ec9ac72
files CHANGES.txt configmix/__init__.py tests/data/conf10_list_extend.yml tests/test.py
diffstat 4 files changed, 125 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sun Oct 29 17:13:32 2023 +0100
+++ b/CHANGES.txt	Sun Oct 29 17:15:41 2023 +0100
@@ -16,8 +16,10 @@
 ~~~~~~~~~~~~~~~~~~~~~~~
 
 - **[feature]**
-  New merge strategy for lists: extend instead of replace. This is a new
-  global load/merge flag.
+  Allow new merge strategies for lists: extend or prepend instead of
+  replace. This is implemented with a new optional parameter
+  ``merge_lists`` for :py:func:`configmix.load` or
+  :py:func:`configmix.merge`.
 
 - **[bugfix]**
   Allow again the installation of the pure-Python version for Python < 3.7.
--- a/configmix/__init__.py	Sun Oct 29 17:13:32 2023 +0100
+++ b/configmix/__init__.py	Sun Oct 29 17:15:41 2023 +0100
@@ -57,6 +57,11 @@
     :keyword strict: enable strict parsing mode for parsers that support it
                      (e.g. to prevent duplicate keys)
     :type strict: bool
+    :keyword merge_lists: When ``None`` then lists will be overwritten
+                          by the merge process. When ``extend`` then
+                          lists will be extended instead.
+                          This parameter is passed to :func:`.merge`.
+    :type merge_lists: str or None
     :returns: the configuration
     :rtype: ~configmix.config.Configuration
 
@@ -64,22 +69,23 @@
     defaults = kwargs.get("defaults")
     extras = kwargs.get("extras")
     strict = kwargs.get("strict", False)
+    merge_lists = kwargs.get("merge_lists", None)
     if defaults is None:
         ex = Configuration()
     else:
-        ex = merge(None, Configuration(defaults))
+        ex = merge(None, Configuration(defaults), merge_lists=merge_lists)
     for f in files:
         if f.startswith(constants.DIR_PREFIX):
             for f2 in _get_configuration_files_from_dir(f[5:]):
                 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict)
                 if nx is not None:
-                    ex = merge(nx, ex)
+                    ex = merge(nx, ex, merge_lists=merge_lists)
         else:
             nx = _load_cfg_from_file(f, strict=strict)
             if nx is not None:
-                ex = merge(nx, ex)
+                ex = merge(nx, ex, merge_lists=merge_lists)
     if extras:
-        ex = merge(Configuration(extras), ex)
+        ex = merge(Configuration(extras), ex, merge_lists=merge_lists)
     return Configuration(ex)
 
 
@@ -91,22 +97,23 @@
     defaults = kwargs.get("defaults")
     extras = kwargs.get("extras")
     strict = kwargs.get("strict", False)
+    merge_lists = kwargs.get("merge_lists", None)
     if defaults is None:
         ex = Configuration()
     else:
-        ex = safe_merge(None, Configuration(defaults))
+        ex = safe_merge(None, Configuration(defaults), merge_lists=merge_lists)
     for f in files:
         if f.startswith(constants.DIR_PREFIX):
             for f2 in _get_configuration_files_from_dir(f[5:]):
                 nx = _load_cfg_from_file(f2, ignore_unknown=True, strict=strict)
                 if nx is not None:
-                    ex = safe_merge(nx, ex)
+                    ex = safe_merge(nx, ex, merge_lists=merge_lists)
         else:
             nx = _load_cfg_from_file(f, strict=strict)
             if nx is not None:
-                ex = safe_merge(nx, ex)
+                ex = safe_merge(nx, ex, merge_lists=merge_lists)
     if extras:
-        ex = safe_merge(Configuration(extras), ex)
+        ex = safe_merge(Configuration(extras), ex, merge_lists=merge_lists)
     return Configuration(ex)
 
 
@@ -389,7 +396,7 @@
         return result
 
 
-def merge(user, default, filter_comments=True, extend_lists=False):
+def merge(user, default, filter_comments=True, merge_lists=None):
     """Logically merge the configuration in `user` into `default`.
 
     :param ~configmix.config.Configuration user:
@@ -399,9 +406,10 @@
                 the base configuration where `user` is logically merged into
     :param bool filter_comments: flag whether to filter comment keys that
                    start with any of the items in :data:`.COMMENTS`
-    :param bool extend_lists: When ``True`` then lists will be
-                              extended instead of overwritten by the
-                              merge process
+    :param merge_lists: When ``None`` then lists will be overwritten
+                        by the merge process. When ``extend`` then
+                        lists will be extended instead.
+    :type merge_lists: str or None
     :returns: `user` with the necessary amendments from `default`.
               If `user` is ``None`` then `default` is returned.
 
@@ -438,14 +446,25 @@
                     # do not copy
                     del user[k]
                 else:
-                    user[k] = _merge(ukv, v, filter_comments, extend_lists)
+                    user[k] = _merge(ukv, v, filter_comments, merge_lists)
             else:
                 user[k] = v
+    elif merge_lists is not None:
+        if merge_lists == "extend":
+            if isinstance(user, list) and isinstance(default, list):
+                for idx, value in enumerate(default):
+                    user.insert(idx, value)
+        elif merge_lists == "prepend":
+            if isinstance(user, list) and isinstance(default, list):
+                user.extend(default)
+        else:
+            raise ValueError(
+                "unknown strategy for merge_lists: %s" % (merge_lists,))
     _filter_deletions(user)
     return user
 
 
-def _merge(user, default, filter_comments, extend_lists):
+def _merge(user, default, filter_comments, merge_lists):
     """Recursion helper for :func:`.merge`
 
     """
@@ -462,13 +481,24 @@
                     # do not copy
                     del user[k]
                 else:
-                    user[k] = _merge(ukv, v, filter_comments, extend_lists)
+                    user[k] = _merge(ukv, v, filter_comments, merge_lists)
             else:
                 user[k] = v
+    elif merge_lists is not None:
+        if merge_lists == "extend":
+            if isinstance(user, list) and isinstance(default, list):
+                for idx, value in enumerate(default):
+                    user.insert(idx, value)
+        elif merge_lists == "prepend":
+            if isinstance(user, list) and isinstance(default, list):
+                user.extend(default)
+        else:
+            raise ValueError(
+                "unknown strategy for merge_lists: %s" % (merge_lists,))
     return user
 
 
-def safe_merge(user, default, filter_comments=True, extend_lists=False):
+def safe_merge(user, default, filter_comments=True, merge_lists=None):
     """A more safe version of :func:`.merge` that makes deep copies of
     the returned container objects.
 
@@ -499,14 +529,25 @@
                     # do not copy
                     del user[k]
                 else:
-                    user[k] = _safe_merge(ukv, v, filter_comments, extend_lists)
+                    user[k] = _safe_merge(ukv, v, filter_comments, merge_lists)
             else:
                 user[k] = copy.deepcopy(v)
+    elif merge_lists is not None:
+        if merge_lists == "extend":
+            if isinstance(user, list) and isinstance(default, list):
+                for idx, value in enumerate(default):
+                    user.insert(idx, copy.deepcopy(value))
+        elif merge_lists == "prepend":
+            if isinstance(user, list) and isinstance(default, list):
+                user.extend(copy.deepcopy(default))
+        else:
+            raise ValueError(
+                "unknown strategy for merge_lists: %s" % (merge_lists,))
     _filter_deletions(user)
     return user
 
 
-def _safe_merge(user, default, filter_comments, extend_lists):
+def _safe_merge(user, default, filter_comments, merge_lists):
     """Recursion helper for :func:`safe_merge`
 
     """
@@ -523,9 +564,20 @@
                     # do not copy
                     del user[k]
                 else:
-                    user[k] = _safe_merge(ukv, v, filter_comments, extend_lists)
+                    user[k] = _safe_merge(ukv, v, filter_comments, merge_lists)
             else:
                 user[k] = copy.deepcopy(v)
+    elif merge_lists is not None:
+        if merge_lists == "extend":
+            if isinstance(user, list) and isinstance(default, list):
+                for idx, value in enumerate(default):
+                    user.insert(idx, copy.deepcopy(value))
+        elif merge_lists == "prepend":
+            if isinstance(user, list) and isinstance(default, list):
+                user.extend(copy.deepcopy(default))
+        else:
+            raise ValueError(
+                "unknown strategy for merge_lists: %s" % (merge_lists,))
     return user
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/data/conf10_list_extend.yml	Sun Oct 29 17:15:41 2023 +0100
@@ -0,0 +1,10 @@
+# -*- coding: utf-8; mode: yaml; indent-tabs-mode: nil; -*-
+%YAML 1.1
+---
+
+tree1:
+  tree2:
+    key8:
+      - val4
+      - val5
+      - val6
--- a/tests/test.py	Sun Oct 29 17:13:32 2023 +0100
+++ b/tests/test.py	Sun Oct 29 17:15:41 2023 +0100
@@ -1001,6 +1001,46 @@
         self.assertEqual(1, cfg.getvar_s(u"expand-me"))
         self.assertEqual(2, cfg.getvar_s(u"expand-me-2"))
 
+    def test54_list_merge_replaces_by_default(self):
+        cfg = self._load(os.path.join(TESTDATADIR, "conf10.toml"),
+                         os.path.join(TESTDATADIR, "conf10.yml"))
+        self.assertEqual([u"val1", u"val2", u"in the root namespace"],
+                         cfg.getvarl_s(u"tree1", u"tree2", u"key8"))
+
+    def test55_list_merge_extend(self):
+        cfg = self._load(os.path.join(TESTDATADIR, "conf10.toml"),
+                         os.path.join(TESTDATADIR, "conf10.yml"),
+                         merge_lists="extend")
+        self.assertEqual([u"val1", u"val2", u"in the root namespace",
+                          u"val1", u"val2", u"in the root namespace"],
+                         cfg.getvarl_s(u"tree1", u"tree2", u"key8"))
+
+    def test56_list_merge_wrong_param(self):
+        try:
+            self._load(os.path.join(TESTDATADIR, "conf10.toml"),
+                       os.path.join(TESTDATADIR, "conf10.yml"),
+                       merge_lists="__unknown-merge-strategy__")
+        except ValueError:
+            pass
+        else:
+            self.fail("Should have thrown a `ValueError' exception")
+
+    def test57_list_merge_extend(self):
+        cfg = self._load(os.path.join(TESTDATADIR, "conf10.toml"),
+                         os.path.join(TESTDATADIR, "conf10_list_extend.yml"),
+                         merge_lists="extend")
+        self.assertEqual([u"val1", u"val2", u"in the root namespace",
+                          u"val4", u"val5", u"val6"],
+                         cfg.getvarl_s(u"tree1", u"tree2", u"key8"))
+
+    def test58_list_merge_prepend(self):
+        cfg = self._load(os.path.join(TESTDATADIR, "conf10.toml"),
+                         os.path.join(TESTDATADIR, "conf10_list_extend.yml"),
+                         merge_lists="prepend")
+        self.assertEqual([u"val4", u"val5", u"val6",
+                          u"val1", u"val2", u"in the root namespace",],
+                         cfg.getvarl_s(u"tree1", u"tree2", u"key8"))
+
 
 class T02LoadAndMerge(_T02MixinLoadAndMerge, unittest.TestCase):