diff --git a/docs/apireference.rst b/docs/apireference.rst index 7ba93408..625d4a8b 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -34,6 +34,9 @@ Documents .. autoclass:: mongoengine.ValidationError :members: +.. autoclass:: mongoengine.FieldDoesNotExist + + Context Managers ================ diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 33a6d826..d31d75ba 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -70,9 +70,9 @@ class BaseDocument(object): signals.pre_init.send(self.__class__, document=self, values=values) - # Check if there are undefined fields supplied, if so raise an - # Exception. - if not self._dynamic: + # Check if there are undefined fields supplied to the constructor, + # if so raise an Exception. + if not self._dynamic and (self._meta.get('strict', True) or _created): for var in values.keys(): if var not in self._fields.keys() + ['id', 'pk', '_cls', '_text_score']: msg = ( diff --git a/mongoengine/document.py b/mongoengine/document.py index eea5dabc..f8275021 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -135,6 +135,11 @@ class Document(BaseDocument): doesn't contain a list) if allow_inheritance is True. This can be disabled by either setting cls to False on the specific index or by setting index_cls to False on the meta dictionary for the document. + + By default, any extra attribute existing in stored data but not declared + in your model will raise a :class:`~mongoengine.FieldDoesNotExist` error. + This can be disabled by setting :attr:`strict` to ``False`` + in the :attr:`meta` dictionnary. """ # The __metaclass__ attribute is removed by 2to3 when running with Python3 diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 6cde7771..a411ac47 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -42,7 +42,14 @@ class NotUniqueError(OperationError): class FieldDoesNotExist(Exception): - pass + """Raised when trying to set a field + not declared in a :class:`~mongoengine.Document` + or an :class:`~mongoengine.EmbeddedDocument`. + + To avoid this behavior on data loading, + you should the :attr:`strict` to ``False`` + in the :attr:`meta` dictionnary. + """ class ValidationError(AssertionError): diff --git a/tests/document/instance.py b/tests/document/instance.py index 10e38d49..2cfdef65 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -16,7 +16,8 @@ from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, from mongoengine import * from mongoengine.errors import (NotRegistered, InvalidDocumentError, - InvalidQueryError, NotUniqueError) + InvalidQueryError, NotUniqueError, + FieldDoesNotExist) from mongoengine.queryset import NULLIFY, Q from mongoengine.connection import get_db from mongoengine.base import get_document @@ -2467,6 +2468,114 @@ class InstanceTest(unittest.TestCase): group = Group.objects.first() self.assertEqual("hello - default", group.name) + def test_load_undefined_fields(self): + class User(Document): + name = StringField() + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'foo': 'Bar', + 'data': [1, 2, 3] + }) + + self.assertRaises(FieldDoesNotExist, User.objects.first) + + def test_load_undefined_fields_with_strict_false(self): + class User(Document): + name = StringField() + + meta = {'strict': False} + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'foo': 'Bar', + 'data': [1, 2, 3] + }) + + user = User.objects.first() + self.assertEqual(user.name, 'John') + self.assertFalse(hasattr(user, 'foo')) + self.assertEqual(user._data['foo'], 'Bar') + self.assertFalse(hasattr(user, 'data')) + self.assertEqual(user._data['data'], [1, 2, 3]) + + def test_load_undefined_fields_on_embedded_document(self): + class Thing(EmbeddedDocument): + name = StringField() + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + self.assertRaises(FieldDoesNotExist, User.objects.first) + + def test_load_undefined_fields_on_embedded_document_with_strict_false_on_doc(self): + class Thing(EmbeddedDocument): + name = StringField() + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + meta = {'strict': False} + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + self.assertRaises(FieldDoesNotExist, User.objects.first) + + def test_load_undefined_fields_on_embedded_document_with_strict_false(self): + class Thing(EmbeddedDocument): + name = StringField() + + meta = {'strict': False} + + class User(Document): + name = StringField() + thing = EmbeddedDocumentField(Thing) + + User.drop_collection() + + User._get_collection().save({ + 'name': 'John', + 'thing': { + 'name': 'My thing', + 'foo': 'Bar', + 'data': [1, 2, 3] + } + }) + + user = User.objects.first() + self.assertEqual(user.name, 'John') + self.assertEqual(user.thing.name, 'My thing') + self.assertFalse(hasattr(user.thing, 'foo')) + self.assertEqual(user.thing._data['foo'], 'Bar') + self.assertFalse(hasattr(user.thing, 'data')) + self.assertEqual(user.thing._data['data'], [1, 2, 3]) + def test_spaces_in_keys(self): class Embedded(DynamicEmbeddedDocument): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 96686a8b..e7a87ed0 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -3195,7 +3195,7 @@ class FieldTest(unittest.TestCase): def test_undefined_field_exception(self): """Tests if a `FieldDoesNotExist` exception is raised when trying to - set a value to a field that's not defined. + instanciate a document with a field that's not defined. """ class Doc(Document): @@ -3206,6 +3206,21 @@ class FieldTest(unittest.TestCase): self.assertRaises(FieldDoesNotExist, test) + def test_undefined_field_exception_with_strict(self): + """Tests if a `FieldDoesNotExist` exception is raised when trying to + instanciate a document with a field that's not defined, + even when strict is set to False. + """ + + class Doc(Document): + foo = StringField(db_field='f') + meta = {'strict': False} + + def test(): + Doc(bar='test') + + self.assertRaises(FieldDoesNotExist, test) + class EmbeddedDocumentListFieldTestCase(unittest.TestCase):