Skip to content

Commit 2f3afce

Browse files
committed
Merge branch 'array_filters' of https://github.com/idoshr/mongoengine into array_filters
# Conflicts: # docs/changelog.rst
2 parents 9405978 + ca5d4fa commit 2f3afce

File tree

9 files changed

+223
-124
lines changed

9 files changed

+223
-124
lines changed

docs/changelog.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ Development
88
===========
99
- (Fill this out as you fix issues and develop your features).
1010
- Fix for uuidRepresentation not read when provided in URI #2741
11+
- Add option to user array_filters https://www.mongodb.com/docs/manual/reference/operator/update/positional-filtered/
12+
- Fix combination of __raw__ and mongoengine syntax #2773
1113
- Add tests against MongoDB 6.0 and MongoDB 7.0 in the pipeline
1214
- Fix validate() not being called when inheritance is used in EmbeddedDocument and validate is overriden #2784
1315
- Add support for readPreferenceTags in connection parameters #2644
14-
- Add option to user array_filters https://www.mongodb.com/docs/manual/reference/operator/update/positional-filtered/
16+
- Use estimated_documents_count OR documents_count when count is called, based on the query #2529
17+
- Fix no_dereference context manager which wasn't turning off auto-dereferencing correctly in some cases #2788
18+
- BREAKING CHANGE: no_dereference context manager no longer returns the class in __enter__ #2788
19+
as it was useless and making it look like it was returning a different class although it was the same.
20+
Thus, it must be called like `with no_dereference(User):` and no longer `with no_dereference(User) as ...:`
21+
- Added __raw__ to :meth:`~mongoengine.Queryset.order_by()` to allow to provide raw pymongo 'sort' argument and get around some of the limitations #2783
1522

1623
Changes in 0.27.0
1724
=================

docs/guide/querying.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,7 @@ data. To turn off dereferencing of the results of a query use
540540
You can also turn off all dereferencing for a fixed period by using the
541541
:class:`~mongoengine.context_managers.no_dereference` context manager::
542542

543-
with no_dereference(Post) as Post:
543+
with no_dereference(Post):
544544
post = Post.objects.first()
545545
assert(isinstance(post.author, DBRef))
546546

@@ -605,6 +605,7 @@ There are several different "modifiers" that you may use with these methods:
605605
* ``add_to_set`` -- add value to a list only if its not in the list already
606606
* ``rename`` -- rename the key name
607607

608+
.. _need to add upsert=True: http://docs.mongodb.org/manual/reference/operator/update/setOnInsert
608609
.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/
609610

610611
The syntax for atomic updates is similar to the querying syntax, but the

mongoengine/context_managers.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import threading
12
from contextlib import contextmanager
23

34
from pymongo.read_concern import ReadConcern
@@ -15,9 +16,29 @@
1516
"query_counter",
1617
"set_write_concern",
1718
"set_read_write_concern",
19+
"no_dereferencing_active_for_class",
1820
)
1921

2022

23+
thread_locals = threading.local()
24+
thread_locals.no_dereferencing_class = {}
25+
26+
27+
def no_dereferencing_active_for_class(cls):
28+
return cls in thread_locals.no_dereferencing_class
29+
30+
31+
def _register_no_dereferencing_for_class(cls):
32+
thread_locals.no_dereferencing_class.setdefault(cls, 0)
33+
thread_locals.no_dereferencing_class[cls] += 1
34+
35+
36+
def _unregister_no_dereferencing_for_class(cls):
37+
thread_locals.no_dereferencing_class[cls] -= 1
38+
if thread_locals.no_dereferencing_class[cls] == 0:
39+
thread_locals.no_dereferencing_class.pop(cls)
40+
41+
2142
class switch_db:
2243
"""switch_db alias context manager.
2344
@@ -107,7 +128,7 @@ class no_dereference:
107128
Turns off all dereferencing in Documents for the duration of the context
108129
manager::
109130
110-
with no_dereference(Group) as Group:
131+
with no_dereference(Group):
111132
Group.objects.find()
112133
"""
113134

@@ -130,15 +151,17 @@ def __init__(self, cls):
130151

131152
def __enter__(self):
132153
"""Change the objects default and _auto_dereference values."""
154+
_register_no_dereferencing_for_class(self.cls)
155+
133156
for field in self.deref_fields:
134157
self.cls._fields[field]._auto_dereference = False
135-
return self.cls
136158

137159
def __exit__(self, t, value, traceback):
138160
"""Reset the default and _auto_dereference values."""
161+
_unregister_no_dereferencing_for_class(self.cls)
162+
139163
for field in self.deref_fields:
140164
self.cls._fields[field]._auto_dereference = True
141-
return self.cls
142165

143166

144167
class no_sub_classes:

