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:
committed by
Mariusz Felisiak
parent
3ff7b15bb7
commit
a697424969
@@ -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__)
|
||||
|
||||
Reference in New Issue
Block a user