Browse Source

Fixed #12663 -- Formalized the Model._meta API for retrieving fields.

Thanks to Russell Keith-Magee for mentoring this Google Summer of
Code 2014 project and everyone else who helped with the patch!
Daniel Pyrathon 10 years ago
parent
commit
fb48eb0581
58 changed files with 2852 additions and 1196 deletions
  1. 5 0
      django/apps/registry.py
  2. 1 1
      django/contrib/admin/checks.py
  3. 8 5
      django/contrib/admin/options.py
  4. 1 1
      django/contrib/admin/templatetags/admin_list.py
  5. 21 9
      django/contrib/admin/utils.py
  6. 1 1
      django/contrib/admin/validation.py
  7. 1 1
      django/contrib/admindocs/views.py
  8. 23 4
      django/contrib/contenttypes/fields.py
  9. 1 1
      django/contrib/gis/db/models/query.py
  10. 4 1
      django/contrib/gis/db/models/sql/compiler.py
  11. 1 1
      django/contrib/gis/sitemaps/views.py
  12. 2 3
      django/contrib/gis/utils/layermapping.py
  13. 1 1
      django/contrib/gis/utils/srs.py
  14. 3 3
      django/core/serializers/python.py
  15. 2 2
      django/core/serializers/xml_serializer.py
  16. 2 2
      django/db/backends/creation.py
  17. 26 18
      django/db/backends/schema.py
  18. 2 2
      django/db/backends/sqlite3/schema.py
  19. 13 25
      django/db/migrations/autodetector.py
  20. 10 10
      django/db/migrations/operations/fields.py
  21. 17 17
      django/db/migrations/operations/models.py
  22. 3 3
      django/db/migrations/state.py
  23. 38 31
      django/db/models/base.py
  24. 22 6
      django/db/models/deletion.py
  25. 16 3
      django/db/models/fields/__init__.py
  26. 107 42
      django/db/models/fields/related.py
  27. 2 6
      django/db/models/manager.py
  28. 481 251
      django/db/models/options.py
  29. 15 11
      django/db/models/query.py
  30. 2 2
      django/db/models/query_utils.py
  31. 15 13
      django/db/models/sql/compiler.py
  32. 39 13
      django/db/models/sql/query.py
  33. 5 3
      django/db/models/sql/subqueries.py
  34. 4 3
      django/forms/models.py
  35. 3 3
      docs/howto/custom-model-fields.txt
  36. 13 0
      docs/internals/deprecation.txt
  37. 92 0
      docs/ref/models/fields.txt
  38. 1 0
      docs/ref/models/index.txt
  39. 287 0
      docs/ref/models/meta.txt
  40. 36 0
      docs/releases/1.8.txt
  41. 1 1
      tests/backends/tests.py
  42. 1 1
      tests/basic/tests.py
  43. 2 0
      tests/fixtures/tests.py
  44. 2 1
      tests/generic_relations/tests.py
  45. 1 1
      tests/generic_relations_regress/tests.py
  46. 6 0
      tests/m2m_and_m2o/tests.py
  47. 2 2
      tests/many_to_one/tests.py
  48. 2 2
      tests/migrations/test_state.py
  49. 51 0
      tests/model_fields/models.py
  50. 220 0
      tests/model_fields/test_field_flags.py
  51. 1 1
      tests/model_fields/tests.py
  52. 796 0
      tests/model_meta/results.py
  53. 0 661
      tests/model_meta/test.py
  54. 166 0
      tests/model_meta/test_legacy.py
  55. 247 0
      tests/model_meta/tests.py
  56. 1 1
      tests/queries/tests.py
  57. 1 1
      tests/queryset_pickle/tests.py
  58. 26 26
      tests/schema/tests.py

+ 5 - 0
django/apps/registry.py

@@ -337,7 +337,12 @@ class Apps(object):
 
         This is mostly used in tests.
         """
+        # Call expire cache on each model. This will purge
+        # the relation tree and the fields cache.
         self.get_models.cache_clear()
+        if self.ready:
+            for model in self.get_models(include_auto_created=True):
+                model._meta._expire_cache()
 
     ### DEPRECATED METHODS GO BELOW THIS LINE ###
 

+ 1 - 1
django/contrib/admin/checks.py

@@ -762,7 +762,7 @@ class ModelAdminChecks(BaseModelAdminChecks):
 
     def _check_list_editable_item(self, cls, model, field_name, label):
         try:
-            field = model._meta.get_field_by_name(field_name)[0]
+            field = model._meta.get_field(field_name)
         except FieldDoesNotExist:
             return refer_to_missing_field(field=field_name, option=label,
                                           model=model, obj=cls, id='admin.E121')

+ 8 - 5
django/contrib/admin/options.py

@@ -406,7 +406,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
         rel_name = None
         for part in parts[:-1]:
             try:
-                field, _, _, _ = model._meta.get_field_by_name(part)
+                field = model._meta.get_field(part)
             except FieldDoesNotExist:
                 # Lookups on non-existent fields are ok, since they're ignored
                 # later.
@@ -422,7 +422,7 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
                 else:
                     rel_name = None
             elif isinstance(field, ForeignObjectRel):
-                model = field.model
+                model = field.related_model
                 rel_name = model._meta.pk.name
             else:
                 rel_name = None
@@ -473,9 +473,12 @@ class BaseModelAdmin(six.with_metaclass(forms.MediaDefiningClass)):
             for inline in admin.inlines:
                 registered_models.add(inline.model)
 
-        for related_object in (opts.get_all_related_objects(include_hidden=True) +
-                               opts.get_all_related_many_to_many_objects()):
-            related_model = related_object.model
+        related_objects = (
+            f for f in opts.get_fields(include_hidden=True)
+            if (f.auto_created and not f.concrete)
+        )
+        for related_object in related_objects:
+            related_model = related_object.related_model
             if (any(issubclass(model, related_model) for model in registered_models) and
                     related_object.field.rel.get_related_field() == field):
                 return True

+ 1 - 1
django/contrib/admin/templatetags/admin_list.py

@@ -326,7 +326,7 @@ def date_hierarchy(cl):
     """
     if cl.date_hierarchy:
         field_name = cl.date_hierarchy
-        field = cl.opts.get_field_by_name(field_name)[0]
+        field = cl.opts.get_field(field_name)
         dates_or_datetimes = 'datetimes' if isinstance(field, models.DateTimeField) else 'dates'
         year_field = '%s__year' % field_name
         month_field = '%s__month' % field_name

+ 21 - 9
django/contrib/admin/utils.py

@@ -25,7 +25,7 @@ def lookup_needs_distinct(opts, lookup_path):
     Returns True if 'distinct()' should be used to query the given lookup path.
     """
     field_name = lookup_path.split('__', 1)[0]
-    field = opts.get_field_by_name(field_name)[0]
+    field = opts.get_field(field_name)
     if hasattr(field, 'get_path_info') and any(path.m2m for path in field.get_path_info()):
         return True
     return False
@@ -265,7 +265,7 @@ def model_ngettext(obj, n=None):
 def lookup_field(name, obj, model_admin=None):
     opts = obj._meta
     try:
-        f = opts.get_field(name)
+        f = _get_non_gfk_field(opts, name)
     except FieldDoesNotExist:
         # For non-field values, the value is either a method, property or
         # returned via a callable.
@@ -291,6 +291,17 @@ def lookup_field(name, obj, model_admin=None):
     return f, attr, value
 
 
+def _get_non_gfk_field(opts, name):
+    """
+    For historical reasons, the admin app relies on GenericForeignKeys as being
+    "not found" by get_field(). This could likely be cleaned up.
+    """
+    field = opts.get_field(name)
+    if field.is_relation and field.one_to_many and not field.related_model:
+        raise FieldDoesNotExist()
+    return field
+
+
 def label_for_field(name, model, model_admin=None, return_attr=False):
     """
     Returns a sensible label for a field name. The name can be a callable,
@@ -301,7 +312,7 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
     """
     attr = None
     try:
-        field = model._meta.get_field_by_name(name)[0]
+        field = _get_non_gfk_field(model._meta, name)
         try:
             label = field.verbose_name
         except AttributeError:
@@ -349,11 +360,10 @@ def label_for_field(name, model, model_admin=None, return_attr=False):
 def help_text_for_field(name, model):
     help_text = ""
     try:
-        field_data = model._meta.get_field_by_name(name)
+        field = _get_non_gfk_field(model._meta, name)
     except FieldDoesNotExist:
         pass
     else:
-        field = field_data[0]
         if hasattr(field, 'help_text'):
             help_text = field.help_text
     return smart_text(help_text)
@@ -425,19 +435,21 @@ def reverse_field_path(model, path):
     parent = model
     pieces = path.split(LOOKUP_SEP)
     for piece in pieces:
-        field, model, direct, m2m = parent._meta.get_field_by_name(piece)
+        field = parent._meta.get_field(piece)
         # skip trailing data field if extant:
         if len(reversed_path) == len(pieces) - 1:  # final iteration
             try:
                 get_model_from_relation(field)
             except NotRelationField:
                 break
-        if direct:
+
+        # Field should point to another model
+        if field.is_relation and not (field.auto_created and not field.concrete):
             related_name = field.related_query_name()
             parent = field.rel.to
         else:
             related_name = field.field.name
-            parent = field.model
+            parent = field.related_model
         reversed_path.insert(0, related_name)
     return (parent, LOOKUP_SEP.join(reversed_path))
 
@@ -458,7 +470,7 @@ def get_fields_from_path(model, path):
             parent = get_model_from_relation(fields[-1])
         else:
             parent = model
-        fields.append(parent._meta.get_field_by_name(piece)[0])
+        fields.append(parent._meta.get_field(piece))
     return fields
 
 

+ 1 - 1
django/contrib/admin/validation.py

@@ -346,7 +346,7 @@ class ModelAdminValidator(BaseValidator):
             check_isseq(cls, 'list_editable', cls.list_editable)
             for idx, field_name in enumerate(cls.list_editable):
                 try:
-                    field = model._meta.get_field_by_name(field_name)[0]
+                    field = model._meta.get_field(field_name)
                 except FieldDoesNotExist:
                     raise ImproperlyConfigured("'%s.list_editable[%d]' refers to a "
                         "field, '%s', not defined on %s.%s."

+ 1 - 1
django/contrib/admindocs/views.py

@@ -262,7 +262,7 @@ class ModelDetailView(BaseAdminDocsView):
                 })
 
         # Gather related objects
-        for rel in opts.get_all_related_objects() + opts.get_all_related_many_to_many_objects():
+        for rel in opts.related_objects:
             verbose = _("related `%(app_label)s.%(object_name)s` objects") % {
                 'app_label': rel.opts.app_label,
                 'object_name': rel.opts.object_name,

+ 23 - 4
django/contrib/contenttypes/fields.py

@@ -21,6 +21,18 @@ class GenericForeignKey(object):
     Provides a generic relation to any object through content-type/object-id
     fields.
     """
+    # Field flags
+    auto_created = False
+    concrete = False
+    editable = False
+    hidden = False
+
+    is_relation = True
+    many_to_many = False
+    many_to_one = False
+    one_to_many = True
+    one_to_one = False
+    related_model = None
 
     def __init__(self, ct_field="content_type", fk_field="object_id", for_concrete_model=True):
         self.ct_field = ct_field
@@ -28,12 +40,13 @@ class GenericForeignKey(object):
         self.for_concrete_model = for_concrete_model
         self.editable = False
         self.rel = None
+        self.column = None
 
     def contribute_to_class(self, cls, name, **kwargs):
         self.name = name
         self.model = cls
         self.cache_attr = "_%s_cache" % name
-        cls._meta.add_virtual_field(self)
+        cls._meta.add_field(self, virtual=True)
 
         # Only run pre-initialization field assignment on non-abstract models
         if not cls._meta.abstract:
@@ -243,6 +256,13 @@ class GenericForeignKey(object):
 
 class GenericRelation(ForeignObject):
     """Provides an accessor to generic related objects (e.g. comments)"""
+    # Field flags
+    auto_created = False
+
+    many_to_many = False
+    many_to_one = True
+    one_to_many = False
+    one_to_one = False
 
     def __init__(self, to, **kwargs):
         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
@@ -303,8 +323,7 @@ class GenericRelation(ForeignObject):
 
     def resolve_related_fields(self):
         self.to_fields = [self.model._meta.pk.name]
-        return [(self.rel.to._meta.get_field_by_name(self.object_id_field_name)[0],
-                 self.model._meta.pk)]
+        return [(self.rel.to._meta.get_field(self.object_id_field_name), self.model._meta.pk)]
 
     def get_path_info(self):
         opts = self.rel.to._meta
@@ -345,7 +364,7 @@ class GenericRelation(ForeignObject):
                                                  for_concrete_model=self.for_concrete_model)
 
     def get_extra_restriction(self, where_class, alias, remote_alias):
-        field = self.rel.to._meta.get_field_by_name(self.content_type_field_name)[0]
+        field = self.rel.to._meta.get_field(self.content_type_field_name)
         contenttype_pk = self.get_content_type().pk
         cond = where_class()
         lookup = field.get_lookup('exact')(Col(remote_alias, field, field), contenttype_pk)

+ 1 - 1
django/contrib/gis/db/models/query.py

@@ -758,7 +758,7 @@ class GeoQuerySet(QuerySet):
         elif geo_field not in opts.local_fields:
             # This geographic field is inherited from another model, so we have to
             # use the db table for the _parent_ model instead.
-            tmp_fld, parent_model, direct, m2m = opts.get_field_by_name(geo_field.name)
+            parent_model = geo_field.model._meta.concrete_model
             return self.query.get_compiler(self.db)._field_column(geo_field, parent_model._meta.db_table)
         else:
             return self.query.get_compiler(self.db)._field_column(geo_field)

+ 4 - 1
django/contrib/gis/db/models/sql/compiler.py

@@ -118,7 +118,10 @@ class GeoSQLCompiler(compiler.SQLCompiler):
         seen = self.query.included_inherited_models.copy()
         if start_alias:
             seen[None] = start_alias
-        for field, model in opts.get_concrete_fields_with_model():
+        for field in opts.concrete_fields:
+            model = field.model._meta.concrete_model
+            if model is opts.model:
+                model = None
             if from_parent and model is not None and issubclass(from_parent, model):
                 # Avoid loading data for already loaded parents.
                 continue

+ 1 - 1
django/contrib/gis/sitemaps/views.py

@@ -23,7 +23,7 @@ def kml(request, label, model, field_name=None, compress=False, using=DEFAULT_DB
 
     if field_name:
         try:
-            field, _, _, _ = klass._meta.get_field_by_name(field_name)
+            field = klass._meta.get_field(field_name)
             if not isinstance(field, GeometryField):
                 raise FieldDoesNotExist
         except FieldDoesNotExist:

+ 2 - 3
django/contrib/gis/utils/layermapping.py

@@ -457,11 +457,10 @@ class LayerMapping(object):
 
     def geometry_field(self):
         "Returns the GeometryField instance associated with the geographic column."
-        # Use the `get_field_by_name` on the model's options so that we
+        # Use `get_field()` on the model's options so that we
         # get the correct field instance if there's model inheritance.
         opts = self.model._meta
-        fld, model, direct, m2m = opts.get_field_by_name(self.geom_field)
-        return fld
+        return opts.get_field(self.geom_field)
 
     def make_multi(self, geom_type, model_field):
         """

+ 1 - 1
django/contrib/gis/utils/srs.py

@@ -61,7 +61,7 @@ def add_srs_entry(srs, auth_name='EPSG', auth_srid=None, ref_sys_name=None,
               }
 
     # Backend-specific fields for the SpatialRefSys model.
-    srs_field_names = SpatialRefSys._meta.get_all_field_names()
+    srs_field_names = {f.name for f in SpatialRefSys._meta.get_fields()}
     if 'srtext' in srs_field_names:
         kwargs['srtext'] = srs.wkt
     if 'ref_sys_name' in srs_field_names:

+ 3 - 3
django/core/serializers/python.py

@@ -8,7 +8,7 @@ from __future__ import unicode_literals
 from django.apps import apps
 from django.conf import settings
 from django.core.serializers import base
-from django.db import models, DEFAULT_DB_ALIAS
+from django.db import DEFAULT_DB_ALIAS, models
 from django.utils.encoding import force_text, is_protected_type
 from django.utils import six
 
@@ -101,12 +101,12 @@ def Deserializer(object_list, **options):
         if 'pk' in d:
             data[Model._meta.pk.attname] = Model._meta.pk.to_python(d.get("pk", None))
         m2m_data = {}
-        model_fields = Model._meta.get_all_field_names()
+        field_names = {f.name for f in Model._meta.get_fields()}
 
         # Handle each field
         for (field_name, field_value) in six.iteritems(d["fields"]):
 
-            if ignore and field_name not in model_fields:
+            if ignore and field_name not in field_names:
                 # skip fields no longer on model
                 continue
 

+ 2 - 2
django/core/serializers/xml_serializer.py

@@ -186,7 +186,7 @@ class Deserializer(base.Deserializer):
         # {m2m_accessor_attribute : [list_of_related_objects]})
         m2m_data = {}
 
-        model_fields = Model._meta.get_all_field_names()
+        field_names = {f.name for f in Model._meta.get_fields()}
         # Deserialize each field.
         for field_node in node.getElementsByTagName("field"):
             # If the field is missing the name attribute, bail (are you
@@ -198,7 +198,7 @@ class Deserializer(base.Deserializer):
             # Get the field from the Model. This will raise a
             # FieldDoesNotExist if, well, the field doesn't exist, which will
             # be propagated correctly unless ignorenonexistent=True is used.
-            if self.ignore and field_name not in model_fields:
+            if self.ignore and field_name not in field_names:
                 continue
             field = Model._meta.get_field(field_name)
 

+ 2 - 2
django/db/backends/creation.py

@@ -199,7 +199,7 @@ class BaseDatabaseCreation(object):
         for f in model._meta.local_fields:
             output.extend(self.sql_indexes_for_field(model, f, style))
         for fs in model._meta.index_together:
-            fields = [model._meta.get_field_by_name(f)[0] for f in fs]
+            fields = [model._meta.get_field(f) for f in fs]
             output.extend(self.sql_indexes_for_fields(model, fields, style))
         return output
 
@@ -290,7 +290,7 @@ class BaseDatabaseCreation(object):
         for f in model._meta.local_fields:
             output.extend(self.sql_destroy_indexes_for_field(model, f, style))
         for fs in model._meta.index_together:
-            fields = [model._meta.get_field_by_name(f)[0] for f in fs]
+            fields = [model._meta.get_field(f) for f in fs]
             output.extend(self.sql_destroy_indexes_for_fields(model, fields, style))
         return output
 

+ 26 - 18
django/db/backends/schema.py

@@ -10,6 +10,11 @@ from django.utils import six
 logger = getLogger('django.db.backends.schema')
 
 
+def _related_non_m2m_objects(opts):
+    # filters out m2m objects from reverse relations.
+    return (obj for obj in opts.related_objects if not obj.field.many_to_many)
+
+
 class BaseDatabaseSchemaEditor(object):
     """
     This class (and its subclasses) are responsible for emitting schema-changing
@@ -261,7 +266,7 @@ class BaseDatabaseSchemaEditor(object):
 
         # Add any unique_togethers
         for fields in model._meta.unique_together:
-            columns = [model._meta.get_field_by_name(field)[0].column for field in fields]
+            columns = [model._meta.get_field(field).column for field in fields]
             column_sqls.append(self.sql_create_table_unique % {
                 "columns": ", ".join(self.quote_name(column) for column in columns),
             })
@@ -309,7 +314,7 @@ class BaseDatabaseSchemaEditor(object):
         news = set(tuple(fields) for fields in new_unique_together)
         # Deleted uniques
         for fields in olds.difference(news):
-            columns = [model._meta.get_field_by_name(field)[0].column for field in fields]
+            columns = [model._meta.get_field(field).column for field in fields]
             constraint_names = self._constraint_names(model, columns, unique=True)
             if len(constraint_names) != 1:
                 raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
@@ -320,7 +325,7 @@ class BaseDatabaseSchemaEditor(object):
             self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_names[0]))
         # Created uniques
         for fields in news.difference(olds):
-            columns = [model._meta.get_field_by_name(field)[0].column for field in fields]
+            columns = [model._meta.get_field(field).column for field in fields]
             self.execute(self._create_unique_sql(model, columns))
 
     def alter_index_together(self, model, old_index_together, new_index_together):
@@ -333,7 +338,7 @@ class BaseDatabaseSchemaEditor(object):
         news = set(tuple(fields) for fields in new_index_together)
         # Deleted indexes
         for fields in olds.difference(news):
