Skip to content

Commit 3160a80

Browse files
authored
Merge pull request MongoEngine#2614 from terencehonles/pymongo40_support
pymongo 4.0 support
2 parents 8af8878 + 2f29d69 commit 3160a80

18 files changed

+419
-179
lines changed

.github/workflows/github-actions.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ env:
2121
PYMONGO_3_9: 3.9
2222
PYMONGO_3_11: 3.11
2323
PYMONGO_3_12: 3.12
24+
PYMONGO_4_0: 4.0
2425

2526
MAIN_PYTHON_VERSION: 3.7
2627

@@ -61,6 +62,9 @@ jobs:
6162
- python-version: 3.7
6263
MONGODB: $MONGODB_4_4
6364
PYMONGO: $PYMONGO_3_12
65+
- python-version: 3.9
66+
MONGODB: $MONGODB_4_4
67+
PYMONGO: $PYMONGO_4_0
6468
steps:
6569
- uses: actions/checkout@v2
6670
- name: Set up Python ${{ matrix.python-version }}

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,4 @@ that much better:
263263
* Timothé Perez (https://github.com/AchilleAsh)
264264
* oleksandr-l5 (https://github.com/oleksandr-l5)
265265
* Ido Shraga (https://github.com/idoshr)
266+
* Terence Honles (https://github.com/terencehonles)

docs/changelog.rst

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,41 @@ Changelog
77
Development
88
===========
99
- (Fill this out as you fix issues and develop your features).
10-
- EnumField improvements: now `choices` limits the values of an enum to allow
10+
- EnumField improvements: now ``choices`` limits the values of an enum to allow
1111
- Fix bug that prevented instance queryset from using custom queryset_class #2589
1212
- Fix deepcopy of EmbeddedDocument #2202
1313
- Fix error when using precision=0 with DecimalField #2535
1414
- Add support for regex and whole word text search query #2568
15+
- BREAKING CHANGE: Updates to support pymongo 4.0. Where possible deprecated
16+
functionality has been migrated, but additional care should be taken when
17+
migrating to pymongo 4.0 as existing code may have been using deprecated
18+
features which have now been removed #2614.
19+
20+
For the pymongo migration guide see:
21+
https://pymongo.readthedocs.io/en/stable/migrate-to-pymongo4.html.
22+
23+
In addition to the changes in the migration guide, the following is a high
24+
level overview of the changes made to MongoEngine when using pymongo 4.0:
25+
26+
- limited support of geohaystack indexes has been removed
27+
- ``QuerySet.map_reduce`` has been migrated from ``Collection.map_reduce``
28+
and ``Collection.inline_map_reduce`` to use
29+
``db.command({mapReduce: ..., ...})`` and support between the two may need
30+
additional verification.
31+
- UUIDs are encoded with the ``pythonLegacy`` encoding by default instead of
32+
the newer and cross platform ``standard`` encoding. Existing UUIDs will
33+
need to be migrated before changing the encoding, and this should be done
34+
explicitly by the user rather than switching to a new default by
35+
MongoEngine. This default will change at a later date, but to allow
36+
specifying and then migrating to the new format a default ``json_options``
37+
has been provided.
38+
- ``Queryset.count`` has been using ``Collection.count_documents`` and
39+
transparently falling back to ``Collection.count`` when using features that
40+
are not supported by ``Collection.count_documents``. ``Collection.count``
41+
has been removed and no automatic fallback is possible. The migration guide
42+
documents the extended functionality which is no longer supported. Rewrite
43+
the unsupported queries or fetch the whole result set and perform the count
44+
locally.
1545

1646
Changes in 0.23.1
1747
===========

mongoengine/base/document.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import copy
22
import numbers
3+
import warnings
34
from functools import partial
45

56
import pymongo
@@ -23,11 +24,17 @@
2324
OperationError,
2425
ValidationError,
2526
)
27+
from mongoengine.pymongo_support import LEGACY_JSON_OPTIONS
2628

2729
__all__ = ("BaseDocument", "NON_FIELD_ERRORS")
2830

2931
NON_FIELD_ERRORS = "__all__"
3032

33+
try:
34+
GEOHAYSTACK = pymongo.GEOHAYSTACK
35+
except AttributeError:
36+
GEOHAYSTACK = None
37+
3138

3239
class BaseDocument:
3340
# TODO simplify how `_changed_fields` is used.
@@ -439,10 +446,19 @@ def to_json(self, *args, **kwargs):
439446
Defaults to True.
440447
"""
441448
use_db_field = kwargs.pop("use_db_field", True)
449+
if "json_options" not in kwargs:
450+
warnings.warn(
451+
"No 'json_options' are specified! Falling back to "
452+
"LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. "
453+
"For use with other MongoDB drivers specify the UUID "
454+
"representation to use.",
455+
DeprecationWarning,
456+
)
457+
kwargs["json_options"] = LEGACY_JSON_OPTIONS
442458
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
443459

444460
@classmethod
445-
def from_json(cls, json_data, created=False):
461+
def from_json(cls, json_data, created=False, **kwargs):
446462
"""Converts json data to a Document instance
447463
448464
:param str json_data: The json data to load into the Document
@@ -460,7 +476,16 @@ def from_json(cls, json_data, created=False):
460476
# TODO should `created` default to False? If the object already exists
461477
# in the DB, you would likely retrieve it from MongoDB itself through
462478
# a query, not load it from JSON data.
463-
return cls._from_son(json_util.loads(json_data), created=created)
479+
if "json_options" not in kwargs:
480+
warnings.warn(
481+
"No 'json_options' are specified! Falling back to "
482+
"LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. "
483+
"For use with other MongoDB drivers specify the UUID "
484+
"representation to use.",
485+
DeprecationWarning,
486+
)
487+
kwargs["json_options"] = LEGACY_JSON_OPTIONS
488+
return cls._from_son(json_util.loads(json_data, **kwargs), created=created)
464489

465490
def __expand_dynamic_values(self, name, value):
466491
"""Expand any dynamic values to their correct types / values."""
@@ -898,7 +923,10 @@ def _build_index_spec(cls, spec):
898923
elif key.startswith("("):
899924
direction = pymongo.GEOSPHERE
900925
elif key.startswith(")"):
901-
direction = pymongo.GEOHAYSTACK
926+
try:
927+
direction = pymongo.GEOHAYSTACK
928+
except AttributeError:
929+
raise NotImplementedError
902930
elif key.startswith("*"):
903931
direction = pymongo.GEO2D
904932
if key.startswith(("+", "-", "*", "$", "#", "(", ")")):
@@ -923,10 +951,10 @@ def _build_index_spec(cls, spec):
923951
index_list.append((key, direction))
924952

925953
# Don't add cls to a geo index
926-
if include_cls and direction not in (
927-
pymongo.GEO2D,
928-
pymongo.GEOHAYSTACK,
929-
pymongo.GEOSPHERE,
954+
if (
955+
include_cls
956+
and direction not in (pymongo.GEO2D, pymongo.GEOSPHERE)
957+
and (GEOHAYSTACK is None or direction != GEOHAYSTACK)
930958
):
931959
index_list.insert(0, ("_cls", 1))
932960

mongoengine/connection.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import warnings
2+
13
from pymongo import MongoClient, ReadPreference, uri_parser
24
from pymongo.database import _check_name
35

6+
from mongoengine.pymongo_support import PYMONGO_VERSION
7+
48
__all__ = [
59
"DEFAULT_CONNECTION_NAME",
610
"DEFAULT_DATABASE_NAME",
@@ -162,6 +166,18 @@ def _get_connection_settings(
162166
kwargs.pop("slaves", None)
163167
kwargs.pop("is_slave", None)
164168

169+
if "uuidRepresentation" not in kwargs:
170+
warnings.warn(
171+
"No uuidRepresentation is specified! Falling back to "
172+
"'pythonLegacy' which is the default for pymongo 3.x. "
173+
"For compatibility with other MongoDB drivers this should be "
174+
"specified as 'standard' or '{java,csharp}Legacy' to work with "
175+
"older drivers in those languages. This will be changed to "
176+
"'standard' in a future release.",
177+
DeprecationWarning,
178+
)
179+
kwargs["uuidRepresentation"] = "pythonLegacy"
180+
165181
conn_settings.update(kwargs)
166182
return conn_settings
167183

@@ -263,15 +279,25 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
263279
raise ConnectionFailure(msg)
264280

265281
def _clean_settings(settings_dict):
266-
irrelevant_fields_set = {
267-
"name",
268-
"username",
269-
"password",
270-
"authentication_source",
271-
"authentication_mechanism",
272-
}
282+
if PYMONGO_VERSION < (4,):
283+
irrelevant_fields_set = {
284+
"name",
285+
"username",
286+
"password",
287+
"authentication_source",
288+
"authentication_mechanism",
289+
}
290+
rename_fields = {}
291+
else:
292+
irrelevant_fields_set = {"name"}
293+
rename_fields = {
294+
"authentication_source": "authSource",
295+
"authentication_mechanism": "authMechanism",
296+
}
273297
return {
274-
k: v for k, v in settings_dict.items() if k not in irrelevant_fields_set
298+
rename_fields.get(k, k): v
299+
for k, v in settings_dict.items()
300+
if k not in irrelevant_fields_set and v is not None
275301
}
276302

277303
raw_conn_settings = _connection_settings[alias].copy()
@@ -351,14 +377,18 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
351377
conn = get_connection(alias)
352378
conn_settings = _connection_settings[alias]
353379
db = conn[conn_settings["name"]]
354-
auth_kwargs = {"source": conn_settings["authentication_source"]}
355-
if conn_settings["authentication_mechanism"] is not None:
356-
auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"]
357380
# Authenticate if necessary
358-
if conn_settings["username"] and (
359-
conn_settings["password"]
360-
or conn_settings["authentication_mechanism"] == "MONGODB-X509"
381+
if (
382+
PYMONGO_VERSION < (4,)
383+
and conn_settings["username"]
384+
and (
385+
conn_settings["password"]
386+
or conn_settings["authentication_mechanism"] == "MONGODB-X509"
387+
)
361388
):
389+
auth_kwargs = {"source": conn_settings["authentication_source"]}
390+
if conn_settings["authentication_mechanism"] is not None:
391+
auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"]
362392
db.authenticate(
363393
conn_settings["username"], conn_settings["password"], **auth_kwargs
364394
)

mongoengine/context_managers.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,13 +210,14 @@ def __init__(self, alias=DEFAULT_CONNECTION_NAME):
210210
}
211211

212212
def _turn_on_profiling(self):
213-
self.initial_profiling_level = self.db.profiling_level()
214-
self.db.set_profiling_level(0)
213+
profile_update_res = self.db.command({"profile": 0})
214+
self.initial_profiling_level = profile_update_res["was"]
215+
215216
self.db.system.profile.drop()
216-
self.db.set_profiling_level(2)
217+
self.db.command({"profile": 2})
217218

218219
def _resets_profiling(self):
219-
self.db.set_profiling_level(self.initial_profiling_level)
220+
self.db.command({"profile": self.initial_profiling_level})
220221

221222
def __enter__(self):
222223
self._turn_on_profiling()

mongoengine/document.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -888,10 +888,6 @@ def ensure_indexes(cls):
888888
index_cls = cls._meta.get("index_cls", True)
889889

890890
collection = cls._get_collection()
891-
# 746: when connection is via mongos, the read preference is not necessarily an indication that
892-
# this code runs on a secondary
893-
if not collection.is_mongos and collection.read_preference > 1:
894-
return
895891

896892
# determine if an index which we are creating includes
897893
# _cls as its first field; if so, we can avoid creating

mongoengine/pymongo_support.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
"""
2-
Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support.
2+
Helper functions, constants, and types to aid with PyMongo support.
33
"""
44
import pymongo
5+
from bson import binary, json_util
56
from pymongo.errors import OperationFailure
67

7-
_PYMONGO_37 = (3, 7)
8-
98
PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])
109

11-
IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37
10+
if PYMONGO_VERSION >= (4,):
11+
LEGACY_JSON_OPTIONS = json_util.LEGACY_JSON_OPTIONS.with_options(
12+
uuid_representation=binary.UuidRepresentation.PYTHON_LEGACY,
13+
)
14+
else:
15+
LEGACY_JSON_OPTIONS = json_util.DEFAULT_JSON_OPTIONS
1216

1317

1418
def count_documents(
@@ -29,15 +33,28 @@ def count_documents(
2933
kwargs["collation"] = collation
3034

3135
# count_documents appeared in pymongo 3.7
32-
if IS_PYMONGO_GTE_37:
36+
if PYMONGO_VERSION >= (3, 7):
3337
try:
3438
return collection.count_documents(filter=filter, **kwargs)
35-
except OperationFailure:
39+
except OperationFailure as err:
40+
if PYMONGO_VERSION >= (4,):
41+
raise
42+
3643
# OperationFailure - accounts for some operators that used to work
3744
# with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
3845
# fallback to deprecated Cursor.count
3946
# Keeping this should be reevaluated the day pymongo removes .count entirely
40-
pass
47+
message = str(err)
48+
if not (
49+
"not allowed in this context" in message
50+
and (
51+
"$where" in message
52+
or "$geoNear" in message
53+
or "$near" in message
54+
or "$nearSphere" in message
55+
)
56+
):
57+
raise
4158

4259
cursor = collection.find(filter)
4360
for option, option_value in kwargs.items():
@@ -49,7 +66,7 @@ def count_documents(
4966

5067
def list_collection_names(db, include_system_collections=False):
5168
"""Pymongo>3.7 deprecates collection_names in favour of list_collection_names"""
52-
if IS_PYMONGO_GTE_37:
69+
if PYMONGO_VERSION >= (3, 7):
5370
collections = db.list_collection_names()
5471
else:
5572
collections = db.collection_names()

0 commit comments

Comments
 (0)