Refs #16508 -- Renamed the current "virtual" fields to "private".

The only reason why GenericForeignKey and GenericRelation are stored
separately inside _meta is that they need to be cloned for every model
subclass, but that's not true for any other virtual field. Actually,
it's only true for GenericRelation.
This commit is contained in:
Michal Petrucha 2016-03-20 18:10:55 +01:00 committed by Tim Graham
parent 47fbbc33de
commit c339a5a6f7
14 changed files with 86 additions and 39 deletions

View File

@ -24,7 +24,7 @@ class GenericInlineModelAdminChecks(InlineModelAdminChecks):
# and that they are part of a GenericForeignKey. # and that they are part of a GenericForeignKey.
gfks = [ gfks = [
f for f in obj.model._meta.virtual_fields f for f in obj.model._meta.private_fields
if isinstance(f, GenericForeignKey) if isinstance(f, GenericForeignKey)
] ]
if len(gfks) == 0: if len(gfks) == 0:

View File

@ -53,7 +53,7 @@ class GenericForeignKey(object):
self.name = name self.name = name
self.model = cls self.model = cls
self.cache_attr = "_%s_cache" % name self.cache_attr = "_%s_cache" % name
cls._meta.add_field(self, virtual=True) cls._meta.add_field(self, private=True)
# Only run pre-initialization field assignment on non-abstract models # Only run pre-initialization field assignment on non-abstract models
if not cls._meta.abstract: if not cls._meta.abstract:
@ -331,7 +331,7 @@ class GenericRelation(ForeignObject):
def _check_generic_foreign_key_existence(self): def _check_generic_foreign_key_existence(self):
target = self.remote_field.model target = self.remote_field.model
if isinstance(target, ModelBase): if isinstance(target, ModelBase):
fields = target._meta.virtual_fields fields = target._meta.private_fields
if any(isinstance(field, GenericForeignKey) and if any(isinstance(field, GenericForeignKey) and
field.ct_field == self.content_type_field_name and field.ct_field == self.content_type_field_name and
field.fk_field == self.object_id_field_name field.fk_field == self.object_id_field_name
@ -409,7 +409,7 @@ class GenericRelation(ForeignObject):
return smart_text([instance._get_pk_val() for instance in qs]) return smart_text([instance._get_pk_val() for instance in qs])
def contribute_to_class(self, cls, name, **kwargs): def contribute_to_class(self, cls, name, **kwargs):
kwargs['virtual_only'] = True kwargs['private_only'] = True
super(GenericRelation, self).contribute_to_class(cls, name, **kwargs) super(GenericRelation, self).contribute_to_class(cls, name, **kwargs)
self.model = cls self.model = cls
setattr(cls, self.name, ReverseGenericManyToOneDescriptor(self.remote_field)) setattr(cls, self.name, ReverseGenericManyToOneDescriptor(self.remote_field))

View File

@ -161,7 +161,7 @@ class ModelBase(type):
new_fields = chain( new_fields = chain(
new_class._meta.local_fields, new_class._meta.local_fields,
new_class._meta.local_many_to_many, new_class._meta.local_many_to_many,
new_class._meta.virtual_fields new_class._meta.private_fields
) )
field_names = {f.name for f in new_fields} field_names = {f.name for f in new_fields}
@ -268,9 +268,9 @@ class ModelBase(type):
if is_proxy: if is_proxy:
new_class.copy_managers(original_base._meta.concrete_managers) new_class.copy_managers(original_base._meta.concrete_managers)
# Inherit virtual fields (like GenericForeignKey) from the parent # Inherit private fields (like GenericForeignKey) from the parent
# class # class
for field in base._meta.virtual_fields: for field in base._meta.private_fields:
if base._meta.abstract and field.name in field_names: if base._meta.abstract and field.name in field_names:
raise FieldError( raise FieldError(
'Local field %r in class %r clashes ' 'Local field %r in class %r clashes '

View File

@ -147,7 +147,7 @@ class Collector(object):
for related in get_candidate_relations_to_delete(opts): for related in get_candidate_relations_to_delete(opts):
if related.field.remote_field.on_delete is not DO_NOTHING: if related.field.remote_field.on_delete is not DO_NOTHING:
return False return False
for field in model._meta.virtual_fields: for field in model._meta.private_fields:
if hasattr(field, 'bulk_related_objects'): if hasattr(field, 'bulk_related_objects'):
# It's something like generic foreign key. # It's something like generic foreign key.
return False return False
@ -221,7 +221,7 @@ class Collector(object):
self.fast_deletes.append(sub_objs) self.fast_deletes.append(sub_objs)
elif sub_objs: elif sub_objs:
field.remote_field.on_delete(self, field, sub_objs, self.using) field.remote_field.on_delete(self, field, sub_objs, self.using)
for field in model._meta.virtual_fields: for field in model._meta.private_fields:
if hasattr(field, 'bulk_related_objects'): if hasattr(field, 'bulk_related_objects'):
# It's something like generic foreign key. # It's something like generic foreign key.
sub_objs = field.bulk_related_objects(new_objs, self.using) sub_objs = field.bulk_related_objects(new_objs, self.using)

View File

@ -672,11 +672,25 @@ class Field(RegisterLookupMixin):
if self.verbose_name is None and self.name: if self.verbose_name is None and self.name:
self.verbose_name = self.name.replace('_', ' ') self.verbose_name = self.name.replace('_', ' ')
def contribute_to_class(self, cls, name, virtual_only=False): def contribute_to_class(self, cls, name, private_only=False, virtual_only=NOT_PROVIDED):
"""
Register the field with the model class it belongs to.
If private_only is True, a separate instance of this field will be
created for every subclass of cls, even if cls is not an abstract
model.
"""
if virtual_only is not NOT_PROVIDED:
warnings.warn(
"The `virtual_only` argument of Field.contribute_to_class() "
"has been renamed to `private_only`.",
RemovedInDjango20Warning, stacklevel=2
)
private_only = virtual_only
self.set_attributes_from_name(name) self.set_attributes_from_name(name)
self.model = cls self.model = cls
if virtual_only: if private_only:
cls._meta.add_field(self, virtual=True) cls._meta.add_field(self, private=True)
else: else:
cls._meta.add_field(self) cls._meta.add_field(self)
if self.choices: if self.choices:

View File

@ -282,9 +282,9 @@ class RelatedField(Field):
# columns from another table. # columns from another table.
return None return None
def contribute_to_class(self, cls, name, virtual_only=False): def contribute_to_class(self, cls, name, private_only=False, **kwargs):
super(RelatedField, self).contribute_to_class(cls, name, virtual_only=virtual_only) super(RelatedField, self).contribute_to_class(cls, name, private_only=private_only, **kwargs)
self.opts = cls._meta self.opts = cls._meta
@ -703,8 +703,8 @@ class ForeignObject(RelatedField):
def get_defaults(self): def get_defaults(self):
return tuple(field.get_default() for field in self.local_related_fields) return tuple(field.get_default() for field in self.local_related_fields)
def contribute_to_class(self, cls, name, virtual_only=False): def contribute_to_class(self, cls, name, private_only=False, **kwargs):
super(ForeignObject, self).contribute_to_class(cls, name, virtual_only=virtual_only) super(ForeignObject, self).contribute_to_class(cls, name, private_only=private_only, **kwargs)
setattr(cls, self.name, ForwardManyToOneDescriptor(self)) setattr(cls, self.name, ForwardManyToOneDescriptor(self))
def contribute_to_related_class(self, cls, related): def contribute_to_related_class(self, cls, related):

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import warnings
from bisect import bisect from bisect import bisect
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from itertools import chain from itertools import chain
@ -12,6 +13,9 @@ from django.db.models.fields import AutoField
from django.db.models.fields.proxy import OrderWrt from django.db.models.fields.proxy import OrderWrt
from django.utils import six from django.utils import six
from django.utils.datastructures import ImmutableList, OrderedSet from django.utils.datastructures import ImmutableList, OrderedSet
from django.utils.deprecation import (
RemovedInDjango20Warning, warn_about_renamed_method,
)
from django.utils.encoding import ( from django.utils.encoding import (
force_text, python_2_unicode_compatible, smart_text, force_text, python_2_unicode_compatible, smart_text,
) )
@ -19,6 +23,8 @@ from django.utils.functional import cached_property
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
NOT_PROVIDED = object()
PROXY_PARENTS = object() PROXY_PARENTS = object()
EMPTY_RELATION_TREE = tuple() EMPTY_RELATION_TREE = tuple()
@ -76,7 +82,7 @@ class Options(object):
self._get_fields_cache = {} self._get_fields_cache = {}
self.local_fields = [] self.local_fields = []
self.local_many_to_many = [] self.local_many_to_many = []
self.virtual_fields = [] self.private_fields = []
self.model_name = None self.model_name = None
self.verbose_name = None self.verbose_name = None
self.verbose_name_plural = None self.verbose_name_plural = None
@ -253,13 +259,19 @@ class Options(object):
auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True) auto = AutoField(verbose_name='ID', primary_key=True, auto_created=True)
model.add_to_class('id', auto) model.add_to_class('id', auto)
def add_field(self, field, virtual=False): def add_field(self, field, private=False, virtual=NOT_PROVIDED):
if virtual is not NOT_PROVIDED:
warnings.warn(
"The `virtual` argument of Options.add_field() has been renamed to `private`.",
RemovedInDjango20Warning, stacklevel=2
)
private = virtual
# Insert the given field in the order in which it was created, using # Insert the given field in the order in which it was created, using
# the "creation_counter" attribute of the field. # the "creation_counter" attribute of the field.
# Move many-to-many related fields from self.fields into # Move many-to-many related fields from self.fields into
# self.many_to_many. # self.many_to_many.
if virtual: if private:
self.virtual_fields.append(field) self.private_fields.append(field)
elif field.is_relation and field.many_to_many: elif field.is_relation and field.many_to_many:
self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field) self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
else: else:
@ -365,7 +377,7 @@ class Options(object):
obtaining this field list. obtaining this field list.
""" """
# For legacy reasons, the fields property should only contain forward # For legacy reasons, the fields property should only contain forward
# fields that are not virtual or with a m2m cardinality. Therefore we # fields that are not private or with a m2m cardinality. Therefore we
# pass these three filters as filters to the generator. # pass these three filters as filters to the generator.
# The third lambda is a longwinded way of checking f.related_model - we don't # The third lambda is a longwinded way of checking f.related_model - we don't
# use that property directly because related_model is a cached property, # use that property directly because related_model is a cached property,
@ -401,6 +413,14 @@ class Options(object):
"concrete_fields", (f for f in self.fields if f.concrete) "concrete_fields", (f for f in self.fields if f.concrete)
) )
@property
@warn_about_renamed_method(
'Options', 'virtual_fields', 'private_fields',
RemovedInDjango20Warning
)
def virtual_fields(self):
return self.private_fields
@cached_property @cached_property
def local_concrete_fields(self): def local_concrete_fields(self):
""" """
@ -686,14 +706,14 @@ class Options(object):
fields.extend( fields.extend(
field for field in chain(self.local_fields, self.local_many_to_many) field for field in chain(self.local_fields, self.local_many_to_many)
) )
# Virtual fields are recopied to each child model, and they get a # Private fields are recopied to each child model, and they get a
# different model as field.model in each child. Hence we have to # different model as field.model in each child. Hence we have to
# add the virtual fields separately from the topmost call. If we # add the private fields separately from the topmost call. If we
# did this recursively similar to local_fields, we would get field # did this recursively similar to local_fields, we would get field
# instances with field.model != self.model. # instances with field.model != self.model.
if topmost_call: if topmost_call:
fields.extend( fields.extend(
f for f in self.virtual_fields f for f in self.private_fields
) )
# In order to avoid list manipulation. Always # In order to avoid list manipulation. Always

