fix bug with how component configs were joined
This commit is contained in:
parent
bc7adb48de
commit
d57e78fcc8
|
@ -3,7 +3,7 @@
|
||||||
__title__ = 'bison'
|
__title__ = 'bison'
|
||||||
__description__ = 'Python application configuration'
|
__description__ = 'Python application configuration'
|
||||||
__url__ = 'https://github.com/edaniszewski/bison'
|
__url__ = 'https://github.com/edaniszewski/bison'
|
||||||
__version__ = '0.0.4'
|
__version__ = '0.0.5'
|
||||||
__author__ = 'Erick Daniszewski'
|
__author__ = 'Erick Daniszewski'
|
||||||
__author_email__ = 'edaniszewski@gmail.com'
|
__author_email__ = 'edaniszewski@gmail.com'
|
||||||
__license__ = 'MIT'
|
__license__ = 'MIT'
|
||||||
|
|
|
@ -86,10 +86,10 @@ class Bison(object):
|
||||||
"""
|
"""
|
||||||
if self._full_config is None:
|
if self._full_config is None:
|
||||||
self._full_config = DotDict()
|
self._full_config = DotDict()
|
||||||
self._full_config.update(self._default)
|
self._full_config.merge(self._default)
|
||||||
self._full_config.update(self._config)
|
self._full_config.merge(self._config)
|
||||||
self._full_config.update(self._environment)
|
self._full_config.merge(self._environment)
|
||||||
self._full_config.update(self._override)
|
self._full_config.merge(self._override)
|
||||||
return self._full_config
|
return self._full_config
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
|
|
|
@ -6,6 +6,8 @@ bison.utils
|
||||||
Utilities for `bison`.
|
Utilities for `bison`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
def build_dot_value(key, value):
|
def build_dot_value(key, value):
|
||||||
"""Build new dictionaries based off of the dot notation key.
|
"""Build new dictionaries based off of the dot notation key.
|
||||||
|
@ -167,3 +169,69 @@ class DotDict(dict):
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
'Subkey "{}" in "{}" invalid for deletion'.format(k, key)
|
'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(
|
@pytest.mark.parametrize(
|
||||||
'paths', [
|
'paths', [
|
||||||
(),
|
(),
|
||||||
|
|
|
@ -391,3 +391,107 @@ class TestDotDict:
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
assert (key in dd) is expected
|
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