diff --git a/bison/__version__.py b/bison/__version__.py index c2e3d74..7228c96 100644 --- a/bison/__version__.py +++ b/bison/__version__.py @@ -3,7 +3,7 @@ __title__ = 'bison' __description__ = 'Python application configuration' __url__ = 'https://github.com/edaniszewski/bison' -__version__ = '0.0.4' +__version__ = '0.0.5' __author__ = 'Erick Daniszewski' __author_email__ = 'edaniszewski@gmail.com' __license__ = 'MIT' diff --git a/bison/bison.py b/bison/bison.py index f1b30f9..445f61e 100644 --- a/bison/bison.py +++ b/bison/bison.py @@ -86,10 +86,10 @@ class Bison(object): """ if self._full_config is None: self._full_config = DotDict() - self._full_config.update(self._default) - self._full_config.update(self._config) - self._full_config.update(self._environment) - self._full_config.update(self._override) + self._full_config.merge(self._default) + self._full_config.merge(self._config) + self._full_config.merge(self._environment) + self._full_config.merge(self._override) return self._full_config def get(self, key, default=None): diff --git a/bison/utils.py b/bison/utils.py index f2065b9..ac706f5 100644 --- a/bison/utils.py +++ b/bison/utils.py @@ -6,6 +6,8 @@ bison.utils Utilities for `bison`. """ +import collections + def build_dot_value(key, value): """Build new dictionaries based off of the dot notation key. @@ -167,3 +169,69 @@ class DotDict(dict): raise KeyError( 'Subkey "{}" in "{}" invalid for deletion'.format(k, key) ) + + def merge(self, source): + """Merge the dictionary with the values from another dictionary + (or other DotDict). + + This is preferable to using `update` in some cases. Merging will recursively + update a dictionary, while updating will just overwrite. As an example, if + we have the DotDict with the values + + >>> d = DotDict({ + ... 'foo': { + ... 'bar': True + ... } + ... }) + >>> d + {'foo': {'bar': True}} + + Then for update, we would get: + + >>> d.update({'foo': {'baz': False}}) + >>> d + {'foo': {'baz': False}} + + For a merge, we would get: + + >>> d.merge({'foo': {'baz': False}}) + >>> d + {'foo': {'bar': True, 'baz': False}} + + So, an `update` will replace the specified dictionary, but a `merge` will + combine the values. + + Args: + source: The dict/iterable which will be used to update the DotDict. + """ + _merge(self, source) + + +def _merge(d, u): + """Merge two dictionaries (or DotDicts) together. + + Args: + d: The dictionary/DotDict to merge into. + u: The source of the data to merge. + """ + for k, v in u.items(): + # if we have a mapping, recursively merge the values + if isinstance(v, collections.Mapping): + d[k] = _merge(d.get(k, {}), v) + + # if d (the dict to merge into) is a dict, just add the + # value to the dict. + elif isinstance(d, collections.MutableMapping): + d[k] = v + + # otherwise if d (the dict to merge into) is not a dict (e.g. when + # recursing into it, `d.get(k, {})` may not be a dict), then do what + # `update` does and prefer the new value. + # + # this means that something like `{'foo': 1}` when updated with + # `{'foo': {'bar': 1}}` would have the original value (`1`) overwritten + # and would become: `{'foo': {'bar': 1}}` + else: + d = {k: v} + + return d diff --git a/tests/test_bison.py b/tests/test_bison.py index 5fe50f5..0d49351 100644 --- a/tests/test_bison.py +++ b/tests/test_bison.py @@ -111,6 +111,118 @@ class TestBison: } } + def test_set_multiple_nested_2(self): + """Set overrides for multiple nested values when some already exist.""" + b = bison.Bison() + assert len(b._override) == 0 + assert len(b.config) == 0 + + # set the override config config to something to begin + b._override = bison.DotDict({ + 'foo': 'bar', + 'bar': { + 'bat': { + 'a': 'test' + }, + 'bird': { + 'b': 'test' + } + } + }) + + b.set('foo', 'baz') + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': 'test' + }, + 'bird': { + 'b': 'test' + } + } + } + + b.set('bar.bat.a', None) + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': None + }, + 'bird': { + 'b': 'test' + } + } + } + + b.set('bar.bird', 'warbler') + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': None + }, + 'bird': 'warbler' + } + } + + def test_set_multiple_nested_3(self): + """Set overrides for multiple nested values when some already exist.""" + b = bison.Bison() + assert len(b._override) == 0 + assert len(b.config) == 0 + + # set a non override config config to something to begin + b._config = bison.DotDict({ + 'foo': 'bar', + 'bar': { + 'bat': { + 'a': 'test' + }, + 'bird': { + 'b': 'test' + } + } + }) + + b.set('foo', 'baz') + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': 'test' + }, + 'bird': { + 'b': 'test' + } + } + } + + b.set('bar.bat.a', None) + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': None + }, + 'bird': { + 'b': 'test' + } + } + } + + b.set('bar.bird', 'warbler') + assert b.config == { + 'foo': 'baz', + 'bar': { + 'bat': { + 'a': None + }, + 'bird': 'warbler' + } + } + @pytest.mark.parametrize( 'paths', [ (), diff --git a/tests/test_utils.py b/tests/test_utils.py index fdd3fde..ae32a9d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -391,3 +391,107 @@ class TestDotDict: } }) assert (key in dd) is expected + + @pytest.mark.parametrize( + 'source,expected', [ + ({}, {'foo': 'bar', 'bar': {'baz': {'key': 'value'}}}), + ({'foo': 'test'}, {'foo': 'test', 'bar': {'baz': {'key': 'value'}}}), + ({'abc': '123'}, {'foo': 'bar', 'abc': '123', 'bar': {'baz': {'key': 'value'}}}), + ({'bar': 'test'}, {'foo': 'bar', 'bar': 'test'}), + ({'bar': {'test': 'value'}}, {'foo': 'bar', 'bar': {'test': 'value'}}), + ({'test': 123}, {'foo': 'bar', 'test': 123, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': 123}, {'foo': 123, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': 123}, {'foo': 'bar', 'bar': 123}), + ({'test': [1, 2, 3]}, {'foo': 'bar', 'test': [1, 2, 3], 'bar': {'baz': {'key': 'value'}}}), + ({'foo': [1, 2, 3]}, {'foo': [1, 2, 3], 'bar': {'baz': {'key': 'value'}}}), + ({'bar': [1, 2, 3]}, {'foo': 'bar', 'bar': [1, 2, 3]}), + ({'test': {'a': 1}}, {'foo': 'bar', 'test': {'a': 1}, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': {'a': 1}}, {'foo': {'a': 1}, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': {'a': 1}}, {'foo': 'bar', 'bar': {'a': 1}}), + ({'test': False}, {'foo': 'bar', 'test': False, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': False}, {'foo': False, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': False}, {'foo': 'bar', 'bar': False}), + ({'test': None}, {'foo': 'bar', 'test': None, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': None}, {'foo': None, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': None}, {'foo': 'bar', 'bar': None}), + ] + ) + def test_update(self, source, expected): + """Update the DotDict""" + dd = utils.DotDict({ + 'foo': 'bar', + 'bar': { + 'baz': { + 'key': 'value' + } + } + }) + dd.update(source) + assert dd == expected + + @pytest.mark.parametrize( + 'source,expected', [ + ({}, {'foo': 'bar', 'bar': {'baz': {'key': 'value'}}}), + ({'foo': 'test'}, {'foo': 'test', 'bar': {'baz': {'key': 'value'}}}), + ({'abc': '123'}, {'foo': 'bar', 'abc': '123', 'bar': {'baz': {'key': 'value'}}}), + ({'bar': 'test'}, {'foo': 'bar', 'bar': 'test'}), + ({'bar': {'test': 'value'}}, {'foo': 'bar', 'bar': {'baz': {'key': 'value'}, 'test': 'value'}}), + ({'test': 123}, {'foo': 'bar', 'test': 123, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': 123}, {'foo': 123, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': 123}, {'foo': 'bar', 'bar': 123}), + ({'test': [1, 2, 3]}, {'foo': 'bar', 'test': [1, 2, 3], 'bar': {'baz': {'key': 'value'}}}), + ({'foo': [1, 2, 3]}, {'foo': [1, 2, 3], 'bar': {'baz': {'key': 'value'}}}), + ({'bar': [1, 2, 3]}, {'foo': 'bar', 'bar': [1, 2, 3]}), + ({'test': {'a': 1}}, {'foo': 'bar', 'test': {'a': 1}, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': {'a': 1}}, {'foo': {'a': 1}, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': {'a': 1}}, {'foo': 'bar', 'bar': {'a': 1, 'baz': {'key': 'value'}}}), + ({'test': False}, {'foo': 'bar', 'test': False, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': False}, {'foo': False, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': False}, {'foo': 'bar', 'bar': False}), + ({'test': None}, {'foo': 'bar', 'test': None, 'bar': {'baz': {'key': 'value'}}}), + ({'foo': None}, {'foo': None, 'bar': {'baz': {'key': 'value'}}}), + ({'bar': None}, {'foo': 'bar', 'bar': None}), + ] + ) + def test_merge(self, source, expected): + """Update the DotDict""" + dd = utils.DotDict({ + 'foo': 'bar', + 'bar': { + 'baz': { + 'key': 'value' + } + } + }) + dd.merge(source) + assert dd == expected + + def test_deep_merge(self): + """Test merging through many nested dicts.""" + dd = utils.DotDict({ + 'foo': { + 'bar': { + 'baz': { + 'bison': True + } + } + } + }) + dd.merge({'foo': { + 'bar': { + 'baz': { + 'birds': False + } + } + }}) + + assert dd == { + 'foo': { + 'bar': { + 'baz': { + 'bison': True, + 'birds': False + } + } + } + }