diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py index d16fc9ab71c..b9fd9ab9003 100644 --- a/django/contrib/admin/views/decorators.py +++ b/django/contrib/admin/views/decorators.py @@ -1,3 +1,11 @@ +import base64 +import md5 +import cPickle as pickle +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + from django import http, template from django.conf import settings from django.contrib.auth.models import User @@ -5,8 +13,6 @@ from django.contrib.auth import authenticate, login from django.shortcuts import render_to_response from django.utils.translation import ugettext_lazy, ugettext as _ from django.utils.safestring import mark_safe -import base64, md5 -import cPickle as pickle ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") LOGIN_FORM_KEY = 'this_is_the_login_form' @@ -104,4 +110,4 @@ def staff_member_required(view_func): else: return _display_login_form(request, ERROR_MESSAGE) - return _checklogin + return wraps(view_func)(_checklogin) diff --git a/django/contrib/auth/decorators.py b/django/contrib/auth/decorators.py index f3f7f530ef2..25bc20780e9 100644 --- a/django/contrib/auth/decorators.py +++ b/django/contrib/auth/decorators.py @@ -1,3 +1,8 @@ +try: + from functools import wraps, update_wrapper +except ImportError: + from django.utils.functional import wraps, update_wrapper # Python 2.3, 2.4 fallback. + from django.contrib.auth import REDIRECT_FIELD_NAME from django.http import HttpResponseRedirect from django.utils.http import urlquote @@ -51,7 +56,7 @@ class _CheckLogin(object): self.test_func = test_func self.login_url = login_url self.redirect_field_name = redirect_field_name - self.__name__ = view_func.__name__ + update_wrapper(self, view_func) def __get__(self, obj, cls=None): view_func = self.view_func.__get__(obj, cls) diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py index d3400baac8f..8e5bcf5bd8b 100644 --- a/django/template/defaultfilters.py +++ b/django/template/defaultfilters.py @@ -2,6 +2,10 @@ import re import random as random_module +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. from django.template import Variable, Library from django.conf import settings @@ -35,7 +39,7 @@ def stringfilter(func): for attr in ('is_safe', 'needs_autoescape'): if hasattr(func, attr): setattr(_dec, attr, getattr(func, attr)) - return _dec + return wraps(func)(_dec) ################### # STRINGS # diff --git a/django/utils/decorators.py b/django/utils/decorators.py index 57ce29fca4e..27a080e7409 100644 --- a/django/utils/decorators.py +++ b/django/utils/decorators.py @@ -1,6 +1,10 @@ "Functions that help with dynamically creating decorators for views." import types +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. def decorator_from_middleware(middleware_class): """ @@ -53,5 +57,5 @@ def decorator_from_middleware(middleware_class): if result is not None: return result return response - return _wrapped_view + return wraps(view_func)(_wrapped_view) return _decorator_from_middleware diff --git a/django/utils/functional.py b/django/utils/functional.py index e0c862b0b7b..3de693e18ce 100644 --- a/django/utils/functional.py +++ b/django/utils/functional.py @@ -1,8 +1,120 @@ +# License for code in this file that was taken from Python 2.5. + +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF +# hereby grants Licensee a nonexclusive, royalty-free, world-wide +# license to reproduce, analyze, test, perform and/or display publicly, +# prepare derivative works, distribute, and otherwise use Python +# alone or in any derivative version, provided, however, that PSF's +# License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +# 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; +# All Rights Reserved" are retained in Python alone or in any derivative +# version prepared by Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. + + def curry(_curried_func, *args, **kwargs): def _curried(*moreargs, **morekwargs): return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs)) return _curried +### Begin from Python 2.5 functools.py ######################################## + +# Summary of changes made to the Python 2.5 code below: +# * swapped ``partial`` for ``curry`` to maintain backwards-compatibility +# in Django. +# * Wrapped the ``setattr`` call in ``update_wrapper`` with a try-except +# block to make it compatible with Python 2.3, which doesn't allow +# assigning to ``__name__``. + +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation. +# All Rights Reserved. + +############################################################################### + +# update_wrapper() and wraps() are tools to help write +# wrapper functions that can handle naive introspection + +WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') +WRAPPER_UPDATES = ('__dict__',) +def update_wrapper(wrapper, + wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Update a wrapper function to look like the wrapped function + + wrapper is the function to be updated + wrapped is the original function + assigned is a tuple naming the attributes assigned directly + from the wrapped function to the wrapper function (defaults to + functools.WRAPPER_ASSIGNMENTS) + updated is a tuple naming the attributes off the wrapper that + are updated with the corresponding attribute from the wrapped + function (defaults to functools.WRAPPER_UPDATES) + """ + for attr in assigned: + try: + setattr(wrapper, attr, getattr(wrapped, attr)) + except TypeError: # Python 2.3 doesn't allow assigning to __name__. + pass + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr)) + # Return the wrapper so this can be used as a decorator via curry() + return wrapper + +def wraps(wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Decorator factory to apply update_wrapper() to a wrapper function + + Returns a decorator that invokes update_wrapper() with the decorated + function as the wrapper argument and the arguments to wraps() as the + remaining arguments. Default arguments are as for update_wrapper(). + This is a convenience function to simplify applying curry() to + update_wrapper(). + """ + return curry(update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + +### End from Python 2.5 functools.py ########################################## + def memoize(func, cache, num_args): """ Wrap a function so that results for any argument tuple are stored in @@ -18,7 +130,7 @@ def memoize(func, cache, num_args): result = func(*args) cache[mem_args] = result return result - return wrapper + return wraps(func)(wrapper) class Promise(object): """ @@ -110,7 +222,7 @@ def lazy(func, *resultclasses): # Creates the proxy object, instead of the actual value. return __proxy__(args, kw) - return __wrapper__ + return wraps(func)(__wrapper__) def allow_lazy(func, *resultclasses): """ @@ -126,4 +238,4 @@ def allow_lazy(func, *resultclasses): else: return func(*args, **kwargs) return lazy(func, *resultclasses)(*args, **kwargs) - return wrapper + return wraps(func)(wrapper) diff --git a/django/views/decorators/cache.py b/django/views/decorators/cache.py index b04cc2340b8..8b620c13452 100644 --- a/django/views/decorators/cache.py +++ b/django/views/decorators/cache.py @@ -11,6 +11,11 @@ Additionally, all headers from the response's Vary header will be taken into account on caching -- just like the middleware does. """ +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + from django.utils.decorators import decorator_from_middleware from django.utils.cache import patch_cache_control, add_never_cache_headers from django.middleware.cache import CacheMiddleware @@ -26,7 +31,7 @@ def cache_control(**kwargs): patch_cache_control(response, **kwargs) return response - return _cache_controlled + return wraps(viewfunc)(_cache_controlled) return _cache_controller @@ -39,4 +44,4 @@ def never_cache(view_func): response = view_func(request, *args, **kwargs) add_never_cache_headers(response) return response - return _wrapped_view_func + return wraps(view_func)(_wrapped_view_func) diff --git a/django/views/decorators/http.py b/django/views/decorators/http.py index 9feb8c0d848..dd4f90ea9c4 100644 --- a/django/views/decorators/http.py +++ b/django/views/decorators/http.py @@ -2,6 +2,11 @@ Decorators for views based on HTTP headers. """ +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + from django.utils.decorators import decorator_from_middleware from django.middleware.http import ConditionalGetMiddleware from django.http import HttpResponseNotAllowed @@ -24,7 +29,7 @@ def require_http_methods(request_method_list): if request.method not in request_method_list: return HttpResponseNotAllowed(request_method_list) return func(request, *args, **kwargs) - return inner + return wraps(func)(inner) return decorator require_GET = require_http_methods(["GET"]) diff --git a/django/views/decorators/vary.py b/django/views/decorators/vary.py index 9b49c45cf2b..ea1b8d307dd 100644 --- a/django/views/decorators/vary.py +++ b/django/views/decorators/vary.py @@ -1,3 +1,8 @@ +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + from django.utils.cache import patch_vary_headers def vary_on_headers(*headers): @@ -16,7 +21,7 @@ def vary_on_headers(*headers): response = func(*args, **kwargs) patch_vary_headers(response, headers) return response - return inner_func + return wraps(func)(inner_func) return decorator def vary_on_cookie(func): @@ -32,4 +37,4 @@ def vary_on_cookie(func): response = func(*args, **kwargs) patch_vary_headers(response, ('Cookie',)) return response - return inner_func + return wraps(func)(inner_func) diff --git a/tests/regressiontests/decorators/models.py b/tests/regressiontests/decorators/models.py new file mode 100644 index 00000000000..e5a795067b9 --- /dev/null +++ b/tests/regressiontests/decorators/models.py @@ -0,0 +1,2 @@ +# A models.py so that tests run. + diff --git a/tests/regressiontests/decorators/tests.py b/tests/regressiontests/decorators/tests.py new file mode 100644 index 00000000000..0c434772f8b --- /dev/null +++ b/tests/regressiontests/decorators/tests.py @@ -0,0 +1,56 @@ +from unittest import TestCase +from sys import version_info + +from django.http import HttpResponse +from django.utils.functional import allow_lazy, lazy, memoize +from django.views.decorators.http import require_http_methods, require_GET, require_POST +from django.views.decorators.vary import vary_on_headers, vary_on_cookie +from django.views.decorators.cache import cache_page, never_cache, cache_control +from django.contrib.auth.decorators import login_required, permission_required, user_passes_test +from django.contrib.admin.views.decorators import staff_member_required + +def fully_decorated(request): + """Expected __doc__""" + return HttpResponse('dummy') +fully_decorated.anything = "Expected __dict__" + +# django.views.decorators.http +fully_decorated = require_http_methods(["GET"])(fully_decorated) +fully_decorated = require_GET(fully_decorated) +fully_decorated = require_POST(fully_decorated) + +# django.views.decorators.vary +fully_decorated = vary_on_headers('Accept-language')(fully_decorated) +fully_decorated = vary_on_cookie(fully_decorated) + +# django.views.decorators.cache +fully_decorated = cache_page(60*15)(fully_decorated) +fully_decorated = cache_control(private=True)(fully_decorated) +fully_decorated = never_cache(fully_decorated) + +# django.contrib.auth.decorators +fully_decorated = user_passes_test(lambda u:True)(fully_decorated) +fully_decorated = login_required(fully_decorated) +fully_decorated = permission_required('change_world')(fully_decorated) + +# django.contrib.admin.views.decorators +fully_decorated = staff_member_required(fully_decorated) + +# django.utils.functional +fully_decorated = memoize(fully_decorated, {}, 1) +fully_decorated = allow_lazy(fully_decorated) +fully_decorated = lazy(fully_decorated) + +class DecoratorsTest(TestCase): + + def test_attributes(self): + """ + Tests that django decorators set certain attributes of the wrapped + function. + """ + # Only check __name__ on Python 2.4 or later since __name__ can't be + # assigned to in earlier Python versions. + if version_info[0] >= 2 and version_info[1] >= 4: + self.assertEquals(fully_decorated.__name__, 'fully_decorated') + self.assertEquals(fully_decorated.__doc__, 'Expected __doc__') + self.assertEquals(fully_decorated.__dict__['anything'], 'Expected __dict__')