View File

@ -81,7 +81,7 @@ def model_to_dict(instance, fields=None, exclude=None):
""" """
opts = instance._meta opts = instance._meta
data = {} data = {}
for f in chain(opts.concrete_fields, opts.virtual_fields, opts.many_to_many): for f in chain(opts.concrete_fields, opts.private_fields, opts.many_to_many):
if not getattr(f, 'editable', False): if not getattr(f, 'editable', False):
continue continue
if fields and f.name not in fields: if fields and f.name not in fields:
@ -142,9 +142,8 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
opts = model._meta opts = model._meta
# Avoid circular import # Avoid circular import
from django.db.models.fields import Field as ModelField from django.db.models.fields import Field as ModelField
sortable_virtual_fields = [f for f in opts.virtual_fields sortable_private_fields = [f for f in opts.private_fields if isinstance(f, ModelField)]
if isinstance(f, ModelField)] for f in sorted(chain(opts.concrete_fields, sortable_private_fields, opts.many_to_many)):
for f in sorted(chain(opts.concrete_fields, sortable_virtual_fields, opts.many_to_many)):
if not getattr(f, 'editable', False): if not getattr(f, 'editable', False):
if (fields is not None and f.name in fields and if (fields is not None and f.name in fields and
(exclude is None or f.name not in exclude)): (exclude is None or f.name not in exclude)):
@ -431,9 +430,9 @@ class BaseModelForm(BaseForm):
fields = self._meta.fields fields = self._meta.fields
opts = self.instance._meta opts = self.instance._meta
# Note that for historical reasons we want to include also # Note that for historical reasons we want to include also
# virtual_fields here. (GenericRelation was previously a fake # private_fields here. (GenericRelation was previously a fake
# m2m field). # m2m field).
for f in chain(opts.many_to_many, opts.virtual_fields): for f in chain(opts.many_to_many, opts.private_fields):
if not hasattr(f, 'save_form_data'): if not hasattr(f, 'save_form_data'):
continue continue
if fields and f.name not in fields: if fields and f.name not in fields:

