diff options
author | Marcel Hellkamp <marc@gsites.de> | 2014-02-19 22:02:30 +0100 |
---|---|---|
committer | Marcel Hellkamp <marc@gsites.de> | 2014-02-19 22:26:09 +0100 |
commit | cf029085ad2525a42ba995159df72df41f283e0e (patch) | |
tree | 461d48141458a829562c480d70582f5149db370a | |
parent | 69b8f85d979ad461c7df14366cadd1a5185fc88f (diff) | |
download | bottle-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.py | 121 | ||||
-rw-r--r-- | docs/configuration.rst | 8 | ||||
-rw-r--r-- | test/test_configdict.py | 148 |
3 files changed, 189 insertions, 88 deletions
@@ -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() + |