Skip to content

Commit

Permalink
Adds multiple merge in load/loads, implements tests and increments ve…
Browse files Browse the repository at this point in the history
…rsion to 0.1.0
  • Loading branch information
halfak committed Dec 17, 2015
1 parent 0bf60d1 commit ac8aec1
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 73 deletions.
6 changes: 3 additions & 3 deletions setup.py
Expand Up @@ -14,15 +14,15 @@ def requirements(fname):

setup(
name="yamlconf",
version="0.0.5", # Update in yamlconf/__init__.py too
version="0.1.0", # Update in yamlconf/__init__.py too
author="Aaron Halfaker",
author_email="ahalfaker@wikimedia.org",
description=("This library provides a means to read yaml configuration " +
"files and propagate default values in reasonable ways. " +
"Nothing complicated."),
license="MIT",
url="https://github.com/halfak/Python-Yaml-Configuration",
py_modules=['yamlconf'],
url="https://github.com/halfak/yamlconf",
packages=find_packages(),
long_description=read('README.rst'),
install_requires=['pyyaml'],
classifiers=[
Expand Down
70 changes: 0 additions & 70 deletions yamlconf.py

This file was deleted.

8 changes: 8 additions & 0 deletions yamlconf/__init__.py
@@ -0,0 +1,8 @@
from .import_module import import_module
from .load import load, loads
from .merge import merge
from .propagate_defaults import propagate_defaults

__all__ = [import_module, load, loads, merge, propagate_defaults]

__version__ = "0.1.0"
21 changes: 21 additions & 0 deletions yamlconf/import_module.py
@@ -0,0 +1,21 @@
import importlib


def import_module(path):
"""
Import a class or module from a path. E.g.
``import_class("difflib.SequenceMatcher")`` returns a reference to the
SequenceMatcher class.
"""
try:
parts = path.split(".")
module_path = ".".join(parts[:-1])
attribute_name = parts[-1]

module = importlib.import_module(module_path)

attribute = getattr(module, attribute_name)

return attribute
except AttributeError:
return importlib.import_module(path)
35 changes: 35 additions & 0 deletions yamlconf/load.py
@@ -0,0 +1,35 @@
import io

import yaml

from .merge import merge
from .propagate_defaults import propagate_defaults


def load(*files):
"""
Loads configuration from one or more files by merging right to left.
:Parameters:
*files : `file-like`
A YAML file to read.
:Returns:
`dict` : the configuration document
"""
doc = merge(*(yaml.load(f) for f in files))
return propagate_defaults(doc)


def loads(*strings):
"""
Loads configuration from one or more files by merging right to left.
:Parameters:
*strings : `str`
YAML strings to read.
:Returns:
`dict` : the configuration document
"""
return load(*(io.StringIO(s) for s in strings))
27 changes: 27 additions & 0 deletions yamlconf/merge.py
@@ -0,0 +1,27 @@

def merge(d, *dicts):
"""
Recursively merges dictionaries
"""

for d_update in dicts:
if not isinstance(d, dict):
raise TypeError("{0} is not a dict".format(d))

dict_merge_pair(d, d_update)

return d


def dict_merge_pair(d1, d2):
"""
Recursively merges values from d2 into d1.
"""
for key in d2:
if key in d1 and isinstance(d1[key], dict) and \
isinstance(d2[key], dict):
dict_merge_pair(d1[key], d2[key])
else:
d1[key] = d2[key]

return d1
22 changes: 22 additions & 0 deletions yamlconf/propagate_defaults.py
@@ -0,0 +1,22 @@
import copy

from .merge import dict_merge_pair


def propagate_defaults(config_doc):
"""
Propagate default values to sections of the doc.
"""
for group_name, group_doc in config_doc.items():
if isinstance(group_doc, dict):
defaults = group_doc.get('defaults', {})

for item_name, item_doc in group_doc.items():
if item_name == 'defaults':
continue
if isinstance(item_doc, dict):

group_doc[item_name] = \
dict_merge_pair(copy.deepcopy(defaults), item_doc)

return config_doc
Empty file added yamlconf/tests/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions yamlconf/tests/config1.yaml
@@ -0,0 +1,7 @@
hats:
red:
color: red
size: 10
bowler:
color: black
size: 25
3 changes: 3 additions & 0 deletions yamlconf/tests/config2.yaml
@@ -0,0 +1,3 @@
hats:
bowler:
size: 35
31 changes: 31 additions & 0 deletions yamlconf/tests/test_load.py
@@ -0,0 +1,31 @@
import os

from nose.tools import eq_

from ..load import load, loads

EXPECTED = {
'hats': {
'red': {
'color': "red",
'size': 10
},
'bowler': {
'color': "black",
'size': 35
}
}
}

PWD = os.path.dirname(os.path.realpath(__file__))


def test_load():

eq_(load(open(os.path.join(PWD, 'config1.yaml')),
open(os.path.join(PWD, 'config2.yaml'))),
EXPECTED)

eq_(loads(open(os.path.join(PWD, 'config1.yaml')).read(),
open(os.path.join(PWD, 'config2.yaml')).read()),
EXPECTED)
46 changes: 46 additions & 0 deletions yamlconf/tests/test_merge.py
@@ -0,0 +1,46 @@
from nose.tools import eq_

from ..merge import merge


def test_merge():
a = {
'foo': {
'bar': {
'foo': 5,
'bar': 7
}
},
'l': [1, 5, 'foo']
}
b = {
'foo': {
'bar': {
'foo': 6
}
},
'bar': 5,
's': "I'm a string",
'l': [1, 2, 3]
}
c = {
'foo': {
'bar': {
'foobar': 10
}
}
}
expected = {
'foo': {
'bar': {
'foo': 6,
'bar': 7,
'foobar': 10
}
},
'bar': 5,
's': "I'm a string",
'l': [1, 2, 3]
}
eq_(merge(a, b, c),
expected)
36 changes: 36 additions & 0 deletions yamlconf/tests/test_propagate_defaults.py
@@ -0,0 +1,36 @@
from nose.tools import eq_

from ..propagate_defaults import propagate_defaults


def test_propagate_defaults():

input = {
'foos': {
'defaults': {
'bar': 1,
'baz': 2
},
'1_foo': {},
'2_foo': {
'baz': 3
}
}
}
expected = {
'foos': {
'defaults': {
'bar': 1,
'baz': 2
},
'1_foo': {
'bar': 1,
'baz': 2
},
'2_foo': {
'bar': 1,
'baz': 3
}
}
}
eq_(propagate_defaults(input), expected)

0 comments on commit ac8aec1

Please sign in to comment.