Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dynamic check in EmbeddedDocumentField lookup_member #2252

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d738462
Fix bug introduced in -1.19 related to DictField validate failing wit…
cp-bagerard Jan 11, 2020
86e965f
remove very old deprecated method
cp-bagerard Dec 30, 2019
095217e
remove methods that were derecated in 2013...
cp-bagerard Jan 13, 2020
ee9a8b1
Add check for _dynamic in lookup_member that returns DynamicField for…
tjhall13 Jan 23, 2020
450658d
fix indirect library version that dropped python2 support recently
cp-bagerard Feb 4, 2020
235b1a3
Merge pull request #2265 from bagerard/fix_ci_lib_drop_python2_support
bagerard Feb 8, 2020
4bca3de
Add support for the elemMatch projection operator. Add basic tests to…
abarto Feb 14, 2020
81f9b35
Add return info in the save function docstring
leodmgs Feb 21, 2020
b453a96
Merge pull request #2272 from leodmgs/add-return-docstr-to-save-function
bagerard Mar 2, 2020
cfb4943
reformat with black
cp-bagerard Mar 2, 2020
d287f48
Fix for combining raw and regular filters
Pacu2 Feb 4, 2020
fda2e2b
Update changelog
Pacu2 Feb 4, 2020
a4d11ee
Merge pull request #2264 from Pacu2/2263/combining-raw-and-regular-qu…
bagerard Mar 3, 2020
aa4a6ae
Fix invalid escape seq in codebase
cp-bagerard Mar 15, 2020
c937af3
Merge pull request #2281 from bagerard/fix_py38_deprecation_warn
bagerard Mar 15, 2020
beabaee
Merge branch 'master' of github.com:MongoEngine/mongoengine into supp…
cp-bagerard Mar 15, 2020
c0c0efc
improve docstring related to #2267 and document the change in the cha…
cp-bagerard Mar 15, 2020
7c53339
Merge pull request #2267 from abarto/support-elemmatch-projection
bagerard Mar 15, 2020
8751224
Merge branch 'master' of github.com:MongoEngine/mongoengine into fix_…
cp-bagerard Mar 16, 2020
ad0669a
update changelog
cp-bagerard Mar 16, 2020
00ae629
Merge pull request #2242 from bagerard/fix_dictfield_validation
bagerard Mar 17, 2020
aadc626
remove qs.slave_okay() that is deprecated since pymongo3
cp-bagerard Mar 17, 2020
8eb5179
Remove Field(name='...') which was deprecated when db_field was intro…
cp-bagerard Mar 17, 2020
beaa974
Merge pull request #2283 from bagerard/remove_slave_okay_deprecated
bagerard Mar 17, 2020
15c3dde
Merge branch 'master' of github.com:MongoEngine/mongoengine into remo…
cp-bagerard Mar 17, 2020
547cd4a
Merge branch 'master' of github.com:MongoEngine/mongoengine into remo…
cp-bagerard Mar 17, 2020
68be9fe
Merge pull request #2284 from bagerard/remove_field_name_attribute_de…
bagerard Mar 17, 2020
6cc6229
Merge pull request #2245 from bagerard/remove_old_deprecated_method
bagerard Mar 17, 2020
283ecfc
Merge branch '2251-fix-lookuperror-in-dynamic-embedded-document' of g…
cp-bagerard Mar 17, 2020
610c723
fix linting
cp-bagerard Mar 17, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,4 @@ that much better:
* Filip Kucharczyk (https://github.com/Pacu2)
* Eric Timmons (https://github.com/daewok)
* Matthew Simpson (https://github.com/mcsimps2)
* Leonardo Domingues (https://github.com/leodmgs)
9 changes: 9 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ Development
===========
- (Fill this out as you fix issues and develop your features).
- Add Mongo 4.0 to Travis
<<<<<<< HEAD
- 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
- 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 name parameter in Field constructor e.g `StringField(name="...")`, it was deprecated a while ago in favor of db_field
- Remove method queryset.slave_okay() that was deprecated a while ago and disappeared since pymongo3
- Fix querying and updating dynamic fields on DynamicEmbeddedDocuments #2251

Changes in 0.19.1
=================
Expand Down
7 changes: 1 addition & 6 deletions mongoengine/base/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class BaseField(object):
def __init__(
self,
db_field=None,
name=None,
required=False,
default=None,
unique=False,
Expand All @@ -51,7 +50,6 @@ def __init__(
"""
:param db_field: The database field to store this field in
(defaults to the name of the field)
:param name: Deprecated - use db_field
:param required: If the field is required. Whether it has to have a
value or not. Defaults to False.
:param default: (optional) The default value for this field if no value
Expand All @@ -75,11 +73,8 @@ def __init__(
existing attributes. Common metadata includes `verbose_name` and
`help_text`.
"""
self.db_field = (db_field or name) if not primary_key else "_id"
self.db_field = db_field if not primary_key else "_id"

if name:
msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
warnings.warn(msg, DeprecationWarning)
self.required = required or primary_key
self.default = default
self.unique = bool(unique or unique_with)
Expand Down
4 changes: 2 additions & 2 deletions mongoengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class InvalidCollectionError(Exception):


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
fields on :class:`~mongoengine.Document`\ s through the
:class:`~mongoengine.EmbeddedDocumentField` field type.
Expand Down Expand Up @@ -332,7 +332,7 @@ def save(
):
"""Save the :class:`~mongoengine.Document` to the database. If the
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
updates of existing documents.
Expand Down
17 changes: 9 additions & 8 deletions mongoengine/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,9 @@ def lookup_member(self, member_name):
if field:
return field

if any(doc_type._dynamic for doc_type in doc_and_subclasses):
return DynamicField(db_field=member_name)

def prepare_query_value(self, op, value):
if value is not None and not isinstance(value, self.document_type):
try:
Expand Down Expand Up @@ -1088,14 +1091,12 @@ def validate(self, value):
msg = "Invalid dictionary key - documents must have only string keys"
self.error(msg)

curr_mongo_ver = get_mongodb_version()

if curr_mongo_ver < MONGODB_36 and key_has_dot_or_dollar(value):
self.error(
'Invalid dictionary key name - keys may not contain "."'
' or startswith "$" characters'
)
elif curr_mongo_ver >= MONGODB_36 and key_starts_with_dollar(value):
# Following condition applies to MongoDB >= 3.6
# older Mongo has stricter constraints but
# it will be rejected upon insertion anyway
# Having a validation that depends on the MongoDB version
# is not straightforward as the field isn't aware of the connected Mongo
if key_starts_with_dollar(value):
self.error(
'Invalid dictionary key name - keys may not startswith "$" characters'
)
Expand Down
2 changes: 1 addition & 1 deletion mongoengine/mongodb_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


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)
"""
Expand Down
48 changes: 7 additions & 41 deletions mongoengine/queryset/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ def __init__(self, document, collection):
self._ordering = None
self._snapshot = False
self._timeout = True
self._slave_okay = False
self._read_preference = None
self._iter = False
self._scalar = []
Expand Down Expand Up @@ -694,8 +693,8 @@ def with_id(self, object_id):
def in_bulk(self, object_ids):
"""Retrieve a set of documents by their ids.

:param object_ids: a list or tuple of ``ObjectId``\ s
:rtype: dict of ObjectIds as keys and collection-specific
:param object_ids: a list or tuple of ObjectId's
:rtype: dict of ObjectId's as keys and collection-specific
Document subclasses as values.

.. versionadded:: 0.3
Expand Down Expand Up @@ -775,7 +774,6 @@ def _clone_into(self, new_qs):
"_ordering",
"_snapshot",
"_timeout",
"_slave_okay",
"_read_preference",
"_iter",
"_scalar",
Expand Down Expand Up @@ -1026,9 +1024,11 @@ def fields(self, _only_called=False, **kwargs):

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(elemMatch__comments="test")

:param kwargs: A set of keyword arguments identifying what to
include, exclude, or slice.
Expand All @@ -1037,7 +1037,7 @@ def fields(self, _only_called=False, **kwargs):
"""

# Check for an operator and transform to mongo-style if there is
operators = ["slice"]
operators = ["slice", "elemMatch"]
cleaned_fields = []
for key, value in kwargs.items():
parts = key.split("__")
Expand Down Expand Up @@ -1140,7 +1140,7 @@ def comment(self, text):

def explain(self):
"""Return an explain plan record for the
:class:`~mongoengine.queryset.QuerySet`\ 's cursor.
:class:`~mongoengine.queryset.QuerySet` cursor.
"""
return self._cursor.explain()

Expand Down Expand Up @@ -1170,20 +1170,6 @@ def timeout(self, enabled):
queryset._timeout = enabled
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):
"""Change the read_preference when querying.

Expand Down Expand Up @@ -1958,23 +1944,3 @@ def _chainable_method(self, method_name, val):
setattr(queryset, "_" + method_name, val)

return queryset

# Deprecated
def ensure_index(self, **kwargs):
"""Deprecated use :func:`Document.ensure_index`"""
msg = (
"Doc.objects()._ensure_index() is deprecated. "
"Use Doc.ensure_index() instead."
)
warnings.warn(msg, DeprecationWarning)
self._document.__class__.ensure_index(**kwargs)
return self

def _ensure_indexes(self):
"""Deprecated use :func:`~Document.ensure_indexes`"""
msg = (
"Doc.objects()._ensure_indexes() is deprecated. "
"Use Doc.ensure_indexes() instead."
)
warnings.warn(msg, DeprecationWarning)
self._document.__class__.ensure_indexes()
4 changes: 2 additions & 2 deletions mongoengine/queryset/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ def query(_doc_cls=None, **kwargs):

key = ".".join(parts)

if op is None or key not in mongo_query:
if key not in mongo_query:
mongo_query[key] = value
elif key in mongo_query:
else:
if isinstance(mongo_query[key], dict) and isinstance(value, dict):
mongo_query[key].update(value)
# $max/minDistance needs to come last - convert to SON
Expand Down
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def run_tests(self):
"Topic :: Software Development :: Libraries :: Python Modules",
]

PYTHON_VERSION = sys.version_info[0]
PY3 = PYTHON_VERSION == 3
PY2 = PYTHON_VERSION == 2

extra_opts = {
"packages": find_packages(exclude=["tests", "tests.*"]),
"tests_require": [
Expand All @@ -116,9 +120,10 @@ def run_tests(self):
"coverage<5.0", # recent coverage switched to sqlite format for the .coverage file which isn't handled properly by coveralls
"blinker",
"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
if "test" in sys.argv:
extra_opts["packages"] = find_packages()
Expand Down
4 changes: 2 additions & 2 deletions tests/fields/test_complex_datetime_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class LogEntry(Document):
for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond):
stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"]
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
)

Expand All @@ -74,7 +74,7 @@ class LogEntry(Document):
"date_with_dots"
]
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):
Expand Down
49 changes: 30 additions & 19 deletions tests/fields/test_dict_field.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from bson import InvalidDocument
import pytest

from mongoengine import *
Expand All @@ -19,22 +19,24 @@ class BlogPost(Document):
post = BlogPost(info=info).save()
assert get_as_pymongo(post) == {"_id": post.id, "info": info}

def test_general_things(self):
"""Ensure that dict types work as expected."""
def test_validate_invalid_type(self):
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):
info = DictField()

BlogPost.drop_collection()

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"}
with pytest.raises(ValidationError):
Expand All @@ -48,25 +50,34 @@ class BlogPost(Document):
with pytest.raises(ValidationError):
post.validate()

post.info = {1: "test"}
with pytest.raises(ValidationError):
post.validate()

post.info = {"nested": {"the.title": "test"}}
if get_mongodb_version() < MONGODB_36:
with pytest.raises(ValidationError):
post.validate()
# 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()
with pytest.raises(InvalidDocument):
post.save()
else:
post.validate()

post.info = {"dollar_and_dot": {"te$st.test": "test"}}
if get_mongodb_version() < MONGODB_36:
with pytest.raises(ValidationError):
post.validate()
post.validate()
with pytest.raises(InvalidDocument):
post.save()
else:
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 = BlogPost()
Expand Down
28 changes: 28 additions & 0 deletions tests/fields/test_embedded_document_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from mongoengine import (
Document,
DynamicEmbeddedDocument,
EmbeddedDocument,
EmbeddedDocumentField,
GenericEmbeddedDocumentField,
Expand Down Expand Up @@ -92,6 +93,33 @@ class Person(Document):
assert exclude_p.settings.foo2 == p.settings.foo2
assert exclude_p.name == p.name

def test_dynamic_embedded_document_attribute(self):
class DynamicSettings(DynamicEmbeddedDocument):
known_field = StringField()

class Person(Document):
name = StringField()
settings = EmbeddedDocumentField(DynamicSettings)

Person.drop_collection()

p = Person(
settings=DynamicSettings(known_field="abc", dynamic_field1="123"),
name="John",
).save()

# Test querying by a dynamic field that is not defined in the schema
assert Person.objects(settings__dynamic_field1="123").first().id == p.id

p_modified = Person.objects(settings__known_field="abc").modify(
set__settings__dynamic_field1="789"
)
p.reload()

# Test if the update occurred successfully
assert p_modified.settings.dynamic_field1 == "123"
assert p.settings.dynamic_field1 == "789"

def test_query_embedded_document_attribute_with_inheritance(self):
class BaseSettings(EmbeddedDocument):
meta = {"allow_inheritance": True}
Expand Down