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
requirement definitions before being loaded and used.
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
refer to different modular parts of the application needing configuration, and the
config data structure must contain at least one.
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
refer to different modular parts of the application needing configuration, and the
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
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}
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]]
important_number = 8237
@ -43,14 +44,13 @@ from copy import deepcopy
from .freezedry import freezedryable, rehydrate
class InvalidConfigError(Exception):
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)
class _ConfigDefinition(ABC):
def __init__(self, default=None, optional=False, helptext=""):
self.default = default
@ -64,7 +64,7 @@ class _ConfigDefinition(ABC):
Raises InvalidConfigError on failure.
"""
pass
@freezedryable
class BoolDef(_ConfigDefinition):
@ -75,6 +75,7 @@ class BoolDef(_ConfigDefinition):
if not isinstance(value, bool):
raise InvalidConfigError("Config value must be a boolean")
@freezedryable
class IntDef(_ConfigDefinition):
def __init__(self, default=None, minval=None, maxval=None,
@ -93,6 +94,7 @@ class IntDef(_ConfigDefinition):
raise InvalidConfigError("Config value must be <= " +
str(self.maxval))
@freezedryable
class StringDef(_ConfigDefinition):
def __init__(self, default="", minlength=None, maxlength=None,
@ -111,6 +113,7 @@ class StringDef(_ConfigDefinition):
raise InvalidConfigError("Config string length must be <= " +
str(self.maxlength))
@freezedryable
class DictDef(_ConfigDefinition):
def __init__(self, default=None, optional=False, helptext=""):
@ -154,7 +157,7 @@ class DictDef(_ConfigDefinition):
except InvalidConfigError as e:
e.args = ("Key: " + key,) + e.args
raise
def get_template(self, include_optional=False):
"""
Return a config dict with the minimum structure required for this ConfigDefinition.
@ -162,7 +165,7 @@ class DictDef(_ConfigDefinition):
defaults that successfully validate.
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
Returns:
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():
if confdef.optional and (not include_optional):
continue
if hasattr(confdef,"get_template"):
template[key]=confdef.get_template(include_optional)
else:
template[key]=confdef.default
if hasattr(confdef, "get_template"):
template[key] = confdef.get_template(include_optional)
else:
template[key] = confdef.default
return template
@ -192,27 +195,32 @@ class _ListDefMixin():
raise
def get_template(self, include_optional=False):
if hasattr(super(),"get_template"):
if hasattr(super(), "get_template"):
return [super().get_template(include_optional)]
else:
return [self.default]
@freezedryable
class BoolListDef(_ListDefMixin, BoolDef):
pass
@freezedryable
class IntListDef(_ListDefMixin, IntDef):
pass
@freezedryable
class StringListDef(_ListDefMixin, StringDef):
pass
@freezedryable
class DictListDef(_ListDefMixin, DictDef):
pass
@freezedryable
class ConfDefinition(DictDef):
pass
@ -231,14 +239,13 @@ class ConfigManager():
and return a plain parsed dict.
"""
if isinstance(source, dict): # load from dict
return source
return source
elif isinstance(source, str): # load from pathname
with open(source, 'r') as conf_file:
return toml.load(conf_file)
return toml.load(conf_file)
else: # load from file
return toml.load(source)
def load(self, source):
"""
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
being replaced by the new source. This includes lists, which will be treated as any other
value and completely replaced.
Args:
source: Either the root dict of a data structure to load directly, a filepath to a TOML file,
or an open TOML file.
"""
source: Either the root dict of a data structure to load directly, a filepath to a TOML
file, or an open TOML file.
"""
self._overlay(self._load_source(source), self.root_config)
self._overlay(self.frozen_config, self.root_config)
def freeze_value(self, bundle_name, *field_names):
"""
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.
Args:
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
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
target_field = self.root_config
@ -289,29 +295,26 @@ class ConfigManager():
frozen_value[name] = {}
frozen_value = frozen_value[name]
frozen_value[names[-1]] = target_field[names[-1]]
def add_confdef(self, bundle_name, confdef):
"""
Stores a ConfigDefinition for future use when validating the corresponding config bundle
Args:
bundle_name (str) : The name to store the config definition under.
confdef (ConfigDefinition): The populated ConfigDefinition to store.
"""
self.confdefs[bundle_name]=confdef
self.confdefs[bundle_name] = confdef
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:
confdefs : A dict of populated ConfigDefinitions to store, using their keys as names.
"""
"""
self.confdefs.update(confdefs)
def list_missing_confdefs(self):
@ -321,7 +324,6 @@ class ConfigManager():
"""
return list(self.root_config.keys() - self.confdefs.keys())
def _overlay(self, src, dest):
for key in src:
# 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])
else:
# 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):
"""
Get a config bundle called ``bundle_name`` and validate
it against the corresponding config definition stored in the ConfigManager.
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
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):
"""
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``
Args:
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses the values
as ConfigDefinitions rather than looking up a stored one in the ConfigManager.
bundle_names: A list of config bundle names to get. If dictionary is supplied, uses
the values as ConfigDefinitions rather than looking up a stored one in the
ConfigManager.
Returns:
A dict of config dicts, with keys matching those passed in ``bundle_names``.
"""
@ -384,7 +387,7 @@ class ConfigManager():
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())
@ -417,4 +420,3 @@ def update_toml_message(filepath, message):
def gen_comment(string):
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