Fixed #26916 -- Fixed prefetch_related when using a cached_property as to_attr.

Thanks Trac alias karyon for the report and Tim for the review.
This commit is contained in:
Simon Charette 2016-07-19 14:55:59 -04:00
parent 081fdaf110
commit 271bfe65d9
No known key found for this signature in database
GPG Key ID: 72AF89A0B1B4EDB3
3 changed files with 23 additions and 2 deletions

View File

@ -25,7 +25,7 @@ from django.db.models.query_utils import (
from django.db.models.sql.constants import CURSOR from django.db.models.sql.constants import CURSOR
from django.utils import six, timezone from django.utils import six, timezone
from django.utils.deprecation import RemovedInDjango20Warning from django.utils.deprecation import RemovedInDjango20Warning
from django.utils.functional import partition from django.utils.functional import cached_property, partition
from django.utils.version import get_version from django.utils.version import get_version
# The maximum number of items to display in a QuerySet.__repr__ # The maximum number of items to display in a QuerySet.__repr__
@ -1545,7 +1545,12 @@ def get_prefetcher(instance, through_attr, to_attr):
if hasattr(rel_obj, 'get_prefetch_queryset'): if hasattr(rel_obj, 'get_prefetch_queryset'):
prefetcher = rel_obj prefetcher = rel_obj
if through_attr != to_attr: if through_attr != to_attr:
is_fetched = hasattr(instance, to_attr) # Special case cached_property instances because hasattr
# triggers attribute computation and assignment.
if isinstance(getattr(instance.__class__, to_attr, None), cached_property):
is_fetched = to_attr in instance.__dict__
else:
is_fetched = hasattr(instance, to_attr)
else: else:
is_fetched = through_attr in instance._prefetched_objects_cache is_fetched = through_attr in instance._prefetched_objects_cache
return prefetcher, rel_obj_descriptor, attr_found, is_fetched return prefetcher, rel_obj_descriptor, attr_found, is_fetched

View File

@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import (
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
# Basic tests # Basic tests
@ -219,6 +220,10 @@ class Person(models.Model):
def all_houses(self): def all_houses(self):
return list(self.houses.all()) return list(self.houses.all())
@cached_property
def cached_all_houses(self):
return self.all_houses
class Meta: class Meta:
ordering = ['id'] ordering = ['id']

View File

@ -743,6 +743,17 @@ class CustomPrefetchTests(TestCase):
).get(pk=self.house3.pk) ).get(pk=self.house3.pk)
self.assertIsInstance(house.rooms.all(), QuerySet) self.assertIsInstance(house.rooms.all(), QuerySet)
def test_to_attr_cached_property(self):
persons = Person.objects.prefetch_related(
Prefetch('houses', House.objects.all(), to_attr='cached_all_houses'),
)
for person in persons:
# To bypass caching at the related descriptor level, don't use
# person.houses.all() here.
all_houses = list(House.objects.filter(occupants=person))
with self.assertNumQueries(0):
self.assertEqual(person.cached_all_houses, all_houses)
class DefaultManagerTests(TestCase): class DefaultManagerTests(TestCase):