From 3db3ab71e97d34260057a6f51d4b2f72da30dc8d Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Mon, 19 Oct 2015 14:17:55 -0400 Subject: [PATCH] Fixed #25563 -- Cached deferred models in their proxied model's _meta.apps. Thanks to Andriy Sokolovskiy for the report and Tim Graham for the review. --- django/db/models/query_utils.py | 9 +++++---- docs/releases/1.8.6.txt | 5 +++++ tests/defer_regress/tests.py | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py index d9040d0843..8e303e8f8a 100644 --- a/django/db/models/query_utils.py +++ b/django/db/models/query_utils.py @@ -10,7 +10,6 @@ from __future__ import unicode_literals import inspect from collections import namedtuple -from django.apps import apps from django.core.exceptions import FieldDoesNotExist from django.db.backends import utils from django.db.models.constants import LOOKUP_SEP @@ -272,12 +271,13 @@ def deferred_class_factory(model, attrs): """ if not attrs: return model + opts = model._meta # Never create deferred models based on deferred model if model._deferred: # Deferred models are proxies for the non-deferred model. We never # create chains of defers => proxy_for_model is the non-deferred # model. - model = model._meta.proxy_for_model + model = opts.proxy_for_model # The app registry wants a unique name for each model, otherwise the new # class won't be created (we get an exception). Therefore, we generate # the name using the passed in attrs. It's OK to reuse an existing class @@ -286,13 +286,14 @@ def deferred_class_factory(model, attrs): name = utils.truncate_name(name, 80, 32) try: - return apps.get_model(model._meta.app_label, name) + return opts.apps.get_model(model._meta.app_label, name) except LookupError: class Meta: proxy = True - app_label = model._meta.app_label + apps = opts.apps + app_label = opts.app_label overrides = {attr: DeferredAttribute(attr, model) for attr in attrs} overrides["Meta"] = Meta diff --git a/docs/releases/1.8.6.txt b/docs/releases/1.8.6.txt index 8fb9e16be5..1c2af01c7d 100644 --- a/docs/releases/1.8.6.txt +++ b/docs/releases/1.8.6.txt @@ -29,3 +29,8 @@ Bugfixes * Avoided a confusing stack trace when starting :djadmin:`runserver` with an invalid :setting:`INSTALLED_APPS` setting (:ticket:`25510`). This regression appeared in 1.8.5 as a side effect of fixing :ticket:`24704`. + +* Made deferred models use their proxied model's ``_meta.apps`` for caching + and retrieval (:ticket:`25563`). This prevents any models generated in data + migrations using ``QuerySet.defer()`` from leaking to test and application + code. diff --git a/tests/defer_regress/tests.py b/tests/defer_regress/tests.py index d8c6e01884..cc1658ce58 100644 --- a/tests/defer_regress/tests.py +++ b/tests/defer_regress/tests.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals from operator import attrgetter from django.apps import apps +from django.apps.registry import Apps from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.backends.db import SessionStore +from django.db import models from django.db.models import Count from django.db.models.query_utils import ( DeferredAttribute, deferred_class_factory, @@ -263,6 +265,24 @@ class DeferRegressionTest(TestCase): deferred_cls = deferred_class_factory(Item, ()) self.assertFalse(deferred_cls._deferred) + def test_deferred_class_factory_apps_reuse(self): + """ + #25563 - model._meta.apps should be used for caching and + retrieval of the created proxy class. + """ + isolated_apps = Apps(['defer_regress']) + + class BaseModel(models.Model): + field = models.BooleanField() + + class Meta: + apps = isolated_apps + app_label = 'defer_regress' + + deferred_model = deferred_class_factory(BaseModel, ['field']) + self.assertIs(deferred_model._meta.apps, isolated_apps) + self.assertIs(deferred_class_factory(BaseModel, ['field']), deferred_model) + class DeferAnnotateSelectRelatedTest(TestCase): def test_defer_annotate_select_related(self):