summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Kraft <benjaminjkraft@gmail.com>2016-01-23 18:02:15 -0800
committerTim Graham <timograham@gmail.com>2016-01-26 06:56:21 -0500
commit13023ba86746980aace2341ba32a9419e7567751 (patch)
tree77ee5f8c4583904508bdabbdcaf638756d800e0f
parentcfda1fa3f8d95f0f4a369da9021dbd770e5fa44a (diff)
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.
-rw-r--r--django/utils/functional.py20
-rw-r--r--docs/releases/1.8.9.txt3
-rw-r--r--docs/releases/1.9.2.txt3
-rw-r--r--tests/utils_tests/test_lazyobject.py85
4 files changed, 104 insertions, 7 deletions
diff --git a/django/utils/functional.py b/django/utils/functional.py
index 2830cc47ab..f674a9bba2 100644
--- a/django/utils/functional.py
+++ b/django/utils/functional.py
@@ -249,6 +249,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)
@@ -301,6 +303,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
@@ -377,6 +388,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/docs/releases/1.9.2.txt b/docs/releases/1.9.2.txt
index 30527c146a..6d0288a337 100644
--- a/docs/releases/1.9.2.txt
+++ b/docs/releases/1.9.2.txt
@@ -82,3 +82,6 @@ Bugfixes
origin from the node via ``Node.token.source[0]``. This was an undocumented,
private API. The origin is now available directly on each node using the
``Node.origin`` attribute (:ticket:`25848`).
+
+* 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 e0f043318c..62d7964fcf 100644
--- a/tests/utils_tests/test_lazyobject.py
+++ b/tests/utils_tests/test_lazyobject.py
@@ -194,28 +194,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.deepcopy(obj)
+ obj2 = copy.copy(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_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_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)