-            columns = [model._meta.get_field_by_name(field)[0].column for field in fields]
+            columns = [model._meta.get_field(field).column for field in fields]
             constraint_names = self._constraint_names(model, list(columns), index=True)
             if len(constraint_names) != 1:
                 raise ValueError("Found wrong number (%s) of constraints for %s(%s)" % (
@@ -344,7 +349,7 @@ class BaseDatabaseSchemaEditor(object):
             self.execute(self._delete_constraint_sql(self.sql_delete_index, model, constraint_names[0]))
         # Created indexes
         for field_names in news.difference(olds):
-            fields = [model._meta.get_field_by_name(field)[0] for field in field_names]
+            fields = [model._meta.get_field(field) for field in field_names]
             self.execute(self._create_index_sql(model, fields, suffix="_idx"))
 
     def alter_db_table(self, model, old_db_table, new_db_table):
@@ -511,10 +516,12 @@ class BaseDatabaseSchemaEditor(object):
         # Drop incoming FK constraints if we're a primary key and things are going
         # to change.
         if old_field.primary_key and new_field.primary_key and old_type != new_type:
-            for rel in new_field.model._meta.get_all_related_objects():
-                rel_fk_names = self._constraint_names(rel.model, [rel.field.column], foreign_key=True)
+            # '_meta.related_field' also contains M2M reverse fields, these
+            # will be filtered out
+            for rel in _related_non_m2m_objects(new_field.model._meta):
+                rel_fk_names = self._constraint_names(rel.related_model, [rel.field.column], foreign_key=True)
                 for fk_name in rel_fk_names:
-                    self.execute(self._delete_constraint_sql(self.sql_delete_fk, rel.model, fk_name))
+                    self.execute(self._delete_constraint_sql(self.sql_delete_fk, rel.related_model, fk_name))
         # Removed an index? (no strict check, as multiple indexes are possible)
         if (old_field.db_index and not new_field.db_index and
                 not old_field.unique and not
@@ -661,7 +668,7 @@ class BaseDatabaseSchemaEditor(object):
         # referring to us.
         rels_to_update = []
         if old_field.primary_key and new_field.primary_key and old_type != new_type:
-            rels_to_update.extend(new_field.model._meta.get_all_related_objects())
+            rels_to_update.extend(_related_non_m2m_objects(new_field.model._meta))
         # Changed to become primary key?
         # Note that we don't detect unsetting of a PK, as we assume another field
         # will always come along and replace it.
@@ -684,14 +691,14 @@ class BaseDatabaseSchemaEditor(object):
                 }
             )
             # Update all referencing columns
-            rels_to_update.extend(new_field.model._meta.get_all_related_objects())
+            rels_to_update.extend(_related_non_m2m_objects(new_field.model._meta))
         # Handle our type alters on the other end of rels from the PK stuff above
         for rel in rels_to_update:
             rel_db_params = rel.field.db_parameters(connection=self.connection)
             rel_type = rel_db_params['type']
             self.execute(
                 self.sql_alter_column % {
-                    "table": self.quote_name(rel.model._meta.db_table),
+                    "table": self.quote_name(rel.related_model._meta.db_table),
                     "changes": self.sql_alter_column_type % {
                         "column": self.quote_name(rel.field.column),
                         "type": rel_type,
@@ -705,8 +712,9 @@ class BaseDatabaseSchemaEditor(object):
             self.execute(self._create_fk_sql(model, new_field, "_fk_%(to_table)s_%(to_column)s"))
         # Rebuild FKs that pointed to us if we previously had to drop them
         if old_field.primary_key and new_field.primary_key and old_type != new_type:
-            for rel in new_field.model._meta.get_all_related_objects():
-                self.execute(self._create_fk_sql(rel.model, rel.field, "_fk"))
+            for rel in new_field.model._meta.related_objects:
+                if not rel.many_to_many:
+                    self.execute(self._create_fk_sql(rel.related_model, rel.field, "_fk"))
         # Does it have check constraints we need to add?
         if old_db_params['check'] != new_db_params['check'] and new_db_params['check']:
             self.execute(
@@ -765,14 +773,14 @@ class BaseDatabaseSchemaEditor(object):
             new_field.rel.through,
             # We need the field that points to the target model, so we can tell alter_field to change it -
             # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
-            old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0],
-            new_field.rel.through._meta.get_field_by_name(new_field.m2m_reverse_field_name())[0],
+            old_field.rel.through._meta.get_field(old_field.m2m_reverse_field_name()),
+            new_field.rel.through._meta.get_field(new_field.m2m_reverse_field_name()),
         )
         self.alter_field(
             new_field.rel.through,
             # for self-referential models we need to alter field from the other end too
-            old_field.rel.through._meta.get_field_by_name(old_field.m2m_field_name())[0],
-            new_field.rel.through._meta.get_field_by_name(new_field.m2m_field_name())[0],
+            old_field.rel.through._meta.get_field(old_field.m2m_field_name()),
+            new_field.rel.through._meta.get_field(new_field.m2m_field_name()),
         )
 
     def _create_index_name(self, model, column_names, suffix=""):
@@ -844,7 +852,7 @@ class BaseDatabaseSchemaEditor(object):
                 output.append(self._create_index_sql(model, [field], suffix=""))
 
         for field_names in model._meta.index_together:
-            fields = [model._meta.get_field_by_name(field)[0] for field in field_names]
+            fields = [model._meta.get_field(field) for field in field_names]
             output.append(self._create_index_sql(model, fields, suffix="_idx"))
         return output
 

+ 2 - 2
django/db/backends/sqlite3/schema.py

@@ -227,8 +227,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
                 alter_fields=[(
                     # We need the field that points to the target model, so we can tell alter_field to change it -
                     # this is m2m_reverse_field_name() (as opposed to m2m_field_name, which points to our model)
-                    old_field.rel.through._meta.get_field_by_name(old_field.m2m_reverse_field_name())[0],
-                    new_field.rel.through._meta.get_field_by_name(new_field.m2m_reverse_field_name())[0],
+                    old_field.rel.through._meta.get_field(old_field.m2m_reverse_field_name()),
+                    new_field.rel.through._meta.get_field(new_field.m2m_reverse_field_name()),
                 )],
                 override_uniques=(new_field.m2m_field_name(), new_field.m2m_reverse_field_name()),
             )

+ 13 - 25
django/db/migrations/autodetector.py

@@ -163,7 +163,7 @@ class MigrationAutodetector(object):
             old_model_name = self.renamed_models.get((app_label, model_name), model_name)
             old_model_state = self.from_state.models[app_label, old_model_name]
             for field_name, field in old_model_state.fields:
-                old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(field_name)[0]
+                old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(field_name)
                 if (hasattr(old_field, "rel") and getattr(old_field.rel, "through", None)
                         and not old_field.rel.through._meta.auto_created):
                     through_key = (
@@ -685,26 +685,14 @@ class MigrationAutodetector(object):
             # and the removal of all its own related fields, and if it's
             # a through model the field that references it.
             dependencies = []
-            for related_object in model._meta.get_all_related_objects():
-                dependencies.append((
-                    related_object.model._meta.app_label,
-                    related_object.model._meta.object_name,
-                    related_object.field.name,
-                    False,
-                ))
-                dependencies.append((
-                    related_object.model._meta.app_label,
-                    related_object.model._meta.object_name,
-                    related_object.field.name,
-                    "alter",
-                ))
-            for related_object in model._meta.get_all_related_many_to_many_objects():
-                dependencies.append((
-                    related_object.model._meta.app_label,
-                    related_object.model._meta.object_name,
-                    related_object.field.name,
-                    False,
-                ))
+            for related_object in model._meta.related_objects:
+                related_object_app_label = related_object.related_model._meta.app_label
+                object_name = related_object.related_model._meta.object_name
+                field_name = related_object.field.name
+                dependencies.append((related_object_app_label, object_name, field_name, False))
+                if not related_object.many_to_many:
+                    dependencies.append((related_object_app_label, object_name, field_name, "alter"))
+
             for name, field in sorted(related_fields.items()):
                 dependencies.append((app_label, model_name, name, False))
             # We're referenced in another field's through=
@@ -743,7 +731,7 @@ class MigrationAutodetector(object):
         for app_label, model_name, field_name in sorted(self.new_field_keys - self.old_field_keys):
             old_model_name = self.renamed_models.get((app_label, model_name), model_name)
             old_model_state = self.from_state.models[app_label, old_model_name]
-            field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0]
+            field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
             # Scan to see if this is actually a rename!
             field_dec = self.deep_deconstruct(field)
             for rem_app_label, rem_model_name, rem_field_name in sorted(self.old_field_keys - self.new_field_keys):
@@ -776,7 +764,7 @@ class MigrationAutodetector(object):
             self._generate_added_field(app_label, model_name, field_name)
 
     def _generate_added_field(self, app_label, model_name, field_name):
-        field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0]
+        field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
         # Fields that are foreignkeys/m2ms depend on stuff
         dependencies = []
         if field.rel and field.rel.to:
@@ -847,8 +835,8 @@ class MigrationAutodetector(object):
             # Did the field change?
             old_model_name = self.renamed_models.get((app_label, model_name), model_name)
             old_field_name = self.renamed_fields.get((app_label, model_name, field_name), field_name)
-            old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field_by_name(old_field_name)[0]
-            new_field = self.new_apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0]
+            old_field = self.old_apps.get_model(app_label, old_model_name)._meta.get_field(old_field_name)
+            new_field = self.new_apps.get_model(app_label, model_name)._meta.get_field(field_name)
             # Implement any model renames on relations; these are handled by RenameModel
             # so we need to exclude them from the comparison
             if hasattr(new_field, "rel") and getattr(new_field.rel, "to", None):

+ 10 - 10
django/db/migrations/operations/fields.py

@@ -44,7 +44,7 @@ class AddField(Operation):
         to_model = to_state.apps.get_model(app_label, self.model_name)
         if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
             from_model = from_state.apps.get_model(app_label, self.model_name)
-            field = to_model._meta.get_field_by_name(self.name)[0]
+            field = to_model._meta.get_field(self.name)
             if not self.preserve_default:
                 field.default = self.field.default
             schema_editor.add_field(
@@ -57,7 +57,7 @@ class AddField(Operation):
     def database_backwards(self, app_label, schema_editor, from_state, to_state):
         from_model = from_state.apps.get_model(app_label, self.model_name)
         if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
-            schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0])
+            schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
 
     def describe(self):
         return "Add field %s to %s" % (self.name, self.model_name)
@@ -100,13 +100,13 @@ class RemoveField(Operation):
     def database_forwards(self, app_label, schema_editor, from_state, to_state):
         from_model = from_state.apps.get_model(app_label, self.model_name)
         if self.allowed_to_migrate(schema_editor.connection.alias, from_model):
-            schema_editor.remove_field(from_model, from_model._meta.get_field_by_name(self.name)[0])
+            schema_editor.remove_field(from_model, from_model._meta.get_field(self.name))
 
     def database_backwards(self, app_label, schema_editor, from_state, to_state):
         to_model = to_state.apps.get_model(app_label, self.model_name)
         if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
             from_model = from_state.apps.get_model(app_label, self.model_name)
-            schema_editor.add_field(from_model, to_model._meta.get_field_by_name(self.name)[0])
+            schema_editor.add_field(from_model, to_model._meta.get_field(self.name))
 
     def describe(self):
         return "Remove field %s from %s" % (self.name, self.model_name)
@@ -158,8 +158,8 @@ class AlterField(Operation):
         to_model = to_state.apps.get_model(app_label, self.model_name)
         if self.allowed_to_migrate(schema_editor.connection.alias, to_model):
             from_model = from_state.apps.get_model(app_label, self.model_name)
-            from_field = from_model._meta.get_field_by_name(self.name)[0]
-            to_field = to_model._meta.get_field_by_name(self.name)[0]
+            from_field = from_model._meta.get_field(self.name)
+            to_field = to_model._meta.get_field(self.name)
             # If the field is a relatedfield with an unresolved rel.to, just
             # set it equal to the other field side. Bandaid fix for AlterField
             # migrations that are part of a RenameModel change.
@@ -231,8 +231,8 @@ class RenameField(Operation):
             from_model = from_state.apps.get_model(app_label, self.model_name)
             schema_editor.alter_field(
                 from_model,
-                from_model._meta.get_field_by_name(self.old_name)[0],
-                to_model._meta.get_field_by_name(self.new_name)[0],
+                from_model._meta.get_field(self.old_name),
+                to_model._meta.get_field(self.new_name),
             )
 
     def database_backwards(self, app_label, schema_editor, from_state, to_state):
@@ -241,8 +241,8 @@ class RenameField(Operation):
             from_model = from_state.apps.get_model(app_label, self.model_name)
             schema_editor.alter_field(
                 from_model,
-                from_model._meta.get_field_by_name(self.new_name)[0],
-                to_model._meta.get_field_by_name(self.old_name)[0],
+                from_model._meta.get_field(self.new_name),
+                to_model._meta.get_field(self.old_name),
             )
 
     def describe(self):

+ 17 - 17
django/db/migrations/operations/models.py

@@ -138,25 +138,27 @@ class RenameModel(Operation):
         )
 
     def state_forwards(self, app_label, state):
-        # Get all of the related objects we need to repoint
         apps = state.apps
         model = apps.get_model(app_label, self.old_name)
         model._meta.apps = apps
-        related_objects = model._meta.get_all_related_objects()
-        related_m2m_objects = model._meta.get_all_related_many_to_many_objects()
+        # Get all of the related objects we need to repoint
+        all_related_objects = (
+            f for f in model._meta.get_fields(include_hidden=True)
+            if f.auto_created and not f.concrete and not (f.hidden or f.many_to_many)
+        )
         # Rename the model
         state.models[app_label, self.new_name.lower()] = state.models[app_label, self.old_name.lower()]
         state.models[app_label, self.new_name.lower()].name = self.new_name
         state.remove_model(app_label, self.old_name)
         # Repoint the FKs and M2Ms pointing to us
-        for related_object in (related_objects + related_m2m_objects):
+        for related_object in all_related_objects:
             # Use the new related key for self referential related objects.
-            if related_object.model == model:
+            if related_object.related_model == model:
                 related_key = (app_label, self.new_name.lower())
             else:
                 related_key = (
-                    related_object.model._meta.app_label,
-                    related_object.model._meta.object_name.lower(),
+                    related_object.related_model._meta.app_label,
+                    related_object.related_model._meta.object_name.lower(),
                 )
             new_fields = []
             for name, field in state.models[related_key].fields:
@@ -179,21 +181,19 @@ class RenameModel(Operation):
                 new_model._meta.db_table,
             )
             # Alter the fields pointing to us
-            related_objects = old_model._meta.get_all_related_objects()
-            related_m2m_objects = old_model._meta.get_all_related_many_to_many_objects()
-            for related_object in (related_objects + related_m2m_objects):
-                if related_object.model == old_model:
+            for related_object in old_model._meta.related_objects:
+                if related_object.related_model == old_model:
                     model = new_model
                     related_key = (app_label, self.new_name.lower())
                 else:
