Fixed #24328 -- cleaned up Options._get_fields() implementation

This commit is contained in:
Anssi Kääriäinen 2015-02-16 09:49:19 +02:00 committed by Tim Graham
parent bed504d70b
commit bad5f262bf
5 changed files with 106 additions and 105 deletions

View File

@ -239,7 +239,8 @@ class ModelBase(type):
else: else:
# .. and abstract ones. # .. and abstract ones.
for field in parent_fields: for field in parent_fields:
new_class.add_to_class(field.name, copy.deepcopy(field)) new_field = copy.deepcopy(field)
new_class.add_to_class(field.name, new_field)
# Pass any non-abstract parent classes onto child. # Pass any non-abstract parent classes onto child.
new_class._meta.parents.update(base._meta.parents) new_class._meta.parents.update(base._meta.parents)

View File

@ -22,6 +22,8 @@ from django.utils.lru_cache import lru_cache
from django.utils.text import camel_case_to_spaces from django.utils.text import camel_case_to_spaces
from django.utils.translation import override, string_concat from django.utils.translation import override, string_concat
PROXY_PARENTS = object()
EMPTY_RELATION_TREE = tuple() EMPTY_RELATION_TREE = tuple()
IMMUTABLE_WARNING = ( IMMUTABLE_WARNING = (
@ -473,8 +475,6 @@ class Options(object):
@cached_property @cached_property
def _forward_fields_map(self): def _forward_fields_map(self):
res = {} res = {}
# call get_fields() with export_ordered_set=True in order to have a
# field_instance -> names map
fields = self._get_fields(reverse=False) fields = self._get_fields(reverse=False)
for field in fields: for field in fields:
res[field.name] = field res[field.name] = field
@ -566,6 +566,10 @@ class Options(object):
# included in the results. # included in the results.
if field.is_relation and field.many_to_one and field.related_model is None: if field.is_relation and field.many_to_one and field.related_model is None:
continue continue
# Relations to child proxy models should not be included.
if (field.model != self.model and
field.model._meta.concrete_model == self.concrete_model):
continue
names.add(field.name) names.add(field.name)
if hasattr(field, 'attname'): if hasattr(field, 'attname'):
@ -576,14 +580,13 @@ class Options(object):
def get_all_related_objects(self, local_only=False, include_hidden=False, def get_all_related_objects(self, local_only=False, include_hidden=False,
include_proxy_eq=False): include_proxy_eq=False):
include_parents = local_only is False include_parents = True if local_only is False else PROXY_PARENTS
fields = self._get_fields( fields = self._get_fields(
forward=False, reverse=True, forward=False, reverse=True,
include_parents=include_parents, include_parents=include_parents,
include_hidden=include_hidden, include_hidden=include_hidden,
) )
fields = (obj for obj in fields if not isinstance(obj.field, ManyToManyField)) fields = (obj for obj in fields if not isinstance(obj.field, ManyToManyField))
if include_proxy_eq: if include_proxy_eq:
children = chain.from_iterable(c._relation_tree children = chain.from_iterable(c._relation_tree
for c in self.concrete_model._meta.proxied_children for c in self.concrete_model._meta.proxied_children
@ -606,9 +609,10 @@ class Options(object):
@raise_deprecation(suggested_alternative="get_fields()") @raise_deprecation(suggested_alternative="get_fields()")
def get_all_related_many_to_many_objects(self, local_only=False): def get_all_related_many_to_many_objects(self, local_only=False):
include_parents = True if local_only is not True else PROXY_PARENTS
fields = self._get_fields( fields = self._get_fields(
forward=False, reverse=True, forward=False, reverse=True,
include_parents=local_only is not True, include_hidden=True include_parents=include_parents, include_hidden=True
) )
return [obj for obj in fields if isinstance(obj.field, ManyToManyField)] return [obj for obj in fields if isinstance(obj.field, ManyToManyField)]
@ -676,19 +680,16 @@ class Options(object):
all_models = self.apps.get_models(include_auto_created=True) all_models = self.apps.get_models(include_auto_created=True)
for model in all_models: for model in all_models:
# Abstract model's fields are copied to child models, hence we will
# see the fields from the child models.
if model._meta.abstract:
continue
fields_with_relations = ( fields_with_relations = (
f for f in model._meta._get_fields(reverse=False) f for f in model._meta._get_fields(reverse=False, include_parents=False)
if f.is_relation and f.related_model is not None if f.is_relation and f.related_model is not None
) )
if model._meta.auto_created:
fields_with_relations = (
f for f in fields_with_relations
if not f.many_to_many
)
for f in fields_with_relations: for f in fields_with_relations:
if not isinstance(f.rel.to, six.string_types): if not isinstance(f.rel.to, six.string_types):
# Set options_instance -> field
related_objects_graph[f.rel.to._meta].append(f) related_objects_graph[f.rel.to._meta].append(f)
for model in all_models: for model in all_models:
@ -698,24 +699,14 @@ class Options(object):
# @cached_property). This means that the _meta._relation_tree is # @cached_property). This means that the _meta._relation_tree is
# only called if related_objects is not in __dict__. # only called if related_objects is not in __dict__.
related_objects = related_objects_graph[model._meta] related_objects = related_objects_graph[model._meta]
model._meta.__dict__['_relation_tree'] = related_objects
# If related_objects are empty, it makes sense to set # It seems it is possible that self is not in all_models, so guard
# EMPTY_RELATION_TREE. This will avoid allocating multiple empty # against that with default for get().
# relation trees. return self.__dict__.get('_relation_tree', EMPTY_RELATION_TREE)
relation_tree = EMPTY_RELATION_TREE
if related_objects:
relation_tree = related_objects
model._meta.__dict__['_relation_tree'] = relation_tree
@cached_property @cached_property
def _relation_tree(self): def _relation_tree(self):
# If cache is not present, populate the cache return self._populate_directed_relation_graph()
self._populate_directed_relation_graph()
# It may happen, often when the registry is not ready, that a not yet
# registered model is queried. In this very rare case we simply return
# an EMPTY_RELATION_TREE. When the registry will be ready, cache will
# be flushed and this model will be computed properly.
return self.__dict__.get('_relation_tree', EMPTY_RELATION_TREE)
def _expire_cache(self, forward=True, reverse=True): def _expire_cache(self, forward=True, reverse=True):
# This method is usually called by apps.cache_clear(), when the # This method is usually called by apps.cache_clear(), when the
@ -744,16 +735,40 @@ class Options(object):
- include_hidden: include fields that have a related_name that - include_hidden: include fields that have a related_name that
starts with a "+" starts with a "+"
""" """
if include_parents is False:
include_parents = PROXY_PARENTS
return self._get_fields(include_parents=include_parents, include_hidden=include_hidden) return self._get_fields(include_parents=include_parents, include_hidden=include_hidden)
def _get_fields(self, forward=True, reverse=True, include_parents=True, include_hidden=False, def _get_fields(self, forward=True, reverse=True, include_parents=True, include_hidden=False,
export_ordered_set=False): seen_models=None):
"""
Internal helper function to return fields of the model.
* If forward=True, then fields defined on this model are returned.
* If reverse=True, then relations pointing to this model are returned.
* If include_hidden=True, then fields with is_hidden=True are returned.
* The include_parents argument toggles if fields from parent models
should be included. It has three values: True, False, and
PROXY_PARENTS. When set to PROXY_PARENTS, the call will return all
fields defined for the current model or any of its parents in the
parent chain to the model's concrete model.
"""
if include_parents not in (True, False, PROXY_PARENTS):
raise TypeError("Invalid argument for include_parents: %s" % (include_parents,))
# This helper function is used to allow recursion in ``get_fields()`` # This helper function is used to allow recursion in ``get_fields()``
# implementation and to provide a fast way for Django's internals to # implementation and to provide a fast way for Django's internals to
# access specific subsets of fields. # access specific subsets of fields.
# We must keep track of which models we have already seen. Otherwise we
# could include the same field multiple times from different models.
topmost_call = False
if seen_models is None:
seen_models = set()
topmost_call = True
seen_models.add(self.model)
# Creates a cache key composed of all arguments # Creates a cache key composed of all arguments
cache_key = (forward, reverse, include_parents, include_hidden, export_ordered_set) cache_key = (forward, reverse, include_parents, include_hidden, topmost_call)
try: try:
# In order to avoid list manipulation. Always return a shallow copy # In order to avoid list manipulation. Always return a shallow copy
# of the results. # of the results.
@ -761,76 +776,54 @@ class Options(object):
except KeyError: except KeyError:
pass pass
# Using an OrderedDict preserves the order of insertion. This is fields = []
# important when displaying a ModelForm or the contrib.admin panel
# and no specific ordering is provided.
fields = OrderedDict()
options = {
'include_parents': include_parents,
'include_hidden': include_hidden,
'export_ordered_set': True,
}
# Abstract models cannot hold reverse fields.
if reverse and not self.abstract:
if include_parents:
parent_list = self.get_parent_list()
# Recursively call _get_fields() on each parent, with the same # Recursively call _get_fields() on each parent, with the same
# options provided in this call. # options provided in this call.
if include_parents is not False:
for parent in self.parents: for parent in self.parents:
for obj, _ in six.iteritems(parent._meta._get_fields(forward=False, **options)): # In diamond inheritance it is possible that we see the same
if obj.many_to_many: # model from two different routes. In that case, avoid adding
# In order for a reverse ManyToManyRel object to be # fields from the same parent again.
# valid, its creation counter must be > 0 and must if parent in seen_models:
# be in the parent list. continue
if not (obj.field.creation_counter < 0 and obj.related_model not in parent_list): if (parent._meta.concrete_model != self.concrete_model and
fields[obj] = True include_parents == PROXY_PARENTS):
continue
elif not ((obj.field.creation_counter < 0 or obj.field.rel.parent_link) for obj in parent._meta._get_fields(
and obj.related_model not in parent_list): forward=forward, reverse=reverse, include_parents=include_parents,
fields[obj] = True include_hidden=include_hidden, seen_models=seen_models):
if hasattr(obj, 'parent_link') and obj.parent_link:
continue
fields.append(obj)
if reverse:
# Tree is computed once and cached until the app cache is expired. # Tree is computed once and cached until the app cache is expired.
# It is composed of a list of fields pointing to the current model # It is composed of a list of fields pointing to the current model
# from other models. If the model is a proxy model, then we also # from other models.
# add the concrete model. all_fields = self._relation_tree
all_fields = ( for field in all_fields:
self._relation_tree if not self.proxy else
chain(self._relation_tree, self.concrete_model._meta._relation_tree)
)
# Pull out all related objects from forward fields
for field in (f.rel for f in all_fields):
# If hidden fields should be included or the relation is not # If hidden fields should be included or the relation is not
# intentionally hidden, add to the fields dict. # intentionally hidden, add to the fields dict.
if include_hidden or not field.hidden: if include_hidden or not field.rel.hidden:
fields[field] = True fields.append(field.rel)
if forward: if forward:
if include_parents: fields.extend(
for parent in self.parents: field for field in chain(self.local_fields, self.local_many_to_many)
# Add the forward fields of each parent. )
fields.update(parent._meta._get_fields(reverse=False, **options)) # Virtual fields are recopied to each child model, and they get a
fields.update( # different model as field.model in each child. Hence we have to
(field, True,) # add the virtual fields separately from the topmost call. If we
for field in chain(self.local_fields, self.local_many_to_many) # did this recursively similar to local_fields, we would get field
# instances with field.model != self.model.
if topmost_call:
fields.extend(
f for f in self.virtual_fields
) )
if not export_ordered_set: # In order to avoid list manipulation. Always
# By default, fields contains field instances as keys and all # return a shallow copy of the results
# possible names if the field instance as values. When
# _get_fields() is called, we only want to return field instances,
# so we just preserve the keys.
fields = list(fields.keys())
# Virtual fields are not inheritable, therefore they are inserted
# only when the recursive _get_fields() call comes to an end.
if forward:
fields.extend(self.virtual_fields)
fields = make_immutable_fields_list("get_fields()", fields) fields = make_immutable_fields_list("get_fields()", fields)
# Store result into cache for later access # Store result into cache for later access
self._get_fields_cache[cache_key] = fields self._get_fields_cache[cache_key] = fields
# In order to avoid list manipulation. Always
# return a shallow copy of the results
return fields return fields

View File

@ -228,11 +228,9 @@ class RelationTreeTests(TestCase):
sorted([field.related_query_name() for field in Relation._meta._relation_tree sorted([field.related_query_name() for field in Relation._meta._relation_tree
if not field.rel.field.rel.is_hidden()]), if not field.rel.field.rel.is_hidden()]),
sorted([ sorted([
'fk_abstract_rel', 'fk_abstract_rel', 'fk_abstract_rel', 'fk_base_rel', 'fk_base_rel', 'fk_abstract_rel', 'fk_base_rel', 'fk_concrete_rel', 'fo_abstract_rel',
'fk_base_rel', 'fk_concrete_rel', 'fk_concrete_rel', 'fo_abstract_rel', 'fo_abstract_rel', 'fo_base_rel', 'fo_concrete_rel', 'm2m_abstract_rel',
'fo_abstract_rel', 'fo_base_rel', 'fo_base_rel', 'fo_base_rel', 'fo_concrete_rel', 'm2m_base_rel', 'm2m_concrete_rel'
'fo_concrete_rel', 'm2m_abstract_rel', 'm2m_abstract_rel', 'm2m_abstract_rel',
'm2m_base_rel', 'm2m_base_rel', 'm2m_base_rel', 'm2m_concrete_rel', 'm2m_concrete_rel',
]) ])
) )
# Testing hidden related objects # Testing hidden related objects
@ -243,9 +241,8 @@ class RelationTreeTests(TestCase):
'BasePerson_following_base+', 'BasePerson_following_base+', 'BasePerson_friends_abstract+', 'BasePerson_following_base+', 'BasePerson_following_base+', 'BasePerson_friends_abstract+',
'BasePerson_friends_abstract+', 'BasePerson_friends_base+', 'BasePerson_friends_base+', 'BasePerson_friends_abstract+', 'BasePerson_friends_base+', 'BasePerson_friends_base+',
'BasePerson_m2m_abstract+', 'BasePerson_m2m_base+', 'Relating_basepeople+', 'BasePerson_m2m_abstract+', 'BasePerson_m2m_base+', 'Relating_basepeople+',
'Relating_basepeople_hidden+', 'followers_abstract', 'followers_abstract', 'followers_abstract', 'Relating_basepeople_hidden+', 'followers_abstract',
'followers_base', 'followers_base', 'followers_base', 'friends_abstract_rel_+', 'friends_abstract_rel_+', 'followers_base', 'friends_abstract_rel_+', 'friends_base_rel_+',
'friends_abstract_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'person',
'person', 'relating_basepeople', 'relating_baseperson', 'person', 'relating_basepeople', 'relating_baseperson',
]) ])
) )

