Merge branch 'master' into fix_2484

This commit is contained in:
Jan Stein 2021-03-25 15:27:48 +01:00
commit da173cf0e2
14 changed files with 186 additions and 100 deletions

View File

@ -11,9 +11,10 @@ on:
tags: tags:
- 'v[0-9]+\.[0-9]+\.[0-9]+*' - 'v[0-9]+\.[0-9]+\.[0-9]+*'
env: env:
MONGODB_3_4: 3.4.19 MONGODB_3_6: 3.6.14
MONGODB_3_6: 3.6.13 MONGODB_4_0: 4.0.23
MONGODB_4_0: 4.0.13 MONGODB_4_2: 4.2
MONGODB_4_4: 4.4
PYMONGO_3_4: 3.4 PYMONGO_3_4: 3.4
PYMONGO_3_6: 3.6 PYMONGO_3_6: 3.6
@ -47,14 +48,14 @@ jobs:
MONGODB: [$MONGODB_4_0] MONGODB: [$MONGODB_4_0]
PYMONGO: [$PYMONGO_3_11] PYMONGO: [$PYMONGO_3_11]
include: include:
- python-version: 3.7
MONGODB: $MONGODB_3_4
PYMONGO: $PYMONGO_3_6
- python-version: 3.7 - python-version: 3.7
MONGODB: $MONGODB_3_6 MONGODB: $MONGODB_3_6
PYMONGO: $PYMONGO_3_9 PYMONGO: $PYMONGO_3_9
- python-version: 3.7 - python-version: 3.7
MONGODB: $MONGODB_3_6 MONGODB: $MONGODB_4_2
PYMONGO: $PYMONGO_3_6
- python-version: 3.7
MONGODB: $MONGODB_4_4
PYMONGO: $PYMONGO_3_11 PYMONGO: $PYMONGO_3_11
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -2,7 +2,17 @@
MONGODB=$1 MONGODB=$1
# Mongo > 4.0 follows different name convention for download links
mongo_build=mongodb-linux-x86_64-${MONGODB} mongo_build=mongodb-linux-x86_64-${MONGODB}
if [[ "$MONGODB" == *"4.2"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
elif [[ "$MONGODB" == *"4.4"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
fi
wget http://fastdl.mongodb.org/linux/$mongo_build.tgz wget http://fastdl.mongodb.org/linux/$mongo_build.tgz
tar xzf $mongo_build.tgz tar xzf $mongo_build.tgz
${PWD}/$mongo_build/bin/mongod --version
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
$mongodb_dir/bin/mongod --version

View File

@ -2,7 +2,8 @@
MONGODB=$1 MONGODB=$1
mongodb_dir=${PWD}/mongodb-linux-x86_64-${MONGODB} mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
mkdir $mongodb_dir/data mkdir $mongodb_dir/data
$mongodb_dir/bin/mongod --dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork $mongodb_dir/bin/mongod --dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork
mongo --eval 'db.version();' # Make sure mongo is awake mongo --eval 'db.version();' # Make sure mongo is awake

View File

@ -1,4 +1,5 @@
========= =========
Changelog Changelog
========= =========
@ -6,8 +7,14 @@ Changelog
Development Development
=========== ===========
- (Fill this out as you fix issues and develop your features). - (Fill this out as you fix issues and develop your features).
- Bugfix: manually setting SequenceField in DynamicDocument doesn't increment the counter #2471
- Bug fix: ignore LazyReferenceFields when clearing _changed_fields #2484 - Bug fix: ignore LazyReferenceFields when clearing _changed_fields #2484
- Improve connection doc #2481
Changes in 0.23.0
=================
- Bugfix: manually setting SequenceField in DynamicDocument doesn't increment the counter #2471
- Add MongoDB 4.2 and 4.4 to CI
- Add support for allowDiskUse on querysets #2468
Changes in 0.22.1 Changes in 0.22.1
================= =================

View File

@ -5,7 +5,7 @@ Connecting to MongoDB
===================== =====================
Connections in MongoEngine are registered globally and are identified with aliases. Connections in MongoEngine are registered globally and are identified with aliases.
If no `alias` is provided during the connection, it will use "default" as alias. If no ``alias`` is provided during the connection, it will use "default" as alias.
To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect` To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect`
function. The first argument is the name of the database to connect to:: function. The first argument is the name of the database to connect to::
@ -14,27 +14,66 @@ function. The first argument is the name of the database to connect to::
connect('project1') connect('project1')
By default, MongoEngine assumes that the :program:`mongod` instance is running By default, MongoEngine assumes that the :program:`mongod` instance is running
on **localhost** on port **27017**. If MongoDB is running elsewhere, you should on **localhost** on port **27017**.
provide the :attr:`host` and :attr:`port` arguments to
:func:`~mongoengine.connect`::
connect('project1', host='192.168.1.35', port=12345) If MongoDB is running elsewhere, you need to provide details on how to connect. There are two ways of
doing this. Using a connection string in URI format (**this is the preferred method**) or individual attributes
provided as keyword arguments.
Connect with URI string
=======================
When using a connection string in URI format you should specify the connection details
as the :attr:`host` to :func:`~mongoengine.connect`. In a web application context for instance, the URI
is typically read from the config file::
connect(host="mongodb://127.0.0.1:27017/my_db")
If the database requires authentication, you can specify it in the
URI. As each database can have its own users configured, you need to tell MongoDB
where to look for the user you are working with, that's what the ``?authSource=admin`` bit
of the MongoDB connection string is for::
# Connects to 'my_db' database by authenticating
# with given credentials against the 'admin' database (by default as authSource isn't provided)
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db")
# Equivalent to previous connection but explicitly states that
# it should use admin as the authentication source database
connect(host="mongodb://my_user:my_password@hostname:port/my_db?authSource=admin")
# Connects to 'my_db' database by authenticating
# with given credentials against that same database
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db")
The URI string can also be used to configure advanced parameters like ssl, replicaSet, etc. For more
information or example about URI string, you can refer to the `official doc <https://docs.mongodb.com/manual/reference/connection-string/>`_::
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=admin&ssl=true&replicaSet=globaldb")
.. note:: URI containing SRV records (e.g "mongodb+srv://server.example.com/") can be used as well
Connect with keyword attributes
===============================
The second option for specifying the connection details is to provide the information as keyword
attributes to :func:`~mongoengine.connect`::
connect('my_db', host='127.0.0.1', port=27017)
If the database requires authentication, :attr:`username`, :attr:`password` If the database requires authentication, :attr:`username`, :attr:`password`
and :attr:`authentication_source` arguments should be provided:: and :attr:`authentication_source` arguments should be provided::
connect('project1', username='webapp', password='pwd123', authentication_source='admin') connect('my_db', username='my_user', password='my_password', authentication_source='admin')
URI style connections are also supported -- just supply the URI as The set of attributes that :func:`~mongoengine.connect` recognizes includes but is not limited to:
the :attr:`host` to :attr:`host`, :attr:`port`, :attr:`read_preference`, :attr:`username`, :attr:`password`, :attr:`authentication_source`, :attr:`authentication_mechanism`,
:func:`~mongoengine.connect`:: :attr:`replicaset`, :attr:`tls`, etc. Most of the parameters accepted by `pymongo.MongoClient <https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`_
can be used with :func:`~mongoengine.connect` and will simply be forwarded when instantiating the `pymongo.MongoClient`.
connect('project1', host='mongodb://localhost/database_name')
.. note:: URI containing SRV records (e.g mongodb+srv://server.example.com/) can be used as well as the :attr:`host`
.. note:: Database, username and password from URI string overrides .. note:: Database, username and password from URI string overrides
corresponding parameters in :func:`~mongoengine.connect`: :: corresponding parameters in :func:`~mongoengine.connect`, this should
obviously be avoided: ::
connect( connect(
db='test', db='test',
@ -43,28 +82,19 @@ the :attr:`host` to
host='mongodb://admin:qwerty@localhost/production' host='mongodb://admin:qwerty@localhost/production'
) )
will establish connection to ``production`` database using will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password.
``admin`` username and ``12345`` password.
.. note:: Calling :func:`~mongoengine.connect` without argument will establish .. note:: Calling :func:`~mongoengine.connect` without argument will establish
a connection to the "test" database by default a connection to the "test" database by default
Replica Sets Read Preferences
============ ================
MongoEngine supports connecting to replica sets:: As stated above, Read preferences are supported through the connection but also via individual
from mongoengine import connect
# Regular connect
connect('dbname', replicaset='rs-name')
# MongoDB URI-style connect
connect(host='mongodb://localhost/dbname?replicaSet=rs-name')
Read preferences are supported through the connection or via individual
queries by passing the read_preference :: queries by passing the read_preference ::
from pymongo import ReadPreference
Bar.objects().read_preference(ReadPreference.PRIMARY) Bar.objects().read_preference(ReadPreference.PRIMARY)
Bar.objects(read_preference=ReadPreference.PRIMARY) Bar.objects(read_preference=ReadPreference.PRIMARY)

View File

@ -290,12 +290,12 @@ as the constructor's argument::
content = StringField() content = StringField()
.. _one-to-many-with-listfields: .. _many-to-many-with-listfields:
One to Many with ListFields Many to Many with ListFields
''''''''''''''''''''''''''' '''''''''''''''''''''''''''
If you are implementing a one to many relationship via a list of references, If you are implementing a many to many relationship via a list of references,
then the references are stored as DBRefs and to query you need to pass an then the references are stored as DBRefs and to query you need to pass an
instance of the object to the query:: instance of the object to the query::

View File

@ -28,7 +28,7 @@ __all__ = (
) )
VERSION = (0, 22, 1) VERSION = (0, 23, 0)
def get_version(): def get_version():

View File

@ -267,6 +267,17 @@ class ComplexBaseField(BaseField):
self.field = field self.field = field
super().__init__(**kwargs) super().__init__(**kwargs)
@staticmethod
def _lazy_load_refs(instance, name, ref_values, *, max_depth):
_dereference = _import_class("DeReference")()
documents = _dereference(
ref_values,
max_depth=max_depth,
instance=instance,
name=name,
)
return documents
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor to automatically dereference references.""" """Descriptor to automatically dereference references."""
if instance is None: if instance is None:
@ -284,19 +295,15 @@ class ComplexBaseField(BaseField):
or isinstance(self.field, (GenericReferenceField, ReferenceField)) or isinstance(self.field, (GenericReferenceField, ReferenceField))
) )
_dereference = _import_class("DeReference")()
if ( if (
instance._initialised instance._initialised
and dereference and dereference
and instance._data.get(self.name) and instance._data.get(self.name)
and not getattr(instance._data[self.name], "_dereferenced", False) and not getattr(instance._data[self.name], "_dereferenced", False)
): ):
instance._data[self.name] = _dereference( ref_values = instance._data.get(self.name)
instance._data.get(self.name), instance._data[self.name] = self._lazy_load_refs(
max_depth=1, ref_values=ref_values, instance=instance, name=self.name, max_depth=1
instance=instance,
name=self.name,
) )
if hasattr(instance._data[self.name], "_dereferenced"): if hasattr(instance._data[self.name], "_dereferenced"):
instance._data[self.name]._dereferenced = True instance._data[self.name]._dereferenced = True
@ -322,7 +329,9 @@ class ComplexBaseField(BaseField):
and isinstance(value, (BaseList, BaseDict)) and isinstance(value, (BaseList, BaseDict))
and not value._dereferenced and not value._dereferenced
): ):
value = _dereference(value, max_depth=1, instance=instance, name=self.name) value = self._lazy_load_refs(
ref_values=value, instance=instance, name=self.name, max_depth=1
)
value._dereferenced = True value._dereferenced = True
instance._data[self.name] = value instance._data[self.name] = value

View File

@ -915,7 +915,7 @@ class ListField(ComplexBaseField):
"""A list field that wraps a standard field, allowing multiple instances """A list field that wraps a standard field, allowing multiple instances
of the field to be used as a list in the database. of the field to be used as a list in the database.
If using with ReferenceFields see: :ref:`one-to-many-with-listfields` If using with ReferenceFields see: :ref:`many-to-many-with-listfields`
.. note:: .. note::
Required means it cannot be empty - as the default for ListFields is [] Required means it cannot be empty - as the default for ListFields is []
@ -1194,6 +1194,14 @@ class ReferenceField(BaseField):
self.document_type_obj = get_document(self.document_type_obj) self.document_type_obj = get_document(self.document_type_obj)
return self.document_type_obj return self.document_type_obj
@staticmethod
def _lazy_load_ref(ref_cls, dbref):
dereferenced_son = ref_cls._get_db().dereference(dbref)
if dereferenced_son is None:
raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
return ref_cls._from_son(dereferenced_son)
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor to allow lazy dereferencing.""" """Descriptor to allow lazy dereferencing."""
if instance is None: if instance is None:
@ -1201,20 +1209,17 @@ class ReferenceField(BaseField):
return self return self
# Get value from document instance if available # Get value from document instance if available
value = instance._data.get(self.name) ref_value = instance._data.get(self.name)
auto_dereference = instance._fields[self.name]._auto_dereference auto_dereference = instance._fields[self.name]._auto_dereference
# Dereference DBRefs # Dereference DBRefs
if auto_dereference and isinstance(value, DBRef): if auto_dereference and isinstance(ref_value, DBRef):
if hasattr(value, "cls"): if hasattr(ref_value, "cls"):
# Dereference using the class type specified in the reference # Dereference using the class type specified in the reference
cls = get_document(value.cls) cls = get_document(ref_value.cls)
else: else:
cls = self.document_type cls = self.document_type
dereferenced = cls._get_db().dereference(value)
if dereferenced is None: instance._data[self.name] = self._lazy_load_ref(cls, ref_value)
raise DoesNotExist("Trying to dereference unknown document %s" % value)
else:
instance._data[self.name] = cls._from_son(dereferenced)
return super().__get__(instance, owner) return super().__get__(instance, owner)
@ -1353,6 +1358,14 @@ class CachedReferenceField(BaseField):
self.document_type_obj = get_document(self.document_type_obj) self.document_type_obj = get_document(self.document_type_obj)
return self.document_type_obj return self.document_type_obj
@staticmethod
def _lazy_load_ref(ref_cls, dbref):
dereferenced_son = ref_cls._get_db().dereference(dbref)
if dereferenced_son is None:
raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
return ref_cls._from_son(dereferenced_son)
def __get__(self, instance, owner): def __get__(self, instance, owner):
if instance is None: if instance is None:
# Document class being used rather than a document object # Document class being used rather than a document object
@ -1364,11 +1377,7 @@ class CachedReferenceField(BaseField):
# Dereference DBRefs # Dereference DBRefs
if auto_dereference and isinstance(value, DBRef): if auto_dereference and isinstance(value, DBRef):
dereferenced = self.document_type._get_db().dereference(value) instance._data[self.name] = self._lazy_load_ref(self.document_type, value)
if dereferenced is None:
raise DoesNotExist("Trying to dereference unknown document %s" % value)
else:
instance._data[self.name] = self.document_type._from_son(dereferenced)
return super().__get__(instance, owner) return super().__get__(instance, owner)
@ -1493,6 +1502,14 @@ class GenericReferenceField(BaseField):
value = value._class_name value = value._class_name
super()._validate_choices(value) super()._validate_choices(value)
@staticmethod
def _lazy_load_ref(ref_cls, dbref):
dereferenced_son = ref_cls._get_db().dereference(dbref)
if dereferenced_son is None:
raise DoesNotExist(f"Trying to dereference unknown document {dbref}")
return ref_cls._from_son(dereferenced_son)
def __get__(self, instance, owner): def __get__(self, instance, owner):
if instance is None: if instance is None:
return self return self
@ -1500,12 +1517,9 @@ class GenericReferenceField(BaseField):
value = instance._data.get(self.name) value = instance._data.get(self.name)
auto_dereference = instance._fields[self.name]._auto_dereference auto_dereference = instance._fields[self.name]._auto_dereference
if auto_dereference and isinstance(value, (dict, SON)): if auto_dereference and isinstance(value, dict):
dereferenced = self.dereference(value) doc_cls = get_document(value["_cls"])
if dereferenced is None: instance._data[self.name] = self._lazy_load_ref(doc_cls, value["_ref"])
raise DoesNotExist("Trying to dereference unknown document %s" % value)
else:
instance._data[self.name] = dereferenced
return super().__get__(instance, owner) return super().__get__(instance, owner)
@ -1524,14 +1538,6 @@ class GenericReferenceField(BaseField):
" saved to the database" " saved to the database"
) )
def dereference(self, value):
doc_cls = get_document(value["_cls"])
reference = value["_ref"]
doc = doc_cls._get_db().dereference(reference)
if doc is not None:
doc = doc_cls._from_son(doc)
return doc
def to_mongo(self, document): def to_mongo(self, document):
if document is None: if document is None:
return None return None

View File

@ -8,6 +8,8 @@ from mongoengine.connection import get_connection
# get_mongodb_version() # get_mongodb_version()
MONGODB_34 = (3, 4) MONGODB_34 = (3, 4)
MONGODB_36 = (3, 6) MONGODB_36 = (3, 6)
MONGODB_42 = (4, 2)
MONGODB_44 = (4, 4)
def get_mongodb_version(): def get_mongodb_version():

View File

@ -1355,21 +1355,18 @@ class BaseQuerySet:
MapReduceDocument = _import_class("MapReduceDocument") MapReduceDocument = _import_class("MapReduceDocument")
if not hasattr(self._collection, "map_reduce"):
raise NotImplementedError("Requires MongoDB >= 1.7.1")
map_f_scope = {} map_f_scope = {}
if isinstance(map_f, Code): if isinstance(map_f, Code):
map_f_scope = map_f.scope map_f_scope = map_f.scope
map_f = str(map_f) map_f = str(map_f)
map_f = Code(queryset._sub_js_fields(map_f), map_f_scope) map_f = Code(queryset._sub_js_fields(map_f), map_f_scope or None)
reduce_f_scope = {} reduce_f_scope = {}
if isinstance(reduce_f, Code): if isinstance(reduce_f, Code):
reduce_f_scope = reduce_f.scope reduce_f_scope = reduce_f.scope
reduce_f = str(reduce_f) reduce_f = str(reduce_f)
reduce_f_code = queryset._sub_js_fields(reduce_f) reduce_f_code = queryset._sub_js_fields(reduce_f)
reduce_f = Code(reduce_f_code, reduce_f_scope) reduce_f = Code(reduce_f_code, reduce_f_scope or None)
mr_args = {"query": queryset._query} mr_args = {"query": queryset._query}
@ -1379,7 +1376,7 @@ class BaseQuerySet:
finalize_f_scope = finalize_f.scope finalize_f_scope = finalize_f.scope
finalize_f = str(finalize_f) finalize_f = str(finalize_f)
finalize_f_code = queryset._sub_js_fields(finalize_f) finalize_f_code = queryset._sub_js_fields(finalize_f)
finalize_f = Code(finalize_f_code, finalize_f_scope) finalize_f = Code(finalize_f_code, finalize_f_scope or None)
mr_args["finalize"] = finalize_f mr_args["finalize"] = finalize_f
if scope: if scope:

View File

@ -7,6 +7,7 @@ import pytest
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.mongodb_support import MONGODB_42, get_mongodb_version
class TestIndexes(unittest.TestCase): class TestIndexes(unittest.TestCase):
@ -452,9 +453,11 @@ class TestIndexes(unittest.TestCase):
.get("stage") .get("stage")
== "IXSCAN" == "IXSCAN"
) )
mongo_db = get_mongodb_version()
PROJECTION_STR = "PROJECTION" if mongo_db < MONGODB_42 else "PROJECTION_COVERED"
assert ( assert (
query_plan.get("queryPlanner").get("winningPlan").get("stage") query_plan.get("queryPlanner").get("winningPlan").get("stage")
== "PROJECTION" == PROJECTION_STR
) )
query_plan = Test.objects(a=1).explain() query_plan = Test.objects(a=1).explain()