-                    model = related_object.model
+                    model = related_object.related_model
                     related_key = (
-                        related_object.model._meta.app_label,
-                        related_object.model._meta.object_name.lower(),
+                        related_object.related_model._meta.app_label,
+                        related_object.related_model._meta.object_name.lower(),
                     )
                 to_field = to_state.apps.get_model(
                     *related_key
-                )._meta.get_field_by_name(related_object.field.name)[0]
+                )._meta.get_field(related_object.field.name)
                 schema_editor.alter_field(
                     model,
                     related_object.field,
@@ -394,11 +394,11 @@ class AlterOrderWithRespectTo(Operation):
             from_model = from_state.apps.get_model(app_label, self.name)
             # Remove a field if we need to
             if from_model._meta.order_with_respect_to and not to_model._meta.order_with_respect_to:
-                schema_editor.remove_field(from_model, from_model._meta.get_field_by_name("_order")[0])
+                schema_editor.remove_field(from_model, from_model._meta.get_field("_order"))
             # Add a field if we need to (altering the column is untouched as
             # it's likely a rename)
             elif to_model._meta.order_with_respect_to and not from_model._meta.order_with_respect_to:
-                field = to_model._meta.get_field_by_name("_order")[0]
+                field = to_model._meta.get_field("_order")
                 if not field.has_default():
                     field.default = 0
                 schema_editor.add_field(

+ 3 - 3
django/db/migrations/state.py

@@ -50,15 +50,15 @@ class ProjectState(object):
             model_name = model_name.lower()
             try:
                 related_old = {
-                    f.model for f in
-                    self.apps.get_model(app_label, model_name)._meta.get_all_related_objects()
+                    f.related_model for f in
+                    self.apps.get_model(app_label, model_name)._meta.related_objects
                 }
             except LookupError:
                 related_old = set()
             self._reload_one_model(app_label, model_name)
             # Reload models if there are relations
             model = self.apps.get_model(app_label, model_name)
-            related_m2m = {f.rel.to for f, _ in model._meta.get_m2m_with_model()}
+            related_m2m = {f.related_model for f in model._meta.many_to_many}
             for rel_model in related_old.union(related_m2m):
                 self._reload_one_model(rel_model._meta.app_label, rel_model._meta.model_name)
             if related_m2m:

+ 38 - 31
django/db/models/base.py

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 
 import copy
 import inspect
+from itertools import chain
 import sys
 import warnings
 
@@ -175,12 +176,12 @@ class ModelBase(type):
             new_class.add_to_class(obj_name, obj)
 
         # All the fields of any type declared on this model
-        new_fields = (
-            new_class._meta.local_fields +
-            new_class._meta.local_many_to_many +
+        new_fields = chain(
+            new_class._meta.local_fields,
+            new_class._meta.local_many_to_many,
             new_class._meta.virtual_fields
         )
-        field_names = set(f.name for f in new_fields)
+        field_names = {f.name for f in new_fields}
 
         # Basic setup for proxy models.
         if is_proxy:
@@ -202,6 +203,7 @@ class ModelBase(type):
                 raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
             new_class._meta.setup_proxy(base)
             new_class._meta.concrete_model = base._meta.concrete_model
+            base._meta.concrete_model._meta.proxied_children.append(new_class._meta)
         else:
             new_class._meta.concrete_model = new_class
 
@@ -342,7 +344,7 @@ class ModelBase(type):
 
         # Give the class a docstring -- its definition.
         if cls.__doc__ is None:
-            cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.attname for f in opts.fields))
+            cls.__doc__ = "%s(%s)" % (cls.__name__, ", ".join(f.name for f in opts.fields))
 
         get_absolute_url_override = settings.ABSOLUTE_URL_OVERRIDES.get(
             '%s.%s' % (opts.app_label, opts.model_name)
@@ -630,7 +632,7 @@ class Model(six.with_metaclass(ModelBase)):
         and not use this method.
         """
         try:
-            field = self._meta.get_field_by_name(field_name)[0]
+            field = self._meta.get_field(field_name)
         except FieldDoesNotExist:
             return getattr(self, field_name)
         return getattr(self, field.attname)
@@ -1438,12 +1440,17 @@ class Model(six.with_metaclass(ModelBase)):
     def _check_local_fields(cls, fields, option):
         from django.db import models
 
+        # In order to avoid hitting the relation tree prematurely, we use our
+        # own fields_map instead of using get_field()
+        forward_fields_map = {
+            field.name: field for field in cls._meta._get_fields(reverse=False)
+        }
+
         errors = []
         for field_name in fields:
             try:
-                field = cls._meta.get_field(field_name,
-                    many_to_many=True)
-            except FieldDoesNotExist:
+                field = forward_fields_map[field_name]
+            except KeyError:
                 errors.append(
                     checks.Error(
                         "'%s' refers to the non-existent field '%s'." % (option, field_name),
@@ -1484,7 +1491,6 @@ class Model(six.with_metaclass(ModelBase)):
     def _check_ordering(cls):
         """ Check "ordering" option -- is it a list of strings and do all fields
         exist? """
-
         if not cls._meta.ordering:
             return []
 
@@ -1500,7 +1506,6 @@ class Model(six.with_metaclass(ModelBase)):
             ]
 
         errors = []
-
         fields = cls._meta.ordering
 
         # Skip '?' fields.
@@ -1518,28 +1523,30 @@ class Model(six.with_metaclass(ModelBase)):
 
         # Skip ordering on pk. This is always a valid order_by field
         # but is an alias and therefore won't be found by opts.get_field.
-        fields = (f for f in fields if f != 'pk')
+        fields = {f for f in fields if f != 'pk'}
 
-        for field_name in fields:
-            try:
-                cls._meta.get_field(field_name, many_to_many=False)
-            except FieldDoesNotExist:
-                if field_name.endswith('_id'):
-                    try:
-                        field = cls._meta.get_field(field_name[:-3], many_to_many=False)
-                    except FieldDoesNotExist:
-                        pass
-                    else:
-                        if field.attname == field_name:
-                            continue
-                errors.append(
-                    checks.Error(
-                        "'ordering' refers to the non-existent field '%s'." % field_name,
-                        hint=None,
-                        obj=cls,
-                        id='models.E015',
-                    )
+        # Check for invalid or non-existent fields in ordering.
+        invalid_fields = []
+
+        # Any field name that is not present in field_names does not exist.
+        # Also, ordering by m2m fields is not allowed.
+        opts = cls._meta
+        valid_fields = set(chain.from_iterable(
+            (f.name, f.attname) if not (f.auto_created and not f.concrete) else (f.field.related_query_name(),)
+            for f in chain(opts.fields, opts.related_objects)
+        ))
+
+        invalid_fields.extend(fields - valid_fields)
+
+        for invalid_field in invalid_fields:
+            errors.append(
+                checks.Error(
+                    "'ordering' refers to the non-existent field '%s'." % invalid_field,
+                    hint=None,
+                    obj=cls,
+                    id='models.E015',
                 )
+            )
         return errors
 
     @classmethod

+ 22 - 6
django/db/models/deletion.py

@@ -1,4 +1,5 @@
 from collections import OrderedDict
+from itertools import chain
 from operator import attrgetter
 
 from django.db import connections, transaction, IntegrityError
@@ -51,6 +52,23 @@ def DO_NOTHING(collector, field, sub_objs, using):
     pass
 
 
+def get_candidate_relations_to_delete(opts):
+    # Collect models that contain candidate relations to delete. This may include
+    # relations coming from proxy models.
+    candidate_models = {opts}
+    candidate_models = candidate_models.union(opts.concrete_model._meta.proxied_children)
+    # For each model, get all candidate fields.
+    candidate_model_fields = chain.from_iterable(
+        opts.get_fields(include_hidden=True) for opts in candidate_models
+    )
+    # The candidate relations are the ones that come from N-1 and 1-1 relations.
+    # N-N  (i.e., many-to-many) relations aren't candidates for deletion.
+    return (
+        f for f in candidate_model_fields
+        if f.auto_created and not f.concrete and (f.one_to_one or f.many_to_one)
+    )
+
+
 class Collector(object):
     def __init__(self, using):
         self.using = using
@@ -134,8 +152,7 @@ class Collector(object):
             return False
         # Foreign keys pointing to this model, both from m2m and other
         # models.
-        for related in opts.get_all_related_objects(
-                include_hidden=True, include_proxy_eq=True):
+        for related in get_candidate_relations_to_delete(opts):
             if related.field.rel.on_delete is not DO_NOTHING:
                 return False
         for field in model._meta.virtual_fields:
@@ -184,7 +201,7 @@ class Collector(object):
         model = new_objs[0].__class__
 
         # Recursively collect concrete model's parent models, but not their
-        # related objects. These will be found by meta.get_all_related_objects()
+        # related objects. These will be found by meta.get_fields()
         concrete_model = model._meta.concrete_model
         for ptr in six.itervalues(concrete_model._meta.parents):
             if ptr:
@@ -199,8 +216,7 @@ class Collector(object):
                              reverse_dependency=True)
 
         if collect_related:
-            for related in model._meta.get_all_related_objects(
-                    include_hidden=True, include_proxy_eq=True):
+            for related in get_candidate_relations_to_delete(model._meta):
                 field = related.field
                 if field.rel.on_delete == DO_NOTHING:
                     continue
@@ -225,7 +241,7 @@ class Collector(object):
         Gets a QuerySet of objects related to ``objs`` via the relation ``related``.
 
         """
-        return related.model._base_manager.using(self.using).filter(
+        return related.related_model._base_manager.using(self.using).filter(
             **{"%s__in" % related.field.name: objs}
         )
 

+ 16 - 3
django/db/models/fields/__init__.py

@@ -31,7 +31,9 @@ from django.utils.ipv6 import clean_ipv6_address
 from django.utils import six
 from django.utils.itercompat import is_iterable
 
-# imported for backwards compatibility
+# When the _meta object was formalized, this exception was moved to
+# django.core.exceptions. It is retained here for backwards compatibility
+# purposes.
 from django.core.exceptions import FieldDoesNotExist  # NOQA
 
 # Avoid "TypeError: Item in ``from list'' not a string" -- unicode_literals
@@ -61,7 +63,7 @@ BLANK_CHOICE_DASH = [("", "---------")]
 
 
 def _load_field(app_label, model_name, field_name):
-    return apps.get_model(app_label, model_name)._meta.get_field_by_name(field_name)[0]
+    return apps.get_model(app_label, model_name)._meta.get_field(field_name)
 
 
 # A guide to Field parameters:
@@ -116,6 +118,15 @@ class Field(RegisterLookupMixin):
     system_check_deprecated_details = None
     system_check_removed_details = None
 
+    # Field flags
+    hidden = False
+
+    many_to_many = None
+    many_to_one = None
+    one_to_many = None
+    one_to_one = None
+    related_model = None
+
     # Generic field type description, usually overridden by subclasses
     def _description(self):
         return _('Field of type: %(field_type)s') % {
@@ -137,6 +148,7 @@ class Field(RegisterLookupMixin):
         self.max_length, self._unique = max_length, unique
         self.blank, self.null = blank, null
         self.rel = rel
+        self.is_relation = self.rel is not None
         self.default = default
         self.editable = editable
         self.serialize = serialize
@@ -603,6 +615,7 @@ class Field(RegisterLookupMixin):
         if not self.name:
             self.name = name
         self.attname, self.column = self.get_attname_column()
+        self.concrete = self.column is not None
         if self.verbose_name is None and self.name:
             self.verbose_name = self.name.replace('_', ' ')
 
@@ -610,7 +623,7 @@ class Field(RegisterLookupMixin):
         self.set_attributes_from_name(name)
         self.model = cls
         if virtual_only:
-            cls._meta.add_virtual_field(self)
+            cls._meta.add_field(self, virtual=True)
         else:
             cls._meta.add_field(self)
         if self.choices:

+ 107 - 42
django/db/models/fields/related.py

@@ -98,6 +98,18 @@ signals.class_prepared.connect(do_pending_lookups)
 
 
 class RelatedField(Field):
+    # Field flags
+    one_to_many = False
+    one_to_one = False
+    many_to_many = False
+    many_to_one = False
+
+    @cached_property
+    def related_model(self):
+        # Can't cache this property until all the models are loaded.
+        apps.check_models_ready()
+        return self.rel.to
+
     def check(self, **kwargs):
         errors = super(RelatedField, self).check(**kwargs)
         errors.extend(self._check_related_name_is_valid())
@@ -235,13 +247,10 @@ class RelatedField(Field):
         # Check clashes between accessors/reverse query names of `field` and
         # any other field accessor -- i. e. Model.foreign accessor clashes with
         # Model.m2m accessor.
-        potential_clashes = rel_opts.get_all_related_many_to_many_objects()
-        potential_clashes += rel_opts.get_all_related_objects()
-        potential_clashes = (r for r in potential_clashes
-            if r.field is not self)
+        potential_clashes = (r for r in rel_opts.related_objects if r.field is not self)
         for clash_field in potential_clashes:
             clash_name = "%s.%s" % (  # i. e. "Model.m2m"
-                clash_field.model._meta.object_name,
+                clash_field.related_model._meta.object_name,
                 clash_field.field.name)
             if clash_field.get_accessor_name() == rel_name:
                 errors.append(
@@ -392,7 +401,7 @@ class SingleRelatedObjectDescriptor(object):
         # consistency with `ReverseSingleRelatedObjectDescriptor`.
         return type(
             str('RelatedObjectDoesNotExist'),
-            (self.related.model.DoesNotExist, AttributeError),
+            (self.related.related_model.DoesNotExist, AttributeError),
             {}
         )
 
@@ -400,11 +409,11 @@ class SingleRelatedObjectDescriptor(object):
         return hasattr(instance, self.cache_name)
 
     def get_queryset(self, **hints):
-        manager = self.related.model._default_manager
+        manager = self.related.related_model._default_manager
         # If the related manager indicates that it should be used for
         # related fields, respect that.
         if not getattr(manager, 'use_for_related_fields', False):
-            manager = self.related.model._base_manager
+            manager = self.related.related_model._base_manager
         return manager.db_manager(hints=hints).all()
 
     def get_prefetch_queryset(self, instances, queryset=None):
@@ -441,7 +450,7 @@ class SingleRelatedObjectDescriptor(object):
                     params['%s__%s' % (self.related.field.name, rh_field.name)] = getattr(instance, rh_field.attname)
                 try:
                     rel_obj = self.get_queryset(instance=instance).get(**params)
-                except self.related.model.DoesNotExist:
+                except self.related.related_model.DoesNotExist:
                     rel_obj = None
                 else:
                     setattr(rel_obj, self.related.field.get_cache_name(), instance)
@@ -470,7 +479,7 @@ class SingleRelatedObjectDescriptor(object):
                     self.related.get_accessor_name(),
                 )
             )
-        elif value is not None and not isinstance(value, self.related.model):
+        elif value is not None and not isinstance(value, self.related.related_model):
             raise ValueError(
                 'Cannot assign "%r": "%s.%s" must be a "%s" instance.' % (
                     value,
@@ -825,9 +834,9 @@ class ForeignRelatedObjectsDescriptor(object):
         # Dynamically create a class that subclasses the related model's default
         # manager.
         return create_foreign_related_manager(
-            self.related.model._default_manager.__class__,
+            self.related.related_model._default_manager.__class__,
             self.related.field,
-            self.related.model,
+            self.related.related_model,
         )
 
 
@@ -1148,7 +1157,7 @@ class ManyRelatedObjectsDescriptor(object):
         # Dynamically create a class that subclasses the related
         # model's default manager.
         return create_many_related_manager(
-            self.related.model._default_manager.__class__,
+            self.related.related_model._default_manager.__class__,
             self.related.field.rel
         )
 
@@ -1156,7 +1165,7 @@ class ManyRelatedObjectsDescriptor(object):
         if instance is None:
             return self
 
-        rel_model = self.related.model
+        rel_model = self.related.related_model
 
         manager = self.related_manager_cls(
             model=rel_model,
@@ -1255,6 +1264,12 @@ class ReverseManyRelatedObjectsDescriptor(object):
 
 
 class ForeignObjectRel(object):
+    # Field flags
+    auto_created = True
+    concrete = False
+    editable = False
+    is_relation = True
+
     def __init__(self, field, to, related_name=None, limit_choices_to=None,
                  parent_link=False, on_delete=None, related_query_name=None):
         self.field = field
@@ -1267,32 +1282,55 @@ class ForeignObjectRel(object):
         self.on_delete = on_delete
         self.symmetrical = False
 
-    # This and the following cached_properties can't be initialized in
+    # Some of the following cached_properties can't be initialized in
     # __init__ as the field doesn't have its model yet. Calling these methods
     # before field.contribute_to_class() has been called will result in
     # AttributeError
     @cached_property
     def model(self):
-        if not self.field.model:
-            raise AttributeError(
-                "This property can't be accessed before self.field.contribute_to_class has been called.")
-        return self.field.model
+        return self.to
 
     @cached_property
     def opts(self):
-        return self.model._meta
+        return self.related_model._meta
 
     @cached_property
     def to_opts(self):
         return self.to._meta
 
     @cached_property
-    def parent_model(self):
-        return self.to
+    def hidden(self):
+        return self.is_hidden()
 
     @cached_property
     def name(self):
-        return '%s.%s' % (self.opts.app_label, self.opts.model_name)
+        return self.field.related_query_name()
+
+    @cached_property
+    def related_model(self):
+        if not self.field.model:
+            raise AttributeError(
+                "This property can't be accessed before self.field.contribute_to_class has been called.")
+        return self.field.model
+
+    @cached_property
+    def many_to_many(self):
+        return self.field.many_to_many
+
+    @cached_property
+    def many_to_one(self):
+        return self.field.one_to_many
+
+    @cached_property
+    def one_to_many(self):
+        return self.field.many_to_one
+
+    @cached_property
+    def one_to_one(self):
+        return self.field.one_to_one
+
+    def __repr__(self):
+        return '<%s: %s.%s>' % (type(self).__name__, self.opts.app_label, self.opts.model_name)
 
     def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH,
                     limit_to_currently_related=False):
@@ -1304,10 +1342,10 @@ class ForeignObjectRel(object):
         initially for utilization by RelatedFieldListFilter.
         """
         first_choice = blank_choice if include_blank else []
-        queryset = self.model._default_manager.all()
+        queryset = self.related_model._default_manager.all()
         if limit_to_currently_related:
             queryset = queryset.complex_filter(
-                {'%s__isnull' % self.parent_model._meta.model_name: False}
+                {'%s__isnull' % self.related_model._meta.model_name: False}
             )
         lst = [(x._get_pk_val(), smart_text(x)) for x in queryset]
         return first_choice + lst
@@ -1318,7 +1356,7 @@ class ForeignObjectRel(object):
 
     def is_hidden(self):
         "Should the related object be hidden?"
-        return self.related_name and self.related_name[-1] == '+'
+        return self.related_name is not None and self.related_name[-1] == '+'
 
     def get_joining_columns(self):
         return self.field.get_reverse_joining_columns()
@@ -1349,7 +1387,7 @@ class ForeignObjectRel(object):
         # Due to backwards compatibility ModelForms need to be able to provide
         # an alternate model. See BaseInlineFormSet.get_default_prefix().
         opts = model._meta if model else self.opts
-        model = model or self.model
+        model = model or self.related_model
         if self.multiple:
             # If this is a symmetrical m2m relation on self, there is no reverse accessor.
             if self.symmetrical and model == self.to:
@@ -1383,11 +1421,11 @@ class ManyToOneRel(ForeignObjectRel):
         Returns the Field in the 'to' object to which this relationship is
         tied.
         """
-        data = self.to._meta.get_field_by_name(self.field_name)
-        if not data[2]:
+        field = self.to._meta.get_field(self.field_name)
+        if not field.concrete:
             raise FieldDoesNotExist("No related field named '%s'" %
                     self.field_name)
-        return data[0]
+        return field
 
     def set_field_name(self):
         self.field_name = self.field_name or self.to._meta.pk.name
@@ -1419,6 +1457,10 @@ class ManyToManyRel(ForeignObjectRel):
         self.through_fields = through_fields
         self.db_constraint = db_constraint
 
+    def is_hidden(self):
+        "Should the related object be hidden?"
+        return self.related_name is not None and self.related_name[-1] == '+'
+
     def get_related_field(self):
         """
         Returns the field in the 'to' object to which this relationship is tied.
@@ -1436,8 +1478,13 @@ class ManyToManyRel(ForeignObjectRel):
 
 
 class ForeignObject(RelatedField):
+    # Field flags
+    many_to_many = False
+    many_to_one = False
+    one_to_many = True
+    one_to_one = False
+
     requires_unique_target = True
-    generate_reverse_relation = True
     related_accessor_class = ForeignRelatedObjectsDescriptor
 
     def __init__(self, to, from_fields, to_fields, swappable=True, **kwargs):
@@ -1556,9 +1603,9 @@ class ForeignObject(RelatedField):
             from_field_name = self.from_fields[index]
             to_field_name = self.to_fields[index]
             from_field = (self if from_field_name == 'self'
-                          else self.opts.get_field_by_name(from_field_name)[0])
+                          else self.opts.get_field(from_field_name))
             to_field = (self.rel.to._meta.pk if to_field_name is None
-                        else self.rel.to._meta.get_field_by_name(to_field_name)[0])
+                        else self.rel.to._meta.get_field(to_field_name))
             related_fields.append((from_field, to_field))
         return related_fields
 
@@ -1731,7 +1778,7 @@ class ForeignObject(RelatedField):
     def contribute_to_related_class(self, cls, related):
         # Internal FK's - i.e., those with a related name ending with '+' -
         # and swapped models don't get a related descriptor.
-        if not self.rel.is_hidden() and not related.model._meta.swapped:
+        if not self.rel.is_hidden() and not related.related_model._meta.swapped:
             setattr(cls, related.get_accessor_name(), self.related_accessor_class(related))
             # While 'limit_choices_to' might be a callable, simply pass
             # it along for later - this is too early because it's still
@@ -1741,6 +1788,12 @@ class ForeignObject(RelatedField):
 
 
 class ForeignKey(ForeignObject):
+    # Field flags
+    many_to_many = False
+    many_to_one = False
+    one_to_many = True
+    one_to_one = False
+
     empty_strings_allowed = False
     default_error_messages = {
         'invalid': _('%(model)s instance with %(field)s %(value)r does not exist.')
@@ -1951,6 +2004,12 @@ class OneToOneField(ForeignKey):
     always returns the object pointed to (since there will only ever be one),
     rather than returning a list.
     """
+    # Field flags
+    many_to_many = False
+    many_to_one = False
+    one_to_many = False
+    one_to_one = True
+
     related_accessor_class = SingleRelatedObjectDescriptor
     description = _("One-to-one relationship")
 
@@ -2036,6 +2095,12 @@ def create_many_to_many_intermediary_model(field, klass):
 
 
 class ManyToManyField(RelatedField):
+    # Field flags
+    many_to_many = True
+    many_to_one = False
+    one_to_many = False
+    one_to_one = False
+
     description = _("Many-to-many relationship")
 
     def __init__(self, to, db_constraint=True, swappable=True, **kwargs):
@@ -2050,7 +2115,6 @@ class ManyToManyField(RelatedField):
             # Class names must be ASCII in Python 2.x, so we forcibly coerce it
             # here to break early if there's a problem.
             to = str(to)
-
         kwargs['verbose_name'] = kwargs.get('verbose_name', None)
         kwargs['rel'] = ManyToManyRel(
             self, to,
@@ -2357,8 +2421,8 @@ class ManyToManyField(RelatedField):
         """
         pathinfos = []
         int_model = self.rel.through
-        linkfield1 = int_model._meta.get_field_by_name(self.m2m_field_name())[0]
-        linkfield2 = int_model._meta.get_field_by_name(self.m2m_reverse_field_name())[0]
+        linkfield1 = int_model._meta.get_field(self.m2m_field_name())
+        linkfield2 = int_model._meta.get_field(self.m2m_reverse_field_name())
         if direct:
             join1infos = linkfield1.get_reverse_path_info()
             join2infos = linkfield2.get_path_info()
@@ -2398,8 +2462,8 @@ class ManyToManyField(RelatedField):
         else:
             link_field_name = None
         for f in self.rel.through._meta.fields:
-            if hasattr(f, 'rel') and f.rel and f.rel.to == related.model and \
-                    (link_field_name is None or link_field_name == f.name):
+            if (f.is_relation and f.rel.to == related.related_model and
+                    (link_field_name is None or link_field_name == f.name)):
                 setattr(self, cache_attr, getattr(f, attr))
                 return getattr(self, cache_attr)
 
@@ -2414,8 +2478,9 @@ class ManyToManyField(RelatedField):
         else:
             link_field_name = None
         for f in self.rel.through._meta.fields:
-            if hasattr(f, 'rel') and f.rel and f.rel.to == related.parent_model:
-                if link_field_name is None and related.model == related.parent_model:
+            # NOTE f.rel.to != f.related_model
+            if f.is_relation and f.rel.to == related.model:
+                if link_field_name is None and related.related_model == related.model:
                     # If this is an m2m-intermediate to self,
                     # the first foreign key you find will be
                     # the source column. Keep searching for
@@ -2479,7 +2544,7 @@ class ManyToManyField(RelatedField):
     def contribute_to_related_class(self, cls, related):
         # Internal M2Ms (i.e., those with a related name ending with '+')
         # and swapped models don't get a related descriptor.
-        if not self.rel.is_hidden() and not related.model._meta.swapped:
+        if not self.rel.is_hidden() and not related.related_model._meta.swapped:
             setattr(cls, related.get_accessor_name(), ManyRelatedObjectsDescriptor(related))
 
         # Set up the accessors for the column names on the m2m table

+ 2 - 6
django/db/models/manager.py

@@ -2,7 +2,6 @@ import copy
 from importlib import import_module
 import inspect
 
-from django.core.exceptions import FieldDoesNotExist
 from django.db import router
 from django.db.models.query import QuerySet
 from django.utils import six
@@ -23,15 +22,12 @@ def ensure_default_manager(cls):
         setattr(cls, 'objects', SwappedManagerDescriptor(cls))
         return
     if not getattr(cls, '_default_manager', None):
-        # Create the default manager, if needed.
-        try:
-            cls._meta.get_field('objects')
+        if any(f.name == 'objects' for f in cls._meta.fields):
             raise ValueError(
                 "Model %s must specify a custom Manager, because it has a "
                 "field named 'objects'" % cls.__name__
             )
-        except FieldDoesNotExist:
-            pass
+        # Create the default manager, if needed.
         cls.add_to_class('objects', Manager())
         cls._base_manager = cls.objects
     elif not getattr(cls, '_base_manager', None):

+ 481 - 251
django/db/models/options.py

@@ -1,20 +1,31 @@
 from __future__ import unicode_literals
 
 from bisect import bisect
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
+from itertools import chain
+import warnings
 
 from django.apps import apps
 from django.conf import settings
 from django.core.exceptions import FieldDoesNotExist
-from django.db.models.fields.related import ManyToManyRel
+from django.db.models.fields.related import ManyToManyField
 from django.db.models.fields import AutoField
 from django.db.models.fields.proxy import OrderWrt
 from django.utils import six
+from django.utils.datastructures import ImmutableList
+from django.utils.deprecation import RemovedInDjango20Warning
 from django.utils.encoding import force_text, smart_text, python_2_unicode_compatible
 from django.utils.functional import cached_property
+from django.utils.lru_cache import lru_cache
 from django.utils.text import camel_case_to_spaces
 from django.utils.translation import activate, deactivate_all, get_language, string_concat
 
+EMPTY_RELATION_TREE = tuple()
+
+IMMUTABLE_WARNING = (
+    "The return type of '%s' should never be mutated. If you want to manipulate this list "
+    "for your own use, make a copy first."
+)
 
 DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'unique_together', 'permissions', 'get_latest_by',
@@ -24,6 +35,24 @@ DEFAULT_NAMES = ('verbose_name', 'verbose_name_plural', 'db_table', 'ordering',
                  'select_on_save', 'default_related_name')
 
 
+class raise_deprecation(object):
+    def __init__(self, suggested_alternative):
+        self.suggested_alternative = suggested_alternative
+
+    def __call__(self, fn):
+        def wrapper(*args, **kwargs):
+            warnings.warn(
+                "'%s is an unofficial API that has been deprecated. "
+                "You may be able to replace it with '%s'" % (
+                    fn.__name__,
+                    self.suggested_alternative,
+                ),
+                RemovedInDjango20Warning, stacklevel=2
+            )
+            return fn(*args, **kwargs)
+        return wrapper
+
+
 def normalize_together(option_together):
     """
     option_together can be either a tuple of tuples, or a single
@@ -46,9 +75,19 @@ def normalize_together(option_together):
         return option_together
 
 
+def make_immutable_fields_list(name, data):
+    return ImmutableList(data, warning=IMMUTABLE_WARNING % name)
+
+
 @python_2_unicode_compatible
 class Options(object):
+    FORWARD_PROPERTIES = ('fields', 'many_to_many', 'concrete_fields',
+                          'local_concrete_fields', '_forward_fields_map')
+    REVERSE_PROPERTIES = ('related_objects', 'fields_map', '_relation_tree')
+
     def __init__(self, meta, app_label=None):
+        self._get_fields_cache = {}
+        self.proxied_children = []
         self.local_fields = []
         self.local_many_to_many = []
         self.virtual_fields = []
@@ -103,6 +142,31 @@ class Options(object):
 
         self.default_related_name = None
 
+    @lru_cache(maxsize=None)
+    def _map_model(self, link):
+        # This helper function is used to allow backwards compatibility with
+        # the previous API. No future methods should use this function.
+        # It maps a field to (field, model or related_model,) depending on the
+        # field type.
+        model = link.model._meta.concrete_model
+        if model is self.model:
+            model = None
+        return link, model
+
+    @lru_cache(maxsize=None)
+    def _map_model_details(self, link):
+        # This helper function is used to allow backwards compatibility with
+        # the previous API. No future methods should use this function.
+        # This function maps a field to a tuple of:
+        #  (field, model or related_model, direct, is_m2m) depending on the
+        # field type.
+        direct = not link.auto_created or link.concrete
+        model = link.model._meta.concrete_model
+        if model is self.model:
+            model = None
+        m2m = link.is_relation and link.many_to_many
+        return link, model, direct, m2m
+
     @property
     def app_config(self):
         # Don't go through get_app_config to avoid triggering imports.
@@ -183,7 +247,17 @@ class Options(object):
 
     def _prepare(self, model):
         if self.order_with_respect_to:
-            self.order_with_respect_to = self.get_field(self.order_with_respect_to)
+            # The app registry will not be ready at this point, so we cannot
+            # use get_field().
+            query = self.order_with_respect_to
+            try:
+                self.order_with_respect_to = next(
+                    f for f in self._get_fields(reverse=False)
+                    if f.name == query or f.attname == query
+                )
+            except StopIteration:
+                raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, query))
+
             self.ordering = ('_order',)
             if not any(isinstance(field, OrderWrt) for field in model._meta.local_fields):
                 model.add_to_class('_order', OrderWrt())
@@ -208,56 +282,41 @@ class Options(object):
                         auto_created=True)
                 model.add_to_class('id', auto)
 
-    def add_field(self, field):
+    def add_field(self, field, virtual=False):
         # Insert the given field in the order in which it was created, using
         # the "creation_counter" attribute of the field.
         # Move many-to-many related fields from self.fields into
         # self.many_to_many.
-        if field.rel and isinstance(field.rel, ManyToManyRel):
+        if virtual:
+            self.virtual_fields.append(field)
+        elif field.is_relation and field.many_to_many:
             self.local_many_to_many.insert(bisect(self.local_many_to_many, field), field)
-            if hasattr(self, '_m2m_cache'):
-                del self._m2m_cache
         else:
             self.local_fields.insert(bisect(self.local_fields, field), field)
             self.setup_pk(field)
-            if hasattr(self, '_field_cache'):
-                del self._field_cache
-                del self._field_name_cache
-                # The fields, concrete_fields and local_concrete_fields are
-                # implemented as cached properties for performance reasons.
-                # The attrs will not exists if the cached property isn't
-                # accessed yet, hence the try-excepts.
-                try:
-                    del self.fields
-                except AttributeError:
-                    pass
-                try:
-                    del self.concrete_fields
-                except AttributeError:
-                    pass
-                try:
-                    del self.local_concrete_fields
-                except AttributeError:
-                    pass
-
-        if hasattr(self, '_name_map'):
-            del self._name_map
 
-    def add_virtual_field(self, field):
-        self.virtual_fields.append(field)
+        # If the field being added is a relation to another known field,
+        # expire the cache on this field and the forward cache on the field
+        # being referenced, because there will be new relationships in the
+        # cache. Otherwise, expire the cache of references *to* this field.
+        # The mechanism for getting at the related model is slightly odd -
+        # ideally, we'd just ask for field.related_model. However, related_model
+        # is a cached property, and all the models haven't been loaded yet, so
+        # we need to make sure we don't cache a string reference.
+        if field.is_relation and hasattr(field.rel, 'to') and field.rel.to:
+            try:
+                field.rel.to._meta._expire_cache(forward=False)
+            except AttributeError:
+                pass
+            self._expire_cache()
+        else:
+            self._expire_cache(reverse=False)
 
     def setup_pk(self, field):
         if not self.pk and field.primary_key:
             self.pk = field
             field.serialize = False
 
-    def pk_index(self):
-        """
-        Returns the index of the primary key field in the self.concrete_fields
-        list.
-        """
-        return self.concrete_fields.index(self.pk)
-
     def setup_proxy(self, target):
         """
         Does the internal setup so that the current model is a proxy for
@@ -273,6 +332,7 @@ class Options(object):
     def __str__(self):
         return "%s.%s" % (smart_text(self.app_label), smart_text(self.model_name))
 
+    @property
     def verbose_name_raw(self):
         """
         There are a few places where the untranslated verbose name is needed
@@ -284,9 +344,9 @@ class Options(object):
         raw = force_text(self.verbose_name)
         activate(lang)
         return raw
-    verbose_name_raw = property(verbose_name_raw)
 
-    def _swapped(self):
+    @property
+    def swapped(self):
         """
         Has this model been swapped out for another? If so, return the model
         name of the replacement; otherwise, return None.
@@ -310,253 +370,253 @@ class Options(object):
                 if '%s.%s' % (swapped_label, swapped_object.lower()) not in (None, model_label):
                     return swapped_for
         return None
-    swapped = property(_swapped)
 
     @cached_property
     def fields(self):
         """
-        The getter for self.fields. This returns the list of field objects
-        available to this model (including through parent models).
-
-        Callers are not permitted to modify this list, since it's a reference
-        to this instance (not a copy).
-        """
-        try:
-            self._field_name_cache
-        except AttributeError:
-            self._fill_fields_cache()
-        return self._field_name_cache
+        Returns a list of all forward fields on the model and its parents,
+        excluding ManyToManyFields.
+
+        Private API intended only to be used by Django itself; get_fields()
+        combined with filtering of field properties is the public API for
+        obtaining this field list.
+        """
+        # For legacy reasons, the fields property should only contain forward
+        # fields that are not virtual or with a m2m cardinality. Therefore we
+        # pass these three filters as filters to the generator.
+        # 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,
+        # and all the models may not have been loaded yet; we don't want to cache
+        # the string reference to the related_model.
+        is_not_an_m2m_field = lambda f: not (f.is_relation and f.many_to_many)
+        is_not_a_generic_relation = lambda f: not (f.is_relation and f.many_to_one)
+        is_not_a_generic_foreign_key = lambda f: not (
+            f.is_relation and f.one_to_many and not (hasattr(f.rel, 'to') and f.rel.to)
+        )
+        return make_immutable_fields_list(
+            "fields",
+            (f for f in self._get_fields(reverse=False) if
+            is_not_an_m2m_field(f) and is_not_a_generic_relation(f)
+            and is_not_a_generic_foreign_key(f))
+        )
 
     @cached_property
     def concrete_fields(self):
-        return [f for f in self.fields if f.column is not None]
+        """
+        Returns a list of all concrete fields on the model and its parents.
+
+        Private API intended only to be used by Django itself; get_fields()
+        combined with filtering of field properties is the public API for
+        obtaining this field list.
+        """
+        return make_immutable_fields_list(
+            "concrete_fields", (f for f in self.fields if f.concrete)
+        )
 
     @cached_property
     def local_concrete_fields(self):
-        return [f for f in self.local_fields if f.column is not None]
-
-    def get_fields_with_model(self):
         """
-        Returns a sequence of (field, model) pairs for all fields. The "model"
-        element is None for fields on the current model. Mostly of use when
-        constructing queries so that we know which model a field belongs to.
+        Returns a list of all concrete fields on the model.
+
+        Private API intended only to be used by Django itself; get_fields()
+        combined with filtering of field properties is the public API for
+        obtaining this field list.
         """
-        try:
-            self._field_cache
-        except AttributeError:
-            self._fill_fields_cache()
-        return self._field_cache
+        return make_immutable_fields_list(
+            "local_concrete_fields", (f for f in self.local_fields if f.concrete)
+        )
 
-    def get_concrete_fields_with_model(self):
-        return [(field, model) for field, model in self.get_fields_with_model() if
-                field.column is not None]
+    @raise_deprecation(suggested_alternative="get_fields()")
+    def get_fields_with_model(self):
+        return [self._map_model(f) for f in self.get_fields()]
 
-    def _fill_fields_cache(self):
-        cache = []
-        for parent in self.parents:
-            for field, model in parent._meta.get_fields_with_model():
-                if model:
-                    cache.append((field, model))
-                else:
-                    cache.append((field, parent))
-        cache.extend((f, None) for f in self.local_fields)
-        self._field_cache = tuple(cache)
-        self._field_name_cache = [x for x, _ in cache]
-
-    def _many_to_many(self):
-        try:
-            self._m2m_cache
-        except AttributeError:
-            self._fill_m2m_cache()
-        return list(self._m2m_cache)
-    many_to_many = property(_many_to_many)
+    @raise_deprecation(suggested_alternative="get_fields()")
+    def get_concrete_fields_with_model(self):
+        return [self._map_model(f) for f in self.concrete_fields]
 
-    def get_m2m_with_model(self):
-        """
-        The many-to-many version of get_fields_with_model().
+    @cached_property
+    def many_to_many(self):
         """
-        try:
-            self._m2m_cache
-        except AttributeError:
-            self._fill_m2m_cache()
-        return list(six.iteritems(self._m2m_cache))
+        Returns a list of all many to many fields on the model and its parents.
 
-    def _fill_m2m_cache(self):
-        cache = OrderedDict()
-        for parent in self.parents:
-            for field, model in parent._meta.get_m2m_with_model():
-                if model:
-                    cache[field] = model
-                else:
-                    cache[field] = parent
-        for field in self.local_many_to_many:
-            cache[field] = None
-        self._m2m_cache = cache
-
-    def get_field(self, name, many_to_many=True):
+        Private API intended only to be used by Django itself; get_fields()
+        combined with filtering of field properties is the public API for
+        obtaining this list.
         """
-        Returns the requested field by name. Raises FieldDoesNotExist on error.
-        """
-        to_search = (self.fields + self.many_to_many) if many_to_many else self.fields
-        for f in to_search:
-            if f.name == name:
-                return f
-        raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, name))
+        return make_immutable_fields_list(
+            "many_to_many",
+            (f for f in self._get_fields(reverse=False)
+            if f.is_relation and f.many_to_many)
+        )
 
-    def get_field_by_name(self, name):
+    @cached_property
+    def related_objects(self):
         """
-        Returns the (field_object, model, direct, m2m), where field_object is
-        the Field instance for the given name, model is the model containing
-        this field (None for local fields), direct is True if the field exists
-        on this model, and m2m is True for many-to-many relations. When
-        'direct' is False, 'field_object' is the corresponding ForeignObjectRel
-        for this field (since the field doesn't have an instance associated
-        with it).
-
-        Uses a cache internally, so after the first access, this is very fast.
+        Returns all related objects pointing to the current model. The related
+        objects can come from a one-to-one, one-to-many, or many-to-many field
+        relation type.
+
+        Private API intended only to be used by Django itself; get_fields()
+        combined with filtering of field properties is the public API for
+        obtaining this field list.
         """
-        try:
+        all_related_fields = self._get_fields(forward=False, reverse=True, include_hidden=True)
+        return make_immutable_fields_list(
+            "related_objects",
+            (obj for obj in all_related_fields
+            if not obj.hidden or obj.field.many_to_many)
+        )
+
+    @raise_deprecation(suggested_alternative="get_fields()")
+    def get_m2m_with_model(self):
+        return [self._map_model(f) for f in self.many_to_many]
+
+    @cached_property
+    def _forward_fields_map(self):
+        res = {}
+        # call get_fields() with export_ordered_set=True in order to have a
+        # field_instance -> names map
+        fields = self._get_fields(reverse=False)
+        for field in fields:
+            res[field.name] = field
+            # Due to the way Django's internals work, get_field() should also
+            # be able to fetch a field by attname. In the case of a concrete
+            # field with relation, includes the *_id name too
             try:
-                return self._name_map[name]
+                res[field.attname] = field
             except AttributeError:
-                cache = self.init_name_map()
-                return cache[name]
-        except KeyError:
-            raise FieldDoesNotExist('%s has no field named %r'
-                    % (self.object_name, name))
+                pass
+        return res
 
-    def get_all_field_names(self):
+    @cached_property
+    def fields_map(self):
+        res = {}
+        fields = self._get_fields(forward=False, include_hidden=True)
+        for field in fields:
+            res[field.name] = field
+            # Due to the way Django's internals work, get_field() should also
+            # be able to fetch a field by attname. In the case of a concrete
+            # field with relation, includes the *_id name too
+            try:
+                res[field.attname] = field
+            except AttributeError:
+                pass
+        return res
+
+    def get_field(self, field_name, many_to_many=None):
         """
-        Returns a list of all field names that are possible for this model
-        (including reverse relation names). This is used for pretty printing
-        debugging output (a list of choices), so any internal-only field names
-        are not included.
+        Returns a field instance given a field name. The field can be either a
+        forward or reverse field, unless many_to_many is specified; if it is,
+        only forward fields will be returned.
+
+        The many_to_many argument exists for backwards compatibility reasons;
+        it has been deprecated and will be removed in Django 2.0.
         """
+        m2m_in_kwargs = many_to_many is not None
+        if m2m_in_kwargs:
+            # Always throw a warning if many_to_many is used regardless of
+            # whether it alters the return type or not.
+            warnings.warn(
+                "The 'many_to_many' argument on get_field() is deprecated; "
+                "use a filter on field.many_to_many instead.",
+                RemovedInDjango20Warning
+            )
+
         try:
-            cache = self._name_map
-        except AttributeError:
-            cache = self.init_name_map()
-        names = sorted(cache.keys())
-        # Internal-only names end with "+" (symmetrical m2m related names being
-        # the main example). Trim them.
-        return [val for val in names if not val.endswith('+')]
-
-    def init_name_map(self):
-        """
-        Initialises the field name -> field object mapping.
-        """
-        cache = {}
-        # We intentionally handle related m2m objects first so that symmetrical
-        # m2m accessor names can be overridden, if necessary.
-        for f, model in self.get_all_related_m2m_objects_with_model():
-            cache[f.field.related_query_name()] = (f, model, False, True)
-        for f, model in self.get_all_related_objects_with_model():
-            cache[f.field.related_query_name()] = (f, model, False, False)
-        for f, model in self.get_m2m_with_model():
-            cache[f.name] = cache[f.attname] = (f, model, True, True)
-        for f, model in self.get_fields_with_model():
-            cache[f.name] = cache[f.attname] = (f, model, True, False)
-        for f in self.virtual_fields:
-            if f.rel:
-                cache[f.name] = cache[f.attname] = (
-                    f, None if f.model == self.model else f.model, True, False)
-        if apps.ready:
-            self._name_map = cache
-        return cache
+            # In order to avoid premature loading of the relation tree
+            # (expensive) we prefer checking if the field is a forward field.
+            field = self._forward_fields_map[field_name]
+
+            if many_to_many is False and field.many_to_many:
+                raise FieldDoesNotExist(
+                    '%s has no field named %r' % (self.object_name, field_name)
+                )
+
+            return field
+        except KeyError:
+            # If the app registry is not ready, reverse fields are
+            # unavailable, therefore we throw a FieldDoesNotExist exception.
+            if not self.apps.ready:
+                raise FieldDoesNotExist(
+                    "%s has no field named %r. The app cache isn't "
+                    "ready yet, so if this is a forward field, it won't "
+                    "be available yet." % (self.object_name, field_name)
+                )
+
+        try:
+            if m2m_in_kwargs:
+                # Previous API does not allow searching reverse fields.
+                raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, field_name))
 