View File

@ -150,6 +150,12 @@ details on these changes.
* Using ``User.is_authenticated()`` and ``User.is_anonymous()`` as methods * Using ``User.is_authenticated()`` and ``User.is_anonymous()`` as methods
will no longer be supported. will no longer be supported.
* The private attribute ``virtual_fields`` of ``Model._meta`` will be removed.
* The private keyword arguments ``virtual_only`` in
``Field.contribute_to_class()`` and ``virtual`` in
``Model._meta.add_field()`` will be removed.
.. _deprecation-removed-in-1.10: .. _deprecation-removed-in-1.10:
1.10 1.10

View File

@ -921,6 +921,14 @@ Miscellaneous
* The template ``Context.has_key()`` method is deprecated in favor of ``in``. * The template ``Context.has_key()`` method is deprecated in favor of ``in``.
* The private attribute ``virtual_fields`` of ``Model._meta`` is
deprecated in favor of ``private_fields``.
* The private keyword arguments ``virtual_only`` in
``Field.contribute_to_class()`` and ``virtual`` in
``Model._meta.add_field()`` are deprecated in favor of ``private_only``
and ``private``, respectively.
.. _removed-features-1.10: .. _removed-features-1.10:
Features removed in 1.10 Features removed in 1.10

View File

@ -64,8 +64,8 @@ class StartsWithRelation(models.ForeignObject):
from_opts = self.remote_field.model._meta from_opts = self.remote_field.model._meta
return [PathInfo(from_opts, to_opts, (to_opts.pk,), self.remote_field, False, False)] return [PathInfo(from_opts, to_opts, (to_opts.pk,), self.remote_field, False, False)]
def contribute_to_class(self, cls, name, virtual_only=False): def contribute_to_class(self, cls, name, private_only=False):
super(StartsWithRelation, self).contribute_to_class(cls, name, virtual_only) super(StartsWithRelation, self).contribute_to_class(cls, name, private_only)
setattr(cls, self.name, ReverseManyToOneDescriptor(self)) setattr(cls, self.name, ReverseManyToOneDescriptor(self))