View File

@ -21,7 +21,10 @@ from mongoengine.queryset import (
QuerySetManager, QuerySetManager,
queryset_manager, queryset_manager,
) )
from tests.utils import requires_mongodb_gte_44 from tests.utils import (
requires_mongodb_gte_44,
requires_mongodb_lt_42,
)
class db_ops_tracker(query_counter): class db_ops_tracker(query_counter):
@ -1490,6 +1493,7 @@ class TestQueryset(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
@requires_mongodb_lt_42
def test_exec_js_query(self): def test_exec_js_query(self):
"""Ensure that queries are properly formed for use in exec_js.""" """Ensure that queries are properly formed for use in exec_js."""
@ -1527,6 +1531,7 @@ class TestQueryset(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
@requires_mongodb_lt_42
def test_exec_js_field_sub(self): def test_exec_js_field_sub(self):
"""Ensure that field substitutions occur properly in exec_js functions.""" """Ensure that field substitutions occur properly in exec_js functions."""
@ -2660,6 +2665,8 @@ class TestQueryset(unittest.TestCase):
title = StringField(primary_key=True) title = StringField(primary_key=True)
tags = ListField(StringField()) tags = ListField(StringField())
BlogPost.drop_collection()
post1 = BlogPost(title="Post #1", tags=["mongodb", "mongoengine"]) post1 = BlogPost(title="Post #1", tags=["mongodb", "mongoengine"])
post2 = BlogPost(title="Post #2", tags=["django", "mongodb"]) post2 = BlogPost(title="Post #2", tags=["django", "mongodb"])
post3 = BlogPost(title="Post #3", tags=["hitchcock films"]) post3 = BlogPost(title="Post #3", tags=["hitchcock films"])
@ -2688,12 +2695,15 @@ class TestQueryset(unittest.TestCase):
} }
""" """
results = BlogPost.objects.map_reduce(map_f, reduce_f, "myresults") results = BlogPost.objects.order_by("_id").map_reduce(
map_f, reduce_f, "myresults2"
)
results = list(results) results = list(results)
assert results[0].object == post1 assert len(results) == 3
assert results[1].object == post2 assert results[0].object.id == post1.id
assert results[2].object == post3 assert results[1].object.id == post2.id
assert results[2].object.id == post3.id
BlogPost.drop_collection() BlogPost.drop_collection()
@ -2701,7 +2711,6 @@ class TestQueryset(unittest.TestCase):
""" """
Test map/reduce custom output Test map/reduce custom output
""" """
register_connection("test2", "mongoenginetest2")
class Family(Document): class Family(Document):
id = IntField(primary_key=True) id = IntField(primary_key=True)
@ -2774,6 +2783,7 @@ class TestQueryset(unittest.TestCase):
family.persons.push(person); family.persons.push(person);
family.totalAge += person.age; family.totalAge += person.age;
}); });
family.persons.sort((a, b) => (a.age > b.age))
} }
}); });
@ -2802,10 +2812,10 @@ class TestQueryset(unittest.TestCase):
"_id": 1, "_id": 1,
"value": { "value": {
"persons": [ "persons": [
{"age": 21, "name": "Wilson Jr"},
{"age": 45, "name": "Wilson Father"},
{"age": 40, "name": "Eliana Costa"},
{"age": 17, "name": "Tayza Mariana"}, {"age": 17, "name": "Tayza Mariana"},
{"age": 21, "name": "Wilson Jr"},
{"age": 40, "name": "Eliana Costa"},
{"age": 45, "name": "Wilson Father"},
], ],
"totalAge": 123, "totalAge": 123,
}, },
@ -2815,9 +2825,9 @@ class TestQueryset(unittest.TestCase):
"_id": 2, "_id": 2,
"value": { "value": {
"persons": [ "persons": [
{"age": 10, "name": "Igor Gabriel"},
{"age": 16, "name": "Isabella Luanna"}, {"age": 16, "name": "Isabella Luanna"},
{"age": 36, "name": "Sandra Mara"}, {"age": 36, "name": "Sandra Mara"},
{"age": 10, "name": "Igor Gabriel"},
], ],
"totalAge": 62, "totalAge": 62,
}, },
@ -2827,8 +2837,8 @@ class TestQueryset(unittest.TestCase):
"_id": 3, "_id": 3,
"value": { "value": {
"persons": [ "persons": [
{"age": 30, "name": "Arthur WA"},
{"age": 25, "name": "Paula Leonel"}, {"age": 25, "name": "Paula Leonel"},
{"age": 30, "name": "Arthur WA"},
], ],
"totalAge": 55, "totalAge": 55,
}, },
@ -3109,6 +3119,7 @@ class TestQueryset(unittest.TestCase):
freq = Person.objects.item_frequencies("city", normalize=True, map_reduce=True) freq = Person.objects.item_frequencies("city", normalize=True, map_reduce=True)
assert freq == {"CRB": 0.5, None: 0.5} assert freq == {"CRB": 0.5, None: 0.5}
@requires_mongodb_lt_42
def test_item_frequencies_with_null_embedded(self): def test_item_frequencies_with_null_embedded(self):
class Data(EmbeddedDocument): class Data(EmbeddedDocument):
name = StringField() name = StringField()
@ -3137,6 +3148,7 @@ class TestQueryset(unittest.TestCase):
ot = Person.objects.item_frequencies("extra.tag", map_reduce=True) ot = Person.objects.item_frequencies("extra.tag", map_reduce=True)
assert ot == {None: 1.0, "friend": 1.0} assert ot == {None: 1.0, "friend": 1.0}
@requires_mongodb_lt_42
def test_item_frequencies_with_0_values(self): def test_item_frequencies_with_0_values(self):
class Test(Document): class Test(Document):
val = IntField() val = IntField()
@ -3151,6 +3163,7 @@ class TestQueryset(unittest.TestCase):
ot = Test.objects.item_frequencies("val", map_reduce=False) ot = Test.objects.item_frequencies("val", map_reduce=False)
assert ot == {0: 1} assert ot == {0: 1}
@requires_mongodb_lt_42
def test_item_frequencies_with_False_values(self): def test_item_frequencies_with_False_values(self):
class Test(Document): class Test(Document):
val = BooleanField() val = BooleanField()
@ -3165,6 +3178,7 @@ class TestQueryset(unittest.TestCase):
ot = Test.objects.item_frequencies("val", map_reduce=False) ot = Test.objects.item_frequencies("val", map_reduce=False)
assert ot == {False: 1} assert ot == {False: 1}
@requires_mongodb_lt_42
def test_item_frequencies_normalize(self): def test_item_frequencies_normalize(self):
class Test(Document): class Test(Document):
val = IntField() val = IntField()
@ -3551,7 +3565,8 @@ class TestQueryset(unittest.TestCase):
Book.objects.create(title="The Stories", authors=[mark_twain, john_tolkien]) Book.objects.create(title="The Stories", authors=[mark_twain, john_tolkien])
authors = Book.objects.distinct("authors") authors = Book.objects.distinct("authors")
assert authors == [mark_twain, john_tolkien] authors_names = {author.name for author in authors}
assert authors_names == {mark_twain.name, john_tolkien.name}
def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self): def test_distinct_ListField_EmbeddedDocumentField_EmbeddedDocumentField(self):
class Continent(EmbeddedDocument): class Continent(EmbeddedDocument):
@ -3588,7 +3603,8 @@ class TestQueryset(unittest.TestCase):
assert country_list == [scotland, tibet] assert country_list == [scotland, tibet]
continent_list = Book.objects.distinct("authors.country.continent") continent_list = Book.objects.distinct("authors.country.continent")
assert continent_list == [europe, asia] continent_list_names = {c.continent_name for c in continent_list}
assert continent_list_names == {europe.continent_name, asia.continent_name}
def test_distinct_ListField_ReferenceField(self): def test_distinct_ListField_ReferenceField(self):
class Bar(Document): class Bar(Document):

View File

@ -34,6 +34,10 @@ def get_as_pymongo(doc):
return doc.__class__.objects.as_pymongo().get(id=doc.id) return doc.__class__.objects.as_pymongo().get(id=doc.id)
def requires_mongodb_lt_42(func):
return _decorated_with_ver_requirement(func, (4, 2), oper=operator.lt)
def requires_mongodb_gte_44(func): def requires_mongodb_gte_44(func):
return _decorated_with_ver_requirement(func, (4, 4), oper=operator.ge) return _decorated_with_ver_requirement(func, (4, 4), oper=operator.ge)