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