Merge branch 'master' of github.com:MongoEngine/mongoengine into remove_old_deprecated_method

This commit is contained in:
Bastien Gérard 2020-03-17 21:39:36 +01:00
commit 547cd4a3ae
12 changed files with 135 additions and 55 deletions

View File

@ -255,3 +255,4 @@ that much better:
* Filip Kucharczyk (https://github.com/Pacu2) * Filip Kucharczyk (https://github.com/Pacu2)
* Eric Timmons (https://github.com/daewok) * Eric Timmons (https://github.com/daewok)
* Matthew Simpson (https://github.com/mcsimps2) * Matthew Simpson (https://github.com/mcsimps2)
* Leonardo Domingues (https://github.com/leodmgs)

View File

@ -9,6 +9,10 @@ Development
- Add Mongo 4.0 to Travis - Add Mongo 4.0 to Travis
- BREAKING CHANGE: Removed ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes`` that were deprecated in 2013. - BREAKING CHANGE: Removed ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes`` that were deprecated in 2013.
``Document.ensure_indexes`` still exists and is the right method to use ``Document.ensure_indexes`` still exists and is the right method to use
- Fixed a bug causing inaccurate query results, while combining ``__raw__`` and regular filters for the same field #2264
- Add support for the `elemMatch` projection operator in .fields() (e.g BlogPost.objects.fields(elemMatch__comments="test")) #2267
- DictField validate failed without default connection (bug introduced in 0.19.0) #2239
- Remove method queryset.slave_okay() that was deprecated a while ago and disappeared since pymongo3
Changes in 0.19.1 Changes in 0.19.1
================= =================

View File

@ -56,7 +56,7 @@ class InvalidCollectionError(Exception):
class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)): class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
"""A :class:`~mongoengine.Document` that isn't stored in its own r"""A :class:`~mongoengine.Document` that isn't stored in its own
collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as
fields on :class:`~mongoengine.Document`\ s through the fields on :class:`~mongoengine.Document`\ s through the
:class:`~mongoengine.EmbeddedDocumentField` field type. :class:`~mongoengine.EmbeddedDocumentField` field type.
@ -332,7 +332,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
): ):
"""Save the :class:`~mongoengine.Document` to the database. If the """Save the :class:`~mongoengine.Document` to the database. If the
document already exists, it will be updated, otherwise it will be document already exists, it will be updated, otherwise it will be
created. created. Returns the saved object instance.
:param force_insert: only try to create a new document, don't allow :param force_insert: only try to create a new document, don't allow
updates of existing documents. updates of existing documents.

View File

@ -1088,14 +1088,12 @@ class DictField(ComplexBaseField):
msg = "Invalid dictionary key - documents must have only string keys" msg = "Invalid dictionary key - documents must have only string keys"
self.error(msg) self.error(msg)
curr_mongo_ver = get_mongodb_version() # Following condition applies to MongoDB >= 3.6
# older Mongo has stricter constraints but
if curr_mongo_ver < MONGODB_36 and key_has_dot_or_dollar(value): # it will be rejected upon insertion anyway
self.error( # Having a validation that depends on the MongoDB version
'Invalid dictionary key name - keys may not contain "."' # is not straightforward as the field isn't aware of the connected Mongo
' or startswith "$" characters' if key_starts_with_dollar(value):
)
elif curr_mongo_ver >= MONGODB_36 and key_starts_with_dollar(value):
self.error( self.error(
'Invalid dictionary key name - keys may not startswith "$" characters' 'Invalid dictionary key name - keys may not startswith "$" characters'
) )

View File

@ -11,7 +11,7 @@ MONGODB_36 = (3, 6)
def get_mongodb_version(): def get_mongodb_version():
"""Return the version of the connected mongoDB (first 2 digits) """Return the version of the default connected mongoDB (first 2 digits)
:return: tuple(int, int) :return: tuple(int, int)
""" """

View File

