From 8f4a579df93f17a301eae23f72a7d3f5f6332ac5 Mon Sep 17 00:00:00 2001 From: Deepak Thukral Date: Sun, 28 Mar 2010 22:22:36 +0200 Subject: [PATCH 1/7] DoesNotExist and MultipleObjectsReturned now contributes Document class --- mongoengine/base.py | 29 +++++++++++++++++++++++++++-- mongoengine/queryset.py | 7 ++++--- tests/queryset.py | 7 +++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/mongoengine/base.py b/mongoengine/base.py index 0a904467..fbd95b3c 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -1,7 +1,9 @@ -from queryset import QuerySet, QuerySetManager - +import sys import pymongo +from queryset import QuerySet, QuerySetManager +from queryset import DoesNotExist, MultipleObjectsReturned + _document_registry = {} @@ -167,7 +169,22 @@ class DocumentMetaclass(type): for field in new_class._fields.values(): field.owner_document = new_class + module = attrs.pop('__module__') + + new_class.add_to_class('DoesNotExist', subclass_exception('DoesNotExist', + tuple(x.DoesNotExist + for k,x in superclasses.items()) + or (DoesNotExist,), module)) + + new_class.add_to_class('MultipleObjectsReturned', subclass_exception('MultipleObjectsReturned', + tuple(x.MultipleObjectsReturned + for k,x in superclasses.items()) + or (MultipleObjectsReturned,), module)) return new_class + + + def add_to_class(self, name, value): + setattr(self, name, value) class TopLevelDocumentMetaclass(DocumentMetaclass): @@ -417,3 +434,11 @@ class BaseDocument(object): if self.id == other.id: return True return False + +if sys.version_info < (2, 5): + # Prior to Python 2.5, Exception was an old-style class + def subclass_exception(name, parents, unused): + return types.ClassType(name, parents, {}) +else: + def subclass_exception(name, parents, module): + return type(name, parents, {'__module__': module}) \ No newline at end of file diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 11dc2bcb..0623719d 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -352,9 +352,10 @@ class QuerySet(object): return self[0] elif count > 1: message = u'%d items returned, instead of 1' % count - raise MultipleObjectsReturned(message) + raise self._document.MultipleObjectsReturned(message) else: - raise DoesNotExist('Document not found') + raise self._document.DoesNotExist("%s matching query does not exist." + % self._document._class_name) def get_or_create(self, *q_objs, **query): """Retreive unique object or create, if it doesn't exist. Raises @@ -380,7 +381,7 @@ class QuerySet(object): return self.first() else: message = u'%d items returned, instead of 1' % count - raise MultipleObjectsReturned(message) + raise self._document.MultipleObjectsReturned(message) def first(self): """Retrieve the first object matching the query. diff --git a/tests/queryset.py b/tests/queryset.py index c0bd8fa9..668f0355 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -147,6 +147,7 @@ class QuerySetTest(unittest.TestCase): """ # Try retrieving when no objects exists self.assertRaises(DoesNotExist, self.Person.objects.get) + self.assertRaises(self.Person.DoesNotExist, self.Person.objects.get) person1 = self.Person(name="User A", age=20) person1.save() @@ -155,6 +156,7 @@ class QuerySetTest(unittest.TestCase): # Retrieve the first person from the database self.assertRaises(MultipleObjectsReturned, self.Person.objects.get) + self.assertRaises(self.Person.MultipleObjectsReturned, self.Person.objects.get) # Use a query to filter the people found to just person2 person = self.Person.objects.get(age=30) @@ -162,6 +164,9 @@ class QuerySetTest(unittest.TestCase): person = self.Person.objects.get(age__lt=30) self.assertEqual(person.name, "User A") + + + def test_get_or_create(self): """Ensure that ``get_or_create`` returns one result or creates a new @@ -175,6 +180,8 @@ class QuerySetTest(unittest.TestCase): # Retrieve the first person from the database self.assertRaises(MultipleObjectsReturned, self.Person.objects.get_or_create) + self.assertRaises(self.Person.MultipleObjectsReturned, + self.Person.objects.get_or_create) # Use a query to filter the people found to just person2 person = self.Person.objects.get_or_create(age=30) From fbcf58c48f84412e5ebe38b20488cdd64a062b56 Mon Sep 17 00:00:00 2001 From: Deepak Thukral Date: Mon, 29 Mar 2010 11:25:17 +0200 Subject: [PATCH 2/7] updated documentation --- mongoengine/queryset.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 0623719d..6c162070 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -15,7 +15,6 @@ REPR_OUTPUT_SIZE = 20 class DoesNotExist(Exception): pass - class MultipleObjectsReturned(Exception): pass @@ -27,6 +26,8 @@ class InvalidQueryError(Exception): class OperationError(Exception): pass +class InvalidCollectionError(Exception): + pass RE_TYPE = type(re.compile('')) @@ -341,8 +342,9 @@ class QuerySet(object): def get(self, *q_objs, **query): """Retrieve the the matching object raising :class:`~mongoengine.queryset.MultipleObjectsReturned` or - :class:`~mongoengine.queryset.DoesNotExist` exceptions if multiple or - no results are found. + `DocumentName.MultipleObjectsReturned` exception if multiple results and + :class:`~mongoengine.queryset.DoesNotExist` or `DocumentName.DoesNotExist` + if no results are found. .. versionadded:: 0.3 """ @@ -359,10 +361,11 @@ class QuerySet(object): def get_or_create(self, *q_objs, **query): """Retreive unique object or create, if it doesn't exist. Raises - :class:`~mongoengine.queryset.MultipleObjectsReturned` if multiple - results are found. A new document will be created if the document - doesn't exists; a dictionary of default values for the new document - may be provided as a keyword argument called :attr:`defaults`. + :class:`~mongoengine.queryset.MultipleObjectsReturned` or + `DocumentName.MultipleObjectsReturned` if multiple results are found. + A new document will be created if the document doesn't exists; a + dictionary of default values for the new document may be provided as a + keyword argument called :attr:`defaults`. .. versionadded:: 0.3 """ @@ -870,10 +873,6 @@ class QuerySet(object): return repr(data) -class InvalidCollectionError(Exception): - pass - - class QuerySetManager(object): def __init__(self, manager_func=None): From 207fd9fcb74f53757d3b445a88a19d58eee355cf Mon Sep 17 00:00:00 2001 From: Deepak Thukral Date: Mon, 29 Mar 2010 11:27:50 +0200 Subject: [PATCH 3/7] keeping import policy in mind --- mongoengine/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/base.py b/mongoengine/base.py index fbd95b3c..55323ddc 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -1,9 +1,9 @@ -import sys -import pymongo - from queryset import QuerySet, QuerySetManager from queryset import DoesNotExist, MultipleObjectsReturned +import sys +import pymongo + _document_registry = {} From 90200dbe9ce2d6ced0fa5fb0b3b5cd274015a616 Mon Sep 17 00:00:00 2001 From: Harry Marr Date: Mon, 12 Apr 2010 15:59:20 +0100 Subject: [PATCH 4/7] Fixed DecimalField bug --- mongoengine/fields.py | 3 +++ tests/fields.py | 10 ++++++++-- tests/queryset.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 9a9f4e0e..94a1e242 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -155,6 +155,9 @@ class DecimalField(BaseField): if not isinstance(value, basestring): value = unicode(value) return decimal.Decimal(value) + + def to_mongo(self, value): + return unicode(value) def validate(self, value): if not isinstance(value, decimal.Decimal): diff --git a/tests/fields.py b/tests/fields.py index 986f0e26..24acc02e 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -136,12 +136,16 @@ class FieldTest(unittest.TestCase): height = DecimalField(min_value=Decimal('0.1'), max_value=Decimal('3.5')) + Person.drop_collection() + person = Person() person.height = Decimal('1.89') - person.validate() + person.save() + person.reload() + self.assertEqual(person.height, Decimal('1.89')) person.height = '2.0' - person.validate() + person.save() person.height = 0.01 self.assertRaises(ValidationError, person.validate) person.height = Decimal('0.01') @@ -149,6 +153,8 @@ class FieldTest(unittest.TestCase): person.height = Decimal('4.0') self.assertRaises(ValidationError, person.validate) + Person.drop_collection() + def test_boolean_validation(self): """Ensure that invalid values cannot be assigned to boolean fields. """ diff --git a/tests/queryset.py b/tests/queryset.py index 668f0355..e1fa6f45 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -623,6 +623,27 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + def test_update_pull(self): + """Ensure that the 'pull' update operation works correctly. + """ + class Comment(EmbeddedDocument): + content = StringField() + + class BlogPost(Document): + slug = StringField() + comments = ListField(EmbeddedDocumentField(Comment)) + + comment1 = Comment(content="test1") + comment2 = Comment(content="test2") + + post = BlogPost(slug="test", comments=[comment1, comment2]) + post.save() + self.assertTrue(comment2 in post.comments) + + BlogPost.objects(slug="test").update(pull__comments__content="test2") + post.reload() + self.assertTrue(comment2 not in post.comments) + def test_order_by(self): """Ensure that QuerySets may be ordered. """ From a39685d98cb43b7ec30cd1c8aa0873b7186e390b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Sun, 11 Apr 2010 20:14:32 +0200 Subject: [PATCH 5/7] make get_or_create returns a tuple with the retrieved or created object and a boolean specifying whether a new object was created --- docs/guide/querying.rst | 4 ++-- mongoengine/queryset.py | 8 +++++--- tests/queryset.py | 15 +++++++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index eba75d7d..bad8b340 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -135,8 +135,8 @@ additional keyword argument, :attr:`defaults` may be provided, which will be used as default values for the new document, in the case that it should need to be created:: - >>> a = User.objects.get_or_create(name='User A', defaults={'age': 30}) - >>> b = User.objects.get_or_create(name='User A', defaults={'age': 40}) + >>> a, created = User.objects.get_or_create(name='User A', defaults={'age': 30}) + >>> b, created = User.objects.get_or_create(name='User A', defaults={'age': 40}) >>> a.name == b.name and a.age == b.age True diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 6c162070..43b6eee8 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -360,7 +360,9 @@ class QuerySet(object): % self._document._class_name) def get_or_create(self, *q_objs, **query): - """Retreive unique object or create, if it doesn't exist. Raises + """Retrieve unique object or create, if it doesn't exist. Returns a tuple of + ``(object, created)``, where ``object`` is the retrieved or created object + and ``created`` is a boolean specifying whether a new object was created. Raises :class:`~mongoengine.queryset.MultipleObjectsReturned` or `DocumentName.MultipleObjectsReturned` if multiple results are found. A new document will be created if the document doesn't exists; a @@ -379,9 +381,9 @@ class QuerySet(object): query.update(defaults) doc = self._document(**query) doc.save() - return doc + return doc, True elif count == 1: - return self.first() + return self.first(), False else: message = u'%d items returned, instead of 1' % count raise self._document.MultipleObjectsReturned(message) diff --git a/tests/queryset.py b/tests/queryset.py index e1fa6f45..aba3bc7d 100644 --- a/tests/queryset.py +++ b/tests/queryset.py @@ -184,15 +184,18 @@ class QuerySetTest(unittest.TestCase): self.Person.objects.get_or_create) # Use a query to filter the people found to just person2 - person = self.Person.objects.get_or_create(age=30) + person, created = self.Person.objects.get_or_create(age=30) self.assertEqual(person.name, "User B") - - person = self.Person.objects.get_or_create(age__lt=30) + self.assertEqual(created, False) + + person, created = self.Person.objects.get_or_create(age__lt=30) self.assertEqual(person.name, "User A") - + self.assertEqual(created, False) + # Try retrieving when no objects exists - new doc should be created - self.Person.objects.get_or_create(age=50, defaults={'name': 'User C'}) - + person, created = self.Person.objects.get_or_create(age=50, defaults={'name': 'User C'}) + self.assertEqual(created, True) + person = self.Person.objects.get(age=50) self.assertEqual(person.name, "User C") From c8e466a160e1eeae20519dbf52dbfc6051c80dc6 Mon Sep 17 00:00:00 2001 From: Josh Ourisman Date: Mon, 12 Apr 2010 12:31:52 -0400 Subject: [PATCH 6/7] Moved SortedListField stuff into its own branch --- mongoengine/fields.py | 20 +++++++++++++++++++- tests/fields.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 94a1e242..90d9e5c1 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1,6 +1,7 @@ from base import BaseField, ObjectIdField, ValidationError, get_document from document import Document, EmbeddedDocument from connection import _get_db +from operator import itemgetter import re import pymongo @@ -12,7 +13,7 @@ __all__ = ['StringField', 'IntField', 'FloatField', 'BooleanField', 'DateTimeField', 'EmbeddedDocumentField', 'ListField', 'DictField', 'ObjectIdField', 'ReferenceField', 'ValidationError', 'DecimalField', 'URLField', 'GenericReferenceField', - 'BinaryField'] + 'BinaryField', 'SortedListField'] RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -310,6 +311,23 @@ class ListField(BaseField): def lookup_member(self, member_name): return self.field.lookup_member(member_name) +class SortedListField(ListField): + """A ListField that sorts the contents of its list before writing to + the database in order to ensure that a sorted list is always + retrieved. + """ + + _ordering = None + + def __init__(self, field, **kwargs): + if 'ordering' in kwargs.keys(): + self._ordering = kwargs.pop('ordering') + super(SortedListField, self).__init__(field, **kwargs) + + def to_mongo(self, value): + if self._ordering is not None: + return sorted([self.field.to_mongo(item) for item in value], key=itemgetter(self._ordering)) + return sorted([self.field.to_mongo(item) for item in value]) class DictField(BaseField): """A dictionary field that wraps a standard Python dictionary. This is diff --git a/tests/fields.py b/tests/fields.py index 24acc02e..7e68155c 100644 --- a/tests/fields.py +++ b/tests/fields.py @@ -218,6 +218,37 @@ class FieldTest(unittest.TestCase): post.comments = 'yay' self.assertRaises(ValidationError, post.validate) + def test_sorted_list_sorting(self): + """Ensure that a sorted list field properly sorts values. + """ + class Comment(EmbeddedDocument): + order = IntField() + content = StringField() + + class BlogPost(Document): + content = StringField() + comments = SortedListField(EmbeddedDocumentField(Comment), ordering='order') + tags = SortedListField(StringField()) + + post = BlogPost(content='Went for a walk today...') + post.save() + + post.tags = ['leisure', 'fun'] + post.save() + post.reload() + self.assertEqual(post.tags, ['fun', 'leisure']) + + comment1 = Comment(content='Good for you', order=1) + comment2 = Comment(content='Yay.', order=0) + comments = [comment1, comment2] + post.comments = comments + post.save() + post.reload() + self.assertEqual(post.comments[0].content, comment2.content) + self.assertEqual(post.comments[1].content, comment1.content) + + BlogPost.drop_collection() + def test_dict_validation(self): """Ensure that dict types work as expected. """ From da3f4c30e29571bc5db4d5c6f8cd8959d36b18bd Mon Sep 17 00:00:00 2001 From: Don Spaulding Date: Wed, 14 Apr 2010 22:40:56 -0500 Subject: [PATCH 7/7] Fix doc typos --- docs/guide/defining-documents.rst | 2 +- docs/guide/querying.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index b5655c7d..d38e6616 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -178,7 +178,7 @@ either a single field name, or a list or tuple of field names:: class User(Document): username = StringField(unique=True) first_name = StringField() - last_name = StringField(unique_with='last_name') + last_name = StringField(unique_with='first_name') Document collections ==================== diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index bad8b340..4447f7da 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -172,7 +172,7 @@ custom manager methods as you like:: @queryset_manager def live_posts(doc_cls, queryset): - return queryset.order_by('-date') + return queryset.filter(published=True) BlogPost(title='test1', published=False).save() BlogPost(title='test2', published=True).save()