Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Dependencies
============
MongoEngine requires:

- PyMongo >=3.12,<5.0
- PyMongo >=4.0

The following optional packages enable additional functionality:

Expand Down
14 changes: 14 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ Changes in 1.0.0
- Log a warning in case users creates multiple Document classes with the same name as it can lead to unexpected behavior #1778
- Fix use of $geoNear or $collStats in aggregate #2493
- BREAKING CHANGE: Further to the deprecation warning, remove ability to use an unpacked list to `Queryset.aggregate(*pipeline)`, a plain list must be provided instead `Queryset.aggregate(pipeline)`, as it's closer to pymongo interface
- BREAKING CHANGE: PyMongo 3.x is no longer supported.

As a consequence:

- ``QuerySet.count()`` no longer supports queries using ``$near``,
``$nearSphere``, ``$geoNear``, or ``$where``. PyMongo's
``count_documents()`` rejects these operators and raises
``OperationFailure``; the removed ``Cursor.count()`` fallback is no
longer available. Use ``$geoWithin`` with ``$center`` or
``$centerSphere`` for countable geospatial filters, and ``$expr`` instead
of ``$where``.
- GeoHaystack index specifications using the ``)`` prefix are no longer
supported and raise ``NotImplementedError``.

- BREAKING CHANGE: Further to the deprecation warning, remove `full_response` from `QuerySet.modify` as it wasn't supported with Pymongo 3+
- Fixed stacklevel of many warnings (to point places emitting the warning more accurately)
- Add support for collation/hint/comment to delete/update and aggregate #2842
Expand Down
16 changes: 2 additions & 14 deletions mongoengine/base/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@

NON_FIELD_ERRORS = "__all__"

try:
GEOHAYSTACK = pymongo.GEOHAYSTACK
except AttributeError:
GEOHAYSTACK = None


class BaseDocument:
# TODO simplify how `_changed_fields` is used.
Expand Down Expand Up @@ -931,10 +926,7 @@ def _build_index_spec(cls, spec):
elif key.startswith("("):
direction = pymongo.GEOSPHERE
elif key.startswith(")"):
try:
direction = pymongo.GEOHAYSTACK
except AttributeError:
raise NotImplementedError
raise NotImplementedError("GeoHaystack indexes are not supported")
elif key.startswith("*"):
direction = pymongo.GEO2D
if key.startswith(("+", "-", "*", "$", "#", "(", ")")):
Expand All @@ -959,11 +951,7 @@ def _build_index_spec(cls, spec):
index_list.append((key, direction))

# Don't add cls to a geo index
if (
include_cls
and direction not in (pymongo.GEO2D, pymongo.GEOSPHERE)
and (GEOHAYSTACK is None or direction != GEOHAYSTACK)
):
if include_cls and direction not in (pymongo.GEO2D, pymongo.GEOSPHERE):
index_list.insert(0, ("_cls", 1))

if index_list:
Expand Down
73 changes: 14 additions & 59 deletions mongoengine/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,9 @@
except ImportError:
from pymongo.database import _check_name

# DriverInfo was added in PyMongo 3.7.
try:
from pymongo.driver_info import DriverInfo
except ImportError:
DriverInfo = None
from pymongo.driver_info import DriverInfo

import mongoengine
from mongoengine.pymongo_support import PYMONGO_VERSION

