summaryrefslogtreecommitdiff
path: root/tests/foreign_object
diff options
context:
space:
mode:
authorKeryn Knight <keryn@kerynknight.com>2021-08-05 19:11:14 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2021-11-03 11:27:04 +0100
commita697424969f7f464bf6492b09a6cdac135499e02 (patch)
tree179e1165bc361e3fe8b822135790ea971449a4d4 /tests/foreign_object
parent3ff7b15bb79f2ee5b7af245c55ae14546243bb77 (diff)
Fixed #32996 -- Cached PathInfos on relations.
PathInfo values are ostensibly static over the lifetime of the object for which they're requested, so the data can be memoized, quickly amortising the cost over the process' duration.
Diffstat (limited to 'tests/foreign_object')
-rw-r--r--tests/foreign_object/tests.py103
1 files changed, 103 insertions, 0 deletions
diff --git a/tests/foreign_object/tests.py b/tests/foreign_object/tests.py
index 72d50cad6b..8b61c87e5d 100644
--- a/tests/foreign_object/tests.py
+++ b/tests/foreign_object/tests.py
@@ -1,4 +1,6 @@
+import copy
import datetime
+import pickle
from operator import attrgetter
from django.core.exceptions import FieldError
@@ -482,3 +484,104 @@ class TestExtraJoinFilterQ(TestCase):
qs = qs.select_related('active_translation_q')
with self.assertNumQueries(1):
self.assertEqual(qs[0].active_translation_q.title, 'title')
+
+
+class TestCachedPathInfo(TestCase):
+ def test_equality(self):
+ """
+ The path_infos and reverse_path_infos attributes are equivalent to
+ calling the get_<method>() with no arguments.
+ """
+ foreign_object = Membership._meta.get_field('person')
+ self.assertEqual(
+ foreign_object.path_infos,
+ foreign_object.get_path_info(),
+ )
+ self.assertEqual(
+ foreign_object.reverse_path_infos,
+ foreign_object.get_reverse_path_info(),
+ )
+
+ def test_copy_removes_direct_cached_values(self):
+ """
+ Shallow copying a ForeignObject (or a ForeignObjectRel) removes the
+ object's direct cached PathInfo values.
+ """
+ foreign_object = Membership._meta.get_field('person')
+ # Trigger storage of cached_property into ForeignObject's __dict__.
+ foreign_object.path_infos
+ foreign_object.reverse_path_infos
+ # The ForeignObjectRel doesn't have reverse_path_infos.
+ foreign_object.remote_field.path_infos
+ self.assertIn('path_infos', foreign_object.__dict__)
+ self.assertIn('reverse_path_infos', foreign_object.__dict__)
+ self.assertIn('path_infos', foreign_object.remote_field.__dict__)
+ # Cached value is removed via __getstate__() on ForeignObjectRel
+ # because no __copy__() method exists, so __reduce_ex__() is used.
+ remote_field_copy = copy.copy(foreign_object.remote_field)
+ self.assertNotIn('path_infos', remote_field_copy.__dict__)
+ # Cached values are removed via __copy__() on ForeignObject for
+ # consistency of behavior.
+ foreign_object_copy = copy.copy(foreign_object)
+ self.assertNotIn('path_infos', foreign_object_copy.__dict__)
+ self.assertNotIn('reverse_path_infos', foreign_object_copy.__dict__)
+ # ForeignObjectRel's remains because it's part of a shallow copy.
+ self.assertIn('path_infos', foreign_object_copy.remote_field.__dict__)
+
+ def test_deepcopy_removes_cached_values(self):
+ """
+ Deep copying a ForeignObject removes the object's cached PathInfo
+ values, including those of the related ForeignObjectRel.
+ """
+ foreign_object = Membership._meta.get_field('person')
+ # Trigger storage of cached_property into ForeignObject's __dict__.
+ foreign_object.path_infos
+ foreign_object.reverse_path_infos
+ # The ForeignObjectRel doesn't have reverse_path_infos.
+ foreign_object.remote_field.path_infos
+ self.assertIn('path_infos', foreign_object.__dict__)
+ self.assertIn('reverse_path_infos', foreign_object.__dict__)
+ self.assertIn('path_infos', foreign_object.remote_field.__dict__)
+ # Cached value is removed via __getstate__() on ForeignObjectRel
+ # because no __deepcopy__() method exists, so __reduce_ex__() is used.
+ remote_field_copy = copy.deepcopy(foreign_object.remote_field)
+ self.assertNotIn('path_infos', remote_field_copy.__dict__)
+ # Field.__deepcopy__() internally uses __copy__() on both the
+ # ForeignObject and ForeignObjectRel, so all cached values are removed.
+ foreign_object_copy = copy.deepcopy(foreign_object)
+ self.assertNotIn('path_infos', foreign_object_copy.__dict__)
+ self.assertNotIn('reverse_path_infos', foreign_object_copy.__dict__)
+ self.assertNotIn('path_infos', foreign_object_copy.remote_field.__dict__)
+
+ def test_pickling_foreignobjectrel(self):
+ """
+ Pickling a ForeignObjectRel removes the path_infos attribute.
+
+ ForeignObjectRel implements __getstate__(), so copy and pickle modules
+ both use that, but ForeignObject implements __reduce__() and __copy__()
+ separately, so doesn't share the same behaviour.
+ """
+ foreign_object_rel = Membership._meta.get_field('person').remote_field
+ # Trigger storage of cached_property into ForeignObjectRel's __dict__.
+ foreign_object_rel.path_infos
+ self.assertIn('path_infos', foreign_object_rel.__dict__)
+ foreign_object_rel_restored = pickle.loads(pickle.dumps(foreign_object_rel))
+ self.assertNotIn('path_infos', foreign_object_rel_restored.__dict__)
+
+ def test_pickling_foreignobject(self):
+ """
+ Pickling a ForeignObject does not remove the cached PathInfo values.
+
+ ForeignObject will always keep the path_infos and reverse_path_infos
+ attributes within the same process, because of the way
+ Field.__reduce__() is used for restoring values.
+ """
+ foreign_object = Membership._meta.get_field('person')
+ # Trigger storage of cached_property into ForeignObjectRel's __dict__
+ foreign_object.path_infos
+ foreign_object.reverse_path_infos
+ self.assertIn('path_infos', foreign_object.__dict__)
+ self.assertIn('reverse_path_infos', foreign_object.__dict__)
+ foreign_object_restored = pickle.loads(pickle.dumps(foreign_object))
+ self.assertIn('path_infos', foreign_object_restored.__dict__)
+ self.assertIn('reverse_path_infos', foreign_object_restored.__dict__)