#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" :py:mod:`weblayer.settings` provides :py:class:`RequirableSettings`, an
implementation of :py:class:`~weblayer.interfaces.ISettings` that allows you
to (optionally) declare the settings that your application requires.
For example, say your application code needs to know the value of a
particular webservice api key. You can require it by calling the
:py:func:`require_setting` method (by convention at the top of a module, so
the requirement is clear)::
>>> require_setting('api_key')
Or by decorating a function or method with :py:func:`require`::
>>> class Foo(object):
... @require('api_key')
... def foo(self):
... pass
...
...
>>> @require('api_key')
... def foo(self): pass
...
Then, once we've executed a `venusian scan`_ (which we fake here: see
`./tests/test_settings.py`_ for real integration tests)::
>>> settings = RequirableSettings()
>>> def mock_require_setting(*args, **kwargs):
... settings._require(*args, **kwargs) # never call this directly!
...
>>> def mock_override_setting(*args, **kwargs):
... settings._override(*args, **kwargs) # never call this directly!
...
>>> mock_require_setting('api_key')
You can call the :py:class:`RequirableSettings` instance with a dictionary
of settings provided by the user / your application. If you pass in a value
for ``api_key``, great, otherwise, you'll get a ``KeyError``::
>>> settings({'api_key': '123'})
>>> settings['api_key']
'123'
>>> settings({})
Traceback (most recent call last):
...
KeyError: u'Required setting `api_key` () is missing'
You can specify default values and help strings ala::
>>> mock_require_setting('baz', default='blah', help=u'what is this?')
>>> settings({'api_key': '123'})
>>> settings['baz']
'blah'
You can't require the same setting twice with different values::
>>> mock_require_setting('baz', default='something else')
Traceback (most recent call last):
...
KeyError: u'baz is already defined'
Unless you explicitly use :py:func:`override_setting` (also available as the
:py:func:`override` decorator):
>>> mock_override_setting('baz', default='something else')
>>> settings({'api_key': '123'})
>>> settings['baz']
'something else'
.. _`venusian scan`: http://docs.repoze.org/venusian/
.. _`./tests/test_settings.py`: http://github.com/thruflo/weblayer/tree/master/src/weblayer/tests/test_settings.py
"""
__all__ = [
'RequirableSettings',
'require_setting',
'require',
'override_setting',
'override'
]
import inspect
import venusian
from UserDict import DictMixin
from zope.interface import implements
from interfaces import ISettings
_CATEGORY = 'weblayer'
_HANGER_NAME = '__weblayer_require_settings_venusian_hanger__'
[docs]class RequirableSettings(object, DictMixin):
""" Utility that provides dictionary-like access to application settings.
Do not use the ``_require`` and ``_override`` methods directly. Instead,
use the :py:func:`require_setting` and :py:func:`override_setting`
functions or the :py:func:`require` and :py:func:`override` decorators.
Note that the
"""
implements(ISettings)
def __init__(self, packages=None, extra_categories=None):
""" If ``packages``, run a `venusian scan`_.
.. _`venusian scan`: http://docs.repoze.org/venusian/
"""
self.__required_settings__ = {}
self._items = {}
if packages:
categories = [_CATEGORY]
if extra_categories is not None:
categories.extend(extra_categories)
scanner = venusian.Scanner(settings=self)
for item in packages:
scanner.scan(item, categories=categories)
def __getitem__(self, name):
""" Get item::
>>> settings = RequirableSettings()
>>> settings({'a': 'foobar'})
>>> settings['a']
'foobar'
"""
return self._items.__getitem__(name)
def __setitem__(self, name, value):
""" Set item::
>>> settings = RequirableSettings()
>>> settings['a'] = 'baz'
>>> settings['a']
'baz'
"""
return self._items.__setitem__(name, value)
def __delitem__(self, name):
""" Delete item::
>>> settings = RequirableSettings()
>>> settings({'a': 'foobar'})
>>> settings['a']
'foobar'
>>> del settings['a']
>>> settings['a']
Traceback (most recent call last):
...
KeyError: 'a'
"""
return self._items.__delitem__(name)
[docs] def keys(self):
""" Return keys::
>>> settings = RequirableSettings()
>>> settings({'a': None, 'b': None})
>>> settings.keys()
['a', 'b']
"""
return self._items.keys()
def __repr__(self):
""" Represent class and items::
>>> settings = RequirableSettings()
>>> settings
<weblayer.settings.RequirableSettings {}>
>>> settings({'a': 'foobar', 'b': ''})
>>> settings
<weblayer.settings.RequirableSettings {'a': 'foobar', 'b': ''}>
"""
return u'<weblayer.settings.RequirableSettings %s>' % (self._items)
def _require(self, name, default=None, help=u''):
""" Require an application setting.
Defaults to ``None``::
>>> settings = RequirableSettings()
>>> settings._require('a')
>>> settings.__required_settings__['a']
(None, u'')
Unless passed in::
>>> settings._require('b', default='b', help=u'help msg')
>>> settings.__required_settings__['b']
('b', u'help msg')
You can call ``require`` as many times as you like::
>>> settings = RequirableSettings()
>>> settings._require('a')
>>> settings._require('a')
>>> settings._require('a')
>>> settings.__required_settings__
{'a': (None, u'')}
But you can't require the same setting *with different values*::
>>> settings._require('a', default='a')
Traceback (most recent call last):
...
KeyError: u'a is already defined'
Whether that means the default value (above) or the help message::
>>> settings._require('a', help=u'elephants')
Traceback (most recent call last):
...
KeyError: u'a is already defined'
"""
if name in self.__required_settings__:
if self.__required_settings__[name] != (default, help):
raise KeyError(u'%s is already defined' % name)
self.__required_settings__[name] = (default, help)
def _override(self, name, default=None, help=u''):
""" Require a setting regardless of whether it has already been
required or not.
Defaults to ``None``::
>>> settings = RequirableSettings()
>>> settings._override('a')
>>> settings.__required_settings__['a']
(None, u'')
Unless passed in::
>>> settings._override('b', default='b', help=u'help msg')
>>> settings.__required_settings__['b']
('b', u'help msg')
With ``override``, you can require the same settings twice with
different values::
>>> settings._override('a', default='a')
>>> settings.__required_settings__['a']
('a', u'')
>>> settings._override('a', help=u'elephants')
>>> settings.__required_settings__['a']
(None, u'elephants')
"""
self.__required_settings__[name] = (default, help)
def __call__(self, items):
""" ``items`` are checked against ``self.__required_settings__``.
If any required settings are missing, if they were declared
with a default value, the setting is set up with the
default value::
>>> settings = RequirableSettings()
>>> reqs = {'a': ('a', u'help msg a'), 'b': ('b', u'help msg b')}
>>> settings.__required_settings__ = reqs
>>> settings()
Traceback (most recent call last):
...
TypeError: __call__() takes exactly 2 arguments (1 given)
>>> settings({'a': 'foobar'})
>>> settings['a']
'foobar'
>>> settings['b']
'b'
Otherwise we throw a ``KeyError``.
>>> reqs = {'a': (None, u'help msg a'), 'b': ('b', u'help msg b')}
>>> settings.__required_settings__ = reqs
>>> settings({})
Traceback (most recent call last):
...
KeyError: u'Required setting `a` (help msg a) is missing'
"""
missing = []
for k, v in self.__required_settings__.iteritems():
if not k in items:
default = v[0]
if default is not None:
self[k] = default
else:
msg = u'Required setting `%s` (%s) is missing' % (k, v[1])
missing.append(msg)
if missing:
raise KeyError(u', '.join(missing))
for k, v in items.iteritems():
self[k] = v
def _attach_callback(
name,
default=None,
help=u'',
category=_CATEGORY,
override=False
):
""" Hangs a callback to ``_require`` or ``_override`` off the module that
this method is called from.
Attaches the callback manually, rather than using `venusian.attach`_ so
that :ref:`weblayer` doesn't depend on a
`CPython implementation detail`_.
.. `venusian.attach`_: http://svn.repoze.org/venusian/trunk/venusian/__init__.py
.. _`CPython implementation detail`: http://docs.python.org/library/sys.html#sys._getframe
"""
# get the module we're being called in
calling_mod = None
for item in inspect.stack():
if item[3] == '<module>':
calling_mod = inspect.getmodule(item[0])
break
# ignore when `None` (e.g.: if called from a doctest)
if calling_mod is None:
return
# make sure it has a harmless function at
# `calling_mod.__weblayer_require_settings_venusian_hanger__`
def _required_settings_hanger(): pass
if not hasattr(calling_mod, _HANGER_NAME):
setattr(calling_mod, _HANGER_NAME, _required_settings_hanger)
# defer the real business
def callback(scanner, *args):
settings = scanner.settings
method = override and settings._override or settings._require
return method(name, default=default, help=help)
hanger = getattr(calling_mod, _HANGER_NAME)
categories = getattr(hanger, venusian.ATTACH_ATTR, {})
callbacks = categories.setdefault(category, [])
callbacks.append(callback)
setattr(hanger, venusian.ATTACH_ATTR, categories)
[docs]def require_setting(name, default=None, help=u'', category=_CATEGORY):
""" Require a setting.
"""
_attach_callback(
name,
default=default,
help=help,
category=category,
override=False
)
[docs]def override_setting(name, default=None, help=u'', category=_CATEGORY):
""" Override a setting.
"""
_attach_callback(
name,
default=default,
help=help,
category=category,
override=True
)
[docs]def require(name, default=None, help=u'', category=_CATEGORY):
""" Decorator to require a setting.
"""
def wrap(wrapped):
""" Called at decoration time. Requires the setting and returns the
unchanged wrapped function / method or class.
"""
require_setting(name, default=default, help=help, category=category)
return wrapped
return wrap
[docs]def override(name, default=None, help=u'', category=_CATEGORY):
""" Decorator to override a setting.
"""
def wrap(wrapped):
""" Called at decoration time. Overrides the setting and returns the
unchanged wrapped function / method or class.
"""
override_setting(name, default=default, help=help, category=category)
return wrapped
return wrap