__all__ = [
"DEFAULT_CONNECTION_NAME",
Expand Down Expand Up @@ -165,19 +160,9 @@ def _get_connection_settings(
ReadPreference.SECONDARY_PREFERRED,
)

# Starting with PyMongo v3.5, the "readpreference" option is
# returned as a string (e.g. "secondaryPreferred") and not an
# int (e.g. 3).
# TODO simplify the code below once we drop support for
# PyMongo v3.4.
read_pf_mode = normalized_uri_options["readpreference"]
if isinstance(read_pf_mode, str):
read_pf_mode = read_pf_mode.lower()
read_pf_mode = normalized_uri_options["readpreference"].lower()
for preference in read_preferences:
if (
preference.name.lower() == read_pf_mode
or preference.mode == read_pf_mode
):
if preference.name.lower() == read_pf_mode:
ReadPrefClass = preference.__class__
break

Expand Down Expand Up @@ -213,7 +198,7 @@ def _get_connection_settings(
if "uuidrepresentation" not in keys and "uuidrepresentation" not in conn_settings:
warnings.warn(
"No uuidRepresentation is specified! Falling back to "
"'pythonLegacy' which is the default for pymongo 3.x. "
"'pythonLegacy' for backward compatibility. "
"For compatibility with other MongoDB drivers this should be "
"specified as 'standard' or '{java,csharp}Legacy' to work with "
"older drivers in those languages. This will be changed to "
Expand Down Expand Up @@ -333,26 +318,14 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
raise ConnectionFailure(msg)

def _clean_settings(settings_dict):
if PYMONGO_VERSION < (4,):
irrelevant_fields_set = {
"name",
"username",
"password",
"authentication_source",
"authentication_mechanism",
"authmechanismproperties",
}
rename_fields = {}
else:
irrelevant_fields_set = {"name"}
rename_fields = {
"authentication_source": "authSource",
"authentication_mechanism": "authMechanism",
}
rename_fields = {
"authentication_source": "authSource",
"authentication_mechanism": "authMechanism",
}
return {
rename_fields.get(k, k): v
for k, v in settings_dict.items()
if k not in irrelevant_fields_set and v is not None
if k != "name" and v is not None
}

raw_conn_settings = _connection_settings[alias].copy()
Expand All @@ -361,10 +334,9 @@ def _clean_settings(settings_dict):
# alias and remove the database name and authentication info (we don't
# care about them at this point).
conn_settings = _clean_settings(raw_conn_settings)
if DriverInfo is not None:
conn_settings.setdefault(
"driver", DriverInfo("MongoEngine", mongoengine.__version__)
)
conn_settings.setdefault(
"driver", DriverInfo("MongoEngine", mongoengine.__version__)
)

# Determine if we should use PyMongo's or mongomock's MongoClient.
if "mongo_client_class" in conn_settings:
Expand Down Expand Up @@ -429,25 +401,8 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):

if alias not in _dbs:
conn = get_connection(alias)
conn_settings = _connection_settings[alias]
db = conn[conn_settings["name"]]
# Authenticate if necessary
if (
PYMONGO_VERSION < (4,)
and conn_settings["username"]
and (
conn_settings["password"]
or conn_settings["authentication_mechanism"] == "MONGODB-X509"
)
and conn_settings["authmechanismproperties"] is None
):
auth_kwargs = {"source": conn_settings["authentication_source"]}
if conn_settings["authentication_mechanism"] is not None:
auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"]
db.authenticate(
conn_settings["username"], conn_settings["password"], **auth_kwargs
)
_dbs[alias] = db
db_name = _connection_settings[alias]["name"]
_dbs[alias] = conn[db_name]
return _dbs[alias]


Expand Down
60 changes: 12 additions & 48 deletions mongoengine/pymongo_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,20 @@

import pymongo
from bson import binary, json_util
from pymongo.errors import OperationFailure

from mongoengine import connection

PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])

# This will be changed to UuidRepresentation.UNSPECIFIED in a future
# (breaking) release.
if PYMONGO_VERSION >= (4,):
LEGACY_JSON_OPTIONS = json_util.LEGACY_JSON_OPTIONS.with_options(
uuid_representation=binary.UuidRepresentation.PYTHON_LEGACY,
)
else:
LEGACY_JSON_OPTIONS = json_util.DEFAULT_JSON_OPTIONS
LEGACY_JSON_OPTIONS = json_util.LEGACY_JSON_OPTIONS.with_options(
uuid_representation=binary.UuidRepresentation.PYTHON_LEGACY,
)


def count_documents(
collection, filter, skip=None, limit=None, hint=None, collation=None
):
"""Pymongo>3.7 deprecates count in favour of count_documents"""
"""Count documents, using collection metadata when possible."""
if limit == 0:
return 0 # Pymongo raises an OperationFailure if called with limit=0