mongoengine/pymongo_support.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ def count_documents(
3737
# count_documents appeared in pymongo 3.7
3838
if PYMONGO_VERSION >= (3, 7):
3939
try:
40-
return collection.count_documents(filter=filter, **kwargs)
40+
if not filter and set(kwargs) <= {"max_time_ms"}:
41+
# when no filter is provided, estimated_document_count
42+
# is a lot faster as it uses the collection metadata
43+
return collection.estimated_document_count(**kwargs)
44+
else:
45+
return collection.count_documents(filter=filter, **kwargs)
4146
except OperationFailure as err:
4247
if PYMONGO_VERSION >= (4,):
4348
raise
@@ -46,15 +51,10 @@ def count_documents(
4651
# with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
4752
# fallback to deprecated Cursor.count
4853
# Keeping this should be reevaluated the day pymongo removes .count entirely
49-
message = str(err)
50-
if not (
51-
"not allowed in this context" in message
52-
and (
53-
"$where" in message
54-
or "$geoNear" in message
55-
or "$near" in message
56-
or "$nearSphere" in message
57-
)
54+
if (
55+
"$geoNear, $near, and $nearSphere are not allowed in this context"
56+
not in str(err)
57+
and "$where is not allowed in this context" not in str(err)
5858
):
5959
raise
6060

mongoengine/queryset/base.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from mongoengine.common import _import_class
1818
from mongoengine.connection import get_db
1919
from mongoengine.context_managers import (
20+
no_dereferencing_active_for_class,
2021
set_read_write_concern,
2122
set_write_concern,
2223
switch_db,
@@ -51,9 +52,6 @@ class BaseQuerySet:
5152
providing :class:`~mongoengine.Document` objects as the results.
5253
"""
5354

54-
__dereference = False
55-
_auto_dereference = True
56-
5755
def __init__(self, document, collection):
5856
self._document = document
5957
self._collection_obj = collection
@@ -74,6 +72,9 @@ def __init__(self, document, collection):
7472
self._as_pymongo = False
7573
self._search_text = None
7674

75+
self.__dereference = False
76+
self.__auto_dereference = True
77+
7778
# If inheritance is allowed, only return instances and instances of
7879
# subclasses of the class being used
7980
if document._meta.get("allow_inheritance") is True:
@@ -801,7 +802,7 @@ def clone(self):
801802
return self._clone_into(self.__class__(self._document, self._collection_obj))
802803

803804
def _clone_into(self, new_qs):
804-
"""Copy all of the relevant properties of this queryset to
805+
"""Copy all the relevant properties of this queryset to
805806
a new queryset (which has to be an instance of
806807
:class:`~mongoengine.queryset.base.BaseQuerySet`).
807808
"""
@@ -831,7 +832,6 @@ def _clone_into(self, new_qs):
831832
"_empty",
832833
"_hint",
833834
"_collation",
834-
"_auto_dereference",
835835
"_search_text",
836836
"_max_time_ms",
837837
"_comment",
@@ -842,6 +842,8 @@ def _clone_into(self, new_qs):
842842
val = getattr(self, prop)
843843
setattr(new_qs, prop, copy.copy(val))
844844

845+
new_qs.__auto_dereference = self._BaseQuerySet__auto_dereference
846+
845847
if self._cursor_obj:
846848
new_qs._cursor_obj = self._cursor_obj.clone()
847849

@@ -1117,7 +1119,7 @@ def all_fields(self):
11171119
)
11181120
return queryset
11191121

1120-
def order_by(self, *keys):
1122+
def order_by(self, *keys, __raw__=None):
11211123
"""Order the :class:`~mongoengine.queryset.QuerySet` by the given keys.
11221124
11231125
The order may be specified by prepending each of the keys by a "+" or
@@ -1127,11 +1129,19 @@ def order_by(self, *keys):
11271129
11281130
:param keys: fields to order the query results by; keys may be
11291131
prefixed with "+" or a "-" to determine the ordering direction.
1132+
:param __raw__: a raw pymongo "sort" argument (provided as a list of (key, direction))
1133+
see 'key_or_list' in `pymongo.cursor.Cursor.sort doc <https://pymongo.readthedocs.io/en/stable/api/pymongo/cursor.html#pymongo.cursor.Cursor.sort>`.
1134+
If both keys and __raw__ are provided, an exception is raised
11301135
"""
1131-
queryset = self.clone()
1136+
if __raw__ and keys:
1137+
raise OperationError("Can not use both keys and __raw__ with order_by() ")
11321138

1139+
queryset = self.clone()
11331140
old_ordering = queryset._ordering
1134-
new_ordering = queryset._get_order_by(keys)
1141+
if __raw__:
1142+
new_ordering = __raw__
1143+
else:
1144+
new_ordering = queryset._get_order_by(keys)
11351145

11361146
if queryset._cursor_obj:
11371147
# If a cursor object has already been created, apply the sort to it
@@ -1747,10 +1757,15 @@ def _dereference(self):
17471757
self.__dereference = _import_class("DeReference")()
17481758
return self.__dereference
17491759

1760+
@property
1761+
def _auto_dereference(self):
1762+
should_deref = not no_dereferencing_active_for_class(self._document)
1763+
return should_deref and self.__auto_dereference
1764+
17501765
def no_dereference(self):
17511766
"""Turn off any dereferencing for the results of this queryset."""
17521767
queryset = self.clone()
1753-
queryset._auto_dereference = False
1768+
queryset.__auto_dereference = False
17541769
return queryset
17551770

17561771
# Helper Functions

mongoengine/queryset/transform.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,23 @@
6262
)
6363

6464

65+
def handle_raw_query(value, mongo_query):
66+
"""Combine a raw query with an existing one"""
67+
for op, v in value.items():
68+
if op not in mongo_query:
69+
mongo_query[op] = v
70+
elif op in mongo_query and isinstance(mongo_query[op], dict):
71+
mongo_query[op].update(v)
72+
73+
6574
# TODO make this less complex
6675
def query(_doc_cls=None, **kwargs):
6776
"""Transform a query from Django-style format to Mongo format."""
6877
mongo_query = {}
6978
merge_query = defaultdict(list)
7079
for key, value in sorted(kwargs.items()):
7180
if key == "__raw__":
72-
mongo_query.update(value)
81+
handle_raw_query(value, mongo_query)
7382
continue
7483

7584
parts = key.rsplit("__")
@@ -234,7 +243,7 @@ def update(_doc_cls=None, **update):
234243

235244
for key, value in update.items():
236245
if key == "__raw__":
237-
mongo_update.update(value)
246+
handle_raw_query(value, mongo_update)
238247
continue
239248

240249
parts = key.split("__")

tests/document/test_indexes.py

Lines changed: 11 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from mongoengine.connection import get_db
1010
from mongoengine.mongodb_support import (
1111
MONGODB_42,
12-
MONGODB_70,
1312
get_mongodb_version,
1413
)
1514
from mongoengine.pymongo_support import PYMONGO_VERSION
@@ -451,89 +450,29 @@ class Test(Document):
451450
# the documents returned might have more keys in that here.
452451
query_plan = Test.objects(id=obj.id).exclude("a").explain()
453452
assert (
454-
query_plan.get("queryPlanner")
455-
.get("winningPlan")
456-
.get("inputStage")
457-
.get("stage")
458-
== "IDHACK"
453+
query_plan["queryPlanner"]["winningPlan"]["inputStage"]["stage"] == "IDHACK"
459454
)
460455

461456
query_plan = Test.objects(id=obj.id).only("id").explain()
462457
assert (
463-
query_plan.get("queryPlanner")
464-
.get("winningPlan")
465-
.get("inputStage")
466-
.get("stage")
467-
== "IDHACK"
458+
query_plan["queryPlanner"]["winningPlan"]["inputStage"]["stage"] == "IDHACK"
468459
)
469460

470461
mongo_db = get_mongodb_version()
471462
query_plan = Test.objects(a=1).only("a").exclude("id").explain()
472-
if mongo_db < MONGODB_70:
473-
assert (
474-
query_plan.get("queryPlanner")
475-
.get("winningPlan")
476-
.get("inputStage")
477-
.get("stage")
478-
== "IXSCAN"
479-
)
480-
else:
481-
assert (
482-
query_plan.get("queryPlanner")
483-
.get("winningPlan")
484-
.get("queryPlan")
485-
.get("inputStage")
486-
.get("stage")
487-
== "IXSCAN"
488-
)
463+
assert (
464+
query_plan["queryPlanner"]["winningPlan"]["inputStage"]["stage"] == "IXSCAN"
465+
)
489466

490467
PROJECTION_STR = "PROJECTION" if mongo_db < MONGODB_42 else "PROJECTION_COVERED"
491-
if mongo_db < MONGODB_70:
492-
assert (
493-
query_plan.get("queryPlanner").get("winningPlan").get("stage")
494-
== PROJECTION_STR
495-
)
496-
else:
497-
assert (
498-
query_plan.get("queryPlanner")
499-
.get("winningPlan")
500-
.get("queryPlan")
501-
.get("stage")
502-
== PROJECTION_STR
503-
)
468+
assert query_plan["queryPlanner"]["winningPlan"]["stage"] == PROJECTION_STR
504469

505470
query_plan = Test.objects(a=1).explain()
506-
if mongo_db < MONGODB_70:
507-
assert (
508-
query_plan.get("queryPlanner")
509-
.get("winningPlan")
510-
.get("inputStage")
511-
.get("stage")
512-
== "IXSCAN"
513-
)
514-
else:
515-
assert (
516-
query_plan.get("queryPlanner")
517-
.get("winningPlan")
518-
.get("queryPlan")
519-
.get("inputStage")
520-
.get("stage")
521-
== "IXSCAN"
522-
)
523-
524-
if mongo_db < MONGODB_70:
525-
assert (
526-
query_plan.get("queryPlanner").get("winningPlan").get("stage")
527-
== "FETCH"
528-
)
529-
else:
530-
assert (
531-
query_plan.get("queryPlanner")
532-
.get("winningPlan")
533-
.get("queryPlan")
534-
.get("stage")
535-
== "FETCH"
536-
)
471+
assert (
472+
query_plan["queryPlanner"]["winningPlan"]["inputStage"]["stage"] == "IXSCAN"
473+
)
474+
475+
assert query_plan.get("queryPlanner").get("winningPlan").get("stage") == "FETCH"
537476

538477
def test_index_on_id(self):
539478
class BlogPost(Document):

0 commit comments

Comments
 (0)