Skip to content

Instantly share code, notes, and snippets.

@hastern
Last active October 3, 2015 16:07
Show Gist options
  • Save hastern/68abc4b4e0aae8fb2aab to your computer and use it in GitHub Desktop.
Save hastern/68abc4b4e0aae8fb2aab to your computer and use it in GitHub Desktop.
A simple dictionary based config loader using either json oder yaml as storage format
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#
# A simple dictionary based configuration loader
#
# -------------
# Usage example
# -------------
#
# from configloader import Configuration
# Configuration.default = {
# 'number': 1,
# 'string': 'text',
# 'key': {
# 'list': [],
# }
# }
#
# # Simply load one local configuration file
# Configuration.load("config.json")
# # Load one configuration in yaml format
# Configuration.use_yaml().load("config.yaml")
# # Load a configuration from user home
# Configuration.user_home().load("config.json")
# # Load a machine wide configuration
# Configuration.machine_wide().load("config.json")
# # Load a configuration within a sub folder
# Configuration.user_home("application").load("config.json")
#
# Access via (read and write):
#
# # Read value
# Configuration['number']
# # Read from sub key
# Configuration['key.list']
#
# # Write value
# Configuration['number'] = 23
#
# ----------------------
# Different file formats
# ----------------------
#
# Configuration.use_json()
# Use JSON file format
#
# Configuration.use_yaml()
# Use YAML file format
#
# ---------------------------
# Different storage locations
# ---------------------------
#
# Configuration.machine_wide(name="<APPLICATION NAME>")
# Load the configuration from a global location (either %PROGRAMDATA% or "/etc")
#
# Configuration.user_home(name="<APPLICATION NAME>")
# Load the configuration from a user specific location (either %APPDATA% or "~/")
#
# Configuration.local()
# Load the configuration relative to script location (based of __FILE__)
#
#
# **DON'T FORGET TO ADD THE CONFIG FILE TO YOUR .gitignore**
#
# The MIT License (MIT)
#
# Copyright (c) 2015 Hanno Sternberg
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import os.path
import sys
import json
try:
import yaml
except ImportError:
yaml = None
class ConfigurationMeta(type):
def __init__(cls, name, bases, attrs):
cls._default = {}
def __getitem__(cls, key):
return cls.get(key)
def __setitem__(cls, key, value):
cls.set(key, value)
@property
def default(cls):
return cls._default
@default.setter
def default(cls, default):
cls._default.update(default)
class Configuration(object):
__metaclass__ = ConfigurationMeta
Delimiter = "."
data = {}
file = ""
folder = ""
autosave = {}
load_method = staticmethod(json.load)
save_method = staticmethod(lambda data, file: json.dump(data, file, sort_keys=True, indent=2, separators=(',', ': ')))
@classmethod
def use_json(cls):
cls.load_method = staticmethod(json.load)
cls.save_method = staticmethod(lambda data, file: json.dump(data, file, sort_keys=True, indent=2, separators=(',', ': ')))
return cls
@classmethod
def use_yaml(cls):
assert yaml is not None
cls.load_method = staticmethod(lambda file: yaml.safe_load(file))
cls.save_method = staticmethod(lambda data, file: yaml.safe_dump(data, file, default_flow_style=False))
return cls
@classmethod
def machine_wide(cls, name=""):
if sys.platform == "win32":
prefix = os.path.abspath(os.environ['PROGRAMDATA'])
else:
prefix = os.path.abspath("/etc")
if name != "":
cls.folder = os.path.join(prefix, name)
else:
cls.folder = prefix
return cls
@classmethod
def user_home(cls, name=""):
if sys.platform == "win32":
prefix = os.path.abspath(os.environ['APPDATA'])
else:
prefix = os.path.expanduser("~")
if name != "":
cls.folder = os.path.join(prefix, name)
else:
cls.folder = prefix
return cls
@classmethod
def local(cls, name=""):
prefix = os.path.dirname(__file__)
if name != "":
cls.folder = os.path.join(prefix, name)
else:
cls.folder = prefix
return cls
@classmethod
def _needsUpdate(cls, data, default):
if data.keys() != default.keys():
return True
for key in data:
if isinstance(data[key], dict):
if cls._needsUpdate(data[key], default[key]):
return True
return False
@classmethod
def merge(cls, data, default, override=False):
for key in default:
if isinstance(default[key], dict):
if key not in data:
data[key] = default[key]
else:
cls.merge(data[key], default[key])
elif key not in data or override:
data[key] = default[key]
@classmethod
def load(cls, file, autosave=False):
cls.data = {}
cls.file = os.path.join(cls.folder, file)
cls.autosave = autosave
if not os.path.exists(cls.file):
cls.data.update(cls.default)
with open(file, "r") as f:
cls.data = cls.load_method(f)
if cls._needsUpdate(cls.data, cls.default):
cls.merge(cls.data, cls.default)
cls.save()
@classmethod
def load_json(cls, file, autosave=False):
cls.use_json().load(file, autosave)
@classmethod
def load_yaml(cls, file, autosave=False):
cls.use_yaml().load(file, autosave)
@classmethod
def save(cls, file=None, data=None):
if file is None:
file = cls.file
if data is None:
data = cls.data
with open(file, "w") as f:
cls.save_method(data, f)
@classmethod
def keys(cls):
return cls.data.keys()
@classmethod
def __iter__(cls):
for entry in cls.data:
yield entry
@classmethod
def get(cls, key):
parts = key.split(cls.Delimiter)
elem = cls.data
for part in parts:
elem = elem[part]
return elem
@classmethod
def set(cls, key, value):
parts = key.split(cls.Delimiter)
key = parts.pop()
elem = cls.data
for part in parts:
if part not in elem:
elem[part] = {}
elem = elem[part]
elem[key] = value
if cls.autosave:
cls.save(cls.data)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment