commit cc50e071e51af63e24583dc7274547796478fec3
Author: Jeff Kistler <jeff@jeffkistler.com>
Date:   Mon Jun 6 20:53:06 2011 -0700

    A working implementation of decorator_include.

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..328d6be
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.pyc
+*.pyo
+*~
+.DS_Store
+.coverage
+.installed.cfg
+MANIFEST
+dist
+parts/*
+eggs/*
+downloads/*
+bin/*
+develop-eggs/*
+src/*.egg-info
\ No newline at end of file
diff --git a/AUTHORS.rst b/AUTHORS.rst
new file mode 100644
index 0000000..a6875c8
--- /dev/null
+++ b/AUTHORS.rst
@@ -0,0 +1 @@
+``decorator_include`` was written by Jeff Kistler in 2011.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..89c4525
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,42 @@
+decorator_include
+=================
+
+Include Django URL patterns with decorators.
+
+Installation
+------------
+
+Installation from Source
+````````````````````````
+
+Unpack the archive, ``cd`` to the source directory, and run the following
+command::
+
+    python setup.py install
+
+Installation with pip and git
+`````````````````````````````
+
+Assuming you have pip and git installed, run the following command to
+install from the GitHub repository::
+
+    pip install git+git://github.com/jeffkistler/decorator_include.git#egg=decorator_include
+
+Usage
+-----
+
+``decorator_include`` is intended for use in URL confs as a replacement
+for the ``django.conf.urls.defaults.include`` function. It works in almost
+the same way as ``include``, however the first argument should be either a
+decorator or an iterable of decorators to apply, in reverse order, to all
+included views. Here is an example URL conf::
+
+    from django.conf.urls.defaults import *
+    from django.contrib.auth.decorators import login_required
+
+    from decorator_include import decorator_include
+
+    urlpatterns = patterns('',
+        url(r'^$', 'mysite.views.index', name='index'),
+        url(r'^secret/', decorator_include(login_required, 'mysite.secret.urls'),
+    )
diff --git a/bootstrap.py b/bootstrap.py
new file mode 100644
index 0000000..7728587
--- /dev/null
+++ b/bootstrap.py
@@ -0,0 +1,77 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+try:
+    import pkg_resources
+except ImportError:
+    ez = {}
+    exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                         ).read() in ez
+    ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+    import pkg_resources
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    def quote (c):
+        return c
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+ws  = pkg_resources.working_set
+
+if is_jython:
+    import subprocess
+    
+    assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd', 
+           quote(tmpeggs), 'zc.buildout'], 
+           env=dict(os.environ,
+               PYTHONPATH=
+               ws.find(pkg_resources.Requirement.parse('setuptools')).location
+               ),
+           ).wait() == 0
+
+else:
+    assert os.spawnle(
+        os.P_WAIT, sys.executable, quote (sys.executable),
+        '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout',
+        dict(os.environ,
+            PYTHONPATH=
+            ws.find(pkg_resources.Requirement.parse('setuptools')).location
+            ),
+        ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)
diff --git a/buildout.cfg b/buildout.cfg
new file mode 100644
index 0000000..33f0585
--- /dev/null
+++ b/buildout.cfg
@@ -0,0 +1,16 @@
+[buildout]
+parts = decorator_include
+unzip = true
+develop = .
+
+[decorator_include]
+recipe = djangorecipe
+version = 1.3
+project = testproject
+settings = settings
+test = decorator_include
+testrunner = test
+eggs =
+     decorator_include
+     django-nose
+     coverage
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..b58821b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,26 @@
+import os
+from distutils.core import setup
+
+def read(fname):
+    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+setup(
+    name = 'django-decorator-include',
+    version = '0.1',
+    license = 'BSD',
+    description = 'Include Django URL patterns with decorators.',
+    long_description = read('README.rst'),
+    author = 'Jeff Kistler',
+    author_email = 'jeff@jeffkistler.com',
+    url = 'https://github.com/jeffkistler/django-decorator-include/',
+    packages = ['decorator_include', 'decorator_include.tests'],
+    package_dir = {'': 'src'},
+    classifiers = [
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Internet :: WWW/HTTP',
+    ]
+)
diff --git a/src/decorator_include/__init__.py b/src/decorator_include/__init__.py
new file mode 100644
index 0000000..fc9a3ff
--- /dev/null
+++ b/src/decorator_include/__init__.py
@@ -0,0 +1,83 @@
+"""
+A replacement for ``django.conf.urls.defaults.include`` that takes a decorator,
+or an iterable of view decorators as the first argument and applies them, in
+reverse order, to all views in the included urlconf.
+"""
+from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
+from django.utils.importlib import import_module
+
+class DecoratedPatterns(object):
+    """
+    A wrapper for an urlconf that applies a decorator to all its views.
+    """
+    def __init__(self, urlconf_name, decorators):
+        self.urlconf_name = urlconf_name
+        try:
+            iter(decorators)
+        except TypeError:
+            decorators = [decorators]
+        self.decorators = decorators
+        if not isinstance(urlconf_name, basestring):
+            self._urlconf_module = self.urlconf_name
+        else:
+            self._urlconf_module = None
+
+    def decorate_pattern(self, pattern):
+        if isinstance(pattern, RegexURLResolver):
+            regex = pattern.regex.pattern
+            urlconf_module = pattern.urlconf_name
+            default_kwargs = pattern.default_kwargs
+            namespace = pattern.namespace
+            app_name = pattern.app_name
+            urlconf = DecoratedPatterns(urlconf_module, self.decorators)
+            decorated = RegexURLResolver(
+                regex, urlconf, default_kwargs,
+                app_name, namespace
+            )
+        else:
+            callback = pattern.callback
+            for decorator in reversed(self.decorators):
+                callback = decorator(callback)
+            decorated = RegexURLPattern(
+                pattern.regex.pattern,
+                callback,
+                pattern.default_args,
+                pattern.name
+            )
+        return decorated
+
+    def _get_urlconf_module(self):
+        if self._urlconf_module is None:
+            self._urlconf_module = import_module(self.urlconf_name)
+        return self._urlconf_module
+    urlconf_module = property(_get_urlconf_module)
+
+    def _get_urlpatterns(self):
+        try:
+            patterns = self.urlconf_module.urlpatterns
+        except AttributeError:
+            patterns = self.urlconf_module
+        return [self.decorate_pattern(pattern) for pattern in patterns]
+    urlpatterns = property(_get_urlpatterns)
+
+    def __getattr__(self, name):
+        return getattr(self.urlconf_module, name)
+
+
+def decorator_include(decorators, arg, namespace=None, app_name=None):
+    """
+    Works like ``django.conf.urls.defaults.include`` but takes a view decorator
+    or an iterable of view decorators as the first argument and applies them,
+    in reverse order, to all views in the included urlconf.
+    """
+    if isinstance(arg, tuple):
+        if namespace:
+            raise ImproperlyConfigured(
+                'Cannot override the namespace for a dynamic module that provides a namespace'
+            )
+        urlconf, app_name, namespace = arg
+    else:
+        urlconf = arg
+    decorated_urlconf = DecoratedPatterns(urlconf, decorators)
+    return (decorated_urlconf, app_name, namespace)
diff --git a/src/decorator_include/models.py b/src/decorator_include/models.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/decorator_include/tests/__init__.py b/src/decorator_include/tests/__init__.py
new file mode 100644
index 0000000..206f65e
--- /dev/null
+++ b/src/decorator_include/tests/__init__.py
@@ -0,0 +1,98 @@
+from django.test import TestCase
+
+class IncludeDecoratedTests(TestCase):
+    urls = 'decorator_include.tests.urls'
+
+    def getDecoratorInclude(self):
+        from decorator_include import decorator_include
+        return decorator_include
+
+    def testBasic(self):
+        decorator_include = self.getDecoratorInclude()
+        def test_decorator(func):
+            func.tested = True
+            return func
+        result = decorator_include(
+            test_decorator,
+            'decorator_include.tests.urls'
+        )
+        self.assertEquals(3, len(result))
+        self.assertTrue('DecoratedPatterns', result[0].__class__.__name__)
+        self.assertTrue(result[1] is None)
+        self.assertTrue(result[2] is None)
+
+    def testBasicNamespace(self):  
+        decorator_include = self.getDecoratorInclude()
+        def test_decorator(func):
+            func.tested = True
+            return func
+        result = decorator_include(
+            test_decorator,
+            'decorator_include.tests.urls',
+            'test'
+        )
+        self.assertEquals(3, len(result))
+        self.assertTrue('DecoratedPatterns', result[0].__class__.__name__)
+        self.assertTrue(result[1] is None)
+        self.assertEquals('test', result[2])
+
+    def testGetURLPatterns(self):
+        decorator_include = self.getDecoratorInclude()
+        def test_decorator(func):
+            func.decorator_flag = 'test'
+            return func
+        result = decorator_include(
+            test_decorator,
+            'decorator_include.tests.urls'
+        )
+        self.assertEquals(3, len(result))
+        self.assertTrue('DecoratedPatterns', result[0].__class__.__name__)
+        patterns = result[0].urlpatterns
+        self.assertEquals(2, len(patterns))
+        self.assertEquals('test', patterns[0].callback.decorator_flag)
+
+    def testMultipleDecorators(self):
+        decorator_include = self.getDecoratorInclude()
+        def first_decorator(func):
+            func.decorator_flag = 'first'
+            return func
+        def second_decorator(func):
+            func.decorator_flag = 'second'
+            func.decorated_by = 'second'
+            return func
+        result = decorator_include(
+            (first_decorator, second_decorator),
+            'decorator_include.tests.urls'
+        )
+        self.assertTrue('DecoratedPatterns', result[0].__class__.__name__)
+        patterns = result[0].urlpatterns
+        pattern = patterns[0]
+        self.assertEquals('first', pattern.callback.decorator_flag)
+        self.assertEquals('second', pattern.callback.decorated_by)
+
+    def testFollowInclude(self):
+        decorator_include = self.getDecoratorInclude()
+        def test_decorator(func):
+            func.decorator_flag = 'test'
+            return func
+        result = decorator_include(
+            test_decorator,
+            'decorator_include.tests.urls'
+        )
+        patterns = result[0].urlpatterns
+        decorated = patterns[1]
+        self.assertEquals('test', decorated.url_patterns[1].callback.decorator_flag)
+        decorated = patterns[1].url_patterns[0].url_patterns[0]
+        self.assertEquals('test', decorated.callback.decorator_flag)
+
+    def testGetIndex(self):
+        response = self.client.get('/')
+        self.assertEquals(200, response.status_code)
+
+    def testGetTest(self):
+        response = self.client.get('/include/test/')
+        self.assertEquals(302, response.status_code)
+    
+    def testGetDeeplyNested(self):
+        response = self.client.get('/include/included/deeply_nested/')
+        self.assertEquals(302, response.status_code)
diff --git a/src/decorator_include/tests/included.py b/src/decorator_include/tests/included.py
new file mode 100644
index 0000000..a7f00fd
--- /dev/null
+++ b/src/decorator_include/tests/included.py
@@ -0,0 +1,10 @@
+from django.conf.urls.defaults import *
+from django.http import HttpResponse
+
+def testify(request):
+    return HttpResponse('testify!')
+
+urlpatterns = patterns('',
+    url(r'^included/', include('decorator_include.tests.included2')),
+    url(r'^test/$', testify, name='testify'),
+)
diff --git a/src/decorator_include/tests/included2.py b/src/decorator_include/tests/included2.py
new file mode 100644
index 0000000..206b815
--- /dev/null
+++ b/src/decorator_include/tests/included2.py
@@ -0,0 +1,9 @@
+from django.conf.urls.defaults import *
+from django.http import HttpResponse
+
+def deeply_nested(request):
+    return HttpResponse('deeply nested!')
+
+urlpatterns = patterns('',
+    url(r'^deeply_nested/$', deeply_nested, name='deeply_nested'),
+)
diff --git a/src/decorator_include/tests/urls.py b/src/decorator_include/tests/urls.py
new file mode 100644
index 0000000..99a52c9
--- /dev/null
+++ b/src/decorator_include/tests/urls.py
@@ -0,0 +1,13 @@
+from django.conf.urls.defaults import *
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+
+from decorator_include import decorator_include
+
+def index(request):
+    return HttpResponse('Index!')
+
+urlpatterns = patterns('',
+    url(r'^$', index, name='index'),
+    url(r'^include/', decorator_include(login_required, 'decorator_include.tests.included')),
+)
diff --git a/testproject/__init__.py b/testproject/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/testproject/settings.py b/testproject/settings.py
new file mode 100644
index 0000000..55e8856
--- /dev/null
+++ b/testproject/settings.py
@@ -0,0 +1,24 @@
+import os.path
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+BASE_DIR = os.path.dirname(__file__)
+
+def absolute_path(path):
+    return os.path.normpath(os.path.join(BASE_DIR, path))
+
+SITE_ID = 1
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': absolute_path('database.sqlite3'),
+    }
+}
+
+INSTALLED_APPS = (
+    'django.contrib.sites',
+    'decorator_include',
+)
+
+ROOT_URLCONF = 'decorator_include.tests.urls'