A working implementation of decorator_include.

This commit is contained in:
Jeff Kistler 2011-06-06 20:53:06 -07:00
commit cc50e071e5
14 changed files with 413 additions and 0 deletions

14
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1 @@
``decorator_include`` was written by Jeff Kistler in 2011.

42
README.rst Normal file
View 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
View 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
View 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
View 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',
]
)

View 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)

View file

View 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)

View 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'),
)

View 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'),
)

View 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
View file

24
testproject/settings.py Normal file
View 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'