Expand All @@ -37,47 +31,17 @@ def count_documents(
if collation is not None:
kwargs["collation"] = collation

# count_documents appeared in pymongo 3.7
if PYMONGO_VERSION >= (3, 7):
try:
is_active_session = connection._get_session() is not None
if not filter and set(kwargs) <= {"max_time_ms"} and not is_active_session:
# when no filter is provided, estimated_document_count
# is a lot faster as it uses the collection metadata
return collection.estimated_document_count(**kwargs)
else:
return collection.count_documents(
filter=filter, session=connection._get_session(), **kwargs
)
except OperationFailure as err:
if PYMONGO_VERSION >= (4,):
raise

# OperationFailure - accounts for some operators that used to work
# with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
# fallback to deprecated Cursor.count
# Keeping this should be reevaluated the day pymongo removes .count entirely
if (
"$geoNear, $near, and $nearSphere are not allowed in this context"
not in str(err)
and "$where is not allowed in this context" not in str(err)
):
raise

cursor = collection.find(filter)
for option, option_value in kwargs.items():
cursor_method = getattr(cursor, option)
cursor = cursor_method(option_value)
with_limit_and_skip = "skip" in kwargs or "limit" in kwargs
return cursor.count(with_limit_and_skip=with_limit_and_skip)
session = connection._get_session()
if not filter and not kwargs and session is None:
# when no filter is provided, estimated_document_count
# is a lot faster as it uses the collection metadata
return collection.estimated_document_count(**kwargs)
return collection.count_documents(filter=filter, session=session, **kwargs)


def list_collection_names(db, include_system_collections=False):
"""Pymongo>3.7 deprecates collection_names in favour of list_collection_names"""
if PYMONGO_VERSION >= (3, 7):
collections = db.list_collection_names(session=connection._get_session())
else:
collections = db.collection_names(session=connection._get_session())
"""Return collection names, optionally including system collections."""
collections = db.list_collection_names(session=connection._get_session())

if not include_system_collections:
collections = [c for c in collections if not c.startswith("system.")]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_version(version_tuple):
"Topic :: Software Development :: Libraries :: Python Modules",
]

install_require = ["pymongo>=3.12,<5.0"]
install_require = ["pymongo>=4.0,<5.0"]
tests_require = [
"pytest",
"pytest-cov",
Expand Down
49 changes: 6 additions & 43 deletions tests/document/test_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,55 +249,18 @@ class Place(Document):
info = [value["key"] for key, value in info.items()]
assert [("location.point", "2dsphere")] in info

def test_explicit_geohaystack_index(self):
"""Ensure that geohaystack indexes work when created via meta[indexes]"""
# This test can be removed when pymongo 3.x is no longer supported
if PYMONGO_VERSION >= (4,):
pytest.skip("GEOHAYSTACK has been removed in pymongo 4.0")

class Place(Document):
location = DictField()
name = StringField()
meta = {"indexes": [(")location.point", "name")]}

assert [
{"fields": [("location.point", "geoHaystack"), ("name", 1)]}
] == Place._meta["index_specs"]

# GeoHaystack index creation is not supported for now from meta, as it
# requires a bucketSize parameter.
if False:
Place.ensure_indexes()
info = Place._get_collection().index_information()
info = [value["key"] for key, value in info.items()]
assert [("location.point", "geoHaystack")] in info

def test_create_geohaystack_index(self):
"""Ensure that geohaystack indexes can be created"""
"""Ensure that removed GeoHaystack indexes raise a clear error."""

class Place(Document):
location = DictField()
name = StringField()

if PYMONGO_VERSION >= (4,):
expected_error = NotImplementedError
elif get_mongodb_version() >= (4, 9):
expected_error = OperationFailure
else:
expected_error = None

# This test can be removed when pymongo 3.x is no longer supported
if expected_error:
with pytest.raises(expected_error):
Place.create_index(
{"fields": (")location.point", "name")},
bucketSize=10,
)
else:
Place.create_index({"fields": (")location.point", "name")}, bucketSize=10)
info = Place._get_collection().index_information()
info = [value["key"] for key, value in info.items()]
assert [("location.point", "geoHaystack"), ("name", 1)] in info
with pytest.raises(NotImplementedError, match="GeoHaystack"):
Place.create_index(
{"fields": (")location.point", "name")},
bucketSize=10,
)

def test_dictionary_indexes(self):
"""Ensure that indexes are used when meta[indexes] contains
Expand Down
9 changes: 3 additions & 6 deletions tests/document/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import bson
import pytest
from bson import DBRef, ObjectId
from pymongo.errors import DuplicateKeyError
from pymongo.errors import DuplicateKeyError, OperationFailure

from mongoengine import *
from mongoengine import signals
Expand Down Expand Up @@ -3047,11 +3047,8 @@ def __str__(self):
return this.name == '1' ||
this.name == '2';}"""})
assert [str(b) for b in custom_qs] == ["1", "2"]

# count only will work with this raw query before pymongo 4.x, but
# the length is also implicitly checked above
if PYMONGO_VERSION < (4,):
assert custom_qs.count() == 2
with pytest.raises(OperationFailure):
custom_qs.count()

def test_switch_db_instance(self):
register_connection("testdb-1", "mongoenginetest2")
Expand Down
Loading