Mercurial > hgrepos > Python > libs > ConfigMix
changeset 276:af371f9c016d
Allow deletion of key-value pairs when merging is done.
When encountering the "{{::DEL::}}" special value the corresponding key-value
pair is deleted.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Sat, 03 Oct 2020 17:11:41 +0200 |
| parents | e2fd8fea1a4c |
| children | 0c330cc6135c |
| files | CHANGES.txt configmix/__init__.py docs/introduction.rst tests/data/conf20.yml tests/data/conf21.yml tests/data/delete-in-dict.yml tests/test.py |
| diffstat | 7 files changed, 127 insertions(+), 18 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.txt Sat Oct 03 15:52:30 2020 +0200 +++ b/CHANGES.txt Sat Oct 03 17:11:41 2020 +0200 @@ -13,6 +13,15 @@ -------------- .. changelog:: + :version: n/a + :released: n/a + + .. change:: + :tags: feature + + Allow the deletion of key-value pairs while merging configurations. + +.. changelog:: :version: 0.10 :released: 2020-09-10
--- a/configmix/__init__.py Sat Oct 03 15:52:30 2020 +0200 +++ b/configmix/__init__.py Sat Oct 03 17:11:41 2020 +0200 @@ -13,7 +13,7 @@ from __future__ import division, print_function, absolute_import -__version__ = "0.10.1.dev1" +__version__ = "0.11.0.dev1" __revision__ = "|VCSRevision|" __date__ = "|VCSJustDate|" @@ -50,6 +50,12 @@ """ +DEL_VALUE = u("{{::DEL::}}") +"""Value for configuration items to signal that the corresponding +key-value is to be deleted when configurations are merged + +""" + def load(*files, **kwargs): """Load the given configuration files, merge them in the given order @@ -418,12 +424,17 @@ The configuration in `default` will be changed **inplace** when filtering out comments (which is the default). + If a value in `user` is equal to :data:`.DEL_VALUE` + (``{{::DEL::}}``) the corresponding key will be deleted from the + merged output. + From http://stackoverflow.com/questions/823196/yaml-merge-in-python """ if user is None: if filter_comments: _filter_comments(default) + _filter_deletions(default) return default if filter_comments: _filter_comments(user) @@ -431,10 +442,15 @@ for k, v in default.items(): if filter_comments and _is_comment(k): continue - if k not in user: + if k in user: + if user[k] == DEL_VALUE: + # do not copy + del user[k] + else: + user[k] = _merge(user[k], v, filter_comments) + else: user[k] = v - else: - user[k] = _merge(user[k], v, filter_comments) + _filter_deletions(user) return user @@ -446,10 +462,14 @@ for k, v in default.items(): if filter_comments and _is_comment(k): continue - if k not in user: + if k in user: + if user[k] == DEL_VALUE: + # do not copy + del user[k] + else: + user[k] = _merge(user[k], v, filter_comments) + else: user[k] = v - else: - user[k] = _merge(user[k], v, filter_comments) return user @@ -466,6 +486,7 @@ if user is None: if filter_comments: _filter_comments(default) + _filter_deletions(default) return copy.deepcopy(default) user = copy.deepcopy(user) if filter_comments: @@ -474,10 +495,15 @@ for k, v in default.items(): if filter_comments and _is_comment(k): continue - if k not in user: + if k in user: + if user[k] == DEL_VALUE: + # do not copy + del user[k] + else: + user[k] = _safe_merge(user[k], v, filter_comments) + else: user[k] = copy.deepcopy(v) - else: - user[k] = _safe_merge(user[k], v, filter_comments) + _filter_deletions(user) return user @@ -489,10 +515,14 @@ for k, v in default.items(): if filter_comments and _is_comment(k): continue - if k not in user: + if k in user: + if user[k] == DEL_VALUE: + # do not copy + del user[k] + else: + user[k] = _safe_merge(user[k], v, filter_comments) + else: user[k] = copy.deepcopy(v) - else: - user[k] = _safe_merge(user[k], v, filter_comments) return user @@ -525,6 +555,23 @@ return False +def _filter_deletions(d): + """Recursively filter deletions in the dict `d`. + + Deletions have values that equal :data:`.DEL_VALUE`. + + """ + if not isinstance(d, dict): + return + # use a copy of the items because we change `d` while iterating + for k, v in list(d.items()): + if v == DEL_VALUE: + del d[k] + else: + if isinstance(d[k], dict): + _filter_deletions(d[k]) + + # # Init loader defaults: mode->loader and extension->mode #
--- a/docs/introduction.rst Sat Oct 03 15:52:30 2020 +0200 +++ b/docs/introduction.rst Sat Oct 03 17:11:41 2020 +0200 @@ -227,6 +227,15 @@ ``[namespace:]variable``. +.. _merging-deletions: + +Deletions +--------- + +By using the special value ``{{::DEL::}}`` the corresponding key-value +pair is deleted when merging is done. + + .. _comments: Comments
--- a/tests/data/conf20.yml Sat Oct 03 15:52:30 2020 +0200 +++ b/tests/data/conf20.yml Sat Oct 03 17:11:41 2020 +0200 @@ -100,3 +100,9 @@ - 3 Str: a string + + +to-be-deleted: 'a value' +to-be-deleted-but-reassigned: 'another value' +# not touched in later config: should be cleaned up +not-deleted: '{{::DEL::}}'
--- a/tests/data/conf21.yml Sat Oct 03 15:52:30 2020 +0200 +++ b/tests/data/conf21.yml Sat Oct 03 17:11:41 2020 +0200 @@ -26,3 +26,7 @@ - 0 - 1 - 2 + + +to-be-deleted: '{{::DEL::}}' +to-be-deleted-but-reassigned: '{{::DEL::}}'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/data/delete-in-dict.yml Sat Oct 03 17:11:41 2020 +0200 @@ -0,0 +1,16 @@ +# -*- coding: utf-8; mode: yaml; indent-tabs-mode: nil; -*- +# +# Part of deletion check +# +%YAML 1.1 +--- + +db: + user: + name: '{{::DEL::}}' + +test: + Str: '{{::DEL::}}' + + +to-be-deleted-but-reassigned: 'the last value'
--- a/tests/test.py Sat Oct 03 15:52:30 2020 +0200 +++ b/tests/test.py Sat Oct 03 17:11:41 2020 +0200 @@ -308,18 +308,36 @@ _check(cfg) + def test07_deletions(self): + cfg = self._load(os.path.join(TESTDATADIR, "conf20.yml"), + os.path.join(TESTDATADIR, "conf21.yml"), + os.path.join(TESTDATADIR, "conf22.ini"), + os.path.join(TESTDATADIR, "conf23.json"), + os.path.join(TESTDATADIR, "conf24.toml"), + os.path.join(TESTDATADIR, "delete-in-dict.yml")) + # automatic clean-up + self.assertRaises(KeyError, cfg.getvar_s, "not-deleted") + # explicit deletion + self.assertRaises(KeyError, cfg.getvar_s, "to-be-deleted") + self.assertRaises(KeyError, cfg.getvar_s, "db.user.name") + self.assertEqual("the-database-password-2",cfg.getvar_s("db.user.pwd")) + self.assertRaises(KeyError, cfg.getvar_s, "test.Str") + self.assertEqual("not a list any more", cfg.getvar_s("test.List")) + self.assertEqual("the last value", + cfg.getvar_s("to-be-deleted-but-reassigned")) + class T02LoadAndMerge(_T02MixinLoadAndMerge, unittest.TestCase): def setUp(self): self._load = configmix.load - def test07_identity(self): + def test08_identity(self): cfg = configmix.ini.load(os.path.join(TESTDATADIR, "conf1.ini")) cfg2 = configmix.merge(cfg, None) self.assertEqual(id(cfg), id(cfg2)) - def test08_identity(self): + def test09_identity(self): cfg = configmix.ini.load(os.path.join(TESTDATADIR, "conf1.ini")) cfg2 = configmix.merge(cfg, {}) self.assertEqual(id(cfg), id(cfg2)) @@ -330,12 +348,12 @@ def setUp(self): self._load = configmix.safe_load - def test07_deepcopy(self): + def test08_deepcopy(self): cfg = configmix.ini.load(os.path.join(TESTDATADIR, "conf1.ini")) cfg2 = configmix.safe_merge(cfg, None) self.assertNotEqual(id(cfg), id(cfg2)) - def test08_deepcopy(self): + def test09_deepcopy(self): cfg = configmix.ini.load(os.path.join(TESTDATADIR, "conf1.ini")) cfg2 = configmix.safe_merge(cfg, {}) self.assertNotEqual(id(cfg), id(cfg2)) @@ -458,7 +476,7 @@ def test04_expand_intint2str_ini(self): cfg = configmix.load(os.path.join(TESTDATADIR, "conf1.ini")) - self.assertEqual("22", cfg.getvar_s("key103")) + self.assertEqual("22", cfg.getvar_s("key103")) if __name__ == "__main__":
