Skip to content

Commit 130e9c5

Browse files
authored
Merge pull request #2314 from bagerard/abarto-feature/allow-setting-read-concern-queryset
Abarto feature/allow setting read concern queryset
2 parents 9b73be2 + 78c9e97 commit 130e9c5

File tree

6 files changed

+152
-6
lines changed

6 files changed

+152
-6
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,3 +256,4 @@ that much better:
256256
* Eric Timmons (https://github.com/daewok)
257257
* Matthew Simpson (https://github.com/mcsimps2)
258258
* Leonardo Domingues (https://github.com/leodmgs)
259+
* Agustin Barto (https://github.com/abarto)

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Development
2121
- ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes``, the right method to use is ``Document.ensure_indexes``
2222
- Added pre-commit #2212
2323
- Renamed requirements-lint.txt to requirements-dev.txt #2212
24+
- Support for setting ReadConcern #2255
2425

2526
Changes in 0.19.1
2627
=================

mongoengine/context_managers.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from contextlib import contextmanager
22

3+
from pymongo.read_concern import ReadConcern
34
from pymongo.write_concern import WriteConcern
45

56
from mongoengine.common import _import_class
@@ -13,6 +14,7 @@
1314
"no_sub_classes",
1415
"query_counter",
1516
"set_write_concern",
17+
"set_read_write_concern",
1618
)
1719

1820

@@ -256,3 +258,21 @@ def set_write_concern(collection, write_concerns):
256258
combined_concerns = dict(collection.write_concern.document.items())
257259
combined_concerns.update(write_concerns)
258260
yield collection.with_options(write_concern=WriteConcern(**combined_concerns))
261+
262+
263+
@contextmanager
264+
def set_read_write_concern(collection, write_concerns, read_concerns):
265+
combined_write_concerns = dict(collection.write_concern.document.items())
266+
267+
if write_concerns is not None:
268+
combined_write_concerns.update(write_concerns)
269+
270+
combined_read_concerns = dict(collection.read_concern.document.items())
271+
272+
if read_concerns is not None:
273+
combined_read_concerns.update(read_concerns)
274+
275+
yield collection.with_options(
276+
write_concern=WriteConcern(**combined_write_concerns),
277+
read_concern=ReadConcern(**combined_read_concerns),
278+
)

mongoengine/queryset/base.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,25 @@
33
import re
44
import warnings
55

6+
from collections.abc import Mapping
7+
68
from bson import SON, json_util
79
from bson.code import Code
810
import pymongo
911
import pymongo.errors
1012
from pymongo.collection import ReturnDocument
1113
from pymongo.common import validate_read_preference
14+
from pymongo.read_concern import ReadConcern
1215

1316
from mongoengine import signals
1417
from mongoengine.base import get_document
1518
from mongoengine.common import _import_class
1619
from mongoengine.connection import get_db
17-
from mongoengine.context_managers import set_write_concern, switch_db
20+
from mongoengine.context_managers import (
21+
set_read_write_concern,
22+
set_write_concern,
23+
switch_db,
24+
)
1825
from mongoengine.errors import (
1926
BulkWriteError,
2027
InvalidQueryError,
@@ -57,6 +64,7 @@ def __init__(self, document, collection):
5764
self._snapshot = False
5865
self._timeout = True
5966
self._read_preference = None
67+
self._read_concern = None
6068
self._iter = False
6169
self._scalar = []
6270
self._none = False
@@ -484,7 +492,13 @@ def delete(self, write_concern=None, _from_doc_delete=False, cascade_refs=None):
484492
return result.deleted_count
485493

486494
def update(
487-
self, upsert=False, multi=True, write_concern=None, full_result=False, **update
495+
self,
496+
upsert=False,
497+
multi=True,
498+
write_concern=None,
499+
read_concern=None,
500+
full_result=False,
501+
**update
488502
):
489503
"""Perform an atomic update on the fields matched by the query.
490504
@@ -496,6 +510,7 @@ def update(
496510
``save(..., write_concern={w: 2, fsync: True}, ...)`` will
497511
wait until at least two servers have recorded the write and
498512
will force an fsync on the primary server.
513+
:param read_concern: Override the read concern for the operation
499514
:param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number
500515
updated items
501516
:param update: Django-style update keyword arguments
@@ -522,7 +537,9 @@ def update(
522537
else:
523538
update["$set"] = {"_cls": queryset._document._class_name}
524539
try:
525-
with set_write_concern(queryset._collection, write_concern) as collection:
540+
with set_read_write_concern(
541+
queryset._collection, write_concern, read_concern
542+
) as collection:
526543
update_func = collection.update_one
527544
if multi:
528545
update_func = collection.update_many
@@ -539,7 +556,7 @@ def update(
539556
raise OperationError(message)
540557
raise OperationError("Update failed (%s)" % err)
541558

542-
def upsert_one(self, write_concern=None, **update):
559+
def upsert_one(self, write_concern=None, read_concern=None, **update):
543560
"""Overwrite or add the first document matched by the query.
544561
545562
:param write_concern: Extra keyword arguments are passed down which
@@ -548,6 +565,7 @@ def upsert_one(self, write_concern=None, **update):
548565
``save(..., write_concern={w: 2, fsync: True}, ...)`` will
549566
wait until at least two servers have recorded the write and
550567
will force an fsync on the primary server.
568+
:param read_concern: Override the read concern for the operation
551569
:param update: Django-style update keyword arguments
552570
553571
:returns the new or overwritten document
@@ -559,6 +577,7 @@ def upsert_one(self, write_concern=None, **update):
559577
multi=False,
560578
upsert=True,
561579
write_concern=write_concern,
580+
read_concern=read_concern,
562581
full_result=True,
563582
**update
564583
)
@@ -1177,6 +1196,22 @@ def read_preference(self, read_preference):
11771196
queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_preference
11781197
return queryset
11791198

1199+
def read_concern(self, read_concern):
1200+
"""Change the read_concern when querying.
1201+
1202+
:param read_concern: override ReplicaSetConnection-level
1203+
preference.
1204+
"""
1205+
if read_concern is not None and not isinstance(read_concern, Mapping):
1206+
raise TypeError("%r is not a valid read concern." % (read_concern,))
1207+
1208+
queryset = self.clone()
1209+
queryset._read_concern = (
1210+
ReadConcern(**read_concern) if read_concern is not None else None
1211+
)
1212+
queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_concern
1213+
return queryset
1214+
11801215
def scalar(self, *fields):
11811216
"""Instead of returning Document instances, return either a specific
11821217
value or a tuple of values in order.
@@ -1623,9 +1658,9 @@ def _cursor(self):
16231658
# XXX In PyMongo 3+, we define the read preference on a collection
16241659
# level, not a cursor level. Thus, we need to get a cloned collection
16251660
# object using `with_options` first.
1626-
if self._read_preference is not None:
1661+
if self._read_preference is not None or self._read_concern is not None:
16271662
self._cursor_obj = self._collection.with_options(
1628-
read_preference=self._read_preference
1663+
read_preference=self._read_preference, read_concern=self._read_concern
16291664
).find(self._query, **self._cursor_args)
16301665
else:
16311666
self._cursor_obj = self._collection.find(self._query, **self._cursor_args)

tests/queryset/test_queryset.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from bson import DBRef, ObjectId
99
import pymongo
10+
from pymongo.read_concern import ReadConcern
1011
from pymongo.read_preferences import ReadPreference
1112
from pymongo.results import UpdateResult
1213
import pytest
@@ -4726,6 +4727,46 @@ def assert_read_pref(qs, expected_read_pref):
47264727
)
47274728
assert_read_pref(bars, ReadPreference.SECONDARY_PREFERRED)
47284729

4730+
def test_read_concern(self):
4731+
class Bar(Document):
4732+
txt = StringField()
4733+
4734+
meta = {"indexes": ["txt"]}
4735+
4736+
Bar.drop_collection()
4737+
bar = Bar.objects.create(txt="xyz")
4738+
4739+
bars = list(Bar.objects.read_concern(None))
4740+
assert bars == [bar]
4741+
4742+
bars = Bar.objects.read_concern({"level": "local"})
4743+
assert bars._read_concern.document == {"level": "local"}
4744+
assert bars._cursor.collection.read_concern.document == {"level": "local"}
4745+
4746+
# Make sure that `.read_concern(...)` does not accept string values.
4747+
with pytest.raises(TypeError):
4748+
Bar.objects.read_concern("local")
4749+
4750+
def assert_read_concern(qs, expected_read_concern):
4751+
assert qs._read_concern.document == expected_read_concern
4752+
assert qs._cursor.collection.read_concern.document == expected_read_concern
4753+
4754+
# Make sure read concern is respected after a `.skip(...)`.
4755+
bars = Bar.objects.skip(1).read_concern({"level": "local"})
4756+
assert_read_concern(bars, {"level": "local"})
4757+
4758+
# Make sure read concern is respected after a `.limit(...)`.
4759+
bars = Bar.objects.limit(1).read_concern({"level": "local"})
4760+
assert_read_concern(bars, {"level": "local"})
4761+
4762+
# Make sure read concern is respected after an `.order_by(...)`.
4763+
bars = Bar.objects.order_by("txt").read_concern({"level": "local"})
4764+
assert_read_concern(bars, {"level": "local"})
4765+
4766+
# Make sure read concern is respected after a `.hint(...)`.
4767+
bars = Bar.objects.hint([("txt", 1)]).read_concern({"level": "majority"})
4768+
assert_read_concern(bars, {"level": "majority"})
4769+
47294770
def test_json_simple(self):
47304771
class Embedded(EmbeddedDocument):
47314772
string = StringField()

tests/test_context_managers.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,61 @@
88
no_dereference,
99
no_sub_classes,
1010
query_counter,
11+
set_read_write_concern,
12+
set_write_concern,
1113
switch_collection,
1214
switch_db,
1315
)
1416
from mongoengine.pymongo_support import count_documents
1517

1618

1719
class TestContextManagers:
20+
def test_set_write_concern(self):
21+
connect("mongoenginetest")
22+
23+
class User(Document):
24+
name = StringField()
25+
26+
collection = User._get_collection()
27+
original_write_concern = collection.write_concern
28+
29+
with set_write_concern(
30+
collection, {"w": "majority", "j": True, "wtimeout": 1234}
31+
) as updated_collection:
32+
assert updated_collection.write_concern.document == {
33+
"w": "majority",
34+
"j": True,
35+
"wtimeout": 1234,
36+
}
37+
38+
assert original_write_concern.document == collection.write_concern.document
39+
40+
def test_set_read_write_concern(self):
41+
connect("mongoenginetest")
42+
43+
class User(Document):
44+
name = StringField()
45+
46+
collection = User._get_collection()
47+
48+
original_read_concern = collection.read_concern
49+
original_write_concern = collection.write_concern
50+
51+
with set_read_write_concern(
52+
collection,
53+
{"w": "majority", "j": True, "wtimeout": 1234},
54+
{"level": "local"},
55+
) as update_collection:
56+
assert update_collection.read_concern.document == {"level": "local"}
57+
assert update_collection.write_concern.document == {
58+
"w": "majority",
59+
"j": True,
60+
"wtimeout": 1234,
61+
}
62+
63+
assert original_read_concern.document == collection.read_concern.document
64+
assert original_write_concern.document == collection.write_concern.document
65+
1866
def test_switch_db_context_manager(self):
1967
connect("mongoenginetest")
2068
register_connection("testdb-1", "mongoenginetest2")

0 commit comments

Comments
 (0)