Compare commits

..

30 Commits

Author SHA1 Message Date
Emmanuel Leblond
21d1faa793 Fix .install_mongodb_on_travis.sh script (all credit to @erdenezul) 2017-08-24 19:34:22 +02:00
Stefan Wójcik
3794b181d5 Support for $position with the $push operator (#1566) 2017-07-31 15:36:35 +02:00
Erdenezul Batmunkh
f09256a24e Fix modify tests #1565 2017-07-31 18:49:52 +08:00
Erdenezul Batmunkh
34fca9d6f5 Add clear comment and tests for positional push #1565 2017-07-31 18:32:34 +08:00
Erdenezul Batmunkh
433f10ef93 position support singular value #1565 2017-07-31 05:15:34 +00:00
Erdenezul Batmunkh
3dcc9bc143 use explicit tests and fix unneccessary indent #1565 2017-07-13 22:59:21 +08:00
erdenezul
7311895894 Merge branch 'master' into support_position_in_push 2017-07-13 20:08:57 +08:00
Davidrjx
a7cab51369 Use a set literal in _clean_settings (#1585) 2017-07-13 12:07:36 +02:00
Erdenezul Batmunkh
fb00b79d19 add docs for positional push operator #1565 2017-06-19 03:28:34 +00:00
Erdenezul Batmunkh
7782aa7379 do not test position push in mongodb_v2.4 #1565 2017-06-19 03:11:59 +00:00
Erdenezul Batmunkh
f3ee4a5dac add tests for push operator #1565 2017-06-19 02:59:17 +00:00
Stefan Wojcik
a8d6e59a7a minor tweaks to code quality in _fields_to_dbfields 2017-06-18 17:25:39 -07:00
Danil
1d4b1870cf to_db_fields fix (#1553) 2017-06-18 17:04:46 -07:00
Erdenezul Batmunkh
f63ad2dd69 dont test in mongoDB v2.4 #1565 2017-06-15 07:36:14 +00:00
Erdenezul Batmunkh
6903eed4e7 support position in 'push' #1565 2017-06-15 06:08:40 +00:00
José Enrique Carrillo Pino
2f1fe5468e Fix empty string casted to datetime today in DateTimeField (#1533) 2017-05-12 12:59:14 -04:00
lanf0n
24d15d4274 fix typo in the save() method's docstring (#1551) 2017-05-11 10:06:36 -04:00
Stefan Wojcik
0bc7aa52d8 more docs tweaks [ci skip] 2017-05-08 00:14:42 -04:00
Stefan Wojcik
e52603b4a7 ver bump to v0.14.0 + changelog/upgrade docs update 2017-05-08 00:12:26 -04:00
Stefan Wójcik
3b88712402 Cleaner as_pymongo (#1549) 2017-05-08 00:02:42 -04:00
Stefan Wojcik
33e9ef2106 dont test pypy3 temporarily 2017-05-07 21:37:38 -04:00
Stefan Wojcik
689fe4ed9a Revert "use a newer pypy3 (https://github.com/travis-ci/travis-ci/issues/6277)"
This reverts commit 944d1c0a4a.
2017-05-07 21:37:14 -04:00
Stefan Wojcik
b82d026f39 Revert "fix tox.ini"
This reverts commit c00914bea2.
2017-05-07 21:37:05 -04:00
Stefan Wojcik
009059def4 revert #1497 2017-05-07 21:29:13 -04:00
Stefan Wójcik
03ff61d113 better db_field validation (#1547) 2017-05-07 21:11:14 -04:00
Stefan Wojcik
c00914bea2 fix tox.ini 2017-05-07 20:32:52 -04:00
Stefan Wojcik
944d1c0a4a use a newer pypy3 (https://github.com/travis-ci/travis-ci/issues/6277) 2017-05-07 19:54:58 -04:00
Stefan Wojcik
2cf23e33e3 Document._get_update_doc helper method 2017-05-07 19:26:10 -04:00
Stefan Wojcik
e2a0b42d03 clarify test_get_changed_fields_query_count 2017-04-30 18:29:22 -04:00
Stefan Wójcik
894e9818ac use an external sphinx rtd theme (#1541)
Externalize Sphinx RTD theme
2017-04-30 15:38:21 -04:00
20 changed files with 366 additions and 288 deletions

View File

@@ -1,5 +1,6 @@
#!/bin/bash #!/bin/bash
sudo apt-get remove mongodb-org-server
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
if [ "$MONGODB" = "2.4" ]; then if [ "$MONGODB" = "2.4" ]; then
@@ -13,7 +14,7 @@ elif [ "$MONGODB" = "2.6" ]; then
sudo apt-get install mongodb-org-server=2.6.12 sudo apt-get install mongodb-org-server=2.6.12
# service should be started automatically # service should be started automatically
elif [ "$MONGODB" = "3.0" ]; then elif [ "$MONGODB" = "3.0" ]; then
echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update sudo apt-get update
sudo apt-get install mongodb-org-server=3.0.14 sudo apt-get install mongodb-org-server=3.0.14
# service should be started automatically # service should be started automatically
@@ -21,3 +22,6 @@ else
echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0." echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0."
exit 1 exit 1
fi; fi;
mkdir db
1>db/logs mongod --dbpath=db &

View File

@@ -16,7 +16,6 @@ python:
- 2.7 - 2.7
- 3.5 - 3.5
- pypy - pypy
- pypy3
env: env:
- MONGODB=2.6 PYMONGO=2.7 - MONGODB=2.6 PYMONGO=2.7
@@ -43,6 +42,8 @@ matrix:
before_install: before_install:
- bash .install_mongodb_on_travis.sh - bash .install_mongodb_on_travis.sh
- sleep 15 # https://docs.travis-ci.com/user/database-setup/#MongoDB-does-not-immediately-accept-connections
- mongo --eval 'db.version();'
install: install:
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev

View File

@@ -243,3 +243,5 @@ that much better:
* Victor Varvaryuk * Victor Varvaryuk
* Stanislav Kaledin (https://github.com/sallyruthstruik) * Stanislav Kaledin (https://github.com/sallyruthstruik)
* Dmitry Yantsen (https://github.com/mrTable) * Dmitry Yantsen (https://github.com/mrTable)
* Renjianxin (https://github.com/Davidrjx)
* Erdenezul Batmunkh (https://github.com/erdenezul)

View File

@@ -6,6 +6,12 @@ Development
=========== ===========
- (Fill this out as you fix issues and develop your features). - (Fill this out as you fix issues and develop your features).
Changes in 0.14.0
=================
- BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo` #1549
- POTENTIAL BREAKING CHANGE: Made EmbeddedDocument not hashable by default #1528
- Improved code quality #1531, #1540, #1541, #1547
Changes in 0.13.0 Changes in 0.13.0
================= =================
- POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see - POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see

View File

@@ -565,6 +565,15 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`::
>>> post.tags >>> post.tags
['database', 'mongodb'] ['database', 'mongodb']
From MongoDB version 2.6, push operator supports $position value which allows
to push values with index.
>>> post = BlogPost(title="Test", tags=["mongo"])
>>> post.save()
>>> post.update(push__tags__0=["database", "code"])
>>> post.reload()
>>> post.tags
['database', 'code', 'mongo']
.. note:: .. note::
Currently only top level lists are handled, future versions of mongodb / Currently only top level lists are handled, future versions of mongodb /
pymongo plan to support nested positional operators. See `The $ positional pymongo plan to support nested positional operators. See `The $ positional

View File

@@ -6,6 +6,18 @@ Development
*********** ***********
(Fill this out whenever you introduce breaking changes to MongoEngine) (Fill this out whenever you introduce breaking changes to MongoEngine)
0.14.0
******
This release includes a few bug fixes and a significant code cleanup. The most
important change is that `QuerySet.as_pymongo` no longer supports a
`coerce_types` mode. If you used it in the past, a) please let us know of your
use case, b) you'll need to override `as_pymongo` to get the desired outcome.
This release also makes the EmbeddedDocument not hashable by default. If you
use embedded documents in sets or dictionaries, you might have to override
`__hash__` and implement a hashing logic specific to your use case. See #1528
for the reason behind this change.
0.13.0 0.13.0
****** ******
This release adds Unicode support to the `EmailField` and changes its This release adds Unicode support to the `EmailField` and changes its

View File

@@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) +
list(signals.__all__) + list(errors.__all__)) list(signals.__all__) + list(errors.__all__))
VERSION = (0, 13, 0) VERSION = (0, 14, 0)
def get_version(): def get_version():

View File

@@ -81,7 +81,14 @@ class BaseField(object):
self.sparse = sparse self.sparse = sparse
self._owner_document = None self._owner_document = None
# Validate the db_field # Make sure db_field is a string (if it's explicitly defined).
if (
self.db_field is not None and
not isinstance(self.db_field, six.string_types)
):
raise TypeError('db_field should be a string.')
# Make sure db_field doesn't contain any forbidden characters.
if isinstance(self.db_field, six.string_types) and ( if isinstance(self.db_field, six.string_types) and (
'.' in self.db_field or '.' in self.db_field or
'\0' in self.db_field or '\0' in self.db_field or

View File

@@ -146,13 +146,14 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
raise MongoEngineConnectionError(msg) raise MongoEngineConnectionError(msg)
def _clean_settings(settings_dict): def _clean_settings(settings_dict):
irrelevant_fields = set([ # set literal more efficient than calling set function
'name', 'username', 'password', 'authentication_source', irrelevant_fields_set = {
'authentication_mechanism' 'name', 'username', 'password',
]) 'authentication_source', 'authentication_mechanism'
}
return { return {
k: v for k, v in settings_dict.items() k: v for k, v in settings_dict.items()
if k not in irrelevant_fields if k not in irrelevant_fields_set
} }
# Retrieve a copy of the connection settings associated with the requested # Retrieve a copy of the connection settings associated with the requested

View File

@@ -1,4 +1,3 @@
from collections import OrderedDict
from bson import DBRef, SON from bson import DBRef, SON
import six import six
@@ -202,10 +201,6 @@ class DeReference(object):
as_tuple = isinstance(items, tuple) as_tuple = isinstance(items, tuple)
iterator = enumerate(items) iterator = enumerate(items)
data = [] data = []
elif isinstance(items, OrderedDict):
is_list = False
iterator = items.iteritems()
data = OrderedDict()
else: else:
is_list = False is_list = False
iterator = items.iteritems() iterator = items.iteritems()

View File

@@ -300,7 +300,7 @@ class Document(BaseDocument):
created. created.
:param force_insert: only try to create a new document, don't allow :param force_insert: only try to create a new document, don't allow
updates of existing documents updates of existing documents.
:param validate: validates the document; set to ``False`` to skip. :param validate: validates the document; set to ``False`` to skip.
:param clean: call the document clean method, requires `validate` to be :param clean: call the document clean method, requires `validate` to be
True. True.
@@ -320,7 +320,7 @@ class Document(BaseDocument):
:param save_condition: only perform save if matching record in db :param save_condition: only perform save if matching record in db
satisfies condition(s) (e.g. version number). satisfies condition(s) (e.g. version number).
Raises :class:`OperationError` if the conditions are not satisfied Raises :class:`OperationError` if the conditions are not satisfied
:parm signal_kwargs: (optional) kwargs dictionary to be passed to :param signal_kwargs: (optional) kwargs dictionary to be passed to
the signal calls. the signal calls.
.. versionchanged:: 0.5 .. versionchanged:: 0.5
@@ -441,6 +441,21 @@ class Document(BaseDocument):
return object_id return object_id
def _get_update_doc(self):
"""Return a dict containing all the $set and $unset operations
that should be sent to MongoDB based on the changes made to this
Document.
"""
updates, removals = self._delta()
update_doc = {}
if updates:
update_doc['$set'] = updates
if removals:
update_doc['$unset'] = removals
return update_doc
def _save_update(self, doc, save_condition, write_concern): def _save_update(self, doc, save_condition, write_concern):
"""Update an existing document. """Update an existing document.
@@ -466,15 +481,10 @@ class Document(BaseDocument):
val = val[ak] val = val[ak]
select_dict['.'.join(actual_key)] = val select_dict['.'.join(actual_key)] = val
updates, removals = self._delta() update_doc = self._get_update_doc()
update_query = {} if update_doc:
if updates:
update_query['$set'] = updates
if removals:
update_query['$unset'] = removals
if updates or removals:
upsert = save_condition is None upsert = save_condition is None
last_error = collection.update(select_dict, update_query, last_error = collection.update(select_dict, update_doc,
upsert=upsert, **write_concern) upsert=upsert, **write_concern)
if not upsert and last_error['n'] == 0: if not upsert and last_error['n'] == 0:
raise SaveConditionError('Race condition preventing' raise SaveConditionError('Race condition preventing'

View File

@@ -6,7 +6,6 @@ import socket
import time import time
import uuid import uuid
import warnings import warnings
from collections import Mapping
from operator import itemgetter from operator import itemgetter
from bson import Binary, DBRef, ObjectId, SON from bson import Binary, DBRef, ObjectId, SON
@@ -484,6 +483,10 @@ class DateTimeField(BaseField):
if not isinstance(value, six.string_types): if not isinstance(value, six.string_types):
return None return None
value = value.strip()
if not value:
return None
# Attempt to parse a datetime: # Attempt to parse a datetime:
if dateutil: if dateutil:
try: try:
@@ -705,14 +708,6 @@ class DynamicField(BaseField):
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" 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): def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB compatible type. """Convert a Python type to a MongoDB compatible type.
""" """
@@ -738,7 +733,7 @@ class DynamicField(BaseField):
is_list = True is_list = True
value = {k: v for k, v in enumerate(value)} value = {k: v for k, v in enumerate(value)}
data = self._container_cls() data = {}
for k, v in value.iteritems(): for k, v in value.iteritems():
data[k] = self.to_mongo(v, use_db_field, fields) data[k] = self.to_mongo(v, use_db_field, fields)

View File

@@ -67,7 +67,6 @@ class BaseQuerySet(object):
self._scalar = [] self._scalar = []
self._none = False self._none = False
self._as_pymongo = False self._as_pymongo = False
self._as_pymongo_coerce = False
self._search_text = None self._search_text = None
# If inheritance is allowed, only return instances and instances of # If inheritance is allowed, only return instances and instances of
@@ -728,11 +727,12 @@ class BaseQuerySet(object):
'%s is not a subclass of BaseQuerySet' % new_qs.__name__) '%s is not a subclass of BaseQuerySet' % new_qs.__name__)
copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj',
'_where_clause', '_loaded_fields', '_ordering', '_snapshot', '_where_clause', '_loaded_fields', '_ordering',
'_timeout', '_class_check', '_slave_okay', '_read_preference', '_snapshot', '_timeout', '_class_check', '_slave_okay',
'_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', '_read_preference', '_iter', '_scalar', '_as_pymongo',
'_limit', '_skip', '_hint', '_auto_dereference', '_limit', '_skip', '_hint', '_auto_dereference',
'_search_text', 'only_fields', '_max_time_ms', '_comment') '_search_text', 'only_fields', '_max_time_ms',
'_comment')
for prop in copy_props: for prop in copy_props:
val = getattr(self, prop) val = getattr(self, prop)
@@ -939,7 +939,8 @@ class BaseQuerySet(object):
posts = BlogPost.objects(...).fields(slice__comments=5) posts = BlogPost.objects(...).fields(slice__comments=5)
:param kwargs: A set keywors arguments identifying what to include. :param kwargs: A set of keyword arguments identifying what to
include, exclude, or slice.
.. versionadded:: 0.5 .. versionadded:: 0.5
""" """
@@ -1128,16 +1129,15 @@ class BaseQuerySet(object):
"""An alias for scalar""" """An alias for scalar"""
return self.scalar(*fields) return self.scalar(*fields)
def as_pymongo(self, coerce_types=False): def as_pymongo(self):
"""Instead of returning Document instances, return raw values from """Instead of returning Document instances, return raw values from
pymongo. pymongo.
:param coerce_types: Field types (if applicable) would be use to This method is particularly useful if you don't need dereferencing
coerce types. and care primarily about the speed of data retrieval.
""" """
queryset = self.clone() queryset = self.clone()
queryset._as_pymongo = True queryset._as_pymongo = True
queryset._as_pymongo_coerce = coerce_types
return queryset return queryset
def max_time_ms(self, ms): def max_time_ms(self, ms):
@@ -1722,25 +1722,33 @@ class BaseQuerySet(object):
return frequencies return frequencies
def _fields_to_dbfields(self, fields): def _fields_to_dbfields(self, fields):
"""Translate fields paths to its db equivalents""" """Translate fields' paths to their db equivalents."""
ret = []
subclasses = [] subclasses = []
document = self._document if self._document._meta['allow_inheritance']:
if document._meta['allow_inheritance']:
subclasses = [get_document(x) subclasses = [get_document(x)
for x in document._subclasses][1:] for x in self._document._subclasses][1:]
db_field_paths = []
for field in fields: for field in fields:
field_parts = field.split('.')
try: try:
field = '.'.join(f.db_field for f in field = '.'.join(
document._lookup_field(field.split('.'))) f if isinstance(f, six.string_types) else f.db_field
ret.append(field) for f in self._document._lookup_field(field_parts)
)
db_field_paths.append(field)
except LookUpError as err: except LookUpError as err:
found = False found = False
# If a field path wasn't found on the main document, go
# through its subclasses and see if it exists on any of them.
for subdoc in subclasses: for subdoc in subclasses:
try: try:
subfield = '.'.join(f.db_field for f in subfield = '.'.join(
subdoc._lookup_field(field.split('.'))) f if isinstance(f, six.string_types) else f.db_field
ret.append(subfield) for f in subdoc._lookup_field(field_parts)
)
db_field_paths.append(subfield)
found = True found = True
break break
except LookUpError: except LookUpError:
@@ -1748,7 +1756,8 @@ class BaseQuerySet(object):
if not found: if not found:
raise err raise err
return ret
return db_field_paths
def _get_order_by(self, keys): def _get_order_by(self, keys):
"""Given a list of MongoEngine-style sort keys, return a list """Given a list of MongoEngine-style sort keys, return a list
@@ -1799,59 +1808,25 @@ class BaseQuerySet(object):
return tuple(data) return tuple(data)
def _get_as_pymongo(self, row): def _get_as_pymongo(self, doc):
# Extract which fields paths we should follow if .fields(...) was """Clean up a PyMongo doc, removing fields that were only fetched
# used. If not, handle all fields. for the sake of MongoEngine's implementation, and return it.
if not getattr(self, '__as_pymongo_fields', None): """
self.__as_pymongo_fields = [] # Always remove _cls as a MongoEngine's implementation detail.
if '_cls' in doc:
del doc['_cls']
for field in self._loaded_fields.fields - set(['_cls']): # If the _id was not included in a .only or was excluded in a .exclude,
self.__as_pymongo_fields.append(field) # remove it from the doc (we always fetch it so that we can properly
while '.' in field: # construct documents).
field, _ = field.rsplit('.', 1) fields = self._loaded_fields
self.__as_pymongo_fields.append(field) if fields and '_id' in doc and (
(fields.value == QueryFieldList.ONLY and '_id' not in fields.fields) or
(fields.value == QueryFieldList.EXCLUDE and '_id' in fields.fields)
):
del doc['_id']
all_fields = not self.__as_pymongo_fields return doc
def clean(data, path=None):
path = path or ''
if isinstance(data, dict):
new_data = {}
for key, value in data.iteritems():
new_path = '%s.%s' % (path, key) if path else key
if all_fields:
include_field = True
elif self._loaded_fields.value == QueryFieldList.ONLY:
include_field = new_path in self.__as_pymongo_fields
else:
include_field = new_path not in self.__as_pymongo_fields
if include_field:
new_data[key] = clean(value, path=new_path)
data = new_data
elif isinstance(data, list):
data = [clean(d, path=path) for d in data]
else:
if self._as_pymongo_coerce:
# If we need to coerce types, we need to determine the
# type of this field and use the corresponding
# .to_python(...)
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
obj = self._document
for chunk in path.split('.'):
obj = getattr(obj, chunk, None)
if obj is None:
break
elif isinstance(obj, EmbeddedDocumentField):
obj = obj.document_type
if obj and data is not None:
data = obj.to_python(data)
return data
return clean(row)
def _sub_js_fields(self, code): def _sub_js_fields(self, code):
"""When fields are specified with [~fieldname] syntax, where """When fields are specified with [~fieldname] syntax, where

View File

@@ -284,7 +284,9 @@ def update(_doc_cls=None, **update):
if isinstance(field, GeoJsonBaseField): if isinstance(field, GeoJsonBaseField):
value = field.to_mongo(value) value = field.to_mongo(value)
if op in (None, 'set', 'push', 'pull'): if op == 'push' and isinstance(value, (list, tuple, set)):
value = [field.prepare_query_value(op, v) for v in value]
elif op in (None, 'set', 'push', 'pull'):
if field.required or value is not None: if field.required or value is not None:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
elif op in ('pushAll', 'pullAll'): elif op in ('pushAll', 'pullAll'):
@@ -333,10 +335,22 @@ def update(_doc_cls=None, **update):
value = {key: value} value = {key: value}
elif op == 'addToSet' and isinstance(value, list): elif op == 'addToSet' and isinstance(value, list):
value = {key: {'$each': value}} value = {key: {'$each': value}}
elif op == 'push':
if parts[-1].isdigit():
key = parts[0]
position = int(parts[-1])
# $position expects an iterable. If pushing a single value,
# wrap it in a list.
if not isinstance(value, (set, tuple, list)):
value = [value]
value = {key: {'$each': value, '$position': position}}
elif isinstance(value, list):
value = {key: {'$each': value}}
else:
value = {key: value}
else: else:
value = {key: value} value = {key: value}
key = '$' + op key = '$' + op
if key not in mongo_update: if key not in mongo_update:
mongo_update[key] = value mongo_update[key] = value
elif key in mongo_update and isinstance(mongo_update[key], dict): elif key in mongo_update and isinstance(mongo_update[key], dict):

View File

@@ -22,6 +22,8 @@ from mongoengine.queryset import NULLIFY, Q
from mongoengine.context_managers import switch_db, query_counter from mongoengine.context_managers import switch_db, query_counter
from mongoengine import signals from mongoengine import signals
from tests.utils import needs_mongodb_v26
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__),
'../fields/mongoengine.png') '../fields/mongoengine.png')
@@ -242,7 +244,7 @@ class InstanceTest(unittest.TestCase):
Zoo.drop_collection() Zoo.drop_collection()
class Zoo(Document): class Zoo(Document):
animals = ListField(GenericReferenceField(Animal)) animals = ListField(GenericReferenceField())
# Save a reference to each animal # Save a reference to each animal
zoo = Zoo(animals=Animal.objects) zoo = Zoo(animals=Animal.objects)
@@ -826,6 +828,22 @@ class InstanceTest(unittest.TestCase):
self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())])
@needs_mongodb_v26
def test_modify_with_positional_push(self):
class BlogPost(Document):
tags = ListField(StringField())
post = BlogPost.objects.create(tags=['python'])
self.assertEqual(post.tags, ['python'])
post.modify(push__tags__0=['code', 'mongo'])
self.assertEqual(post.tags, ['code', 'mongo', 'python'])
# Assert same order of the list items is maintained in the db
self.assertEqual(
BlogPost._get_collection().find_one({'_id': post.pk})['tags'],
['code', 'mongo', 'python']
)
def test_save(self): def test_save(self):
"""Ensure that a document may be saved in the database.""" """Ensure that a document may be saved in the database."""
@@ -3149,6 +3167,22 @@ class InstanceTest(unittest.TestCase):
person.update(set__height=2.0) person.update(set__height=2.0)
@needs_mongodb_v26
def test_push_with_position(self):
"""Ensure that push with position works properly for an instance."""
class BlogPost(Document):
slug = StringField()
tags = ListField(StringField())
blog = BlogPost()
blog.slug = "ABC"
blog.tags = ["python"]
blog.save()
blog.update(push__tags__0=["mongodb", "code"])
blog.reload()
self.assertEqual(blog.tags, ['mongodb', 'code', 'python'])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -5,11 +5,9 @@ import uuid
import math import math
import itertools import itertools
import re import re
import pymongo
import sys import sys
from nose.plugins.skip import SkipTest from nose.plugins.skip import SkipTest
from collections import OrderedDict
import six import six
try: try:
@@ -28,18 +26,37 @@ except ImportError:
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList,
_document_registry, TopLevelDocumentMetaclass) _document_registry)
from tests.utils import MongoDBTestCase, MONGO_TEST_DB from tests.utils import MongoDBTestCase
from mongoengine.python_support import IS_PYMONGO_3
if IS_PYMONGO_3:
from bson import CodecOptions
__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase")
class FieldTest(MongoDBTestCase): class FieldTest(MongoDBTestCase):
def test_datetime_from_empty_string(self):
"""
Ensure an exception is raised when trying to
cast an empty string to datetime.
"""
class MyDoc(Document):
dt = DateTimeField()
md = MyDoc(dt='')
self.assertRaises(ValidationError, md.save)
def test_datetime_from_whitespace_string(self):
"""
Ensure an exception is raised when trying to
cast a whitespace-only string to datetime.
"""
class MyDoc(Document):
dt = DateTimeField()
md = MyDoc(dt=' ')
self.assertRaises(ValidationError, md.save)
def test_default_values_nothing_set(self): def test_default_values_nothing_set(self):
"""Ensure that default field values are used when creating """Ensure that default field values are used when creating
a document. a document.
@@ -4188,67 +4205,6 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase):
self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertTrue(hasattr(CustomData.c_field, 'custom_data'))
self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) 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): class CachedReferenceFieldTest(MongoDBTestCase):

View File

@@ -197,14 +197,18 @@ class OnlyExcludeAllTest(unittest.TestCase):
title = StringField() title = StringField()
text = StringField() text = StringField()
class VariousData(EmbeddedDocument):
some = BooleanField()
class BlogPost(Document): class BlogPost(Document):
content = StringField() content = StringField()
author = EmbeddedDocumentField(User) author = EmbeddedDocumentField(User)
comments = ListField(EmbeddedDocumentField(Comment)) comments = ListField(EmbeddedDocumentField(Comment))
various = MapField(field=EmbeddedDocumentField(VariousData))
BlogPost.drop_collection() BlogPost.drop_collection()
post = BlogPost(content='Had a good coffee today...') post = BlogPost(content='Had a good coffee today...', various={'test_dynamic':{'some': True}})
post.author = User(name='Test User') post.author = User(name='Test User')
post.comments = [Comment(title='I aggree', text='Great post!'), Comment(title='Coffee', text='I hate coffee')] post.comments = [Comment(title='I aggree', text='Great post!'), Comment(title='Coffee', text='I hate coffee')]
post.save() post.save()
@@ -215,6 +219,9 @@ class OnlyExcludeAllTest(unittest.TestCase):
self.assertEqual(obj.author.name, 'Test User') self.assertEqual(obj.author.name, 'Test User')
self.assertEqual(obj.comments, []) self.assertEqual(obj.comments, [])
obj = BlogPost.objects.only('various.test_dynamic.some').get()
self.assertEqual(obj.various["test_dynamic"].some, True)
obj = BlogPost.objects.only('content', 'comments.title',).get() obj = BlogPost.objects.only('content', 'comments.title',).get()
self.assertEqual(obj.content, 'Had a good coffee today...') self.assertEqual(obj.content, 'Had a good coffee today...')
self.assertEqual(obj.author, None) self.assertEqual(obj.author, None)

View File

@@ -1,6 +1,8 @@
import unittest import unittest
from mongoengine import connect, Document, IntField from mongoengine import connect, Document, IntField, StringField, ListField
from tests.utils import needs_mongodb_v26
__all__ = ("FindAndModifyTest",) __all__ = ("FindAndModifyTest",)
@@ -94,6 +96,37 @@ class FindAndModifyTest(unittest.TestCase):
self.assertEqual(old_doc.to_mongo(), {"_id": 1}) self.assertEqual(old_doc.to_mongo(), {"_id": 1})
self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])
@needs_mongodb_v26
def test_modify_with_push(self):
class BlogPost(Document):
tags = ListField(StringField())
BlogPost.drop_collection()
blog = BlogPost.objects.create()
# Push a new tag via modify with new=False (default).
BlogPost(id=blog.id).modify(push__tags='code')
self.assertEqual(blog.tags, [])
blog.reload()
self.assertEqual(blog.tags, ['code'])
# Push a new tag via modify with new=True.
blog = BlogPost.objects(id=blog.id).modify(push__tags='java', new=True)
self.assertEqual(blog.tags, ['code', 'java'])
# Push a new tag with a positional argument.
blog = BlogPost.objects(id=blog.id).modify(
push__tags__0='python',
new=True)
self.assertEqual(blog.tags, ['python', 'code', 'java'])
# Push multiple new tags with a positional argument.
blog = BlogPost.objects(id=blog.id).modify(
push__tags__1=['go', 'rust'],
new=True)
self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java'])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@@ -917,7 +917,9 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(Blog.objects.count(), 3) self.assertEqual(Blog.objects.count(), 3)
def test_get_changed_fields_query_count(self): def test_get_changed_fields_query_count(self):
"""Make sure we don't perform unnecessary db operations when
none of document's fields were updated.
"""
class Person(Document): class Person(Document):
name = StringField() name = StringField()
owns = ListField(ReferenceField('Organization')) owns = ListField(ReferenceField('Organization'))
@@ -925,8 +927,8 @@ class QuerySetTest(unittest.TestCase):
class Organization(Document): class Organization(Document):
name = StringField() name = StringField()
owner = ReferenceField('Person') owner = ReferenceField(Person)
employees = ListField(ReferenceField('Person')) employees = ListField(ReferenceField(Person))
class Project(Document): class Project(Document):
name = StringField() name = StringField()
@@ -945,35 +947,35 @@ class QuerySetTest(unittest.TestCase):
with query_counter() as q: with query_counter() as q:
self.assertEqual(q, 0) self.assertEqual(q, 0)
fresh_o1 = Organization.objects.get(id=o1.id) # Fetching a document should result in a query.
self.assertEqual(1, q) org = Organization.objects.get(id=o1.id)
fresh_o1._get_changed_fields()
self.assertEqual(1, q)
with query_counter() as q:
self.assertEqual(q, 0)
fresh_o1 = Organization.objects.get(id=o1.id)
fresh_o1.save() # No changes, does nothing
self.assertEqual(q, 1) self.assertEqual(q, 1)
with query_counter() as q: # Checking changed fields of a newly fetched document should not
self.assertEqual(q, 0) # result in a query.
org._get_changed_fields()
fresh_o1 = Organization.objects.get(id=o1.id)
fresh_o1.save(cascade=False) # No changes, does nothing
self.assertEqual(q, 1) self.assertEqual(q, 1)
# Saving a doc without changing any of its fields should not result
# in a query (with or without cascade=False).
org = Organization.objects.get(id=o1.id)
with query_counter() as q: with query_counter() as q:
org.save()
self.assertEqual(q, 0) self.assertEqual(q, 0)
fresh_o1 = Organization.objects.get(id=o1.id) org = Organization.objects.get(id=o1.id)
fresh_o1.employees.append(p2) # Dereferences with query_counter() as q:
fresh_o1.save(cascade=False) # Saves org.save(cascade=False)
self.assertEqual(q, 0)
self.assertEqual(q, 3) # Saving a doc after you append a reference to it should result in
# two db operations (a query for the reference and an update).
# TODO dereferencing of p2 shouldn't be necessary.
org = Organization.objects.get(id=o1.id)
with query_counter() as q:
org.employees.append(p2) # dereferences p2
org.save() # saves the org
self.assertEqual(q, 2)
@skip_pymongo3 @skip_pymongo3
def test_slave_okay(self): def test_slave_okay(self):
@@ -1901,6 +1903,32 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
@needs_mongodb_v26
def test_update_push_with_position(self):
"""Ensure that the 'push' update with position works properly.
"""
class BlogPost(Document):
slug = StringField()
tags = ListField(StringField())
BlogPost.drop_collection()
post = BlogPost.objects.create(slug="test")
BlogPost.objects.filter(id=post.id).update(push__tags="code")
BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"])
post.reload()
self.assertEqual(post.tags, ['mongodb', 'python', 'code'])
BlogPost.objects.filter(id=post.id).update(set__tags__2="java")
post.reload()
self.assertEqual(post.tags, ['mongodb', 'python', 'java'])
#test push with singular value
BlogPost.objects.filter(id=post.id).update(push__tags__0='scala')
post.reload()
self.assertEqual(post.tags, ['scala', 'mongodb', 'python', 'java'])
def test_update_push_and_pull_add_to_set(self): def test_update_push_and_pull_add_to_set(self):
"""Ensure that the 'pull' update operation works correctly. """Ensure that the 'pull' update operation works correctly.
""" """
@@ -4045,6 +4073,35 @@ class QuerySetTest(unittest.TestCase):
plist = list(Person.objects.scalar('name', 'state')) plist = list(Person.objects.scalar('name', 'state'))
self.assertEqual(plist, [(u'Wilson JR', s1)]) self.assertEqual(plist, [(u'Wilson JR', s1)])
def test_generic_reference_field_with_only_and_as_pymongo(self):
class TestPerson(Document):
name = StringField()
class TestActivity(Document):
name = StringField()
owner = GenericReferenceField()
TestPerson.drop_collection()
TestActivity.drop_collection()
person = TestPerson(name='owner')
person.save()
a1 = TestActivity(name='a1', owner=person)
a1.save()
activity = TestActivity.objects(owner=person).scalar('id', 'owner').no_dereference().first()
self.assertEqual(activity[0], a1.pk)
self.assertEqual(activity[1]['_ref'], DBRef('test_person', person.pk))
activity = TestActivity.objects(owner=person).only('id', 'owner')[0]
self.assertEqual(activity.pk, a1.pk)
self.assertEqual(activity.owner, person)
activity = TestActivity.objects(owner=person).only('id', 'owner').as_pymongo().first()
self.assertEqual(activity['_id'], a1.pk)
self.assertTrue(activity['owner']['_ref'], DBRef('test_person', person.pk))
def test_scalar_db_field(self): def test_scalar_db_field(self):
class TestDoc(Document): class TestDoc(Document):
@@ -4390,21 +4447,44 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(doc_objects, Doc.objects.from_json(json_data)) self.assertEqual(doc_objects, Doc.objects.from_json(json_data))
def test_as_pymongo(self): def test_as_pymongo(self):
from decimal import Decimal from decimal import Decimal
class LastLogin(EmbeddedDocument):
location = StringField()
ip = StringField()
class User(Document): class User(Document):
id = ObjectIdField('_id') id = ObjectIdField('_id')
name = StringField() name = StringField()
age = IntField() age = IntField()
price = DecimalField() price = DecimalField()
last_login = EmbeddedDocumentField(LastLogin)
User.drop_collection() User.drop_collection()
User(name="Bob Dole", age=89, price=Decimal('1.11')).save()
User(name="Barack Obama", age=51, price=Decimal('2.22')).save() User.objects.create(name="Bob Dole", age=89, price=Decimal('1.11'))
User.objects.create(
name="Barack Obama",
age=51,
price=Decimal('2.22'),
last_login=LastLogin(
location='White House',
ip='104.107.108.116'
)
)
results = User.objects.as_pymongo()
self.assertEqual(
set(results[0].keys()),
set(['_id', 'name', 'age', 'price'])
)
self.assertEqual(
set(results[1].keys()),
set(['_id', 'name', 'age', 'price', 'last_login'])
)
results = User.objects.only('id', 'name').as_pymongo() results = User.objects.only('id', 'name').as_pymongo()
self.assertEqual(sorted(results[0].keys()), sorted(['_id', 'name'])) self.assertEqual(set(results[0].keys()), set(['_id', 'name']))
users = User.objects.only('name', 'price').as_pymongo() users = User.objects.only('name', 'price').as_pymongo()
results = list(users) results = list(users)
@@ -4415,16 +4495,20 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(results[1]['name'], 'Barack Obama') self.assertEqual(results[1]['name'], 'Barack Obama')
self.assertEqual(results[1]['price'], 2.22) self.assertEqual(results[1]['price'], 2.22)
# Test coerce_types users = User.objects.only('name', 'last_login').as_pymongo()
users = User.objects.only(
'name', 'price').as_pymongo(coerce_types=True)
results = list(users) results = list(users)
self.assertTrue(isinstance(results[0], dict)) self.assertTrue(isinstance(results[0], dict))
self.assertTrue(isinstance(results[1], dict)) self.assertTrue(isinstance(results[1], dict))
self.assertEqual(results[0]['name'], 'Bob Dole') self.assertEqual(results[0], {
self.assertEqual(results[0]['price'], Decimal('1.11')) 'name': 'Bob Dole'
self.assertEqual(results[1]['name'], 'Barack Obama') })
self.assertEqual(results[1]['price'], Decimal('2.22')) self.assertEqual(results[1], {
'name': 'Barack Obama',
'last_login': {
'location': 'White House',
'ip': '104.107.108.116'
}
})
def test_as_pymongo_json_limit_fields(self): def test_as_pymongo_json_limit_fields(self):
@@ -4588,7 +4672,6 @@ class QuerySetTest(unittest.TestCase):
def test_no_cache(self): def test_no_cache(self):
"""Ensure you can add meta data to file""" """Ensure you can add meta data to file"""
class Noddy(Document): class Noddy(Document):
fields = DictField() fields = DictField()
@@ -4606,15 +4689,19 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(len(list(docs)), 100) self.assertEqual(len(list(docs)), 100)
# Can't directly get a length of a no-cache queryset.
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
len(docs) len(docs)
# Another iteration over the queryset should result in another db op.
with query_counter() as q: with query_counter() as q:
self.assertEqual(q, 0)
list(docs) list(docs)
self.assertEqual(q, 1) self.assertEqual(q, 1)
# ... and another one to double-check.
with query_counter() as q:
list(docs) list(docs)
self.assertEqual(q, 2) self.assertEqual(q, 1)
def test_nested_queryset_iterator(self): def test_nested_queryset_iterator(self):
# Try iterating the same queryset twice, nested. # Try iterating the same queryset twice, nested.

View File

@@ -2,15 +2,10 @@
import unittest import unittest
from bson import DBRef, ObjectId from bson import DBRef, ObjectId
from collections import OrderedDict
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.context_managers import query_counter 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): class FieldTest(unittest.TestCase):
@@ -1292,70 +1287,5 @@ class FieldTest(unittest.TestCase):
self.assertEqual(q, 2) 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__': if __name__ == '__main__':
unittest.main() unittest.main()