diff --git a/mongoengine/base.py b/mongoengine/base.py index 77c2d7d1..ffceb794 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -253,7 +253,16 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): # __metaclass__ is only set on the class with the __metaclass__ # attribute (i.e. it is not set on subclasses). This differentiates # 'real' documents from the 'Document' class - if attrs.get('__metaclass__') == TopLevelDocumentMetaclass: + # + # Also assume a class is abstract if it has abstract set to True in + # its meta dictionary. This allows custom Document superclasses. + if (attrs.get('__metaclass__') == TopLevelDocumentMetaclass or + ('meta' in attrs and attrs['meta'].get('abstract', False))): + # Make sure no base class was non-abstract + non_abstract_bases = [b for b in bases + if hasattr(b,'_meta') and not b._meta.get('abstract', False)] + if non_abstract_bases: + raise ValueError("Abstract document cannot have non-abstract base") return super_new(cls, name, bases, attrs) collection = name.lower() @@ -276,6 +285,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): base_indexes += base._meta.get('indexes', []) meta = { + 'abstract': False, 'collection': collection, 'max_documents': None, 'max_size': None, diff --git a/tests/document.py b/tests/document.py index 8f47ec3c..fe67312e 100644 --- a/tests/document.py +++ b/tests/document.py @@ -29,6 +29,9 @@ class DocumentTest(unittest.TestCase): age = IntField() self.Person = Person + def tearDown(self): + self.Person.drop_collection() + def test_drop_collection(self): """Ensure that the collection may be dropped from the database. """ @@ -188,6 +191,34 @@ class DocumentTest(unittest.TestCase): self.assertFalse('_cls' in comment.to_mongo()) self.assertFalse('_types' in comment.to_mongo()) + def test_abstract_documents(self): + """Ensure that a document superclass can be marked as abstract + thereby not using it as the name for the collection.""" + + class Animal(Document): + name = StringField() + meta = {'abstract': True} + + class Fish(Animal): pass + class Guppy(Fish): pass + + class Mammal(Animal): + meta = {'abstract': True} + class Human(Mammal): pass + + self.assertFalse('collection' in Animal._meta) + self.assertFalse('collection' in Mammal._meta) + + self.assertEqual(Fish._meta['collection'], 'fish') + self.assertEqual(Guppy._meta['collection'], 'fish') + self.assertEqual(Human._meta['collection'], 'human') + + def create_bad_abstract(): + class EvilHuman(Human): + evil = BooleanField(default=True) + meta = {'abstract': True} + self.assertRaises(ValueError, create_bad_abstract) + def test_collection_name(self): """Ensure that a collection with a specified name may be used. """ @@ -907,9 +938,6 @@ class DocumentTest(unittest.TestCase): A.drop_collection() B.drop_collection() - def tearDown(self): - self.Person.drop_collection() - def test_document_hash(self): """Test document in list, dict, set """