1
0
mirror of https://github.com/django/django.git synced 2025-10-24 06:06:09 +00:00

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.
This commit is contained in:
Keryn Knight
2021-08-05 19:11:14 +01:00
committed by Mariusz Felisiak
parent 3ff7b15bb7
commit a697424969
11 changed files with 182 additions and 29 deletions

View File

@@ -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__)