@ -60,7 +60,6 @@ class BaseQuerySet(object):
self._ordering = None self._ordering = None
self._snapshot = False self._snapshot = False
self._timeout = True self._timeout = True
self._slave_okay = False
self._read_preference = None self._read_preference = None
self._iter = False self._iter = False
self._scalar = [] self._scalar = []
@ -694,8 +693,8 @@ class BaseQuerySet(object):
def in_bulk(self, object_ids): def in_bulk(self, object_ids):
"""Retrieve a set of documents by their ids. """Retrieve a set of documents by their ids.
:param object_ids: a list or tuple of ``ObjectId``\ s :param object_ids: a list or tuple of ObjectId's
:rtype: dict of ObjectIds as keys and collection-specific :rtype: dict of ObjectId's as keys and collection-specific
Document subclasses as values. Document subclasses as values.
.. versionadded:: 0.3 .. versionadded:: 0.3
@ -775,7 +774,6 @@ class BaseQuerySet(object):
"_ordering", "_ordering",
"_snapshot", "_snapshot",
"_timeout", "_timeout",
"_slave_okay",
"_read_preference", "_read_preference",
"_iter", "_iter",
"_scalar", "_scalar",
@ -1026,9 +1024,11 @@ class BaseQuerySet(object):
posts = BlogPost.objects(...).fields(comments=0) posts = BlogPost.objects(...).fields(comments=0)
To retrieve a subrange of array elements: To retrieve a subrange or sublist of array elements,
support exist for both the `slice` and `elemMatch` projection operator:
posts = BlogPost.objects(...).fields(slice__comments=5) posts = BlogPost.objects(...).fields(slice__comments=5)
posts = BlogPost.objects(...).fields(elemMatch__comments="test")
:param kwargs: A set of keyword arguments identifying what to :param kwargs: A set of keyword arguments identifying what to
include, exclude, or slice. include, exclude, or slice.
@ -1037,7 +1037,7 @@ class BaseQuerySet(object):
""" """
# Check for an operator and transform to mongo-style if there is # Check for an operator and transform to mongo-style if there is
operators = ["slice"] operators = ["slice", "elemMatch"]
cleaned_fields = [] cleaned_fields = []
for key, value in kwargs.items(): for key, value in kwargs.items():
parts = key.split("__") parts = key.split("__")
@ -1140,7 +1140,7 @@ class BaseQuerySet(object):
def explain(self): def explain(self):
"""Return an explain plan record for the """Return an explain plan record for the
:class:`~mongoengine.queryset.QuerySet`\ 's cursor. :class:`~mongoengine.queryset.QuerySet` cursor.
""" """
return self._cursor.explain() return self._cursor.explain()
@ -1170,20 +1170,6 @@ class BaseQuerySet(object):
queryset._timeout = enabled queryset._timeout = enabled
return queryset return queryset
# DEPRECATED. Has no more impact on PyMongo 3+
def slave_okay(self, enabled):
"""Enable or disable the slave_okay when querying.
:param enabled: whether or not the slave_okay is enabled
.. deprecated:: Ignored with PyMongo 3+
"""
msg = "slave_okay is deprecated as it has no impact when using PyMongo 3+."
warnings.warn(msg, DeprecationWarning)
queryset = self.clone()
queryset._slave_okay = enabled
return queryset
def read_preference(self, read_preference): def read_preference(self, read_preference):
"""Change the read_preference when querying. """Change the read_preference when querying.

View File

@ -169,9 +169,9 @@ def query(_doc_cls=None, **kwargs):
key = ".".join(parts) key = ".".join(parts)
if op is None or key not in mongo_query: if key not in mongo_query:
mongo_query[key] = value mongo_query[key] = value
elif key in mongo_query: else:
if isinstance(mongo_query[key], dict) and isinstance(value, dict): if isinstance(mongo_query[key], dict) and isinstance(value, dict):
mongo_query[key].update(value) mongo_query[key].update(value)
# $max/minDistance needs to come last - convert to SON # $max/minDistance needs to come last - convert to SON