View File

@ -78,13 +78,13 @@ class FieldFlagsTests(test.SimpleTestCase):
super(FieldFlagsTests, cls).setUpClass() super(FieldFlagsTests, cls).setUpClass()
cls.fields = ( cls.fields = (
list(AllFieldsModel._meta.fields) + list(AllFieldsModel._meta.fields) +
list(AllFieldsModel._meta.virtual_fields) list(AllFieldsModel._meta.private_fields)
) )
cls.all_fields = ( cls.all_fields = (
cls.fields + cls.fields +
list(AllFieldsModel._meta.many_to_many) + list(AllFieldsModel._meta.many_to_many) +
list(AllFieldsModel._meta.virtual_fields) list(AllFieldsModel._meta.private_fields)
) )
cls.fields_and_reverse_objects = ( cls.fields_and_reverse_objects = (

View File

@ -863,7 +863,7 @@ TEST_RESULTS = {
'm2m_concrete_rel', 'm2m_concrete_rel',
], ],
}, },
'virtual_fields': { 'private_fields': {
AbstractPerson: [ AbstractPerson: [
'generic_relation_abstract', 'generic_relation_abstract',
'content_object_abstract', 'content_object_abstract',

View File

@ -157,11 +157,11 @@ class RelatedObjectsTests(OptionsBaseTests):
) )
class VirtualFieldsTests(OptionsBaseTests): class PrivateFieldsTests(OptionsBaseTests):
def test_virtual_fields(self): def test_private_fields(self):
for model, expected_names in TEST_RESULTS['virtual_fields'].items(): for model, expected_names in TEST_RESULTS['private_fields'].items():
objects = model._meta.virtual_fields objects = model._meta.private_fields
self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names)) self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names))