Skip to content

Commit

Permalink
Merge branch 'release/0.4'
Browse files Browse the repository at this point in the history
  • Loading branch information
jezdez committed Aug 25, 2011
2 parents 6569982 + 96130ae commit 53c5de7
Show file tree
Hide file tree
Showing 17 changed files with 989 additions and 170 deletions.
1 change: 1 addition & 0 deletions AUTHORS
@@ -0,0 +1 @@
Jannis Leidel <jannis@leidel.info>
27 changes: 27 additions & 0 deletions LICENSE
@@ -0,0 +1,27 @@
Copyright (c) 2011, Jannis Leidel and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

* Neither the name of django-appconf nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4 changes: 3 additions & 1 deletion MANIFEST.in
@@ -1 +1,3 @@
include README.rst
include README.rst
include LICENSE
include AUTHORS
162 changes: 43 additions & 119 deletions README.rst
@@ -1,10 +1,16 @@
django-appconf
==============

An app configuration object to be used for handling configuration
defaults of packaged apps gracefully. Say you have an app called ``myapp``
and want to define a few defaults, and refer to the defaults easily in the
apps code. Add a ``settings.py`` to your app's models.py::
A helper class for handling configuration defaults of packaged Django
apps gracefully.

Overview
--------

Say you have an app called ``myapp`` with a few defaults, which you want
to refer to in the app's code without repeating yourself all the time.
``appconf`` provides a simple class to implement those defaults. Simply add
something like the following code somewhere in your app files::

from appconf import AppConf

Expand All @@ -14,132 +20,50 @@ apps code. Add a ``settings.py`` to your app's models.py::
"two",
)

class Meta:
app_label = 'myapp'

The settings are initialized with the app label of where the setting is
located at. E.g. if your ``models.py`` is in the ``myapp`` package,
the prefix of the settings will be ``MYAPP``.

The ``MyAppConf`` class will automatically look at Django's
global setting to determine each of the settings. E.g. adding this to
your site's ``settings.py`` will set the ``SETTING_1`` app setting
accordingly::

MYAPP_SETTING_1 = "uno"

Usage
-----

Instead of using ``from django.conf import settings`` as you would
usually do, you can **optionally** switch to using your apps own
settings module to access the settings::

from myapp.models import MyAppConf

myapp_settings = MyAppConf()

print myapp_settings.MYAPP_SETTING_1
.. note::

``AppConf`` class automatically work as proxies for the other
settings, which aren't related to the app. For example the following
code is perfectly valid::
``AppConf`` classes depend on being imported during startup of the Django
process. Even though there are multiple modules loaded automatically,
only the ``models`` modules (usually the ``models.py`` file of your
app) are guaranteed to be loaded at startup. Therefore it's recommended
to put your ``AppConf`` subclass(es) there, too.

from myapp.models import MyAppConf
The settings are initialized with the capitalized app label of where the
setting is located at. E.g. if your ``models.py`` with the ``AppConf`` class
is in the ``myapp`` package, the prefix of the settings will be ``MYAPP``.

settings = MyAppConf()
You can override the default prefix by specifying a ``prefix`` attribute of
an inner ``Meta`` class::

if "myapp" in settings.INSTALLED_APPS:
print "yay, myapp is installed!"

In case you want to set some settings ad-hoc, you can simply pass
the value when instanciating the ``AppConf`` class::

from myapp.models import MyAppConf

settings = MyAppConf(SETTING_1='something completely different')

if 'different' in settings.MYAPP_SETTINGS_1:
print 'yay, I'm different!'

Custom handling
---------------

Each of the settings can be individually configured with callbacks.
For example, in case a value of a setting depends on other settings
or other dependencies. The following example sets one setting to a
different value depending on a global setting::

from django.conf import settings
from appconf import AppConf

class MyCustomAppConf(AppConf):
ENABLED = True

def configure_enabled(self, value):
return value and not self.DEBUG
class MyAppConf(AppConf):
SETTING_1 = "one"
SETTING_2 = (
"two",
)

