django1/tests/model_inheritance/test_abstract_inheritance.py

423 lines
14 KiB
Python
Raw Normal View History

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 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),
]
)