View File

@ -108,6 +108,10 @@ CLASSIFIERS = [
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
PYTHON_VERSION = sys.version_info[0]
PY3 = PYTHON_VERSION == 3
PY2 = PYTHON_VERSION == 2
extra_opts = { extra_opts = {
"packages": find_packages(exclude=["tests", "tests.*"]), "packages": find_packages(exclude=["tests", "tests.*"]),
"tests_require": [ "tests_require": [
@ -116,9 +120,10 @@ extra_opts = {
"coverage<5.0", # recent coverage switched to sqlite format for the .coverage file which isn't handled properly by coveralls "coverage<5.0", # recent coverage switched to sqlite format for the .coverage file which isn't handled properly by coveralls
"blinker", "blinker",
"Pillow>=2.0.0, <7.0.0", # 7.0.0 dropped Python2 support "Pillow>=2.0.0, <7.0.0", # 7.0.0 dropped Python2 support
"zipp<2.0.0", # (dependency of pytest) dropped python2 support
], ],
} }
if sys.version_info[0] == 3: if PY3:
extra_opts["use_2to3"] = True extra_opts["use_2to3"] = True
if "test" in sys.argv: if "test" in sys.argv:
extra_opts["packages"] = find_packages() extra_opts["packages"] = find_packages()

View File

@ -65,7 +65,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond):
stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"] stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"]
assert ( assert (
re.match("^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored) re.match(r"^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored)
is not None is not None
) )
@ -74,7 +74,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
"date_with_dots" "date_with_dots"
] ]
assert ( assert (
re.match("^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$", stored) is not None re.match(r"^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$", stored) is not None
) )
def test_complexdatetime_usage(self): def test_complexdatetime_usage(self):

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest import pytest
from bson import InvalidDocument
from mongoengine import * from mongoengine import *
from mongoengine.base import BaseDict from mongoengine.base import BaseDict
@ -19,22 +20,24 @@ class TestDictField(MongoDBTestCase):
post = BlogPost(info=info).save() post = BlogPost(info=info).save()
assert get_as_pymongo(post) == {"_id": post.id, "info": info} assert get_as_pymongo(post) == {"_id": post.id, "info": info}
def test_general_things(self): def test_validate_invalid_type(self):
"""Ensure that dict types work as expected.""" class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
invalid_infos = ["my post", ["test", "test"], {1: "test"}]
for invalid_info in invalid_infos:
with pytest.raises(ValidationError):
BlogPost(info=invalid_info).validate()
def test_keys_with_dots_or_dollars(self):
class BlogPost(Document): class BlogPost(Document):
info = DictField() info = DictField()
BlogPost.drop_collection() BlogPost.drop_collection()
post = BlogPost() post = BlogPost()
post.info = "my post"
with pytest.raises(ValidationError):
post.validate()
post.info = ["test", "test"]
with pytest.raises(ValidationError):
post.validate()
post.info = {"$title": "test"} post.info = {"$title": "test"}
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
@ -48,25 +51,34 @@ class TestDictField(MongoDBTestCase):
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
post.validate() post.validate()
post.info = {1: "test"}
with pytest.raises(ValidationError):
post.validate()
post.info = {"nested": {"the.title": "test"}} post.info = {"nested": {"the.title": "test"}}
if get_mongodb_version() < MONGODB_36: if get_mongodb_version() < MONGODB_36:
with pytest.raises(ValidationError): # MongoDB < 3.6 rejects dots
# To avoid checking the mongodb version from the DictField class
# we rely on MongoDB to reject the data during the save
post.validate() post.validate()
with pytest.raises(InvalidDocument):
post.save()
else: else:
post.validate() post.validate()
post.info = {"dollar_and_dot": {"te$st.test": "test"}} post.info = {"dollar_and_dot": {"te$st.test": "test"}}
if get_mongodb_version() < MONGODB_36: if get_mongodb_version() < MONGODB_36:
with pytest.raises(ValidationError):
post.validate() post.validate()
with pytest.raises(InvalidDocument):
post.save()
else: else:
post.validate() post.validate()
post.info = {"title": "test"} def test_general_things(self):
"""Ensure that dict types work as expected."""
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
post = BlogPost(info={"title": "test"})
post.save() post.save()
post = BlogPost() post = BlogPost()

