commit 775e1b9225c8e77e2beaae03eecf83ed6c03ebbd Author: Jannis Leidel Date: Sat Jul 21 15:56:04 2012 +0200 Initial version. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7cf0792 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.coverage +docs/_build +*.egg-info +test.db diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5bcb972 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python +python: + - "2.5" + - "2.6" + - "2.7" +before_install: + - export PIP_USE_MIRRORS=true + - export PIP_INDEX_URL=https://simple.crate.io/ +install: + - pip install -e . + - pip install -r requirements/tests.txt Django==$DJANGO +script: + - make test +env: + - DJANGO=1.3.1 + - DJANGO=1.4 +branches: + only: + - develop diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc6fcd9 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +test: + flake8 configurations --ignore=E501,E127,E128,E124 + coverage run --branch --source=configurations manage.py test configurations + coverage report --omit=configurations/test* diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..92a2ade --- /dev/null +++ b/README.rst @@ -0,0 +1,75 @@ +django-configurations +===================== + +.. image:: https://secure.travis-ci.org/jezdez/django-configurations.png + :alt: Build Status + :target: https://secure.travis-ci.org/jezdez/django-configurations + +django-configurations eases Django site configuration by relying +on the composability of Python classes. It extends the notion of +Django's module based settings loading with well established +object oriented programming patterns. + +Quickstart +---------- + +Install django-configurations:: + + pip install django-configurations + +Then subclass the included ``configurations.Settings`` class in your +project's ``settings.py`` or any other module you're using to store the +settings constants, e.g.:: + + from configurations import Settings + + class MySiteSettings(Settings): + DEBUG = True + +Set the ``DJANGO_CONFIGURATION`` environment variable to the name of the class +you just created, e.g. in bash:: + + export DJANGO_CONFIGURATION=MySettings + +and the ``DJANGO_SETTINGS_MODULE`` environment variable to the module +import path as usual, e.g. in bash:: + + export DJANGO_SETTINGS_MODULE=mysite.settings + +To enable Django to use your configuration you now have to modify your +``manage.py`` or ``wsgi.py`` script to use django-configurations's versions +of the appropriate starter functions, e.g. a typical ``manage.py`` using +django-configurations would look like this:: + + #!/usr/bin/env python + import os + import sys + + if __name__ == "__main__": + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + os.environ.setdefault('DJANGO_', 'MySettings') + + from configurations.management import execute_from_command_line + + execute_from_command_line(sys.argv) + +Notice in line 9 we don't use the common tool +``django.core.management.execute_from_command_line`` but instead +``configurations.management.execute_from_command_line``. + +The same applies to your ``wsgi.py`` file, e.g.:: + + import os + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings') + os.environ.setdefault('DJANGO_CONFIGURATION', 'MySettings') + + from configurations.wsgi import get_wsgi_application + + application = get_wsgi_application() + +Here we don't use the default ``django.core.wsgi.get_wsgi_application`` +function but instead ``configurations.wsgi.get_wsgi_application``. + +That's it! You can now use your project with ``manage.py`` and your favorite +WSGI enabled server. diff --git a/configurations/__init__.py b/configurations/__init__.py new file mode 100644 index 0000000..df2aa1d --- /dev/null +++ b/configurations/__init__.py @@ -0,0 +1,5 @@ +# flake8: noqa +from .base import Settings + +__version__ = '0.1a1' +__all__ = ['Settings'] diff --git a/configurations/base.py b/configurations/base.py new file mode 100644 index 0000000..a1f262d --- /dev/null +++ b/configurations/base.py @@ -0,0 +1,60 @@ +from django.conf import global_settings +from django.core.exceptions import ImproperlyConfigured + +from .utils import uppercase_attributes + +__all__ = ['Settings'] + + +install_failure = ("django-configurations settings importer wasn't " + "correctly installed. Please use one of the starter " + "functions to install it as mentioned in the docs: " + "http://django-configurations.readthedocs.org/") + + +class SettingsBase(type): + + def __new__(cls, name, bases, attrs): + if bases != (object,): + # if this is actually a subclass in a settings module + # we better check if the importer was correctly installed + from . import importer + if not importer.installed: + raise ImproperlyConfigured(install_failure) + settings_vars = uppercase_attributes(global_settings) + parents = [base for base in bases if isinstance(base, SettingsBase)] + if parents: + for base in bases[::-1]: + settings_vars.update(uppercase_attributes(base)) + attrs = dict(settings_vars, **attrs) + return super(SettingsBase, cls).__new__(cls, name, bases, attrs) + + def __repr__(self): + return "" % (self.__module__, self.__name__) + + +class Settings(object): + """ + The base configuration class to inherit from. + + :: + + class Develop(Settings): + EXTRA_AWESOME = True + + @property + def SOMETHING(self): + return completely.different() + + def OTHER(self): + if whatever: + return (1, 2, 3) + return (4, 5, 6) + + The module this configuration class is located in will + automatically get the class and instance level attributes + with upper characters if the ``DJANGO_CONFIGURATION`` is set + to the name of the class. + + """ + __metaclass__ = SettingsBase diff --git a/configurations/importer.py b/configurations/importer.py new file mode 100644 index 0000000..7d1dd1a --- /dev/null +++ b/configurations/importer.py @@ -0,0 +1,75 @@ +import imp +import os +import sys + +from django.core.exceptions import ImproperlyConfigured +from django.conf import ENVIRONMENT_VARIABLE + +from .utils import uppercase_attributes + + +installed = False + + +def install(): + global installed + if not installed: + sys.meta_path.append(SettingsImporter()) + installed = True + + +class SettingsImporter(object): + class_varname = 'DJANGO_CONFIGURATION' + error_msg = "Settings cannot be imported, environment variable %s is undefined." + + def __init__(self): + self.validate() + + def __repr__(self): + return "" % (self.module, self.name) + + @property + def module(self): + return os.environ.get(ENVIRONMENT_VARIABLE) + + @property + def name(self): + return os.environ.get(self.class_varname) + + def validate(self): + if self.name is None: + raise ImproperlyConfigured(self.error_msg % self.class_varname) + if self.module is None: + raise ImproperlyConfigured(self.error_msg % ENVIRONMENT_VARIABLE) + + def find_module(self, fullname, path=None): + if fullname is not None and fullname == self.module: + module = fullname.rsplit('.', 1)[-1] + return SettingsLoader(self.name, imp.find_module(module, path)) + return None + + +class SettingsLoader(object): + + def __init__(self, name, location): + self.name = name + self.location = location + + def load_module(self, fullname): + if fullname in sys.modules: + mod = sys.modules[fullname] # pragma: no cover + else: + mod = imp.load_module(fullname, *self.location) + try: + cls = getattr(mod, self.name) + obj = cls() + except AttributeError: # pragma: no cover + raise ImproperlyConfigured("Couldn't find settings '%s' in " + "module '%s'" % + (self.name, mod.__package__)) + for name, value in uppercase_attributes(obj).items(): + if callable(value): + value = value() + setattr(mod, name, value) + setattr(mod, 'CONFIGURATION', '%s.%s' % (fullname, self.name)) + return mod diff --git a/configurations/management.py b/configurations/management.py new file mode 100644 index 0000000..f91a8aa --- /dev/null +++ b/configurations/management.py @@ -0,0 +1,5 @@ +from . import importer + +importer.install() + +from django.core.management import execute_from_command_line # noqa diff --git a/configurations/tests/__init__.py b/configurations/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configurations/tests/settings/__init__.py b/configurations/tests/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/configurations/tests/settings/base.py b/configurations/tests/settings/base.py new file mode 100644 index 0000000..d732846 --- /dev/null +++ b/configurations/tests/settings/base.py @@ -0,0 +1,9 @@ +from configurations import Settings + + +def test_callback(request): + return {} + + +class Base(Settings): + pass diff --git a/configurations/tests/settings/main.py b/configurations/tests/settings/main.py new file mode 100644 index 0000000..ff0259f --- /dev/null +++ b/configurations/tests/settings/main.py @@ -0,0 +1,43 @@ +import os +from configurations import Settings + + +class Test(Settings): + SITE_ID = 1 + + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(os.path.dirname(__file__), 'test.db'), + } + } + + INSTALLED_APPS = [ + 'django.contrib.sessions', + 'django.contrib.contenttypes', + 'django.contrib.sites', + 'django.contrib.auth', + 'django.contrib.admin', + 'configurations.tests', + ] + + ROOT_URLCONF = 'configurations.tests.urls' + + TEST_RUNNER = 'discover_runner.DiscoverRunner' + + TEST_SETTING = True + + _SOMETHING = 'YEAH' + + DEBUG = True + + @property + def LALA(self): + return 1 + + def LALA2(self): + return 1 + + def TEMPLATE_CONTEXT_PROCESSORS(self): + return Settings.TEMPLATE_CONTEXT_PROCESSORS + ( + 'configurations.tests.settings.base.test_callback',) diff --git a/configurations/tests/settings/multiple_inheritance.py b/configurations/tests/settings/multiple_inheritance.py new file mode 100644 index 0000000..9f07169 --- /dev/null +++ b/configurations/tests/settings/multiple_inheritance.py @@ -0,0 +1,8 @@ +from .main import Test + + +class Inheritance(Test): + + def TEMPLATE_CONTEXT_PROCESSORS(self): + return super(Inheritance, self).TEMPLATE_CONTEXT_PROCESSORS() + ( + 'configurations.tests.settings.base.test_callback',) diff --git a/configurations/tests/settings/single_inheritance.py b/configurations/tests/settings/single_inheritance.py new file mode 100644 index 0000000..1ce0bda --- /dev/null +++ b/configurations/tests/settings/single_inheritance.py @@ -0,0 +1,8 @@ +from .base import Base + + +class Inheritance(Base): + + def TEMPLATE_CONTEXT_PROCESSORS(self): + return super(Inheritance, self).TEMPLATE_CONTEXT_PROCESSORS + ( + 'configurations.tests.settings.base.test_callback',) diff --git a/configurations/tests/test_inheritance.py b/configurations/tests/test_inheritance.py new file mode 100644 index 0000000..cc55acc --- /dev/null +++ b/configurations/tests/test_inheritance.py @@ -0,0 +1,41 @@ +import os + +from django.test import TestCase + +from mock import patch + + +class InheritanceTests(TestCase): + + @patch.dict(os.environ, clear=True, + DJANGO_CONFIGURATION='Inheritance', + DJANGO_SETTINGS_MODULE='configurations.tests.settings.single_inheritance') + def test_inherited(self): + from configurations.tests.settings import single_inheritance + self.assertEquals(single_inheritance.TEMPLATE_CONTEXT_PROCESSORS, ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'configurations.tests.settings.base.test_callback', + )) + + @patch.dict(os.environ, clear=True, + DJANGO_CONFIGURATION='Inheritance', + DJANGO_SETTINGS_MODULE='configurations.tests.settings.multiple_inheritance') + def test_inherited2(self): + from configurations.tests.settings import multiple_inheritance + self.assertEquals(multiple_inheritance.TEMPLATE_CONTEXT_PROCESSORS, ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'configurations.tests.settings.base.test_callback', + 'configurations.tests.settings.base.test_callback', + )) diff --git a/configurations/tests/test_main.py b/configurations/tests/test_main.py new file mode 100644 index 0000000..ad83eaa --- /dev/null +++ b/configurations/tests/test_main.py @@ -0,0 +1,70 @@ +import os + +from django.test import TestCase +from django.core.exceptions import ImproperlyConfigured + +from mock import patch + +from ..importer import SettingsImporter + + +class MainTests(TestCase): + + def test_simple(self): + from configurations.tests.settings import main + self.assertEquals(main.LALA, 1) + self.assertEquals(main.TEMPLATE_CONTEXT_PROCESSORS, ( + 'django.contrib.auth.context_processors.auth', + 'django.core.context_processors.debug', + 'django.core.context_processors.i18n', + 'django.core.context_processors.media', + 'django.core.context_processors.static', + 'django.core.context_processors.tz', + 'django.contrib.messages.context_processors.messages', + 'configurations.tests.settings.base.test_callback', + )) + self.assertEquals(main.TEST_SETTING, True) + + def test_global_arrival(self): + from django.conf import settings + self.assertEquals(settings.LALA, 1) + self.assertRaises(AttributeError, lambda: settings._SOMETHING) + + @patch.dict(os.environ, clear=True, DJANGO_CONFIGURATION='Test') + def test_empty_module_var(self): + self.assertRaises(ImproperlyConfigured, SettingsImporter) + + @patch.dict(os.environ, clear=True, + DJANGO_SETTINGS_MODULE='configurations.tests.settings.main') + def test_empty_class_var(self): + self.assertRaises(ImproperlyConfigured, SettingsImporter) + + def test_global_settings(self): + from configurations.base import Settings + self.assertEquals(Settings.LOGGING_CONFIG, 'django.utils.log.dictConfig') + self.assertEquals(repr(Settings), + "") + + def test_repr(self): + from configurations.tests.settings.main import Test + self.assertEquals(repr(Test), + "") + + @patch.dict(os.environ, clear=True, + DJANGO_SETTINGS_MODULE='configurations.tests.settings.main', + DJANGO_CONFIGURATION='Test') + def test_initialization(self): + importer = SettingsImporter() + self.assertEquals(importer.module, 'configurations.tests.settings.main') + self.assertEquals(importer.name, 'Test') + self.assertEquals(repr(importer), + "") + + @patch.dict(os.environ, clear=True, + DJANGO_SETTINGS_MODULE='configurations.tests.settings.inheritance', + DJANGO_CONFIGURATION='Inheritance') + def test_initialization_inheritance(self): + importer = SettingsImporter() + self.assertEquals(importer.module, + 'configurations.tests.settings.inheritance') + self.assertEquals(importer.name, 'Inheritance') diff --git a/configurations/tests/urls.py b/configurations/tests/urls.py new file mode 100644 index 0000000..e7c3cc7 --- /dev/null +++ b/configurations/tests/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls.defaults import include, patterns + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + (r'^admin/', include(admin.site.urls)), +) diff --git a/configurations/utils.py b/configurations/utils.py new file mode 100644 index 0000000..d4ecd36 --- /dev/null +++ b/configurations/utils.py @@ -0,0 +1,7 @@ +def isuppercase(name): + return name == name.upper() and not name.startswith('_') + + +def uppercase_attributes(obj): + return dict((name, getattr(obj, name)) + for name in filter(isuppercase, dir(obj))) diff --git a/configurations/wsgi.py b/configurations/wsgi.py new file mode 100644 index 0000000..54ab753 --- /dev/null +++ b/configurations/wsgi.py @@ -0,0 +1,14 @@ +from . import importer + +importer.install() + +try: + from django.core.wsgi import get_wsgi_application +except ImportError: # pragma: no cover + from django.core.handlers.wsgi import WSGIHandler + + def get_wsgi_application(): # noqa + return WSGIHandler() + +# this is just for the crazy ones +application = get_wsgi_application() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..f0e5085 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-configurations.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-configurations.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/django-configurations" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-configurations" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..72f8b45 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +# +# django-configurations documentation build configuration file, created by +# sphinx-quickstart on Sat Jul 21 15:03:23 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'django-configurations' +copyright = u'2012, Jannis Leidel' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +try: + from configurations import __version__ + # The short X.Y version. + version = '.'.join(__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. + release = __version__ +except ImportError: + version = release = 'dev' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'django-configurationsdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'django-configurations.tex', u'django-configurations Documentation', + u'Jannis Leidel', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'django-configurations', u'django-configurations Documentation', + [u'Jannis Leidel'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'django-configurations', u'django-configurations Documentation', + u'Jannis Leidel', 'django-configurations', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'django-configurations' +epub_author = u'Jannis Leidel' +epub_publisher = u'Jannis Leidel' +epub_copyright = u'2012, Jannis Leidel' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +#epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..b07ffec --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,115 @@ +.. include:: ../README.rst + +Wait, what? +----------- + +django-configurations helps you organize the configuration of your Django +project by providing the glue code to bridge between Django's module based +settings system and programming patterns like mixins_, facades_, factories_ +and adapters_ that are useful for non-trivial configuration scenarios. + +It allows you to use the native abilities of Python inheritance without the +side effects of module level namespaces that often lead to the unfortunate +use of the ``import *`` anti-pattern. + +.. _mixins: http://en.wikipedia.org/wiki/Mixin +.. _facades: http://en.wikipedia.org/wiki/Facade_pattern +.. _factories: http://en.wikipedia.org/wiki/Factory_method_pattern +.. _adapters: http://en.wikipedia.org/wiki/Adapter_pattern + +Okay, how does it work? +----------------------- + +Any subclass of the ``configurations.Settings`` class will automatically +use the values of its class and instance attributes (including properties +and methods) to set module level variables of the same module -- that's +how Django will interface to the django-configurations based settings during +startup and also the reason why it requires you to use its own startup +functions. + +That means when Django starts up django-configurations will have a look at +the ``DJANGO_CONFIGURATION`` environment variable to figure out which class +in the settings module (as defined by the ``DJANGO_SETTINGS_MODULE`` +environment variable) should be used for the process. It then instantiates +the class defined with ``DJANGO_CONFIGURATION`` and copies the uppercase +attributes to the module level variables. + +But isn't that magic? +--------------------- + +Yes, it looks like magic, but it's also maintainable and non-intrusive. +No monkey patching is needed to teach Django how to load settings via +django-configurations because it uses Python import hooks (`PEP 302`_) +behind the scenes. + +.. _`PEP 302`: http://www.python.org/dev/peps/pep-0302/ + +Usage patterns +-------------- + +There are various configuration patterns that can be implemented with +django-configurations. The most common pattern is to have a base class +and various subclasses based on the enviroment they are supposed to be +used in, e.g. in production, staging and development. + +Server specific settings +^^^^^^^^^^^^^^^^^^^^^^^^ + +For example, imagine you have a base setting class in your ``settings.py`` +file:: + + from configurations import Settings + + class Base(Settings): + TIME_ZONE = 'Europe/Berlin' + + class Dev(Base): + DEBUG = True + TEMPLATE_DEBUG = DEBUG + + class Prod(Base): + TIME_ZONE = 'America/New_York' + +You can now set the ``DJANGO_CONFIGURATION`` environment variable to one +of the class names you've defined, e.g. on your production server it +should be ``Prod``. In bash that would be:: + + export DJANGO_SETTINGS_MODULE=mysite.settings + export DJANGO_CONFIGURATION=Prod + +Global settings defaults +^^^^^^^^^^^^^^^^^^^^^^^^ + +Every ``configurations.Settings`` subclass will automatically contain +Django's global settings as class attributes, so you can refer to them when +setting other values, e.g.:: + + class Base(Settings): + TEMPLATE_CONTEXT_PROCESSORS = \ + Settings.TEMPLATE_CONTEXT_PROCESSORS + ( + 'django.core.context_processors.request', + ) + + @property + def LANGUAGES(self): + return Settings.LANGUAGES + (('tlh', 'Klingon'),) + +Mixins +^^^^^^ + +You might want to apply some configuration values for each and every +project you're working on without having to repeat yourself. Just define +a few mixin you re-use multiple times:: + + class FullPageCaching(object): + USE_ETAGS = True + + +Thanks +------ + +- Pinax for spearheading the efforts to extend the Django project metaphor + with reusable project templates and a flexible configuration environment. + +- django-classbasedsettings by Matthew Tretter for being the immediate + inspiration for django-configurations. diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..482b884 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-configurations.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-configurations.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..91fb9ca --- /dev/null +++ b/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'configurations.tests.settings.main') + os.environ.setdefault('DJANGO_CONFIGURATION', 'Test') + + from configurations.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 0000000..8c15f07 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,4 @@ +flake8 +coverage +django-discover-runner +mock \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7207bdc --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +import codecs +import re +from os import path +from setuptools import setup + + +def read(*parts): + file_path = path.join(path.dirname(__file__), *parts) + return codecs.open(file_path).read() + + +def find_version(*parts): + version_file = read(*parts) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + + +setup( + name='django-configurations', + version=find_version('configurations', '__init__.py'), + description='A helper class for handling configuration defaults ' + 'of packaged apps gracefully.', + long_description=read('README.rst'), + author='Jannis Leidel', + author_email='jannis@leidel.info', + license='BSD', + url='http://django-configurations.readthedocs.org/', + packages=[ + 'configurations', + 'configurations.tests', + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Utilities', + ], +)