Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feautre/generate example config #19

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions confme/core/base_config.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import io
import logging
import os
from pathlib import Path
from typing import Union, Any, List, Tuple, Callable, TypeVar, Dict

import yaml
from pydantic import BaseSettings
from tabulate import tabulate

from confme import source_backend
from confme.core.argument_overwrite import argument_overwrite
from confme.core.default_gen import Generator
from confme.core.env_overwrite import env_overwrite
from confme.utils.base_exception import ConfmeException
from confme.utils.dict_util import recursive_update, flatten
Expand Down Expand Up @@ -164,3 +167,18 @@ def log_config(self, print_fn: Callable[[str], None] = logging.info):
flat_config = self.get_flat_repr()
str_config = tabulate(flat_config, headers=['Key', 'Value'], tablefmt="github")
print_fn(str_config)

@classmethod
def generate_example(cls, print_fn: Callable[[str], None] = print):
# generate config
generator = Generator()
example = generator.generate(cls)

# convert to yaml
stream = io.StringIO()
yaml.dump(example, stream=stream)
stream.seek(0)

# print
print_fn(stream.read())

144 changes: 144 additions & 0 deletions confme/core/default_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from abc import abstractmethod
from collections import OrderedDict
from typing import List, Dict, Callable, Type

from pydantic import BaseSettings

from confme.utils.base_exception import ConfmeException


class NodeGenerator:

@abstractmethod
def is_applicable(self, node: Dict, definitions: Dict): pass

@abstractmethod
def generate(self, node: Dict, definitions: Dict, traverse: Callable): pass


class PrimitiveNodeGenerator(NodeGenerator):
PRIMITIVE_TYPES = {
'string': 'bla',
'integer': 42,
'number': 42.42
}

def is_applicable(self, node: Dict, definitions: Dict):
if 'type' in node and node['type'] \
in self.PRIMITIVE_TYPES.keys():
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
return self.PRIMITIVE_TYPES[node['type']]


class EnumNodeGenerator(NodeGenerator):

def is_applicable(self, node: Dict, definitions: Dict):
if 'enum' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
return node['enum'][0]


class ObjectNodeGenerator(NodeGenerator):
def is_applicable(self, node: Dict, definitions: Dict):
if 'type' in node and node['type'] == 'object' and 'properties' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
res = OrderedDict()
for k, p in node['properties'].items():
res[k] = traverse(p)
return res


class DictNodeGenerator(NodeGenerator):
def is_applicable(self, node: Dict, definitions: Dict):
if 'type' in node and node['type'] == 'object' and 'additionalProperties' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
value = traverse(node['additionalProperties'])
return OrderedDict(**{'value1': value, 'value2': value})


class ListNodeGenerator(NodeGenerator):
def is_applicable(self, node: Dict, definitions: Dict):
if 'type' in node and node['type'] == 'array':
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
element = traverse(node['items'])
return [element] * 3


class AllOffNodeGenerator(NodeGenerator):

def is_applicable(self, node: Dict, definitions: Dict):
if 'allOf' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
return traverse(node['allOf'][0])


class AnyOffNodeGenerator(NodeGenerator):

def is_applicable(self, node: Dict, definitions: Dict):
if 'anyOf' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
return traverse(node['anyOf'][0])


class RefNodeGenerator(NodeGenerator):
def is_applicable(self, node: Dict, definitions: Dict):
if '$ref' in node:
return True
return False

def generate(self, node: Dict, definitions: Dict, traverse: Callable):
ref = node['$ref'].split('/')[-1]
definition = definitions[ref]
return traverse(definition)


class Generator:

def __init__(self, generators: List[NodeGenerator] = None):
if generators is None:
generators = [RefNodeGenerator(),
AllOffNodeGenerator(),
ListNodeGenerator(),
ObjectNodeGenerator(),
DictNodeGenerator(),
PrimitiveNodeGenerator(),
EnumNodeGenerator()]

self._generators = generators
self._definitions = {}

def generate(self, config: Type[BaseSettings]) -> Dict:
return self._traverse(config.schema())

def _traverse(self, node: Dict):
self._extract_definitions(node)
for g in self._generators:
if g.is_applicable(node, self._definitions):
return g.generate(node, self._definitions, self._traverse)

raise ConfmeException(f'Could not find Generator to handle node: {node}')

def _extract_definitions(self, node: Dict):
if 'definitions' in node:
self._definitions.update(**node['definitions'])
4 changes: 3 additions & 1 deletion confme/core/global_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from typing import Dict, TypeVar

from confme import BaseConfig
from confme.utils.deprecation import deprecated_cls

T = TypeVar('T', bound='BaseConfig')


@deprecated_cls(comment='GlobalConfig will be removed in next major release! Please use BaseConfig instead, all '
'functionalities were transfered into it already.')
class GlobalConfig(BaseConfig):
_KEY_LOOKUP = ['env', 'environment', 'environ', 'stage']
_config_path: Path = None
Expand Down
38 changes: 38 additions & 0 deletions confme/utils/deprecation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import functools
import warnings


def deprecated(comment: str = ''):
return functools.partial(_deprecated_func, comment=comment)


def deprecated_cls(comment: str = ''):
return functools.partial(_deprecated_cls, comment=comment)


def _deprecated_func(func, comment: str):
"""This is a decorator which can be used to mark functions
as deprecated. It will result in a warning being emitted
when the function is used."""

@functools.wraps(func)
def new_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(f'Call to deprecated function {func.__name__}.\n'
f'Comment: {comment}',
category=DeprecationWarning,
stacklevel=2)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return func(*args, **kwargs)

return new_func


def _deprecated_cls(cls, comment: str):
warnings.simplefilter('always', DeprecationWarning) # turn off filter
warnings.warn(f'Call to deprecated class {cls.__name__}.\n'
f'Comment: {comment}',
category=DeprecationWarning,
stacklevel=2)
warnings.simplefilter('default', DeprecationWarning) # reset filter
return cls
27 changes: 27 additions & 0 deletions tests/unit/test_default_gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from typing import Dict

import pytest

from confme.core.default_gen import Generator
from tests.unit.config_model import RootConfig


@pytest.fixture
def expected_dict():
return {'rootValue': 42,
'rangeValue': 42,
'childNode': {
'testStr': 'bla',
'testInt': 42,
'testFloat': 42.42,
'testOptional': 42.42,
'password': 'bla',
'anyEnum': 'value1'
}}


def test_load_config_from_dict(expected_dict: Dict):
gen = Generator()
res = gen.generate(RootConfig)

assert res == expected_dict
23 changes: 23 additions & 0 deletions tests/unit/test_full_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,26 @@ def test_get_flat_rep(config_yaml: str):
assert flat_repr[5] == ('childNode.testOptional', None)
assert flat_repr[6] == ('childNode.password', os.environ['highSecure'])
assert flat_repr[7] == ('childNode.anyEnum', AnyEnum.V2)


def test_generate_example():
expected_output = 'rootValue: 42\n' \
'rangeValue: 42\n' \
'childNode:\n' \
' testStr: "bla"\n' \
' testInt: 42\n' \
' testFloat: 42.42\n' \
' testOptional: 42.42\n' \
' password: "bla"\n' \
' anyEnum: value1'
output = None
def _print(out: str):
nonlocal output
output = out

RootConfig.generate_example(_print)

assert output == expected_output