+            # Retrieve field instance by name from cached or just-computed
+            # field map.
+            return self.fields_map[field_name]
+        except KeyError:
+            raise FieldDoesNotExist('%s has no field named %r' % (self.object_name, field_name))
+
+    @raise_deprecation(suggested_alternative="get_field()")
+    def get_field_by_name(self, name):
+        return self._map_model_details(self.get_field(name))
+
+    @raise_deprecation(suggested_alternative="get_fields()")
+    def get_all_field_names(self):
+        names = set()
+        fields = self.get_fields()
+        for field in fields:
+            # For backwards compatibility GenericForeignKey should not be
+            # included in the results.
+            if field.is_relation and field.one_to_many and field.related_model is None:
+                continue
+
+            names.add(field.name)
+            if hasattr(field, 'attname'):
+                names.add(field.attname)
+        return list(names)
+
+    @raise_deprecation(suggested_alternative="get_fields()")
     def get_all_related_objects(self, local_only=False, include_hidden=False,
                                 include_proxy_eq=False):
-        return [k for k, v in self.get_all_related_objects_with_model(
-                local_only=local_only, include_hidden=include_hidden,
-                include_proxy_eq=include_proxy_eq)]
 
-    def get_all_related_objects_with_model(self, local_only=False,
-                                           include_hidden=False,
+        include_parents = local_only is False
+        fields = self._get_fields(
+            forward=False, reverse=True,
+            include_parents=include_parents,
+            include_hidden=include_hidden,
+        )
+        fields = (obj for obj in fields if not isinstance(obj.field, ManyToManyField))
+
+        if include_proxy_eq:
+            children = chain.from_iterable(c._relation_tree
+                                           for c in self.concrete_model._meta.proxied_children
+                                           if c is not self)
+            relations = (f.rel for f in children
+                         if include_hidden or not f.rel.field.rel.is_hidden())
+            fields = chain(fields, relations)
+        return list(fields)
+
+    @raise_deprecation(suggested_alternative="get_fields()")
+    def get_all_related_objects_with_model(self, local_only=False, include_hidden=False,
                                            include_proxy_eq=False):
-        """
-        Returns a list of (related-object, model) pairs. Similar to
-        get_fields_with_model().
-        """
-        try:
-            self._related_objects_cache
-        except AttributeError:
-            self._fill_related_objects_cache()
-        predicates = []
-        if local_only:
-            predicates.append(lambda k, v: not v)
-        if not include_hidden:
-            predicates.append(lambda k, v: not k.field.rel.is_hidden())
-        cache = (self._related_objects_proxy_cache if include_proxy_eq
-                 else self._related_objects_cache)
-        return [t for t in cache.items() if all(p(*t) for p in predicates)]
-
-    def _fill_related_objects_cache(self):
-        cache = OrderedDict()
-        parent_list = self.get_parent_list()
-        for parent in self.parents:
-            for obj, model in parent._meta.get_all_related_objects_with_model(include_hidden=True):
-                if (obj.field.creation_counter < 0 or obj.field.rel.parent_link) and obj.model not in parent_list:
-                    continue
-                if not model:
-                    cache[obj] = parent
-                else:
-                    cache[obj] = model
-        # Collect also objects which are in relation to some proxy child/parent of self.
-        proxy_cache = cache.copy()
-        for klass in self.apps.get_models(include_auto_created=True):
-            if not klass._meta.swapped:
-                for f in klass._meta.local_fields + klass._meta.virtual_fields:
-                    if (hasattr(f, 'rel') and f.rel and not isinstance(f.rel.to, six.string_types)
-                            and f.generate_reverse_relation):
-                        if self == f.rel.to._meta:
-                            cache[f.rel] = None
-                            proxy_cache[f.rel] = None
-                        elif self.concrete_model == f.rel.to._meta.concrete_model:
-                            proxy_cache[f.rel] = None
-        self._related_objects_cache = cache
-        self._related_objects_proxy_cache = proxy_cache
+        return [
+            self._map_model(f) for f in self.get_all_related_objects(
+                local_only=local_only,
+                include_hidden=include_hidden,
+                include_proxy_eq=include_proxy_eq,
+            )
+        ]
 
+    @raise_deprecation(suggested_alternative="get_fields()")
     def get_all_related_many_to_many_objects(self, local_only=False):
-        try:
-            cache = self._related_many_to_many_cache
-        except AttributeError:
-            cache = self._fill_related_many_to_many_cache()
-        if local_only:
-            return [k for k, v in cache.items() if not v]
-        return list(cache)
+        fields = self._get_fields(
+            forward=False, reverse=True,
+            include_parents=local_only is not True, include_hidden=True
+        )
+        return [obj for obj in fields if isinstance(obj.field, ManyToManyField)]
 
+    @raise_deprecation(suggested_alternative="get_fields()")
     def get_all_related_m2m_objects_with_model(self):
-        """
-        Returns a list of (related-m2m-object, model) pairs. Similar to
-        get_fields_with_model().
-        """
-        try:
-            cache = self._related_many_to_many_cache
-        except AttributeError:
-            cache = self._fill_related_many_to_many_cache()
-        return list(six.iteritems(cache))
-
-    def _fill_related_many_to_many_cache(self):
-        cache = OrderedDict()
-        parent_list = self.get_parent_list()
-        for parent in self.parents:
-            for obj, model in parent._meta.get_all_related_m2m_objects_with_model():
-                if obj.field.creation_counter < 0 and obj.model not in parent_list:
-                    continue
-                if not model:
-                    cache[obj] = parent
-                else:
-                    cache[obj] = model
-        for klass in self.apps.get_models():
-            if not klass._meta.swapped:
-                for f in klass._meta.local_many_to_many:
-                    if (f.rel
-                            and not isinstance(f.rel.to, six.string_types)
-                            and self == f.rel.to._meta):
-                        cache[f.rel] = None
-        if apps.ready:
-            self._related_many_to_many_cache = cache
-        return cache
+        fields = self._get_fields(forward=False, reverse=True, include_hidden=True)
+        return [self._map_model(obj) for obj in fields if isinstance(obj.field, ManyToManyField)]
 
     def get_base_chain(self, model):
         """
@@ -605,3 +665,173 @@ class Options(object):
                 # of the chain to the ancestor is that parent
                 # links
                 return self.parents[parent] or parent_link
+
+    def _populate_directed_relation_graph(self):
+        """
+        This method is used by each model to find its reverse objects. As this
+        method is very expensive and is accessed frequently (it looks up every
+        field in a model, in every app), it is computed on first access and then
+        is set as a property on every model.
+        """
+        related_objects_graph = defaultdict(list)
+
+        all_models = self.apps.get_models(include_auto_created=True)
+        for model in all_models:
+            fields_with_relations = (
+                f for f in model._meta._get_fields(reverse=False)
+                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:
+                if not isinstance(f.rel.to, six.string_types):
+                    # Set options_instance -> field
+                    related_objects_graph[f.rel.to._meta].append(f)
+
+        for model in all_models:
+            # Set the relation_tree using the internal __dict__. In this way
+            # we avoid calling the cached property. In attribute lookup,
+            # __dict__ takes precedence over a data descriptor (such as
+            # @cached_property). This means that the _meta._relation_tree is
+            # only called if related_objects is not in __dict__.
+            related_objects = related_objects_graph[model._meta]
+
+            # If related_objects are empty, it makes sense to set
+            # EMPTY_RELATION_TREE. This will avoid allocating multiple empty
+            # relation trees.
+            relation_tree = EMPTY_RELATION_TREE
+            if related_objects:
+                relation_tree = related_objects
+            model._meta.__dict__['_relation_tree'] = relation_tree
+
+    @cached_property
+    def _relation_tree(self):
+        # If cache is not present, populate the cache
+        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):
+        # This method is usually called by apps.cache_clear(), when the
+        # registry is finalized, or when a new field is added.
+        properties_to_expire = []
+        if forward:
+            properties_to_expire.extend(self.FORWARD_PROPERTIES)
+        if reverse and not self.abstract:
+            properties_to_expire.extend(self.REVERSE_PROPERTIES)
+
+        for cache_key in properties_to_expire:
+            try:
+                delattr(self, cache_key)
+            except AttributeError:
+                pass
+
+        self._get_fields_cache = {}
+
+    def get_fields(self, include_parents=True, include_hidden=False):
+        """
+        Returns a list of fields associated to the model. By default will only
+        return forward fields. This can be changed by enabling or disabling
+        field types using the parameters:
+
+        - include_parents: include fields derived from inheritance
+        - include_hidden:  include fields that have a related_name that
+                           starts with a "+"
+        """
+        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,
+                    export_ordered_set=False):
+        # This helper function is used to allow recursion in ``get_fields()``
+        # implementation and to provide a fast way for Django's internals to
+        # access specific subsets of fields.
+
+        # Creates a cache key composed of all arguments
+        cache_key = (forward, reverse, include_parents, include_hidden, export_ordered_set)
+        try:
+            # In order to avoid list manipulation. Always return a shallow copy
+            # of the results.
+            return self._get_fields_cache[cache_key]
+        except KeyError:
+            pass
+
+        # Using an OrderedDict preserves the order of insertion. This is
+        # 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
+                # options provided in this call.
+                for parent in self.parents:
+                    for obj, _ in six.iteritems(parent._meta._get_fields(forward=False, **options)):
+                        if obj.many_to_many:
+                            # In order for a reverse ManyToManyRel object to be
+                            # valid, its creation counter must be > 0 and must
+                            # be in the parent list.
+                            if not (obj.field.creation_counter < 0 and obj.related_model not in parent_list):
+                                fields[obj] = True
+
+                        elif not ((obj.field.creation_counter < 0 or obj.field.rel.parent_link)
+                                  and obj.related_model not in parent_list):
+                            fields[obj] = True
+
+            # 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
+            # from other models. If the model is a proxy model, then we also
+            # add the concrete model.
+            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
+                # intentionally hidden, add to the fields dict.
+                if include_hidden or not field.hidden:
+                    fields[field] = True
+        if forward:
+            if include_parents:
+                for parent in self.parents:
+                    # Add the forward fields of each parent.
+                    fields.update(parent._meta._get_fields(reverse=False, **options))
+            fields.update(
+                (field, True,)
+                for field in chain(self.local_fields, self.local_many_to_many)
+            )
+
+        if not export_ordered_set:
+            # By default, fields contains field instances as keys and all
+            # 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)
+
+        # Store result into cache for later access
+        self._get_fields_cache[cache_key] = fields
+
+        # In order to avoid list manipulation. Always
+        # return a shallow copy of the results
+        return fields

+ 15 - 11
django/db/models/query.py

@@ -252,9 +252,8 @@ class QuerySet(object):
         # If only/defer clauses have been specified,
         # build the list of fields that are to be loaded.
         if only_load:
-            for field, model in self.model._meta.get_concrete_fields_with_model():
-                if model is None:
-                    model = self.model
+            for field in self.model._meta.concrete_fields:
+                model = field.model._meta.model
                 try:
                     if field.name in only_load[model]:
                         # Add a field that has been explicitly included
@@ -818,7 +817,7 @@ class QuerySet(object):
         obj = self._clone()
         names = getattr(self, '_fields', None)
         if names is None:
-            names = set(self.model._meta.get_all_field_names())
+            names = {f.name for f in self.model._meta.get_fields()}
 
         # Add the annotations to the query
         for alias, annotation in annotations.items():
@@ -1329,7 +1328,8 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
         skip = set()
         init_list = []
         # Build the list of fields that *haven't* been requested
-        for field, model in klass._meta.get_concrete_fields_with_model():
+        for field in klass._meta.concrete_fields:
+            model = field.model._meta.concrete_model
             if from_parent and model and issubclass(from_parent, model):
                 # Avoid loading fields already loaded for parent model for
                 # child models.
@@ -1381,18 +1381,19 @@ def get_klass_info(klass, max_depth=0, cur_depth=0, requested=None,
 
     reverse_related_fields = []
     if restricted:
-        for o in klass._meta.get_all_related_objects():
+        for o in klass._meta.related_objects:
             if o.field.unique and select_related_descend(o.field, restricted, requested,
-                                                         only_load.get(o.model), reverse=True):
+                                                         only_load.get(o.related_model), reverse=True):
                 next = requested[o.field.related_query_name()]
-                parent = klass if issubclass(o.model, klass) else None
-                klass_info = get_klass_info(o.model, max_depth=max_depth, cur_depth=cur_depth + 1,
+                parent = klass if issubclass(o.related_model, klass) else None
+                klass_info = get_klass_info(o.related_model, max_depth=max_depth, cur_depth=cur_depth + 1,
                                             requested=next, only_load=only_load, from_parent=parent)
                 reverse_related_fields.append((o.field, klass_info))
     if field_names:
         pk_idx = field_names.index(klass._meta.pk.attname)
     else:
-        pk_idx = klass._meta.pk_index()
+        meta = klass._meta
+        pk_idx = meta.concrete_fields.index(meta.pk)
 
     return klass, field_names, field_count, related_fields, reverse_related_fields, pk_idx
 
@@ -1485,7 +1486,10 @@ def get_cached_row(row, index_start, using, klass_info, offset=0,
     for f, klass_info in reverse_related_fields:
         # Transfer data from this object to childs.
         parent_data = []
-        for rel_field, rel_model in klass_info[0]._meta.get_fields_with_model():
+        for rel_field in klass_info[0]._meta.fields:
+            rel_model = rel_field.model._meta.concrete_model
+            if rel_model == klass_info[0]._meta.model:
+                rel_model = None
             if rel_model is not None and isinstance(obj, rel_model):
                 parent_data.append((rel_field, getattr(obj, rel_field.attname)))
         # Recursively retrieve the data for the related object

+ 2 - 2
django/db/models/query_utils.py

@@ -109,7 +109,7 @@ class DeferredAttribute(object):
             # self.field_name is the attname of the field, but only() takes the
             # actual name, so we need to translate it here.
             try:
-                f = opts.get_field_by_name(self.field_name)[0]
+                f = opts.get_field(self.field_name)
             except FieldDoesNotExist:
                 f = [f for f in opts.fields if f.attname == self.field_name][0]
             name = f.name
@@ -136,7 +136,7 @@ class DeferredAttribute(object):
         field is a primary key field.
         """
         opts = instance._meta
-        f = opts.get_field_by_name(name)[0]
+        f = opts.get_field(name)
         link_field = opts.get_ancestor_link(f.model)
         if f.primary_key and f != link_field:
             return getattr(instance, link_field.attname)

+ 15 - 13
django/db/models/sql/compiler.py

@@ -298,7 +298,12 @@ class SQLCompiler(object):
         # be used by local fields.
         seen_models = {None: start_alias}
 
-        for field, model in opts.get_concrete_fields_with_model():
+        for field in opts.concrete_fields:
+            model = field.model._meta.concrete_model
+            # A proxy model will have a different model and concrete_model. We
+            # will assign None if the field belongs to this model.
+            if model == opts.model:
+                model = None
             if from_parent and model is not None and issubclass(from_parent, model):
                 # Avoid loading data for already loaded parents.
                 continue
@@ -601,10 +606,10 @@ class SQLCompiler(object):
         connections to the root model).
         """
         def _get_field_choices():
-            direct_choices = (f.name for (f, _) in opts.get_fields_with_model() if f.rel)
+            direct_choices = (f.name for f in opts.fields if f.is_relation)
             reverse_choices = (
                 f.field.related_query_name()
-                for f in opts.get_all_related_objects() if f.field.unique
+                for f in opts.related_objects if f.field.unique
             )
             return chain(direct_choices, reverse_choices)
 
@@ -628,12 +633,13 @@ class SQLCompiler(object):
             else:
                 restricted = False
 
-        for f, model in opts.get_fields_with_model():
+        for f in opts.fields:
+            field_model = f.model._meta.concrete_model
             fields_found.add(f.name)
 
             if restricted:
                 next = requested.get(f.name, {})
-                if not f.rel:
+                if not f.is_relation:
                     # If a non-related field is used like a relation,
                     # or if a single non-relational field is given.
                     if next or (cur_depth == 1 and f.name in requested):
@@ -647,10 +653,6 @@ class SQLCompiler(object):
             else:
                 next = False
 
-            # The get_fields_with_model() returns None for fields that live
-            # in the field's local model. So, for those fields we want to use
-            # the f.model - that is the field's local model.
-            field_model = model or f.model
             if not select_related_descend(f, restricted, requested,
                                           only_load.get(field_model)):
                 continue
@@ -666,9 +668,9 @@ class SQLCompiler(object):
 
         if restricted:
             related_fields = [
-                (o.field, o.model)
-                for o in opts.get_all_related_objects()
-                if o.field.unique
+                (o.field, o.related_model)
+                for o in opts.related_objects
+                if o.field.unique and not o.many_to_many
             ]
             for f, model in related_fields:
                 if not select_related_descend(f, restricted, requested,
@@ -760,7 +762,7 @@ class SQLCompiler(object):
                     if self.query.select:
                         fields = [f.field for f in self.query.select]
                     elif self.query.default_cols:
-                        fields = self.query.get_meta().concrete_fields
+                        fields = list(self.query.get_meta().concrete_fields)
                     else:
                         fields = []
 

+ 39 - 13
django/db/models/sql/query.py

@@ -11,6 +11,7 @@ from itertools import count, product
 
 from collections import Mapping, OrderedDict
 import copy
+from itertools import chain
 import warnings
 
 from django.core.exceptions import FieldDoesNotExist, FieldError
@@ -33,6 +34,13 @@ from django.utils.tree import Node
 __all__ = ['Query', 'RawQuery']
 
 
+def get_field_names_from_opts(opts):
+    return set(chain.from_iterable(
+        (f.name, f.attname) if f.concrete else (f.name,)
+        for f in opts.get_fields()
+    ))
+
+
 class RawQuery(object):
     """
     A single raw SQL query
@@ -593,9 +601,9 @@ class Query(object):
             opts = orig_opts
             for name in parts[:-1]:
                 old_model = cur_model
-                source = opts.get_field_by_name(name)[0]
+                source = opts.get_field(name)
                 if is_reverse_o2o(source):
-                    cur_model = source.model
+                    cur_model = source.related_model
                 else:
                     cur_model = source.rel.to
                 opts = cur_model._meta
