diff --git a/django/db/models/base.py b/django/db/models/base.py index b57726fbcf0..b55c5a32b18 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -58,6 +58,11 @@ def subclass_exception(name, bases, module, attached_to): }) +def _has_contribute_to_class(value): + # Only call contribute_to_class() if it's bound. + return not inspect.isclass(value) and hasattr(value, 'contribute_to_class') + + class ModelBase(type): """Metaclass for all models.""" def __new__(cls, name, bases, attrs, **kwargs): @@ -75,8 +80,15 @@ class ModelBase(type): classcell = attrs.pop('__classcell__', None) if classcell is not None: new_attrs['__classcell__'] = classcell - new_class = super_new(cls, name, bases, new_attrs, **kwargs) attr_meta = attrs.pop('Meta', None) + # Pass all attrs without a (Django-specific) contribute_to_class() + # method to type.__new__() so that they're properly initialized + # (i.e. __set_name__()). + for obj_name, obj in list(attrs.items()): + if not _has_contribute_to_class(obj): + new_attrs[obj_name] = attrs.pop(obj_name) + new_class = super_new(cls, name, bases, new_attrs, **kwargs) + abstract = getattr(attr_meta, 'abstract', False) meta = attr_meta or getattr(new_class, 'Meta', None) base_meta = getattr(new_class, '_meta', None) @@ -300,8 +312,7 @@ class ModelBase(type): return new_class def add_to_class(cls, name, value): - # We should call the contribute_to_class method only if it's bound - if not inspect.isclass(value) and hasattr(value, 'contribute_to_class'): + if _has_contribute_to_class(value): value.contribute_to_class(cls, name) else: setattr(cls, name, value) diff --git a/tests/model_inheritance/tests.py b/tests/model_inheritance/tests.py index 4e8e92a60c3..5f89b4aa83a 100644 --- a/tests/model_inheritance/tests.py +++ b/tests/model_inheritance/tests.py @@ -181,18 +181,33 @@ class ModelInheritanceTests(TestCase): def test_init_subclass(self): saved_kwargs = {} - class A: + class A(models.Model): def __init_subclass__(cls, **kwargs): super().__init_subclass__() saved_kwargs.update(kwargs) kwargs = {'x': 1, 'y': 2, 'z': 3} - class B(A, models.Model, **kwargs): + class B(A, **kwargs): pass self.assertEqual(saved_kwargs, kwargs) + @unittest.skipUnless(PY36, '__set_name__ is new in Python 3.6') + @isolate_apps('model_inheritance') + def test_set_name(self): + class ClassAttr: + called = None + + def __set_name__(self_, owner, name): + self.assertIsNone(self_.called) + self_.called = (owner, name) + + class A(models.Model): + attr = ClassAttr() + + self.assertEqual(A.attr.called, (A, 'attr')) + class ModelInheritanceDataTests(TestCase): @classmethod