From cab659dce6048d391a4dd470d5bf4b8cbbaf36ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sat, 16 Feb 2019 21:54:05 +0100 Subject: [PATCH 1/3] Fix documentation of Queryset.update regarding full_result #1995 --- docs/changelog.rst | 2 ++ mongoengine/queryset/base.py | 20 ++++++++++++++------ tests/queryset/queryset.py | 13 +++++++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d8bed7e6..b6c5b277 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ Development - (Fill this out as you fix issues and develop your features). - Fix .only() working improperly after using .count() of the same instance of QuerySet - POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (_cls, _id) when using `QuerySet.as_pymongo` #1976 +- Document a BREAKING CHANGE introduced in 0.15.3 and not reported at that time (#1995) ================= Changes in 0.16.3 @@ -64,6 +65,7 @@ Changes in 0.16.0 Changes in 0.15.3 ================= +- BREAKING CHANGES: `Queryset.update/update_one` methods now returns an UpdateResult when `full_result=True` is provided and no longer a dict (relates to #1491) - Subfield resolve error in generic_emdedded_document query #1651 #1652 - use each modifier only with $position #1673 #1675 - Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 0ebeafa6..391f4f86 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -498,11 +498,12 @@ class BaseQuerySet(object): ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. - :param full_result: Return the full result dictionary rather than just the number - updated, e.g. return - ``{'n': 2, 'nModified': 2, 'ok': 1.0, 'updatedExisting': True}``. + :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number + updated items :param update: Django-style update keyword arguments + :returns the number of updated documents (unless ``full_result`` is True) + .. versionadded:: 0.2 """ if not update and not upsert: @@ -566,7 +567,7 @@ class BaseQuerySet(object): document = self._document.objects.with_id(atomic_update.upserted_id) return document - def update_one(self, upsert=False, write_concern=None, **update): + def update_one(self, upsert=False, write_concern=None, full_result=False, **update): """Perform an atomic update on the fields of the first document matched by the query. @@ -577,12 +578,19 @@ class BaseQuerySet(object): ``save(..., write_concern={w: 2, fsync: True}, ...)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. + :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number + updated items :param update: Django-style update keyword arguments - + full_result + :returns the number of updated documents (unless ``full_result`` is True) .. versionadded:: 0.2 """ return self.update( - upsert=upsert, multi=False, write_concern=write_concern, **update) + upsert=upsert, + multi=False, + write_concern=write_concern, + full_result=full_result, + **update) def modify(self, upsert=False, full_response=False, remove=False, new=False, **update): """Update and return the updated document. diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c183aa86..4dac6922 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2233,6 +2233,19 @@ class QuerySetTest(unittest.TestCase): bar.reload() self.assertEqual(len(bar.foos), 0) + def test_update_one_check_return_with_full_result(self): + class BlogTag(Document): + name = StringField(required=True) + + BlogTag.drop_collection() + + BlogTag(name='garbage').save() + default_update = BlogTag.objects.update_one(name='new') + self.assertEqual(default_update, 1) + + full_result_update = BlogTag.objects.update_one(name='new', full_result=True) + self.assertIsInstance(full_result_update, UpdateResult) + def test_update_one_pop_generic_reference(self): class BlogTag(Document): From 4a46f5f095e424fb0b91ac4ef85bed9241f33ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sun, 17 Feb 2019 21:32:32 +0100 Subject: [PATCH 2/3] Separate fields tests into separate modules (date/datetime/complexdatetime) relates to #1983 --- tests/fields/fields.py | 497 +------------------- tests/fields/test_complex_datetime_field.py | 189 ++++++++ tests/fields/test_date_field.py | 184 ++++++++ tests/fields/test_datetime_field.py | 208 ++++++++ 4 files changed, 583 insertions(+), 495 deletions(-) create mode 100644 tests/fields/test_complex_datetime_field.py create mode 100644 tests/fields/test_date_field.py create mode 100644 tests/fields/test_datetime_field.py diff --git a/tests/fields/fields.py b/tests/fields/fields.py index b09c0a2d..8f256782 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -10,11 +10,6 @@ import sys from nose.plugins.skip import SkipTest import six -try: - import dateutil -except ImportError: - dateutil = None - from decimal import Decimal from bson import Binary, DBRef, ObjectId, SON @@ -30,55 +25,9 @@ from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, from tests.utils import MongoDBTestCase -__all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") - class FieldTest(MongoDBTestCase): - def test_datetime_from_empty_string(self): - """ - Ensure an exception is raised when trying to - cast an empty string to datetime. - """ - class MyDoc(Document): - dt = DateTimeField() - - md = MyDoc(dt='') - self.assertRaises(ValidationError, md.save) - - def test_date_from_empty_string(self): - """ - Ensure an exception is raised when trying to - cast an empty string to datetime. - """ - class MyDoc(Document): - dt = DateField() - - md = MyDoc(dt='') - self.assertRaises(ValidationError, md.save) - - def test_datetime_from_whitespace_string(self): - """ - Ensure an exception is raised when trying to - cast a whitespace-only string to datetime. - """ - class MyDoc(Document): - dt = DateTimeField() - - md = MyDoc(dt=' ') - self.assertRaises(ValidationError, md.save) - - def test_date_from_whitespace_string(self): - """ - Ensure an exception is raised when trying to - cast a whitespace-only string to datetime. - """ - class MyDoc(Document): - dt = DateField() - - md = MyDoc(dt=' ') - self.assertRaises(ValidationError, md.save) - def test_default_values_nothing_set(self): """Ensure that default field values are used when creating a document. @@ -695,273 +644,6 @@ class FieldTest(MongoDBTestCase): person.api_key = api_key self.assertRaises(ValidationError, person.validate) - def test_datetime_validation(self): - """Ensure that invalid values cannot be assigned to datetime - fields. - """ - class LogEntry(Document): - time = DateTimeField() - - log = LogEntry() - log.time = datetime.datetime.now() - log.validate() - - log.time = datetime.date.today() - log.validate() - - log.time = datetime.datetime.now().isoformat(' ') - log.validate() - - if dateutil: - log.time = datetime.datetime.now().isoformat('T') - log.validate() - - log.time = -1 - self.assertRaises(ValidationError, log.validate) - log.time = 'ABC' - self.assertRaises(ValidationError, log.validate) - - def test_date_validation(self): - """Ensure that invalid values cannot be assigned to datetime - fields. - """ - class LogEntry(Document): - time = DateField() - - log = LogEntry() - log.time = datetime.datetime.now() - log.validate() - - log.time = datetime.date.today() - log.validate() - - log.time = datetime.datetime.now().isoformat(' ') - log.validate() - - if dateutil: - log.time = datetime.datetime.now().isoformat('T') - log.validate() - - log.time = -1 - self.assertRaises(ValidationError, log.validate) - log.time = 'ABC' - self.assertRaises(ValidationError, log.validate) - - def test_datetime_tz_aware_mark_as_changed(self): - from mongoengine import connection - - # Reset the connections - connection._connection_settings = {} - connection._connections = {} - connection._dbs = {} - - connect(db='mongoenginetest', tz_aware=True) - - class LogEntry(Document): - time = DateTimeField() - - LogEntry.drop_collection() - - LogEntry(time=datetime.datetime(2013, 1, 1, 0, 0, 0)).save() - - log = LogEntry.objects.first() - log.time = datetime.datetime(2013, 1, 1, 0, 0, 0) - self.assertEqual(['time'], log._changed_fields) - - def test_datetime(self): - """Tests showing pymongo datetime fields handling of microseconds. - Microseconds are rounded to the nearest millisecond and pre UTC - handling is wonky. - - See: http://api.mongodb.org/python/current/api/bson/son.html#dt - """ - class LogEntry(Document): - date = DateTimeField() - - LogEntry.drop_collection() - - # Test can save dates - log = LogEntry() - log.date = datetime.date.today() - log.save() - log.reload() - self.assertEqual(log.date.date(), datetime.date.today()) - - # Post UTC - microseconds are rounded (down) nearest millisecond and - # dropped - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) - d2 = datetime.datetime(1970, 1, 1, 0, 0, 1) - log = LogEntry() - log.date = d1 - log.save() - log.reload() - self.assertNotEqual(log.date, d1) - self.assertEqual(log.date, d2) - - # Post UTC - microseconds are rounded (down) nearest millisecond - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) - d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000) - log.date = d1 - log.save() - log.reload() - self.assertNotEqual(log.date, d1) - self.assertEqual(log.date, d2) - - if not six.PY3: - # Pre UTC dates microseconds below 1000 are dropped - # This does not seem to be true in PY3 - d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) - d2 = datetime.datetime(1969, 12, 31, 23, 59, 59) - log.date = d1 - log.save() - log.reload() - self.assertNotEqual(log.date, d1) - self.assertEqual(log.date, d2) - - def test_date(self): - """Tests showing pymongo date fields - - See: http://api.mongodb.org/python/current/api/bson/son.html#dt - """ - class LogEntry(Document): - date = DateField() - - LogEntry.drop_collection() - - # Test can save dates - log = LogEntry() - log.date = datetime.date.today() - log.save() - log.reload() - self.assertEqual(log.date, datetime.date.today()) - - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) - d2 = datetime.datetime(1970, 1, 1, 0, 0, 1) - log = LogEntry() - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1.date()) - self.assertEqual(log.date, d2.date()) - - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) - d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000) - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1.date()) - self.assertEqual(log.date, d2.date()) - - if not six.PY3: - # Pre UTC dates microseconds below 1000 are dropped - # This does not seem to be true in PY3 - d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) - d2 = datetime.datetime(1969, 12, 31, 23, 59, 59) - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1.date()) - self.assertEqual(log.date, d2.date()) - - def test_datetime_usage(self): - """Tests for regular datetime fields""" - class LogEntry(Document): - date = DateTimeField() - - LogEntry.drop_collection() - - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1) - log = LogEntry() - log.date = d1 - log.validate() - log.save() - - for query in (d1, d1.isoformat(' ')): - log1 = LogEntry.objects.get(date=query) - self.assertEqual(log, log1) - - if dateutil: - log1 = LogEntry.objects.get(date=d1.isoformat('T')) - self.assertEqual(log, log1) - - # create additional 19 log entries for a total of 20 - for i in range(1971, 1990): - d = datetime.datetime(i, 1, 1, 0, 0, 1) - LogEntry(date=d).save() - - self.assertEqual(LogEntry.objects.count(), 20) - - # Test ordering - logs = LogEntry.objects.order_by("date") - i = 0 - while i < 19: - self.assertTrue(logs[i].date <= logs[i + 1].date) - i += 1 - - logs = LogEntry.objects.order_by("-date") - i = 0 - while i < 19: - self.assertTrue(logs[i].date >= logs[i + 1].date) - i += 1 - - # Test searching - logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) - self.assertEqual(logs.count(), 10) - - logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1)) - self.assertEqual(logs.count(), 10) - - logs = LogEntry.objects.filter( - date__lte=datetime.datetime(1980, 1, 1), - date__gte=datetime.datetime(1975, 1, 1), - ) - self.assertEqual(logs.count(), 5) - - def test_date_usage(self): - """Tests for regular datetime fields""" - class LogEntry(Document): - date = DateField() - - LogEntry.drop_collection() - - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1) - log = LogEntry() - log.date = d1 - log.validate() - log.save() - - for query in (d1, d1.isoformat(' ')): - log1 = LogEntry.objects.get(date=query) - self.assertEqual(log, log1) - - if dateutil: - log1 = LogEntry.objects.get(date=d1.isoformat('T')) - self.assertEqual(log, log1) - - # create additional 19 log entries for a total of 20 - for i in range(1971, 1990): - d = datetime.datetime(i, 1, 1, 0, 0, 1) - LogEntry(date=d).save() - - self.assertEqual(LogEntry.objects.count(), 20) - - # Test ordering - logs = LogEntry.objects.order_by("date") - i = 0 - while i < 19: - self.assertTrue(logs[i].date <= logs[i + 1].date) - i += 1 - - logs = LogEntry.objects.order_by("-date") - i = 0 - while i < 19: - self.assertTrue(logs[i].date >= logs[i + 1].date) - i += 1 - - # Test searching - logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) - self.assertEqual(logs.count(), 10) - def test_list_validation(self): """Ensure that a list field only accepts lists with valid elements.""" AccessLevelChoices = ( @@ -1699,7 +1381,7 @@ class FieldTest(MongoDBTestCase): post.save() post = BlogPost() - post.info = {'title' : 'dollar_sign', 'details' : {'te$t' : 'test'} } + post.info = {'title': 'dollar_sign', 'details': {'te$t': 'test'}} post.save() post = BlogPost() @@ -1718,7 +1400,7 @@ class FieldTest(MongoDBTestCase): post = BlogPost.objects.filter(info__title__exact='dollar_sign').first() self.assertIn('te$t', post['info']['details']) - + # Confirm handles non strings or non existing keys self.assertEqual( BlogPost.objects.filter(info__details__test__exact=5).count(), 0) @@ -5400,180 +5082,5 @@ class GenericLazyReferenceFieldTest(MongoDBTestCase): check_fields_type(occ) -class ComplexDateTimeFieldTest(MongoDBTestCase): - def test_complexdatetime_storage(self): - """Tests for complex datetime fields - which can handle - microseconds without rounding. - """ - class LogEntry(Document): - date = ComplexDateTimeField() - date_with_dots = ComplexDateTimeField(separator='.') - - LogEntry.drop_collection() - - # Post UTC - microseconds are rounded (down) nearest millisecond and - # dropped - with default datetimefields - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) - log = LogEntry() - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1) - - # Post UTC - microseconds are rounded (down) nearest millisecond - with - # default datetimefields - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1) - - # Pre UTC dates microseconds below 1000 are dropped - with default - # datetimefields - d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1) - - # Pre UTC microseconds above 1000 is wonky - with default datetimefields - # log.date has an invalid microsecond value so I can't construct - # a date to compare. - for i in range(1001, 3113, 33): - d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, i) - log.date = d1 - log.save() - log.reload() - self.assertEqual(log.date, d1) - log1 = LogEntry.objects.get(date=d1) - self.assertEqual(log, log1) - - # Test string padding - microsecond = map(int, [math.pow(10, x) for x in range(6)]) - mm = dd = hh = ii = ss = [1, 10] - - for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): - stored = LogEntry(date=datetime.datetime(*values)).to_mongo()['date'] - self.assertTrue(re.match('^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$', stored) is not None) - - # Test separator - stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()['date_with_dots'] - self.assertTrue(re.match('^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$', stored) is not None) - - def test_complexdatetime_usage(self): - """Tests for complex datetime fields - which can handle - microseconds without rounding. - """ - class LogEntry(Document): - date = ComplexDateTimeField() - - LogEntry.drop_collection() - - d1 = datetime.datetime(1950, 1, 1, 0, 0, 1, 999) - log = LogEntry() - log.date = d1 - log.save() - - log1 = LogEntry.objects.get(date=d1) - self.assertEqual(log, log1) - - # create extra 59 log entries for a total of 60 - for i in range(1951, 2010): - d = datetime.datetime(i, 1, 1, 0, 0, 1, 999) - LogEntry(date=d).save() - - self.assertEqual(LogEntry.objects.count(), 60) - - # Test ordering - logs = LogEntry.objects.order_by("date") - i = 0 - while i < 59: - self.assertTrue(logs[i].date <= logs[i + 1].date) - i += 1 - - logs = LogEntry.objects.order_by("-date") - i = 0 - while i < 59: - self.assertTrue(logs[i].date >= logs[i + 1].date) - i += 1 - - # Test searching - logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) - self.assertEqual(logs.count(), 30) - - logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1)) - self.assertEqual(logs.count(), 30) - - logs = LogEntry.objects.filter( - date__lte=datetime.datetime(2011, 1, 1), - date__gte=datetime.datetime(2000, 1, 1), - ) - self.assertEqual(logs.count(), 10) - - LogEntry.drop_collection() - - # Test microsecond-level ordering/filtering - for microsecond in (99, 999, 9999, 10000): - LogEntry( - date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond) - ).save() - - logs = list(LogEntry.objects.order_by('date')) - for next_idx, log in enumerate(logs[:-1], start=1): - next_log = logs[next_idx] - self.assertTrue(log.date < next_log.date) - - logs = list(LogEntry.objects.order_by('-date')) - for next_idx, log in enumerate(logs[:-1], start=1): - next_log = logs[next_idx] - self.assertTrue(log.date > next_log.date) - - logs = LogEntry.objects.filter( - date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)) - self.assertEqual(logs.count(), 4) - - def test_no_default_value(self): - class Log(Document): - timestamp = ComplexDateTimeField() - - Log.drop_collection() - - log = Log() - self.assertIsNone(log.timestamp) - log.save() - - fetched_log = Log.objects.with_id(log.id) - self.assertIsNone(fetched_log.timestamp) - - def test_default_static_value(self): - NOW = datetime.datetime.utcnow() - class Log(Document): - timestamp = ComplexDateTimeField(default=NOW) - - Log.drop_collection() - - log = Log() - self.assertEqual(log.timestamp, NOW) - log.save() - - fetched_log = Log.objects.with_id(log.id) - self.assertEqual(fetched_log.timestamp, NOW) - - def test_default_callable(self): - NOW = datetime.datetime.utcnow() - - class Log(Document): - timestamp = ComplexDateTimeField(default=datetime.datetime.utcnow) - - Log.drop_collection() - - log = Log() - self.assertGreaterEqual(log.timestamp, NOW) - log.save() - - fetched_log = Log.objects.with_id(log.id) - self.assertGreaterEqual(fetched_log.timestamp, NOW) - - if __name__ == '__main__': unittest.main() diff --git a/tests/fields/test_complex_datetime_field.py b/tests/fields/test_complex_datetime_field.py new file mode 100644 index 00000000..bac534c0 --- /dev/null +++ b/tests/fields/test_complex_datetime_field.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +import datetime +import math +import itertools +import re + +try: + from bson.int64 import Int64 +except ImportError: + Int64 = long + +from mongoengine import * + +from tests.utils import MongoDBTestCase + + +class ComplexDateTimeFieldTest(MongoDBTestCase): + def test_complexdatetime_storage(self): + """Tests for complex datetime fields - which can handle + microseconds without rounding. + """ + class LogEntry(Document): + date = ComplexDateTimeField() + date_with_dots = ComplexDateTimeField(separator='.') + + LogEntry.drop_collection() + + # Post UTC - microseconds are rounded (down) nearest millisecond and + # dropped - with default datetimefields + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) + log = LogEntry() + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1) + + # Post UTC - microseconds are rounded (down) nearest millisecond - with + # default datetimefields + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1) + + # Pre UTC dates microseconds below 1000 are dropped - with default + # datetimefields + d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1) + + # Pre UTC microseconds above 1000 is wonky - with default datetimefields + # log.date has an invalid microsecond value so I can't construct + # a date to compare. + for i in range(1001, 3113, 33): + d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, i) + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1) + log1 = LogEntry.objects.get(date=d1) + self.assertEqual(log, log1) + + # Test string padding + microsecond = map(int, [math.pow(10, x) for x in range(6)]) + mm = dd = hh = ii = ss = [1, 10] + + for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): + stored = LogEntry(date=datetime.datetime(*values)).to_mongo()['date'] + self.assertTrue(re.match('^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$', stored) is not None) + + # Test separator + stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()['date_with_dots'] + self.assertTrue(re.match('^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$', stored) is not None) + + def test_complexdatetime_usage(self): + """Tests for complex datetime fields - which can handle + microseconds without rounding. + """ + class LogEntry(Document): + date = ComplexDateTimeField() + + LogEntry.drop_collection() + + d1 = datetime.datetime(1950, 1, 1, 0, 0, 1, 999) + log = LogEntry() + log.date = d1 + log.save() + + log1 = LogEntry.objects.get(date=d1) + self.assertEqual(log, log1) + + # create extra 59 log entries for a total of 60 + for i in range(1951, 2010): + d = datetime.datetime(i, 1, 1, 0, 0, 1, 999) + LogEntry(date=d).save() + + self.assertEqual(LogEntry.objects.count(), 60) + + # Test ordering + logs = LogEntry.objects.order_by("date") + i = 0 + while i < 59: + self.assertTrue(logs[i].date <= logs[i + 1].date) + i += 1 + + logs = LogEntry.objects.order_by("-date") + i = 0 + while i < 59: + self.assertTrue(logs[i].date >= logs[i + 1].date) + i += 1 + + # Test searching + logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) + self.assertEqual(logs.count(), 30) + + logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1)) + self.assertEqual(logs.count(), 30) + + logs = LogEntry.objects.filter( + date__lte=datetime.datetime(2011, 1, 1), + date__gte=datetime.datetime(2000, 1, 1), + ) + self.assertEqual(logs.count(), 10) + + LogEntry.drop_collection() + + # Test microsecond-level ordering/filtering + for microsecond in (99, 999, 9999, 10000): + LogEntry( + date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond) + ).save() + + logs = list(LogEntry.objects.order_by('date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date < next_log.date) + + logs = list(LogEntry.objects.order_by('-date')) + for next_idx, log in enumerate(logs[:-1], start=1): + next_log = logs[next_idx] + self.assertTrue(log.date > next_log.date) + + logs = LogEntry.objects.filter( + date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)) + self.assertEqual(logs.count(), 4) + + def test_no_default_value(self): + class Log(Document): + timestamp = ComplexDateTimeField() + + Log.drop_collection() + + log = Log() + self.assertIsNone(log.timestamp) + log.save() + + fetched_log = Log.objects.with_id(log.id) + self.assertIsNone(fetched_log.timestamp) + + def test_default_static_value(self): + NOW = datetime.datetime.utcnow() + class Log(Document): + timestamp = ComplexDateTimeField(default=NOW) + + Log.drop_collection() + + log = Log() + self.assertEqual(log.timestamp, NOW) + log.save() + + fetched_log = Log.objects.with_id(log.id) + self.assertEqual(fetched_log.timestamp, NOW) + + def test_default_callable(self): + NOW = datetime.datetime.utcnow() + + class Log(Document): + timestamp = ComplexDateTimeField(default=datetime.datetime.utcnow) + + Log.drop_collection() + + log = Log() + self.assertGreaterEqual(log.timestamp, NOW) + log.save() + + fetched_log = Log.objects.with_id(log.id) + self.assertGreaterEqual(fetched_log.timestamp, NOW) diff --git a/tests/fields/test_date_field.py b/tests/fields/test_date_field.py new file mode 100644 index 00000000..b5aed5c1 --- /dev/null +++ b/tests/fields/test_date_field.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +import datetime +import unittest +import uuid +import math +import itertools +import re +import sys + +from nose.plugins.skip import SkipTest +import six + +try: + import dateutil +except ImportError: + dateutil = None + +from decimal import Decimal + +from bson import Binary, DBRef, ObjectId, SON +try: + from bson.int64 import Int64 +except ImportError: + Int64 = long + +from mongoengine import * +from mongoengine.connection import get_db +from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, + _document_registry, LazyReference) + +from tests.utils import MongoDBTestCase + + +class TestDateField(MongoDBTestCase): + def test_date_from_empty_string(self): + """ + Ensure an exception is raised when trying to + cast an empty string to datetime. + """ + class MyDoc(Document): + dt = DateField() + + md = MyDoc(dt='') + self.assertRaises(ValidationError, md.save) + + def test_date_from_whitespace_string(self): + """ + Ensure an exception is raised when trying to + cast a whitespace-only string to datetime. + """ + class MyDoc(Document): + dt = DateField() + + md = MyDoc(dt=' ') + self.assertRaises(ValidationError, md.save) + + def test_default_values_today(self): + """Ensure that default field values are used when creating + a document. + """ + class Person(Document): + day = DateField(default=datetime.date.today) + + person = Person() + person.validate() + self.assertEqual(person.day, person.day) + self.assertEqual(person.day, datetime.date.today()) + self.assertEqual(person._data['day'], person.day) + + def test_date(self): + """Tests showing pymongo date fields + + See: http://api.mongodb.org/python/current/api/bson/son.html#dt + """ + class LogEntry(Document): + date = DateField() + + LogEntry.drop_collection() + + # Test can save dates + log = LogEntry() + log.date = datetime.date.today() + log.save() + log.reload() + self.assertEqual(log.date, datetime.date.today()) + + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) + d2 = datetime.datetime(1970, 1, 1, 0, 0, 1) + log = LogEntry() + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1.date()) + self.assertEqual(log.date, d2.date()) + + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) + d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000) + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1.date()) + self.assertEqual(log.date, d2.date()) + + if not six.PY3: + # Pre UTC dates microseconds below 1000 are dropped + # This does not seem to be true in PY3 + d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) + d2 = datetime.datetime(1969, 12, 31, 23, 59, 59) + log.date = d1 + log.save() + log.reload() + self.assertEqual(log.date, d1.date()) + self.assertEqual(log.date, d2.date()) + + def test_regular_usage(self): + """Tests for regular datetime fields""" + class LogEntry(Document): + date = DateField() + + LogEntry.drop_collection() + + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1) + log = LogEntry() + log.date = d1 + log.validate() + log.save() + + for query in (d1, d1.isoformat(' ')): + log1 = LogEntry.objects.get(date=query) + self.assertEqual(log, log1) + + if dateutil: + log1 = LogEntry.objects.get(date=d1.isoformat('T')) + self.assertEqual(log, log1) + + # create additional 19 log entries for a total of 20 + for i in range(1971, 1990): + d = datetime.datetime(i, 1, 1, 0, 0, 1) + LogEntry(date=d).save() + + self.assertEqual(LogEntry.objects.count(), 20) + + # Test ordering + logs = LogEntry.objects.order_by("date") + i = 0 + while i < 19: + self.assertTrue(logs[i].date <= logs[i + 1].date) + i += 1 + + logs = LogEntry.objects.order_by("-date") + i = 0 + while i < 19: + self.assertTrue(logs[i].date >= logs[i + 1].date) + i += 1 + + # Test searching + logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) + self.assertEqual(logs.count(), 10) + + def test_validation(self): + """Ensure that invalid values cannot be assigned to datetime + fields. + """ + class LogEntry(Document): + time = DateField() + + log = LogEntry() + log.time = datetime.datetime.now() + log.validate() + + log.time = datetime.date.today() + log.validate() + + log.time = datetime.datetime.now().isoformat(' ') + log.validate() + + if dateutil: + log.time = datetime.datetime.now().isoformat('T') + log.validate() + + log.time = -1 + self.assertRaises(ValidationError, log.validate) + log.time = 'ABC' + self.assertRaises(ValidationError, log.validate) diff --git a/tests/fields/test_datetime_field.py b/tests/fields/test_datetime_field.py new file mode 100644 index 00000000..24d1c777 --- /dev/null +++ b/tests/fields/test_datetime_field.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +import datetime +import six + +try: + import dateutil +except ImportError: + dateutil = None + +try: + from bson.int64 import Int64 +except ImportError: + Int64 = long + +from mongoengine import * +from mongoengine import connection + +from tests.utils import MongoDBTestCase + + +class TestDateTimeField(MongoDBTestCase): + def test_datetime_from_empty_string(self): + """ + Ensure an exception is raised when trying to + cast an empty string to datetime. + """ + class MyDoc(Document): + dt = DateTimeField() + + md = MyDoc(dt='') + self.assertRaises(ValidationError, md.save) + + def test_datetime_from_whitespace_string(self): + """ + Ensure an exception is raised when trying to + cast a whitespace-only string to datetime. + """ + class MyDoc(Document): + dt = DateTimeField() + + md = MyDoc(dt=' ') + self.assertRaises(ValidationError, md.save) + + def test_default_value_utcnow(self): + """Ensure that default field values are used when creating + a document. + """ + class Person(Document): + created = DateTimeField(default=datetime.datetime.utcnow) + + utcnow = datetime.datetime.utcnow() + person = Person() + person.validate() + person_created_t0 = person.created + self.assertLess(person.created - utcnow, datetime.timedelta(seconds=1)) + self.assertEqual(person_created_t0, person.created) # make sure it does not change + self.assertEqual(person._data['created'], person.created) + + def test_handling_microseconds(self): + """Tests showing pymongo datetime fields handling of microseconds. + Microseconds are rounded to the nearest millisecond and pre UTC + handling is wonky. + + See: http://api.mongodb.org/python/current/api/bson/son.html#dt + """ + class LogEntry(Document): + date = DateTimeField() + + LogEntry.drop_collection() + + # Test can save dates + log = LogEntry() + log.date = datetime.date.today() + log.save() + log.reload() + self.assertEqual(log.date.date(), datetime.date.today()) + + # Post UTC - microseconds are rounded (down) nearest millisecond and + # dropped + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) + d2 = datetime.datetime(1970, 1, 1, 0, 0, 1) + log = LogEntry() + log.date = d1 + log.save() + log.reload() + self.assertNotEqual(log.date, d1) + self.assertEqual(log.date, d2) + + # Post UTC - microseconds are rounded (down) nearest millisecond + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) + d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000) + log.date = d1 + log.save() + log.reload() + self.assertNotEqual(log.date, d1) + self.assertEqual(log.date, d2) + + if not six.PY3: + # Pre UTC dates microseconds below 1000 are dropped + # This does not seem to be true in PY3 + d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999) + d2 = datetime.datetime(1969, 12, 31, 23, 59, 59) + log.date = d1 + log.save() + log.reload() + self.assertNotEqual(log.date, d1) + self.assertEqual(log.date, d2) + + def test_regular_usage(self): + """Tests for regular datetime fields""" + class LogEntry(Document): + date = DateTimeField() + + LogEntry.drop_collection() + + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1) + log = LogEntry() + log.date = d1 + log.validate() + log.save() + + for query in (d1, d1.isoformat(' ')): + log1 = LogEntry.objects.get(date=query) + self.assertEqual(log, log1) + + if dateutil: + log1 = LogEntry.objects.get(date=d1.isoformat('T')) + self.assertEqual(log, log1) + + # create additional 19 log entries for a total of 20 + for i in range(1971, 1990): + d = datetime.datetime(i, 1, 1, 0, 0, 1) + LogEntry(date=d).save() + + self.assertEqual(LogEntry.objects.count(), 20) + + # Test ordering + logs = LogEntry.objects.order_by("date") + i = 0 + while i < 19: + self.assertTrue(logs[i].date <= logs[i + 1].date) + i += 1 + + logs = LogEntry.objects.order_by("-date") + i = 0 + while i < 19: + self.assertTrue(logs[i].date >= logs[i + 1].date) + i += 1 + + # Test searching + logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) + self.assertEqual(logs.count(), 10) + + logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1)) + self.assertEqual(logs.count(), 10) + + logs = LogEntry.objects.filter( + date__lte=datetime.datetime(1980, 1, 1), + date__gte=datetime.datetime(1975, 1, 1), + ) + self.assertEqual(logs.count(), 5) + + def test_datetime_validation(self): + """Ensure that invalid values cannot be assigned to datetime + fields. + """ + class LogEntry(Document): + time = DateTimeField() + + log = LogEntry() + log.time = datetime.datetime.now() + log.validate() + + log.time = datetime.date.today() + log.validate() + + log.time = datetime.datetime.now().isoformat(' ') + log.validate() + + if dateutil: + log.time = datetime.datetime.now().isoformat('T') + log.validate() + + log.time = -1 + self.assertRaises(ValidationError, log.validate) + log.time = 'ABC' + self.assertRaises(ValidationError, log.validate) + + +class TestDateTimeTzAware(MongoDBTestCase): + def test_datetime_tz_aware_mark_as_changed(self): + # Reset the connections + connection._connection_settings = {} + connection._connections = {} + connection._dbs = {} + + connect(db='mongoenginetest', tz_aware=True) + + class LogEntry(Document): + time = DateTimeField() + + LogEntry.drop_collection() + + LogEntry(time=datetime.datetime(2013, 1, 1, 0, 0, 0)).save() + + log = LogEntry.objects.first() + log.time = datetime.datetime(2013, 1, 1, 0, 0, 0) + self.assertEqual(['time'], log._changed_fields) From 57a38282a9a583983c6625d3e81731f9b461786f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Mon, 18 Feb 2019 22:03:03 +0100 Subject: [PATCH 3/3] Add DeprecationWarning for EmbeddedDocument.save & .reload - those will be removed soon --- docs/changelog.rst | 1 + mongoengine/document.py | 6 ++++++ tests/document/instance.py | 24 +++++++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 25e1a585..dbd328d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ Development - Fix .only() working improperly after using .count() of the same instance of QuerySet - POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (_cls, _id) when using `QuerySet.as_pymongo` #1976 - Fix InvalidStringData error when using modify on a BinaryField #1127 +- DEPRECATION: `EmbeddedDocument.save` & `.reload` are marked as deprecated and will be removed in a next version of mongoengine #1552 ================= Changes in 0.16.3 diff --git a/mongoengine/document.py b/mongoengine/document.py index 7a491b7d..57364ae6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -90,9 +90,15 @@ class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)): return data def save(self, *args, **kwargs): + warnings.warn("EmbeddedDocument.save is deprecated and will be removed in a next version of mongoengine." + "Use the parent document's .save() or ._instance.save()", + DeprecationWarning, stacklevel=2) self._instance.save(*args, **kwargs) def reload(self, *args, **kwargs): + warnings.warn("EmbeddedDocument.reload is deprecated and will be removed in a next version of mongoengine." + "Use the parent document's .reload() or ._instance.reload()", + DeprecationWarning, stacklevel=2) self._instance.reload(*args, **kwargs) diff --git a/tests/document/instance.py b/tests/document/instance.py index 5319ace4..39e47524 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -7,6 +7,8 @@ import uuid import weakref from datetime import datetime + +import warnings from bson import DBRef, ObjectId from pymongo.errors import DuplicateKeyError @@ -1482,7 +1484,7 @@ class InstanceTest(MongoDBTestCase): Message.drop_collection() # All objects share the same id, but each in a different collection - user = User(id=1, name='user-name')#.save() + user = User(id=1, name='user-name') # .save() message = Message(id=1, author=user).save() message.author.name = 'tutu' @@ -2000,7 +2002,6 @@ class InstanceTest(MongoDBTestCase): child_record.delete() self.assertEqual(Record.objects(name='parent').get().children, []) - def test_reverse_delete_rule_with_custom_id_field(self): """Ensure that a referenced document with custom primary key is also deleted upon deletion. @@ -3086,6 +3087,24 @@ class InstanceTest(MongoDBTestCase): "UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) + def test_embedded_document_save_reload_warning(self): + """Relates to #1570""" + class Embedded(EmbeddedDocument): + pass + + class Doc(Document): + emb = EmbeddedDocumentField(Embedded) + + doc = Doc(emb=Embedded()).save() + doc.emb.save() # Make sure its still working + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + with self.assertRaises(DeprecationWarning): + doc.emb.save() + + with self.assertRaises(DeprecationWarning): + doc.emb.reload() + def test_embedded_document_equality(self): class Test(Document): field = StringField(required=True) @@ -3381,7 +3400,6 @@ class InstanceTest(MongoDBTestCase): class User(Document): company = ReferenceField(Company) - # Ensure index creation exception aren't swallowed (#1688) with self.assertRaises(DuplicateKeyError): User.objects().select_related()