@@ -605,8 +613,11 @@ class Query(object):
                 if not is_reverse_o2o(source):
                     must_include[old_model].add(source)
                 add_to_dict(must_include, cur_model, opts.pk)
-            field, model, _, _ = opts.get_field_by_name(parts[-1])
-            if model is None:
+            field = opts.get_field(parts[-1])
+            is_reverse_object = field.auto_created and not field.concrete
+            model = field.related_model if is_reverse_object else field.model
+            model = model._meta.concrete_model
+            if model == opts.model:
                 model = cur_model
             if not is_reverse_o2o(field):
                 add_to_dict(seen, model, field)
@@ -618,10 +629,11 @@ class Query(object):
             # models.
             workset = {}
             for model, values in six.iteritems(seen):
-                for field, m in model._meta.get_fields_with_model():
+                for field in model._meta.fields:
                     if field in values:
                         continue
-                    add_to_dict(workset, m or model, field)
+                    m = field.model._meta.concrete_model
+                    add_to_dict(workset, m, field)
             for model, values in six.iteritems(must_include):
                 # If we haven't included a model in workset, we don't add the
                 # corresponding must_include fields for that model, since an
@@ -934,8 +946,9 @@ class Query(object):
         root_alias = self.tables[0]
         seen = {None: root_alias}
 
-        for field, model in opts.get_fields_with_model():
-            if model not in seen:
+        for field in opts.fields:
+            model = field.model._meta.concrete_model
+            if model is not opts.model and model not in seen:
                 self.join_parent_model(opts, model, root_alias, seen)
         self.included_inherited_models = seen
 
@@ -1368,7 +1381,19 @@ class Query(object):
             if name == 'pk':
                 name = opts.pk.name
             try:
-                field, model, _, _ = opts.get_field_by_name(name)
+                field = opts.get_field(name)
+
+                # Fields that contain one-to-many relations with a generic
+                # model (like a GenericForeignKey) cannot generate reverse
+                # relations and therefore cannot be used for reverse querying.
+                if field.is_relation and not field.related_model:
+                    raise FieldError(
+                        "Field %r does not generate an automatic reverse "
+                        "relation and therefore cannot be used for reverse "
+                        "querying. If it is a GenericForeignKey, consider "
+                        "adding a GenericRelation." % name
+                    )
+                model = field.model._meta.concrete_model
             except FieldDoesNotExist:
                 # is it an annotation?
                 if self._annotations and name in self._annotations:
@@ -1382,14 +1407,15 @@ class Query(object):
                 # one step.
                 pos -= 1
                 if pos == -1 or fail_on_missing:
-                    available = opts.get_all_field_names() + list(self.annotation_select)
+                    field_names = list(get_field_names_from_opts(opts))
+                    available = sorted(field_names + list(self.annotation_select))
                     raise FieldError("Cannot resolve keyword %r into field. "
                                      "Choices are: %s" % (name, ", ".join(available)))
                 break
             # Check if we need any joins for concrete inheritance cases (the
             # field lives in parent, but we are currently in one of its
             # children)
-            if model:
+            if model is not opts.model:
                 # The field lives on a base class of the current model.
                 # Skip the chain of proxy to the concrete proxied model
                 proxied_model = opts.concrete_model
@@ -1432,7 +1458,7 @@ class Query(object):
         return path, final_field, targets, names[pos + 1:]
 
     def raise_field_error(self, opts, name):
-        available = opts.get_all_field_names() + list(self.annotation_select)
+        available = list(get_field_names_from_opts(opts)) + list(self.annotation_select)
         raise FieldError("Cannot resolve keyword %r into field. "
                          "Choices are: %s" % (name, ", ".join(available)))
 
@@ -1693,7 +1719,7 @@ class Query(object):
                 # from the model on which the lookup failed.
                 raise
             else:
