From 271bfe65d986f5ecbaeb7a70a3092356c0a9e222 Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Tue, 19 Jul 2016 14:55:59 -0400 Subject: [PATCH] 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. --- django/db/models/query.py | 9 +++++++-- tests/prefetch_related/models.py | 5 +++++ tests/prefetch_related/tests.py | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/django/db/models/query.py b/django/db/models/query.py index cff7135ef6..05f6ed370d 100644 --- a/django/db/models/query.py +++ b/django/db/models/query.py @@ -25,7 +25,7 @@ from django.db.models.query_utils import ( from django.db.models.sql.constants import CURSOR from django.utils import six, timezone 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 # 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'): prefetcher = rel_obj 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: is_fetched = through_attr in instance._prefetched_objects_cache return prefetcher, rel_obj_descriptor, attr_found, is_fetched diff --git a/tests/prefetch_related/models.py b/tests/prefetch_related/models.py index 32570e9109..064ce1dfbd 100644 --- a/tests/prefetch_related/models.py +++ b/tests/prefetch_related/models.py @@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import ( from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.encoding import python_2_unicode_compatible +from django.utils.functional import cached_property # Basic tests @@ -219,6 +220,10 @@ class Person(models.Model): def all_houses(self): return list(self.houses.all()) + @cached_property + def cached_all_houses(self): + return self.all_houses + class Meta: ordering = ['id'] diff --git a/tests/prefetch_related/tests.py b/tests/prefetch_related/tests.py index c34682a33d..7c36975084 100644 --- a/tests/prefetch_related/tests.py +++ b/tests/prefetch_related/tests.py @@ -743,6 +743,17 @@ class CustomPrefetchTests(TestCase): ).get(pk=self.house3.pk) 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):