Skip to content

Commit

Permalink
Merge pull request #2723 from bagerard/clone_null_check_oid
Browse files Browse the repository at this point in the history
Work on top of "Added null check for Issue"
  • Loading branch information
bagerard authored Jan 8, 2023
2 parents d783f32 + e81a106 commit c4ef4d4
Show file tree
Hide file tree
Showing 14 changed files with 118 additions and 23 deletions.
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Development
Although it could work to switch an existing DecimalField to Decimal128Field without applying a migration script,
it is not recommended to do so (DecimalField uses float/str to store the value, Decimal128Field uses Decimal128).
- BREAKING CHANGE: When using ListField(EnumField) or DictField(EnumField), the values weren't always cast into the Enum (#2531)
- BREAKING CHANGE (bugfix) Querying ObjectIdField or ComplexDateTimeField with None will no longer raise a ValidationError (#2681)

Changes in 0.25.0
=================
Expand Down
2 changes: 1 addition & 1 deletion mongoengine/base/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def to_mongo(self, use_db_field=True, fields=None):
value = field.generate()
self._data[field_name] = value

if (value is not None) or (field.null):
if value is not None or field.null:
if use_db_field:
data[field.db_field] = value
else:
Expand Down
29 changes: 16 additions & 13 deletions mongoengine/base/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,20 @@ def __init__(
: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
has been set (or if the value has been unset). It can be a
has been set, if the value is set to None or has been unset. It can be a
callable.
:param unique: Is the field value unique or not. Defaults to False.
:param unique: Is the field value unique or not (Creates an index). Defaults to False.
:param unique_with: (optional) The other field this field should be
unique with.
:param primary_key: Mark this field as the primary key. Defaults to False.
unique with (Creates an index).
:param primary_key: Mark this field as the primary key ((Creates an index)). Defaults to False.
:param validation: (optional) A callable to validate the value of the
field. The callable takes the value as parameter and should raise
a ValidationError if validation fails
:param choices: (optional) The valid choices
:param null: (optional) If the field value can be null. If no and there is a default value
then the default value is set
:param null: (optional) If the field value can be null when a default exist. If not set, the default value
will be used in case a field with a default value is set to None. Defaults to False.
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
means that uniqueness won't be enforced for `None` values
means that uniqueness won't be enforced for `None` values (Creates an index). Defaults to False.
:param **kwargs: (optional) Arbitrary indirection-free metadata for
this field can be supplied as additional keyword arguments and
accessed as attributes of the field. Must not conflict with any
Expand Down Expand Up @@ -521,14 +521,17 @@ def to_python(self, value):
return value

def to_mongo(self, value):
if not isinstance(value, ObjectId):
try:
return ObjectId(str(value))
except Exception as e:
self.error(str(e))
return value
if isinstance(value, ObjectId):
return value

try:
return ObjectId(str(value))
except Exception as e:
self.error(str(e))

def prepare_query_value(self, op, value):
if value is None:
return value
return self.to_mongo(value)

def validate(self, value):
Expand Down
4 changes: 3 additions & 1 deletion mongoengine/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,9 @@ def save(
try:
# Save a new document or update an existing one
if created:
object_id = self._save_create(doc, force_insert, write_concern)
object_id = self._save_create(
doc=doc, force_insert=force_insert, write_concern=write_concern
)
else:
object_id, created = self._save_update(
doc, save_condition, write_concern
Expand Down
9 changes: 4 additions & 5 deletions mongoengine/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,9 +490,6 @@ def __init__(
super().__init__(**kwargs)

def to_python(self, value):
if value is None:
return value

# Convert to string for python 2.6 before casting to Decimal
try:
value = decimal.Decimal("%s" % value)
Expand All @@ -506,8 +503,6 @@ def to_python(self, value):
return value.quantize(decimal.Decimal(), rounding=self.rounding)

def to_mongo(self, value):
if value is None:
return value
if self.force_string:
return str(self.to_python(value))
return float(self.to_python(value))
Expand All @@ -528,6 +523,8 @@ def validate(self, value):
self.error("Decimal value is too large")

def prepare_query_value(self, op, value):
if value is None:
return value
return super().prepare_query_value(op, self.to_mongo(value))


Expand Down Expand Up @@ -731,6 +728,8 @@ def to_mongo(self, value):
return self._convert_from_datetime(value)

def prepare_query_value(self, op, value):
if value is None:
return value
return super().prepare_query_value(op, self._convert_from_datetime(value))


Expand Down
15 changes: 13 additions & 2 deletions tests/document/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
import weakref
from datetime import datetime
from unittest.mock import Mock

import bson
import pytest
Expand Down Expand Up @@ -1034,6 +1035,17 @@ def test_save(self):
"_id": person.id,
}

def test_save_write_concern(self):
class Recipient(Document):
email = EmailField(required=True)

rec = Recipient(email="garbage@garbage.com")

fn = Mock()
rec._save_create = fn
rec.save(write_concern={"w": 0})
assert fn.call_args[1]["write_concern"] == {"w": 0}

def test_save_skip_validation(self):
class Recipient(Document):
email = EmailField(required=True)
Expand Down Expand Up @@ -3593,8 +3605,7 @@ class User(Document):
cdt_fld = ComplexDateTimeField(null=True)

User.objects.delete()
u = User(name="user")
u.save()
u = User(name="user").save()
u_from_db = User.objects.get(name="user")
u_from_db.height = None
u_from_db.save()
Expand Down
6 changes: 6 additions & 0 deletions tests/fields/test_complex_datetime_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,9 @@ class Log(Document):

with pytest.raises(ValidationError):
log.save()

def test_query_none_value_dont_raise(self):
class Log(Document):
timestamp = ComplexDateTimeField()

_ = list(Log.objects(timestamp=None))
2 changes: 2 additions & 0 deletions tests/fields/test_decimal_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class Person(Document):
fetched_person = Person.objects.first()
fetched_person.value is None

assert Person.objects(value=None).first() is not None

def test_validation(self):
"""Ensure that invalid values cannot be assigned to decimal fields."""

Expand Down
6 changes: 6 additions & 0 deletions tests/fields/test_float_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ class BigPerson(Document):
big_person.height = 2**100000 # Too big for a float value
with pytest.raises(ValidationError):
big_person.validate()

def test_query_none_value_dont_raise(self):
class BigPerson(Document):
height = FloatField()

_ = list(BigPerson.objects(height=None))
7 changes: 7 additions & 0 deletions tests/fields/test_object_id_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,10 @@ class MyDoc(Document):
doc = MyDoc(oid="not-an-oid!")
with pytest.raises(ValidationError, match="Invalid ObjectID"):
doc.save()

def test_query_none_value_dont_raise(self):
# cf issue #2681
class MyDoc(Document):
oid = ObjectIdField(null=True)

_ = list(MyDoc.objects(oid=None))
9 changes: 9 additions & 0 deletions tests/queryset/test_modify.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import unittest

import pytest

from mongoengine import (
Document,
IntField,
Expand Down Expand Up @@ -30,6 +32,13 @@ def test_modify(self):
assert old_doc.to_json() == doc.to_json()
self._assert_db_equal([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}])

def test_modify_full_response_raise_value_error_for_recent_mongo(self):
Doc(id=0, value=0).save()
Doc(id=1, value=1).save()

with pytest.raises(ValueError):
Doc.objects(id=1).modify(set__value=-1, full_response=True)

def test_modify_with_new(self):
Doc(id=0, value=0).save()
doc = Doc(id=1, value=1).save()
Expand Down
33 changes: 33 additions & 0 deletions tests/queryset/test_queryset.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
QuerySetManager,
queryset_manager,
)
from mongoengine.queryset.base import BaseQuerySet
from tests.utils import (
requires_mongodb_gte_42,
requires_mongodb_gte_44,
Expand Down Expand Up @@ -150,6 +151,14 @@ def test_limit_0_returns_all_documents(self):
persons = list(self.Person.objects().limit(0))
assert len(persons) == 2 == n_docs

def test_limit_0(self):
"""Ensure that QuerySet.limit works as expected."""
self.Person.objects.create(name="User A", age=20)

# Test limit with 0 as parameter
qs = self.Person.objects.limit(0)
assert qs.count() == 0

def test_limit(self):
"""Ensure that QuerySet.limit works as expected."""
user_a = self.Person.objects.create(name="User A", age=20)
Expand Down Expand Up @@ -3507,6 +3516,26 @@ class Bar(Document):
assert Foo.objects.distinct("bar") == [bar]
assert Foo.objects.no_dereference().distinct("bar") == [bar.pk]

def test_base_queryset_iter_raise_not_implemented(self):
class Tmp(Document):
pass

qs = BaseQuerySet(document=Tmp, collection=Tmp._get_collection())
with pytest.raises(NotImplementedError):
_ = list(qs)

def test_search_text_raise_if_called_2_times(self):
class News(Document):
title = StringField()
content = StringField()
is_active = BooleanField(default=True)

News.drop_collection()
with pytest.raises(OperationError):
News.objects.search_text("t1", language="portuguese").search_text(
"t2", language="french"
)

def test_text_indexes(self):
class News(Document):
title = StringField()
Expand Down Expand Up @@ -3899,6 +3928,10 @@ class BlogPost(Document):
assert objects[post_2.id].title == post_2.title
assert objects[post_5.id].title == post_5.title

objects = BlogPost.objects.as_pymongo().in_bulk(ids)
assert len(objects) == 3
assert isinstance(objects[post_1.id], dict)

BlogPost.drop_collection()

def tearDown(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_connection_mongomock.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class Person(Document):
name = StringField()

Person.drop_collection()
assert Person.objects.count() == 0
assert Person.objects.limit(0).count(with_limit_and_skip=True) == 0

bob = Person(name="Bob").save()
john = Person(name="John").save()
Expand Down
16 changes: 16 additions & 0 deletions tests/test_pymongo_support.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from mongoengine import Document
from mongoengine.pymongo_support import count_documents
from tests.utils import MongoDBTestCase


class TestPymongoSupport(MongoDBTestCase):
def test_count_documents(self):
class Test(Document):
pass

Test.drop_collection()
Test().save()
Test().save()
assert count_documents(Test._get_collection(), filter={}) == 2
assert count_documents(Test._get_collection(), filter={}, skip=1) == 1
assert count_documents(Test._get_collection(), filter={}, limit=0) == 0

0 comments on commit c4ef4d4

Please sign in to comment.