From 79c3950562a3f9c9d6d5e37990dfa9eb84052de7 Mon Sep 17 00:00:00 2001
From: Ben Kraft <benjaminjkraft@gmail.com>
Date: Sat, 23 Jan 2016 18:02:15 -0800
Subject: [PATCH] [1.8.x] Fixed #26122 -- Fixed copying a LazyObject

Shallow copying of `django.utils.functional.LazyObject` or its subclasses has
been broken in a couple of different ways in the past, most recently due to
35355a4.
---
 django/utils/functional.py           | 20 +++++++
 docs/releases/1.8.9.txt              |  3 +
 tests/utils_tests/test_lazyobject.py | 85 +++++++++++++++++++++++++---
 3 files changed, 101 insertions(+), 7 deletions(-)

diff --git a/django/utils/functional.py b/django/utils/functional.py
index a3c0b88edd..b230273b15 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -240,6 +240,8 @@ class LazyObject(object):
     _wrapped = None
 
     def __init__(self):
+        # Note: if a subclass overrides __init__(), it will likely need to
+        # override __copy__() and __deepcopy__() as well.
         self._wrapped = empty
 
     __getattr__ = new_method_proxy(getattr)
@@ -292,6 +294,15 @@ class LazyObject(object):
     def __getstate__(self):
         return {}
 
+    def __copy__(self):
+        if self._wrapped is empty:
+            # If uninitialized, copy the wrapper. Use type(self), not
+            # self.__class__, because the latter is proxied.
+            return type(self)()
+        else:
+            # If initialized, return a copy of the wrapped object.
+            return copy.copy(self._wrapped)
+
     def __deepcopy__(self, memo):
         if self._wrapped is empty:
             # We have to use type(self), not self.__class__, because the
@@ -373,6 +384,15 @@ class SimpleLazyObject(LazyObject):
             repr_attr = self._wrapped
         return '<%s: %r>' % (type(self).__name__, repr_attr)
 
+    def __copy__(self):
+        if self._wrapped is empty:
+            # If uninitialized, copy the wrapper. Use SimpleLazyObject, not
+            # self.__class__, because the latter is proxied.
+            return SimpleLazyObject(self._setupfunc)
+        else:
+            # If initialized, return a copy of the wrapped object.
+            return copy.copy(self._wrapped)
+
     def __deepcopy__(self, memo):
         if self._wrapped is empty:
             # We have to use SimpleLazyObject, not self.__class__, because the
diff --git a/docs/releases/1.8.9.txt b/docs/releases/1.8.9.txt
index 752dc409c4..0009de0bdb 100644
--- a/docs/releases/1.8.9.txt
+++ b/docs/releases/1.8.9.txt
@@ -31,3 +31,6 @@ Bugfixes
 
 * Fixed a crash when using a reverse ``OneToOneField`` in
   ``ModelAdmin.readonly_fields`` (:ticket:`26060`).
+
+* Fixed a regression in Django 1.8.5 that broke copying a ``SimpleLazyObject``
+  with ``copy.copy()`` (:ticket:`26122`).
diff --git a/tests/utils_tests/test_lazyobject.py b/tests/utils_tests/test_lazyobject.py
index f975e605d2..52e402783e 100644
--- a/tests/utils_tests/test_lazyobject.py
+++ b/tests/utils_tests/test_lazyobject.py
@@ -183,28 +183,99 @@ class LazyObjectTestCase(TestCase):
         self.assertEqual(unpickled, obj)
         self.assertEqual(unpickled.foo, obj.foo)
 
-    def test_deepcopy(self):
-        # Check that we *can* do deep copy, and that it returns the right
-        # objects.
+    # Test copying lazy objects wrapping both builtin types and user-defined
+    # classes since a lot of the relevant code does __dict__ manipulation and
+    # builtin types don't have __dict__.
 
+    def test_copy_list(self):
+        # Copying a list works and returns the correct objects.
+        l = [1, 2, 3]
+
+        obj = self.lazy_wrap(l)
+        len(l)  # forces evaluation
+        obj2 = copy.copy(obj)
+
+        self.assertIsNot(obj, obj2)
+        self.assertIsInstance(obj2, list)
+        self.assertEqual(obj2, [1, 2, 3])
+
+    def test_copy_list_no_evaluation(self):
+        # Copying a list doesn't force evaluation.
+        l = [1, 2, 3]
+
+        obj = self.lazy_wrap(l)
+        obj2 = copy.copy(obj)
+
+        self.assertIsNot(obj, obj2)
+        self.assertIs(obj._wrapped, empty)
+        self.assertIs(obj2._wrapped, empty)
+
+    def test_copy_class(self):
+        # Copying a class works and returns the correct objects.
+        foo = Foo()
+
+        obj = self.lazy_wrap(foo)
+        str(foo)  # forces evaluation
+        obj2 = copy.copy(obj)
+
+        self.assertIsNot(obj, obj2)
+        self.assertIsInstance(obj2, Foo)
+        self.assertEqual(obj2, Foo())
+
+    def test_copy_class_no_evaluation(self):
+        # Copying a class doesn't force evaluation.
+        foo = Foo()
+
+        obj = self.lazy_wrap(foo)
+        obj2 = copy.copy(obj)
+
+        self.assertIsNot(obj, obj2)
+        self.assertIs(obj._wrapped, empty)
+        self.assertIs(obj2._wrapped, empty)
+
+    def test_deepcopy_list(self):
+        # Deep copying a list works and returns the correct objects.
         l = [1, 2, 3]
 
         obj = self.lazy_wrap(l)
         len(l)  # forces evaluation
         obj2 = copy.deepcopy(obj)
 
+        self.assertIsNot(obj, obj2)
         self.assertIsInstance(obj2, list)
         self.assertEqual(obj2, [1, 2, 3])
 
-    def test_deepcopy_no_evaluation(self):
-        # copying doesn't force evaluation
-
+    def test_deepcopy_list_no_evaluation(self):
+        # Deep copying doesn't force evaluation.
         l = [1, 2, 3]
 
         obj = self.lazy_wrap(l)
         obj2 = copy.deepcopy(obj)
 
-        # Copying shouldn't force evaluation
+        self.assertIsNot(obj, obj2)
+        self.assertIs(obj._wrapped, empty)
+        self.assertIs(obj2._wrapped, empty)
+
+    def test_deepcopy_class(self):
+        # Deep copying a class works and returns the correct objects.
+        foo = Foo()
+
+        obj = self.lazy_wrap(foo)
+        str(foo)  # forces evaluation
+        obj2 = copy.deepcopy(obj)
+
+        self.assertIsNot(obj, obj2)
+        self.assertIsInstance(obj2, Foo)
+        self.assertEqual(obj2, Foo())
+
+    def test_deepcopy_class_no_evaluation(self):
+        # Deep copying doesn't force evaluation.
+        foo = Foo()
+
+        obj = self.lazy_wrap(foo)
+        obj2 = copy.deepcopy(obj)
+
+        self.assertIsNot(obj, obj2)
         self.assertIs(obj._wrapped, empty)
         self.assertIs(obj2._wrapped, empty)