aboutsummaryrefslogtreecommitdiff
path: root/configlib
diff options
context:
space:
mode:
Diffstat (limited to 'configlib')
-rw-r--r--configlib/__init__.py9
-rw-r--r--configlib/model.py85
-rw-r--r--configlib/model_impl.py58
-rw-r--r--configlib/util.py22
-rw-r--r--configlib/version.py15
5 files changed, 161 insertions, 28 deletions
diff --git a/configlib/__init__.py b/configlib/__init__.py
index c0f25aa..371062f 100644
--- a/configlib/__init__.py
+++ b/configlib/__init__.py
@@ -1,6 +1,11 @@
+"""
+An easy python config library.
+
+"""
+
from .model import Config, ConfigValueMissingException, InvalidConfigTypingException
from .model_impl import BaseConfig
-from .version import VersionInfo, version
+from .version import VersionInfo, VERSION
__all__ = ['ConfigValueMissingException', 'Config', 'InvalidConfigTypingException',
- 'BaseConfig', 'VersionInfo', 'version']
+ 'BaseConfig', 'VersionInfo', 'VERSION']
diff --git a/configlib/model.py b/configlib/model.py
index 6d79464..5f5c694 100644
--- a/configlib/model.py
+++ b/configlib/model.py
@@ -1,41 +1,100 @@
+"""
+Abstract models and classes for the config
+
+"""
+
import json
+import os
from abc import abstractmethod, ABC
-from typing import TypeVar, Type, Union, AnyStr, TextIO
+from typing import Type, Union, AnyStr, TextIO
-_T = TypeVar('_T', bound='Config')
+from configlib.util import snake_case
class InvalidConfigTypingException(Exception):
- pass
+ """
+ The typing found in the given class is missing arguments.
+ Example:
+
+ >>> import typing
+ >>> someting: typing.List[str, int]
+
+ is illegal since :class:`typing.List` only takes one argument.
+ """
class ConfigValueMissingException(Exception):
- pass
+ """
+ The given config file is missing an argument
+ """
class Config(ABC):
+ """
+ Base class for a Config. Do NOT extend this. use :class:`configlib.BaseConfig` instead.
+
+ """
@classmethod
@abstractmethod
def get_name(cls) -> str:
- pass
+ """
+ Get the name for a config
+
+ :return: the name
+ """
@classmethod
@abstractmethod
- def parse_dict(cls: Type[_T], data: dict) -> _T:
- pass
+ def parse_dict(cls: Type['Config'], data: dict) -> 'Config':
+ """
+ For the given data return the config instance.
+
+ :param data: the loaded data dict
+ :return: a loaded config
+ """
@classmethod
- def load(cls: Type[_T], file: Union[AnyStr, TextIO]) -> _T:
+ def load(cls: Type['Config'], file: Union[AnyStr, TextIO]) -> 'Config':
+ """
+ Load a specified config file
+
+ :param file: the file object or file path
+ :return: the parsed config according to :func:`.parse_dict`
+ """
if hasattr(file, 'read'):
return cls.loads(file.read())
- with open(file) as fp:
- return cls.load(fp)
+ with open(file) as file_pointer:
+ return cls.load(file_pointer)
@classmethod
- def loads(cls: Type[_T], text: str) -> _T:
+ def loads(cls: Type['Config'], text: str) -> 'Config':
+ """
+ Load data from text
+
+ :param text: the text data
+ :return: the parsed config
+ """
return cls.parse_dict(json.loads(text))
@classmethod
- def get_instance(cls: Type[_T]) -> _T:
- return cls
+ def get_instance(cls: Type['Config']) -> 'Config':
+ """
+ get a Config instance according to the matching environment variable
+
+ :return: the parsed config
+ """
+ name = os.environ.get(snake_case(cls.get_name()), '').strip()
+ return cls.get_instance_for_env(name)
+
+ @classmethod
+ def get_instance_for_env(cls: Type['Config'], env: str) -> 'Config':
+ """
+ get a Config instance for a given environment
+
+ :param env: the wanted environment
+ :return: the parsed config
+ """
+ if env:
+ env = '-' + env
+ return cls.load('config/' + snake_case(cls.get_name()) + env + '.json')
diff --git a/configlib/model_impl.py b/configlib/model_impl.py
index 4b7a866..dc8cde0 100644
--- a/configlib/model_impl.py
+++ b/configlib/model_impl.py
@@ -1,21 +1,37 @@
-from typing import TypeVar, Type, Dict, List
+"""
+Implementations for the modules of :module:`configlib.model`
+"""
+
+from typing import Type, Dict, List
from .model import Config, ConfigValueMissingException, InvalidConfigTypingException
from .util import snake_case
-_T = TypeVar('_T', bound=Config)
-
-_T0 = TypeVar('_T0')
+def parse_list_impl(cls: Type[object], data, path='') -> list:
+ """
+ parse a list and reinterpret the individual elements according to `cls`
-def parse_list_impl(cls: Type[_T0], data, path=''):
+ :param cls: the type of each list element
+ :param data: the actual list elements
+ :param path: the path inside the config. used for error reporting
+ :return: the list with transformed elements.
+ """
lis = []
for i, item in enumerate(data):
- list.append(parse_single_item(cls, item, path + '[' + str(i) + ']'))
+ lis.append(parse_single_item(cls, item, path + '[' + str(i) + ']'))
return lis
-def parse_obj_impl(cls: Type[_T0], data, path='Config') -> _T0:
+def parse_obj_impl(cls: Type[object], data, path='Config') -> object:
+ """
+ parse a dict into an object according to `cls`
+
+ :param cls: the type of the resulting object
+ :param data: the dict object
+ :param path: the path inside the config. used for error reporting
+ :return: the parsed object
+ """
obj = cls()
annotations: Dict[str, Type] = obj.__annotations__
for key, val_type in annotations.items():
@@ -26,14 +42,31 @@ def parse_obj_impl(cls: Type[_T0], data, path='Config') -> _T0:
return obj
-def parse_dict_impl(val_type: Type[_T0], val, path) -> _T0:
+def parse_dict_impl(val_type: Type[object], val, path) -> dict:
+ """
+ Parse a dict and reinterpret the values according to `val_type`
+
+ :param val_type: the type of the values of the dict
+ :param val: the actual dict to be reinterpreted
+ :param path: the path inside the config. used for error reporting
+ :return: the reinterpreted dict
+ """
dic = {}
for key, value in val.items():
dic[key] = parse_single_item(val_type, value, path + '[' + repr(key) + ']')
return dic
-def parse_single_item(val_type: Type[_T0], val, path) -> _T0:
+# noinspection PyUnresolvedReferences
+def parse_single_item(val_type: Type[object], val, path):
+ """
+ dynamically parse an item into a dict, list, object or a primitive depending on `val_type`
+
+ :param val_type: the type to be discussed
+ :param val: the value to be parsed
+ :param path: the path inside the config. used for error reporting
+ :return: the parsed something
+ """
if issubclass(val_type, (str, int, float)):
return val
if isinstance(val_type, List):
@@ -50,10 +83,15 @@ def parse_single_item(val_type: Type[_T0], val, path) -> _T0:
class BaseConfig(Config):
+ """
+ A :class:`Config` implementation using type hints for parsing.
+ """
+
@classmethod
def get_name(cls) -> str:
return snake_case(cls.__name__).upper()
@classmethod
- def parse_dict(cls: Type[_T], data: dict) -> _T:
+ def parse_dict(cls: Type['BaseConfig'], data: dict) -> 'BaseConfig':
+ # noinspection PyTypeChecker
return parse_obj_impl(cls, data)
diff --git a/configlib/util.py b/configlib/util.py
index 3c7618d..f325f25 100644
--- a/configlib/util.py
+++ b/configlib/util.py
@@ -1,8 +1,18 @@
+"""
+Utility methods. Should not be imported from outside of :module:`configlib`
+"""
+
import re
from typing import List
def parse_case(any_case: str) -> List[str]:
+ """
+ parses a multiword string from cases like PascalCase or snake_case
+
+ :param any_case: the multi-word string
+ :return: the words lowercased as an array
+ """
if '_' in any_case:
return any_case.lower().split('_')
if '-' in any_case:
@@ -11,8 +21,20 @@ def parse_case(any_case: str) -> List[str]:
def snake_case(any_case: str) -> str:
+ """
+ parses a multiword string from cases like PascalCase or snake_case
+
+ :param any_case: the multi-word string
+ :return: the words in snake_case
+ """
return '_'.join(parse_case(any_case))
def pascal_case(any_case: str) -> str:
+ """
+ parses a multiword string from cases like PascalCase or snake_case
+
+ :param any_case: the multi-word string
+ :return: the words in PascalCase
+ """
return ''.join(word.capitalize() for word in parse_case(any_case))
diff --git a/configlib/version.py b/configlib/version.py
index 475dc6e..2c6d4a3 100644
--- a/configlib/version.py
+++ b/configlib/version.py
@@ -1,16 +1,25 @@
-from dataclasses import dataclass
+"""versioninfo"""
-@dataclass
+# pylint: disable=too-few-public-methods
class VersionInfo:
+ """Version info dataclass"""
major: int
minor: int
build: int
level: str
serial: int
+ # pylint: disable=too-many-arguments
+ def __init__(self, major: int, minor: int, build: int, level: str, serial: int):
+ self.major: int = major
+ self.minor: int = minor
+ self.build: int = build
+ self.level: str = level
+ self.serial: int = serial
+
def __str__(self):
return '{major}.{minor}.{build}{level}{serial}'.format(**self.__dict__)
-version = VersionInfo(1, 0, 0, 'a', 0)
+VERSION = VersionInfo(1, 0, 0, 'a', 0)