Merge pull request #6 from edaniszewski/dict-merge

fix bug with how component configs were joined
This commit is contained in:
Erick Daniszewski 2018-03-16 09:14:31 -04:00 committed by GitHub
commit f0f7f0591b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 289 additions and 5 deletions

View File

@ -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'

View File

@ -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):

View File

@ -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

View File

@ -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', [
(),

View File

@ -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
}
}
}
}