diff --git a/gluon/contrib/appconfig.py b/gluon/contrib/appconfig.py new file mode 100644 index 00000000..972247f6 --- /dev/null +++ b/gluon/contrib/appconfig.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Read from configuration files easily without hurting performances + +USAGE: +During development you can load a config file either in .ini or .json +format (by default app/private/appconfig.ini or app/private/appconfig.json) +The result is a dict holding the configured values. Passing reload=True +is meant only for development: in production, leave reload to False and all +values will be cached + +from gluon.contrib.appconfig import AppConfig +myconfig = AppConfig(path_to_configfile, reload=False) + +print myconfig['db']['uri'] + +The returned dict can walk with "dot notation" an arbitrarely nested dict + +print myconfig.take('db.uri') + +You can even pass a cast function, i.e. + +print myconfig.take('auth.expiration', cast=int) + +Once the value has been fetched (and casted) it won't change until the process +is restarted (or reload=True is passed). + +""" +import thread +import os +from ConfigParser import SafeConfigParser +from gluon import current +from gluon.serializers import json_parser + +locker = thread.allocate_lock() + + +def AppConfig(*args, **vars): + + locker.acquire() + reload_ = vars.pop('reload', False) + try: + instance_name = 'AppConfig_' + current.request.application + if reload_ or not hasattr(AppConfig, instance_name): + setattr(AppConfig, instance_name, AppConfigLoader(*args, **vars)) + return getattr(AppConfig, instance_name).settings + finally: + locker.release() + + +class AppConfigDict(dict): + """ + dict that has a .take() method to fetch nested values and puts + them into cache + """ + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.int_cache = {} + + def take(self, path, cast=None): + parts = path.split('.') + if path in self.int_cache: + return self.int_cache[path] + value = self + walking = [] + for part in parts: + if part not in value: + raise BaseException("%s not in config [%s]" % + (part, '-->'.join(walking))) + value = value[part] + walking.append(part) + if cast is None: + self.int_cache[path] = value + else: + try: + value = cast(value) + self.int_cache[path] = value + except (ValueError, TypeError): + raise BaseException("%s can't be converted to %s" % + (value, cast)) + return value + + +class AppConfigLoader(object): + + def __init__(self, configfile=None): + if not configfile: + priv_folder = os.path.join(current.request.folder, 'private') + configfile = os.path.join(priv_folder, 'appconfig.ini') + if not os.path.isfile(configfile): + configfile = os.path.join(priv_folder, 'appconfig.json') + if not os.path.isfile(configfile): + configfile = None + if not configfile or not os.path.isfile(configfile): + raise BaseException("Config file not found") + self.file = configfile + self.ctype = os.path.splitext(configfile)[1][1:] + self.settings = None + self.read_config() + + def read_config_ini(self): + config = SafeConfigParser() + config.read(self.file) + settings = {} + for section in config.sections(): + settings[section] = {} + for option in config.options(section): + settings[section][option] = config.get(section, option) + self.settings = AppConfigDict(settings) + + def read_config_json(self): + with open(self.file, 'r') as c: + self.settings = AppConfigDict(json_parser.load(c)) + + def read_config(self): + if self.settings is None: + try: + getattr(self, 'read_config_' + self.ctype)() + except AttributeError: + raise BaseException("Unsupported config file format") + return self.settings diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py index 22f3978d..ed98082c 100644 --- a/gluon/tests/test_contribs.py +++ b/gluon/tests/test_contribs.py @@ -4,14 +4,24 @@ """ Unit tests for contribs """ import unittest +import os from fix_path import fix_sys_path fix_sys_path(__file__) +from gluon.storage import Storage +import gluon.contrib.fpdf as fpdf +import gluon.contrib.pyfpdf as pyfpdf +from gluon.contrib.appconfig import AppConfig -from utils import md5_hash -import contrib.fpdf as fpdf -import contrib.pyfpdf as pyfpdf + +def setUpModule(): + pass + + +def tearDownModule(): + if os.path.isfile('appconfig.json'): + os.unlink('appconfig.json') class TestContribs(unittest.TestCase): @@ -35,6 +45,28 @@ class TestContribs(unittest.TestCase): self.assertTrue(fpdf.FPDF_VERSION in pdf_out, 'version string') self.assertTrue('hello world' in pdf_out, 'sample message') + def test_appconfig(self): + """ + Test for the appconfig module + """ + from gluon import current + s = Storage({'application': 'admin', + 'folder': 'applications/admin'}) + current.request = s + simple_config = '{"config1" : "abc", "config2" : "bcd", "config3" : { "key1" : 1, "key2" : 2} }' + with open('appconfig.json', 'w') as g: + g.write(simple_config) + myappconfig = AppConfig('appconfig.json') + self.assertEqual(myappconfig['config1'], 'abc') + self.assertEqual(myappconfig['config2'], 'bcd') + self.assertEqual(myappconfig.take('config1'), 'abc') + self.assertEqual(myappconfig.take('config3.key1', cast=str), '1') + # once parsed, can't be casted to other types + self.assertEqual(myappconfig.take('config3.key1', cast=int), '1') + + self.assertEqual(myappconfig.take('config3.key2'), 2) + + current.request = {} if __name__ == '__main__': unittest.main()