The value of ``MYAPP_ENABLED`` will vary depending on the
value of the global ``DEBUG`` setting.
class Meta:
prefix = 'acme'

Each of the app settings can be customized by providing
a method ``configure_<lower_setting_name>`` that takes the default
value as defined in the class attributes as the only parameter.
The method needs to return the value to be use for the setting in
question.
The ``MyAppConf`` class will automatically look at Django's global settings
to determine if you've overridden it. For example, adding this to your site's
``settings.py`` would override ``SETTING_1`` of the above ``MyAppConf``::

After each of the ``_configure`` method have be called, the ``AppConf``
class will additionally call a main ``configure`` method, which can
be used to do any further custom configuration handling, e.g. if multiple
settings depend on each other. For that a ``configured_data`` dictionary
is provided in the setting instance::
MYAPP_SETTING_1 = "uno"

In case you want to use a different settings object instead of the default
``'django.conf.settings'``, set the ``holder`` attribute of the inner
``Meta`` class to a dotted import path::

from django.conf import settings
from appconf import AppConf

class MyCustomAppConf(AppConf):
ENABLED = True
MODE = 'development'

def configure_enabled(self, value):
return value and not self.DEBUG

def configure(self):
mode = self.configured_data['MODE']
enabled = self.configured_data['ENABLED']
if not enabled and mode != 'development':
print "WARNING: app not enabled in %s mode!" % mode

Changelog
---------

0.3 (2011-08-23)
^^^^^^^^^^^^^^^^

* Added tests with 100% coverage.

* Added ability to subclass ``Meta`` classes.

* Fixed various bugs with subclassing and configuration in subclasses.

0.2.2 (2011-08-22)
^^^^^^^^^^^^^^^^^^

* Fixed another issue in the ``configure()`` API.

0.2.1 (2011-08-22)
^^^^^^^^^^^^^^^^^^

* Fixed minor issue in ``configure()`` API.

0.2 (2011-08-22)
^^^^^^^^^^^^^^^^

* Added ``configure()`` API to ``AppConf`` class which is called after
configuring each setting.

0.1 (2011-08-22)
^^^^^^^^^^^^^^^^
class MyAppConf(AppConf):
SETTING_1 = "one"
SETTING_2 = (
"two",
)

* First public release.
class Meta:
prefix = 'acme'
holder = 'acme.conf.settings'
95 changes: 63 additions & 32 deletions appconf.py
@@ -1,18 +1,22 @@
import sys

# following PEP 386, versiontools will pick it up
__version__ = (0, 3, 0, "final", 0)
__version__ = (0, 4, 0, "final", 0)


class AppConfOptions(object):

def __init__(self, meta, app_label=None):
self.app_label = app_label
def __init__(self, meta, prefix=None):
self.prefix = prefix
self.holder_path = getattr(meta, 'holder', 'django.conf.settings')
self.holder = import_attribute(self.holder_path)
self.proxy = getattr(meta, 'proxy', False)
self.configured_data = {}

def prefixed_name(self, name):
if name.startswith(self.app_label.upper()):
if name.startswith(self.prefix.upper()):
return name
return "%s_%s" % (self.app_label.upper(), name.upper())
return "%s_%s" % (self.prefix.upper(), name.upper())

def contribute_to_class(self, cls, name):
cls._meta = self
Expand All @@ -38,31 +42,37 @@ def __new__(cls, name, bases, attrs):
attr_meta = type('Meta', (object,), {})
meta = getattr(new_class, 'Meta', None)

app_label = getattr(meta, 'app_label', None)
if app_label is None:
# Figure out the app_label by looking one level up.
prefix = getattr(meta, 'prefix', getattr(meta, 'app_label', None))
if prefix is None:
# Figure out the prefix by looking one level up.
# For 'django.contrib.sites.models', this would be 'sites'.
model_module = sys.modules[new_class.__module__]
app_label = model_module.__name__.split('.')[-2]
prefix = model_module.__name__.split('.')[-2]

