Skip to content

Commit

Permalink
Implemented a better way to override configuration values
Browse files Browse the repository at this point in the history
  • Loading branch information
agronholm committed Sep 30, 2015
1 parent d9d30d3 commit ba0e709
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 4 deletions.
4 changes: 2 additions & 2 deletions asphalt/core/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict, Any, Union
import asyncio

from .util import PluginContainer
from .util import PluginContainer, merge_config
from .context import Context

__all__ = 'Component', 'ContainerComponent', 'component_types'
Expand Down Expand Up @@ -60,7 +60,7 @@ def add_component(self, alias: str, type: Union[str, type]=None, **kwargs):
raise ValueError('there is already a child component named "{}"'.format(alias))

# Allow the external configuration to override the constructor arguments
kwargs.update(self.component_config.get(alias, {}))
kwargs = merge_config(kwargs, self.component_config.get(alias, {}))

component = component_types.create_object(type or alias, **kwargs)
self.child_components[alias] = component
Expand Down
32 changes: 31 additions & 1 deletion asphalt/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from pkg_resources import EntryPoint, iter_entry_points

__all__ = 'resolve_reference', 'qualified_name', 'blocking', 'asynchronous', 'PluginContainer'
__all__ = ('resolve_reference', 'qualified_name', 'merge_config', 'blocking', 'asynchronous',
'PluginContainer')

event_loop = event_loop_thread_id = None

Expand Down Expand Up @@ -57,6 +58,35 @@ def qualified_name(obj) -> str:
return qualname if module in ('typing', 'builtins') else '{}.{}'.format(module, qualname)


def merge_config(original: dict, overrides: dict) -> dict:
"""
Returns a copy of the ``original`` configuration dictionary, with overrides from ``overrides``
applied. This similar to what :meth:`dict.update` does, but when a dictionary is about to be
replaced with another dictionary, it instead merges the contents.
If a key in ``overrides`` is a dotted path (ie. ``foo.bar.baz: value``), it is assumed to be a
shorthand for ``foo: {bar: {baz: value}}``.
:param original: a configuration dictionary
:param overrides: a dictionary containing overriding values to the configuration
:return: the merge result
"""

copied = original.copy()
for key, value in overrides.items():
if '.' in key:
key, rest = key.split('.', 1)
value = {rest: value}

orig_value = copied.get(key)
if isinstance(orig_value, dict) and isinstance(value, dict):
copied[key] = merge_config(orig_value, value)
else:
copied[key] = value

return copied


def blocking(func: Callable[..., Any]):
"""
Returns a wrapper that guarantees that the target callable will be run in a thread other than
Expand Down
13 changes: 12 additions & 1 deletion tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pytest

from asphalt.core.util import (
resolve_reference, qualified_name, blocking, asynchronous, PluginContainer)
resolve_reference, qualified_name, blocking, asynchronous, PluginContainer, merge_config)


class BaseDummyPlugin:
Expand Down Expand Up @@ -38,6 +38,17 @@ def test_resolve_reference_error(inputval, error_type, error_text):
assert str(exc.value) == error_text


@pytest.mark.parametrize('overrides', [
{'a': 2, 'foo': 6, 'b': {'x': 5, 'z': {'r': 'bar', 's': [6, 7]}}},
{'a': 2, 'foo': 6, 'b.x': 5, 'b.z': {'r': 'bar', 's': [6, 7]}},
{'a': 2, 'foo': 6, 'b.x': 5, 'b.z.r': 'bar', 'b.z.s': [6, 7]}
], ids=['nested_dicts', 'part_nested', 'dotted_paths'])
def test_merge_config(overrides):
original = {'a': 1, 'b': {'x': 2, 'y': 3, 'z': {'r': [1, 2], 's': [3, 4]}}}
expected = {'a': 2, 'foo': 6, 'b': {'x': 5, 'y': 3, 'z': {'r': 'bar', 's': [6, 7]}}}
assert merge_config(original, overrides) == expected


@pytest.mark.parametrize('inputval, expected', [
(qualified_name, 'asphalt.core.util.qualified_name'),
(asyncio.Event(), 'asyncio.locks.Event'),
Expand Down

0 comments on commit ba0e709

Please sign in to comment.