Commit 13023ba8 authored by Ben Kraft's avatar Ben Kraft Committed by Tim Graham
Browse files

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
35355a4f.
parent cfda1fa3
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
@@ -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
+3 −0
Original line number Diff line number Diff line
@@ -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`).
+3 −0
Original line number Diff line number Diff line
@@ -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`).
+78 −7
Original line number Diff line number Diff line
@@ -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)