Compare commits
17 Commits
cleanup-qs
...
v0.12.0
Author | SHA1 | Date | |
---|---|---|---|
|
888a6da4a5 | ||
|
972ac73dd9 | ||
|
d8b238d5f1 | ||
|
63206c3da2 | ||
|
5713de8966 | ||
|
58f293fef3 | ||
|
ffbb2c9689 | ||
|
9cd3dcdebf | ||
|
f2fe58c3c5 | ||
|
b78010aa94 | ||
|
49035543b9 | ||
|
f9ccf635ca | ||
|
6aaf9ba470 | ||
|
5957dc72eb | ||
|
e32a9777d7 | ||
|
84a8f1eb2b | ||
|
6810953014 |
@@ -5,11 +5,21 @@ Changelog
|
||||
Development
|
||||
===========
|
||||
- (Fill this out as you fix issues and develop your features).
|
||||
- Fixed using sets in field choices #1481
|
||||
|
||||
Changes in 0.12.0
|
||||
=================
|
||||
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
|
||||
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
|
||||
- Fixed connecting to a replica set with PyMongo 2.x #1436
|
||||
- Fixed using sets in field choices #1481
|
||||
- Fixed deleting items from a `ListField` #1318
|
||||
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
|
||||
- Fixed behavior of a `dec` update operator #1450
|
||||
- Added a `rename` update operator #1454
|
||||
- Added validation for the `db_field` parameter #1448
|
||||
- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
|
||||
- Fixed the error message displayed when validating unicode URLs #1486
|
||||
- Raise an error when trying to save an abstract document #1449
|
||||
|
||||
Changes in 0.11.0
|
||||
=================
|
||||
|
@@ -6,6 +6,9 @@ Development
|
||||
***********
|
||||
(Fill this out whenever you introduce breaking changes to MongoEngine)
|
||||
|
||||
|
||||
0.12.0
|
||||
******
|
||||
This release includes various fixes for the `BaseQuerySet` methods and how they
|
||||
are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size
|
||||
to an already-existing queryset wouldn't modify the underlying PyMongo cursor.
|
||||
|
@@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) +
|
||||
list(signals.__all__) + list(errors.__all__))
|
||||
|
||||
|
||||
VERSION = (0, 11, 0)
|
||||
VERSION = (0, 12, 0)
|
||||
|
||||
|
||||
def get_version():
|
||||
|
@@ -684,8 +684,13 @@ class BaseDocument(object):
|
||||
# class if unavailable
|
||||
class_name = son.get('_cls', cls._class_name)
|
||||
|
||||
# Convert SON to a dict, making sure each key is a string
|
||||
data = {str(key): value for key, value in son.iteritems()}
|
||||
# Convert SON to a data dict, making sure each key is a string and
|
||||
# corresponds to the right db field.
|
||||
data = {}
|
||||
for key, value in son.iteritems():
|
||||
key = str(key)
|
||||
key = cls._db_field_map.get(key, key)
|
||||
data[key] = value
|
||||
|
||||
# Return correct subclass for document type
|
||||
if class_name != cls._class_name:
|
||||
|
@@ -1,3 +1,4 @@
|
||||
from collections import OrderedDict
|
||||
from bson import DBRef, SON
|
||||
import six
|
||||
|
||||
@@ -201,6 +202,10 @@ class DeReference(object):
|
||||
as_tuple = isinstance(items, tuple)
|
||||
iterator = enumerate(items)
|
||||
data = []
|
||||
elif isinstance(items, OrderedDict):
|
||||
is_list = False
|
||||
iterator = items.iteritems()
|
||||
data = OrderedDict()
|
||||
else:
|
||||
is_list = False
|
||||
iterator = items.iteritems()
|
||||
|
@@ -5,6 +5,7 @@ import re
|
||||
import time
|
||||
import uuid
|
||||
import warnings
|
||||
from collections import Mapping
|
||||
from operator import itemgetter
|
||||
|
||||
from bson import Binary, DBRef, ObjectId, SON
|
||||
@@ -619,6 +620,14 @@ class DynamicField(BaseField):
|
||||
|
||||
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
|
||||
|
||||
def __init__(self, container_class=dict, *args, **kwargs):
|
||||
self._container_cls = container_class
|
||||
if not issubclass(self._container_cls, Mapping):
|
||||
self.error('The class that is specified in `container_class` parameter '
|
||||
'must be a subclass of `dict`.')
|
||||
|
||||
super(DynamicField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_mongo(self, value, use_db_field=True, fields=None):
|
||||
"""Convert a Python type to a MongoDB compatible type.
|
||||
"""
|
||||
@@ -644,7 +653,7 @@ class DynamicField(BaseField):
|
||||
is_list = True
|
||||
value = {k: v for k, v in enumerate(value)}
|
||||
|
||||
data = {}
|
||||
data = self._container_cls()
|
||||
for k, v in value.iteritems():
|
||||
data[k] = self.to_mongo(v, use_db_field, fields)
|
||||
|
||||
@@ -998,8 +1007,8 @@ class ReferenceField(BaseField):
|
||||
|
||||
def validate(self, value):
|
||||
|
||||
if not isinstance(value, (self.document_type, DBRef)):
|
||||
self.error('A ReferenceField only accepts DBRef or documents')
|
||||
if not isinstance(value, (self.document_type, DBRef, ObjectId)):
|
||||
self.error('A ReferenceField only accepts DBRef, ObjectId or documents')
|
||||
|
||||
if isinstance(value, Document) and value.id is None:
|
||||
self.error('You can only reference documents once they have been '
|
||||
|
@@ -158,44 +158,49 @@ class BaseQuerySet(object):
|
||||
# self._cursor
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Support skip and limit using getitem and slicing syntax."""
|
||||
"""Return a document instance corresponding to a given index if
|
||||
the key is an integer. If the key is a slice, translate its
|
||||
bounds into a skip and a limit, and return a cloned queryset
|
||||
with that skip/limit applied. For example:
|
||||
|
||||
>>> User.objects[0]
|
||||
<User: User object>
|
||||
>>> User.objects[1:3]
|
||||
[<User: User object>, <User: User object>]
|
||||
"""
|
||||
queryset = self.clone()
|
||||
|
||||
# Slice provided
|
||||
# Handle a slice
|
||||
if isinstance(key, slice):
|
||||
try:
|
||||
queryset._cursor_obj = queryset._cursor[key]
|
||||
queryset._skip, queryset._limit = key.start, key.stop
|
||||
if key.start and key.stop:
|
||||
queryset._limit = key.stop - key.start
|
||||
except IndexError as err:
|
||||
# PyMongo raises an error if key.start == key.stop, catch it,
|
||||
# bin it, kill it.
|
||||
start = key.start or 0
|
||||
if start >= 0 and key.stop >= 0 and key.step is None:
|
||||
if start == key.stop:
|
||||
queryset.limit(0)
|
||||
queryset._skip = key.start
|
||||
queryset._limit = key.stop - start
|
||||
return queryset
|
||||
raise err
|
||||
queryset._cursor_obj = queryset._cursor[key]
|
||||
queryset._skip, queryset._limit = key.start, key.stop
|
||||
if key.start and key.stop:
|
||||
queryset._limit = key.stop - key.start
|
||||
|
||||
# Allow further QuerySet modifications to be performed
|
||||
return queryset
|
||||
# Integer index provided
|
||||
|
||||
# Handle an index
|
||||
elif isinstance(key, int):
|
||||
if queryset._scalar:
|
||||
return queryset._get_scalar(
|
||||
queryset._document._from_son(queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields))
|
||||
queryset._document._from_son(
|
||||
queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields
|
||||
)
|
||||
)
|
||||
|
||||
if queryset._as_pymongo:
|
||||
return queryset._get_as_pymongo(queryset._cursor[key])
|
||||
return queryset._document._from_son(queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields)
|
||||
|
||||
raise AttributeError
|
||||
return queryset._document._from_son(
|
||||
queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields
|
||||
)
|
||||
|
||||
raise AttributeError('Provide a slice or an integer index')
|
||||
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
@@ -5,8 +5,10 @@ import uuid
|
||||
import math
|
||||
import itertools
|
||||
import re
|
||||
import pymongo
|
||||
|
||||
from nose.plugins.skip import SkipTest
|
||||
from collections import OrderedDict
|
||||
import six
|
||||
|
||||
try:
|
||||
@@ -25,9 +27,12 @@ except ImportError:
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList,
|
||||
_document_registry)
|
||||
_document_registry, TopLevelDocumentMetaclass)
|
||||
|
||||
from tests.utils import MongoDBTestCase
|
||||
from tests.utils import MongoDBTestCase, MONGO_TEST_DB
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
if IS_PYMONGO_3:
|
||||
from bson import CodecOptions
|
||||
|
||||
__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase")
|
||||
|
||||
@@ -1901,6 +1906,51 @@ class FieldTest(MongoDBTestCase):
|
||||
doc = self.db.test.find_one()
|
||||
self.assertEqual(doc['x']['i'], 2)
|
||||
|
||||
def test_double_embedded_db_field(self):
|
||||
"""Make sure multiple layers of embedded docs resolve db fields
|
||||
properly and can be initialized using dicts.
|
||||
"""
|
||||
class C(EmbeddedDocument):
|
||||
txt = StringField()
|
||||
|
||||
class B(EmbeddedDocument):
|
||||
c = EmbeddedDocumentField(C, db_field='fc')
|
||||
|
||||
class A(Document):
|
||||
b = EmbeddedDocumentField(B, db_field='fb')
|
||||
|
||||
a = A(
|
||||
b=B(
|
||||
c=C(txt='hi')
|
||||
)
|
||||
)
|
||||
a.validate()
|
||||
|
||||
a = A(b={'c': {'txt': 'hi'}})
|
||||
a.validate()
|
||||
|
||||
def test_double_embedded_db_field_from_son(self):
|
||||
"""Make sure multiple layers of embedded docs resolve db fields
|
||||
from SON properly.
|
||||
"""
|
||||
class C(EmbeddedDocument):
|
||||
txt = StringField()
|
||||
|
||||
class B(EmbeddedDocument):
|
||||
c = EmbeddedDocumentField(C, db_field='fc')
|
||||
|
||||
class A(Document):
|
||||
b = EmbeddedDocumentField(B, db_field='fb')
|
||||
|
||||
a = A._from_son(SON([
|
||||
('fb', SON([
|
||||
('fc', SON([
|
||||
('txt', 'hi')
|
||||
]))
|
||||
]))
|
||||
]))
|
||||
self.assertEqual(a.b.c.txt, 'hi')
|
||||
|
||||
def test_embedded_document_validation(self):
|
||||
"""Ensure that invalid embedded documents cannot be assigned to
|
||||
embedded document fields.
|
||||
@@ -2043,6 +2093,12 @@ class FieldTest(MongoDBTestCase):
|
||||
post1.author = post2
|
||||
self.assertRaises(ValidationError, post1.validate)
|
||||
|
||||
# Ensure ObjectID's are accepted as references
|
||||
user_object_id = user.pk
|
||||
post3 = BlogPost(content="Chips and curry sauce taste good.")
|
||||
post3.author = user_object_id
|
||||
post3.save()
|
||||
|
||||
# Make sure referencing a saved document of the right type works
|
||||
user.save()
|
||||
post1.author = user
|
||||
@@ -2053,6 +2109,20 @@ class FieldTest(MongoDBTestCase):
|
||||
post1.author = post2
|
||||
self.assertRaises(ValidationError, post1.validate)
|
||||
|
||||
def test_objectid_reference_fields(self):
|
||||
"""Make sure storing Object ID references works."""
|
||||
class Person(Document):
|
||||
name = StringField()
|
||||
parent = ReferenceField('self')
|
||||
|
||||
Person.drop_collection()
|
||||
|
||||
p1 = Person(name="John").save()
|
||||
Person(name="Ross", parent=p1.pk).save()
|
||||
|
||||
p = Person.objects.get(name="Ross")
|
||||
self.assertEqual(p.parent, p1)
|
||||
|
||||
def test_dbref_reference_fields(self):
|
||||
"""Make sure storing references as bson.dbref.DBRef works."""
|
||||
class Person(Document):
|
||||
@@ -4045,6 +4115,67 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase):
|
||||
self.assertTrue(hasattr(CustomData.c_field, 'custom_data'))
|
||||
self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a'])
|
||||
|
||||
def test_dynamicfield_with_container_class(self):
|
||||
"""
|
||||
Tests that object can be stored in order by DynamicField class
|
||||
with container_class parameter.
|
||||
"""
|
||||
raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)]
|
||||
|
||||
class Doc(Document):
|
||||
ordered_data = DynamicField(container_class=OrderedDict)
|
||||
unordered_data = DynamicField()
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
doc = Doc(ordered_data=OrderedDict(raw_data), unordered_data=dict(raw_data)).save()
|
||||
|
||||
# checks that the data is in order
|
||||
self.assertEqual(type(doc.ordered_data), OrderedDict)
|
||||
self.assertEqual(type(doc.unordered_data), dict)
|
||||
self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a')
|
||||
|
||||
# checks that the data is stored to the database in order
|
||||
pymongo_db = pymongo.MongoClient()[MONGO_TEST_DB]
|
||||
if IS_PYMONGO_3:
|
||||
codec_option = CodecOptions(document_class=OrderedDict)
|
||||
db_doc = pymongo_db.doc.with_options(codec_options=codec_option).find_one()
|
||||
else:
|
||||
db_doc = pymongo_db.doc.find_one(as_class=OrderedDict)
|
||||
|
||||
self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a')
|
||||
|
||||
def test_dynamicfield_with_wrong_container_class(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
class DocWithInvalidField:
|
||||
data = DynamicField(container_class=list)
|
||||
|
||||
def test_dynamicfield_with_wrong_container_class_and_reload_docuemnt(self):
|
||||
# This is because 'codec_options' is supported on pymongo3 or later
|
||||
if IS_PYMONGO_3:
|
||||
class OrderedDocument(Document):
|
||||
my_metaclass = TopLevelDocumentMetaclass
|
||||
__metaclass__ = TopLevelDocumentMetaclass
|
||||
|
||||
@classmethod
|
||||
def _get_collection(cls):
|
||||
collection = super(OrderedDocument, cls)._get_collection()
|
||||
opts = CodecOptions(document_class=OrderedDict)
|
||||
|
||||
return collection.with_options(codec_options=opts)
|
||||
|
||||
raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)]
|
||||
|
||||
class Doc(OrderedDocument):
|
||||
data = DynamicField(container_class=OrderedDict)
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
doc = Doc(data=OrderedDict(raw_data)).save()
|
||||
doc.reload()
|
||||
|
||||
self.assertEqual(type(doc.data), OrderedDict)
|
||||
self.assertEqual(','.join(doc.data.keys()), 'd,c,b,a')
|
||||
|
||||
class CachedReferenceFieldTest(MongoDBTestCase):
|
||||
|
||||
|
@@ -4962,20 +4962,6 @@ class QuerySetTest(unittest.TestCase):
|
||||
for p in Person.objects():
|
||||
self.assertEqual(p.name, 'a')
|
||||
|
||||
def test_last_field_name_like_operator(self):
|
||||
class EmbeddedItem(EmbeddedDocument):
|
||||
type = StringField()
|
||||
|
||||
class Doc(Document):
|
||||
item = EmbeddedDocumentField(EmbeddedItem)
|
||||
|
||||
Doc.drop_collection()
|
||||
|
||||
doc = Doc(item=EmbeddedItem(type="axe"))
|
||||
doc.save()
|
||||
|
||||
self.assertEqual(1, Doc.objects(item__type__="axe").count())
|
||||
|
||||
def test_len_during_iteration(self):
|
||||
"""Tests that calling len on a queyset during iteration doesn't
|
||||
stop paging.
|
||||
|
@@ -2,10 +2,15 @@
|
||||
import unittest
|
||||
|
||||
from bson import DBRef, ObjectId
|
||||
from collections import OrderedDict
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.context_managers import query_counter
|
||||
from mongoengine.python_support import IS_PYMONGO_3
|
||||
from mongoengine.base import TopLevelDocumentMetaclass
|
||||
if IS_PYMONGO_3:
|
||||
from bson import CodecOptions
|
||||
|
||||
|
||||
class FieldTest(unittest.TestCase):
|
||||
@@ -1287,5 +1292,70 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(q, 2)
|
||||
|
||||
def test_dynamic_field_dereference(self):
|
||||
class Merchandise(Document):
|
||||
name = StringField()
|
||||
price = IntField()
|
||||
|
||||
class Store(Document):
|
||||
merchandises = DynamicField()
|
||||
|
||||
Merchandise.drop_collection()
|
||||
Store.drop_collection()
|
||||
|
||||
merchandises = {
|
||||
'#1': Merchandise(name='foo', price=100).save(),
|
||||
'#2': Merchandise(name='bar', price=120).save(),
|
||||
'#3': Merchandise(name='baz', price=110).save(),
|
||||
}
|
||||
Store(merchandises=merchandises).save()
|
||||
|
||||
store = Store.objects().first()
|
||||
for obj in store.merchandises.values():
|
||||
self.assertFalse(isinstance(obj, Merchandise))
|
||||
|
||||
store.select_related()
|
||||
for obj in store.merchandises.values():
|
||||
self.assertTrue(isinstance(obj, Merchandise))
|
||||
|
||||
def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self):
|
||||
# This is because 'codec_options' is supported on pymongo3 or later
|
||||
if IS_PYMONGO_3:
|
||||
class OrderedDocument(Document):
|
||||
my_metaclass = TopLevelDocumentMetaclass
|
||||
__metaclass__ = TopLevelDocumentMetaclass
|
||||
|
||||
@classmethod
|
||||
def _get_collection(cls):
|
||||
collection = super(OrderedDocument, cls)._get_collection()
|
||||
opts = CodecOptions(document_class=OrderedDict)
|
||||
|
||||
return collection.with_options(codec_options=opts)
|
||||
|
||||
class Merchandise(Document):
|
||||
name = StringField()
|
||||
price = IntField()
|
||||
|
||||
class Store(OrderedDocument):
|
||||
merchandises = DynamicField(container_class=OrderedDict)
|
||||
|
||||
Merchandise.drop_collection()
|
||||
Store.drop_collection()
|
||||
|
||||
merchandises = OrderedDict()
|
||||
merchandises['#1'] = Merchandise(name='foo', price=100).save()
|
||||
merchandises['#2'] = Merchandise(name='bar', price=120).save()
|
||||
merchandises['#3'] = Merchandise(name='baz', price=110).save()
|
||||
|
||||
Store(merchandises=merchandises).save()
|
||||
|
||||
store = Store.objects().first()
|
||||
|
||||
store.select_related()
|
||||
|
||||
# confirms that the load data order is same with the one at storing
|
||||
self.assertTrue(type(store.merchandises), OrderedDict)
|
||||
self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
Reference in New Issue
Block a user