diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index d16fc9ab71..b9fd9ab900 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 f3f7f530ef..25bc20780e 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/core/handlers/base.py b/django/core/handlers/base.py
index 7f68946f3d..a81bec322f 100644
--- a/django/core/handlers/base.py
+++ b/django/core/handlers/base.py
@@ -109,7 +109,8 @@ class BaseHandler(object):
except exceptions.PermissionDenied:
return http.HttpResponseForbidden('
Permission denied
')
except SystemExit:
- pass # See http://code.djangoproject.com/ticket/1023
+ # Allow sys.exit() to actually exit. See tickets #1023 and #4701
+ raise
except: # Handle everything else, including SuspiciousOperation, etc.
# Get the exception info now, in case another exception is thrown later.
exc_info = sys.exc_info()
diff --git a/django/db/backends/postgresql/operations.py b/django/db/backends/postgresql/operations.py
index ec54e165fb..f4e3b61a93 100644
--- a/django/db/backends/postgresql/operations.py
+++ b/django/db/backends/postgresql/operations.py
@@ -29,7 +29,7 @@ class DatabaseOperations(BaseDatabaseOperations):
def field_cast_sql(self, db_type):
if db_type == 'inet':
- return 'CAST(%s AS TEXT)'
+ return 'HOST(%s)'
return '%s'
def last_insert_id(self, cursor, table_name, pk_name):
diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 20b35cc3d8..818c8a9d7c 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -23,26 +23,64 @@ RECURSIVE_RELATIONSHIP_CONSTANT = 'self'
pending_lookups = {}
-def add_lookup(rel_cls, field):
- name = field.rel.to
- module = rel_cls.__module__
- key = (module, name)
- # Has the model already been loaded?
- # If so, resolve the string reference right away
- model = get_model(rel_cls._meta.app_label, field.rel.to, False)
+def add_lazy_relation(cls, field, relation):
+ """
+ Adds a lookup on ``cls`` when a related field is defined using a string,
+ i.e.::
+
+ class MyModel(Model):
+ fk = ForeignKey("AnotherModel")
+
+ This string can be:
+
+ * RECURSIVE_RELATIONSHIP_CONSTANT (i.e. "self") to indicate a recursive
+ relation.
+
+ * The name of a model (i.e "AnotherModel") to indicate another model in
+ the same app.
+
+ * An app-label and model name (i.e. "someapp.AnotherModel") to indicate
+ another model in a different app.
+
+ If the other model hasn't yet been loaded -- almost a given if you're using
+ lazy relationships -- then the relation won't be set up until the
+ class_prepared signal fires at the end of model initialization.
+ """
+ # Check for recursive relations
+ if relation == RECURSIVE_RELATIONSHIP_CONSTANT:
+ app_label = cls._meta.app_label
+ model_name = cls.__name__
+
+ else:
+ # Look for an "app.Model" relation
+ try:
+ app_label, model_name = relation.split(".")
+ except ValueError:
+ # If we can't split, assume a model in current app
+ app_label = cls._meta.app_label
+ model_name = relation
+
+ # Try to look up the related model, and if it's already loaded resolve the
+ # string right away. If get_model returns None, it means that the related
+ # model isn't loaded yet, so we need to pend the relation until the class
+ # is prepared.
+ model = get_model(app_label, model_name, False)
if model:
field.rel.to = model
- field.do_related_class(model, rel_cls)
+ field.do_related_class(model, cls)
else:
- # Mark the related field for later lookup
- pending_lookups.setdefault(key, []).append((rel_cls, field))
-
+ key = (app_label, model_name)
+ value = (cls, field)
+ pending_lookups.setdefault(key, []).append(value)
+
def do_pending_lookups(sender):
- other_cls = sender
- key = (other_cls.__module__, other_cls.__name__)
- for rel_cls, field in pending_lookups.setdefault(key, []):
- field.rel.to = other_cls
- field.do_related_class(other_cls, rel_cls)
+ """
+ Handle any pending relations to the sending model. Sent from class_prepared.
+ """
+ key = (sender._meta.app_label, sender.__name__)
+ for cls, field in pending_lookups.pop(key, []):
+ field.rel.to = sender
+ field.do_related_class(sender, cls)
dispatcher.connect(do_pending_lookups, signal=signals.class_prepared)
@@ -66,9 +104,7 @@ class RelatedField(object):
sup.contribute_to_class(cls, name)
other = self.rel.to
if isinstance(other, basestring):
- if other == RECURSIVE_RELATIONSHIP_CONSTANT:
- self.rel.to = cls.__name__
- add_lookup(cls, self)
+ add_lazy_relation(cls, self, other)
else:
self.do_related_class(other, cls)
diff --git a/django/http/__init__.py b/django/http/__init__.py
index 69e9d51204..7a85e20515 100644
--- a/django/http/__init__.py
+++ b/django/http/__init__.py
@@ -171,7 +171,7 @@ class QueryDict(MultiValueDict):
dict.__setitem__(result, key, value)
return result
- def __deepcopy__(self, memo={}):
+ def __deepcopy__(self, memo):
import copy
result = self.__class__('', mutable=True)
memo[id(self)] = result
@@ -223,7 +223,7 @@ class QueryDict(MultiValueDict):
def copy(self):
"Returns a mutable copy of this object."
- return self.__deepcopy__()
+ return self.__deepcopy__({})
def urlencode(self):
output = []
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index d3400baac8..8e5bcf5bd8 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 57ce29fca4..27a080e740 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 e0c862b0b7..3de693e18c 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 b04cc2340b..8b620c1345 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 9feb8c0d84..dd4f90ea9c 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 9b49c45cf2..ea1b8d307d 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/docs/model-api.txt b/docs/model-api.txt
index abf69835be..2d8d984237 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -784,9 +784,17 @@ you can use the name of the model, rather than the model object itself::
class Manufacturer(models.Model):
# ...
-Note, however, that you can only use strings to refer to models in the same
-models.py file -- you cannot use a string to reference a model in a different
-application, or to reference a model that has been imported from elsewhere.
+Note, however, that this only refers to models in the same models.py file -- you
+cannot use a string to reference a model defined in another application or
+imported from elsewhere.
+
+**New in Django development version:** to refer to models defined in another
+application, you must instead explicitially specify the application label. That
+is, if the ``Manufacturer`` model above is defined in another application called
+``production``, you'd need to use::
+
+ class Car(models.Model):
+ manufacturer = models.ForeignKey('production.Manufacturer')
Behind the scenes, Django appends ``"_id"`` to the field name to create its
database column name. In the above example, the database table for the ``Car``
diff --git a/extras/django_bash_completion b/extras/django_bash_completion
index 8906609a68..7b2b1947d8 100644
--- a/extras/django_bash_completion
+++ b/extras/django_bash_completion
@@ -61,13 +61,13 @@ _django_completion()
||
# python manage.py, /some/path/python manage.py (if manage.py exists)
( ${COMP_CWORD} -eq 2 &&
- ( $( basename ${COMP_WORDS[0]} ) == python?([1-9]\.[0-9]) ) &&
- ( $( basename ${COMP_WORDS[1]} ) == manage.py) &&
+ ( $( basename -- ${COMP_WORDS[0]} ) == python?([1-9]\.[0-9]) ) &&
+ ( $( basename -- ${COMP_WORDS[1]} ) == manage.py) &&
( -r ${COMP_WORDS[1]} ) )
||
( ${COMP_CWORD} -eq 2 &&
- ( $( basename ${COMP_WORDS[0]} ) == python?([1-9]\.[0-9]) ) &&
- ( $( basename ${COMP_WORDS[1]} ) == django-admin.py) &&
+ ( $( basename -- ${COMP_WORDS[0]} ) == python?([1-9]\.[0-9]) ) &&
+ ( $( basename -- ${COMP_WORDS[1]} ) == django-admin.py) &&
( -r ${COMP_WORDS[1]} ) ) ]] ; then
case ${cur} in
@@ -149,7 +149,7 @@ unset pythons
if command -v whereis &>/dev/null; then
python_interpreters=$(whereis python | cut -d " " -f 2-)
for python in $python_interpreters; do
- pythons="${pythons} $(basename $python)"
+ pythons="${pythons} $(basename -- $python)"
done
pythons=$(echo $pythons | tr " " "\n" | sort -u | tr "\n" " ")
else
diff --git a/tests/modeltests/mutually_referential/models.py b/tests/modeltests/mutually_referential/models.py
index 7cf7bf8bb2..5176721f3d 100644
--- a/tests/modeltests/mutually_referential/models.py
+++ b/tests/modeltests/mutually_referential/models.py
@@ -1,18 +1,22 @@
"""
24. Mutually referential many-to-one relationships
-To define a many-to-one relationship, use ``ForeignKey()`` .
+Strings can be used instead of model literals to set up "lazy" relations.
"""
from django.db.models import *
class Parent(Model):
name = CharField(max_length=100, core=True)
+
+ # Use a simple string for forward declarations.
bestchild = ForeignKey("Child", null=True, related_name="favoured_by")
class Child(Model):
name = CharField(max_length=100)
- parent = ForeignKey(Parent)
+
+ # You can also explicitally specify the related app.
+ parent = ForeignKey("mutually_referential.Parent")
__test__ = {'API_TESTS':"""
# Create a Parent
diff --git a/tests/regressiontests/decorators/__init__.py b/tests/regressiontests/decorators/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/regressiontests/decorators/models.py b/tests/regressiontests/decorators/models.py
new file mode 100644
index 0000000000..e5a795067b
--- /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 0000000000..0c434772f8
--- /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__')