Permalink
Browse files

Merge branch 'release/0.4'

  • Loading branch information...
2 parents 6569982 + 96130ae commit 53c5de7e7e9d79b285565ff515ce39d6bdc02f18 @jezdez jezdez committed Aug 25, 2011
Showing with 989 additions and 170 deletions.
  1. +1 −0 AUTHORS
  2. +27 −0 LICENSE
  3. +3 −1 MANIFEST.in
  4. +43 −119 README.rst
  5. +63 −32 appconf.py
  6. +130 −0 docs/Makefile
  7. +48 −0 docs/changelog.rst
  8. +220 −0 docs/conf.py
  9. +12 −0 docs/index.rst
  10. +20 −0 docs/installation.rst
  11. +170 −0 docs/make.bat
  12. +76 −0 docs/reference.rst
  13. +95 −0 docs/usage.rst
  14. +6 −5 setup.py
  15. +30 −2 tests/testapp/models.py
  16. +37 −11 tests/testapp/tests.py
  17. +8 −0 tests/tox.ini
View
@@ -0,0 +1 @@
+Jannis Leidel <jannis@leidel.info>
View
27 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.
View
@@ -1 +1,3 @@
-include README.rst
+include README.rst
+include LICENSE
+include AUTHORS
View
@@ -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
@@ -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'
View
@@ -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
@@ -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'):
@@ -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):
@@ -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
Oops, something went wrong.

0 comments on commit 53c5de7

Please sign in to comment.