Merge pull request #6 from edaniszewski/dict-merge
fix bug with how component configs were joined
This commit is contained in:
commit
f0f7f0591b
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', [
|
||||
(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue