test_abstract_inheritance.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. from django.contrib.contenttypes.fields import (
  2. GenericForeignKey, GenericRelation,
  3. )
  4. from django.contrib.contenttypes.models import ContentType
  5. from django.core.checks import Error
  6. from django.core.exceptions import FieldDoesNotExist, FieldError
  7. from django.db import models
  8. from django.test import SimpleTestCase
  9. from django.test.utils import isolate_apps
  10. @isolate_apps('model_inheritance')
  11. class AbstractInheritanceTests(SimpleTestCase):
  12. def test_single_parent(self):
  13. class AbstractBase(models.Model):
  14. name = models.CharField(max_length=30)
  15. class Meta:
  16. abstract = True
  17. class AbstractDescendant(AbstractBase):
  18. name = models.CharField(max_length=50)
  19. class Meta:
  20. abstract = True
  21. class DerivedChild(AbstractBase):
  22. name = models.CharField(max_length=50)
  23. class DerivedGrandChild(AbstractDescendant):
  24. pass
  25. self.assertEqual(AbstractDescendant._meta.get_field('name').max_length, 50)
  26. self.assertEqual(DerivedChild._meta.get_field('name').max_length, 50)
  27. self.assertEqual(DerivedGrandChild._meta.get_field('name').max_length, 50)
  28. def test_multiple_inheritance_allows_inherited_field(self):
  29. """
  30. Single layer multiple inheritance is as expected, deriving the
  31. inherited field from the first base.
  32. """
  33. class ParentA(models.Model):
  34. name = models.CharField(max_length=255)
  35. class Meta:
  36. abstract = True
  37. class ParentB(models.Model):
  38. name = models.IntegerField()
  39. class Meta:
  40. abstract = True
  41. class Child(ParentA, ParentB):
  42. pass
  43. self.assertEqual(Child.check(), [])
  44. inherited_field = Child._meta.get_field('name')
  45. self.assertIsInstance(inherited_field, models.CharField)
  46. self.assertEqual(inherited_field.max_length, 255)
  47. def test_diamond_shaped_multiple_inheritance_is_depth_first(self):
  48. """
  49. In contrast to standard Python MRO, resolution of inherited fields is
  50. strictly depth-first, rather than breadth-first in diamond-shaped cases.
  51. This is because a copy of the parent field descriptor is placed onto
  52. the model class in ModelBase.__new__(), rather than the attribute
  53. lookup going via bases. (It only **looks** like inheritance.)
  54. Here, Child inherits name from Root, rather than ParentB.
  55. """
  56. class Root(models.Model):
  57. name = models.CharField(max_length=255)
  58. class Meta:
  59. abstract = True
  60. class ParentA(Root):
  61. class Meta:
  62. abstract = True
  63. class ParentB(Root):
  64. name = models.IntegerField()
  65. class Meta:
  66. abstract = True
  67. class Child(ParentA, ParentB):
  68. pass
  69. self.assertEqual(Child.check(), [])
  70. inherited_field = Child._meta.get_field('name')
  71. self.assertIsInstance(inherited_field, models.CharField)
  72. self.assertEqual(inherited_field.max_length, 255)
  73. def test_target_field_may_be_pushed_down(self):
  74. """
  75. Where the Child model needs to inherit a field from a different base
  76. than that given by depth-first resolution, the target field can be
  77. **pushed down** by being re-declared.
  78. """
  79. class Root(models.Model):
  80. name = models.CharField(max_length=255)
  81. class Meta:
  82. abstract = True
  83. class ParentA(Root):
  84. class Meta:
  85. abstract = True
  86. class ParentB(Root):
  87. name = models.IntegerField()
  88. class Meta:
  89. abstract = True
  90. class Child(ParentA, ParentB):
  91. name = models.IntegerField()
  92. self.assertEqual(Child.check(), [])
  93. inherited_field = Child._meta.get_field('name')
  94. self.assertIsInstance(inherited_field, models.IntegerField)
  95. def test_multiple_inheritance_cannot_shadow_concrete_inherited_field(self):
  96. class ConcreteParent(models.Model):
  97. name = models.CharField(max_length=255)
  98. class AbstractParent(models.Model):
  99. name = models.IntegerField()
  100. class Meta:
  101. abstract = True
  102. class FirstChild(ConcreteParent, AbstractParent):
  103. pass
  104. class AnotherChild(AbstractParent, ConcreteParent):
  105. pass
  106. self.assertIsInstance(FirstChild._meta.get_field('name'), models.CharField)
  107. self.assertEqual(
  108. AnotherChild.check(),
  109. [Error(
  110. "The field 'name' clashes with the field 'name' "
  111. "from model 'model_inheritance.concreteparent'.",
  112. obj=AnotherChild._meta.get_field('name'),
  113. id="models.E006",
  114. )]
  115. )
  116. def test_virtual_field(self):
  117. class RelationModel(models.Model):
  118. content_type = models.ForeignKey(ContentType, models.CASCADE)
  119. object_id = models.PositiveIntegerField()
  120. content_object = GenericForeignKey('content_type', 'object_id')
  121. class RelatedModelAbstract(models.Model):
  122. field = GenericRelation(RelationModel)
  123. class Meta:
  124. abstract = True
  125. class ModelAbstract(models.Model):
  126. field = models.CharField(max_length=100)
  127. class Meta:
  128. abstract = True
  129. class OverrideRelatedModelAbstract(RelatedModelAbstract):
  130. field = models.CharField(max_length=100)
  131. class ExtendModelAbstract(ModelAbstract):
  132. field = GenericRelation(RelationModel)
  133. self.assertIsInstance(OverrideRelatedModelAbstract._meta.get_field('field'), models.CharField)
  134. self.assertIsInstance(ExtendModelAbstract._meta.get_field('field'), GenericRelation)
  135. def test_cannot_override_indirect_abstract_field(self):
  136. class AbstractBase(models.Model):
  137. name = models.CharField(max_length=30)
  138. class Meta:
  139. abstract = True
  140. class ConcreteDescendant(AbstractBase):
  141. pass
  142. msg = (
  143. "Local field 'name' in class 'Descendant' clashes with field of "
  144. "the same name from base class 'ConcreteDescendant'."
  145. )
  146. with self.assertRaisesMessage(FieldError, msg):
  147. class Descendant(ConcreteDescendant):
  148. name = models.IntegerField()
  149. def test_override_field_with_attr(self):
  150. class AbstractBase(models.Model):
  151. first_name = models.CharField(max_length=50)
  152. last_name = models.CharField(max_length=50)
  153. middle_name = models.CharField(max_length=30)
  154. full_name = models.CharField(max_length=150)
  155. class Meta:
  156. abstract = True
  157. class Descendant(AbstractBase):
  158. middle_name = None
  159. def full_name(self):
  160. return self.first_name + self.last_name
  161. msg = "Descendant has no field named %r"
  162. with self.assertRaisesMessage(FieldDoesNotExist, msg % 'middle_name'):
  163. Descendant._meta.get_field('middle_name')
  164. with self.assertRaisesMessage(FieldDoesNotExist, msg % 'full_name'):
  165. Descendant._meta.get_field('full_name')
  166. def test_overriding_field_removed_by_concrete_model(self):
  167. class AbstractModel(models.Model):
  168. foo = models.CharField(max_length=30)
  169. class Meta:
  170. abstract = True
  171. class RemovedAbstractModelField(AbstractModel):
  172. foo = None
  173. class OverrideRemovedFieldByConcreteModel(RemovedAbstractModelField):
  174. foo = models.CharField(max_length=50)
  175. self.assertEqual(OverrideRemovedFieldByConcreteModel._meta.get_field('foo').max_length, 50)
  176. def test_shadowed_fkey_id(self):
  177. class Foo(models.Model):
  178. pass
  179. class AbstractBase(models.Model):
  180. foo = models.ForeignKey(Foo, models.CASCADE)
  181. class Meta:
  182. abstract = True
  183. class Descendant(AbstractBase):
  184. foo_id = models.IntegerField()
  185. self.assertEqual(
  186. Descendant.check(),
  187. [Error(
  188. "The field 'foo_id' clashes with the field 'foo' "
  189. "from model 'model_inheritance.descendant'.",
  190. obj=Descendant._meta.get_field('foo_id'),
  191. id='models.E006',
  192. )]
  193. )
  194. def test_shadow_related_name_when_set_to_none(self):
  195. class AbstractBase(models.Model):
  196. bar = models.IntegerField()
  197. class Meta:
  198. abstract = True
  199. class Foo(AbstractBase):
  200. bar = None
  201. foo = models.IntegerField()
  202. class Bar(models.Model):
  203. bar = models.ForeignKey(Foo, models.CASCADE, related_name='bar')
  204. self.assertEqual(Bar.check(), [])
  205. def test_reverse_foreign_key(self):
  206. class AbstractBase(models.Model):
  207. foo = models.CharField(max_length=100)
  208. class Meta:
  209. abstract = True
  210. class Descendant(AbstractBase):
  211. pass
  212. class Foo(models.Model):
  213. foo = models.ForeignKey(Descendant, models.CASCADE, related_name='foo')
  214. self.assertEqual(
  215. Foo._meta.get_field('foo').check(),
  216. [
  217. Error(
  218. "Reverse accessor 'Descendant.foo' for "
  219. "'model_inheritance.Foo.foo' clashes with field name "
  220. "'model_inheritance.Descendant.foo'.",
  221. hint=(
  222. "Rename field 'model_inheritance.Descendant.foo', or "
  223. "add/change a related_name argument to the definition "
  224. "for field 'model_inheritance.Foo.foo'."
  225. ),
  226. obj=Foo._meta.get_field('foo'),
  227. id='fields.E302',
  228. ),
  229. Error(
  230. "Reverse query name for 'model_inheritance.Foo.foo' "
  231. "clashes with field name "
  232. "'model_inheritance.Descendant.foo'.",
  233. hint=(
  234. "Rename field 'model_inheritance.Descendant.foo', or "
  235. "add/change a related_name argument to the definition "
  236. "for field 'model_inheritance.Foo.foo'."
  237. ),
  238. obj=Foo._meta.get_field('foo'),
  239. id='fields.E303',
  240. ),
  241. ]
  242. )
  243. def test_multi_inheritance_field_clashes(self):
  244. class AbstractBase(models.Model):
  245. name = models.CharField(max_length=30)
  246. class Meta:
  247. abstract = True
  248. class ConcreteBase(AbstractBase):
  249. pass
  250. class AbstractDescendant(ConcreteBase):
  251. class Meta:
  252. abstract = True
  253. class ConcreteDescendant(AbstractDescendant):
  254. name = models.CharField(max_length=100)
  255. self.assertEqual(
  256. ConcreteDescendant.check(),
  257. [Error(
  258. "The field 'name' clashes with the field 'name' from "
  259. "model 'model_inheritance.concretebase'.",
  260. obj=ConcreteDescendant._meta.get_field('name'),
  261. id="models.E006",
  262. )]
  263. )
  264. def test_override_one2one_relation_auto_field_clashes(self):
  265. class ConcreteParent(models.Model):
  266. name = models.CharField(max_length=255)
  267. class AbstractParent(models.Model):
  268. name = models.IntegerField()
  269. class Meta:
  270. abstract = True
  271. msg = (
  272. "Auto-generated field 'concreteparent_ptr' in class 'Descendant' "
  273. "for parent_link to base class 'ConcreteParent' clashes with "
  274. "declared field of the same name."
  275. )
  276. with self.assertRaisesMessage(FieldError, msg):
  277. class Descendant(ConcreteParent, AbstractParent):
  278. concreteparent_ptr = models.CharField(max_length=30)
  279. def test_abstract_model_with_regular_python_mixin_mro(self):
  280. class AbstractModel(models.Model):
  281. name = models.CharField(max_length=255)
  282. age = models.IntegerField()
  283. class Meta:
  284. abstract = True
  285. class Mixin:
  286. age = None
  287. class Mixin2:
  288. age = 2
  289. class DescendantMixin(Mixin):
  290. pass
  291. class ConcreteModel(models.Model):
  292. foo = models.IntegerField()
  293. class ConcreteModel2(ConcreteModel):
  294. age = models.SmallIntegerField()
  295. def fields(model):
  296. if not hasattr(model, '_meta'):
  297. return []
  298. return [(f.name, f.__class__) for f in model._meta.get_fields()]
  299. model_dict = {'__module__': 'model_inheritance'}
  300. model1 = type('Model1', (AbstractModel, Mixin), model_dict.copy())
  301. model2 = type('Model2', (Mixin2, AbstractModel), model_dict.copy())
  302. model3 = type('Model3', (DescendantMixin, AbstractModel), model_dict.copy())
  303. model4 = type('Model4', (Mixin2, Mixin, AbstractModel), model_dict.copy())
  304. model5 = type('Model5', (Mixin2, ConcreteModel2, Mixin, AbstractModel), model_dict.copy())
  305. self.assertEqual(
  306. fields(model1),
  307. [('id', models.AutoField), ('name', models.CharField), ('age', models.IntegerField)]
  308. )
  309. self.assertEqual(fields(model2), [('id', models.AutoField), ('name', models.CharField)])
  310. self.assertEqual(getattr(model2, 'age'), 2)
  311. self.assertEqual(fields(model3), [('id', models.AutoField), ('name', models.CharField)])
  312. self.assertEqual(fields(model4), [('id', models.AutoField), ('name', models.CharField)])
  313. self.assertEqual(getattr(model4, 'age'), 2)
  314. self.assertEqual(
  315. fields(model5),
  316. [
  317. ('id', models.AutoField), ('foo', models.IntegerField),
  318. ('concretemodel_ptr', models.OneToOneField),
  319. ('age', models.SmallIntegerField), ('concretemodel2_ptr', models.OneToOneField),
  320. ('name', models.CharField),
  321. ]
  322. )