-                names = sorted(opts.get_all_field_names() + list(self.extra)
+                names = sorted(list(get_field_names_from_opts(opts)) + list(self.extra)
                                + list(self.annotation_select))
                 raise FieldError("Cannot resolve keyword %r into field. "
                                  "Choices are: %s" % (name, ", ".join(names)))

+ 5 - 3
django/db/models/sql/subqueries.py

@@ -120,13 +120,15 @@ class UpdateQuery(Query):
         """
         values_seq = []
         for name, val in six.iteritems(values):
-            field, model, direct, m2m = self.get_meta().get_field_by_name(name)
-            if not direct or m2m:
+            field = self.get_meta().get_field(name)
+            direct = not (field.auto_created and not field.concrete) or not field.concrete
+            model = field.model._meta.concrete_model
+            if not direct or (field.is_relation and field.many_to_many):
                 raise FieldError(
                     'Cannot update model field %r (only non-relations and '
                     'foreign keys permitted).' % field
                 )
-            if model:
+            if model is not self.get_meta().model:
                 self.add_related_update(model, field, val)
                 continue
             values_seq.append((field, model, val))

+ 4 - 3
django/forms/models.py

@@ -6,6 +6,7 @@ and database field objects.
 from __future__ import unicode_literals
 
 from collections import OrderedDict
+from itertools import chain
 import warnings
 
 from django.core.exceptions import (
@@ -89,7 +90,7 @@ def save_instance(form, instance, fields=None, fail_message='saved',
         # Note that for historical reasons we want to include also
         # virtual_fields here. (GenericRelation was previously a fake
         # m2m field).
-        for f in opts.many_to_many + opts.virtual_fields:
+        for f in chain(opts.many_to_many, opts.virtual_fields):
             if not hasattr(f, 'save_form_data'):
                 continue
             if fields and f.name not in fields:
@@ -127,7 +128,7 @@ def model_to_dict(instance, fields=None, exclude=None):
     from django.db.models.fields.related import ManyToManyField
     opts = instance._meta
     data = {}
-    for f in opts.concrete_fields + opts.virtual_fields + opts.many_to_many:
+    for f in chain(opts.concrete_fields, opts.virtual_fields, opts.many_to_many):
         if not getattr(f, 'editable', False):
             continue
         if fields and f.name not in fields:
@@ -186,7 +187,7 @@ def fields_for_model(model, fields=None, exclude=None, widgets=None,
     from django.db.models.fields import Field as ModelField
     sortable_virtual_fields = [f for f in opts.virtual_fields
                                if isinstance(f, ModelField)]
-    for f in sorted(opts.concrete_fields + sortable_virtual_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):
             continue
         if fields is not None and f.name not in fields:

+ 3 - 3
docs/howto/custom-model-fields.txt

@@ -217,9 +217,9 @@ The ``Field.__init__()`` method takes the following parameters:
 * :attr:`~django.db.models.Field.db_tablespace`: Only for index creation, if the
   backend supports :doc:`tablespaces </topics/db/tablespaces>`. You can usually
   ignore this option.
-* ``auto_created``: ``True`` if the field was automatically created, as for the
-  :class:`~django.db.models.OneToOneField` used by model inheritance. For
-  advanced use only.
+* :attr:`~django.db.models.Field.auto_created`: ``True`` if the field was
+  automatically created, as for the :class:`~django.db.models.OneToOneField`
+  used by model inheritance. For advanced use only.
 
 All of the options without an explanation in the above list have the same
 meaning they do for normal Django fields. See the :doc:`field documentation

+ 13 - 0
docs/internals/deprecation.txt

@@ -56,6 +56,19 @@ details on these changes.
 
 * ``django.template.resolve_variable`` will be removed.
 
+* The following private APIs will be removed from
+  :class:`django.db.models.options.Options` (``Model._meta``):
+
+  * ``get_field_by_name()``
+  * ``get_all_field_names()``
+  * ``get_fields_with_model()``
+  * ``get_concrete_fields_with_model()``
+  * ``get_m2m_with_model()``
+  * ``get_all_related_objects()``
+  * ``get_all_related_objects_with_model()``
+  * ``get_all_related_many_to_many_objects()``
+  * ``get_all_related_m2m_objects_with_model()``
+
 * The ``error_message`` argument of ``django.forms.RegexField`` will be removed.
 
 * The ``unordered_list`` filter will no longer support old style lists.

+ 92 - 0
docs/ref/models/fields.txt

@@ -1790,3 +1790,95 @@ Field API reference
 
         This method must be added to fields prior to 1.7 to migrate its data
         using :doc:`/topics/migrations`.
+
+.. _model-field-attributes:
+
+=========================
+Field attribute reference
+=========================
+
+.. versionadded:: 1.8
+
+Every ``Field`` instance contains several attributes that allow
+introspecting its behavior. Use these attributes instead of ``isinstance``
+checks when you need to write code that depends on a field's functionality.
+These attributes can be used together with the :ref:`Model._meta API
+<model-meta-field-api>` to narrow down a search for specific field types.
+Custom model fields should implement these flags.
+
+Attributes for fields
+=====================
+
+.. attribute:: Field.auto_created
+
+     Boolean flag that indicates if the field was automatically created, such
+     as the ``OneToOneField`` used by model inheritance.
+
+.. attribute:: Field.concrete
+
+    Boolean flag that indicates if the field has a database column associated
+    with it.
+
+.. attribute:: Field.hidden
+
+    Boolean flag that indicates if a field is used to back another non-hidden
+    field's functionality (e.g. the ``content_type`` and ``object_id`` fields
+    that make up a ``GenericForeignKey``). The ``hidden`` flag is used to
+    distinguish what constitutes the public subset of fields on the model from
+    all the fields on the model.
+
+    .. note::
+
+        :meth:`Options.get_fields()
+        <django.db.models.options.Options.get_fields()>`
+        excludes hidden fields by default. Pass in ``include_hidden=True`` to
+        return hidden fields in the results.
+
+.. attribute:: Field.is_relation
+
+    Boolean flag that indicates if a field contains references to one or
+    more other models for its functionality (e.g. ``ForeignKey``,
+    ``ManyToManyField``, ``OneToOneField``, etc.).
+
+.. attribute:: Field.model
+
+    Returns the model on which the field is defined. If a field is defined on
+    a superclass of a model, ``model`` will refer to the superclass, not the
+    class of the instance.
+
+Attributes for fields with relations
+====================================
+
+These attributes are used to query for the cardinality and other details of a
+relation. These attribute are present on all fields; however, they will only
+have meaningful values if the field is a relation type
+(:attr:`Field.is_relation=True <Field.is_relation>`).
+
+.. attribute:: Field.one_to_many
+
+    Boolean flag that is ``True`` if the field has a one-to-many relation, such
+    as a ``ForeignKey``; ``False`` otherwise.
+
+.. attribute:: Field.one_to_one
+
+    Boolean flag that is ``True`` if the field has a one-to-one relation, such
+    as a ``OneToOneField``; ``False`` otherwise.
+
+.. attribute:: Field.many_to_many
+
+    Boolean flag that is ``True`` if the field has a many-to-many relation;
+    ``False`` otherwise. The only field included with Django where this is
+    ``True`` is ``ManyToManyField``.
+
+.. attribute:: Field.many_to_one
+
+    Boolean flag that is ``True`` if the field has a many-to-one relation, such
+    as a ``GenericRelation`` or the reverse of a ``ForeignKey``; ``False``
+    otherwise.
+
+.. attribute:: Field.related_model
+
+    Points to the model the field relates to. For example, ``Author`` in
+    ``ForeignKey(Author)``. If a field has a generic relation (such as a
+    ``GenericForeignKey`` or a ``GenericRelation``) then ``related_model``
+    will be ``None``.

+ 1 - 0
docs/ref/models/index.txt

@@ -8,6 +8,7 @@ Model API reference. For introductory material, see :doc:`/topics/db/models`.
    :maxdepth: 1
 
    fields
+   meta
    relations
    class
    options

+ 287 - 0
docs/ref/models/meta.txt

@@ -0,0 +1,287 @@
+===================
+Model ``_meta`` API
+===================
+
+.. module:: django.db.models.options
+   :synopsis: Model meta-class layer
+
+.. class:: Options
+
+The model ``_meta`` API is at the core of the Django ORM. It enables other
+parts of the system such as lookups, queries, forms, and the admin to
+understand the capabilities of each model. The API is accessible through
+the ``_meta`` attribute of each model class, which is an instance of an
+``django.db.models.options.Options`` object.
+
+Methods that it provides can be used to:
+
+* Retrieve all field instances of a model
+* Retrieve a single field instance of a model by name
+
+.. versionchanged:: 1.8
+
+    The Model ``_meta`` API has always existed as a Django internal, but
+    wasn't formally documented and supported. As part of the effort to
+    make this API public, some of the already existing API entry points
+    have changed slightly. A :ref:`migration guide <migrating-old-meta-api>`
+    has been provided to assist in converting your code to use the new,
+    official API.
+
+.. _model-meta-field-api:
+
+Field access API
+================
+
+Retrieving a single field instance of a model by name
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. method:: Options.get_field(field_name)
+
+    Returns the field instance given a name of a field.
+
+    ``field_name`` can be the name of a field on the model, a field
+    on an abstract or inherited model, or a field defined on another
+    model that points to the model. In the latter case, the ``field_name``
+    will be the ``related_name`` defined by the user or the name automatically
+    generated by Django itself.
+
+    :attr:`Hidden fields <django.db.models.Field.hidden>` cannot be retrieved
+    by name.
+
+    If a field with the given name is not found a
+    :class:`~django.core.exceptions.FieldDoesNotExist` exception will be
+    raised.
+
+    .. code-block:: python
+
+        >>> from django.contrib.auth.models import User
+
+        # A field on the model
+        >>> User._meta.get_field('username')
+        <django.db.models.fields.CharField: username>
+
+        # A field from another model that has a relation with the current model
+        >>> User._meta.get_field('logentry')
+        <ManyToOneRel: admin.logentry>
+
+        # A non existent field
+        >>> User._meta.get_field('does_not_exist')
+        Traceback (most recent call last):
+            ...
+        FieldDoesNotExist: User has no field named 'does_not_exist'
+
+    .. deprecated:: 1.8
+
+        :meth:`Options.get_field()` previously accepted a ``many_to_many``
+        parameter which could be set to ``False`` to avoid searching
+        ``ManyToManyField``\s. The old behavior has been preserved for
+        backwards compatibility; however, the parameter and this behavior
+        has been deprecated.
+
+        If you wish to filter out ``ManyToManyField``\s, you can inspect the
+        :attr:`Field.many_to_many <django.db.models.Field.many_to_many>`
+        attribute after calling ``get_field()``.
+
+Retrieving all field instances of a model
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. method:: Options.get_fields(include_parents=True, include_hidden=False)
+
+    .. versionadded:: 1.8
+
+    Returns a tuple of fields associated with a model. ``get_fields()`` accepts
+    two parameters that can be used to control which fields are returned:
+
+    ``include_parents``
+        ``True`` by default. Recursively includes fields defined on parent
+        classes. If set to ``False``, ``get_fields()`` will only search for
+        fields declared directly on the current model. Fields from models that
+        directly inherit from abstract models or proxy classes are considered
+        to be local, not on the parent.
+
+    ``include_hidden``
+        ``False`` by default. If set to ``True``, ``get_fields()`` will include
+        fields that are used to back other field's functionality. This will
+        also include any fields that have a ``related_name`` (such
+        as :class:`~django.db.models.ManyToManyField`, or
+        :class:`~django.db.models.ForeignKey`) that start with a "+".
+
+    .. code-block:: python
+
+        >>> from django.contrib.auth.models import User
+        >>> User._meta.get_fields()
+        (<ManyToOneRel: admin.logentry>,
+         <django.db.models.fields.AutoField: id>,
+         <django.db.models.fields.CharField: password>,
+         <django.db.models.fields.DateTimeField: last_login>,
+         <django.db.models.fields.BooleanField: is_superuser>,
+         <django.db.models.fields.CharField: username>,
+         <django.db.models.fields.CharField: first_name>,
+         <django.db.models.fields.CharField: last_name>,
+         <django.db.models.fields.EmailField: email>,
+         <django.db.models.fields.BooleanField: is_staff>,
+         <django.db.models.fields.BooleanField: is_active>,
+         <django.db.models.fields.DateTimeField: date_joined>,
+         <django.db.models.fields.related.ManyToManyField: groups>,
+         <django.db.models.fields.related.ManyToManyField: user_permissions>)
+
+        # Also include hidden fields.
+        >>> User._meta.get_fields(include_hidden=True)
+        (<ManyToOneRel: auth.user_groups>,
+         <ManyToOneRel: auth.user_user_permissions>,
+         <ManyToOneRel: admin.logentry>,
+         <django.db.models.fields.AutoField: id>,
+         <django.db.models.fields.CharField: password>,
+         <django.db.models.fields.DateTimeField: last_login>,
+         <django.db.models.fields.BooleanField: is_superuser>,
+         <django.db.models.fields.CharField: username>,
+         <django.db.models.fields.CharField: first_name>,
+         <django.db.models.fields.CharField: last_name>,
+         <django.db.models.fields.EmailField: email>,
+         <django.db.models.fields.BooleanField: is_staff>,
+         <django.db.models.fields.BooleanField: is_active>,
+         <django.db.models.fields.DateTimeField: date_joined>,
+         <django.db.models.fields.related.ManyToManyField: groups>,
+         <django.db.models.fields.related.ManyToManyField: user_permissions>)
+
+.. _migrating-old-meta-api:
+
+Migrating from the old API
+==========================
+
+As part of the formalization of the ``Model._meta`` API (from the
+:class:`django.db.models.options.Options` class), a number of methods and
+properties have been deprecated and will be removed in Django 2.0.
+
+These old APIs can be replicated by either:
+
+* invoking :meth:`Options.get_field()
+  <django.db.models.options.Options.get_field()>`, or;
+
+* invoking :meth:`Options.get_fields()
+  <django.db.models.options.Options.get_fields()>` to retrieve a list of all
+  fields, and then filtering this list using the :ref:`field attributes
+  <model-field-attributes>` that describe (or retrieve, in the case of
+  ``_with_model`` variants) the properties of the desired fields.
+
+Although it's possible to make strictly equivalent replacements of the old
+methods, that might not be the best approach. Taking the time to refactor any
+field loops to make better use of the new API - and possibly include fields
+that were previously excluded - will almost certainly result in better code.
+
+Assuming you have a model named ``MyModel``, the following substitutions
+can be made to convert your code to the new API:
+
+* ``MyModel._meta.get_field(name)``::
+
+      f = MyModel._meta.get_field(name)
+
+  then check if:
+
+  - ``f.auto_created == False``, because the new ``get_field()``
+    API will find "reverse" relations), and:
+
+  - ``f.is_relation and f.related_model is None``, because the new
+    ``get_field()`` API will find
+    :class:`~django.contrib.contenttypes.fields.GenericForeignKey` relations;
+
+* ``MyModel._meta.get_field_by_name(name)``:
+
+  ``get_field_by_name()`` returned four values:
+  ``(field, model, direct,  m2m)``:
+
+  - ``field`` can be found by ``MyModel._meta.get_field(name)``
+
+  - ``model`` can be found through the
+    :attr:`~django.db.models.Field.model` attribute on the field.
+
+  - ``direct`` can be found by: ``not field.auto_created or field.concrete``
+
+    The :attr:`~django.db.models.Field.auto_created` check excludes
+    all "forward" and "reverse" relations that are created by Django, but
+    this also includes ``AutoField`` and ``OneToOneField`` on proxy models.
+    We avoid filtering out these attributes using the
+    :attr:`concrete <django.db.models.Field.concrete>` attribute.
+
+  - ``m2m`` can be found through the
+    :attr:`~django.db.models.Field.many_to_many` attribute on the field.
+
+* ``MyModel._meta.get_fields_with_model()``::
+
+      [
+          (f, f.model if f.model != MyModel else None)
+          for f in MyModel._meta.get_fields()
+          if not f.is_relation
+              or f.one_to_one
+              or (f.one_to_many and f.related_model)
+      ]
+
+* ``MyModel._meta.get_concrete_fields_with_model()``::
+
+      [
+          (f, f.model if f.model != MyModel else None)
+          for f in MyModel._meta.get_fields()
+          if f.concrete and (
+              not f.is_relation
+              or f.one_to_one
+              or (f.one_to_many and f.related_model)
+          )
+      ]
+
+* ``MyModel._meta.get_m2m_with_model()``::
+
+      [
+          (f, f.model if f.model != MyModel else None)
+          for f in MyModel._meta.get_fields()
+          if f.many_to_many and not f.auto_created
+      ]
+
+* ``MyModel._meta.get_all_related_objects()``::
+
+      [
+          f for f in MyModel._meta.get_fields()
+          if f.many_to_one and f.auto_created
+      ]
+
+* ``MyModel._meta.get_all_related_objects_with_model()``::
+
+      [
+          (f, f.model if f.model != MyModel else None)
+          for f in MyModel._meta.get_fields()
+          if f.many_to_one and f.auto_created
+      ]
+
+* ``MyModel._meta.get_all_related_many_to_many_objects()``::
+
+      [
+          f for f in MyModel._meta.get_fields(include_hidden=True)
+          if f.many_to_many and f.auto_created
+      ]
+
+* ``MyModel._meta.get_all_related_m2m_objects_with_model()``::
+
+      [
+          (f, f.model if f.model != MyModel else None)
+          for f in MyModel._meta.get_fields(include_hidden=True)
+          if f.many_to_many and f.auto_created
+      ]
+
+* ``MyModel._meta.get_all_field_names()``::
+
+      from itertools import chain
+      list(set(chain.from_iterable(
+          (field.name, field.attname) if hasattr(field, 'attname') else (field.name,)
+          for field in MyModel._meta.get_fields()
+          # For complete backwards compatibility, you may want to exclude
+          # GenericForeignKey from the results.
+          if not (field.one_to_many and field.related_model is None)
+      )))
+
+  This provides a 100% backwards compatible replacement, ensuring that both
+  field names and attribute names ``ForeignKey``\s are included, but fields
+  associated with ``GenericForeignKey``\s are not. A simpler version would be::
+
+      [f.name for f in MyModel._meta.get_fields()]
+
+  While this isn't 100% backwards compatible, it may be sufficient in many
+  situations.

+ 36 - 0
docs/releases/1.8.txt

@@ -29,6 +29,22 @@ Like Django 1.7, Django 1.8 requires Python 2.7 or above, though we
 What's new in Django 1.8
 ========================
 
+``Model._meta`` API
+~~~~~~~~~~~~~~~~~~~
+
+Django now has a formalized API for :doc:`Model._meta </ref/models/meta>`,
+providing an officially supported way to :ref:`retrieve fields
+<model-meta-field-api>` and filter fields based on their :ref:`attributes
+<model-field-attributes>`.
+
+The ``Model._meta`` object has been part of Django since the days of pre-0.96
+"Magic Removal" -- it just wasn't an official, stable API. In recognition of
+this, we've endeavored to maintain backwards-compatibility with the old
+API endpoint where possible. However, API endpoints that aren't part of the
+new official API have been deprecated and will eventually be removed. A
+:ref:`guide to migrating from the old API to the new API
+<migrating-old-meta-api>` has been provided.
+
 Security enhancements
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -998,6 +1014,26 @@ Miscellaneous
 Features deprecated in 1.8
 ==========================
 
+Selected methods in ``django.db.models.options.Options``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+As part of the formalization of the ``Model._meta`` API (from the
+:class:`django.db.models.options.Options` class), a number of methods have been
+deprecated and will be removed in in Django 2.0:
+
+* ``get_all_field_names()``
+* ``get_all_related_objects()``
+* ``get_all_related_objects_with_model()``
+* ``get_all_related_many_to_many_objects()``
+* ``get_all_related_m2m_objects_with_model()``
+* ``get_concrete_fields_with_model()``
+* ``get_field_by_name()``
+* ``get_fields_with_model()``
+* ``get_m2m_with_model()``
+
+A :ref:`migration guide <migrating-old-meta-api>` has been provided to assist
+in converting your code from the old API to the new, official API.
+
 Loading ``cycle`` and ``firstof`` template tags from ``future`` library
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

+ 1 - 1
tests/backends/tests.py

@@ -1020,7 +1020,7 @@ class DBConstraintTestCase(TestCase):
         self.assertEqual(models.Object.objects.count(), 2)
         self.assertEqual(obj.related_objects.count(), 1)
 
-        intermediary_model = models.Object._meta.get_field_by_name("related_objects")[0].rel.through
+        intermediary_model = models.Object._meta.get_field("related_objects").rel.through
         intermediary_model.objects.create(from_object_id=obj.id, to_object_id=12345)
         self.assertEqual(obj.related_objects.count(), 1)
         self.assertEqual(intermediary_model.objects.count(), 2)

+ 1 - 1
tests/basic/tests.py

@@ -776,7 +776,7 @@ class ModelRefreshTests(TestCase):
 
 class TestRelatedObjectDeprecation(TestCase):
     def test_field_related_deprecation(self):
-        field = SelfRef._meta.get_field_by_name('selfref')[0]
+        field = SelfRef._meta.get_field('selfref')
         with warnings.catch_warnings(record=True) as warns:
             warnings.simplefilter('always')
             self.assertIsInstance(field.related, ForeignObjectRel)

+ 2 - 0
tests/fixtures/tests.py

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
 import os
 import warnings
 
+from django.apps import apps
 from django.contrib.sites.models import Site
 from django.core import management
 from django.db import connection, IntegrityError
@@ -76,6 +77,7 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
         ])
 
     def test_loading_and_dumping(self):
+        apps.clear_cache()
         Site.objects.all().delete()
         # Load fixture 1. Single JSON file, with two objects.
         management.call_command('loaddata', 'fixture1.json', verbosity=0)

+ 2 - 1
tests/generic_relations/tests.py

@@ -403,7 +403,8 @@ class GenericRelationsTests(TestCase):
         self.assertEqual(tag.content_object.id, diamond.id)
 
     def test_query_content_type(self):
-        with six.assertRaisesRegex(self, FieldError, "^Cannot resolve keyword 'content_object' into field."):
+        msg = "Field 'content_object' does not generate an automatic reverse relation"
+        with self.assertRaisesMessage(FieldError, msg):
             TaggedItem.objects.get(content_object='')
 
 

+ 1 - 1
tests/generic_relations_regress/tests.py

@@ -260,7 +260,7 @@ class GenericRelationTests(TestCase):
         form = GenericRelationForm({'links': None})
         self.assertTrue(form.is_valid())
         form.save()
-        links = HasLinkThing._meta.get_field_by_name('links')[0]
+        links = HasLinkThing._meta.get_field('links')
         self.assertEqual(links.save_form_data_calls, 1)
 
     def test_ticket_22998(self):

+ 6 - 0
tests/m2m_and_m2o/tests.py

@@ -5,6 +5,12 @@ from .models import Issue, User, UnicodeReferenceModel
 
 
 class RelatedObjectTests(TestCase):
+
+    def test_related_objects_have_name_attribute(self):
+        for field_name in ('test_issue_client', 'test_issue_cc'):
+            obj = User._meta.get_field(field_name)
+            self.assertEqual(field_name, obj.field.related_query_name())
+
     def test_m2m_and_m2o(self):
         r = User.objects.create(username="russell")
         g = User.objects.create(username="gustav")

+ 2 - 2
tests/many_to_one/tests.py

@@ -437,11 +437,11 @@ class ManyToOneTests(TestCase):
         expected_message = "Cannot resolve keyword 'notafield' into field. Choices are: %s"
 
         self.assertRaisesMessage(FieldError,
-                                 expected_message % ', '.join(Reporter._meta.get_all_field_names()),
+                                 expected_message % ', '.join(sorted(f.name for f in Reporter._meta.get_fields())),
                                  Article.objects.values_list,
                                  'reporter__notafield')
         self.assertRaisesMessage(FieldError,
-                                 expected_message % ', '.join(['EXTRA'] + Article._meta.get_all_field_names()),
+                                 expected_message % ', '.join(['EXTRA'] + sorted(f.name for f in Article._meta.get_fields())),
                                  Article.objects.extra(select={'EXTRA': 'EXTRA_SELECT'}).values_list,
                                  'notafield')
 

+ 2 - 2
tests/migrations/test_state.py

@@ -202,8 +202,8 @@ class StateTests(TestCase):
         ))
 
         new_apps = project_state.apps
-        self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("name")[0].max_length, 100)
-        self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field_by_name("hidden")[0].null, False)
+        self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("name").max_length, 100)
+        self.assertEqual(new_apps.get_model("migrations", "Tag")._meta.get_field("hidden").null, False)
 
         self.assertEqual(len(new_apps.get_model("migrations", "SubTag")._meta.local_fields), 2)
 

+ 51 - 0
tests/model_fields/models.py

@@ -8,6 +8,11 @@ except ImportError:
     Image = None
 
 from django.core.files.storage import FileSystemStorage
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.contenttypes.models import ContentType
+from django.db.models.fields.related import (
+    ForeignObject, ForeignKey, ManyToManyField, OneToOneField,
+)
 from django.db import models
 from django.db.models.fields.files import ImageFieldFile, ImageField
 from django.utils import six
@@ -295,6 +300,52 @@ if Image:
                                   height_field='headshot_height',
                                   width_field='headshot_width')
 
+
+class AllFieldsModel(models.Model):
+    big_integer = models.BigIntegerField()
+    binary = models.BinaryField()
+    boolean = models.BooleanField(default=False)
+    char = models.CharField(max_length=10)
+    csv = models.CommaSeparatedIntegerField(max_length=10)
+    date = models.DateField()
+    datetime = models.DateTimeField()
+    decimal = models.DecimalField(decimal_places=2, max_digits=2)
+    duration = models.DurationField()
+    email = models.EmailField()
+    file_path = models.FilePathField()
+    floatf = models.FloatField()
+    integer = models.IntegerField()
+    ip_address = models.IPAddressField()
+    generic_ip = models.GenericIPAddressField()
+    null_boolean = models.NullBooleanField()
+    positive_integer = models.PositiveIntegerField()
+    positive_small_integer = models.PositiveSmallIntegerField()
+    slug = models.SlugField()
+    small_integer = models.SmallIntegerField()
+    text = models.TextField()
+    time = models.TimeField()
+    url = models.URLField()
+    uuid = models.UUIDField()
+
+    fo = ForeignObject(
+        'self',
+        from_fields=['abstract_non_concrete_id'],
+        to_fields=['id'],
+        related_name='reverse'
+    )
+    fk = ForeignKey(
+        'self',
+        related_name='reverse2'
+    )
+    m2m = ManyToManyField('self')
+    oto = OneToOneField('self')
+
+    object_id = models.PositiveIntegerField()
+    content_type = models.ForeignKey(ContentType)
+    gfk = GenericForeignKey()
+    gr = GenericRelation(DataModel)
+
+
 ###############################################################################
 
 

+ 220 - 0
tests/model_fields/test_field_flags.py

@@ -0,0 +1,220 @@
+from django import test
+
+from django.contrib.contenttypes.fields import (
+    GenericForeignKey, GenericRelation,
+)
+from django.db import models
+from django.db.models.fields.related import (
+    ForeignObject, ForeignKey, OneToOneField, ManyToManyField,
+    ManyToOneRel, ForeignObjectRel,
+)
+
+from .models import AllFieldsModel
+
+
+NON_CONCRETE_FIELDS = (
+    ForeignObject,
+    GenericForeignKey,
+    GenericRelation,
+)
+
+NON_EDITABLE_FIELDS = (
+    models.BinaryField,
+    GenericForeignKey,
+    GenericRelation,
+)
+
+RELATION_FIELDS = (
+    ForeignKey,
+    ForeignObject,
+    ManyToManyField,
+    OneToOneField,
+    GenericForeignKey,
+    GenericRelation,
+)
+
+ONE_TO_MANY_CLASSES = {
+    ForeignObject,
+    ForeignKey,
+    GenericForeignKey,
+}
+
+MANY_TO_ONE_CLASSES = {
+    ForeignObjectRel,
+    ManyToOneRel,
+    GenericRelation,
+}
+
+MANY_TO_MANY_CLASSES = {
+    ManyToManyField,
+}
+
+ONE_TO_ONE_CLASSES = {
+    OneToOneField,
+}
+
+FLAG_PROPERTIES = (
+    'concrete',
+    'editable',
+    'is_relation',
+    'model',
+    'hidden',
+    'one_to_many',
+    'many_to_one',
+    'many_to_many',
+    'one_to_one',
+    'related_model',
+)
+
+FLAG_PROPERTIES_FOR_RELATIONS = (
+    'one_to_many',
+    'many_to_one',
+    'many_to_many',
+    'one_to_one',
+)
+
+
+class FieldFlagsTests(test.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        super(FieldFlagsTests, cls).setUpClass()
+        cls.fields = (
+            list(AllFieldsModel._meta.fields) +
+            list(AllFieldsModel._meta.virtual_fields)
+        )
+
+        cls.all_fields = (
+            cls.fields +
+            list(AllFieldsModel._meta.many_to_many) +
+            list(AllFieldsModel._meta.virtual_fields)
+        )
+
+        cls.fields_and_reverse_objects = (
+            cls.all_fields +
+            list(AllFieldsModel._meta.related_objects)
+        )
+
+    def test_each_field_should_have_a_concrete_attribute(self):
+        self.assertTrue(all(f.concrete.__class__ == bool for f in self.fields))
+
+    def test_each_field_should_have_an_editable_attribute(self):
+        self.assertTrue(all(f.editable.__class__ == bool for f in self.all_fields))
+
+    def test_each_field_should_have_a_has_rel_attribute(self):
+        self.assertTrue(all(f.is_relation.__class__ == bool for f in self.all_fields))
+
+    def test_each_object_should_have_auto_created(self):
+        self.assertTrue(
+            all(f.auto_created.__class__ == bool
+            for f in self.fields_and_reverse_objects)
+        )
+
+    def test_non_concrete_fields(self):
+        for field in self.fields:
+            if type(field) in NON_CONCRETE_FIELDS:
+                self.assertFalse(field.concrete)
+            else:
+                self.assertTrue(field.concrete)
+
+    def test_non_editable_fields(self):
+        for field in self.all_fields:
+            if type(field) in NON_EDITABLE_FIELDS:
+                self.assertFalse(field.editable)
+            else:
+                self.assertTrue(field.editable)
+
+    def test_related_fields(self):
+        for field in self.all_fields:
+            if type(field) in RELATION_FIELDS:
+                self.assertTrue(field.is_relation)
+            else:
+                self.assertFalse(field.is_relation)
+
+    def test_field_names_should_always_be_available(self):
+        for field in self.fields_and_reverse_objects:
+            self.assertTrue(field.name)
+
+    def test_all_field_types_should_have_flags(self):
+        for field in self.fields_and_reverse_objects:
+            for flag in FLAG_PROPERTIES:
+                self.assertTrue(hasattr(field, flag), "Field %s does not have flag %s" % (field, flag))
+            if field.is_relation:
+                true_cardinality_flags = sum(
+                    getattr(field, flag) is True
+                    for flag in FLAG_PROPERTIES_FOR_RELATIONS
+                )
+                # If the field has a relation, there should be only one of the
+                # 4 cardinality flags available.
+                self.assertEqual(1, true_cardinality_flags)
+
+    def test_cardinality_m2m(self):
+        m2m_type_fields = (
+            f for f in self.all_fields
+            if f.is_relation and f.many_to_many
+        )
+        # Test classes are what we expect
+        self.assertEqual(MANY_TO_MANY_CLASSES, {f.__class__ for f in m2m_type_fields})
+
+        # Ensure all m2m reverses are m2m
+        for field in m2m_type_fields:
+            reverse_field = field.rel
+            self.assertTrue(reverse_field.is_relation)
+            self.assertTrue(reverse_field.many_to_many)
+            self.assertTrue(reverse_field.related_model)
+
+    def test_cardinality_o2m(self):
+        o2m_type_fields = [
+            f for f in self.fields_and_reverse_objects
+            if f.is_relation and f.one_to_many
+        ]
+        # Test classes are what we expect
+        self.assertEqual(ONE_TO_MANY_CLASSES, {f.__class__ for f in o2m_type_fields})
+
+        # Ensure all o2m reverses are m2o
+        for field in o2m_type_fields:
+            if field.concrete:
+                reverse_field = field.rel
+                self.assertTrue(reverse_field.is_relation and reverse_field.many_to_one)
+
+    def test_cardinality_m2o(self):
+        m2o_type_fields = [
+            f for f in self.fields_and_reverse_objects
+            if f.is_relation and f.many_to_one
+        ]
+        # Test classes are what we expect
+        self.assertEqual(MANY_TO_ONE_CLASSES, {f.__class__ for f in m2o_type_fields})
+
+        # Ensure all m2o reverses are o2m
+        for obj in m2o_type_fields:
+            if hasattr(obj, 'field'):
+                reverse_field = obj.field
+                self.assertTrue(reverse_field.is_relation and reverse_field.one_to_many)
+
+    def test_cardinality_o2o(self):
+        o2o_type_fields = [
+            f for f in self.all_fields
+            if f.is_relation and f.one_to_one
+        ]
+        # Test classes are what we expect
+        self.assertEqual(ONE_TO_ONE_CLASSES, {f.__class__ for f in o2o_type_fields})
+
+        # Ensure all o2o reverses are o2o
+        for obj in o2o_type_fields:
+            if hasattr(obj, 'field'):
+                reverse_field = obj.field
+                self.assertTrue(reverse_field.is_relation and reverse_field.one_to_one)
+
+    def test_hidden_flag(self):
+        incl_hidden = set(AllFieldsModel._meta.get_fields(include_hidden=True))
+        no_hidden = set(AllFieldsModel._meta.get_fields())
+        fields_that_should_be_hidden = (incl_hidden - no_hidden)
+        for f in incl_hidden:
+            self.assertEqual(f in fields_that_should_be_hidden, f.hidden)
+
+    def test_model_and_reverse_model_should_equal_on_relations(self):
+        for field in AllFieldsModel._meta.get_fields():
+            is_concrete_forward_field = field.concrete and field.related_model
+            if is_concrete_forward_field:
+                reverse_field = field.rel
+                self.assertEqual(field.model, reverse_field.related_model)
+                self.assertEqual(field.related_model, reverse_field.model)

+ 1 - 1
tests/model_fields/tests.py

@@ -198,7 +198,7 @@ class ForeignKeyTests(test.TestCase):
         self.assertEqual(warnings, expected_warnings)
 
     def test_related_name_converted_to_text(self):
-        rel_name = Bar._meta.get_field_by_name('a')[0].rel.related_name
+        rel_name = Bar._meta.get_field('a').rel.related_name
         self.assertIsInstance(rel_name, six.text_type)
 
 

+ 796 - 0
tests/model_meta/results.py

@@ -0,0 +1,796 @@
+from .models import (
+    AbstractPerson, BasePerson, Person, Relating, Relation,
+)
+
+TEST_RESULTS = {
+    'get_all_field_names': {
+        Person: [
+            'baseperson_ptr',
+            'baseperson_ptr_id',
+            'content_type_abstract',
+            'content_type_abstract_id',
+            'content_type_base',
+            'content_type_base_id',
+            'content_type_concrete',
+            'content_type_concrete_id',
+            'data_abstract',
+            'data_base',
+            'data_inherited',
+            'data_not_concrete_abstract',
+            'data_not_concrete_base',
+            'data_not_concrete_inherited',
+            'fk_abstract',
+            'fk_abstract_id',
+            'fk_base',
+            'fk_base_id',
+            'fk_inherited',
+            'fk_inherited_id',
+            'followers_abstract',
+            'followers_base',
+            'followers_concrete',
+            'following_abstract',
+            'following_base',
+            'following_inherited',
+            'friends_abstract',
+            'friends_base',
+            'friends_inherited',
+            'generic_relation_abstract',
+            'generic_relation_base',
+            'generic_relation_concrete',
+            'id',
+            'm2m_abstract',
+            'm2m_base',
+            'm2m_inherited',
+            'object_id_abstract',
+            'object_id_base',
+            'object_id_concrete',
+            'relating_basepeople',
+            'relating_baseperson',
+            'relating_people',
+            'relating_person',
+        ],
+        BasePerson: [
+            'content_type_abstract',
+            'content_type_abstract_id',
+            'content_type_base',
+            'content_type_base_id',
+            'data_abstract',
+            'data_base',
+            'data_not_concrete_abstract',
+            'data_not_concrete_base',
+            'fk_abstract',
+            'fk_abstract_id',
+            'fk_base',
+            'fk_base_id',
+            'followers_abstract',
+            'followers_base',
+            'following_abstract',
+            'following_base',
+            'friends_abstract',
+            'friends_base',
+            'generic_relation_abstract',
+            'generic_relation_base',
+            'id',
+            'm2m_abstract',
+            'm2m_base',
+            'object_id_abstract',
+            'object_id_base',
+            'person',
+            'relating_basepeople',
+            'relating_baseperson'
+        ],
+        AbstractPerson: [
+            'content_type_abstract',
+            'content_type_abstract_id',
+            'data_abstract',
+            'data_not_concrete_abstract',
+            'fk_abstract',
+            'fk_abstract_id',
+            'following_abstract',
+            'friends_abstract',
+            'generic_relation_abstract',
+            'm2m_abstract',
+            'object_id_abstract',
+        ],
+        Relating: [
+            'basepeople',
+            'basepeople_hidden',
+            'baseperson',
+            'baseperson_hidden',
+            'baseperson_hidden_id',
+            'baseperson_id',
+            'id',
+            'people',
+            'people_hidden',
+            'person',
+            'person_hidden',
+            'person_hidden_id',
+            'person_id',
+            'proxyperson',
+            'proxyperson_hidden',
+            'proxyperson_hidden_id',
+            'proxyperson_id',
+        ],
+    },
+    'fields': {
+        Person: [
+            'id',
+            'data_abstract',
+            'fk_abstract_id',
+            'data_not_concrete_abstract',
+            'content_type_abstract_id',
+            'object_id_abstract',
+            'data_base',
+            'fk_base_id',
+            'data_not_concrete_base',
+            'content_type_base_id',
+            'object_id_base',
+            'baseperson_ptr_id',
+            'data_inherited',
+            'fk_inherited_id',
+            'data_not_concrete_inherited',
+            'content_type_concrete_id',
+            'object_id_concrete',
+        ],
+        BasePerson: [
+            'id',
+            'data_abstract',
+            'fk_abstract_id',
+            'data_not_concrete_abstract',
+            'content_type_abstract_id',
+            'object_id_abstract',
+            'data_base',
+            'fk_base_id',
+            'data_not_concrete_base',
+            'content_type_base_id',
+            'object_id_base',
+        ],
+        AbstractPerson: [
+            'data_abstract',
+            'fk_abstract_id',
+            'data_not_concrete_abstract',
+            'content_type_abstract_id',
+            'object_id_abstract',
+        ],
+        Relating: [
+            'id',
+            'baseperson_id',
+            'baseperson_hidden_id',
+            'person_id',
+            'person_hidden_id',
+            'proxyperson_id',
+            'proxyperson_hidden_id',
+        ],
+    },
+    'local_fields': {
+        Person: [
+            'baseperson_ptr_id',
+            'data_inherited',
+            'fk_inherited_id',
+            'data_not_concrete_inherited',
+            'content_type_concrete_id',
+            'object_id_concrete',
+        ],
+        BasePerson: [
+            'id',
+            'data_abstract',
+            'fk_abstract_id',
+            'data_not_concrete_abstract',
+            'content_type_abstract_id',
+            'object_id_abstract',
+            'data_base',
+            'fk_base_id',
+            'data_not_concrete_base',
+            'content_type_base_id',
+            'object_id_base',
+        ],
+        AbstractPerson: [
+            'data_abstract',
+            'fk_abstract_id',
+            'data_not_concrete_abstract',
+            'content_type_abstract_id',
+            'object_id_abstract',
+        ],
+        Relating: [
+            'id',
+            'baseperson_id',
+            'baseperson_hidden_id',
+            'person_id',
+            'person_hidden_id',
+            'proxyperson_id',
+            'proxyperson_hidden_id',
+        ],
+    },
+    'local_concrete_fields': {
+        Person: [
+            'baseperson_ptr_id',
+            'data_inherited',
+            'fk_inherited_id',
+            'content_type_concrete_id',
+            'object_id_concrete',
+        ],
+        BasePerson: [
+            'id',
+            'data_abstract',
+            'fk_abstract_id',
+            'content_type_abstract_id',
+            'object_id_abstract',
+            'data_base',
+            'fk_base_id',
+            'content_type_base_id',
+            'object_id_base',
+        ],
+        AbstractPerson: [
+            'data_abstract',
+            'fk_abstract_id',
+            'content_type_abstract_id',
+            'object_id_abstract',
+        ],
+        Relating: [
+            'id',
+            'baseperson_id',
+            'baseperson_hidden_id',
+            'person_id',
+            'person_hidden_id',
+            'proxyperson_id',
+            'proxyperson_hidden_id',
+        ],
+    },
+    'many_to_many': {
+        Person: [
+            'm2m_abstract',
+            'friends_abstract',
+            'following_abstract',
+            'm2m_base',
+            'friends_base',
+            'following_base',
+            'm2m_inherited',
+            'friends_inherited',
+            'following_inherited',
+        ],
+        BasePerson: [
+            'm2m_abstract',
+            'friends_abstract',
+            'following_abstract',
+            'm2m_base',
+            'friends_base',
+            'following_base',
+        ],
+        AbstractPerson: [
+            'm2m_abstract',
+            'friends_abstract',
+            'following_abstract',
+        ],
+        Relating: [
+            'basepeople',
+            'basepeople_hidden',
+            'people',
+            'people_hidden',
+        ],
+    },
+    'many_to_many_with_model': {
+        Person: [
+            BasePerson,
+            BasePerson,
+            BasePerson,
+            BasePerson,
+            BasePerson,
+            BasePerson,
+            None,
+            None,
+            None,
+        ],
+        BasePerson: [
+            None,
+            None,
+            None,
+            None,
+            None,
+            None,
+        ],
+        AbstractPerson: [
+            None,
+            None,
+            None,
+        ],
+        Relating: [
+            None,
+            None,
+            None,
+            None,
+        ],
+    },
+    'get_all_related_objects_with_model_legacy': {
+        Person: (
+            ('relating_baseperson', BasePerson),
+            ('relating_person', None),
+        ),
+        BasePerson: (
+            ('person', None),
+            ('relating_baseperson', None),
+        ),
+        Relation: (
+            ('fk_abstract_rel', None),
+            ('fo_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fo_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_hidden_local': {
+        Person: (
+            ('+', None),
+            ('+', None),
+            ('Person_following_inherited+', None),
+            ('Person_following_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_m2m_inherited+', None),
+            ('Relating_people+', None),
+            ('Relating_people_hidden+', None),
+            ('followers_concrete', None),
+            ('friends_inherited_rel_+', None),
+            ('relating_people', None),
+            ('relating_person', None),
+        ),
+        BasePerson: (
+            ('+', None),
+            ('+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Relating_basepeople+', None),
+            ('Relating_basepeople_hidden+', None),
+            ('followers_abstract', None),
+            ('followers_base', None),
+            ('friends_abstract_rel_+', None),
+            ('friends_base_rel_+', None),
+            ('person', None),
+            ('relating_basepeople', None),
+            ('relating_baseperson', None),
+        ),
+        Relation: (
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Person_m2m_inherited+', None),
+            ('fk_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_abstract_rel', None),
+            ('fo_base_rel', None),
+            ('fo_concrete_rel', None),
+            ('m2m_abstract_rel', None),
+            ('m2m_base_rel', None),
+            ('m2m_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_hidden': {
+        Person: (
+            ('+', BasePerson),
+            ('+', BasePerson),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_m2m_abstract+', BasePerson),
+            ('BasePerson_m2m_base+', BasePerson),
+            ('Person_following_inherited+', None),
+            ('Person_following_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_m2m_inherited+', None),
+            ('Relating_basepeople+', BasePerson),
+            ('Relating_basepeople_hidden+', BasePerson),
+            ('Relating_people+', None),
+            ('Relating_people_hidden+', None),
+            ('followers_abstract', BasePerson),
+            ('followers_base', BasePerson),
+            ('followers_concrete', None),
+            ('friends_abstract_rel_+', BasePerson),
+            ('friends_base_rel_+', BasePerson),
+            ('friends_inherited_rel_+', None),
+            ('relating_basepeople', BasePerson),
+            ('relating_baseperson', BasePerson),
+            ('relating_people', None),
+            ('relating_person', None),
+        ),
+        BasePerson: (
+            ('+', None),
+            ('+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Relating_basepeople+', None),
+            ('Relating_basepeople_hidden+', None),
+            ('followers_abstract', None),
+            ('followers_base', None),
+            ('friends_abstract_rel_+', None),
+            ('friends_base_rel_+', None),
+            ('person', None),
+            ('relating_basepeople', None),
+            ('relating_baseperson', None),
+        ),
+        Relation: (
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Person_m2m_inherited+', None),
+            ('fk_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_abstract_rel', None),
+            ('fo_base_rel', None),
+            ('fo_concrete_rel', None),
+            ('m2m_abstract_rel', None),
+            ('m2m_base_rel', None),
+            ('m2m_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_local': {
+        Person: (
+            ('followers_concrete', None),
+            ('relating_person', None),
+            ('relating_people', None),
+        ),
+        BasePerson: (
+            ('followers_abstract', None),
+            ('followers_base', None),
+            ('person', None),
+            ('relating_baseperson', None),
+            ('relating_basepeople', None),
+        ),
+        Relation: (
+            ('fk_abstract_rel', None),
+            ('fo_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fo_base_rel', None),
+            ('m2m_abstract_rel', None),
+            ('m2m_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_concrete_rel', None),
+            ('m2m_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model': {
+        Person: (
+            ('followers_abstract', BasePerson),
+            ('followers_base', BasePerson),
+            ('relating_baseperson', BasePerson),
+            ('relating_basepeople', BasePerson),
+            ('followers_concrete', None),
+            ('relating_person', None),
+            ('relating_people', None),
+        ),
+        BasePerson: (
+            ('followers_abstract', None),
+            ('followers_base', None),
+            ('person', None),
+            ('relating_baseperson', None),
+            ('relating_basepeople', None),
+        ),
+        Relation: (
+            ('fk_abstract_rel', None),
+            ('fo_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fo_base_rel', None),
+            ('m2m_abstract_rel', None),
+            ('m2m_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_concrete_rel', None),
+            ('m2m_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_local_legacy': {
+        Person: (
+            ('relating_person', None),
+        ),
+        BasePerson: (
+            ('person', None),
+            ('relating_baseperson', None)
+        ),
+        Relation: (
+            ('fk_abstract_rel', None),
+            ('fo_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fo_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_hidden_legacy': {
+        BasePerson: (
+            ('+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Relating_basepeople+', None),
+            ('Relating_basepeople_hidden+', None),
+            ('person', None),
+            ('relating_baseperson', None),
+        ),
+        Person: (
+            ('+', BasePerson),
+            ('+', None),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_m2m_abstract+', BasePerson),
+            ('BasePerson_m2m_base+', BasePerson),
+            ('Person_following_inherited+', None),
+            ('Person_following_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_m2m_inherited+', None),
+            ('Relating_basepeople+', BasePerson),
+            ('Relating_basepeople_hidden+', BasePerson),
+            ('Relating_people+', None),
+            ('Relating_people_hidden+', None),
+            ('relating_baseperson', BasePerson),
+            ('relating_person', None),
+        ),
+        Relation: (
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Person_m2m_inherited+', None),
+            ('fk_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_abstract_rel', None),
+            ('fo_base_rel', None),
+            ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_hidden_local_legacy': {
+        BasePerson: (
+            ('+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Relating_basepeople+', None),
+            ('Relating_basepeople_hidden+', None),
+            ('person', None),
+            ('relating_baseperson', None),
+        ),
+        Person: (
+            ('+', None),
+            ('Person_following_inherited+', None),
+            ('Person_following_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_m2m_inherited+', None),
+            ('Relating_people+', None),
+            ('Relating_people_hidden+', None),
+            ('relating_person', None),
+        ),
+        Relation: (
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Person_m2m_inherited+', None),
+            ('fk_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_abstract_rel', None),
+            ('fo_base_rel', None),
+            ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_proxy_legacy': {
+        BasePerson: (
+            ('person', None),
+            ('relating_baseperson', None),
+        ),
+        Person: (
+            ('relating_baseperson', BasePerson),
+            ('relating_person', None), ('relating_proxyperson', None),
+        ),
+        Relation: (
+            ('fk_abstract_rel', None), ('fo_abstract_rel', None),
+            ('fk_base_rel', None), ('fo_base_rel', None),
+            ('fk_concrete_rel', None), ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_objects_with_model_proxy_hidden_legacy': {
+        BasePerson: (
+            ('+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_abstract+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_following_base+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_abstract+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_friends_base+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Relating_basepeople+', None),
+            ('Relating_basepeople_hidden+', None),
+            ('person', None),
+            ('relating_baseperson', None),
+        ),
+        Person: (
+            ('+', BasePerson),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_abstract+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_following_base+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_abstract+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_friends_base+', BasePerson),
+            ('BasePerson_m2m_abstract+', BasePerson),
+            ('BasePerson_m2m_base+', BasePerson),
+            ('Person_following_inherited+', None),
+            ('Person_following_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_friends_inherited+', None),
+            ('Person_m2m_inherited+', None),
+            ('Relating_basepeople+', BasePerson),
+            ('Relating_basepeople_hidden+', BasePerson),
+            ('Relating_people+', None),
+            ('Relating_people_hidden+', None),
+            ('relating_baseperson', BasePerson),
+            ('relating_person', None),
+            ('relating_proxyperson', None),
+        ),
+        Relation: (
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('+', None),
+            ('BasePerson_m2m_abstract+', None),
+            ('BasePerson_m2m_base+', None),
+            ('Person_m2m_inherited+', None),
+            ('fk_abstract_rel', None),
+            ('fk_base_rel', None),
+            ('fk_concrete_rel', None),
+            ('fo_abstract_rel', None),
+            ('fo_base_rel', None),
+            ('fo_concrete_rel', None),
+        ),
+    },
+    'get_all_related_many_to_many_with_model_legacy': {
+        BasePerson: (
+            ('friends_abstract_rel_+', None),
+            ('followers_abstract', None),
+            ('friends_base_rel_+', None),
+            ('followers_base', None),
+            ('relating_basepeople', None),
+            ('+', None),
+        ),
+        Person: (
+            ('friends_abstract_rel_+', BasePerson),
+            ('followers_abstract', BasePerson),
+            ('friends_base_rel_+', BasePerson),
+            ('followers_base', BasePerson),
+            ('relating_basepeople', BasePerson),
+            ('+', BasePerson),
+            ('friends_inherited_rel_+', None),
+            ('followers_concrete', None),
+            ('relating_people', None),
+            ('+', None),
+        ),
+        Relation: (
+            ('m2m_abstract_rel', None),
+            ('m2m_base_rel', None),
+            ('m2m_concrete_rel', None),
+        ),
+    },
+    'get_all_related_many_to_many_local_legacy': {
+        BasePerson: [
+            'friends_abstract_rel_+',
+            'followers_abstract',
+            'friends_base_rel_+',
+            'followers_base',
+            'relating_basepeople',
+            '+',
+        ],
+        Person: [
+            'friends_inherited_rel_+',
+            'followers_concrete',
+            'relating_people',
+            '+',
+        ],
+        Relation: [
+            'm2m_abstract_rel',
+            'm2m_base_rel',
+            'm2m_concrete_rel',
+        ],
+    },
+    'virtual_fields': {
+        AbstractPerson: [
+            'generic_relation_abstract',
+            'content_object_abstract',
+        ],
+        BasePerson: [
+            'generic_relation_base',
+            'content_object_base',
+            'generic_relation_abstract',
+            'content_object_abstract',
+        ],
+        Person: [
+            'content_object_concrete',
+            'generic_relation_concrete',
+            'generic_relation_base',
+            'content_object_base',
+            'generic_relation_abstract',
+            'content_object_abstract',
+        ],
+    },
+}

+ 0 - 661
tests/model_meta/test.py

@@ -1,661 +0,0 @@
-from django import test
-from django.contrib.contenttypes.fields import GenericRelation
-from django.core.exceptions import FieldDoesNotExist
-from django.db.models.fields import related, CharField, Field
-
-from .models import (
-    AbstractPerson, BasePerson, Person, Relating, Relation
-)
-
-TEST_RESULTS = {
-    'fields': {
-        Person: [
-            'id',
-            'data_abstract',
-            'fk_abstract_id',
-            'data_not_concrete_abstract',
-            'content_type_abstract_id',
-            'object_id_abstract',
-            'data_base',
-            'fk_base_id',
-            'data_not_concrete_base',
-            'content_type_base_id',
-            'object_id_base',
-            'baseperson_ptr_id',
-            'data_inherited',
-            'fk_inherited_id',
-            'data_not_concrete_inherited',
-            'content_type_concrete_id',
-            'object_id_concrete',
-        ],
-        BasePerson: [
-            'id',
-            'data_abstract',
-            'fk_abstract_id',
-            'data_not_concrete_abstract',
-            'content_type_abstract_id',
-            'object_id_abstract',
-            'data_base',
-            'fk_base_id',
-            'data_not_concrete_base',
-            'content_type_base_id',
-            'object_id_base',
-        ],
-        AbstractPerson: [
-            'data_abstract',
-            'fk_abstract_id',
-            'data_not_concrete_abstract',
-            'content_type_abstract_id',
-            'object_id_abstract',
-        ],
-        Relating: [
-            'id',
-            'baseperson_id',
-            'baseperson_hidden_id',
-            'person_id',
-            'person_hidden_id',
-            'proxyperson_id',
-            'proxyperson_hidden_id',
-        ],
-    },
-    'local_fields': {
-        Person: [
-            'baseperson_ptr_id',
-            'data_inherited',
-            'fk_inherited_id',
-            'data_not_concrete_inherited',
-            'content_type_concrete_id',
-            'object_id_concrete',
-        ],
-        BasePerson: [
-            'id',
-            'data_abstract',
-            'fk_abstract_id',
-            'data_not_concrete_abstract',
-            'content_type_abstract_id',
-            'object_id_abstract',
-            'data_base',
-            'fk_base_id',
-            'data_not_concrete_base',
-            'content_type_base_id',
-            'object_id_base',
-        ],
-        AbstractPerson: [
-            'data_abstract',
-            'fk_abstract_id',
-            'data_not_concrete_abstract',
-            'content_type_abstract_id',
-            'object_id_abstract',
-        ],
-        Relating: [
-            'id',
-            'baseperson_id',
-            'baseperson_hidden_id',
-            'person_id',
-            'person_hidden_id',
-            'proxyperson_id',
-            'proxyperson_hidden_id',
-        ],
-    },
-    'local_concrete_fields': {
-        Person: [
-            'baseperson_ptr_id',
-            'data_inherited',
-            'fk_inherited_id',
-            'content_type_concrete_id',
-            'object_id_concrete',
-        ],
-        BasePerson: [
-            'id',
-            'data_abstract',
-            'fk_abstract_id',
-            'content_type_abstract_id',
-            'object_id_abstract',
-            'data_base',
-            'fk_base_id',
-            'content_type_base_id',
-            'object_id_base',
-        ],
-        AbstractPerson: [
-            'data_abstract',
-            'fk_abstract_id',
-            'content_type_abstract_id',
-            'object_id_abstract',
-        ],
-        Relating: [
-            'id',
-            'baseperson_id',
-            'baseperson_hidden_id',
-            'person_id',
-            'person_hidden_id',
-            'proxyperson_id',
-            'proxyperson_hidden_id',
-        ],
-    },
-    'many_to_many': {
-        Person: [
-            'm2m_abstract',
-            'friends_abstract',
-            'following_abstract',
-            'm2m_base',
-            'friends_base',
-            'following_base',
-            'm2m_inherited',
-            'friends_inherited',
-            'following_inherited',
-        ],
-        BasePerson: [
-            'm2m_abstract',
-            'friends_abstract',
-            'following_abstract',
-            'm2m_base',
-            'friends_base',
-            'following_base',
-        ],
-        AbstractPerson: [
-            'm2m_abstract',
-            'friends_abstract',
-            'following_abstract',
-        ],
-        Relating: [
-            'basepeople',
-            'basepeople_hidden',
-            'people',
-            'people_hidden',
-        ],
-    },
-    'many_to_many_with_model': {
-        Person: [
-            BasePerson,
-            BasePerson,
-            BasePerson,
-            BasePerson,
-            BasePerson,
-            BasePerson,
-            None,
-            None,
-            None,
-        ],
-        BasePerson: [
-            None,
-            None,
-            None,
-            None,
-            None,
-            None,
-        ],
-        AbstractPerson: [
-            None,
-            None,
-            None,
-        ],
-        Relating: [
-            None,
-            None,
-            None,
-            None,
-        ],
-    },
-    'get_all_related_objects_with_model': {
-        Person: (
-            ('relating_baseperson', BasePerson),
-            ('relating_person', None),
-        ),
-        BasePerson: (
-            ('person', None),
-            ('relating_baseperson', None),
-        ),
-        Relation: (
-            ('fk_abstract_rel', None),
-            ('fo_abstract_rel', None),
-            ('fk_base_rel', None),
-            ('fo_base_rel', None),
-            ('fk_concrete_rel', None),
-            ('fo_concrete_rel', None),
-        ),
-    },
-    'get_all_related_objects_with_model_local': {
-        Person: (
-            ('relating_person', None),
-        ),
-        BasePerson: (
-            ('person', None),
-            ('relating_baseperson', None)
-        ),
-        Relation: (
-            ('fk_abstract_rel', None),
-            ('fo_abstract_rel', None),
-            ('fk_base_rel', None),
-            ('fo_base_rel', None),
-            ('fk_concrete_rel', None),
-            ('fo_concrete_rel', None),
-        ),
-    },
-    'get_all_related_objects_with_model_hidden': {
-        BasePerson: (
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.person', None),
-            ('model_meta.relating_basepeople', None),
-            ('model_meta.relating_basepeople_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Person: (
-            ('model_meta.baseperson_friends_base', BasePerson),
-            ('model_meta.baseperson_friends_base', BasePerson),
-            ('model_meta.baseperson_m2m_base', BasePerson),
-            ('model_meta.baseperson_following_base', BasePerson),
-            ('model_meta.baseperson_following_base', BasePerson),
-            ('model_meta.baseperson_m2m_abstract', BasePerson),
-            ('model_meta.baseperson_friends_abstract', BasePerson),
-            ('model_meta.baseperson_friends_abstract', BasePerson),
-            ('model_meta.baseperson_following_abstract', BasePerson),
-            ('model_meta.baseperson_following_abstract', BasePerson),
-            ('model_meta.relating_basepeople', BasePerson),
-            ('model_meta.relating_basepeople_hidden', BasePerson),
-            ('model_meta.relating', BasePerson),
-            ('model_meta.relating', BasePerson),
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.relating_people', None),
-            ('model_meta.relating_people_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Relation: (
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-        ),
-    },
-    'get_all_related_objects_with_model_hidden_local': {
-        BasePerson: (
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.person', None),
-            ('model_meta.relating_basepeople', None),
-            ('model_meta.relating_basepeople_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Person: (
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.relating_people', None),
-            ('model_meta.relating_people_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Relation: (
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-        ),
-    },
-    'get_all_related_objects_with_model_proxy': {
-        BasePerson: (
-            ('person', None),
-            ('relating_baseperson', None),
-        ),
-        Person: (
-            ('relating_baseperson', BasePerson),
-            ('relating_person', None), ('relating_proxyperson', None),
-        ),
-        Relation: (
-            ('fk_abstract_rel', None), ('fo_abstract_rel', None),
-            ('fk_base_rel', None), ('fo_base_rel', None),
-            ('fk_concrete_rel', None), ('fo_concrete_rel', None),
-        ),
-    },
-    'get_all_related_objects_with_model_proxy_hidden': {
-        BasePerson: (
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_friends_base', None),
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_following_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_friends_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.baseperson_following_abstract', None),
-            ('model_meta.person', None),
-            ('model_meta.relating_basepeople', None),
-            ('model_meta.relating_basepeople_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Person: (
-            ('model_meta.baseperson_friends_base', BasePerson),
-            ('model_meta.baseperson_friends_base', BasePerson),
-            ('model_meta.baseperson_m2m_base', BasePerson),
-            ('model_meta.baseperson_following_base', BasePerson),
-            ('model_meta.baseperson_following_base', BasePerson),
-            ('model_meta.baseperson_m2m_abstract', BasePerson),
-            ('model_meta.baseperson_friends_abstract', BasePerson),
-            ('model_meta.baseperson_friends_abstract', BasePerson),
-            ('model_meta.baseperson_following_abstract', BasePerson),
-            ('model_meta.baseperson_following_abstract', BasePerson),
-            ('model_meta.relating_basepeople', BasePerson),
-            ('model_meta.relating_basepeople_hidden', BasePerson),
-            ('model_meta.relating', BasePerson),
-            ('model_meta.relating', BasePerson),
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_friends_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.person_following_inherited', None),
-            ('model_meta.relating_people', None),
-            ('model_meta.relating_people_hidden', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-            ('model_meta.relating', None),
-        ),
-        Relation: (
-            ('model_meta.baseperson_m2m_base', None),
-            ('model_meta.baseperson_m2m_abstract', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.baseperson', None),
-            ('model_meta.person_m2m_inherited', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.person', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-            ('model_meta.proxyperson', None),
-        ),
-    },
-    'get_all_related_many_to_many_with_model': {
-        BasePerson: (
-            ('friends_abstract_rel_+', None),
-            ('followers_abstract', None),
-            ('friends_base_rel_+', None),
-            ('followers_base', None),
-            ('relating_basepeople', None),
-            ('+', None),
-        ),
-        Person: (
-            ('friends_abstract_rel_+', BasePerson),
-            ('followers_abstract', BasePerson),
-            ('friends_base_rel_+', BasePerson),
-            ('followers_base', BasePerson),
-            ('relating_basepeople', BasePerson),
-            ('+', BasePerson),
-            ('friends_inherited_rel_+', None),
-            ('followers_concrete', None),
-            ('relating_people', None),
-            ('+', None),
-        ),
-        Relation: (
-            ('m2m_abstract_rel', None),
-            ('m2m_base_rel', None),
-            ('m2m_concrete_rel', None),
-        ),
-    },
-    'get_all_related_many_to_many_local': {
-        BasePerson: [
-            'friends_abstract_rel_+',
-            'followers_abstract',
-            'friends_base_rel_+',
-            'followers_base',
-            'relating_basepeople',
-            '+',
-        ],
-        Person: [
-            'friends_inherited_rel_+',
-            'followers_concrete',
-            'relating_people',
-            '+',
-        ],
-        Relation: [
-            'm2m_abstract_rel',
-            'm2m_base_rel',
-            'm2m_concrete_rel',
-        ],
-    },
-    'virtual_fields': {
-        AbstractPerson: [
-            'generic_relation_abstract',
-            'content_object_abstract',
-        ],
-        BasePerson: [
-            'generic_relation_base',
-            'content_object_base',
-            'generic_relation_abstract',
-            'content_object_abstract',
-        ],
-        Person: [
-            'content_object_concrete',
-            'generic_relation_concrete',
-            'generic_relation_base',
-            'content_object_base',
-            'generic_relation_abstract',
-            'content_object_abstract',
-        ],
-    },
-}
-
-
-class OptionsBaseTests(test.TestCase):
-
-    def _map_rq_names(self, res):
-        return tuple((o.field.related_query_name(), m) for o, m in res)
-
-    def _map_names(self, res):
-        return tuple((f.name, m) for f, m in res)
-
-
-class DataTests(OptionsBaseTests):
-
-    def test_fields(self):
-        for model, expected_result in TEST_RESULTS['fields'].items():
-            fields = model._meta.fields
-            self.assertEqual([f.attname for f in fields], expected_result)
-
-    def test_local_fields(self):
-        is_data_field = lambda f: isinstance(f, Field) and not isinstance(f, related.ManyToManyField)
-
-        for model, expected_result in TEST_RESULTS['local_fields'].items():
-            fields = model._meta.local_fields
-            self.assertEqual([f.attname for f in fields], expected_result)
-            self.assertTrue(all([f.model is model for f in fields]))
-            self.assertTrue(all([is_data_field(f) for f in fields]))
-
-    def test_local_concrete_fields(self):
-        for model, expected_result in TEST_RESULTS['local_concrete_fields'].items():
-            fields = model._meta.local_concrete_fields
-            self.assertEqual([f.attname for f in fields], expected_result)
-            self.assertTrue(all([f.column is not None for f in fields]))
-
-
-class M2MTests(OptionsBaseTests):
-
-    def test_many_to_many(self):
-        for model, expected_result in TEST_RESULTS['many_to_many'].items():
-            fields = model._meta.many_to_many
-            self.assertEqual([f.attname for f in fields], expected_result)
-            self.assertTrue(all([isinstance(f.rel, related.ManyToManyRel) for f in fields]))
-
-    def test_many_to_many_with_model(self):
-        for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
-            models = [model for field, model in model._meta.get_m2m_with_model()]
-            self.assertEqual(models, expected_result)
-
-
-class RelatedObjectsTests(OptionsBaseTests):
-    def setUp(self):
-        self.key_name = lambda r: r[0]
-
-    def test_related_objects(self):
-        result_key = 'get_all_related_objects_with_model'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model()
-            self.assertEqual(self._map_rq_names(objects), expected)
-
-    def test_related_objects_local(self):
-        result_key = 'get_all_related_objects_with_model_local'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model(local_only=True)
-            self.assertEqual(self._map_rq_names(objects), expected)
-
-    def test_related_objects_include_hidden(self):
-        result_key = 'get_all_related_objects_with_model_hidden'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model(include_hidden=True)
-            self.assertEqual(
-                sorted(self._map_names(objects), key=self.key_name),
-                sorted(expected, key=self.key_name)
-            )
-
-    def test_related_objects_include_hidden_local_only(self):
-        result_key = 'get_all_related_objects_with_model_hidden_local'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model(
-                include_hidden=True, local_only=True)
-            self.assertEqual(
-                sorted(self._map_names(objects), key=self.key_name),
-                sorted(expected, key=self.key_name)
-            )
-
-    def test_related_objects_proxy(self):
-        result_key = 'get_all_related_objects_with_model_proxy'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model(
-                include_proxy_eq=True)
-            self.assertEqual(self._map_rq_names(objects), expected)
-
-    def test_related_objects_proxy_hidden(self):
-        result_key = 'get_all_related_objects_with_model_proxy_hidden'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_objects_with_model(
-                include_proxy_eq=True, include_hidden=True)
-            self.assertEqual(
-                sorted(self._map_names(objects), key=self.key_name),
-                sorted(expected, key=self.key_name)
-            )
-
-
-class RelatedM2MTests(OptionsBaseTests):
-
-    def test_related_m2m_with_model(self):
-        result_key = 'get_all_related_many_to_many_with_model'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_m2m_objects_with_model()
-            self.assertEqual(self._map_rq_names(objects), expected)
-
-    def test_related_m2m_local_only(self):
-        result_key = 'get_all_related_many_to_many_local'
-        for model, expected in TEST_RESULTS[result_key].items():
-            objects = model._meta.get_all_related_many_to_many_objects(local_only=True)
-            self.assertEqual([o.field.related_query_name() for o in objects], expected)
-
-    def test_related_m2m_asymmetrical(self):
-        m2m = Person._meta.many_to_many
-        self.assertIn('following_base', [f.attname for f in m2m])
-        related_m2m = Person._meta.get_all_related_many_to_many_objects()
-        self.assertIn('followers_base', [o.field.related_query_name() for o in related_m2m])
-
-    def test_related_m2m_symmetrical(self):
-        m2m = Person._meta.many_to_many
-        self.assertIn('friends_base', [f.attname for f in m2m])
-        related_m2m = Person._meta.get_all_related_many_to_many_objects()
-        self.assertIn('friends_inherited_rel_+', [o.field.related_query_name() for o in related_m2m])
-
-
-class VirtualFieldsTests(OptionsBaseTests):
-
-    def test_virtual_fields(self):
-        for model, expected_names in TEST_RESULTS['virtual_fields'].items():
-            objects = model._meta.virtual_fields
-            self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names))
-
-
-class GetFieldByNameTests(OptionsBaseTests):
-
-    def test_get_data_field(self):
-        field_info = Person._meta.get_field_by_name('data_abstract')
-        self.assertEqual(field_info[1:], (BasePerson, True, False))
-        self.assertIsInstance(field_info[0], CharField)
-
-    def test_get_m2m_field(self):
-        field_info = Person._meta.get_field_by_name('m2m_base')
-        self.assertEqual(field_info[1:], (BasePerson, True, True))
-        self.assertIsInstance(field_info[0], related.ManyToManyField)
-
-    def test_get_related_object(self):
-        field_info = Person._meta.get_field_by_name('relating_baseperson')
-        self.assertEqual(field_info[1:], (BasePerson, False, False))
-        self.assertIsInstance(field_info[0], related.ForeignObjectRel)
-
-    def test_get_related_m2m(self):
-        field_info = Person._meta.get_field_by_name('relating_people')
-        self.assertEqual(field_info[1:], (None, False, True))
-        self.assertIsInstance(field_info[0], related.ForeignObjectRel)
-
-    def test_get_generic_foreign_key(self):
-        # For historic reasons generic foreign keys aren't available.
-        with self.assertRaises(FieldDoesNotExist):
-            Person._meta.get_field_by_name('content_object_base')
-
-    def test_get_generic_relation(self):
-        field_info = Person._meta.get_field_by_name('generic_relation_base')
-        self.assertEqual(field_info[1:], (None, True, False))
-        self.assertIsInstance(field_info[0], GenericRelation)

+ 166 - 0
tests/model_meta/test_legacy.py

@@ -0,0 +1,166 @@
+import warnings
+
+from django import test
+from django.contrib.contenttypes.fields import GenericRelation
+from django.core.exceptions import FieldDoesNotExist
+from django.db.models.fields import related, CharField
+from django.utils.deprecation import RemovedInDjango20Warning
+
+from .models import BasePerson, Person
+from .results import TEST_RESULTS
+
+
+class OptionsBaseTests(test.TestCase):
+
+    def _map_related_query_names(self, res):
+        return tuple((o.field.related_query_name(), m) for o, m in res)
+
+    def _map_names(self, res):
+        return tuple((f.name, m) for f, m in res)
+
+
+class M2MTests(OptionsBaseTests):
+
+    def test_many_to_many_with_model(self):
+        for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
+            with warnings.catch_warnings(record=True) as warning:
+                warnings.simplefilter("always")
+                models = [model for field, model in model._meta.get_m2m_with_model()]
+                self.assertEqual([RemovedInDjango20Warning], [w.message.__class__ for w in warning])
+            self.assertEqual(models, expected_result)
+
+
+@test.ignore_warnings(category=RemovedInDjango20Warning)
+class RelatedObjectsTests(OptionsBaseTests):
+    key_name = lambda self, r: r[0]
+
+    def test_related_objects(self):
+        result_key = 'get_all_related_objects_with_model_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model()
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_objects_local(self):
+        result_key = 'get_all_related_objects_with_model_local_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model(local_only=True)
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_objects_include_hidden(self):
+        result_key = 'get_all_related_objects_with_model_hidden_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model(include_hidden=True)
+            self.assertEqual(
+                sorted(self._map_names(objects), key=self.key_name),
+                sorted(expected, key=self.key_name)
+            )
+
+    def test_related_objects_include_hidden_local_only(self):
+        result_key = 'get_all_related_objects_with_model_hidden_local_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model(
+                include_hidden=True, local_only=True)
+            self.assertEqual(
+                sorted(self._map_names(objects), key=self.key_name),
+                sorted(expected, key=self.key_name)
+            )
+
+    def test_related_objects_proxy(self):
+        result_key = 'get_all_related_objects_with_model_proxy_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model(
+                include_proxy_eq=True)
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_objects_proxy_hidden(self):
+        result_key = 'get_all_related_objects_with_model_proxy_hidden_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_objects_with_model(
+                include_proxy_eq=True, include_hidden=True)
+            self.assertEqual(
+                sorted(self._map_names(objects), key=self.key_name),
+                sorted(expected, key=self.key_name)
+            )
+
+
+@test.ignore_warnings(category=RemovedInDjango20Warning)
+class RelatedM2MTests(OptionsBaseTests):
+
+    def test_related_m2m_with_model(self):
+        result_key = 'get_all_related_many_to_many_with_model_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_m2m_objects_with_model()
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_m2m_local_only(self):
+        result_key = 'get_all_related_many_to_many_local_legacy'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = model._meta.get_all_related_many_to_many_objects(local_only=True)
+            self.assertEqual([o.field.related_query_name() for o in objects], expected)
+
+    def test_related_m2m_asymmetrical(self):
+        m2m = Person._meta.many_to_many
+        self.assertTrue('following_base' in [f.attname for f in m2m])
+        related_m2m = Person._meta.get_all_related_many_to_many_objects()
+        self.assertTrue('followers_base' in [o.field.related_query_name() for o in related_m2m])
+
+    def test_related_m2m_symmetrical(self):
+        m2m = Person._meta.many_to_many
+        self.assertTrue('friends_base' in [f.attname for f in m2m])
+        related_m2m = Person._meta.get_all_related_many_to_many_objects()
+        self.assertIn('friends_inherited_rel_+', [o.field.related_query_name() for o in related_m2m])
+
+
+@test.ignore_warnings(category=RemovedInDjango20Warning)
+class GetFieldByNameTests(OptionsBaseTests):
+
+    def test_get_data_field(self):
+        field_info = Person._meta.get_field_by_name('data_abstract')
+        self.assertEqual(field_info[1:], (BasePerson, True, False))
+        self.assertIsInstance(field_info[0], CharField)
+
+    def test_get_m2m_field(self):
+        field_info = Person._meta.get_field_by_name('m2m_base')
+        self.assertEqual(field_info[1:], (BasePerson, True, True))
+        self.assertIsInstance(field_info[0], related.ManyToManyField)
+
+    def test_get_related_object(self):
+        field_info = Person._meta.get_field_by_name('relating_baseperson')
+        self.assertEqual(field_info[1:], (BasePerson, False, False))
+        self.assertTrue(field_info[0].auto_created)
+
+    def test_get_related_m2m(self):
+        field_info = Person._meta.get_field_by_name('relating_people')
+        self.assertEqual(field_info[1:], (None, False, True))
+        self.assertTrue(field_info[0].auto_created)
+
+    def test_get_generic_relation(self):
+        field_info = Person._meta.get_field_by_name('generic_relation_base')
+        self.assertEqual(field_info[1:], (None, True, False))
+        self.assertIsInstance(field_info[0], GenericRelation)
+
+    def test_get_m2m_field_invalid(self):
+        with warnings.catch_warnings(record=True) as warning:
+            warnings.simplefilter("always")
+            self.assertRaises(
+                FieldDoesNotExist,
+                Person._meta.get_field,
+                **{'field_name': 'm2m_base', 'many_to_many': False}
+            )
+            self.assertEqual(Person._meta.get_field('m2m_base', many_to_many=True).name, 'm2m_base')
+
+            # 2 RemovedInDjango20Warning messages should be raised, one for each call of get_field()
+            # with the 'many_to_many' argument.
+            self.assertEqual(
+                [RemovedInDjango20Warning, RemovedInDjango20Warning],
+                [w.message.__class__ for w in warning]
+            )
+
+
+@test.ignore_warnings(category=RemovedInDjango20Warning)
+class GetAllFieldNamesTestCase(OptionsBaseTests):
+
+    def test_get_all_field_names(self):
+        for model, expected_names in TEST_RESULTS['get_all_field_names'].items():
+            objects = model._meta.get_all_field_names()
+            self.assertEqual(sorted(map(str, objects)), sorted(expected_names))

+ 247 - 0
tests/model_meta/tests.py

@@ -0,0 +1,247 @@
+from django.apps import apps
+from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.core.exceptions import FieldDoesNotExist
+from django.db.models.fields import related, CharField, Field
+from django.db.models.options import IMMUTABLE_WARNING, EMPTY_RELATION_TREE
+from django.test import TestCase
+
+from .models import Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating
+from .results import TEST_RESULTS
+
+
+class OptionsBaseTests(TestCase):
+
+    def _map_related_query_names(self, res):
+        return tuple((o.name, m) for o, m in res)
+
+    def _map_names(self, res):
+        return tuple((f.name, m) for f, m in res)
+
+    def _model(self, current_model, field):
+        model = field.model._meta.concrete_model
+        return None if model == current_model else model
+
+    def _details(self, current_model, relation):
+        direct = isinstance(relation, Field) or isinstance(relation, GenericForeignKey)
+        model = relation.model._meta.concrete_model
+        if model == current_model:
+            model = None
+
+        field = relation if direct else relation.field
+        m2m = isinstance(field, related.ManyToManyField)
+        return relation, model, direct, m2m
+
+
+class GetFieldsTests(OptionsBaseTests):
+
+    def test_get_fields_is_immutable(self):
+        msg = IMMUTABLE_WARNING % "get_fields()"
+        for _ in range(2):
+            # Running unit test twice to ensure both non-cached and cached result
+            # are immutable.
+            fields = Person._meta.get_fields()
+            with self.assertRaisesMessage(AttributeError, msg):
+                fields += ["errors"]
+
+
+class DataTests(OptionsBaseTests):
+
+    def test_fields(self):
+        for model, expected_result in TEST_RESULTS['fields'].items():
+            fields = model._meta.fields
+            self.assertEqual([f.attname for f in fields], expected_result)
+
+    def test_local_fields(self):
+        is_data_field = lambda f: isinstance(f, Field) and not isinstance(f, related.ManyToManyField)
+
+        for model, expected_result in TEST_RESULTS['local_fields'].items():
+            fields = model._meta.local_fields
+            self.assertEqual([f.attname for f in fields], expected_result)
+            for f in fields:
+                self.assertEqual(f.model, model)
+                self.assertTrue(is_data_field(f))
+
+    def test_local_concrete_fields(self):
+        for model, expected_result in TEST_RESULTS['local_concrete_fields'].items():
+            fields = model._meta.local_concrete_fields
+            self.assertEqual([f.attname for f in fields], expected_result)
+            for f in fields:
+                self.assertIsNotNone(f.column)
+
+
+class M2MTests(OptionsBaseTests):
+
+    def test_many_to_many(self):
+        for model, expected_result in TEST_RESULTS['many_to_many'].items():
+            fields = model._meta.many_to_many
+            self.assertEqual([f.attname for f in fields], expected_result)
+            for f in fields:
+                self.assertTrue(f.many_to_many and f.is_relation)
+
+    def test_many_to_many_with_model(self):
+        for model, expected_result in TEST_RESULTS['many_to_many_with_model'].items():
+            models = [self._model(model, field) for field in model._meta.many_to_many]
+            self.assertEqual(models, expected_result)
+
+
+class RelatedObjectsTests(OptionsBaseTests):
+    key_name = lambda self, r: r[0]
+
+    def test_related_objects(self):
+        result_key = 'get_all_related_objects_with_model'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = [
+                (field, self._model(model, field))
+                for field in model._meta.get_fields()
+                if field.auto_created and not field.concrete
+            ]
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_objects_local(self):
+        result_key = 'get_all_related_objects_with_model_local'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = [
+                (field, self._model(model, field))
+                for field in model._meta.get_fields(include_parents=False)
+                if field.auto_created and not field.concrete
+            ]
+            self.assertEqual(self._map_related_query_names(objects), expected)
+
+    def test_related_objects_include_hidden(self):
+        result_key = 'get_all_related_objects_with_model_hidden'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = [
+                (field, self._model(model, field))
+                for field in model._meta.get_fields(include_hidden=True)
+                if field.auto_created and not field.concrete
+            ]
+            self.assertEqual(
+                sorted(self._map_names(objects), key=self.key_name),
+                sorted(expected, key=self.key_name)
+            )
+
+    def test_related_objects_include_hidden_local_only(self):
+        result_key = 'get_all_related_objects_with_model_hidden_local'
+        for model, expected in TEST_RESULTS[result_key].items():
+            objects = [
+                (field, self._model(model, field))
+                for field in model._meta.get_fields(include_hidden=True, include_parents=False)
+                if field.auto_created and not field.concrete
+            ]
+            self.assertEqual(
+                sorted(self._map_names(objects), key=self.key_name),
+                sorted(expected, key=self.key_name)
+            )
+
+
+class VirtualFieldsTests(OptionsBaseTests):
+
+    def test_virtual_fields(self):
+        for model, expected_names in TEST_RESULTS['virtual_fields'].items():
+            objects = model._meta.virtual_fields
+            self.assertEqual(sorted([f.name for f in objects]), sorted(expected_names))
+
+
+class GetFieldByNameTests(OptionsBaseTests):
+
+    def test_get_data_field(self):
+        field_info = self._details(Person, Person._meta.get_field('data_abstract'))
+        self.assertEqual(field_info[1:], (BasePerson, True, False))
+        self.assertIsInstance(field_info[0], CharField)
+
+    def test_get_m2m_field(self):
+        field_info = self._details(Person, Person._meta.get_field('m2m_base'))
+        self.assertEqual(field_info[1:], (BasePerson, True, True))
+        self.assertIsInstance(field_info[0], related.ManyToManyField)
+
+    def test_get_related_object(self):
+        field_info = self._details(Person, Person._meta.get_field('relating_baseperson'))
+        self.assertEqual(field_info[1:], (BasePerson, False, False))
+        self.assertIsInstance(field_info[0], related.ForeignObjectRel)
+
+    def test_get_related_m2m(self):
+        field_info = self._details(Person, Person._meta.get_field('relating_people'))
+        self.assertEqual(field_info[1:], (None, False, True))
+        self.assertIsInstance(field_info[0], related.ForeignObjectRel)
+
+    def test_get_generic_relation(self):
+        field_info = self._details(Person, Person._meta.get_field('generic_relation_base'))
+        self.assertEqual(field_info[1:], (None, True, False))
+        self.assertIsInstance(field_info[0], GenericRelation)
+
+    def test_get_fields_only_searaches_forward_on_apps_not_ready(self):
+        opts = Person._meta
+        # If apps registry is not ready, get_field() searches over only
+        # forward fields.
+        opts.apps.ready = False
+        try:
+            # 'data_abstract' is a forward field, and therefore will be found
+            self.assertTrue(opts.get_field('data_abstract'))
+            msg = (
+                "Person has no field named 'relating_baseperson'. The app "
+                "cache isn't ready yet, so if this is a forward field, it "
+                "won't be available yet."
+            )
+            # 'data_abstract' is a reverse field, and will raise an exception
+            with self.assertRaisesMessage(FieldDoesNotExist, msg):
+                opts.get_field('relating_baseperson')
+        finally:
+            opts.apps.ready = True
+
+
+class RelationTreeTests(TestCase):
+    all_models = (Relation, AbstractPerson, BasePerson, Person, ProxyPerson, Relating)
+
+    def setUp(self):
+        apps.clear_cache()
+
+    def test_clear_cache_clears_relation_tree(self):
+        # The apps.clear_cache is setUp() should have deleted all trees.
+        # Exclude abstract models that are not included in the Apps registry
+        # and have no cache.
+        all_models_with_cache = (m for m in self.all_models if not m._meta.abstract)
+        for m in all_models_with_cache:
+            self.assertNotIn('_relation_tree', m._meta.__dict__)
+
+    def test_first_relation_tree_access_populates_all(self):
+        # On first access, relation tree should have populated cache.
+        self.assertTrue(self.all_models[0]._meta._relation_tree)
+
+        # AbstractPerson does not have any relations, so relation_tree
+        # should just return an EMPTY_RELATION_TREE.
+        self.assertEqual(AbstractPerson._meta._relation_tree, EMPTY_RELATION_TREE)
+
+        # All the other models should already have their relation tree
+        # in the internal __dict__ .
+        all_models_but_abstractperson = (m for m in self.all_models if m is not AbstractPerson)
+        for m in all_models_but_abstractperson:
+            self.assertIn('_relation_tree', m._meta.__dict__)
+
+    def test_relations_related_objects(self):
+        # Testing non hidden related objects
+        self.assertEqual(
+            sorted([field.related_query_name() for field in Relation._meta._relation_tree
+                   if not field.rel.field.rel.is_hidden()]),
+            sorted([
+                'fk_abstract_rel', 'fk_abstract_rel', 'fk_abstract_rel', 'fk_base_rel', 'fk_base_rel',
+                'fk_base_rel', 'fk_concrete_rel', 'fk_concrete_rel', 'fo_abstract_rel', 'fo_abstract_rel',
+                'fo_abstract_rel', 'fo_base_rel', 'fo_base_rel', 'fo_base_rel', 'fo_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
+        self.assertEqual(
+            sorted([field.related_query_name() for field in BasePerson._meta._relation_tree]),
+            sorted([
+                '+', '+', 'BasePerson_following_abstract+', 'BasePerson_following_abstract+',
+                'BasePerson_following_base+', 'BasePerson_following_base+', 'BasePerson_friends_abstract+',
+                'BasePerson_friends_abstract+', 'BasePerson_friends_base+', 'BasePerson_friends_base+',
+                'BasePerson_m2m_abstract+', 'BasePerson_m2m_base+', 'Relating_basepeople+',
+                'Relating_basepeople_hidden+', 'followers_abstract', 'followers_abstract', 'followers_abstract',
+                'followers_base', 'followers_base', 'followers_base', 'friends_abstract_rel_+', 'friends_abstract_rel_+',
+                'friends_abstract_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'friends_base_rel_+', 'person',
+                'person', 'relating_basepeople', 'relating_baseperson',
+            ])
+        )
+        self.assertEqual([field.related_query_name() for field in AbstractPerson._meta._relation_tree], [])

+ 1 - 1
tests/queries/tests.py

@@ -2093,7 +2093,7 @@ class CloneTests(TestCase):
         testing is impossible, this is a sanity check against invalid use of
         deepcopy. refs #16759.
         """
-        opts_class = type(Note._meta.get_field_by_name("misc")[0])
+        opts_class = type(Note._meta.get_field("misc"))
         note_deepcopy = getattr(opts_class, "__deepcopy__", None)
         opts_class.__deepcopy__ = lambda obj, memo: self.fail("Model fields shouldn't be cloned")
         try:

+ 1 - 1
tests/queryset_pickle/tests.py

@@ -79,7 +79,7 @@ class PickleabilityTestCase(TestCase):
         m1 = M2MModel.objects.create()
         g1 = Group.objects.create(name='foof')
         m1.groups.add(g1)
-        m2m_through = M2MModel._meta.get_field_by_name('groups')[0].rel.through
+        m2m_through = M2MModel._meta.get_field('groups').rel.through
         original = m2m_through.objects.get()
         dumped = pickle.dumps(original)
         reloaded = pickle.loads(dumped)

+ 26 - 26
tests/schema/tests.py

@@ -138,7 +138,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Book,
-                Book._meta.get_field_by_name("author")[0],
+                Book._meta.get_field("author"),
                 new_field,
                 strict=True,
             )
