diff options
author | Marcel Hellkamp <marc@gsites.de> | 2013-07-16 19:53:11 +0200 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2013-07-16 20:31:20 +0200 |
commit | f493648bea751e621fe4469c71e52c37454a849d (patch) | |
tree | f46eb769fe8249c5cb74ffb28d3ae9cb5ed2e5ac | |
parent | a00a438d6bb084528a5a0f947620dcdb404febb7 (diff) | |
download | bottle-f493648bea751e621fe4469c71e52c37454a849d.tar.gz |
Merged the new ConfDict and the old ConfigDict into one class.
Added depr() warnings to all old ConfigDict APIs.
Added ConfigDict.load_dict()
-rw-r--r-- | bottle.py | 111 | ||||
-rw-r--r-- | test/test_config.py | 20 | ||||
-rw-r--r-- | test/test_mount.py | 4 |
3 files changed, 82 insertions, 53 deletions
@@ -471,7 +471,7 @@ class Route(object): #: Additional keyword arguments passed to the :meth:`Bottle.route` #: decorator are stored in this dictionary. Used for route-specific #: plugin configuration and meta-data. - self.config = ConfigDict(config) + self.config = ConfigDict().load_dict(config) def __call__(self, *a, **ka): depr("Some APIs changed to return Route() instances instead of"\ @@ -544,6 +544,13 @@ class Route(object): to recover the original function before inspection. ''' return getargspec(self.get_undecorated_callback())[0] + def get_config(key, default=None): + ''' Lookup a config field and return its value, first checking the + route.config, then route.app.config.''' + for conf in (self.config, self.app.conifg): + if key in conf: return conf[key] + return default + def __repr__(self): cb = self.get_undecorated_callback() return '<%s %r %r>' % (self.method, self.rule, cb) @@ -570,17 +577,12 @@ class Bottle(object): def __init__(self, catchall=True, autojson=True): #: A :class:`ConfDict` for app specific configuration. - self.conf = ConfDict() - self.conf._on_change = functools.partial(self.trigger_hook, 'config') - self.conf.meta_set('autojson', 'validate', bool) - self.conf.meta_set('catchall', 'validate', bool) - self.conf['catchall'] = catchall - self.conf['autojson'] = autojson - - #: Deprecated self.config = ConfigDict() - self.config.autojson = autojson - self.config.catchall = catchall + self.config._on_change = functools.partial(self.trigger_hook, 'config') + self.config.meta_set('autojson', 'validate', bool) + self.config.meta_set('catchall', 'validate', bool) + self.config['catchall'] = catchall + self.config['autojson'] = autojson #: A :class:`ResourceManager` for application files self.resources = ResourceManager() @@ -591,12 +593,12 @@ class Bottle(object): # Core plugins self.plugins = [] # List of installed plugins. - if self.config.autojson: + if self.config['autojson']: self.install(JSONPlugin()) self.install(TemplatePlugin()) #: If true, most exceptions are caught and returned as :exc:`HTTPError` - catchall = DictProperty('conf', 'catchall') + catchall = DictProperty('config', 'catchall') __hook_names = 'before_request', 'after_request', 'app_reset', 'config' __hook_reversed = 'after_request' @@ -1660,9 +1662,11 @@ class LocalResponse(BaseResponse): _headers = local_property('response_headers') body = local_property('response_body') + Request = BaseRequest Response = BaseResponse + class HTTPResponse(Response, BottleException): def __init__(self, body='', status=None, headers=None, header=None, **more_headers): @@ -1686,6 +1690,7 @@ class HTTPResponse(Response, BottleException): output = property(_output, _output, doc='Alias for .body') + class HTTPError(HTTPResponse): default_status = 500 def __init__(self, status=None, body=None, exception=None, traceback=None, @@ -1704,6 +1709,7 @@ class HTTPError(HTTPResponse): class PluginError(BottleException): pass + class JSONPlugin(object): name = 'json' api = 2 @@ -1862,7 +1868,6 @@ class MultiDict(DictMixin): getlist = getall - class FormsDict(MultiDict): ''' This :class:`MultiDict` subclass is used to store request form data. Additionally to the normal dict-like item access methods (which return @@ -1982,33 +1987,58 @@ class WSGIHeaderDict(DictMixin): def __contains__(self, key): return self._ekey(key) in self.environ -class ConfDict(dict): - ''' A dict-subclass with some extras. - >>> c = ConfDict() - >>> c['key'] = 'value' - >>> c.update('some.namespace', key='some value') - >>> print(c) - {'key': 'value', 'some.namespace.key':'some value'} +class ConfigDict(dict): + ''' A dict-like configuration storage with additional support for + namespaces, validators, meta-data, on_change listeners and more. ''' __slots__ = ('_meta', '_on_change') - def __init__(self): + def __init__(self, *a, **ka): self._meta = {} self._on_change = lambda name, value: None + if a or ka: + depr('Constructor does no longer accept parameters.') + self.update(*a, **ka) def load_config(self, filename): - ''' Load an *.ini style config file.''' + ''' Load values from an *.ini style config file. + + If the config file contains sections, their names are used as + namespaces for the values within. The two special sections + ``DEFAULT`` and ``bottle`` refer to the root namespace (no prefix). + ''' conf = ConfigParser() conf.read(filename) for section in conf.sections(): for key, value in conf.items(section): - self['%s.%s' % (section, key)] = value + if section not in ('DEFAULT', 'bottle'): + key = section + '.' + key + self[key] = value + return self + + def load_dict(self, source, namespace=''): + ''' Load values from a dictionary structure. Nesting can be used to + represent namespaces. + + >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) + {'some.namespace.key': 'value'} + ''' + for key, value in source.items(): + if isinstance(key, str): + nskey = (namespace + '.' + key).strip('.') + if isinstance(value, dict): + self.load_dict(value, namespace=nskey) + else: + self[nskey] = value + else: + raise TypeError('Key has type %r (not a string)' % type(key)) + return self def update(self, *a, **ka): ''' If the first parameter is a string, all keys are prefixed with this - string. Apart from that it works just as the usual dict.update(). + namespace. Apart from that it works just as the usual dict.update(). Example: ``update('some.namespace', key='value')`` ''' prefix = '' if a and isinstance(a[0], str): @@ -2023,18 +2053,16 @@ class ConfDict(dict): def __setitem__(self, key, value): if not isinstance(key, str): - raise TypeError('Keys must be strings') - filtr = self.meta_get(key, 'filter') - if filtr is not None: - value = filtr(value) + raise TypeError('Key has type %r (not a string)' % type(key)) + value = self.meta_get(key, 'filter', lambda x: x)(value) if key in self and self[key] is value: return self._on_change(key, value) dict.__setitem__(self, key, value) def __delitem__(self, key): + self._on_change(key, None) dict.__delitem__(self, key) - self._on_change(key, value) def meta_get(self, key, metafield, default=None): ''' Return the value of a meta field for a key. ''' @@ -2051,26 +2079,17 @@ class ConfDict(dict): ''' Return an iterable of meta field names defined for a key. ''' return self._meta.get(key, {}).keys() - -class ConfigDict(dict): - ''' A dict-subclass with some extras: You can access keys like attributes. - Uppercase attributes create new ConfigDicts and act as name-spaces. - Other missing attributes return None. Calling a ConfigDict updates its - values and returns itself. - - >>> cfg = ConfigDict() - >>> cfg.Namespace.value = 5 - >>> cfg.OtherNamespace(a=1, b=2) - >>> cfg - {'Namespace': {'value': 5}, 'OtherNamespace': {'a': 1, 'b': 2}} - ''' - + # Deprecated ConfigDict features def __getattr__(self, key): + depr('Attribute access is deprecated.') #0.12 if key not in self and key[0].isupper(): self[key] = ConfigDict() return self.get(key) def __setattr__(self, key, value): + if key in self.__slots__: + return dict.__setattr__(self, key, value) + depr('Attribute assignment is deprecated.') #0.12 if hasattr(dict, key): raise AttributeError('Read-only attribute.') if key in self and self[key] and isinstance(self[key], ConfigDict): @@ -2081,10 +2100,12 @@ class ConfigDict(dict): if key in self: del self[key] def __call__(self, *a, **ka): - for key, value in dict(*a, **ka).items(): setattr(self, key, value) + depr('Calling ConfDict is deprecated. Use the update() method.') #0.12 + self.update(*a, **ka) return self + class AppStack(list): """ A stack-like list. Calling it returns the head of the stack. """ diff --git a/test/test_config.py b/test/test_config.py index b8f94e3..b16ddc5 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,9 +1,9 @@ import unittest -from bottle import ConfDict +from bottle import ConfigDict class TestConfDict(unittest.TestCase): def test_write(self): - c = ConfDict() + c = ConfigDict() c['key'] = 'value' self.assertEqual(c['key'], 'value') self.assertTrue('key' in c) @@ -11,27 +11,35 @@ class TestConfDict(unittest.TestCase): self.assertEqual(c['key'], 'value2') def test_update(self): - c = ConfDict() + c = ConfigDict() c['key'] = 'value' c.update(key='value2', key2='value3') self.assertEqual(c['key'], 'value2') self.assertEqual(c['key2'], 'value3') def test_namespaces(self): - c = ConfDict() + c = ConfigDict() c.update('a.b', key='value') self.assertEqual(c['a.b.key'], 'value') self.assertEqual(c['a.b.key'], 'value') def test_meta(self): - c = ConfDict() + c = ConfigDict() c.meta_set('bool', 'filter', bool) c.meta_set('int', 'filter', int) c['bool'] = 'I am so true!' c['int'] = '6' self.assertTrue(c['bool'] is True) - self.assertEquals(c['int'], 6) + self.assertEquals(c['int'], 6) self.assertRaises(ValueError, lambda: c.update(int='not an int')) + + def test_load_dict(self): + c = ConfigDict() + d = dict(a=dict(b=dict(foo=5, bar=6), baz=7)) + c.load_dict(d) + self.assertEquals(c['a.b.foo'], 5) + self.assertEquals(c['a.b.bar'], 6) + self.assertEquals(c['a.baz'], 7) if __name__ == '__main__': #pragma: no cover unittest.main() diff --git a/test/test_mount.py b/test/test_mount.py index d268d42..cf0684b 100644 --- a/test/test_mount.py +++ b/test/test_mount.py @@ -23,10 +23,10 @@ class TestAppMounting(ServerTestBase): def test_mount_meta(self): self.app.mount('/test/', self.subapp) self.assertEqual( - self.app.routes[0].config.mountpoint['prefix'], + self.app.routes[0].config['mountpoint.prefix'], '/test/') self.assertEqual( - self.app.routes[0].config.mountpoint['target'], + self.app.routes[0].config['mountpoint.target'], self.subapp) def test_no_slash_prefix(self): |