new_class.add_to_class('_meta', AppConfOptions(meta, app_label))
new_class.add_to_class('_meta', AppConfOptions(meta, prefix))
new_class.add_to_class('Meta', attr_meta)

for parent in parents[::-1]:
if hasattr(parent, '_meta'):
new_class._meta.names.update(parent._meta.names)
new_class._meta.defaults.update(parent._meta.defaults)
new_class._meta.configured_data.update(parent._meta.configured_data)

for name in filter(lambda name: name == name.upper(), attrs):
prefixed_name = new_class._meta.prefixed_name(name)
new_class._meta.names[name] = prefixed_name
new_class._meta.defaults[prefixed_name] = attrs.pop(name)

# Add all attributes to the class.
for obj_name, obj in attrs.items():
new_class.add_to_class(obj_name, obj)
for name, value in attrs.items():
new_class.add_to_class(name, value)

return new_class._configure()
new_class._configure()
for name, value in new_class._meta.configured_data.iteritems():
prefixed_name = new_class._meta.prefixed_name(name)
setattr(new_class._meta.holder, prefixed_name, value)
new_class.add_to_class(name, value)
return new_class

def add_to_class(cls, name, value):
if hasattr(value, 'contribute_to_class'):
Expand All @@ -71,25 +81,37 @@ def add_to_class(cls, name, value):
setattr(cls, name, value)

def _configure(cls):
from django.conf import settings
# the ad-hoc settings class instance used to configure each value
obj = cls()
obj.configured_data = dict()
for name, prefixed_name in obj._meta.names.iteritems():
default_value = obj._meta.defaults.get(prefixed_name)
value = getattr(settings, prefixed_name, default_value)
value = getattr(obj._meta.holder, prefixed_name, default_value)
callback = getattr(obj, "configure_%s" % name.lower(), None)
if callable(callback):
value = callback(value)
obj.configured_data[name] = value
obj.configured_data = obj.configure()

# Finally, set the setting in the global setting object
for name, value in obj.configured_data.iteritems():
prefixed_name = obj._meta.prefixed_name(name)
setattr(settings, prefixed_name, value)

return cls
cls._meta.configured_data[name] = value
cls._meta.configured_data = obj.configure()


def import_attribute(import_path, exception_handler=None):
from django.utils.importlib import import_module
module_name, object_name = import_path.rsplit('.', 1)
try:
module = import_module(module_name)
except: # pragma: no cover
if callable(exception_handler):
exctype, excvalue, tb = sys.exc_info()
return exception_handler(import_path, exctype, excvalue, tb)
else:
raise
try:
return getattr(module, object_name)
except: # pragma: no cover
if callable(exception_handler):
exctype, excvalue, tb = sys.exc_info()
return exception_handler(import_path, exctype, excvalue, tb)
else:
raise


class AppConf(object):
Expand All @@ -100,29 +122,38 @@ class AppConf(object):
__metaclass__ = AppConfMetaClass

def __init__(self, **kwargs):
from django.conf import settings
self._holder = settings
for name, value in kwargs.iteritems():
setattr(self, self._meta.prefixed_name(name), value)
setattr(self, name, value)

def __dir__(self):
return sorted(list(set(dir(self._holder))))
return sorted(list(set(self._meta.names.keys())))

# For instance access..
@property
def configured_data(self):
return self._meta.configured_data

# For Python < 2.6:
@property
def __members__(self):
return self.__dir__()

def __getattr__(self, name):
return getattr(self._holder, name)
if self._meta.proxy:
return getattr(self._meta.holder, name)
raise AttributeError("%s not found. Use '%s' instead." %
(name, self._meta.holder_path))

def __setattr__(self, name, value):
if name == name.upper():
return setattr(self._holder, name, value)
setattr(self._meta.holder,
self._meta.prefixed_name(name), value)
object.__setattr__(self, name, value)

def configure(self):
"""
Hook for doing any extra configuration.
Hook for doing any extra configuration, returning a dictionary
containing the configured data.
"""
return self.configured_data

0 comments on commit 53c5de7

Please sign in to comment.