summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2014-02-19 22:02:30 +0100
committerMarcel Hellkamp <marc@gsites.de>2014-02-19 22:26:09 +0100
commitcf029085ad2525a42ba995159df72df41f283e0e (patch)
tree461d48141458a829562c480d70582f5149db370a
parent69b8f85d979ad461c7df14366cadd1a5185fc88f (diff)
downloadbottle-cf029085ad2525a42ba995159df72df41f283e0e.tar.gz
Fix #588: `ConfigDict` namespaces break route options
This is a big and nasty workaround, but it solves the issue...
-rw-r--r--bottle.py121
-rw-r--r--docs/configuration.rst8
-rw-r--r--test/test_configdict.py148
3 files changed, 189 insertions, 88 deletions
diff --git a/bottle.py b/bottle.py
index b3f8bf7..b38eda5 100644
--- a/bottle.py
+++ b/bottle.py
@@ -477,7 +477,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().load_dict(config)
+ self.config = ConfigDict().load_dict(config, make_namespaces=True)
def __call__(self, *a, **ka):
depr("Some APIs changed to return Route() instances instead of"\
@@ -1994,10 +1994,78 @@ class WSGIHeaderDict(DictMixin):
class ConfigDict(dict):
''' A dict-like configuration storage with additional support for
namespaces, validators, meta-data, on_change listeners and more.
- '''
+ This storage is optimized for fast read access. Retrieving a key
+ or using non-altering dict methods (e.g. `dict.get()`) has no overhead
+ compared to a native dict.
+ '''
__slots__ = ('_meta', '_on_change')
+ class Namespace(DictMixin):
+
+ def __init__(self, config, namespace):
+ self._config = config
+ self._prefix = namespace
+
+ def __getitem__(self, key):
+ depr('Accessing namespaces as dicts is discouraged. '
+ 'Only use flat item access: '
+ 'cfg["names"]["pace"]["key"] -> cfg["name.space.key"]') #0.12
+ return self._config[self._prefix + '.' + key]
+
+ def __setitem__(self, key, value):
+ self._config[self._prefix + '.' + key] = value
+
+ def __delitem__(self, key):
+ del self._config[self._prefix + '.' + key]
+
+ def __iter__(self):
+ ns_prefix = self._prefix + '.'
+ for key in self._config:
+ ns, dot, name = key.rpartition('.')
+ if ns == self._prefix and name:
+ yield name
+
+ def keys(self): return [x for x in self]
+ def __len__(self): return len(self.keys())
+ def __contains__(self, key): return self._prefix + '.' + key in self._config
+ def __repr__(self): return '<Config.Namespace %s.*>' % self._prefix
+ def __str__(self): return '<Config.Namespace %s.*>' % self._prefix
+
+ # 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.Namespace(self._config, self._prefix + '.' + key)
+ if key not in self and key.startswith('__'):
+ raise AttributeError(key)
+ return self.get(key)
+
+ def __setattr__(self, key, value):
+ if key in ('_config', '_prefix'):
+ self.__dict__[key] = value
+ return
+ depr('Attribute assignment is deprecated.') #0.12
+ if hasattr(DictMixin, key):
+ raise AttributeError('Read-only attribute.')
+ if key in self and self[key] and isinstance(self[key], self.__class__):
+ raise AttributeError('Non-empty namespace attribute.')
+ self[key] = value
+
+ def __delattr__(self, key):
+ if key in self:
+ val = self.pop(key)
+ if isinstance(val, self.__class__):
+ prefix = key + '.'
+ for key in self:
+ if key.startswith(prefix):
+ del self[prefix+key]
+
+ def __call__(self, *a, **ka):
+ depr('Calling ConfDict is deprecated. Use the update() method.') #0.12
+ self.update(*a, **ka)
+ return self
+
def __init__(self, *a, **ka):
self._meta = {}
self._on_change = lambda name, value: None
@@ -2021,22 +2089,28 @@ class ConfigDict(dict):
self[key] = value
return self
- def load_dict(self, source, namespace=''):
- ''' Load values from a dictionary structure. Nesting can be used to
+ def load_dict(self, source, namespace='', make_namespaces=False):
+ ''' Import values from a dictionary structure. Nesting can be used to
represent namespaces.
- >>> c.load_dict({'some': {'namespace': {'key': 'value'} } })
- {'some.namespace.key': 'value'}
+ >>> ConfigDict().load_dict({'name': {'space': {'key': 'value'}}})
+ {'name.space.key': 'value'}
'''
- for key, value in source.items():
- if isinstance(key, str):
- nskey = (namespace + '.' + key).strip('.')
+ stack = [(namespace, source)]
+ while stack:
+ prefix, source = stack.pop()
+ if not isinstance(source, dict):
+ raise TypeError('Source is not a dict (r)' % type(key))
+ for key, value in source.items():
+ if not isinstance(key, str):
+ raise TypeError('Key is not a string (%r)' % type(key))
+ full_key = prefix + '.' + key if prefix else key
if isinstance(value, dict):
- self.load_dict(value, namespace=nskey)
+ stack.append((full_key, value))
+ if make_namespaces:
+ self[full_key] = self.Namespace(self, full_key)
else:
- self[nskey] = value
- else:
- raise TypeError('Key has type %r (not a string)' % type(key))
+ self[full_key] = value
return self
def update(self, *a, **ka):
@@ -2053,10 +2127,12 @@ class ConfigDict(dict):
def setdefault(self, key, value):
if key not in self:
self[key] = value
+ return self[key]
def __setitem__(self, key, value):
if not isinstance(key, str):
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
@@ -2064,9 +2140,12 @@ class ConfigDict(dict):
dict.__setitem__(self, key, value)
def __delitem__(self, key):
- self._on_change(key, None)
dict.__delitem__(self, key)
+ def clear(self):
+ for key in self:
+ del self[key]
+
def meta_get(self, key, metafield, default=None):
''' Return the value of a meta field for a key. '''
return self._meta.get(key, {}).get(metafield, default)
@@ -2086,7 +2165,9 @@ class ConfigDict(dict):
def __getattr__(self, key):
depr('Attribute access is deprecated.') #0.12
if key not in self and key[0].isupper():
- self[key] = ConfigDict()
+ self[key] = self.Namespace(self, key)
+ if key not in self and key.startswith('__'):
+ raise AttributeError(key)
return self.get(key)
def __setattr__(self, key, value):
@@ -2095,12 +2176,18 @@ class ConfigDict(dict):
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):
+ if key in self and self[key] and isinstance(self[key], self.Namespace):
raise AttributeError('Non-empty namespace attribute.')
self[key] = value
def __delattr__(self, key):
- if key in self: del self[key]
+ if key in self:
+ val = self.pop(key)
+ if isinstance(val, self.Namespace):
+ prefix = key + '.'
+ for key in self:
+ if key.startswith(prefix):
+ del self[prefix+key]
def __call__(self, *a, **ka):
depr('Calling ConfDict is deprecated. Use the update() method.') #0.12
diff --git a/docs/configuration.rst b/docs/configuration.rst
index b6d1d21..8c820ba 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -4,9 +4,6 @@ Configuration (DRAFT)
.. currentmodule:: bottle
-.. warning::
- This is a draft for a new API. `Tell us <mailto:bottlepy@googlegroups.com>`_ what you think.
-
Bottle applications can store their configuration in :attr:`Bottle.config`, a dict-like object and central place for application specific settings. This dictionary controls many aspects of the framework, tells (newer) plugins what to do, and can be used to store your own configuration as well.
Configuration Basics
@@ -68,10 +65,13 @@ or just don't want to hack python module files just to change the database port.
.. code-block:: ini
+ [bottle]
+ debug = True
+
[sqlite]
db = /tmp/test.db
commit = auto
-
+
[myapp]
admin_user = defnull
diff --git a/test/test_configdict.py b/test/test_configdict.py
index 227ea63..654ad7a 100644
--- a/test/test_configdict.py
+++ b/test/test_configdict.py
@@ -1,67 +1,81 @@
-import unittest
-from bottle import ConfigDict
-
-class TestConfigDict(unittest.TestCase):
- def test_isadict(self):
- """ ConfigDict should behaves like a normal dict. """
- # It is a dict-subclass, so this kind of pointless, but it doen't hurt.
- d, m = dict(a=5), ConfigDict(a=5)
- d['key'], m['key'] = 'value', 'value'
- d['k2'], m['k2'] = 'v1', 'v1'
- d['k2'], m['k2'] = 'v2', 'v2'
- self.assertEqual(d.keys(), m.keys())
- self.assertEqual(list(d.values()), list(m.values()))
- self.assertEqual(d.get('key'), m.get('key'))
- self.assertEqual(d.get('cay'), m.get('cay'))
- self.assertEqual(list(iter(d)), list(iter(m)))
- self.assertEqual([k for k in d], [k for k in m])
- self.assertEqual(len(d), len(m))
- self.assertEqual('key' in d, 'key' in m)
- self.assertEqual('cay' in d, 'cay' in m)
- self.assertRaises(KeyError, lambda: m['cay'])
-
- def test_attr_access(self):
- """ ConfigDict allow attribute access to keys. """
- c = ConfigDict()
- c.test = 5
- self.assertEqual(5, c.test)
- self.assertEqual(5, c['test'])
- c['test'] = 6
- self.assertEqual(6, c.test)
- self.assertEqual(6, c['test'])
- del c.test
- self.assertTrue('test' not in c)
- self.assertEqual(None, c.test)
-
- def test_namespaces(self):
- """ Access to a non-existent uppercase attribute creates a new namespace. """
- c = ConfigDict()
- self.assertEqual(c.__class__, c.Name.Space.__class__)
- c.Name.Space.value = 5
- self.assertEqual(5, c.Name.Space.value)
- self.assertTrue('value' in c.Name.Space)
- self.assertTrue('Space' in c.Name)
- self.assertTrue('Name' in c)
- self.assertTrue('value' not in c)
- # Overwriting namespaces is not allowed.
- self.assertRaises(AttributeError, lambda: setattr(c, 'Name', 5))
- # Overwriting methods defined on dict is not allowed.
- self.assertRaises(AttributeError, lambda: setattr(c, 'keys', 5))
- # but not with the dict API:
- c['Name'] = 5
- self.assertEqual(5, c.Name)
-
- def test_call(self):
- """ Calling updates and returns the dict. """
- c = ConfigDict()
- self.assertEqual(c, c(a=1))
- self.assertTrue('a' in c)
- self.assertEqual(1, c.a)
-
-
-
-
-
-if __name__ == '__main__': #pragma: no cover
- unittest.main()
-
+import unittest
+from bottle import ConfigDict
+
+def setitem(d, key, value):
+ d[key] = value
+
+class TestConfigDict(unittest.TestCase):
+
+ def test_isadict(self):
+ """ ConfigDict should behaves like a normal dict. """
+ # It is a dict-subclass, so this kind of pointless, but it doen't hurt.
+ d, m = dict(a=5), ConfigDict(a=5)
+ d['key'], m['key'] = 'value', 'value'
+ d['k2'], m['k2'] = 'v1', 'v1'
+ d['k2'], m['k2'] = 'v2', 'v2'
+ self.assertEqual(d.keys(), m.keys())
+ self.assertEqual(list(d.values()), list(m.values()))
+ self.assertEqual(d.get('key'), m.get('key'))
+ self.assertEqual(d.get('cay'), m.get('cay'))
+ self.assertEqual(list(iter(d)), list(iter(m)))
+ self.assertEqual([k for k in d], [k for k in m])
+ self.assertEqual(len(d), len(m))
+ self.assertEqual('key' in d, 'key' in m)
+ self.assertEqual('cay' in d, 'cay' in m)
+ self.assertRaises(KeyError, lambda: m['cay'])
+
+ def test_attr_access(self):
+ """ ConfigDict allow attribute access to keys. """
+ c = ConfigDict()
+ c.test = 5
+ self.assertEqual(5, c.test)
+ self.assertEqual(5, c['test'])
+ c['test'] = 6
+ self.assertEqual(6, c.test)
+ self.assertEqual(6, c['test'])
+ del c.test
+ self.assertTrue('test' not in c)
+ self.assertEqual(None, c.test)
+
+ def test_namespaces(self):
+ """ Access to a non-existent uppercase attribute creates a new namespace. """
+ c = ConfigDict()
+ self.assertEqual(ConfigDict.Namespace, c.Name.Space.__class__)
+ c.Name.Space.value = 5
+ self.assertEqual(5, c.Name.Space.value)
+ self.assertTrue('value' in c.Name.Space)
+ self.assertTrue('Space' in c.Name)
+ self.assertTrue('Name' in c)
+ self.assertTrue('value' not in c)
+ # Overwriting namespaces is not allowed.
+ self.assertRaises(AttributeError, lambda: setattr(c, 'Name', 5))
+ # Overwriting methods defined on dict is not allowed.
+ self.assertRaises(AttributeError, lambda: setattr(c, 'keys', 5))
+ # but not with the dict API:
+ c['Name'] = 5
+ self.assertEqual(5, c.Name)
+
+ def test_call(self):
+ """ Calling updates and returns the dict. """
+ c = ConfigDict()
+ self.assertEqual(c, c(a=1))
+ self.assertTrue('a' in c)
+ self.assertEqual(1, c.a)
+
+ def test_issue588(self):
+ """`ConfigDict` namespaces break route options"""
+ c = ConfigDict()
+ c.load_dict({'a': {'b': 'c'}}, make_namespaces=True)
+ self.assertEqual('c', c['a.b'])
+ self.assertEqual('c', c['a']['b'])
+ self.assertEqual({'b': 'c'}, c['a'])
+
+ def test_string_key_only(self):
+ c = ConfigDict()
+ self.assertRaises(TypeError, lambda: setitem(c, 5, 6))
+ self.assertRaises(TypeError, lambda: c.load_dict({5:6}))
+
+
+if __name__ == '__main__': #pragma: no cover
+ unittest.main()
+