@@ -393,7 +393,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Author,
-                Author._meta.get_field_by_name("name")[0],
+                Author._meta.get_field("name"),
                 new_field,
                 strict=True,
             )
@@ -424,7 +424,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Note,
-                Note._meta.get_field_by_name("info")[0],
+                Note._meta.get_field("info"),
                 new_field,
                 strict=True,
             )
@@ -451,7 +451,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Author,
-                Author._meta.get_field_by_name("height")[0],
+                Author._meta.get_field("height"),
                 new_field
             )
         # Ensure the field is right afterwards
@@ -479,7 +479,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 AuthorWithDefaultHeight,
-                AuthorWithDefaultHeight._meta.get_field_by_name("height")[0],
+                AuthorWithDefaultHeight._meta.get_field("height"),
                 new_field,
             )
         # Ensure the field is right afterwards
@@ -512,7 +512,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Book,
-                Book._meta.get_field_by_name("author")[0],
+                Book._meta.get_field("author"),
                 new_field,
                 strict=True,
             )
@@ -542,7 +542,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Author,
-                Author._meta.get_field_by_name("id")[0],
+                Author._meta.get_field("id"),
                 new_field,
                 strict=True,
             )
@@ -568,7 +568,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Author,
-                Author._meta.get_field_by_name("name")[0],
+                Author._meta.get_field("name"),
                 new_field,
                 strict=True,
             )
