diff options
| author | Keryn Knight <keryn@kerynknight.com> | 2021-08-05 19:11:14 +0100 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2021-11-03 11:27:04 +0100 |
| commit | a697424969f7f464bf6492b09a6cdac135499e02 (patch) | |
| tree | 179e1165bc361e3fe8b822135790ea971449a4d4 /tests/foreign_object/tests.py | |
| parent | 3ff7b15bb79f2ee5b7af245c55ae14546243bb77 (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/tests.py')
| -rw-r--r-- | tests/foreign_object/tests.py | 103 |
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__) |
