A working implementation of decorator_include.
This commit is contained in:
commit
cc50e071e5
14 changed files with 413 additions and 0 deletions
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
.coverage
|
||||||
|
.installed.cfg
|
||||||
|
MANIFEST
|
||||||
|
dist
|
||||||
|
parts/*
|
||||||
|
eggs/*
|
||||||
|
downloads/*
|
||||||
|
bin/*
|
||||||
|
develop-eggs/*
|
||||||
|
src/*.egg-info
|
1
AUTHORS.rst
Normal file
1
AUTHORS.rst
Normal file
|
@ -0,0 +1 @@
|
||||||
|
``decorator_include`` was written by Jeff Kistler in 2011.
|
42
README.rst
Normal file
42
README.rst
Normal file
|
@ -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'),
|
||||||
|
)
|
77
bootstrap.py
Normal file
77
bootstrap.py
Normal file
|
@ -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)
|
16
buildout.cfg
Normal file
16
buildout.cfg
Normal file
|
@ -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
|
26
setup.py
Normal file
26
setup.py
Normal file
|
@ -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',
|
||||||
|
]
|
||||||
|
)
|
83
src/decorator_include/__init__.py
Normal file
83
src/decorator_include/__init__.py
Normal file
|
@ -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)
|
0
src/decorator_include/models.py
Normal file
0
src/decorator_include/models.py
Normal file
98
src/decorator_include/tests/__init__.py
Normal file
98
src/decorator_include/tests/__init__.py
Normal file
|
@ -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)
|
10
src/decorator_include/tests/included.py
Normal file
10
src/decorator_include/tests/included.py
Normal file
|
@ -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'),
|
||||||
|
)
|
9
src/decorator_include/tests/included2.py
Normal file
9
src/decorator_include/tests/included2.py
Normal file
|
@ -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'),
|
||||||
|
)
|
13
src/decorator_include/tests/urls.py
Normal file
13
src/decorator_include/tests/urls.py
Normal file
|
@ -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')),
|
||||||
|
)
|
0
testproject/__init__.py
Normal file
0
testproject/__init__.py
Normal file
24
testproject/settings.py
Normal file
24
testproject/settings.py
Normal file
|
@ -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'
|
Reference in a new issue