aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-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
-rw-r--r--pylintrc.cfg2
-rw-r--r--requirements-dev.txt4
-rw-r--r--setup.cfg5
-rw-r--r--setup.py11
-rw-r--r--tests/__init__.py6
11 files changed, 182 insertions, 36 deletions
diff --git a/.gitignore b/.gitignore
index 4d93ca8..38914ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
configlib.egg-info
venv/
+.pytest_cache/
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)
diff --git a/pylintrc.cfg b/pylintrc.cfg
new file mode 100644
index 0000000..facac26
--- /dev/null
+++ b/pylintrc.cfg
@@ -0,0 +1,2 @@
+[MASTER]
+ignore=tests, setup.py
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..da0d5b1
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,4 @@
+pylint
+pytest
+pytest-runner
+pytest-pylint
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..bdd3e25
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[aliases]
+test = pytest
+
+[tool:pytest]
+addopts = --pylint --pylint-rcfile=pylintrc.cfg
diff --git a/setup.py b/setup.py
index c7445fd..27cb9c7 100644
--- a/setup.py
+++ b/setup.py
@@ -5,11 +5,14 @@ find_packages
with open('configlib/version.py') as f:
_loc, _glob = {}, {}
exec(f.read(), _loc, _glob)
- version = {**_loc, **_glob}['version']
+ version = {**_loc, **_glob}['VERSION']
with open('requirements.txt') as f:
requirements = f.read().splitlines()
+with open('requirements-dev.txt') as f:
+ dev_requirements = f.read().splitlines()
+
with open('README.md') as f:
readme = f.read()
@@ -23,8 +26,12 @@ setup(
version=str(version),
install_requires=requirements,
long_description=readme,
- test_suite='tests.all_tests',
+ setup_requires=['pytest-runner', 'pytest-pylint'],
+ tests_require=['pytest', 'pylint'],
license="MIT",
+ extras_require={
+ 'dev': dev_requirements,
+ },
packages=['configlib'],
description="An easy python config manager",
classifiers=[
diff --git a/tests/__init__.py b/tests/__init__.py
index e8b0db2..8b13789 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,7 +1 @@
-import unittest
-
-def all_tests():
- test_loader = unittest.TestLoader()
- test_suite = test_loader.discover('tests', pattern='test_*.py')
- return test_suite