diff --git a/djblets/datagrid/grids.py b/djblets/datagrid/grids.py
index eeb1e43173198cc023b755fbb1181e15e26811eb..2f1595c326834e0f7e4e1b5a22a6ae914782808f 100644
--- a/djblets/datagrid/grids.py
+++ b/djblets/datagrid/grids.py
@@ -65,7 +65,6 @@ from django.template.defaultfilters import date, timesince
 from django.template.loader import render_to_string, get_template
 from django.utils import six
 from django.utils.cache import patch_cache_control
-from django.utils.functional import cached_property
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext_lazy as _
@@ -83,6 +82,7 @@ except ImportError:
     # Django < 1.8
     template_engines = None
 
+from djblets.util.decorators import cached_property
 from djblets.util.http import get_url_params_except
 
 
diff --git a/djblets/util/decorators.py b/djblets/util/decorators.py
index 559bb7dc80ffeef150dd7da29aca53d62d8f242a..17639c7337f1e488d7fc708ddb4463af75c7d75d 100644
--- a/djblets/util/decorators.py
+++ b/djblets/util/decorators.py
@@ -33,6 +33,7 @@ import warnings
 from django import template
 from django.conf import settings
 from django.template import TemplateSyntaxError, Variable
+from django.utils.functional import cached_property as django_cached_property
 
 
 # The decorator decorator.  This is copyright unknown, verbatim from
@@ -237,6 +238,31 @@ def blocktag(*args, **kwargs):
         return _blocktag_func
 
 
+class cached_property(django_cached_property):
+    """Decorator for creating a read-only property that caches a value.
+
+    This is a drop-in replacement for Django's
+    :py:class:`~django.utils.functional.cached_property` that retains the
+    docstring and attributes of the original method.
+
+    While Django 1.8+ does retain the docstring, it does not retain the
+    attributes.
+    """
+
+    def __init__(self, func):
+        """Initialize the property.
+
+        Args:
+            func (callable):
+                The function that will be called when this property is
+                accessed. The property will have its name, documentation,
+                and other attributes.
+        """
+        super(cached_property, self).__init__(func)
+
+        update_wrapper(self, func)
+
+
 @simple_decorator
 def root_url(url_func):
     """Decorates a function that returns a URL to add the SITE_ROOT."""
diff --git a/djblets/util/tests.py b/djblets/util/tests.py
index 00b22252b2793551d80d9f889f53aa0d024ea160..fcab87f6bf68e861d1209b15afee0797573641b7 100644
--- a/djblets/util/tests.py
+++ b/djblets/util/tests.py
@@ -34,6 +34,7 @@ from django.template import Context, Template, TemplateSyntaxError
 from django.utils.html import strip_spaces_between_tags
 
 from djblets.testing.testcases import TestCase, TagTest
+from djblets.util.decorators import cached_property
 from djblets.util.http import (get_http_accept_lists,
                                get_http_requested_mimetype,
                                is_mimetype_a)
@@ -329,3 +330,43 @@ class SerializerTest(TestCase):
         encoder = DjbletsJSONEncoder()
 
         self.assertEqual(encoder.encode(obj), '{"foo": 1}')
+
+
+class DecoratorTests(TestCase):
+    """Tests for djblets.util.decorators."""
+
+    def test_cached_property(self):
+        """Testing @cached_property retains attributes and docstring"""
+        class MyClass(object):
+            def expensive_method(self, state=[0]):
+                state[0] += 1
+
+                return state[0]
+
+            def my_prop1(self):
+                """This is my docstring."""
+                return self.expensive_method()
+
+            my_prop1.some_attr = 105
+            my_prop1 = cached_property(my_prop1)
+
+            @cached_property
+            def my_prop2(self):
+                """Another one!"""
+                return 'foo'
+
+        instance = MyClass()
+
+        self.assertEqual(instance.my_prop1, 1)
+        self.assertEqual(instance.my_prop1, 1)
+        self.assertEqual(instance.my_prop2, 'foo')
+
+        prop1_instance = instance.__class__.__dict__['my_prop1']
+        self.assertEqual(prop1_instance.__name__, 'my_prop1')
+        self.assertEqual(prop1_instance.__doc__, 'This is my docstring.')
+        self.assertEqual(getattr(prop1_instance, 'some_attr'), 105)
+
+        prop2_instance = instance.__class__.__dict__['my_prop2']
+        self.assertEqual(prop2_instance.__name__, 'my_prop2')
+        self.assertEqual(prop2_instance.__doc__, 'Another one!')
+        self.assertFalse(hasattr(prop2_instance, 'some_attr'))
