summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMalcolm Tredinnick <malcolm.tredinnick@gmail.com>2008-02-22 01:05:05 +0000
committerMalcolm Tredinnick <malcolm.tredinnick@gmail.com>2008-02-22 01:05:05 +0000
commit0c20e88e65b8c2b1d097510ee2d7cfe6b2cf9b97 (patch)
tree14378fabc83fb32b4a23941a856c0c690ef4aa5a
parentca123b0760de38b4df50ca321927a365d2d1c796 (diff)
queryset-refactor: Fixed up and documented Meta-class inheritance.
Should be mostly logical (for versions of "logical" that may require you to be a Vulcan, admittedly, but that's not entirely my fault). git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7141 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/db/models/base.py31
-rw-r--r--django/db/models/options.py2
-rw-r--r--docs/model-api.txt86
-rw-r--r--tests/modeltests/model_inheritance/models.py32
4 files changed, 136 insertions, 15 deletions
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 139fc3d8df..9a4d6664ab 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -35,14 +35,29 @@ class ModelBase(type):
# Create the class.
module = attrs.pop('__module__')
- meta = attrs.pop('Meta', None)
new_class = type.__new__(cls, name, bases, {'__module__': module})
+ attr_meta = attrs.pop('Meta', None)
+ abstract = getattr(attr_meta, 'abstract', False)
+ if not attr_meta:
+ meta = getattr(new_class, 'Meta', None)
+ else:
+ meta = attr_meta
+ base_meta = getattr(new_class, '_meta', None)
+
new_class.add_to_class('_meta', Options(meta))
- new_class.add_to_class('DoesNotExist',
- subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
- new_class.add_to_class('MultipleObjectsReturned',
- subclass_exception('MultipleObjectsReturned',
- MultipleObjectsReturned, module))
+ if not abstract:
+ new_class.add_to_class('DoesNotExist',
+ subclass_exception('DoesNotExist', ObjectDoesNotExist, module))
+ new_class.add_to_class('MultipleObjectsReturned',
+ subclass_exception('MultipleObjectsReturned', MultipleObjectsReturned, module))
+ if base_meta and not base_meta.abstract:
+ # Non-abstract child classes inherit some attributes from their
+ # non-abstract parent (unless an ABC comes before it in the
+ # method resolution order).
+ if not hasattr(meta, 'ordering'):
+ new_class._meta.ordering = base_meta.ordering
+ if not hasattr(meta, 'get_latest_by'):
+ new_class._meta.get_latest_by = base_meta.get_latest_by
# Do the appropriate setup for any model parents.
abstract_parents = []
@@ -86,10 +101,12 @@ class ModelBase(type):
% (field.name, name, parent.__name__))
new_class.add_to_class(field.name, field)
- if new_class._meta.abstract:
+ if abstract:
# Abstract base models can't be instantiated and don't appear in
# the list of models for an app. We do the final setup for them a
# little differently from normal models.
+ attr_meta.abstract = False
+ new_class.Meta = attr_meta
return new_class
new_class._prepare()
diff --git a/django/db/models/options.py b/django/db/models/options.py
index 05b1d52f8d..a5e853de33 100644
--- a/django/db/models/options.py
+++ b/django/db/models/options.py
@@ -55,7 +55,7 @@ class Options(object):
# Next, apply any overridden values from 'class Meta'.
if self.meta:
- meta_attrs = self.meta.__dict__
+ meta_attrs = self.meta.__dict__.copy()
del meta_attrs['__module__']
del meta_attrs['__doc__']
for attr_name in DEFAULT_NAMES:
diff --git a/docs/model-api.txt b/docs/model-api.txt
index 6ca08ae4e5..2687c00d14 100644
--- a/docs/model-api.txt
+++ b/docs/model-api.txt
@@ -2031,6 +2031,18 @@ You can also prevent saving::
Model inheritance
=================
+**New in Django development version**
+
+Model inheritance in Django works almost identically to the way normal class
+inheritance works in Python. The only decision you have to make is whether you
+want the parent models to be models in their own right (with their own
+database tables), or if the parents are just holders of common information
+that will only be visible through the child models.
+
+Often, you will just want to use the parent class to hold information that you
+don't want to have to type out for each child model. This class isn't going to
+ever be used in isolation, so `abstract base classes`_ are what you're after. However, if you're subclassing an existing model (perhaps something from another application entirely), or want each model to have its own database table, `multi-table inheritance`_ is the way to go.
+
Abstract base classes
---------------------
@@ -2063,6 +2075,38 @@ For many uses, this type of model inheritance will be exactly what you want.
It provides a way to factor out common information at the Python level, whilst
still only creating one database table per child model at the database level.
+``Meta`` inheritance
+~~~~~~~~~~~~~~~~~~~~
+
+When an abstract base class is created, Django makes any ``Meta`` inner class
+you declared on the base class available as an attribute. If a child class
+does not declared its own ``Meta`` class, it will inherit the parent's
+``Meta``. If the child wants to extend the parent's ``Meta`` class, it can
+subclass it. For example::
+
+ class CommonInfo(models.Model):
+ ...
+ class Meta:
+ abstract = True
+ ordering = ['name']
+
+ class Student(CommonInfo):
+ ...
+ class Meta(CommonInfo.Meta):
+ db_table = 'student_info'
+
+Django does make one adjustment to the ``Meta`` class of an abstract base
+class: before installing the ``Meta`` attribute, it sets ``abstract=False``.
+This means that children of abstract base classes don't automatically become
+abstract classes themselves. Of course, you can make an abstract base class
+that inherits from another abstract base class. You just need to remember to
+explicitly set ``abstract=True`` each time.
+
+Some attributes won't make sense to include in the ``Meta`` class of an
+abstract base class. For example, including ``db_table`` would mean that all
+the child classes (the ones that don't specify their own ``Meta``) would use
+the same database table, which is almost certainly not what you want.
+
Multi-table inheritance
-----------------------
@@ -2100,8 +2144,29 @@ However, if ``p`` in the above example was *not* a ``Restaurant`` (it had been
created directly as a ``Place`` object or was the parent of some other class),
referring to ``p.restaurant`` would give an error.
-Normally you won't need to worry too much about how model inheritance works.
-It will behave similarly to Python class inheritance.
+``Meta`` and multi-table inheritance
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the multi-table inheritance situation, it doesn't make sense for a child
+class to inherit from its parent's ``Meta`` class. All the ``Meta`` options
+have already been applied to the parent class and applying them again would
+normally only lead to contradictory behaviour (this is in contrast with the
+abstract base class case, where the base class doesn't exist in its own
+right).
+
+So a child model does not have access to its parent's ``Meta`` class. However,
+there are a few limited cases where the child inherits behaviour from the
+parent: if the child does not specify an ``ordering`` attribute or a
+``get_latest_by`` attribute, it will inherit these from its parent.
+
+If the parent has an ordering and you don't want the child to have any natural
+ordering, you can explicity set it to be empty::
+
+ class ChildModel(ParentModel):
+ ...
+ class Meta:
+ # Remove parent's ordering effect
+ ordering = []
Inheritance and reverse relations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2127,6 +2192,23 @@ For more information about reverse relations, refer to the `Database API
reference`_ . For now, just remember to run ``manage.py validate`` when
you're writing your models and pay attention to the error messages.
+Multiple inheritance
+--------------------
+
+Just as with Python's subclassing, it's possible for a Django model to inherit
+from multiple parent models. Keep in mind that normal Python name resolution
+rules apply. The first base class that a particular name appears in (e.g.
+``Meta``) will be the one that is used. We stop searching once we find the
+name once. This means that if multiple parents contain a ``Meta`` class, only
+the first one is going to be used. All others will be ignored.
+
+Generally, you won't need to inherit from multiple parents. The main use-case
+where this is useful is for ''mix-in'' classes: adding a particular extra
+field or method to every class that inherits the mix-in. Try to keep your
+inheritance hierarchies as simple and straightforward as possible so that you
+won't have to struggle to work out where a particular piece of information is
+coming from.
+
Models across files
===================
diff --git a/tests/modeltests/model_inheritance/models.py b/tests/modeltests/model_inheritance/models.py
index 940b2ee10c..1187c5fe64 100644
--- a/tests/modeltests/model_inheritance/models.py
+++ b/tests/modeltests/model_inheritance/models.py
@@ -20,6 +20,7 @@ class CommonInfo(models.Model):
class Meta:
abstract = True
+ ordering = ['name']
def __unicode__(self):
return u'%s %s' % (self.__class__.__name__, self.name)
@@ -30,6 +31,9 @@ class Worker(CommonInfo):
class Student(CommonInfo):
school_class = models.CharField(max_length=10)
+ class Meta:
+ pass
+
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
@@ -37,7 +41,13 @@ class Place(models.Model):
def __unicode__(self):
return u"%s the place" % self.name
-class Restaurant(Place):
+class Rating(models.Model):
+ rating = models.IntegerField(null=True, blank=True)
+
+ class Meta:
+ abstract = True
+
+class Restaurant(Place, Rating):
serves_hot_dogs = models.BooleanField()
serves_pizza = models.BooleanField()
@@ -71,6 +81,8 @@ __test__ = {'API_TESTS':"""
>>> w = Worker(name='Fred', age=35, job='Quarry worker')
>>> w.save()
+>>> w2 = Worker(name='Barney', age=34, job='Quarry worker')
+>>> w2.save()
>>> s = Student(name='Pebbles', age=5, school_class='1B')
>>> s.save()
>>> unicode(w)
@@ -78,6 +90,16 @@ u'Worker Fred'
>>> unicode(s)
u'Student Pebbles'
+# The children inherit the Meta class of their parents (if they don't specify
+# their own).
+>>> Worker.objects.values('name')
+[{'name': u'Barney'}, {'name': u'Fred'}]
+
+# Since Student does not subclass CommonInfo's Meta, it has the effect of
+# completely overriding it. So ordering by name doesn't take place for Students.
+>>> Student._meta.ordering
+[]
+
# However, the CommonInfo class cannot be used as a normal model (it doesn't
# exist as a model).
>>> CommonInfo.objects.all()
@@ -96,7 +118,7 @@ AttributeError: type object 'CommonInfo' has no attribute 'objects'
>>> p2.save()
Test constructor for Restaurant.
->>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton', serves_hot_dogs=True, serves_pizza=False)
+>>> r = Restaurant(name='Demon Dogs', address='944 W. Fullerton',serves_hot_dogs=True, serves_pizza=False, rating=2)
>>> r.save()
# Test the constructor for ItalianRestaurant.
@@ -106,9 +128,9 @@ Test constructor for Restaurant.
# Make sure Restaurant and ItalianRestaurant have the right fields in the right
# order.
>>> [f.name for f in Restaurant._meta.fields]
-['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza']
+['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza']
>>> [f.name for f in ItalianRestaurant._meta.fields]
-['id', 'name', 'address', 'place_ptr', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi']
+['id', 'name', 'address', 'place_ptr', 'rating', 'serves_hot_dogs', 'serves_pizza', 'restaurant_ptr', 'serves_gnocchi']
# Even though p.supplier for a Place 'p' (a parent of a Supplier), a Restaurant
# object cannot access that reverse relation, since it's not part of the
@@ -118,7 +140,7 @@ Test constructor for Restaurant.
>>> Restaurant.objects.filter(supplier__name='foo')
Traceback (most recent call last):
...
-TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, serves_hot_dogs, serves_pizza
+TypeError: Cannot resolve keyword 'supplier' into field. Choices are: address, id, italianrestaurant, lot, name, place_ptr, provider, rating, serves_hot_dogs, serves_pizza
# Parent fields can be used directly in filters on the child model.
>>> Restaurant.objects.filter(name='Demon Dogs')