View File

@ -158,7 +158,7 @@ class ProxyTrackerUser(TrackerUser):
@python_2_unicode_compatible @python_2_unicode_compatible
class Issue(models.Model): class Issue(models.Model):
summary = models.CharField(max_length=255) summary = models.CharField(max_length=255)
assignee = models.ForeignKey(TrackerUser) assignee = models.ForeignKey(ProxyTrackerUser)
def __str__(self): def __str__(self):
return ':'.join((self.__class__.__name__, self.summary,)) return ':'.join((self.__class__.__name__, self.summary,))

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.contrib import admin from django.contrib import admin
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core import checks, management from django.core import checks, exceptions, management
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import DEFAULT_DB_ALIAS, models from django.db import DEFAULT_DB_ALIAS, models
from django.db.models import signals from django.db.models import signals
@ -328,8 +328,18 @@ class ProxyModelTests(TestCase):
resp = StateProxy.objects.select_related().get(name='New South Wales') resp = StateProxy.objects.select_related().get(name='New South Wales')
self.assertEqual(resp.name, 'New South Wales') self.assertEqual(resp.name, 'New South Wales')
def test_filter_proxy_relation_reverse(self):
tu = TrackerUser.objects.create(
name='Contributor', status='contrib')
with self.assertRaises(exceptions.FieldError):
TrackerUser.objects.filter(issue=None),
self.assertQuerysetEqual(
ProxyTrackerUser.objects.filter(issue=None),
[tu], lambda x: x
)
def test_proxy_bug(self): def test_proxy_bug(self):
contributor = TrackerUser.objects.create(name='Contributor', contributor = ProxyTrackerUser.objects.create(name='Contributor',
status='contrib') status='contrib')
someone = BaseUser.objects.create(name='Someone') someone = BaseUser.objects.create(name='Someone')
Bug.objects.create(summary='fix this', version='1.1beta', Bug.objects.create(summary='fix this', version='1.1beta',