Skip to content

Commit 90e141a

Browse files
authored
Merge pull request #290 from IlyaSkriblovsky/fix-pymongo4-compat
PyMongo 4+ support
2 parents 5e4eb16 + 9a3a7ab commit 90e141a

File tree

11 files changed

+114
-39
lines changed

11 files changed

+114
-39
lines changed

docs/source/NEWS.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
=========
33

4+
Release UPCOMING (yyyy-mm-dd)
5+
-----------------------------
6+
7+
API Changes
8+
^^^^^^^^^^^
9+
10+
- PyMongo 4+ is now supported. If you will migrate from PyMongo 3 to PyMongo 4, please be sure
11+
to check their PyMongo's guide because newer version has a number of incompatible changes.
12+
13+
414
Release 23.0.0 (2023-01-29)
515
---------------------------
616

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Twisted
2-
pymongo>=3.0
2+
pymongo>=3.0,<4.9
33
-e .

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
url="https://github.com/twisted/txmongo",
1111
keywords=["mongo", "mongodb", "pymongo", "gridfs", "txmongo"],
1212
packages=["txmongo", "txmongo._gridfs"],
13-
install_requires=["twisted>=14.0", "pymongo>=3.0, <4.0"],
13+
install_requires=["twisted>=14.0", "pymongo>=3.0, <4.9"],
1414
extras_require={
1515
'srv': ['pymongo[srv]>=3.6'],
1616
},

tests/basic/test_bulk.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from bson import BSON
22
from pymongo import InsertOne
3-
from pymongo.errors import BulkWriteError, OperationFailure, NotMasterError
3+
from pymongo.errors import BulkWriteError, OperationFailure
44
from pymongo.operations import UpdateOne, DeleteOne, UpdateMany, ReplaceOne
55
from pymongo.results import BulkWriteResult
66
from pymongo.write_concern import WriteConcern
@@ -10,6 +10,12 @@
1010
from tests.utils import SingleCollectionTest
1111
from txmongo.protocol import Reply
1212

13+
try:
14+
from pymongo.errors import NotPrimaryError
15+
except ImportError:
16+
# For pymongo < 3.12
17+
from pymongo.errors import NotMasterError as NotPrimaryError
18+
1319

1420
class TestArgsValidation(SingleCollectionTest):
1521

@@ -242,4 +248,4 @@ def fake_send_query(*args):
242248
with patch('txmongo.protocol.MongoProtocol.send_QUERY', side_effect=fake_send_query):
243249
yield self.assertFailure(
244250
self.coll.bulk_write([UpdateOne({}, {'$set': {'x': 42}}, upsert=True)], ordered=True),
245-
OperationFailure, NotMasterError)
251+
OperationFailure, NotPrimaryError)

