summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2013-07-16 19:53:11 +0200
committerMarcel Hellkamp <marc@gsites.de>2013-07-16 20:31:20 +0200
commitf493648bea751e621fe4469c71e52c37454a849d (patch)
treef46eb769fe8249c5cb74ffb28d3ae9cb5ed2e5ac
parenta00a438d6bb084528a5a0f947620dcdb404febb7 (diff)
downloadbottle-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.py111
-rw-r--r--test/test_config.py20
-rw-r--r--test/test_mount.py4
3 files changed, 82 insertions, 53 deletions
diff --git a/bottle.py b/bottle.py
index 26c1b57..c0d1dfe 100644
--- a/bottle.py
+++ b/bottle.py
@@ -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):