Add general package boilerplate, setup dev install. Closes #2

master
Tom Wilson 6 years ago
parent ed8701b1e7
commit ccd5a50892

116
.gitignore vendored

@ -0,0 +1,116 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/

@ -2,10 +2,10 @@
Configuration management module. Enables configuration to be validated against Configuration management module. Enables configuration to be validated against
requirement definitions before being loaded and used. requirement definitions before being loaded and used.
Compatible with both raw config data structures and TOML files, config data must Compatible with both raw config data structures and TOML files, config data must
start with a root dict containing named "config bundles". These are intended to start with a root dict containing named "config bundles". These are intended to
refer to different modular parts of the application needing configuration, and the refer to different modular parts of the application needing configuration, and the
config data structure must contain at least one. config data structure must contain at least one.
Each config bundle itself needs to have a dict at the root, and so in practice a minimal Each config bundle itself needs to have a dict at the root, and so in practice a minimal
TOML config file would look like:: TOML config file would look like::
@ -18,7 +18,8 @@ This would resolve to a config bundle named "myapp" that results in the dict::
{"config_thingy_a": "foooooo!", "important_number": 8237} {"config_thingy_a": "foooooo!", "important_number": 8237}
Root items that are not dicts are not supported, for instance both the following TOML files would fail:: Root items that are not dicts are not supported, for instance both the following
TOML files would fail::
[[myapp]] [[myapp]]
important_number = 8237 important_number = 8237
@ -43,14 +44,13 @@ from copy import deepcopy
from .freezedry import freezedryable, rehydrate from .freezedry import freezedryable, rehydrate
class InvalidConfigError(Exception): class InvalidConfigError(Exception):
pass pass
# The Table and Array terms from the TOML convention essentially # The Table and Array terms from the TOML convention essentially
# map directly to Dictionaries (Tables), and Lists (Arrays) # map directly to Dictionaries (Tables), and Lists (Arrays)
class _ConfigDefinition(ABC): class _ConfigDefinition(ABC):
def __init__(self, default=None, optional=False, helptext=""): def __init__(self, default=None, optional=False, helptext=""):
self.default = default self.default = default
@ -64,7 +64,7 @@ class _ConfigDefinition(ABC):
Raises InvalidConfigError on failure. Raises InvalidConfigError on failure.
""" """
pass pass
@freezedryable @freezedryable
class BoolDef(_ConfigDefinition): class BoolDef(_ConfigDefinition):
@ -75,6 +75,7 @@ class BoolDef(_ConfigDefinition):
if not isinstance(value, bool): if not isinstance(value, bool):
raise InvalidConfigError("Config value must be a boolean") raise InvalidConfigError("Config value must be a boolean")
@freezedryable @freezedryable
class IntDef(_ConfigDefinition): class IntDef(_ConfigDefinition):
def __init__(self, default=None, minval=None, maxval=None, def __init__(self, default=None, minval=None, maxval=None,
@ -93,6 +94,7 @@ class IntDef(_ConfigDefinition):
raise InvalidConfigError("Config value must be <= " + raise InvalidConfigError("Config value must be <= " +
str(self.maxval)) str(self.maxval))
@freezedryable @freezedryable
class StringDef(_ConfigDefinition): class StringDef(_ConfigDefinition):
def __init__(self, default="", minlength=None, maxlength=None, def __init__(self, default="", minlength=None, maxlength=None,
@ -111,6 +113,7 @@ class StringDef(_ConfigDefinition):
raise InvalidConfigError("Config string length must be <= " + raise InvalidConfigError("Config string length must be <= " +
str(self.maxlength)) str(self.maxlength))
@freezedryable @freezedryable
class DictDef(_ConfigDefinition): class DictDef(_ConfigDefinition):
def __init__(self, default=None, optional=False, helptext=""): def __init__(self, default=None, optional=False, helptext=""):
@ -154,7 +157,7 @@ class DictDef(_ConfigDefinition):
except InvalidConfigError as e: except InvalidConfigError as e:
e.args = ("Key: " + key,) + e.args e.args = ("Key: " + key,) + e.args
raise raise
def get_template(self, include_optional=False): def get_template(self, include_optional=False):
""" """
Return a config dict with the minimum structure required for this ConfigDefinition. Return a config dict with the minimum structure required for this ConfigDefinition.
@ -162,7 +165,7 @@ class DictDef(_ConfigDefinition):
defaults that successfully validate. defaults that successfully validate.
Args: Args:
include_optional: If set true, will include *all* config fields, not just the include_optional: If set true, will include *all* config fields, not just the
required ones required ones
Returns: Returns:
Dict containing the structure that should be passed back in (with values) to comply Dict containing the structure that should be passed back in (with values) to comply
@ -172,11 +175,11 @@ class DictDef(_ConfigDefinition):
for key, confdef in self.def_dict.items(): for key, confdef in self.def_dict.items():
if confdef.optional and (not include_optional): if confdef.optional and (not include_optional):
continue continue
if hasattr(confdef,"get_template"): if hasattr(confdef, "get_template"):
template[key]=confdef.get_template(include_optional) template[key] = confdef.get_template(include_optional)
else: else:
template[key]=confdef.default template[key] = confdef.default
return template return template
@ -192,27 +195,32 @@ class _ListDefMixin():
raise raise
def get_template(self, include_optional=False): def get_template(self, include_optional=False):
if hasattr(super(),"get_template"): if hasattr(super(), "get_template"):
return [super().get_template(include_optional)] return [super().get_template(include_optional)]
else: else:
return [self.default] return [self.default]
@freezedryable @freezedryable
class BoolListDef(_ListDefMixin, BoolDef): class BoolListDef(_ListDefMixin, BoolDef):
pass pass
@freezedryable @freezedryable
class IntListDef(_ListDefMixin, IntDef): class IntListDef(_ListDefMixin, IntDef):
pass pass
@freezedryable @freezedryable
class StringListDef(_ListDefMixin, StringDef): class StringListDef(_ListDefMixin, StringDef):
pass pass
@freezedryable @freezedryable
class DictListDef(_ListDefMixin, DictDef): class DictListDef(_ListDefMixin, DictDef):
pass pass
@freezedryable @freezedryable
class ConfDefinition(DictDef): class ConfDefinition(DictDef):
pass pass
@ -231,14 +239,13 @@ class ConfigManager():
and return a plain parsed dict. and return a plain parsed dict.
""" """
if isinstance(source, dict): # load from dict if isinstance(source, dict): # load from dict
return source return source
elif isinstance(source, str): # load from pathname elif isinstance(source, str): # load from pathname
with open(source, 'r') as conf_file: with open(source, 'r') as conf_file:
return toml.load(conf_file) return toml.load(conf_file)
else: # load from file else: # load from file
return toml.load(source) return toml.load(source)
def load(self, source): def load(self, source):
""" """
Load a config source into the ConfigManager, replacing any existing config. Load a config source into the ConfigManager, replacing any existing config.
@ -256,27 +263,26 @@ class ConfigManager():
config. Dicts will be recursively processed with keys being merged and existing values config. Dicts will be recursively processed with keys being merged and existing values
being replaced by the new source. This includes lists, which will be treated as any other being replaced by the new source. This includes lists, which will be treated as any other
value and completely replaced. value and completely replaced.
Args: Args:
source: Either the root dict of a data structure to load directly, a filepath to a TOML file, source: Either the root dict of a data structure to load directly, a filepath to a TOML
or an open TOML file. file, or an open TOML file.
""" """
self._overlay(self._load_source(source), self.root_config) self._overlay(self._load_source(source), self.root_config)
self._overlay(self.frozen_config, self.root_config) self._overlay(self.frozen_config, self.root_config)
def freeze_value(self, bundle_name, *field_names): def freeze_value(self, bundle_name, *field_names):
""" """
Freeze the given config field so that subsequent calls to ``load`` and ``load_overlay`` Freeze the given config field so that subsequent calls to ``load`` and ``load_overlay``
cannot change it. Can only be used for dict values or dict values nested in parent dicts. cannot change it. Can only be used for dict values or dict values nested in parent dicts.
Args: Args:
bundle_name: The name of the bundle to look for the field in. bundle_name: The name of the bundle to look for the field in.
*field_names: a series of strings that locate the config field, either a single *field_names: a series of strings that locate the config field, either a single
key or series of nested keys. key or series of nested keys.
""" """
#Bundle names are really no different from any other nested dict # Bundle names are really no different from any other nested dict
names = (bundle_name,) + field_names names = (bundle_name,) + field_names
target_field = self.root_config target_field = self.root_config
@ -289,29 +295,26 @@ class ConfigManager():
frozen_value[name] = {} frozen_value[name] = {}
frozen_value = frozen_value[name] frozen_value = frozen_value[name]
frozen_value[names[-1]] = target_field[names[-1]] frozen_value[names[-1]] = target_field[names[-1]]
def add_confdef(self, bundle_name, confdef): def add_confdef(self, bundle_name, confdef):
""" """
Stores a ConfigDefinition for future use when validating the corresponding config bundle Stores a ConfigDefinition for future use when validating the corresponding config bundle
Args: Args:
bundle_name (str) : The name to store the config definition under. bundle_name (str) : The name to store the config definition under.
confdef (ConfigDefinition): The populated ConfigDefinition to store. confdef (ConfigDefinition): The populated ConfigDefinition to store.
""" """
self.confdefs[bundle_name]=confdef self.confdefs[bundle_name] = confdef
def add_confdefs(self, confdefs): def add_confdefs(self, confdefs):
""" """
Stores multiple ConfigDefinitions at once for future use when validating the corresponding config bundles Stores multiple ConfigDefinitions at once for future use when validating the corresponding
config bundles
Args: Args:
confdefs : A dict of populated ConfigDefinitions to store, using their keys as names. confdefs : A dict of populated ConfigDefinitions to store, using their keys as names.
""" """
self.confdefs.update(confdefs) self.confdefs.update(confdefs)
def list_missing_confdefs(self): def list_missing_confdefs(self):
@ -321,7 +324,6 @@ class ConfigManager():
""" """
return list(self.root_config.keys() - self.confdefs.keys()) return list(self.root_config.keys() - self.confdefs.keys())
def _overlay(self, src, dest): def _overlay(self, src, dest):
for key in src: for key in src:
# If the key is also in the dest and both are dicts, merge them. # If the key is also in the dest and both are dicts, merge them.
@ -329,14 +331,14 @@ class ConfigManager():
self._overlay(src[key], dest[key]) self._overlay(src[key], dest[key])
else: else:
# Otherwise it's either an existing value to be replaced or needs to be added. # Otherwise it's either an existing value to be replaced or needs to be added.
dest[key] = src[key] dest[key] = src[key]
def get_config_bundle(self, bundle_name, conf_def=None): def get_config_bundle(self, bundle_name, conf_def=None):
""" """
Get a config bundle called ``bundle_name`` and validate Get a config bundle called ``bundle_name`` and validate
it against the corresponding config definition stored in the ConfigManager. it against the corresponding config definition stored in the ConfigManager.
If ``conf_def`` is supplied, it gets used instead. Returns a validated If ``conf_def`` is supplied, it gets used instead. Returns a validated
config bundle dict. config bundle dict.
Note that as part of validation, optional keys that are missing will be Note that as part of validation, optional keys that are missing will be
filled in with their default values (see ``DictDef``). This function will copy filled in with their default values (see ``DictDef``). This function will copy
@ -363,13 +365,14 @@ class ConfigManager():
def get_config_bundles(self, bundle_names): def get_config_bundles(self, bundle_names):
""" """
Get multiple config bundles from the root dict at once, validating each one with the Get multiple config bundles from the root dict at once, validating each one with the
corresponding confdef stored in the ConfigManager. See ``get_config_bundle`` corresponding confdef stored in the ConfigManager. See ``get_config_bundle``
Args: Args:
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values bundle_names: A list of config bundle names to get. If dictionary is supplied, uses
as ConfigDefinitions rather than looking up a stored one in the ConfigManager. the values as ConfigDefinitions rather than looking up a stored one in the
ConfigManager.
Returns: Returns:
A dict of config dicts, with keys matching those passed in ``bundle_names``. A dict of config dicts, with keys matching those passed in ``bundle_names``.
""" """
@ -384,7 +387,7 @@ class ConfigManager():
def get_bundle_names(self): def get_bundle_names(self):
""" """
Returns a list of names of top level config bundles Returns a list of names of top level config bundles
""" """
return list(self.root_config.keys()) return list(self.root_config.keys())
@ -417,4 +420,3 @@ def update_toml_message(filepath, message):
def gen_comment(string): def gen_comment(string):
return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n' return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'

@ -0,0 +1,2 @@
[pytest]
pep8maxlinelength = 99

@ -0,0 +1,26 @@
from setuptools import setup
setup(name='config-spec',
version='0.3dev',
description='',
url='https://git.distreon.net/novirium/config-spec',
author='novirium',
author_email='t.wilson.au@gmail.com',
license='MIT',
packages=[],
py_modules=['configspec'],
install_requires=[
"preserve@git+https://git.distreon.net/novirium/python-preserve.git"
],
extras_require={
'dev': [
'pylint',
'autopep8',
'pytest',
'pytest-pep8',
'pytest-cov'
]
},
long_description=open('README.md').read(),
long_description_content_type='text/markdown'
)
Loading…
Cancel
Save