@@ -587,7 +587,7 @@ class SchemaTests(TransactionTestCase):
             editor.create_model(TagM2MTest)
             editor.create_model(BookWithM2M)
         # Ensure there is now an m2m table there
-        columns = self.column_classes(BookWithM2M._meta.get_field_by_name("tags")[0].rel.through)
+        columns = self.column_classes(BookWithM2M._meta.get_field("tags").rel.through)
         self.assertEqual(columns['tagm2mtest_id'][0], "IntegerField")
 
     def test_m2m_create_through(self):
@@ -661,7 +661,7 @@ class SchemaTests(TransactionTestCase):
         self.assertEqual(len(self.column_classes(AuthorTag)), 3)
         # "Alter" the field's blankness. This should not actually do anything.
         with connection.schema_editor() as editor:
-            old_field = AuthorWithM2MThrough._meta.get_field_by_name("tags")[0]
+            old_field = AuthorWithM2MThrough._meta.get_field("tags")
             new_field = ManyToManyField("schema.TagM2MTest", related_name="authors", through="AuthorTag")
             new_field.contribute_to_class(AuthorWithM2MThrough, "tags")
             editor.alter_field(
@@ -683,7 +683,7 @@ class SchemaTests(TransactionTestCase):
             editor.create_model(TagM2MTest)
             editor.create_model(UniqueTest)
         # Ensure the M2M exists and points to TagM2MTest
-        constraints = self.get_constraints(BookWithM2M._meta.get_field_by_name("tags")[0].rel.through._meta.db_table)
+        constraints = self.get_constraints(BookWithM2M._meta.get_field("tags").rel.through._meta.db_table)
         if connection.features.supports_foreign_keys:
             for name, details in constraints.items():
                 if details['columns'] == ["tagm2mtest_id"] and details['foreign_key']:
@@ -698,11 +698,11 @@ class SchemaTests(TransactionTestCase):
             with connection.schema_editor() as editor:
                 editor.alter_field(
                     Author,
-                    BookWithM2M._meta.get_field_by_name("tags")[0],
+                    BookWithM2M._meta.get_field("tags"),
                     new_field,
                 )
             # Ensure old M2M is gone
-            self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field_by_name("tags")[0].rel.through)
+            self.assertRaises(DatabaseError, self.column_classes, BookWithM2M._meta.get_field("tags").rel.through)
             # Ensure the new M2M exists and points to UniqueTest
             constraints = self.get_constraints(new_field.rel.through._meta.db_table)
             if connection.features.supports_foreign_keys:
@@ -715,10 +715,10 @@ class SchemaTests(TransactionTestCase):
         finally:
             # Cleanup through table separately
             with connection.schema_editor() as editor:
-                editor.remove_field(BookWithM2M, BookWithM2M._meta.get_field_by_name("uniques")[0])
+                editor.remove_field(BookWithM2M, BookWithM2M._meta.get_field("uniques"))
             # Cleanup model states
             BookWithM2M._meta.local_many_to_many.remove(new_field)
-            del BookWithM2M._meta._m2m_cache
+            BookWithM2M._meta._expire_cache()
 
     @unittest.skipUnless(connection.features.supports_column_check_constraints, "No check constraints")
     def test_check_constraints(self):
@@ -741,7 +741,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Author,
-                Author._meta.get_field_by_name("height")[0],
+                Author._meta.get_field("height"),
                 new_field,
                 strict=True,
             )
@@ -754,7 +754,7 @@ class SchemaTests(TransactionTestCase):
             editor.alter_field(
                 Author,
                 new_field,
-                Author._meta.get_field_by_name("height")[0],
+                Author._meta.get_field("height"),
                 strict=True,
             )
         constraints = self.get_constraints(Author._meta.db_table)
@@ -781,7 +781,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Tag,
-                Tag._meta.get_field_by_name("slug")[0],
+                Tag._meta.get_field("slug"),
                 new_field,
                 strict=True,
             )
@@ -809,8 +809,8 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Tag,
-                Tag._meta.get_field_by_name("slug")[0],
-                TagUniqueRename._meta.get_field_by_name("slug2")[0],
+                Tag._meta.get_field("slug"),
+                TagUniqueRename._meta.get_field("slug2"),
                 strict=True,
             )
         # Ensure the field is still unique
@@ -976,7 +976,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 Book,
-                Book._meta.get_field_by_name("title")[0],
+                Book._meta.get_field("title"),
                 new_field,
                 strict=True,
             )
@@ -990,7 +990,7 @@ class SchemaTests(TransactionTestCase):
             editor.alter_field(
                 Book,
                 new_field,
-                Book._meta.get_field_by_name("title")[0],
+                Book._meta.get_field("title"),
                 strict=True,
             )
         # Ensure the table is there and has the index again
@@ -1002,7 +1002,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.add_field(
                 Book,
-                BookWithSlug._meta.get_field_by_name("slug")[0],
+                BookWithSlug._meta.get_field("slug"),
             )
         self.assertIn(
             "slug",
@@ -1014,7 +1014,7 @@ class SchemaTests(TransactionTestCase):
         with connection.schema_editor() as editor:
             editor.alter_field(
                 BookWithSlug,
-                BookWithSlug._meta.get_field_by_name("slug")[0],
+                BookWithSlug._meta.get_field("slug"),
                 new_field2,
                 strict=True,
             )
@@ -1039,10 +1039,10 @@ class SchemaTests(TransactionTestCase):
         new_field.set_attributes_from_name("slug")
         new_field.model = Tag
         with connection.schema_editor() as editor:
-            editor.remove_field(Tag, Tag._meta.get_field_by_name("id")[0])
+            editor.remove_field(Tag, Tag._meta.get_field("id"))
             editor.alter_field(
                 Tag,
-                Tag._meta.get_field_by_name("slug")[0],
+                Tag._meta.get_field("slug"),
                 new_field,
             )
         # Ensure the PK changed