diff --git a/AUTHORS b/AUTHORS index 35383123..22375f28 100644 --- a/AUTHORS +++ b/AUTHORS @@ -115,4 +115,5 @@ that much better: * Jaime Irurzun * Alexandre González * Thomas Steinacher - * Tommi Komulainen \ No newline at end of file + * Tommi Komulainen + * Peter Landry diff --git a/README.rst b/README.rst index 548737cd..1305b6e9 100644 --- a/README.rst +++ b/README.rst @@ -2,6 +2,7 @@ MongoEngine =========== :Info: MongoEngine is an ORM-like layer on top of PyMongo. +:Repository: https://github.com/MongoEngine/mongoengine :Author: Harry Marr (http://github.com/hmarr) :Maintainer: Ross Lawley (http://github.com/rozza) diff --git a/docs/changelog.rst b/docs/changelog.rst index 08091acb..c05da946 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,8 +2,14 @@ Changelog ========= -Changes in 0.6.X -================ +Changes in 0.6.20 +================= +- Added support for distinct and db_alias (MongoEngine/mongoengine#59) +- Improved support for chained querysets when constraining the same fields (hmarr/mongoengine#554) +- Fixed BinaryField lookup re (MongoEngine/mongoengine#48) + +Changes in 0.6.19 +================= - Added Binary support to UUID (MongoEngine/mongoengine#47) - Fixed MapField lookup for fields without declared lookups (MongoEngine/mongoengine#46) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index a9567e20..14498017 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -232,7 +232,7 @@ custom manager methods as you like:: BlogPost(title='test1', published=False).save() BlogPost(title='test2', published=True).save() assert len(BlogPost.objects) == 2 - assert len(BlogPost.live_posts) == 1 + assert len(BlogPost.live_posts()) == 1 Custom QuerySets ================ @@ -243,11 +243,16 @@ a document, set ``queryset_class`` to the custom class in a :class:`~mongoengine.Document`\ s ``meta`` dictionary:: class AwesomerQuerySet(QuerySet): - pass + + def get_awesome(self): + return self.filter(awesome=True) class Page(Document): meta = {'queryset_class': AwesomerQuerySet} + # To call: + Page.objects.get_awesome() + .. versionadded:: 0.4 Aggregation diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 5f4f7f1d..ea3dd5e6 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -12,7 +12,7 @@ from signals import * __all__ = (document.__all__ + fields.__all__ + connection.__all__ + queryset.__all__ + signals.__all__) -VERSION = (0, 6, 18) +VERSION = (0, 6, 20) def get_version(): diff --git a/mongoengine/base.py b/mongoengine/base.py index d4a7b324..3d7a0c4a 100644 --- a/mongoengine/base.py +++ b/mongoengine/base.py @@ -1,5 +1,5 @@ import operator -import sys +import sys import warnings from collections import defaultdict @@ -16,7 +16,6 @@ import pymongo from bson import ObjectId from bson.dbref import DBRef - class NotRegistered(Exception): pass @@ -112,6 +111,7 @@ class ValidationError(AssertionError): _document_registry = {} +_module_registry = {} def get_document(name): @@ -199,7 +199,8 @@ class BaseField(object): """Descriptor for assigning a value to a field in a document. """ instance._data[self.name] = value - instance._mark_as_changed(self.name) + if instance._initialised: + instance._mark_as_changed(self.name) def error(self, message="", errors=None, field_name=None): """Raises a ValidationError. @@ -264,7 +265,7 @@ class ComplexBaseField(BaseField): """ field = None - _dereference = False + __dereference = False def __get__(self, instance, owner): """Descriptor to automatically dereference references. @@ -276,8 +277,6 @@ class ComplexBaseField(BaseField): dereference = self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)) if not self._dereference and instance._initialised and dereference: - from dereference import DeReference - self._dereference = DeReference() # Cached instance._data[self.name] = self._dereference( instance._data.get(self.name), max_depth=1, instance=instance, name=self.name @@ -293,14 +292,13 @@ class ComplexBaseField(BaseField): value = BaseDict(value, instance, self.name) instance._data[self.name] = value - if self._dereference and instance._initialised and \ - isinstance(value, (BaseList, BaseDict)) and not value._dereferenced: + if (instance._initialised and isinstance(value, (BaseList, BaseDict)) + and not value._dereferenced): value = self._dereference( value, max_depth=1, instance=instance, name=self.name ) value._dereferenced = True instance._data[self.name] = value - return value def __set__(self, instance, value): @@ -441,6 +439,13 @@ class ComplexBaseField(BaseField): owner_document = property(_get_owner_document, _set_owner_document) + @property + def _dereference(self,): + if not self.__dereference: + from dereference import DeReference + self.__dereference = DeReference() # Cached + return self.__dereference + class ObjectIdField(BaseField): """An field wrapper around MongoDB's ObjectIds. @@ -473,6 +478,7 @@ class DocumentMetaclass(type): """ def __new__(cls, name, bases, attrs): + def _get_mixin_fields(base): attrs = {} attrs.update(dict([(k, v) for k, v in base.__dict__.items() @@ -501,9 +507,7 @@ class DocumentMetaclass(type): class_name = [name] superclasses = {} simple_class = True - for base in bases: - # Include all fields present in superclasses if hasattr(base, '_fields'): doc_fields.update(base._fields) @@ -543,20 +547,18 @@ class DocumentMetaclass(type): if not simple_class and not meta['allow_inheritance'] and not meta['abstract']: raise ValueError('Only direct subclasses of Document may set ' '"allow_inheritance" to False') - attrs['_meta'] = meta - attrs['_class_name'] = doc_class_name - attrs['_superclasses'] = superclasses # Add the document's fields to the _fields attribute field_names = {} - for attr_name, attr_value in attrs.items(): - if hasattr(attr_value, "__class__") and \ - issubclass(attr_value.__class__, BaseField): - attr_value.name = attr_name - if not attr_value.db_field: - attr_value.db_field = attr_name - doc_fields[attr_name] = attr_value - field_names[attr_value.db_field] = field_names.get(attr_value.db_field, 0) + 1 + for attr_name, attr_value in attrs.iteritems(): + if not isinstance(attr_value, BaseField): + continue + attr_value.name = attr_name + if not attr_value.db_field: + attr_value.db_field = attr_name + doc_fields[attr_name] = attr_value + + field_names[attr_value.db_field] = field_names.get(attr_value.db_field, 0) + 1 duplicate_db_fields = [k for k, v in field_names.items() if v > 1] if duplicate_db_fields: @@ -564,11 +566,24 @@ class DocumentMetaclass(type): attrs['_fields'] = doc_fields attrs['_db_field_map'] = dict([(k, v.db_field) for k, v in doc_fields.items() if k != v.db_field]) attrs['_reverse_db_field_map'] = dict([(v, k) for k, v in attrs['_db_field_map'].items()]) + attrs['_meta'] = meta + attrs['_class_name'] = doc_class_name + attrs['_superclasses'] = superclasses - from mongoengine import Document, EmbeddedDocument, DictField + if 'Document' not in _module_registry: + from mongoengine.document import Document, EmbeddedDocument + from mongoengine.fields import DictField + _module_registry['Document'] = Document + _module_registry['EmbeddedDocument'] = EmbeddedDocument + _module_registry['DictField'] = DictField + else: + Document = _module_registry.get('Document') + EmbeddedDocument = _module_registry.get('EmbeddedDocument') + DictField = _module_registry.get('DictField') new_class = super_new(cls, name, bases, attrs) - for field in new_class._fields.values(): + + for field in new_class._fields.itervalues(): field.owner_document = new_class delete_rule = getattr(field, 'reverse_delete_rule', DO_NOTHING) @@ -605,12 +620,12 @@ class DocumentMetaclass(type): global _document_registry _document_registry[doc_class_name] = new_class - # in Python 2, User-defined methods objects have special read-only - # attributes 'im_func' and 'im_self' which contain the function obj + # in Python 2, User-defined methods objects have special read-only + # attributes 'im_func' and 'im_self' which contain the function obj # and class instance object respectively. With Python 3 these special # attributes have been replaced by __func__ and __self__. The Blinker # module continues to use im_func and im_self, so the code below - # copies __func__ into im_func and __self__ into im_self for + # copies __func__ into im_func and __self__ into im_self for # classmethod objects in Document derived classes. if PY3: for key, val in new_class.__dict__.items(): @@ -738,7 +753,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): unique_indexes = cls._unique_with_indexes(new_class) new_class._meta['unique_indexes'] = unique_indexes - for field_name, field in new_class._fields.items(): + for field_name, field in new_class._fields.iteritems(): # Check for custom primary key if field.primary_key: current_pk = new_class._meta['id_field'] @@ -809,21 +824,23 @@ class BaseDocument(object): self._data = {} # Assign default values to instance - for attr_name, field in self._fields.items(): - value = getattr(self, attr_name, None) - setattr(self, attr_name, value) + for key, field in self._fields.iteritems(): + if self._db_field_map.get(key, key) in values: + continue + value = getattr(self, key, None) + setattr(self, key, value) # Set passed values after initialisation if self._dynamic: self._dynamic_fields = {} dynamic_data = {} - for key, value in values.items(): + for key, value in values.iteritems(): if key in self._fields or key == '_id': setattr(self, key, value) elif self._dynamic: dynamic_data[key] = value else: - for key, value in values.items(): + for key, value in values.iteritems(): key = self._reverse_db_field_map.get(key, key) setattr(self, key, value) @@ -832,7 +849,7 @@ class BaseDocument(object): if self._dynamic: self._dynamic_lock = False - for key, value in dynamic_data.items(): + for key, value in dynamic_data.iteritems(): setattr(self, key, value) # Flag initialised diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index f74e224c..637380d6 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -34,7 +34,9 @@ class DeReference(object): doc_type = None if instance and instance._fields: - doc_type = instance._fields[name].field + doc_type = instance._fields[name] + if hasattr(doc_type, 'field'): + doc_type = doc_type.field if isinstance(doc_type, ReferenceField): doc_type = doc_type.document_type diff --git a/mongoengine/fields.py b/mongoengine/fields.py index da59fb12..fa960232 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1333,7 +1333,7 @@ class UUIDField(BaseField): super(UUIDField, self).__init__(**kwargs) def to_python(self, value): - if not self.binary: + if not self._binary: if not isinstance(value, basestring): value = unicode(value) return uuid.UUID(value) diff --git a/mongoengine/queryset.py b/mongoengine/queryset.py index 1067e32e..2e609122 100644 --- a/mongoengine/queryset.py +++ b/mongoengine/queryset.py @@ -329,6 +329,7 @@ class QuerySet(object): """ __already_indexed = set() + __dereference = False def __init__(self, document, collection): self._document = document @@ -600,7 +601,6 @@ class QuerySet(object): if self._hint != -1: self._cursor_obj.hint(self._hint) - return self._cursor_obj @classmethod @@ -765,8 +765,22 @@ class QuerySet(object): key = '.'.join(parts) if op is None or key not in mongo_query: mongo_query[key] = value - elif key in mongo_query and isinstance(mongo_query[key], dict): - mongo_query[key].update(value) + elif key in mongo_query: + if isinstance(mongo_query[key], dict) and isinstance(value, dict): + mongo_query[key].update(value) + elif isinstance(mongo_query[key], list): + mongo_query[key].append(value) + else: + mongo_query[key] = [mongo_query[key], value] + + for k, v in mongo_query.items(): + if isinstance(v, list): + value = [{k:val} for val in v] + if '$and' in mongo_query.keys(): + mongo_query['$and'].append(value) + else: + mongo_query['$and'] = value + del mongo_query[k] return mongo_query @@ -1152,9 +1166,10 @@ class QuerySet(object): .. versionadded:: 0.4 .. versionchanged:: 0.5 - Fixed handling references + .. versionchanged:: 0.6 - Improved db_field refrence handling """ - from dereference import DeReference - return DeReference()(self._cursor.distinct(field), 1) + return self._dereference(self._cursor.distinct(field), 1, + name=field, instance=self._document) def only(self, *fields): """Load only a subset of this document's fields. :: @@ -1854,13 +1869,30 @@ class QuerySet(object): .. versionadded:: 0.5 """ - from dereference import DeReference # Make select related work the same for querysets max_depth += 1 - return DeReference()(self, max_depth=max_depth) + return self._dereference(self, max_depth=max_depth) + + @property + def _dereference(self): + if not self.__dereference: + from dereference import DeReference + self.__dereference = DeReference() # Cached + return self.__dereference class QuerySetManager(object): + """ + The default QuerySet Manager. + + Custom QuerySet Manager functions can extend this class and users can + add extra queryset functionality. Any custom manager methods must accept a + :class:`~mongoengine.Document` class as its first argument, and a + :class:`~mongoengine.queryset.QuerySet` as its second argument. + + The method function should return a :class:`~mongoengine.queryset.QuerySet` + , probably the same one that was passed in, but modified in some way. + """ get_queryset = None diff --git a/python-mongoengine.spec b/python-mongoengine.spec index 4dfbeccc..4e803263 100644 --- a/python-mongoengine.spec +++ b/python-mongoengine.spec @@ -5,7 +5,7 @@ %define srcname mongoengine Name: python-%{srcname} -Version: 0.6.18 +Version: 0.6.20 Release: 1%{?dist} Summary: A Python Document-Object Mapper for working with MongoDB diff --git a/tests/test_fields.py b/tests/test_fields.py index dca4f21e..271e3a10 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -284,6 +284,7 @@ class FieldTest(unittest.TestCase): uu = uuid.uuid4() Person(api_key=uu).save() self.assertEqual(1, Person.objects(api_key=uu).count()) + self.assertEqual(uu, Person.objects.first().api_key) person = Person() valid = (uuid.uuid4(), uuid.uuid1()) @@ -308,6 +309,7 @@ class FieldTest(unittest.TestCase): uu = uuid.uuid4() Person(api_key=uu).save() self.assertEqual(1, Person.objects(api_key=uu).count()) + self.assertEqual(uu, Person.objects.first().api_key) person = Person() valid = (uuid.uuid4(), uuid.uuid1()) diff --git a/tests/test_queryset.py b/tests/test_queryset.py index 1960fc62..6d175318 100644 --- a/tests/test_queryset.py +++ b/tests/test_queryset.py @@ -830,7 +830,11 @@ class QuerySetTest(unittest.TestCase): def test_filter_chaining(self): """Ensure filters can be chained together. """ + class Blog(Document): + id = StringField(unique=True, primary_key=True) + class BlogPost(Document): + blog = ReferenceField(Blog) title = StringField() is_published = BooleanField() published_date = DateTimeField() @@ -839,13 +843,24 @@ class QuerySetTest(unittest.TestCase): def published(doc_cls, queryset): return queryset(is_published=True) - blog_post_1 = BlogPost(title="Blog Post #1", + Blog.drop_collection() + BlogPost.drop_collection() + + blog_1 = Blog(id="1") + blog_2 = Blog(id="2") + blog_3 = Blog(id="3") + + blog_1.save() + blog_2.save() + blog_3.save() + + blog_post_1 = BlogPost(blog=blog_1, title="Blog Post #1", is_published = True, published_date=datetime(2010, 1, 5, 0, 0 ,0)) - blog_post_2 = BlogPost(title="Blog Post #2", + blog_post_2 = BlogPost(blog=blog_2, title="Blog Post #2", is_published = True, published_date=datetime(2010, 1, 6, 0, 0 ,0)) - blog_post_3 = BlogPost(title="Blog Post #3", + blog_post_3 = BlogPost(blog=blog_3, title="Blog Post #3", is_published = True, published_date=datetime(2010, 1, 7, 0, 0 ,0)) @@ -859,7 +874,14 @@ class QuerySetTest(unittest.TestCase): published_date__lt=datetime(2010, 1, 7, 0, 0 ,0)) self.assertEqual(published_posts.count(), 2) + + blog_posts = BlogPost.objects + blog_posts = blog_posts.filter(blog__in=[blog_1, blog_2]) + blog_posts = blog_posts.filter(blog=blog_3) + self.assertEqual(blog_posts.count(), 0) + BlogPost.drop_collection() + Blog.drop_collection() def test_ordering(self): """Ensure default ordering is applied and can be overridden. @@ -2280,6 +2302,28 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Foo.objects.distinct("bar"), [bar]) + def test_distinct_handles_references_to_alias(self): + register_connection('testdb', 'mongoenginetest2') + + class Foo(Document): + bar = ReferenceField("Bar") + meta = {'db_alias': 'testdb'} + + class Bar(Document): + text = StringField() + meta = {'db_alias': 'testdb'} + + Bar.drop_collection() + Foo.drop_collection() + + bar = Bar(text="hi") + bar.save() + + foo = Foo(bar=bar) + foo.save() + + self.assertEquals(Foo.objects.distinct("bar"), [bar]) + def test_custom_manager(self): """Ensure that custom QuerySetManager instances work as expected. """