##
#
# Copyright (C) 2018 Matt Molyneaux
#
# This file is part of Exhibition.
#
# Exhibition is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Exhibition is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Exhibition. If not, see <https://www.gnu.org/licenses/>.
#
##
import io
from ruamel.yaml import YAML
SITE_YAML_PATH = "site.yaml"
yaml_parser = YAML(typ="safe")
[docs]class Config:
"""
Configuration object that implements a dict-like interface
If a key cannot be found in this instance, the parent :class:`Config` will
be searched (and its parent, etc.)
"""
def __init__(self, data=None, parent=None, node=None):
"""
:param data:
Can be one of a string, a file-like object, a dict-like object, or
``None``. The first two will be assumed as YAML
:param parent:
Parent :class:`Config` or ``None`` if this is the root
configuration object
:param node:
The node that this object to bound to, or ``None`` if it is the
root configuration object
"""
assert (parent is None) == (node is None), \
"Either both parent and node are defined or they are both None"
self.parent = parent
self.node = node
self._base_config = {}
if data:
self.load(data)
[docs] def load(self, data):
"""
Load data into configutation object
:param data:
If a string or file-like object, ``data`` is parsed as if it were
YAML data. If a dict-like object, ``data`` is added to the internal
dictionary.
Otherwise an :class:`AssertionError` exception is raised
"""
if isinstance(data, (str, io.IOBase)):
self._base_config.update(yaml_parser.load(data))
elif isinstance(data, dict):
self._base_config.update(data)
else:
raise AssertionError("data needs to be a string, file-like, or dict-like object")
[docs] @classmethod
def from_path(cls, path):
"""Load YAML data from a file"""
with open(path) as f:
obj = cls(f)
return obj
[docs] def get_name(self):
if self.node is None:
return SITE_YAML_PATH
else:
return self.node.full_path
def __getitem__(self, key):
try:
return self._base_config[key]
except KeyError as exp:
exp_str = "Could not find %s in %s" % (key, self.get_name())
if self.parent is None:
raise KeyError(exp_str) from exp
else:
try:
return self.parent[key]
except KeyError as exp_parent:
raise KeyError(exp_str) from exp_parent
def __setitem__(self, key, value):
self._base_config[key] = value
def __contains__(self, key):
return key in self.keys()
def __len__(self):
return len(list(self.keys()))
def __iter__(self):
return self.keys()
[docs] def keys(self):
_keys_set = set()
for k in self._base_config.keys():
_keys_set.add(k)
yield k
if self.parent is not None:
for k in self.parent.keys():
if k not in _keys_set:
_keys_set.add(k)
yield k
[docs] def values(self):
for k in self.keys():
yield self[k]
[docs] def items(self):
for k in self.keys():
yield (k, self[k])
[docs] def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
[docs] def update(self, *args, **kwargs):
self._base_config.update(*args, **kwargs)
[docs] def copy(self):
klass = type(self)
return klass(self._base_config.copy(), parent=self.parent, node=self.node)
def __repr__(self):
return "<%s: %s: %s>" % (self.__class__.__name__,
self.get_name(),
self._base_config.keys())