from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.checks import Error from django.core.exceptions import FieldDoesNotExist, FieldError from django.db import models from django.test import SimpleTestCase from django.test.utils import isolate_apps @isolate_apps("model_inheritance") class AbstractInheritanceTests(SimpleTestCase): def test_single_parent(self): class AbstractBase(models.Model): name = models.CharField(max_length=30) class Meta: abstract = True class AbstractDescendant(AbstractBase): name = models.CharField(max_length=50) class Meta: abstract = True class DerivedChild(AbstractBase): name = models.CharField(max_length=50) class DerivedGrandChild(AbstractDescendant): pass self.assertEqual(AbstractDescendant._meta.get_field("name").max_length, 50) self.assertEqual(DerivedChild._meta.get_field("name").max_length, 50) self.assertEqual(DerivedGrandChild._meta.get_field("name").max_length, 50) def test_multiple_inheritance_allows_inherited_field(self): """ Single layer multiple inheritance is as expected, deriving the inherited field from the first base. """ class ParentA(models.Model): name = models.CharField(max_length=255) class Meta: abstract = True class ParentB(models.Model): name = models.IntegerField() class Meta: abstract = True class Child(ParentA, ParentB): pass self.assertEqual(Child.check(), []) inherited_field = Child._meta.get_field("name") self.assertIsInstance(inherited_field, models.CharField) self.assertEqual(inherited_field.max_length, 255) def test_diamond_shaped_multiple_inheritance_is_depth_first(self): """ In contrast to standard Python MRO, resolution of inherited fields is strictly depth-first, rather than breadth-first in diamond-shaped cases. This is because a copy of the parent field descriptor is placed onto the model class in ModelBase.__new__(), rather than the attribute lookup going via bases. (It only **looks** like inheritance.) Here, Child inherits name from Root, rather than ParentB. """ class Root(models.Model): name = models.CharField(max_length=255) class Meta: abstract = True class ParentA(Root): class Meta: abstract = True class ParentB(Root): name = models.IntegerField() class Meta: abstract = True class Child(ParentA, ParentB): pass self.assertEqual(Child.check(), []) inherited_field = Child._meta.get_field("name") self.assertIsInstance(inherited_field, models.CharField) self.assertEqual(inherited_field.max_length, 255) def test_target_field_may_be_pushed_down(self): """ Where the Child model needs to inherit a field from a different base than that given by depth-first resolution, the target field can be **pushed down** by being re-declared. """ class Root(models.Model): name = models.CharField(max_length=255) class Meta: abstract = True class ParentA(Root): class Meta: abstract = True class ParentB(Root): name = models.IntegerField() class Meta: abstract = True class Child(ParentA, ParentB): name = models.IntegerField() self.assertEqual(Child.check(), []) inherited_field = Child._meta.get_field("name") self.assertIsInstance(inherited_field, models.IntegerField) def test_multiple_inheritance_cannot_shadow_concrete_inherited_field(self): class ConcreteParent(models.Model): name = models.CharField(max_length=255) class AbstractParent(models.Model): name = models.IntegerField() class Meta: abstract = True class FirstChild(ConcreteParent, AbstractParent): pass class AnotherChild(AbstractParent, ConcreteParent): pass self.assertIsInstance(FirstChild._meta.get_field("name"), models.CharField) self.assertEqual( AnotherChild.check(), [ Error( "The field 'name' clashes with the field 'name' " "from model 'model_inheritance.concreteparent'.", obj=AnotherChild._meta.get_field("name"), id="models.E006", ) ], ) def test_virtual_field(self): class RelationModel(models.Model): content_type = models.ForeignKey(ContentType, models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey("content_type", "object_id") class RelatedModelAbstract(models.Model): field = GenericRelation(RelationModel) class Meta: abstract = True class ModelAbstract(models.Model): field = models.CharField(max_length=100) class Meta: abstract = True class OverrideRelatedModelAbstract(RelatedModelAbstract): field = models.CharField(max_length=100) class ExtendModelAbstract(ModelAbstract): field = GenericRelation(RelationModel) self.assertIsInstance( OverrideRelatedModelAbstract._meta.get_field("field"), models.CharField ) self.assertIsInstance( ExtendModelAbstract._meta.get_field("field"), GenericRelation ) def test_cannot_override_indirect_abstract_field(self): class AbstractBase(models.Model): name = models.CharField(max_length=30) class Meta: abstract = True class ConcreteDescendant(AbstractBase): pass msg = ( "Local field 'name' in class 'Descendant' clashes with field of " "the same name from base class 'ConcreteDescendant'." ) with self.assertRaisesMessage(FieldError, msg): class Descendant(ConcreteDescendant): name = models.IntegerField() def test_override_field_with_attr(self): class AbstractBase(models.Model): first_name = models.CharField(max_length=50) last_name = models.CharField(max_length=50) middle_name = models.CharField(max_length=30) full_name = models.CharField(max_length=150) class Meta: abstract = True class Descendant(AbstractBase): middle_name = None def full_name(self): return self.first_name + self.last_name msg = "Descendant has no field named %r" with self.assertRaisesMessage(FieldDoesNotExist, msg % "middle_name"): Descendant._meta.get_field("middle_name") with self.assertRaisesMessage(FieldDoesNotExist, msg % "full_name"): Descendant._meta.get_field("full_name") def test_overriding_field_removed_by_concrete_model(self): class AbstractModel(models.Model): foo = models.CharField(max_length=30) class Meta: abstract = True class RemovedAbstractModelField(AbstractModel): foo = None class OverrideRemovedFieldByConcreteModel(RemovedAbstractModelField): foo = models.CharField(max_length=50) self.assertEqual( OverrideRemovedFieldByConcreteModel._meta.get_field("foo").max_length, 50 ) def test_shadowed_fkey_id(self): class Foo(models.Model): pass class AbstractBase(models.Model): foo = models.ForeignKey(Foo, models.CASCADE) class Meta: abstract = True class Descendant(AbstractBase): foo_id = models.IntegerField() self.assertEqual( Descendant.check(), [ Error( "The field 'foo_id' clashes with the field 'foo' " "from model 'model_inheritance.descendant'.", obj=Descendant._meta.get_field("foo_id"), id="models.E006", ) ], ) def test_shadow_related_name_when_set_to_none(self): class AbstractBase(models.Model): bar = models.IntegerField() class Meta: abstract = True class Foo(AbstractBase): bar = None foo = models.IntegerField() class Bar(models.Model): bar = models.ForeignKey(Foo, models.CASCADE, related_name="bar") self.assertEqual(Bar.check(), []) def test_reverse_foreign_key(self): class AbstractBase(models.Model): foo = models.CharField(max_length=100) class Meta: abstract = True class Descendant(AbstractBase): pass class Foo(models.Model): foo = models.ForeignKey(Descendant, models.CASCADE, related_name="foo") self.assertEqual( Foo._meta.get_field("foo").check(), [ Error( "Reverse accessor 'Descendant.foo' for " "'model_inheritance.Foo.foo' clashes with field name " "'model_inheritance.Descendant.foo'.", hint=( "Rename field 'model_inheritance.Descendant.foo', or " "add/change a related_name argument to the definition " "for field 'model_inheritance.Foo.foo'." ), obj=Foo._meta.get_field("foo"), id="fields.E302", ), Error( "Reverse query name for 'model_inheritance.Foo.foo' " "clashes with field name " "'model_inheritance.Descendant.foo'.", hint=( "Rename field 'model_inheritance.Descendant.foo', or " "add/change a related_name argument to the definition " "for field 'model_inheritance.Foo.foo'." ), obj=Foo._meta.get_field("foo"), id="fields.E303", ), ], ) def test_multi_inheritance_field_clashes(self): class AbstractBase(models.Model): name = models.CharField(max_length=30) class Meta: abstract = True class ConcreteBase(AbstractBase): pass class AbstractDescendant(ConcreteBase): class Meta: abstract = True class ConcreteDescendant(AbstractDescendant): name = models.CharField(max_length=100) self.assertEqual( ConcreteDescendant.check(), [ Error( "The field 'name' clashes with the field 'name' from " "model 'model_inheritance.concretebase'.", obj=ConcreteDescendant._meta.get_field("name"), id="models.E006", ) ], ) def test_override_one2one_relation_auto_field_clashes(self): class ConcreteParent(models.Model): name = models.CharField(max_length=255) class AbstractParent(models.Model): name = models.IntegerField() class Meta: abstract = True msg = ( "Auto-generated field 'concreteparent_ptr' in class 'Descendant' " "for parent_link to base class 'ConcreteParent' clashes with " "declared field of the same name." ) with self.assertRaisesMessage(FieldError, msg): class Descendant(ConcreteParent, AbstractParent): concreteparent_ptr = models.CharField(max_length=30) def test_abstract_model_with_regular_python_mixin_mro(self): class AbstractModel(models.Model): name = models.CharField(max_length=255) age = models.IntegerField() class Meta: abstract = True class Mixin: age = None class Mixin2: age = 2 class DescendantMixin(Mixin): pass class ConcreteModel(models.Model): foo = models.IntegerField() class ConcreteModel2(ConcreteModel): age = models.SmallIntegerField() def fields(model): if not hasattr(model, "_meta"): return [] return [(f.name, f.__class__) for f in model._meta.get_fields()] model_dict = {"__module__": "model_inheritance"} model1 = type("Model1", (AbstractModel, Mixin), model_dict.copy()) model2 = type("Model2", (Mixin2, AbstractModel), model_dict.copy()) model3 = type("Model3", (DescendantMixin, AbstractModel), model_dict.copy()) model4 = type("Model4", (Mixin2, Mixin, AbstractModel), model_dict.copy()) model5 = type( "Model5", (Mixin2, ConcreteModel2, Mixin, AbstractModel), model_dict.copy() ) self.assertEqual( fields(model1), [ ("id", models.AutoField), ("name", models.CharField), ("age", models.IntegerField), ], ) self.assertEqual( fields(model2), [("id", models.AutoField), ("name", models.CharField)] ) self.assertEqual(getattr(model2, "age"), 2) self.assertEqual( fields(model3), [("id", models.AutoField), ("name", models.CharField)] ) self.assertEqual( fields(model4), [("id", models.AutoField), ("name", models.CharField)] ) self.assertEqual(getattr(model4, "age"), 2) self.assertEqual( fields(model5), [ ("id", models.AutoField), ("foo", models.IntegerField), ("concretemodel_ptr", models.OneToOneField), ("age", models.SmallIntegerField), ("concretemodel2_ptr", models.OneToOneField), ("name", models.CharField), ], )