Skip to content

Commit

Permalink
#13 added support for configuration update
Browse files Browse the repository at this point in the history
  • Loading branch information
Iwan Silvan Bolzern committed Aug 12, 2020
1 parent 7e904ea commit 1bf8ca4
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 10 deletions.
64 changes: 63 additions & 1 deletion confme/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""ConfigurationMadeEasy package exports"""
import argparse
from typing import Any, Tuple, List

from pydantic import BaseSettings

from confme import source_backend
from confme.utils.base_exception import ConfmeException
from confme.utils.typing import get_schema
from confme.utils.dict_util import flatten, InfiniteDict, recursive_update


def argument_overwrite(config_cls):
# extract possible parameters
config_dict = get_schema(config_cls)
parameters = flatten(config_dict)
parameters, _ = flatten(config_dict)

# get arguments from command line
parser = argparse.ArgumentParser()
Expand Down Expand Up @@ -45,3 +47,63 @@ def load(cls, path: str) -> 'BaseConfig':
config_content = recursive_update(config_content, argument_overwrite(cls))

return cls.parse_obj(config_content)

def update_by_str(self, path: str, value: Any):
"""Given a path string separated by dots (.) this method allows to update nested configuration objects.
CAVEAT: For this update no type check is applied!
e.g. given this configuration
```
class DatabaseConfig(BaseConfig):
host: str
port: int
user: str
class MyConfig(BaseConfig):
name: int
database: DatabaseConfig
config = MyConfig.load('test.yaml')
config.update_by_str('database.host', 'my new host')
:param path: dot (.) separated string which value should be updated
:param value: update value
"""
path = path.split('.')
current = self
for i, segment in enumerate(path):
if not hasattr(current, segment):
raise ConfmeException(f'{segment} not found in path {".".join(path[:i])}!')

# for the last item we don't want to assign it to the current element because we want to assign the given
# value instead
if i + 1 < len(path):
current = getattr(current, segment)
else:
setattr(current, segment, value)

def get_flat_repr(self) -> List[Tuple[str, Any]]:
"""Returns a flat representation of your configuration structure (tree).
e.g. given this configuration
```
class DatabaseConfig(BaseConfig):
host: str
port: int
user: str
class MyConfig(BaseConfig):
name: int
database: DatabaseConfig
config = MyConfig.load('test.yaml')
config.get_flat_repr()
...
[('name', 'test1'),
('database.host', 'localhost'),
('database.port', '5678'),
('database.user', 'db_user')]
:return: list of configuration parameter key and it's corresponding value
"""
keys, values = flatten(self.dict())
return list(zip(keys, values))

6 changes: 6 additions & 0 deletions confme/utils/base_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@


class ConfmeException(Exception):

def __init__(self, msg: str):
super(ConfmeException, self).__init__(msg)
12 changes: 8 additions & 4 deletions confme/utils/dict_util.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import collections.abc
from collections import defaultdict
from typing import Any, List, MutableMapping, Dict
from typing import Any, List, MutableMapping, Dict, Tuple


def flatten(d: Dict, parent_key='', sep='.'):
def flatten(d: Dict, parent_key='', sep='.') -> Tuple[List, List]:
items = []
values = []
for k, v in d.items():
new_key = parent_key + sep + k if parent_key else k
if isinstance(v, MutableMapping):
items.extend(flatten(v, new_key, sep=sep))
inner_items, inner_values = flatten(v, new_key, sep=sep)
items.extend(inner_items)
values.extend(inner_values)
else:
items.append(new_key)
return items
values.append(v)
return items, values


def recursive_update(d, u):
Expand Down
46 changes: 41 additions & 5 deletions tests/unit/test_full_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import pytest

from confme import ConfmeException
from tests.unit.config_model import RootConfig, AnyEnum, FlatConfig


Expand All @@ -13,7 +14,7 @@ def config_yaml(tmp_path: str):
config_content = 'rootValue: 1\n' \
'rangeValue: 5\n' \
'childNode:\n' \
' testStr: "Das ist ein test"\n' \
' testStr: "This is a test"\n' \
' testInt: 42\n' \
' testFloat: 42.42\n' \
' anyEnum: value2'
Expand All @@ -28,7 +29,7 @@ def config_yaml(tmp_path: str):
@pytest.fixture
def flat_config_yaml(tmp_path: str):
config_content = 'oneValue: 1\n' \
'twoValue: "Das ist ein test"'
'twoValue: "This is a test"'

config_path = path.join(tmp_path, f'{uuid.uuid4()}.yaml')
with open(config_path, 'w') as config_file:
Expand All @@ -45,7 +46,7 @@ def test_load_config(config_yaml: str):

assert root_config.rootValue == 1
assert root_config.rangeValue == 5
assert root_config.childNode.testStr == 'Das ist ein test'
assert root_config.childNode.testStr == 'This is a test'
assert root_config.childNode.testInt == 42
assert root_config.childNode.testFloat == 42.42
assert root_config.childNode.testOptional is None
Expand All @@ -54,9 +55,44 @@ def test_load_config(config_yaml: str):


def test_load_flat_config(flat_config_yaml: str):

flat_config = FlatConfig.load(flat_config_yaml)
logging.info(f'Config loaded: {flat_config.dict()}')

assert flat_config.oneValue == 1
assert flat_config.twoValue == 'Das ist ein test'
assert flat_config.twoValue == 'This is a test'


def test_update_by_str(config_yaml: str):
os.environ['highSecure'] = 'superSecureSecret'

root_config = RootConfig.load(config_yaml)
logging.info(f'Config loaded: {root_config.dict()}')

root_config.update_by_str('rootValue', 10)
root_config.update_by_str('childNode.testStr', 'This str was changed')

assert root_config.rootValue == 10
assert root_config.childNode.testStr == 'This str was changed'

with pytest.raises(ConfmeException):
root_config.update_by_str('rootValue2', 10)

with pytest.raises(ConfmeException):
root_config.update_by_str('childNode2.testStr', 10)


def test_get_flat_rep(config_yaml: str):
os.environ['highSecure'] = 'superSecureSecret'

root_config = RootConfig.load(config_yaml)
logging.info(f'Config loaded: {root_config.dict()}')

flat_repr = root_config.get_flat_repr()
assert flat_repr[0] == ('rootValue', 1)
assert flat_repr[1] == ('rangeValue', 5)
assert flat_repr[2] == ('childNode.testStr', 'This is a test')
assert flat_repr[3] == ('childNode.testInt', 42)
assert flat_repr[4] == ('childNode.testFloat', 42.42)
assert flat_repr[5] == ('childNode.testOptional', None)
assert flat_repr[6] == ('childNode.password', os.environ['highSecure'])
assert flat_repr[7] == ('childNode.anyEnum', AnyEnum.V2)

0 comments on commit 1bf8ca4

Please sign in to comment.