diff --git a/django/utils/functional.py b/django/utils/functional.py index 2830cc47ab6..f674a9bba23 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 752dc409c43..0009de0bdba 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 30527c146a3..6d0288a3379 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 e0f043318c9..62d7964fcf3 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.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)