tests/basic/test_collection.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616
Based on pymongo driver's test_collection.py
1717
"""
1818

19+
import uuid
20+
21+
from bson import UuidRepresentation, CodecOptions
1922
from pymongo import errors
2023
from twisted.internet import defer
2124
from twisted.trial import unittest
25+
2226
import txmongo
2327
from txmongo import filter as qf
2428
from txmongo.collection import Collection
@@ -446,3 +450,40 @@ def test_convert(self):
446450
{"filter": {'x': 42}, "projection": {'a': 1}, "skip": 5, "limit": 6,
447451
"sort": qf.sort([('s', 1)])}
448452
)
453+
454+
455+
class TestUuid(unittest.TestCase):
456+
457+
def setUp(self):
458+
self.conn = txmongo.MongoConnection(mongo_host, mongo_port, codec_options=CodecOptions(uuid_representation=UuidRepresentation.STANDARD))
459+
self.db = self.conn.mydb
460+
self.coll = self.db.conn
461+
462+
@defer.inlineCallbacks
463+
def tearDown(self):
464+
yield self.conn.drop_database(self.db)
465+
yield self.conn.disconnect()
466+
467+
@defer.inlineCallbacks
468+
def test_uuid_in_crud(self):
469+
task_id = uuid.uuid4()
470+
yield self.coll.insert({'task_id': task_id}, safe=True)
471+
472+
doc = yield self.coll.find_one(fields={"_id": 0})
473+
self.assertEqual(doc, {'task_id': task_id})
474+
475+
doc = yield self.coll.find_one({'task_id': task_id}, fields={"_id": 0})
476+
self.assertEqual(doc, {'task_id': task_id})
477+
478+
new = uuid.uuid4()
479+
yield self.coll.update_one({'task_id': task_id}, {'$set': {'task_id': new}})
480+
doc = yield self.coll.find_one()
481+
self.assertEqual(doc['task_id'], new)
482+
483+
yield self.coll.delete_one({'task_id': new})
484+
cnt = yield self.coll.count()
485+
self.assertEqual(cnt, 0)
486+
487+
@defer.inlineCallbacks
488+
def test_uuid_in_batch(self):
489+
yield self.coll.batch.insert_many([{'task_id': uuid.uuid4()} for _ in range(10)])

tests/basic/test_write_concern.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_AllOperations(self):
137137

138138
@defer.inlineCallbacks
139139
def test_ConnectionUrlParams(self):
140-
conn = ConnectionPool("mongodb://{0}:{1}/?w=2&j=true".format(mongo_host, mongo_port))
140+
conn = ConnectionPool("mongodb://{0}:{1}/?w=2&journal=true".format(mongo_host, mongo_port))
141141
coll = conn.mydb.mycol
142142

143143
try:

txmongo/collection.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,33 @@
22
# Use of this source code is governed by the Apache License that can be
33
# found in the LICENSE file.
44

5+
import collections.abc
56
import io
67
import struct
7-
import collections.abc
88
import warnings
9+
910
from bson import BSON, ObjectId
1011
from bson.code import Code
11-
from bson.son import SON
1212
from bson.codec_options import CodecOptions
13+
from bson.son import SON
1314
from pymongo.bulk import _Bulk, _COMMANDS
15+
from pymongo.collection import ReturnDocument
16+
from pymongo.common import validate_ok_for_update, validate_ok_for_replace, \
17+
validate_is_mapping, validate_boolean
1418
from pymongo.errors import InvalidName, BulkWriteError, InvalidOperation, OperationFailure
1519
from pymongo.message import _OP_MAP, _INSERT
1620
from pymongo.results import InsertOneResult, InsertManyResult, UpdateResult, \
1721
DeleteResult, BulkWriteResult
18-
from pymongo.common import validate_ok_for_update, validate_ok_for_replace, \
19-
validate_is_mapping, validate_boolean
20-
from pymongo.collection import ReturnDocument
2122
from pymongo.write_concern import WriteConcern
23+
from twisted.internet import defer
24+
from twisted.python.compat import comparable
25+
26+
from txmongo import filter as qf
2227
from txmongo.filter import _QueryFilter
2328
from txmongo.protocol import DELETE_SINGLE_REMOVE, UPDATE_UPSERT, UPDATE_MULTI, \
2429
Query, Getmore, Insert, Update, Delete, KillCursors
2530
from txmongo.pymongo_internals import _check_write_command_response, _merge_command, _check_command_response
2631
from txmongo.utils import check_deadline, timeout
27-
from txmongo import filter as qf
28-
from twisted.internet import defer
29-
from twisted.python.compat import comparable
3032

3133

3234
@comparable
@@ -358,7 +360,6 @@ def query():
358360
return self.__real_find_with_cursor(**new_kwargs)
359361

360362
def __real_find_with_cursor(self, filter=None, projection=None, skip=0, limit=0, sort=None, batch_size=0,**kwargs):
361-
362363
if filter is None:
363364
filter = SON()
364365

@@ -376,6 +377,8 @@ def __real_find_with_cursor(self, filter=None, projection=None, skip=0, limit=0,
376377
projection = self._normalize_fields_projection(projection)
377378

378379
filter = self.__apply_find_filter(filter, sort)
380+
if not isinstance(filter, BSON):
381+
filter = BSON.encode(filter, codec_options=self.codec_options)
379382

380383
as_class = kwargs.get("as_class")
381384
proto = self._database.connection.getprotocol()
@@ -603,7 +606,7 @@ def insert(self, docs, safe=None, flags=0, **kwargs):
603606
else:
604607
raise TypeError("TxMongo: insert takes a document or a list of documents.")
605608

606-
docs = [BSON.encode(d) for d in docs]
609+
docs = [BSON.encode(d, codec_options=self.codec_options) for d in docs]
607610
insert = Insert(flags=flags, collection=str(self), documents=docs)
608611

609612
def on_proto(proto):
@@ -654,8 +657,7 @@ def on_ok(result):
654657
return InsertOneResult(inserted_id, self.write_concern.acknowledged)
655658
return self._insert_one(document, _deadline).addCallback(on_ok)
656659

657-
@staticmethod
658-
def _generate_batch_commands(collname, command, docs_field, documents, ordered,
660+
def _generate_batch_commands(self, collname, command, docs_field, documents, ordered,
659661
write_concern, max_bson, max_count):
660662
# Takes a list of documents and generates one or many `insert` commands
661663
# with documents list in each command is less or equal to max_bson bytes
@@ -669,7 +671,7 @@ def _generate_batch_commands(collname, command, docs_field, documents, ordered,
669671
("writeConcern", write_concern.document)])
670672

671673
buf = io.BytesIO()
672-
buf.write(BSON.encode(msg))
674+
buf.write(BSON.encode(msg, codec_options=self.codec_options))
673675
buf.seek(-1, io.SEEK_END) # -1 because we don't need final NUL from partial command
674676
buf.write(docs_field) # type, name and length placeholder of 'documents' array
675677
docs_start = buf.tell() - 4
@@ -693,7 +695,7 @@ def prepare_command():
693695
idx_offset = 0
694696
for doc in documents:
695697
key = str(idx).encode('ascii')
696-
value = BSON.encode(doc)
698+
value = BSON.encode(doc, codec_options=self.codec_options)
697699

698700
enough_size = buf.tell() + len(key)+2 + len(value) - docs_start > max_bson
699701
enough_count = idx >= max_count
@@ -796,8 +798,8 @@ def update(self, spec, document, upsert=False, multi=False, safe=None, flags=0,
796798
if upsert:
797799
flags |= UPDATE_UPSERT
798800

799-
spec = BSON.encode(spec)
800-
document = BSON.encode(document)
801+
spec = BSON.encode(spec, codec_options=self.codec_options)
802+
document = BSON.encode(document, codec_options=self.codec_options)
801803
update = Update(flags=flags, collection=str(self),
802804
selector=spec, update=document)
803805

@@ -959,7 +961,7 @@ def remove(self, spec, safe=None, single=False, flags=0, **kwargs):
959961
if single:
960962
flags |= DELETE_SINGLE_REMOVE
961963

962-
spec = BSON.encode(spec)
964+
spec = BSON.encode(spec, codec_options=self.codec_options)
963965
delete = Delete(flags=flags, collection=str(self), selector=spec)
964966

965967
def on_proto(proto):

txmongo/connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
from bson.codec_options import DEFAULT_CODEC_OPTIONS
66
from pymongo.errors import AutoReconnect, ConfigurationError, OperationFailure
7-
from pymongo.uri_parser import parse_uri
87
from pymongo.read_preferences import ReadPreference
8+
from pymongo.uri_parser import parse_uri
99
from pymongo.write_concern import WriteConcern
1010
from twisted.internet import defer, reactor, task
1111
from twisted.internet.protocol import ReconnectingClientFactory, ClientFactory
1212
from twisted.python import log
13+
1314
from txmongo.database import Database
1415
from txmongo.protocol import MongoProtocol, Query
1516
from txmongo.utils import timeout, get_err

txmongo/database.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# found in the LICENSE file.
44

55
from bson.son import SON
6-
from bson.codec_options import DEFAULT_CODEC_OPTIONS
6+
77
from txmongo.collection import Collection
88
from txmongo.pymongo_internals import _check_command_response
99
from txmongo.utils import timeout
@@ -53,10 +53,12 @@ def codec_options(self):
5353

5454
@timeout
5555
def command(self, command, value=1, check=True, allowable_errors=None,
56-
codec_options=DEFAULT_CODEC_OPTIONS, _deadline=None, **kwargs):
57-
"""command(command, value=1, check=True, allowable_errors=None, codec_options=DEFAULT_CODEC_OPTIONS)"""
56+
codec_options=None, _deadline=None, **kwargs):
57+
"""command(command, value=1, check=True, allowable_errors=None, codec_options=None)"""
5858
if isinstance(command, (bytes, str)):
5959
command = SON([(command, value)])
60+
if codec_options is None:
61+
codec_options = self.__codec_options
6062
options = kwargs.copy()
6163
command.update(options)
6264

txmongo/protocol.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,29 @@
1515

1616
import base64
1717
import hashlib
18-
19-
from bson import BSON, SON, Binary
20-
from collections import namedtuple
21-
from hashlib import sha1
2218
import hmac
2319
import logging
20+
import struct
21+
from collections import namedtuple
22+
from hashlib import sha1
23+
from random import SystemRandom
24+
25+
from bson import BSON, SON, Binary
2426
from pymongo import auth
2527
from pymongo.errors import AutoReconnect, ConnectionFailure, DuplicateKeyError, OperationFailure, \
26-
NotMasterError, CursorNotFound
27-
from random import SystemRandom
28-
import struct
28+
CursorNotFound
2929
from twisted.internet import defer, protocol, error
3030
from twisted.python import failure, log
31+
3132
from txmongo.utils import get_err
3233

3334

35+
try:
36+
from pymongo.errors import NotPrimaryError
37+
except ImportError:
38+
# For pymongo < 3.12
39+
from pymongo.errors import NotMasterError as NotPrimaryError
40+
3441
try:
3542
from hashlib import pbkdf2_hmac as _hi
3643
except ImportError:
@@ -397,7 +404,7 @@ def handle_REPLY(self, request):
397404
msg = "TxMongo: " + doc.get("$err", "Unknown error")
398405
fail_conn = False
399406
if code == 13435:
400-
err = NotMasterError(msg)
407+
err = NotPrimaryError(msg)
401408
fail_conn = True
402409
else:
403410
err = OperationFailure(msg, code)

txmongo/pymongo_internals.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
from pymongo.bulk import _UOP
21
from pymongo.errors import (DuplicateKeyError, WriteError, WTimeoutError, WriteConcernError, OperationFailure,
3-
NotMasterError, ExecutionTimeout, CursorNotFound)
2+
ExecutionTimeout, CursorNotFound)
43
from pymongo.message import _INSERT, _DELETE, _UPDATE
54

65

6+
try:
7+
from pymongo.errors import NotPrimaryError
8+
except ImportError:
9+
# For pymongo < 3.12
10+
from pymongo.errors import NotMasterError as NotPrimaryError
11+
12+
713

814
# Copied from pymongo/helpers.py:32 at commit d7d94b2776098dba32686ddf3ada1f201172daaf
915

@@ -100,7 +106,7 @@ def _merge_command(run, full_result, results):
100106
idx = doc["index"] + offset
101107
replacement["index"] = run.index(idx)
102108
# Add the failed operation to the error document.
103-
replacement[_UOP] = run.ops[idx]
109+
replacement["op"] = run.ops[idx]
104110
full_result["writeErrors"].append(replacement)
105111

106112
wc_error = result.get("writeConcernError")
@@ -140,10 +146,10 @@ def _check_command_response(response, msg=None, allowable_errors=None,
140146
code = details.get("code")
141147
# Server is "not master" or "recovering"
142148
if code in _NOT_MASTER_CODES:
143-
raise NotMasterError(errmsg, response)
149+
raise NotPrimaryError(errmsg, response)
144150
elif ("not master" in errmsg
145151
or "node is recovering" in errmsg):
146-
raise NotMasterError(errmsg, response)
152+
raise NotPrimaryError(errmsg, response)
147153

148154
# Server assertion failures
149155
if errmsg == "db assertion failure":

0 commit comments

Comments
 (0)