View File

@ -4476,6 +4476,74 @@ class TestQueryset(unittest.TestCase):
expected = "[u'A1', u'A2']" expected = "[u'A1', u'A2']"
assert expected == "%s" % sorted(names) assert expected == "%s" % sorted(names)
def test_fields(self):
class Bar(EmbeddedDocument):
v = StringField()
z = StringField()
class Foo(Document):
x = StringField()
y = IntField()
items = EmbeddedDocumentListField(Bar)
Foo.drop_collection()
Foo(x="foo1", y=1).save()
Foo(x="foo2", y=2, items=[]).save()
Foo(x="foo3", y=3, items=[Bar(z="a", v="V")]).save()
Foo(
x="foo4",
y=4,
items=[
Bar(z="a", v="V"),
Bar(z="b", v="W"),
Bar(z="b", v="X"),
Bar(z="c", v="V"),
],
).save()
Foo(
x="foo5",
y=5,
items=[
Bar(z="b", v="X"),
Bar(z="c", v="V"),
Bar(z="d", v="V"),
Bar(z="e", v="V"),
],
).save()
foos_with_x = list(Foo.objects.order_by("y").fields(x=1))
assert all(o.x is not None for o in foos_with_x)
foos_without_y = list(Foo.objects.order_by("y").fields(y=0))
assert all(o.y is None for o in foos_with_x)
foos_with_sliced_items = list(Foo.objects.order_by("y").fields(slice__items=1))
assert foos_with_sliced_items[0].items == []
assert foos_with_sliced_items[1].items == []
assert len(foos_with_sliced_items[2].items) == 1
assert foos_with_sliced_items[2].items[0].z == "a"
assert len(foos_with_sliced_items[3].items) == 1
assert foos_with_sliced_items[3].items[0].z == "a"
assert len(foos_with_sliced_items[4].items) == 1
assert foos_with_sliced_items[4].items[0].z == "b"
foos_with_elem_match_items = list(
Foo.objects.order_by("y").fields(elemMatch__items={"z": "b"})
)
assert foos_with_elem_match_items[0].items == []
assert foos_with_elem_match_items[1].items == []
assert foos_with_elem_match_items[2].items == []
assert len(foos_with_elem_match_items[3].items) == 1
assert foos_with_elem_match_items[3].items[0].z == "b"
assert foos_with_elem_match_items[3].items[0].v == "W"
assert len(foos_with_elem_match_items[4].items) == 1
assert foos_with_elem_match_items[4].items[0].z == "b"
def test_elem_match(self): def test_elem_match(self):
class Foo(EmbeddedDocument): class Foo(EmbeddedDocument):
shape = StringField() shape = StringField()

View File

@ -24,6 +24,12 @@ class TestTransform(unittest.TestCase):
} }
assert transform.query(friend__age__gte=30) == {"friend.age": {"$gte": 30}} assert transform.query(friend__age__gte=30) == {"friend.age": {"$gte": 30}}
assert transform.query(name__exists=True) == {"name": {"$exists": True}} assert transform.query(name__exists=True) == {"name": {"$exists": True}}
assert transform.query(name=["Mark"], __raw__={"name": {"$in": "Tom"}}) == {
"$and": [{"name": ["Mark"]}, {"name": {"$in": "Tom"}}]
}
assert transform.query(name__in=["Tom"], __raw__={"name": "Mark"}) == {
"$and": [{"name": {"$in": ["Tom"]}}, {"name": "Mark"}]
}
def test_transform_update(self): def test_transform_update(self):
class LisDoc(Document): class LisDoc(Document):