From c9d496e9a0c55acbe1429a9b678cd6874ab7091d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Mon, 12 Dec 2016 23:08:11 -0500 Subject: [PATCH 001/268] Fix connecting to MongoReplicaSetClient (#1436) --- mongoengine/connection.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index bb353cff..a532c896 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -66,9 +66,9 @@ def register_connection(alias, name=None, host=None, port=None, 'authentication_mechanism': authentication_mechanism } - # Handle uri style connections conn_host = conn_settings['host'] - # host can be a list or a string, so if string, force to a list + + # Host can be a list or a string, so if string, force to a list. if isinstance(conn_host, six.string_types): conn_host = [conn_host] @@ -170,23 +170,22 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): else: connection_class = MongoClient - # Handle replica set connections - if 'replicaSet' in conn_settings: + # For replica set connections with PyMongo 2.x, use + # MongoReplicaSetClient. + # TODO remove this once we stop supporting PyMongo 2.x. + if 'replicaSet' in conn_settings and not IS_PYMONGO_3: + connection_class = MongoReplicaSetClient + conn_settings['hosts_or_uri'] = conn_settings.pop('host', None) + + # hosts_or_uri has to be a string, so if 'host' was provided + # as a list, join its parts and separate them by ',' + if isinstance(conn_settings['hosts_or_uri'], list): + conn_settings['hosts_or_uri'] = ','.join( + conn_settings['hosts_or_uri']) # Discard port since it can't be used on MongoReplicaSetClient conn_settings.pop('port', None) - # Discard replicaSet if it's not a string - if not isinstance(conn_settings['replicaSet'], six.string_types): - del conn_settings['replicaSet'] - - # For replica set connections with PyMongo 2.x, use - # MongoReplicaSetClient. - # TODO remove this once we stop supporting PyMongo 2.x. - if not IS_PYMONGO_3: - connection_class = MongoReplicaSetClient - conn_settings['hosts_or_uri'] = conn_settings.pop('host', None) - # Iterate over all of the connection settings and if a connection with # the same parameters is already established, use it instead of creating # a new one. From a4d0da0085345ba9958b811fc96b999a6260653d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 12 Dec 2016 23:08:57 -0500 Subject: [PATCH 002/268] update the changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e04c48fb..57fba3ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Development =========== - (Fill this out as you fix issues and develop you features). +- Fixed connecting to a replica set with PyMongo 2.x #1436 Changes in 0.11.0 ================= From 65914fb2b26e219c26ee4ce459a108fa644cc474 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 12 Dec 2016 23:24:19 -0500 Subject: [PATCH 003/268] fix the way MongoDB URI w/ ?replicaset is passed --- mongoengine/connection.py | 2 +- tests/test_connection.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index a532c896..c7c16ca3 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -96,7 +96,7 @@ def register_connection(alias, name=None, host=None, port=None, uri_options = uri_dict['options'] if 'replicaset' in uri_options: - conn_settings['replicaSet'] = True + conn_settings['replicaSet'] = uri_options['replicaset'] if 'authsource' in uri_options: conn_settings['authentication_source'] = uri_options['authsource'] if 'authmechanism' in uri_options: diff --git a/tests/test_connection.py b/tests/test_connection.py index d8f1a79e..34168b85 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -200,6 +200,13 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'test') + def test_connect_uri_with_replicaset(self): + """Ensure connect() works when specifying a replicaSet.""" + c = connect(host='mongodb://localhost/test?replicaSet=local-rs') + db = get_db() + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'test') + def test_uri_without_credentials_doesnt_override_conn_settings(self): """Ensure connect() uses the username & password params if the URI doesn't explicitly specify them. From 76524b7498a87a9b3e40f89c4efd26376c13e92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malthe=20J=C3=B8rgensen?= Date: Tue, 13 Dec 2016 05:27:25 +0100 Subject: [PATCH 004/268] Raise TypeError when `__in`-operator used with a Document (#1237) --- mongoengine/common.py | 5 +++- mongoengine/queryset/transform.py | 17 +++++++++-- setup.cfg | 2 +- tests/queryset/queryset.py | 50 +++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/mongoengine/common.py b/mongoengine/common.py index 3e63e98e..bde7e78c 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -34,7 +34,10 @@ def _import_class(cls_name): queryset_classes = ('OperationError',) deref_classes = ('DeReference',) - if cls_name in doc_classes: + if cls_name == 'BaseDocument': + from mongoengine.base import document as module + import_classes = ['BaseDocument'] + elif cls_name in doc_classes: from mongoengine import document as module import_classes = doc_classes elif cls_name in field_classes: diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index af59917c..61d43490 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -101,8 +101,21 @@ def query(_doc_cls=None, **kwargs): value = value['_id'] elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict): - # 'in', 'nin' and 'all' require a list of values - value = [field.prepare_query_value(op, v) for v in value] + # Raise an error if the in/nin/all/near param is not iterable. We need a + # special check for BaseDocument, because - although it's iterable - using + # it as such in the context of this method is most definitely a mistake. + BaseDocument = _import_class('BaseDocument') + if isinstance(value, BaseDocument): + raise TypeError("When using the `in`, `nin`, `all`, or " + "`near`-operators you can\'t use a " + "`Document`, you must wrap your object " + "in a list (object -> [object]).") + elif not hasattr(value, '__iter__'): + raise TypeError("The `in`, `nin`, `all`, or " + "`near`-operators must be applied to an " + "iterable (e.g. a list).") + else: + value = [field.prepare_query_value(op, v) for v in value] # If we're querying a GenericReferenceField, we need to alter the # key depending on the value: diff --git a/setup.cfg b/setup.cfg index 1887c476..eabe3271 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,5 +7,5 @@ cover-package=mongoengine [flake8] ignore=E501,F401,F403,F405,I201 exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests -max-complexity=45 +max-complexity=47 application-import-names=mongoengine,tests diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index e4c71de7..28b831cd 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4963,6 +4963,56 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(i, 249) self.assertEqual(j, 249) + def test_in_operator_on_non_iterable(self): + """Ensure that using the `__in` operator on a non-iterable raises an + error. + """ + class User(Document): + name = StringField() + + class BlogPost(Document): + content = StringField() + authors = ListField(ReferenceField(User)) + + User.drop_collection() + BlogPost.drop_collection() + + author = User(name='Test User') + author.save() + post = BlogPost(content='Had a good coffee today...', authors=[author]) + post.save() + + blog_posts = BlogPost.objects(authors__in=[author]) + self.assertEqual(list(blog_posts), [post]) + + # Using the `__in`-operator with a non-iterable should raise a TypeError + self.assertRaises(TypeError, BlogPost.objects(authors__in=author.id).count) + + def test_in_operator_on_document(self): + """Ensure that using the `__in` operator on a `Document` raises an + error. + """ + class User(Document): + name = StringField() + + class BlogPost(Document): + content = StringField() + authors = ListField(ReferenceField(User)) + + User.drop_collection() + BlogPost.drop_collection() + + author = User(name='Test User') + author.save() + post = BlogPost(content='Had a good coffee today...', authors=[author]) + post.save() + + blog_posts = BlogPost.objects(authors__in=[author]) + self.assertEqual(list(blog_posts), [post]) + + # Using the `__in`-operator with a `Document` should raise a TypeError + self.assertRaises(TypeError, BlogPost.objects(authors__in=author).count) + if __name__ == '__main__': unittest.main() From 8e884fd3ea85031bd5fa2a855a6eed0becd0730f Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 12 Dec 2016 23:30:38 -0500 Subject: [PATCH 005/268] make the __in=non_iterable_or_doc tests more concise --- tests/queryset/queryset.py | 37 ++++++++----------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 28b831cd..11ce0a27 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4977,40 +4977,19 @@ class QuerySetTest(unittest.TestCase): User.drop_collection() BlogPost.drop_collection() - author = User(name='Test User') - author.save() - post = BlogPost(content='Had a good coffee today...', authors=[author]) - post.save() + author = User.objects.create(name='Test User') + post = BlogPost.objects.create(content='Had a good coffee today...', + authors=[author]) + # Make sure using `__in` with a list works blog_posts = BlogPost.objects(authors__in=[author]) self.assertEqual(list(blog_posts), [post]) - # Using the `__in`-operator with a non-iterable should raise a TypeError - self.assertRaises(TypeError, BlogPost.objects(authors__in=author.id).count) + # Using `__in` with a non-iterable should raise a TypeError + self.assertRaises(TypeError, BlogPost.objects(authors__in=author.pk).count) - def test_in_operator_on_document(self): - """Ensure that using the `__in` operator on a `Document` raises an - error. - """ - class User(Document): - name = StringField() - - class BlogPost(Document): - content = StringField() - authors = ListField(ReferenceField(User)) - - User.drop_collection() - BlogPost.drop_collection() - - author = User(name='Test User') - author.save() - post = BlogPost(content='Had a good coffee today...', authors=[author]) - post.save() - - blog_posts = BlogPost.objects(authors__in=[author]) - self.assertEqual(list(blog_posts), [post]) - - # Using the `__in`-operator with a `Document` should raise a TypeError + # Using `__in` with a `Document` (which is seemingly iterable but not + # in a way we'd expect) should raise a TypeError, too self.assertRaises(TypeError, BlogPost.objects(authors__in=author).count) From b1d8aca46a3abd5999655cf7d90ede82a2e89c15 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 12 Dec 2016 23:33:49 -0500 Subject: [PATCH 006/268] update the changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 57fba3ed..af3437c1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Development =========== - (Fill this out as you fix issues and develop you features). - Fixed connecting to a replica set with PyMongo 2.x #1436 +- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237 Changes in 0.11.0 ================= From 553f496d847ce51018e8df591c1fd4052fbbcbed Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 13 Dec 2016 00:42:03 -0500 Subject: [PATCH 007/268] fix tests --- tests/test_connection.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index 34168b85..9143571f 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -202,10 +202,16 @@ class ConnectionTest(unittest.TestCase): def test_connect_uri_with_replicaset(self): """Ensure connect() works when specifying a replicaSet.""" - c = connect(host='mongodb://localhost/test?replicaSet=local-rs') - db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) - self.assertEqual(db.name, 'test') + if IS_PYMONGO_3: + c = connect(host='mongodb://localhost/test?replicaSet=local-rs') + db = get_db() + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'test') + else: + # PyMongo < v3.x raises an exception: + # "localhost:27017 is not a member of replica set local-rs" + with self.assertRaises(MongoEngineConnectionError): + c = connect(host='mongodb://localhost/test?replicaSet=local-rs') def test_uri_without_credentials_doesnt_override_conn_settings(self): """Ensure connect() uses the username & password params if the URI From 57c2e867d8c729d95eafa1f66a5b4767855af2e3 Mon Sep 17 00:00:00 2001 From: Manuel Jeckelmann Date: Mon, 19 Dec 2016 23:54:43 +0100 Subject: [PATCH 008/268] Remove py26 from contributing docs (#1439) Python 2.6 is not supported anymore with version 0.11.0 --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index aeba41f7..06bf2f61 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -20,7 +20,7 @@ post to the `user group ` Supported Interpreters ---------------------- -MongoEngine supports CPython 2.6 and newer. Language +MongoEngine supports CPython 2.7 and newer. Language features not supported by all interpreters can not be used. Please also ensure that your code is properly converted by `2to3 `_ for Python 3 support. From c84f703f926c2e37095c0703d884b2dc5a06b7e6 Mon Sep 17 00:00:00 2001 From: George Karakostas Date: Thu, 22 Dec 2016 20:06:55 +0200 Subject: [PATCH 009/268] Update documentation to include a Q import (#1441) --- docs/guide/querying.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 913de5d6..980947df 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -479,6 +479,8 @@ operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the first positional argument to :attr:`Document.objects` when you filter it by calling it with keyword arguments:: + from mongoengine.queryset.visitor import Q + # Get published posts Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now())) From b8454c7f5b685852db136c38c603316bcbe90345 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Thu, 22 Dec 2016 12:11:44 -0600 Subject: [PATCH 010/268] Fixed ListField deletion bug (#1435) --- mongoengine/base/datastructures.py | 5 +- tests/fields/fields.py | 244 +++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 5e90a2e5..8a7681e5 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -138,10 +138,7 @@ class BaseList(list): return super(BaseList, self).__setitem__(key, value) def __delitem__(self, key, *args, **kwargs): - if isinstance(key, slice): - self._mark_as_changed() - else: - self._mark_as_changed(key) + self._mark_as_changed() return super(BaseList, self).__delitem__(key) def __setslice__(self, *args, **kwargs): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 678786fa..d47b2ab6 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1042,6 +1042,7 @@ class FieldTest(unittest.TestCase): self.assertEqual( BlogPost.objects.filter(info__100__test__exact='test').count(), 0) + # test queries by list post = BlogPost() post.info = ['1', '2'] post.save() @@ -1053,8 +1054,251 @@ class FieldTest(unittest.TestCase): post.info *= 2 post.save() self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4', '1', '2', '3', '4']).count(), 1) + BlogPost.drop_collection() + + def test_list_field_manipulative_operators(self): + """Ensure that ListField works with standard list operators that manipulate the list. + """ + class BlogPost(Document): + ref = StringField() + info = ListField(StringField()) + + BlogPost.drop_collection() + + post = BlogPost() + post.ref = "1234" + post.info = ['0', '1', '2', '3', '4', '5'] + post.save() + + def reset_post(): + post.info = ['0', '1', '2', '3', '4', '5'] + post.save() + + # '__add__(listB)' + # listA+listB + # operator.add(listA, listB) + reset_post() + temp = ['a', 'b'] + post.info = post.info + temp + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'a', 'b']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'a', 'b']) + + # '__delitem__(index)' + # aka 'del list[index]' + # aka 'operator.delitem(list, index)' + reset_post() + del post.info[2] # del from middle ('2') + self.assertEqual(post.info, ['0', '1', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '3', '4', '5']) + + # '__delitem__(slice(i, j))' + # aka 'del list[i:j]' + # aka 'operator.delitem(list, slice(i,j))' + reset_post() + del post.info[1:3] # removes '1', '2' + self.assertEqual(post.info, ['0', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '3', '4', '5']) + + # '__iadd__' + # aka 'list += list' + reset_post() + temp = ['a', 'b'] + post.info += temp + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'a', 'b']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'a', 'b']) + + # '__imul__' + # aka 'list *= number' + reset_post() + post.info *= 2 + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + + # '__mul__' + # aka 'listA*listB' + reset_post() + post.info = post.info * 2 + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + + # '__rmul__' + # aka 'listB*listA' + reset_post() + post.info = 2 * post.info + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', '0', '1', '2', '3', '4', '5']) + + # '__setitem__(index, value)' + # aka 'list[index]=value' + # aka 'setitem(list, value)' + reset_post() + post.info[4] = 'a' + self.assertEqual(post.info, ['0', '1', '2', '3', 'a', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', 'a', '5']) + + # '__setitem__(slice(i, j), listB)' + # aka 'listA[i:j] = listB' + # aka 'setitem(listA, slice(i, j), listB)' + reset_post() + post.info[1:3] = ['h', 'e', 'l', 'l', 'o'] + self.assertEqual(post.info, ['0', 'h', 'e', 'l', 'l', 'o', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', 'h', 'e', 'l', 'l', 'o', '3', '4', '5']) + + # 'append' + reset_post() + post.info.append('h') + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'h']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'h']) + + # 'extend' + reset_post() + post.info.extend(['h', 'e', 'l', 'l', 'o']) + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'h', 'e', 'l', 'l', 'o']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', '4', '5', 'h', 'e', 'l', 'l', 'o']) + # 'insert' + + # 'pop' + reset_post() + x = post.info.pop(2) + y = post.info.pop() + self.assertEqual(post.info, ['0', '1', '3', '4']) + self.assertEqual(x, '2') + self.assertEqual(y, '5') + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '3', '4']) + + # 'remove' + reset_post() + post.info.remove('2') + self.assertEqual(post.info, ['0', '1', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '3', '4', '5']) + + # 'reverse' + reset_post() + post.info.reverse() + self.assertEqual(post.info, ['5', '4', '3', '2', '1', '0']) + post.save() + post.reload() + self.assertEqual(post.info, ['5', '4', '3', '2', '1', '0']) + + # 'sort': though this operator method does manipulate the list, it is tested in + # the 'test_list_field_lexicograpic_operators' function + BlogPost.drop_collection() + + def test_list_field_invalid_operators(self): + class BlogPost(Document): + ref = StringField() + info = ListField(StringField()) + post = BlogPost() + post.ref = "1234" + post.info = ['0', '1', '2', '3', '4', '5'] + # '__hash__' + # aka 'hash(list)' + # # assert TypeError + self.assertRaises(TypeError, lambda: hash(post.info)) + + def test_list_field_lexicographic_operators(self): + """Ensure that ListField works with standard list operators that do lexigraphic ordering. + """ + class BlogPost(Document): + ref = StringField() + text_info = ListField(StringField()) + oid_info = ListField(ObjectIdField()) + bool_info = ListField(BooleanField()) + BlogPost.drop_collection() + + blogSmall = BlogPost(ref="small") + blogSmall.text_info = ["a", "a", "a"] + blogSmall.bool_info = [False, False] + blogSmall.save() + blogSmall.reload() + + blogLargeA = BlogPost(ref="big") + blogLargeA.text_info = ["a", "z", "j"] + blogLargeA.bool_info = [False, True] + blogLargeA.save() + blogLargeA.reload() + + blogLargeB = BlogPost(ref="big2") + blogLargeB.text_info = ["a", "z", "j"] + blogLargeB.oid_info = [ + "54495ad94c934721ede76f90", + "54495ad94c934721ede76d23", + "54495ad94c934721ede76d00" + ] + blogLargeB.bool_info = [False, True] + blogLargeB.save() + blogLargeB.reload() + # '__eq__' aka '==' + self.assertEqual(blogLargeA.text_info, blogLargeB.text_info) + self.assertEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__ge__' aka '>=' + self.assertGreaterEqual(blogLargeA.text_info, blogSmall.text_info) + self.assertGreaterEqual(blogLargeA.text_info, blogLargeB.text_info) + self.assertGreaterEqual(blogLargeA.bool_info, blogSmall.bool_info) + self.assertGreaterEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__gt__' aka '>' + self.assertGreaterEqual(blogLargeA.text_info, blogSmall.text_info) + self.assertGreaterEqual(blogLargeA.bool_info, blogSmall.bool_info) + # '__le__' aka '<=' + self.assertLessEqual(blogSmall.text_info, blogLargeB.text_info) + self.assertLessEqual(blogLargeA.text_info, blogLargeB.text_info) + self.assertLessEqual(blogSmall.bool_info, blogLargeB.bool_info) + self.assertLessEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__lt__' aka '<' + self.assertLess(blogSmall.text_info, blogLargeB.text_info) + self.assertLess(blogSmall.bool_info, blogLargeB.bool_info) + # '__ne__' aka '!=' + self.assertNotEqual(blogSmall.text_info, blogLargeB.text_info) + self.assertNotEqual(blogSmall.bool_info, blogLargeB.bool_info) + # 'sort' + blogLargeB.bool_info = [True, False, True, False] + blogLargeB.text_info.sort() + blogLargeB.oid_info.sort() + blogLargeB.bool_info.sort() + sorted_target_list = [ + ObjectId("54495ad94c934721ede76d00"), + ObjectId("54495ad94c934721ede76d23"), + ObjectId("54495ad94c934721ede76f90") + ] + self.assertEqual(blogLargeB.text_info, ["a", "j", "z"]) + self.assertEqual(blogLargeB.oid_info, sorted_target_list) + self.assertEqual(blogLargeB.bool_info, [False, False, True, True]) + blogLargeB.save() + blogLargeB.reload() + self.assertEqual(blogLargeB.text_info, ["a", "j", "z"]) + self.assertEqual(blogLargeB.oid_info, sorted_target_list) + self.assertEqual(blogLargeB.bool_info, [False, False, True, True]) + + BlogPost.drop_collection() + def test_list_assignment(self): """Ensure that list field element assignment and slicing work """ From 96d20756ca1ce29608362aa753236a07a670d55d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Thu, 22 Dec 2016 13:13:19 -0500 Subject: [PATCH 011/268] remove redundant whitespace --- tests/fields/fields.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index d47b2ab6..c6089c8b 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1057,7 +1057,6 @@ class FieldTest(unittest.TestCase): BlogPost.drop_collection() - def test_list_field_manipulative_operators(self): """Ensure that ListField works with standard list operators that manipulate the list. """ @@ -1078,7 +1077,7 @@ class FieldTest(unittest.TestCase): # '__add__(listB)' # listA+listB - # operator.add(listA, listB) + # operator.add(listA, listB) reset_post() temp = ['a', 'b'] post.info = post.info + temp @@ -1298,7 +1297,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(blogLargeB.bool_info, [False, False, True, True]) BlogPost.drop_collection() - + def test_list_assignment(self): """Ensure that list field element assignment and slicing work """ @@ -1346,7 +1345,6 @@ class FieldTest(unittest.TestCase): post.reload() self.assertEqual(post.info, [1, 2, 3, 4, 'n5']) - def test_list_field_passed_in_value(self): class Foo(Document): bars = ListField(ReferenceField("Bar")) From 9f4b04ea0f4d4a6d15a3f59f674d11f073ebe2a1 Mon Sep 17 00:00:00 2001 From: Manuel Jeckelmann Date: Thu, 22 Dec 2016 19:19:18 +0100 Subject: [PATCH 012/268] Fix querying an embedded document field by an invalid value (#1440) --- mongoengine/base/document.py | 6 ++++-- mongoengine/fields.py | 8 ++++++-- tests/document/instance.py | 4 ++++ tests/queryset/queryset.py | 18 ++++++++++++++++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 03dc7562..03536d6f 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -16,8 +16,7 @@ from mongoengine.base.datastructures import (BaseDict, BaseList, SemiStrictDict, StrictDict) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class -from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, - LookUpError, OperationError, ValidationError) +from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, LookUpError, OperationError, ValidationError) __all__ = ('BaseDocument',) @@ -675,6 +674,9 @@ class BaseDocument(object): if not only_fields: only_fields = [] + if son and not isinstance(son, dict): + raise ValueError("The source SON object needs to be of type 'dict'") + # Get the class name from the document, falling back to the given # class if unavailable class_name = son.get('_cls', cls._class_name) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index d812a762..3991ef5c 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -28,7 +28,7 @@ from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField, ObjectIdField, get_document) from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument -from mongoengine.errors import DoesNotExist, ValidationError +from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError from mongoengine.python_support import StringIO from mongoengine.queryset import DO_NOTHING, QuerySet @@ -566,7 +566,11 @@ class EmbeddedDocumentField(BaseField): def prepare_query_value(self, op, value): if value is not None and not isinstance(value, self.document_type): - value = self.document_type._from_son(value) + try: + value = self.document_type._from_son(value) + except ValueError: + raise InvalidQueryError("Querying the embedded document '%s' failed, due to an invalid query value" % + (self.document_type._class_name,)) super(EmbeddedDocumentField, self).prepare_query_value(op, value) return self.to_mongo(value) diff --git a/tests/document/instance.py b/tests/document/instance.py index b92bafa9..d961f034 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1860,6 +1860,10 @@ class InstanceTest(unittest.TestCase): 'occurs': {"hello": None} }) + # Tests for issue #1438: https://github.com/MongoEngine/mongoengine/issues/1438 + with self.assertRaises(ValueError): + Word._from_son('this is not a valid SON dict') + def test_reverse_delete_rule_cascade_and_nullify(self): """Ensure that a referenced document is also deleted upon deletion. """ diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 11ce0a27..a15807a5 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1266,7 +1266,7 @@ class QuerySetTest(unittest.TestCase): def test_find_embedded(self): """Ensure that an embedded document is properly returned from - a query. + different manners of querying. """ class User(EmbeddedDocument): name = StringField() @@ -1277,8 +1277,9 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + user = User(name='Test User') BlogPost.objects.create( - author=User(name='Test User'), + author=user, content='Had a good coffee today...' ) @@ -1286,6 +1287,19 @@ class QuerySetTest(unittest.TestCase): self.assertTrue(isinstance(result.author, User)) self.assertEqual(result.author.name, 'Test User') + result = BlogPost.objects.get(author__name=user.name) + self.assertTrue(isinstance(result.author, User)) + self.assertEqual(result.author.name, 'Test User') + + result = BlogPost.objects.get(author={'name': user.name}) + self.assertTrue(isinstance(result.author, User)) + self.assertEqual(result.author.name, 'Test User') + + # Fails, since the string is not a type that is able to represent the + # author's document structure (should be dict) + with self.assertRaises(InvalidQueryError): + BlogPost.objects.get(author=user.name) + def test_find_empty_embedded(self): """Ensure that you can save and find an empty embedded document.""" class User(EmbeddedDocument): From 5c3928190aab390443a1c5d967b17dadd8f68f14 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Thu, 22 Dec 2016 13:20:05 -0500 Subject: [PATCH 013/268] fix line width --- mongoengine/base/document.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 03536d6f..1667215d 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -16,7 +16,8 @@ from mongoengine.base.datastructures import (BaseDict, BaseList, SemiStrictDict, StrictDict) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class -from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, LookUpError, OperationError, ValidationError) +from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, + LookUpError, OperationError, ValidationError) __all__ = ('BaseDocument',) From 2770cec187835ded145049341a4e72c1c97f8e1d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 27 Dec 2016 10:20:13 -0500 Subject: [PATCH 014/268] better docstring for BaseQuerySet.fields --- mongoengine/queryset/base.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 3ee978b8..5c0fb5b3 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -900,18 +900,24 @@ class BaseQuerySet(object): return self.fields(**fields) def fields(self, _only_called=False, **kwargs): - """Manipulate how you load this document's fields. Used by `.only()` - and `.exclude()` to manipulate which fields to retrieve. Fields also - allows for a greater level of control for example: + """Manipulate how you load this document's fields. Used by `.only()` + and `.exclude()` to manipulate which fields to retrieve. If called + directly, use a set of kwargs similar to the MongoDB projection + document. For example: - Retrieving a Subrange of Array Elements: + Include only a subset of fields: - You can use the $slice operator to retrieve a subrange of elements in - an array. For example to get the first 5 comments:: + posts = BlogPost.objects(...).fields(author=1, title=1) - post = BlogPost.objects(...).fields(slice__comments=5) + Exclude a specific field: - :param kwargs: A dictionary identifying what to include + posts = BlogPost.objects(...).fields(comments=0) + + To retrieve a subrange of array elements: + + posts = BlogPost.objects(...).fields(slice__comments=5) + + :param kwargs: A set keywors arguments identifying what to include. .. versionadded:: 0.5 """ From e07cb82c151841c948019a5384c7c0f728c461e9 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 27 Dec 2016 17:38:26 -0500 Subject: [PATCH 015/268] validate db_field --- mongoengine/base/fields.py | 11 +++++++++++ tests/fields/fields.py | 31 ++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index b0644cec..2bcdcfdf 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -81,6 +81,17 @@ class BaseField(object): self.sparse = sparse self._owner_document = None + # Validate the db_field + if self.db_field and ( + '.' in self.db_field or + '\0' in self.db_field or + self.db_field.startswith('$') + ): + raise ValueError( + 'field names cannot contain dots (".") or null characters ' + '("\\0"), and they must not start with a dollar sign ("$").' + ) + # Detect and report conflicts between metadata and base properties. conflicts = set(dir(self)) & set(kwargs) if conflicts: diff --git a/tests/fields/fields.py b/tests/fields/fields.py index c6089c8b..5874a36d 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -306,6 +306,24 @@ class FieldTest(unittest.TestCase): person.id = '497ce96f395f2f052a494fd4' person.validate() + def test_db_field_validation(self): + """Ensure that db_field doesn't accept invalid values.""" + + # dot in the name + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='user.name') + + # name starting with $ + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='$name') + + # name containing a null character + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='name\0') + def test_string_validation(self): """Ensure that invalid values cannot be assigned to string fields. """ @@ -3973,30 +3991,25 @@ class FieldTest(unittest.TestCase): """Tests if a `FieldDoesNotExist` exception is raised when trying to instanciate a document with a field that's not defined. """ - class Doc(Document): - foo = StringField(db_field='f') + foo = StringField() - def test(): + with self.assertRaises(FieldDoesNotExist): Doc(bar='test') - self.assertRaises(FieldDoesNotExist, test) def test_undefined_field_exception_with_strict(self): """Tests if a `FieldDoesNotExist` exception is raised when trying to instanciate a document with a field that's not defined, even when strict is set to False. """ - class Doc(Document): - foo = StringField(db_field='f') + foo = StringField() meta = {'strict': False} - def test(): + with self.assertRaises(FieldDoesNotExist): Doc(bar='test') - self.assertRaises(FieldDoesNotExist, test) - def test_long_field_is_considered_as_int64(self): """ Tests that long fields are stored as long in mongo, even if long value From 91dad4060f437037ad35e3892b1953bdfc2d0128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Wed, 28 Dec 2016 00:51:47 -0500 Subject: [PATCH 016/268] raise an error when trying to save an abstract document (#1449) --- mongoengine/document.py | 4 +++- tests/document/instance.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index e86a45d9..0fa2460d 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -313,6 +313,9 @@ class Document(BaseDocument): .. versionchanged:: 0.10.7 Add signal_kwargs argument """ + if self._meta.get('abstract'): + raise InvalidDocumentError('Cannot save an abstract document.') + signal_kwargs = signal_kwargs or {} signals.pre_save.send(self.__class__, document=self, **signal_kwargs) @@ -828,7 +831,6 @@ class Document(BaseDocument): """ Lists all of the indexes that should be created for given collection. It includes all the indexes from super- and sub-classes. """ - if cls._meta.get('abstract'): return [] diff --git a/tests/document/instance.py b/tests/document/instance.py index d961f034..b187f766 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -435,6 +435,15 @@ class InstanceTest(unittest.TestCase): person.to_dbref() + def test_save_abstract_document(self): + """Saving an abstract document should fail.""" + class Doc(Document): + name = StringField() + meta = {'abstract': True} + + with self.assertRaises(InvalidDocumentError): + Doc(name='aaa').save() + def test_reload(self): """Ensure that attributes may be reloaded. """ From f4e1d80a8753e1088433570f5cb17c8a206b619e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Wed, 28 Dec 2016 02:04:49 -0500 Subject: [PATCH 017/268] support a negative dec operator (#1450) --- mongoengine/queryset/transform.py | 3 +-- tests/queryset/queryset.py | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 61d43490..bb04ee37 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -233,8 +233,7 @@ def update(_doc_cls=None, **update): # Support decrement by flipping a positive value's sign # and using 'inc' op = 'inc' - if value > 0: - value = -value + value = -value elif op == 'add_to_set': op = 'addToSet' elif op == 'set_on_insert': diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a15807a5..2d5b5b0f 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1826,6 +1826,11 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertEqual(post.hits, 10) + # Negative dec operator is equal to a positive inc operator + BlogPost.objects.update_one(dec__hits=-1) + post.reload() + self.assertEqual(post.hits, 11) + BlogPost.objects.update(push__tags='mongo') post.reload() self.assertTrue('mongo' in post.tags) From c6cc0136176146ed0f0aed396bc2041eecc2d51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Wed, 28 Dec 2016 11:40:57 -0500 Subject: [PATCH 018/268] fix BaseQuerySet.fields when mixing exclusion/inclusion with complex values like $slice (#1452) --- mongoengine/queryset/base.py | 15 ++++++++++++++- tests/queryset/field_list.py | 10 ++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 5c0fb5b3..098f198e 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -933,7 +933,20 @@ class BaseQuerySet(object): key = '.'.join(parts) cleaned_fields.append((key, value)) - fields = sorted(cleaned_fields, key=operator.itemgetter(1)) + # Sort fields by their values, explicitly excluded fields first, then + # explicitly included, and then more complicated operators such as + # $slice. + def _sort_key(field_tuple): + key, value = field_tuple + if isinstance(value, (int)): + return value # 0 for exclusion, 1 for inclusion + else: + return 2 # so that complex values appear last + + fields = sorted(cleaned_fields, key=_sort_key) + + # Clone the queryset, group all fields by their value, convert + # each of them to db_fields, and set the queryset's _loaded_fields queryset = self.clone() for value, group in itertools.groupby(fields, lambda x: x[1]): fields = [field for field, value in group] diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index 76d5f779..d1277e06 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -141,6 +141,16 @@ class OnlyExcludeAllTest(unittest.TestCase): self.assertEqual(qs._loaded_fields.as_dict(), {'b': {'$slice': 5}}) + def test_mix_slice_with_other_fields(self): + class MyDoc(Document): + a = ListField() + b = ListField() + c = ListField() + + qs = MyDoc.objects.fields(a=1, b=0, slice__c=2) + self.assertEqual(qs._loaded_fields.as_dict(), + {'c': {'$slice': 2}, 'a': 1}) + def test_only(self): """Ensure that QuerySet.only only returns the requested fields. """ From 74b37d11cf1dfba93fc6ee8ff4bdbda767f0b975 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 28 Dec 2016 11:46:18 -0500 Subject: [PATCH 019/268] only validate db_field if it's a string type --- mongoengine/base/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 2bcdcfdf..9ba9dc9a 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -41,7 +41,7 @@ class BaseField(object): """ :param db_field: The database field to store this field in (defaults to the name of the field) - :param name: Depreciated - use db_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 @@ -82,7 +82,7 @@ class BaseField(object): self._owner_document = None # Validate the db_field - if self.db_field and ( + if isinstance(self.db_field, six.string_types) and ( '.' in self.db_field or '\0' in self.db_field or self.db_field.startswith('$') From b013a065f7d61f1439312b775a372f9d4cebf59a Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 28 Dec 2016 11:50:28 -0500 Subject: [PATCH 020/268] remove readme mention of the irc channel --- CONTRIBUTING.rst | 2 +- README.rst | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 06bf2f61..2668499c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -14,7 +14,7 @@ Before starting to write code, look for existing `tickets `_ or `create one `_ for your specific issue or feature request. That way you avoid working on something -that might not be of interest or that has already been addressed. If in doubt +that might not be of interest or that has already been addressed. If in doubt post to the `user group ` Supported Interpreters diff --git a/README.rst b/README.rst index e46a835f..1afc819d 100644 --- a/README.rst +++ b/README.rst @@ -130,7 +130,6 @@ Community `_ - `MongoEngine Developers mailing list `_ -- `#mongoengine IRC channel `_ Contributing ============ From 96e95ac533d171ab7f371f0089487d9cfc870755 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 28 Dec 2016 17:18:55 -0500 Subject: [PATCH 021/268] minor readme tweaks --- README.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 1afc819d..e42fd3ea 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ Some simple examples of what MongoEngine code looks like: class BlogPost(Document): title = StringField(required=True, max_length=200) - posted = DateTimeField(default=datetime.datetime.now) + posted = DateTimeField(default=datetime.datetime.utcnow) tags = ListField(StringField(max_length=50)) meta = {'allow_inheritance': True} @@ -87,17 +87,18 @@ Some simple examples of what MongoEngine code looks like: ... print ... - >>> len(BlogPost.objects) + # Count all blog posts and its subtypes + >>> BlogPost.objects.count() 2 - >>> len(TextPost.objects) + >>> TextPost.objects.count() 1 - >>> len(LinkPost.objects) + >>> LinkPost.objects.count() 1 - # Find tagged posts - >>> len(BlogPost.objects(tags='mongoengine')) + # Count tagged posts + >>> BlogPost.objects(tags='mongoengine').count() 2 - >>> len(BlogPost.objects(tags='mongodb')) + >>> BlogPost.objects(tags='mongodb').count() 1 Tests @@ -133,4 +134,4 @@ Community Contributing ============ -We welcome contributions! see the `Contribution guidelines `_ +We welcome contributions! See the `Contribution guidelines `_ From 47c58bce2bcff79a60f2550710f68dd58d1d33d1 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 28 Dec 2016 21:08:18 -0500 Subject: [PATCH 022/268] fix "connect" example in the docs --- docs/guide/connecting.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index 48926499..827e5a3c 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -33,7 +33,7 @@ the :attr:`host` to corresponding parameters in :func:`~mongoengine.connect`: :: connect( - name='test', + db='test', username='user', password='12345', host='mongodb://admin:qwerty@localhost/production' From b7ec587e5b82717dd704a7812b576ceff405cff3 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 28 Dec 2016 22:15:46 -0500 Subject: [PATCH 023/268] better docstring for BaseDocument.to_json --- mongoengine/base/document.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 1667215d..9d366706 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -402,9 +402,11 @@ class BaseDocument(object): raise ValidationError(message, errors=errors) def to_json(self, *args, **kwargs): - """Converts a document to JSON. - :param use_db_field: Set to True by default but enables the output of the json structure with the field names - and not the mongodb store db_names in case of set to False + """Convert this document to JSON. + + :param use_db_field: Serialize field names as they appear in + MongoDB (as opposed to attribute names on this document). + Defaults to True. """ use_db_field = kwargs.pop('use_db_field', True) return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs) From 2ee8984b443dda0cf7ea6fac51e801f161d0f16c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Wed, 28 Dec 2016 23:25:38 -0500 Subject: [PATCH 024/268] add a $rename operator (#1454) --- mongoengine/base/common.py | 2 +- tests/document/instance.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/common.py b/mongoengine/base/common.py index da2b8b68..b9971ff7 100644 --- a/mongoengine/base/common.py +++ b/mongoengine/base/common.py @@ -5,7 +5,7 @@ __all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry') UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push', 'push_all', 'pull', 'pull_all', 'add_to_set', - 'set_on_insert', 'min', 'max']) + 'set_on_insert', 'min', 'max', 'rename']) _document_registry = {} diff --git a/tests/document/instance.py b/tests/document/instance.py index b187f766..9b52c809 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1232,6 +1232,19 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, None) + def test_update_rename_operator(self): + """Test the $rename operator.""" + coll = self.Person._get_collection() + doc = self.Person(name='John').save() + raw_doc = coll.find_one({'_id': doc.pk}) + self.assertEqual(set(raw_doc.keys()), set(['_id', '_cls', 'name'])) + + doc.update(rename__name='first_name') + raw_doc = coll.find_one({'_id': doc.pk}) + self.assertEqual(set(raw_doc.keys()), + set(['_id', '_cls', 'first_name'])) + self.assertEqual(raw_doc['first_name'], 'John') + def test_inserts_if_you_set_the_pk(self): p1 = self.Person(name='p1', id=bson.ObjectId()).save() p2 = self.Person(name='p2') From c6240ca41598330f3592f9f4dccd7a0b41f2a0a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Thu, 29 Dec 2016 12:37:38 -0500 Subject: [PATCH 025/268] Test connection's write concern (#1456) --- tests/test_connection.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index 9143571f..cc75f1e7 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -296,6 +296,19 @@ class ConnectionTest(unittest.TestCase): conn = get_connection('t2') self.assertFalse(get_tz_awareness(conn)) + def test_write_concern(self): + """Ensure write concern can be specified in connect() via + a kwarg or as part of the connection URI. + """ + conn1 = connect(alias='conn1', host='mongodb://localhost/testing?w=1&j=true') + conn2 = connect('testing', alias='conn2', w=1, j=True) + if IS_PYMONGO_3: + self.assertEqual(conn1.write_concern.document, {'w': 1, 'j': True}) + self.assertEqual(conn2.write_concern.document, {'w': 1, 'j': True}) + else: + self.assertEqual(dict(conn1.write_concern), {'w': 1, 'j': True}) + self.assertEqual(dict(conn2.write_concern), {'w': 1, 'j': True}) + def test_datetime(self): connect('mongoenginetest', tz_aware=True) d = datetime.datetime(2010, 5, 5, tzinfo=utc) From ebd34427c7a10d322a3f5c2bdef8f34afc08ca2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Fri, 30 Dec 2016 05:43:56 -0500 Subject: [PATCH 026/268] Cleaner Document.save (#1458) --- mongoengine/document.py | 142 ++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 57 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 0fa2460d..b79e5e97 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -332,68 +332,20 @@ class Document(BaseDocument): signals.pre_save_post_validation.send(self.__class__, document=self, created=created, **signal_kwargs) + if self._meta.get('auto_create_index', True): + self.ensure_indexes() + try: - collection = self._get_collection() - if self._meta.get('auto_create_index', True): - self.ensure_indexes() + # Save a new document or update an existing one if created: - if force_insert: - object_id = collection.insert(doc, **write_concern) - else: - object_id = collection.save(doc, **write_concern) - # In PyMongo 3.0, the save() call calls internally the _update() call - # but they forget to return the _id value passed back, therefore getting it back here - # Correct behaviour in 2.X and in 3.0.1+ versions - if not object_id and pymongo.version_tuple == (3, 0): - pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk) - object_id = ( - self._qs.filter(pk=pk_as_mongo_obj).first() and - self._qs.filter(pk=pk_as_mongo_obj).first().pk - ) # TODO doesn't this make 2 queries? + object_id = self._save_create(doc, force_insert, write_concern) else: - object_id = doc['_id'] - updates, removals = self._delta() - # Need to add shard key to query, or you get an error - if save_condition is not None: - select_dict = transform.query(self.__class__, - **save_condition) - else: - select_dict = {} - select_dict['_id'] = object_id - shard_key = self._meta.get('shard_key', tuple()) - for k in shard_key: - path = self._lookup_field(k.split('.')) - actual_key = [p.db_field for p in path] - val = doc - for ak in actual_key: - val = val[ak] - select_dict['.'.join(actual_key)] = val - - def is_new_object(last_error): - if last_error is not None: - updated = last_error.get('updatedExisting') - if updated is not None: - return not updated - return created - - update_query = {} - - if updates: - update_query['$set'] = updates - if removals: - update_query['$unset'] = removals - if updates or removals: - upsert = save_condition is None - last_error = collection.update(select_dict, update_query, - upsert=upsert, **write_concern) - if not upsert and last_error['n'] == 0: - raise SaveConditionError('Race condition preventing' - ' document update detected') - created = is_new_object(last_error) + object_id, created = self._save_update(doc, save_condition, + write_concern) if cascade is None: - cascade = self._meta.get( - 'cascade', False) or cascade_kwargs is not None + cascade = (self._meta.get('cascade', False) or + cascade_kwargs is not None) if cascade: kwargs = { @@ -406,6 +358,7 @@ class Document(BaseDocument): kwargs.update(cascade_kwargs) kwargs['_refs'] = _refs self.cascade_save(**kwargs) + except pymongo.errors.DuplicateKeyError as err: message = u'Tried to save duplicate unique keys (%s)' raise NotUniqueError(message % six.text_type(err)) @@ -418,16 +371,91 @@ class Document(BaseDocument): raise NotUniqueError(message % six.text_type(err)) raise OperationError(message % six.text_type(err)) + # Make sure we store the PK on this document now that it's saved id_field = self._meta['id_field'] if created or id_field not in self._meta.get('shard_key', []): self[id_field] = self._fields[id_field].to_python(object_id) signals.post_save.send(self.__class__, document=self, created=created, **signal_kwargs) + self._clear_changed_fields() self._created = False + return self + def _save_create(self, doc, force_insert, write_concern): + """Save a new document. + + Helper method, should only be used inside save(). + """ + collection = self._get_collection() + + if force_insert: + return collection.insert(doc, **write_concern) + + object_id = collection.save(doc, **write_concern) + + # In PyMongo 3.0, the save() call calls internally the _update() call + # but they forget to return the _id value passed back, therefore getting it back here + # Correct behaviour in 2.X and in 3.0.1+ versions + if not object_id and pymongo.version_tuple == (3, 0): + pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk) + object_id = ( + self._qs.filter(pk=pk_as_mongo_obj).first() and + self._qs.filter(pk=pk_as_mongo_obj).first().pk + ) # TODO doesn't this make 2 queries? + + return object_id + + def _save_update(self, doc, save_condition, write_concern): + """Update an existing document. + + Helper method, should only be used inside save(). + """ + collection = self._get_collection() + object_id = doc['_id'] + created = False + + select_dict = {} + if save_condition is not None: + select_dict = transform.query(self.__class__, **save_condition) + + select_dict['_id'] = object_id + + # Need to add shard key to query, or you get an error + shard_key = self._meta.get('shard_key', tuple()) + for k in shard_key: + path = self._lookup_field(k.split('.')) + actual_key = [p.db_field for p in path] + val = doc + for ak in actual_key: + val = val[ak] + select_dict['.'.join(actual_key)] = val + + updates, removals = self._delta() + update_query = {} + if updates: + update_query['$set'] = updates + if removals: + update_query['$unset'] = removals + if updates or removals: + upsert = save_condition is None + last_error = collection.update(select_dict, update_query, + upsert=upsert, **write_concern) + if not upsert and last_error['n'] == 0: + raise SaveConditionError('Race condition preventing' + ' document update detected') + if last_error is not None: + updated_existing = last_error.get('updatedExisting') + if updated_existing is False: + created = True + # !!! This is bad, means we accidentally created a new, + # potentially corrupted document. See + # https://github.com/MongoEngine/mongoengine/issues/564 + + return object_id, created + def cascade_save(self, **kwargs): """Recursively save any references and generic references on the document. From 193aa4e1f2c58d5a734c257be7e284b20799208c Mon Sep 17 00:00:00 2001 From: lanf0n Date: Fri, 6 Jan 2017 11:37:09 +0800 Subject: [PATCH 027/268] [#1459] fix typo `__neq__` to `__ne__` (#1461) --- mongoengine/base/datastructures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 8a7681e5..b9aca8fa 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -429,7 +429,7 @@ class StrictDict(object): def __eq__(self, other): return self.items() == other.items() - def __neq__(self, other): + def __ne__(self, other): return self.items() != other.items() @classmethod From b5fb82d95dd4801562e5ad6d242e58d31fa49558 Mon Sep 17 00:00:00 2001 From: Eli Boyarski Date: Sun, 8 Jan 2017 19:57:36 +0200 Subject: [PATCH 028/268] Typo fix (#1463) --- tests/fields/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5874a36d..5c83d58c 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1985,7 +1985,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(content, User.objects.first().groups[0].content) def test_reference_miss(self): - """Ensure an exception is raised when dereferencing unknow document + """Ensure an exception is raised when dereferencing unknown document """ class Foo(Document): From 1b6743ee5317d60d5f98981868307d7407c367f3 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 8 Jan 2017 14:50:09 -0500 Subject: [PATCH 029/268] add a changelog entry about broken references raising DoesNotExist --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index af3437c1..a38378c8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,7 @@ Changes in 0.11.0 - BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428 - BREAKING CHANGE: Dropped Python 2.6 support. #1428 - BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428 +- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334 - Fixed absent rounding for DecimalField when `force_string` is set. #1103 Changes in 0.10.8 From e5acbcc0dd4c9dfe827c88b08a59160a73239781 Mon Sep 17 00:00:00 2001 From: Eli Boyarski Date: Mon, 9 Jan 2017 18:24:27 +0200 Subject: [PATCH 030/268] Improved a docstring for FieldDoesNotExist (#1466) --- mongoengine/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 2549e822..131596d1 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -50,8 +50,8 @@ class FieldDoesNotExist(Exception): or an :class:`~mongoengine.EmbeddedDocument`. To avoid this behavior on data loading, - you should the :attr:`strict` to ``False`` - in the :attr:`meta` dictionnary. + you should set the :attr:`strict` to ``False`` + in the :attr:`meta` dictionary. """ From 4eedf000250a9267b9e40646f0f4e32ceef3298f Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 17 Jan 2017 02:42:23 -0500 Subject: [PATCH 031/268] nicer readme note about dependencies --- README.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index e42fd3ea..fa872afd 100644 --- a/README.rst +++ b/README.rst @@ -35,16 +35,20 @@ setup.py install``. Dependencies ============ -- pymongo>=2.7.1 -- sphinx (optional - for documentation generation) +All of the dependencies can easily be installed via `pip `_. -Optional Dependencies ---------------------- -- **Image Fields**: Pillow>=2.0.0 +Bare minimum you need to use this package: +- pymongo>=2.7.1 +- six>=1.10.0 + +If you utilize a ``DateTimeField`` and want to use a more flexible date parser: - dateutil>=2.1.0 -.. note - MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: PyMongo 3.0.1 +If you to utilize an ``ImageField`` or ``ImageGridFsProxy``: +- Pillow>=2.0.0 + +If you want to generate the documentation (e.g. to contribute to it): +- sphinx Examples ======== From affc12df4b4e23a88cf589d3b44bcfaecf25cc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Tue, 17 Jan 2017 02:43:29 -0500 Subject: [PATCH 032/268] Update README.rst --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index fa872afd..a7b9c48c 100644 --- a/README.rst +++ b/README.rst @@ -38,16 +38,20 @@ Dependencies All of the dependencies can easily be installed via `pip `_. Bare minimum you need to use this package: + - pymongo>=2.7.1 - six>=1.10.0 If you utilize a ``DateTimeField`` and want to use a more flexible date parser: + - dateutil>=2.1.0 If you to utilize an ``ImageField`` or ``ImageGridFsProxy``: + - Pillow>=2.0.0 If you want to generate the documentation (e.g. to contribute to it): + - sphinx Examples From 80530bb13c357cbac14547bec268fdcf5cfe19c7 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 17 Jan 2017 02:46:37 -0500 Subject: [PATCH 033/268] nicer readme --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index a7b9c48c..068cc4ba 100644 --- a/README.rst +++ b/README.rst @@ -35,18 +35,16 @@ setup.py install``. Dependencies ============ -All of the dependencies can easily be installed via `pip `_. - -Bare minimum you need to use this package: +All of the dependencies can easily be installed via `pip `_. At the very least, you'll need these two packages to use MongoEngine: - pymongo>=2.7.1 - six>=1.10.0 -If you utilize a ``DateTimeField`` and want to use a more flexible date parser: +If you utilize a ``DateTimeField`` and you want to use a more flexible date parser: - dateutil>=2.1.0 -If you to utilize an ``ImageField`` or ``ImageGridFsProxy``: +If you also utilize an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 From f33cd625bf4fa0f208a59604785f8d394c27a38c Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Tue, 17 Jan 2017 02:47:45 -0500 Subject: [PATCH 034/268] nicer readme --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 068cc4ba..adfa0c71 100644 --- a/README.rst +++ b/README.rst @@ -40,11 +40,11 @@ All of the dependencies can easily be installed via `pip ` - pymongo>=2.7.1 - six>=1.10.0 -If you utilize a ``DateTimeField`` and you want to use a more flexible date parser: +If you utilize a ``DateTimeField``, you might also use a more flexible date parser: - dateutil>=2.1.0 -If you also utilize an ``ImageField`` or ``ImageGridFsProxy``: +If you need to use an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 From d8a732836522d964454b3101d4ccb9943d1de9bb Mon Sep 17 00:00:00 2001 From: martin sereinig Date: Mon, 6 Feb 2017 22:11:42 +0100 Subject: [PATCH 035/268] Fix docs regarding reverse_delete_rule and delete signals (#1473) --- docs/guide/defining-documents.rst | 5 ----- docs/guide/signals.rst | 7 ------- mongoengine/fields.py | 4 ---- 3 files changed, 16 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index f59f856e..e656dee0 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -361,11 +361,6 @@ Its value can take any of the following constants: In Django, be sure to put all apps that have such delete rule declarations in their :file:`models.py` in the :const:`INSTALLED_APPS` tuple. - -.. warning:: - Signals are not triggered when doing cascading updates / deletes - if this - is required you must manually handle the update / delete. - Generic reference fields '''''''''''''''''''''''' A second kind of reference field also exists, diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index 797a4869..30277966 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -142,11 +142,4 @@ cleaner looking while still allowing manual execution of the callback:: modified = DateTimeField() -ReferenceFields and Signals ---------------------------- - -Currently `reverse_delete_rule` does not trigger signals on the other part of -the relationship. If this is required you must manually handle the -reverse deletion. - .. _blinker: http://pypi.python.org/pypi/blinker diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 3991ef5c..0ea7d3b6 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -888,10 +888,6 @@ class ReferenceField(BaseField): Foo.register_delete_rule(Bar, 'foo', NULLIFY) - .. note :: - `reverse_delete_rule` does not trigger pre / post delete signals to be - triggered. - .. versionchanged:: 0.5 added `reverse_delete_rule` """ From 3ca2e953fb5ff6f5f5373cae9dce6b97609d2d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Thu, 9 Feb 2017 12:02:46 -0800 Subject: [PATCH 036/268] Fix limit/skip/hint/batch_size chaining (#1476) --- mongoengine/queryset/base.py | 183 +++++++++++++++++++++---------- mongoengine/queryset/queryset.py | 8 +- tests/queryset/queryset.py | 114 +++++++++++++++---- 3 files changed, 220 insertions(+), 85 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 098f198e..7e485686 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -86,6 +86,7 @@ class BaseQuerySet(object): self._batch_size = None self.only_fields = [] self._max_time_ms = None + self._comment = None def __call__(self, q_obj=None, class_check=True, read_preference=None, **query): @@ -706,39 +707,36 @@ class BaseQuerySet(object): with switch_db(self._document, alias) as cls: collection = cls._get_collection() - return self.clone_into(self.__class__(self._document, collection)) + return self._clone_into(self.__class__(self._document, collection)) def clone(self): - """Creates a copy of the current - :class:`~mongoengine.queryset.QuerySet` + """Create a copy of the current queryset.""" + return self._clone_into(self.__class__(self._document, self._collection_obj)) - .. versionadded:: 0.5 + def _clone_into(self, new_qs): + """Copy all of the relevant properties of this queryset to + a new queryset (which has to be an instance of + :class:`~mongoengine.queryset.base.BaseQuerySet`). """ - return self.clone_into(self.__class__(self._document, self._collection_obj)) - - def clone_into(self, cls): - """Creates a copy of the current - :class:`~mongoengine.queryset.base.BaseQuerySet` into another child class - """ - if not isinstance(cls, BaseQuerySet): + if not isinstance(new_qs, BaseQuerySet): raise OperationError( - '%s is not a subclass of BaseQuerySet' % cls.__name__) + '%s is not a subclass of BaseQuerySet' % new_qs.__name__) copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', '_where_clause', '_loaded_fields', '_ordering', '_snapshot', '_timeout', '_class_check', '_slave_okay', '_read_preference', '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', '_limit', '_skip', '_hint', '_auto_dereference', - '_search_text', 'only_fields', '_max_time_ms') + '_search_text', 'only_fields', '_max_time_ms', '_comment') for prop in copy_props: val = getattr(self, prop) - setattr(cls, prop, copy.copy(val)) + setattr(new_qs, prop, copy.copy(val)) if self._cursor_obj: - cls._cursor_obj = self._cursor_obj.clone() + new_qs._cursor_obj = self._cursor_obj.clone() - return cls + return new_qs def select_related(self, max_depth=1): """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or @@ -760,7 +758,11 @@ class BaseQuerySet(object): """ queryset = self.clone() queryset._limit = n if n != 0 else 1 - # Return self to allow chaining + + # If a cursor object has already been created, apply the limit to it. + if queryset._cursor_obj: + queryset._cursor_obj.limit(queryset._limit) + return queryset def skip(self, n): @@ -771,6 +773,11 @@ class BaseQuerySet(object): """ queryset = self.clone() queryset._skip = n + + # If a cursor object has already been created, apply the skip to it. + if queryset._cursor_obj: + queryset._cursor_obj.skip(queryset._skip) + return queryset def hint(self, index=None): @@ -788,6 +795,11 @@ class BaseQuerySet(object): """ queryset = self.clone() queryset._hint = index + + # If a cursor object has already been created, apply the hint to it. + if queryset._cursor_obj: + queryset._cursor_obj.hint(queryset._hint) + return queryset def batch_size(self, size): @@ -801,6 +813,11 @@ class BaseQuerySet(object): """ queryset = self.clone() queryset._batch_size = size + + # If a cursor object has already been created, apply the batch size to it. + if queryset._cursor_obj: + queryset._cursor_obj.batch_size(queryset._batch_size) + return queryset def distinct(self, field): @@ -972,13 +989,31 @@ class BaseQuerySet(object): def order_by(self, *keys): """Order the :class:`~mongoengine.queryset.QuerySet` by the keys. The order may be specified by prepending each of the keys by a + or a -. - Ascending order is assumed. + Ascending order is assumed. If no keys are passed, existing ordering + is cleared instead. :param keys: fields to order the query results by; keys may be prefixed with **+** or **-** to determine the ordering direction """ queryset = self.clone() - queryset._ordering = queryset._get_order_by(keys) + + old_ordering = queryset._ordering + new_ordering = queryset._get_order_by(keys) + + if queryset._cursor_obj: + + # If a cursor object has already been created, apply the sort to it + if new_ordering: + queryset._cursor_obj.sort(new_ordering) + + # If we're trying to clear a previous explicit ordering, we need + # to clear the cursor entirely (because PyMongo doesn't allow + # clearing an existing sort on a cursor). + elif old_ordering: + queryset._cursor_obj = None + + queryset._ordering = new_ordering + return queryset def comment(self, text): @@ -1424,10 +1459,13 @@ class BaseQuerySet(object): raise StopIteration raw_doc = self._cursor.next() + if self._as_pymongo: return self._get_as_pymongo(raw_doc) - doc = self._document._from_son(raw_doc, - _auto_dereference=self._auto_dereference, only_fields=self.only_fields) + + doc = self._document._from_son( + raw_doc, _auto_dereference=self._auto_dereference, + only_fields=self.only_fields) if self._scalar: return self._get_scalar(doc) @@ -1437,7 +1475,6 @@ class BaseQuerySet(object): def rewind(self): """Rewind the cursor to its unevaluated state. - .. versionadded:: 0.3 """ self._iter = False @@ -1487,43 +1524,54 @@ class BaseQuerySet(object): @property def _cursor(self): - if self._cursor_obj is None: + """Return a PyMongo cursor object corresponding to this queryset.""" - # In PyMongo 3+, we define the read preference on a collection - # level, not a cursor level. Thus, we need to get a cloned - # collection object using `with_options` first. - if IS_PYMONGO_3 and self._read_preference is not None: - self._cursor_obj = self._collection\ - .with_options(read_preference=self._read_preference)\ - .find(self._query, **self._cursor_args) - else: - self._cursor_obj = self._collection.find(self._query, - **self._cursor_args) - # Apply where clauses to cursor - if self._where_clause: - where_clause = self._sub_js_fields(self._where_clause) - self._cursor_obj.where(where_clause) + # If _cursor_obj already exists, return it immediately. + if self._cursor_obj is not None: + return self._cursor_obj - if self._ordering: - # Apply query ordering - self._cursor_obj.sort(self._ordering) - elif self._ordering is None and self._document._meta['ordering']: - # Otherwise, apply the ordering from the document model, unless - # it's been explicitly cleared via order_by with no arguments - order = self._get_order_by(self._document._meta['ordering']) - self._cursor_obj.sort(order) + # Create a new PyMongo cursor. + # XXX In PyMongo 3+, we define the read preference on a collection + # level, not a cursor level. Thus, we need to get a cloned collection + # object using `with_options` first. + if IS_PYMONGO_3 and self._read_preference is not None: + self._cursor_obj = self._collection\ + .with_options(read_preference=self._read_preference)\ + .find(self._query, **self._cursor_args) + else: + self._cursor_obj = self._collection.find(self._query, + **self._cursor_args) + # Apply "where" clauses to cursor + if self._where_clause: + where_clause = self._sub_js_fields(self._where_clause) + self._cursor_obj.where(where_clause) - if self._limit is not None: - self._cursor_obj.limit(self._limit) + # Apply ordering to the cursor. + # XXX self._ordering can be equal to: + # * None if we didn't explicitly call order_by on this queryset. + # * A list of PyMongo-style sorting tuples. + # * An empty list if we explicitly called order_by() without any + # arguments. This indicates that we want to clear the default + # ordering. + if self._ordering: + # explicit ordering + self._cursor_obj.sort(self._ordering) + elif self._ordering is None and self._document._meta['ordering']: + # default ordering + order = self._get_order_by(self._document._meta['ordering']) + self._cursor_obj.sort(order) - if self._skip is not None: - self._cursor_obj.skip(self._skip) + if self._limit is not None: + self._cursor_obj.limit(self._limit) - if self._hint != -1: - self._cursor_obj.hint(self._hint) + if self._skip is not None: + self._cursor_obj.skip(self._skip) - if self._batch_size is not None: - self._cursor_obj.batch_size(self._batch_size) + if self._hint != -1: + self._cursor_obj.hint(self._hint) + + if self._batch_size is not None: + self._cursor_obj.batch_size(self._batch_size) return self._cursor_obj @@ -1698,7 +1746,13 @@ class BaseQuerySet(object): return ret def _get_order_by(self, keys): - """Creates a list of order by fields""" + """Given a list of MongoEngine-style sort keys, return a list + of sorting tuples that can be applied to a PyMongo cursor. For + example: + + >>> qs._get_order_by(['-last_name', 'first_name']) + [('last_name', -1), ('first_name', 1)] + """ key_list = [] for key in keys: if not key: @@ -1711,17 +1765,19 @@ class BaseQuerySet(object): direction = pymongo.ASCENDING if key[0] == '-': direction = pymongo.DESCENDING + if key[0] in ('-', '+'): key = key[1:] + key = key.replace('__', '.') try: key = self._document._translate_field_name(key) except Exception: + # TODO this exception should be more specific pass + key_list.append((key, direction)) - if self._cursor_obj and key_list: - self._cursor_obj.sort(key_list) return key_list def _get_scalar(self, doc): @@ -1819,10 +1875,21 @@ class BaseQuerySet(object): return code def _chainable_method(self, method_name, val): + """Call a particular method on the PyMongo cursor call a particular chainable method + with the provided value. + """ queryset = self.clone() - method = getattr(queryset._cursor, method_name) - method(val) + + # Get an existing cursor object or create a new one + cursor = queryset._cursor + + # Find the requested method on the cursor and call it with the + # provided value + getattr(cursor, method_name)(val) + + # Cache the value on the queryset._{method_name} setattr(queryset, '_' + method_name, val) + return queryset # Deprecated diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 9c1f24e1..b5d2765b 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -136,13 +136,15 @@ class QuerySet(BaseQuerySet): return self._len def no_cache(self): - """Convert to a non_caching queryset + """Convert to a non-caching queryset .. versionadded:: 0.8.3 Convert to non caching queryset """ if self._result_cache is not None: raise OperationError('QuerySet already cached') - return self.clone_into(QuerySetNoCache(self._document, self._collection)) + + return self._clone_into(QuerySetNoCache(self._document, + self._collection)) class QuerySetNoCache(BaseQuerySet): @@ -153,7 +155,7 @@ class QuerySetNoCache(BaseQuerySet): .. versionadded:: 0.8.3 Convert to caching queryset """ - return self.clone_into(QuerySet(self._document, self._collection)) + return self._clone_into(QuerySet(self._document, self._collection)) def __repr__(self): """Provides the string representation of the QuerySet diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 2d5b5b0f..c54fa13d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -106,58 +106,111 @@ class QuerySetTest(unittest.TestCase): list(BlogPost.objects(author2__name="test")) def test_find(self): - """Ensure that a query returns a valid set of results. - """ - self.Person(name="User A", age=20).save() - self.Person(name="User B", age=30).save() + """Ensure that a query returns a valid set of results.""" + user_a = self.Person.objects.create(name='User A', age=20) + user_b = self.Person.objects.create(name='User B', age=30) # Find all people in the collection people = self.Person.objects self.assertEqual(people.count(), 2) results = list(people) + self.assertTrue(isinstance(results[0], self.Person)) self.assertTrue(isinstance(results[0].id, (ObjectId, str, unicode))) - self.assertEqual(results[0].name, "User A") + + self.assertEqual(results[0], user_a) + self.assertEqual(results[0].name, 'User A') self.assertEqual(results[0].age, 20) - self.assertEqual(results[1].name, "User B") + + self.assertEqual(results[1], user_b) + self.assertEqual(results[1].name, 'User B') self.assertEqual(results[1].age, 30) - # Use a query to filter the people found to just person1 + # Filter people by age people = self.Person.objects(age=20) self.assertEqual(people.count(), 1) person = people.next() + self.assertEqual(person, user_a) self.assertEqual(person.name, "User A") self.assertEqual(person.age, 20) - # Test limit + def test_limit(self): + """Ensure that QuerySet.limit works as expected.""" + user_a = self.Person.objects.create(name='User A', age=20) + user_b = self.Person.objects.create(name='User B', age=30) + + # Test limit on a new queryset people = list(self.Person.objects.limit(1)) self.assertEqual(len(people), 1) - self.assertEqual(people[0].name, 'User A') + self.assertEqual(people[0], user_a) - # Test skip + # Test limit on an existing queryset + people = self.Person.objects + self.assertEqual(len(people), 2) + people2 = people.limit(1) + self.assertEqual(len(people), 2) + self.assertEqual(len(people2), 1) + self.assertEqual(people2[0], user_a) + + # Test chaining of only after limit + person = self.Person.objects().limit(1).only('name').first() + self.assertEqual(person, user_a) + self.assertEqual(person.name, 'User A') + self.assertEqual(person.age, None) + + def test_skip(self): + """Ensure that QuerySet.skip works as expected.""" + user_a = self.Person.objects.create(name='User A', age=20) + user_b = self.Person.objects.create(name='User B', age=30) + + # Test skip on a new queryset people = list(self.Person.objects.skip(1)) self.assertEqual(len(people), 1) - self.assertEqual(people[0].name, 'User B') + self.assertEqual(people[0], user_b) - person3 = self.Person(name="User C", age=40) - person3.save() + # Test skip on an existing queryset + people = self.Person.objects + self.assertEqual(len(people), 2) + people2 = people.skip(1) + self.assertEqual(len(people), 2) + self.assertEqual(len(people2), 1) + self.assertEqual(people2[0], user_b) + + # Test chaining of only after skip + person = self.Person.objects().skip(1).only('name').first() + self.assertEqual(person, user_b) + self.assertEqual(person.name, 'User B') + self.assertEqual(person.age, None) + + def test_slice(self): + """Ensure slicing a queryset works as expected.""" + user_a = self.Person.objects.create(name='User A', age=20) + user_b = self.Person.objects.create(name='User B', age=30) + user_c = self.Person.objects.create(name="User C", age=40) # Test slice limit people = list(self.Person.objects[:2]) self.assertEqual(len(people), 2) - self.assertEqual(people[0].name, 'User A') - self.assertEqual(people[1].name, 'User B') + self.assertEqual(people[0], user_a) + self.assertEqual(people[1], user_b) # Test slice skip people = list(self.Person.objects[1:]) self.assertEqual(len(people), 2) - self.assertEqual(people[0].name, 'User B') - self.assertEqual(people[1].name, 'User C') + self.assertEqual(people[0], user_b) + self.assertEqual(people[1], user_c) # Test slice limit and skip people = list(self.Person.objects[1:2]) self.assertEqual(len(people), 1) - self.assertEqual(people[0].name, 'User B') + self.assertEqual(people[0], user_b) + + # Test slice limit and skip on an existing queryset + people = self.Person.objects + self.assertEqual(len(people), 3) + people2 = people[1:2] + self.assertEqual(len(people2), 1) + self.assertEqual(people2[0], user_b) # Test slice limit and skip cursor reset qs = self.Person.objects[1:2] @@ -168,6 +221,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(len(people), 1) self.assertEqual(people[0].name, 'User B') + # Test empty slice people = list(self.Person.objects[1:1]) self.assertEqual(len(people), 0) @@ -187,12 +241,6 @@ class QuerySetTest(unittest.TestCase): self.assertEqual("[, ]", "%s" % self.Person.objects[51:53]) - # Test only after limit - self.assertEqual(self.Person.objects().limit(2).only('name')[0].age, None) - - # Test only after skip - self.assertEqual(self.Person.objects().skip(2).only('name')[0].age, None) - def test_find_one(self): """Ensure that a query using find_one returns a valid result. """ @@ -1226,6 +1274,7 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + # default ordering should be used by default with db_ops_tracker() as q: BlogPost.objects.filter(title='whatever').first() self.assertEqual(len(q.get_ops()), 1) @@ -1234,11 +1283,28 @@ class QuerySetTest(unittest.TestCase): {'published_date': -1} ) + # calling order_by() should clear the default ordering with db_ops_tracker() as q: BlogPost.objects.filter(title='whatever').order_by().first() self.assertEqual(len(q.get_ops()), 1) self.assertFalse('$orderby' in q.get_ops()[0]['query']) + # calling an explicit order_by should use a specified sort + with db_ops_tracker() as q: + BlogPost.objects.filter(title='whatever').order_by('published_date').first() + self.assertEqual(len(q.get_ops()), 1) + self.assertEqual( + q.get_ops()[0]['query']['$orderby'], + {'published_date': 1} + ) + + # calling order_by() after an explicit sort should clear it + with db_ops_tracker() as q: + qs = BlogPost.objects.filter(title='whatever').order_by('published_date') + qs.order_by().first() + self.assertEqual(len(q.get_ops()), 1) + self.assertFalse('$orderby' in q.get_ops()[0]['query']) + def test_no_ordering_for_get(self): """ Ensure that Doc.objects.get doesn't use any ordering. """ From ed34c2ca68a8005c1c48ef067163e63e273087c2 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Thu, 9 Feb 2017 12:13:56 -0800 Subject: [PATCH 037/268] update the changelog and upgrade docs --- docs/changelog.rst | 2 ++ docs/upgrade.rst | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a38378c8..cd99b73e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,8 @@ Changelog Development =========== - (Fill this out as you fix issues and develop you features). +- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476 +- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476 - Fixed connecting to a replica set with PyMongo 2.x #1436 - Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237 diff --git a/docs/upgrade.rst b/docs/upgrade.rst index c0ae7205..17b1c4ac 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -2,6 +2,20 @@ Upgrading ######### +Development +*********** +(Fill this out whenever you introduce breaking changes to MongoEngine) + +This release includes various fixes for the `BaseQuerySet` methods and how they +are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size +to an already-existing queryset wouldn't modify the underlying PyMongo cursor. +This has been fixed now, so you'll need to make sure that your code didn't rely +on the broken implementation. + +Additionally, a public `BaseQuerySet.clone_into` has been renamed to a private +`_clone_into`. If you directly used that method in your code, you'll need to +rename its occurrences. + 0.11.0 ****** This release includes a major rehaul of MongoEngine's code quality and From b27c7ce11baaf65fd36448b8b692fba15927b06c Mon Sep 17 00:00:00 2001 From: bagerard Date: Wed, 15 Feb 2017 14:51:47 +0100 Subject: [PATCH 038/268] allow to use sets in field choices (#1482) --- docs/changelog.rst | 3 +- docs/guide/defining-documents.rst | 6 +-- mongoengine/base/fields.py | 3 +- tests/fields/fields.py | 61 ++++++++++++++++++------------- tests/fields/file_tests.py | 8 ++-- tests/utils.py | 22 +++++++++++ 6 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 tests/utils.py diff --git a/docs/changelog.rst b/docs/changelog.rst index cd99b73e..0da97e90 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,8 @@ Changelog Development =========== -- (Fill this out as you fix issues and develop you features). +- (Fill this out as you fix issues and develop your features). +- Fixed using sets in field choices #1481 - POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476 - POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476 - Fixed connecting to a replica set with PyMongo 2.x #1436 diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index e656dee0..d41ae7e6 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -150,7 +150,7 @@ arguments can be set on all fields: .. note:: If set, this field is also accessible through the `pk` field. :attr:`choices` (Default: None) - An iterable (e.g. a list or tuple) of choices to which the value of this + An iterable (e.g. list, tuple or set) of choices to which the value of this field should be limited. Can be either be a nested tuples of value (stored in mongo) and a @@ -214,8 +214,8 @@ document class as the first argument:: Dictionary Fields ----------------- -Often, an embedded document may be used instead of a dictionary – generally -embedded documents are recommended as dictionaries don’t support validation +Often, an embedded document may be used instead of a dictionary – generally +embedded documents are recommended as dictionaries don’t support validation or custom field types. However, sometimes you will not know the structure of what you want to store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate:: diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 9ba9dc9a..5658b185 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -193,7 +193,8 @@ class BaseField(object): EmbeddedDocument = _import_class('EmbeddedDocument') choice_list = self.choices - if isinstance(choice_list[0], (list, tuple)): + if isinstance(next(iter(choice_list)), (list, tuple)): + # next(iter) is useful for sets choice_list = [k for k, _ in choice_list] # Choices which are other types of Documents diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5c83d58c..f24bcae4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- -import six -from nose.plugins.skip import SkipTest - import datetime import unittest import uuid import math import itertools import re + +from nose.plugins.skip import SkipTest import six try: @@ -27,21 +26,13 @@ from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, _document_registry) -from mongoengine.errors import NotRegistered, DoesNotExist + +from tests.utils import MongoDBTestCase __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") -class FieldTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - self.db = get_db() - - def tearDown(self): - self.db.drop_collection('fs.files') - self.db.drop_collection('fs.chunks') - self.db.drop_collection('mongoengine.counters') +class FieldTest(MongoDBTestCase): def test_default_values_nothing_set(self): """Ensure that default field values are used when creating a document. @@ -3186,26 +3177,42 @@ class FieldTest(unittest.TestCase): att.delete() self.assertEqual(0, Attachment.objects.count()) - def test_choices_validation(self): - """Ensure that value is in a container of allowed values. + def test_choices_allow_using_sets_as_choices(self): + """Ensure that sets can be used when setting choices """ class Shirt(Document): - size = StringField(max_length=3, choices=( - ('S', 'Small'), ('M', 'Medium'), ('L', 'Large'), - ('XL', 'Extra Large'), ('XXL', 'Extra Extra Large'))) + size = StringField(choices={'M', 'L'}) - Shirt.drop_collection() + Shirt(size='M').validate() + + def test_choices_validation_allow_no_value(self): + """Ensure that .validate passes and no value was provided + for a field setup with choices + """ + class Shirt(Document): + size = StringField(choices=('S', 'M')) shirt = Shirt() shirt.validate() - shirt.size = "S" + def test_choices_validation_accept_possible_value(self): + """Ensure that value is in a container of allowed values. + """ + class Shirt(Document): + size = StringField(choices=('S', 'M')) + + shirt = Shirt(size='S') shirt.validate() - shirt.size = "XS" - self.assertRaises(ValidationError, shirt.validate) + def test_choices_validation_reject_unknown_value(self): + """Ensure that unallowed value are rejected upon validation + """ + class Shirt(Document): + size = StringField(choices=('S', 'M')) - Shirt.drop_collection() + shirt = Shirt(size="XS") + with self.assertRaises(ValidationError): + shirt.validate() def test_choices_validation_documents(self): """ @@ -4420,7 +4427,8 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): my_list = ListField(EmbeddedDocumentField(EmbeddedWithUnique)) A(my_list=[]).save() - self.assertRaises(NotUniqueError, lambda: A(my_list=[]).save()) + with self.assertRaises(NotUniqueError): + A(my_list=[]).save() class EmbeddedWithSparseUnique(EmbeddedDocument): number = IntField(unique=True, sparse=True) @@ -4431,6 +4439,9 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): B(my_list=[]).save() B(my_list=[]).save() + A.drop_collection() + B.drop_collection() + def test_filtered_delete(self): """ Tests the delete method of a List of Embedded Documents diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index b266a5e5..8364d5ef 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -18,15 +18,13 @@ try: except ImportError: HAS_PIL = False +from tests.utils import MongoDBTestCase + TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png') TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), 'mongodb_leaf.png') -class FileTest(unittest.TestCase): - - def setUp(self): - connect(db='mongoenginetest') - self.db = get_db() +class FileTest(MongoDBTestCase): def tearDown(self): self.db.drop_collection('fs.files') diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 00000000..128bbff0 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,22 @@ +import unittest + +from mongoengine import connect +from mongoengine.connection import get_db + +MONGO_TEST_DB = 'mongoenginetest' + + +class MongoDBTestCase(unittest.TestCase): + """Base class for tests that need a mongodb connection + db is being dropped automatically + """ + + @classmethod + def setUpClass(cls): + cls._connection = connect(db=MONGO_TEST_DB) + cls._connection.drop_database(MONGO_TEST_DB) + cls.db = get_db() + + @classmethod + def tearDownClass(cls): + cls._connection.drop_database(MONGO_TEST_DB) From 3fe8031cf3d607f960664c971fff328bd98c19db Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 22 Feb 2017 12:44:05 -0500 Subject: [PATCH 039/268] fix EmbeddedDocumentListFieldTestCase --- tests/fields/fields.py | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index f24bcae4..e6898e1b 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -218,9 +218,9 @@ class FieldTest(MongoDBTestCase): self.assertTrue(isinstance(ret.comp_dt_fld, datetime.datetime)) def test_not_required_handles_none_from_database(self): - """Ensure that every fields can handle null values from the database. + """Ensure that every field can handle null values from the + database. """ - class HandleNoneFields(Document): str_fld = StringField(required=True) int_fld = IntField(required=True) @@ -4031,12 +4031,13 @@ class FieldTest(MongoDBTestCase): self.assertTrue(isinstance(doc.some_long, six.integer_types)) -class EmbeddedDocumentListFieldTestCase(unittest.TestCase): - - @classmethod - def setUpClass(cls): - cls.db = connect(db='EmbeddedDocumentListFieldTestCase') +class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): + def setUp(self): + """ + Create two BlogPost entries in the database, each with + several EmbeddedDocuments. + """ class Comments(EmbeddedDocument): author = StringField() message = StringField() @@ -4044,14 +4045,11 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): class BlogPost(Document): comments = EmbeddedDocumentListField(Comments) - cls.Comments = Comments - cls.BlogPost = BlogPost + BlogPost.drop_collection() + + self.Comments = Comments + self.BlogPost = BlogPost - def setUp(self): - """ - Create two BlogPost entries in the database, each with - several EmbeddedDocuments. - """ self.post1 = self.BlogPost(comments=[ self.Comments(author='user1', message='message1'), self.Comments(author='user2', message='message1') @@ -4063,13 +4061,6 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): self.Comments(author='user3', message='message1') ]).save() - def tearDown(self): - self.BlogPost.drop_collection() - - @classmethod - def tearDownClass(cls): - cls.db.drop_database('EmbeddedDocumentListFieldTestCase') - def test_no_keyword_filter(self): """ Tests the filter method of a List of Embedded Documents @@ -4436,12 +4427,12 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): class B(Document): my_list = ListField(EmbeddedDocumentField(EmbeddedWithSparseUnique)) - B(my_list=[]).save() - B(my_list=[]).save() - A.drop_collection() B.drop_collection() + B(my_list=[]).save() + B(my_list=[]).save() + def test_filtered_delete(self): """ Tests the delete method of a List of Embedded Documents @@ -4478,6 +4469,8 @@ class EmbeddedDocumentListFieldTestCase(unittest.TestCase): a_field = IntField() c_field = IntField(custom_data=custom_data) + CustomData.drop_collection() + a1 = CustomData(a_field=1, c_field=2).save() self.assertEqual(2, a1.c_field) self.assertFalse(hasattr(a1.c_field, 'custom_data')) From 3f3166679659ac19b0651fb92d321153568a53b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Fri, 24 Feb 2017 16:18:34 -0500 Subject: [PATCH 040/268] Fix the exception message when validating unicode URLs (#1486) --- mongoengine/fields.py | 4 ++-- tests/fields/fields.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0ea7d3b6..11425095 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -139,12 +139,12 @@ class URLField(StringField): # Check first if the scheme is valid scheme = value.split('://')[0].lower() if scheme not in self.schemes: - self.error('Invalid scheme {} in URL: {}'.format(scheme, value)) + self.error(u'Invalid scheme {} in URL: {}'.format(scheme, value)) return # Then check full URL if not self.url_regex.match(value): - self.error('Invalid URL: {}'.format(value)) + self.error(u'Invalid URL: {}'.format(value)) return diff --git a/tests/fields/fields.py b/tests/fields/fields.py index e6898e1b..318c0c59 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -341,11 +341,12 @@ class FieldTest(MongoDBTestCase): person.validate() def test_url_validation(self): - """Ensure that URLFields validate urls properly. - """ + """Ensure that URLFields validate urls properly.""" class Link(Document): url = URLField() + Link.drop_collection() + link = Link() link.url = 'google' self.assertRaises(ValidationError, link.validate) @@ -353,6 +354,27 @@ class FieldTest(MongoDBTestCase): link.url = 'http://www.google.com:8080' link.validate() + def test_unicode_url_validation(self): + """Ensure unicode URLs are validated properly.""" + class Link(Document): + url = URLField() + + Link.drop_collection() + + link = Link() + link.url = u'http://привет.com' + + # TODO fix URL validation - this *IS* a valid URL + # For now we just want to make sure that the error message is correct + try: + link.validate() + self.assertTrue(False) + except ValidationError as e: + self.assertEqual( + unicode(e), + u"ValidationError (Link:None) (Invalid URL: http://\u043f\u0440\u0438\u0432\u0435\u0442.com: ['url'])" + ) + def test_url_scheme_validation(self): """Ensure that URLFields validate urls with specific schemes properly. """ From e93a95d0cb92d91225bdf30abb8e661503fde6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sat, 25 Feb 2017 14:09:10 -0500 Subject: [PATCH 041/268] Test and document controlling the size of the connection pool (#1489) --- mongoengine/connection.py | 9 +++++++-- tests/test_connection.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index c7c16ca3..7eae810f 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -51,7 +51,9 @@ def register_connection(alias, name=None, host=None, port=None, MONGODB-CR (MongoDB Challenge Response protocol) for older servers. :param is_mock: explicitly use mongomock for this connection (can also be done by using `mongomock://` as db host prefix) - :param kwargs: allow ad-hoc parameters to be passed into the pymongo driver + :param kwargs: ad-hoc parameters to be passed into the pymongo driver, + for example maxpoolsize, tz_aware, etc. See the documentation + for pymongo's `MongoClient` for a full list. .. versionchanged:: 0.10.6 - added mongomock support """ @@ -241,9 +243,12 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs): running on the default port on localhost. If authentication is needed, provide username and password arguments as well. - Multiple databases are supported by using aliases. Provide a separate + Multiple databases are supported by using aliases. Provide a separate `alias` to connect to a different instance of :program:`mongod`. + See the docstring for `register_connection` for more details about all + supported kwargs. + .. versionchanged:: 0.6 - added multiple database support. """ if alias not in _connections: diff --git a/tests/test_connection.py b/tests/test_connection.py index cc75f1e7..a1d3bfb6 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -285,8 +285,7 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) def test_connection_kwargs(self): - """Ensure that connection kwargs get passed to pymongo. - """ + """Ensure that connection kwargs get passed to pymongo.""" connect('mongoenginetest', alias='t1', tz_aware=True) conn = get_connection('t1') @@ -296,6 +295,32 @@ class ConnectionTest(unittest.TestCase): conn = get_connection('t2') self.assertFalse(get_tz_awareness(conn)) + def test_connection_pool_via_kwarg(self): + """Ensure we can specify a max connection pool size using + a connection kwarg. + """ + # Use "max_pool_size" or "maxpoolsize" depending on PyMongo version + # (former was changed to the latter as described in + # https://jira.mongodb.org/browse/PYTHON-854). + # TODO remove once PyMongo < 3.0 support is dropped + if pymongo.version_tuple[0] >= 3: + pool_size_kwargs = {'maxpoolsize': 100} + else: + pool_size_kwargs = {'max_pool_size': 100} + + conn = connect('mongoenginetest', alias='max_pool_size_via_kwarg', **pool_size_kwargs) + self.assertEqual(conn.max_pool_size, 100) + + def test_connection_pool_via_uri(self): + """Ensure we can specify a max connection pool size using + an option in a connection URI. + """ + if pymongo.version_tuple[0] == 2 and pymongo.version_tuple[1] < 9: + raise SkipTest('maxpoolsize as a URI option is only supported in PyMongo v2.9+') + + conn = connect(host='mongodb://localhost/test?maxpoolsize=100', alias='max_pool_size_via_uri') + self.assertEqual(conn.max_pool_size, 100) + def test_write_concern(self): """Ensure write concern can be specified in connect() via a kwarg or as part of the connection URI. From 2bedb36d7f025bdda72fde0b927eafa10143f823 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 26 Feb 2017 21:52:43 +0200 Subject: [PATCH 042/268] Test against multiple MongoDB versions in Travis (#1074) --- .install_mongodb_on_travis.sh | 23 ++ .travis.yml | 67 +++-- CONTRIBUTING.rst | 29 ++- README.rst | 45 ++-- tests/document/indexes.py | 21 +- tests/queryset/geo.py | 452 +++++++++++++++++++--------------- tests/queryset/queryset.py | 82 +++--- tests/test_connection.py | 26 +- tests/utils.py | 58 ++++- tox.ini | 13 +- 10 files changed, 475 insertions(+), 341 deletions(-) create mode 100644 .install_mongodb_on_travis.sh diff --git a/.install_mongodb_on_travis.sh b/.install_mongodb_on_travis.sh new file mode 100644 index 00000000..8563ae74 --- /dev/null +++ b/.install_mongodb_on_travis.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 + +if [ "$MONGODB" = "2.4" ]; then + echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list + sudo apt-get update + sudo apt-get install mongodb-10gen=2.4.14 + sudo service mongodb start +elif [ "$MONGODB" = "2.6" ]; then + echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list + sudo apt-get update + sudo apt-get install mongodb-org-server=2.6.12 + # service should be started automatically +elif [ "$MONGODB" = "3.0" ]; then + echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list + sudo apt-get update + sudo apt-get install mongodb-org-server=3.0.14 + # service should be started automatically +else + echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0." + exit 1 +fi; diff --git a/.travis.yml b/.travis.yml index cb6c97e6..47448950 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,48 @@ +# For full coverage, we'd have to test all supported Python, MongoDB, and +# PyMongo combinations. However, that would result in an overly long build +# with a very large number of jobs, hence we only test a subset of all the +# combinations: +# * MongoDB v2.4 & v3.0 are only tested against Python v2.7 & v3.5. +# * MongoDB v2.4 is tested against PyMongo v2.7 & v3.x. +# * MongoDB v3.0 is tested against PyMongo v3.x. +# * MongoDB v2.6 is currently the "main" version tested against Python v2.7, +# v3.5, PyPy & PyPy3, and PyMongo v2.7, v2.8 & v3.x. +# +# Reminder: Update README.rst if you change MongoDB versions we test. + language: python python: -- '2.7' -- '3.3' -- '3.4' -- '3.5' +- 2.7 +- 3.5 - pypy - pypy3 env: -- PYMONGO=2.7 -- PYMONGO=2.8 -- PYMONGO=3.0 -- PYMONGO=dev +- MONGODB=2.6 PYMONGO=2.7 +- MONGODB=2.6 PYMONGO=2.8 +- MONGODB=2.6 PYMONGO=3.0 matrix: + # Finish the build as soon as one job fails fast_finish: true + include: + - python: 2.7 + env: MONGODB=2.4 PYMONGO=2.7 + - python: 2.7 + env: MONGODB=2.4 PYMONGO=3.0 + - python: 2.7 + env: MONGODB=3.0 PYMONGO=3.0 + - python: 3.5 + env: MONGODB=2.4 PYMONGO=2.7 + - python: 3.5 + env: MONGODB=2.4 PYMONGO=3.0 + - python: 3.5 + env: MONGODB=3.0 PYMONGO=3.0 + before_install: -- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 -- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | - sudo tee /etc/apt/sources.list.d/mongodb.list -- travis_retry sudo apt-get update -- travis_retry sudo apt-get install mongodb-org-server +- bash .install_mongodb_on_travis.sh install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev @@ -30,14 +50,17 @@ install: python-tk - travis_retry pip install --upgrade pip - travis_retry pip install coveralls -- travis_retry pip install flake8 +- travis_retry pip install flake8 flake8-import-order - travis_retry pip install tox>=1.9 - travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32) - travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test +# Cache dependencies installed via pip +cache: pip + # Run flake8 for py27 before_script: -- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e flake8; fi +- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi script: - tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage @@ -45,22 +68,34 @@ script: # For now only submit coveralls for Python v2.7. Python v3.x currently shows # 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible # code in a separate dir and runs tests on that. -after_script: +after_success: - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi notifications: irc: irc.freenode.org#mongoengine +# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z) branches: only: - master - /^v.*$/ +# Whenever a new release is created via GitHub, publish it on PyPI. deploy: provider: pypi user: the_drow password: secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek= + + # create a source distribution and a pure python wheel for faster installs + distributions: "sdist bdist_wheel" + + # only deploy on tagged commits (aka GitHub releases) and only for the + # parent repo's builds running Python 2.7 along with dev PyMongo (we run + # Travis against many different Python and PyMongo versions and we don't + # want the deploy to occur multiple times). on: tags: true repo: MongoEngine/mongoengine + condition: "$PYMONGO = 3.0" + python: 2.7 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 2668499c..5707886b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -29,19 +29,20 @@ Style Guide ----------- MongoEngine aims to follow `PEP8 `_ -including 4 space indents. When possible we try to stick to 79 character line limits. -However, screens got bigger and an ORM has a strong focus on readability and -if it can help, we accept 119 as maximum line length, in a similar way as -`django does `_ +including 4 space indents. When possible we try to stick to 79 character line +limits. However, screens got bigger and an ORM has a strong focus on +readability and if it can help, we accept 119 as maximum line length, in a +similar way as `django does +`_ Testing ------- All tests are run on `Travis `_ -and any pull requests are automatically tested by Travis. Any pull requests -without tests will take longer to be integrated and might be refused. +and any pull requests are automatically tested. Any pull requests without +tests will take longer to be integrated and might be refused. -You may also submit a simple failing test as a PullRequest if you don't know +You may also submit a simple failing test as a pull request if you don't know how to fix it, it will be easier for other people to work on it and it may get fixed faster. @@ -49,13 +50,18 @@ General Guidelines ------------------ - Avoid backward breaking changes if at all possible. +- If you *have* to introduce a breaking change, make it very clear in your + pull request's description. Also, describe how users of this package + should adapt to the breaking change in docs/upgrade.rst. - Write inline documentation for new classes and methods. - Write tests and make sure they pass (make sure you have a mongod running on the default port, then execute ``python setup.py nosetests`` from the cmd line to run the test suite). -- Ensure tests pass on every Python and PyMongo versions. - You can test on these versions locally by executing ``tox`` -- Add enhancements or problematic bug fixes to docs/changelog.rst +- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions. + You can test various Python and PyMongo versions locally by executing + ``tox``. For different MongoDB versions, you can rely on our automated + Travis tests. +- Add enhancements or problematic bug fixes to docs/changelog.rst. - Add yourself to AUTHORS :) Documentation @@ -69,3 +75,6 @@ just make your changes to the inline documentation of the appropriate branch and submit a `pull request `_. You might also use the github `Edit `_ button. + +If you want to test your documentation changes locally, you need to install +the ``sphinx`` package. diff --git a/README.rst b/README.rst index adfa0c71..e1e2aef6 100644 --- a/README.rst +++ b/README.rst @@ -19,23 +19,31 @@ MongoEngine About ===== MongoEngine is a Python Object-Document Mapper for working with MongoDB. -Documentation available at https://mongoengine-odm.readthedocs.io - there is currently -a `tutorial `_, a `user guide -`_ and an `API reference -`_. +Documentation is available at https://mongoengine-odm.readthedocs.io - there +is currently a `tutorial `_, +a `user guide `_, and +an `API reference `_. + +Supported MongoDB Versions +========================== +MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future +versions should be supported as well, but aren't actively tested at the moment. +Make sure to open an issue or submit a pull request if you experience any +problems with MongoDB v3.2+. Installation ============ We recommend the use of `virtualenv `_ and of `pip `_. You can then use ``pip install -U mongoengine``. -You may also have `setuptools `_ and thus -you can use ``easy_install -U mongoengine``. Otherwise, you can download the +You may also have `setuptools `_ +and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the source from `GitHub `_ and run ``python setup.py install``. Dependencies ============ -All of the dependencies can easily be installed via `pip `_. At the very least, you'll need these two packages to use MongoEngine: +All of the dependencies can easily be installed via `pip `_. +At the very least, you'll need these two packages to use MongoEngine: - pymongo>=2.7.1 - six>=1.10.0 @@ -48,10 +56,6 @@ If you need to use an ``ImageField`` or ``ImageGridFsProxy``: - Pillow>=2.0.0 -If you want to generate the documentation (e.g. to contribute to it): - -- sphinx - Examples ======== Some simple examples of what MongoEngine code looks like: @@ -110,11 +114,11 @@ Some simple examples of what MongoEngine code looks like: Tests ===== To run the test suite, ensure you are running a local instance of MongoDB on -the standard port and have ``nose`` installed. Then, run: ``python setup.py nosetests``. +the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``. -To run the test suite on every supported Python version and every supported PyMongo version, -you can use ``tox``. -tox and each supported Python version should be installed in your environment: +To run the test suite on every supported Python and PyMongo version, you can +use ``tox``. You'll need to make sure you have each supported Python version +installed in your environment and then: .. code-block:: shell @@ -123,13 +127,16 @@ tox and each supported Python version should be installed in your environment: # Run the test suites $ tox -If you wish to run one single or selected tests, use the nosetest convention. It will find the folder, -eventually the file, go to the TestClass specified after the colon and eventually right to the single test. -Also use the -s argument if you want to print out whatever or access pdb while testing. +If you wish to run a subset of tests, use the nosetests convention: .. code-block:: shell - $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest.test_cls_field -s + # Run all the tests in a particular test file + $ python setup.py nosetests --tests tests/fields/fields.py + # Run only particular test class in that file + $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest + # Use the -s option if you want to print some debug statements or use pdb + $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s Community ========= diff --git a/tests/document/indexes.py b/tests/document/indexes.py index af93e7db..5d219eda 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -2,14 +2,14 @@ import unittest import sys - -import pymongo - from nose.plugins.skip import SkipTest from datetime import datetime +import pymongo from mongoengine import * -from mongoengine.connection import get_db, get_connection +from mongoengine.connection import get_db + +from tests.utils import get_mongodb_version, needs_mongodb_v26 __all__ = ("IndexesTest", ) @@ -494,8 +494,7 @@ class IndexesTest(unittest.TestCase): obj = Test(a=1) obj.save() - connection = get_connection() - IS_MONGODB_3 = connection.server_info()['versionArray'][0] >= 3 + IS_MONGODB_3 = get_mongodb_version()[0] >= 3 # Need to be explicit about covered indexes as mongoDB doesn't know if # the documents returned might have more keys in that here. @@ -733,14 +732,6 @@ class IndexesTest(unittest.TestCase): Log.drop_collection() - if pymongo.version_tuple[0] < 2 and pymongo.version_tuple[1] < 3: - raise SkipTest('pymongo needs to be 2.3 or higher for this test') - - connection = get_connection() - version_array = connection.server_info()['versionArray'] - if version_array[0] < 2 and version_array[1] < 2: - raise SkipTest('MongoDB needs to be 2.2 or higher for this test') - # Indexes are lazy so use list() to perform query list(Log.objects) info = Log.objects._collection.index_information() @@ -874,8 +865,8 @@ class IndexesTest(unittest.TestCase): info['provider_ids.foo_1_provider_ids.bar_1']['key']) self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse']) + @needs_mongodb_v26 def test_text_indexes(self): - class Book(Document): title = DictField() meta = { diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index d10c51cd..51a32382 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -1,105 +1,139 @@ -from datetime import datetime, timedelta +import datetime import unittest -from pymongo.errors import OperationFailure from mongoengine import * -from mongoengine.connection import get_connection -from nose.plugins.skip import SkipTest + +from tests.utils import MongoDBTestCase, needs_mongodb_v3 __all__ = ("GeoQueriesTest",) -class GeoQueriesTest(unittest.TestCase): +class GeoQueriesTest(MongoDBTestCase): - def setUp(self): - connect(db='mongoenginetest') - - def test_geospatial_operators(self): - """Ensure that geospatial queries are working. - """ + def _create_event_data(self, point_field_class=GeoPointField): + """Create some sample data re-used in many of the tests below.""" class Event(Document): title = StringField() date = DateTimeField() - location = GeoPointField() + location = point_field_class() def __unicode__(self): return self.title + self.Event = Event + Event.drop_collection() - event1 = Event(title="Coltrane Motion @ Double Door", - date=datetime.now() - timedelta(days=1), - location=[-87.677137, 41.909889]).save() - event2 = Event(title="Coltrane Motion @ Bottom of the Hill", - date=datetime.now() - timedelta(days=10), - location=[-122.4194155, 37.7749295]).save() - event3 = Event(title="Coltrane Motion @ Empty Bottle", - date=datetime.now(), - location=[-87.686638, 41.900474]).save() + event1 = Event.objects.create( + title="Coltrane Motion @ Double Door", + date=datetime.datetime.now() - datetime.timedelta(days=1), + location=[-87.677137, 41.909889]) + event2 = Event.objects.create( + title="Coltrane Motion @ Bottom of the Hill", + date=datetime.datetime.now() - datetime.timedelta(days=10), + location=[-122.4194155, 37.7749295]) + event3 = Event.objects.create( + title="Coltrane Motion @ Empty Bottle", + date=datetime.datetime.now(), + location=[-87.686638, 41.900474]) + + return event1, event2, event3 + + def test_near(self): + """Make sure the "near" operator works.""" + event1, event2, event3 = self._create_event_data() # find all events "near" pitchfork office, chicago. # note that "near" will show the san francisco event, too, # although it sorts to last. - events = Event.objects(location__near=[-87.67892, 41.9120459]) + events = self.Event.objects(location__near=[-87.67892, 41.9120459]) self.assertEqual(events.count(), 3) self.assertEqual(list(events), [event1, event3, event2]) + # ensure ordering is respected by "near" + events = self.Event.objects(location__near=[-87.67892, 41.9120459]) + events = events.order_by("-date") + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event3, event1, event2]) + + def test_near_and_max_distance(self): + """Ensure the "max_distance" operator works alongside the "near" + operator. + """ + event1, event2, event3 = self._create_event_data() + + # find events within 10 degrees of san francisco + point = [-122.415579, 37.7566023] + events = self.Event.objects(location__near=point, + location__max_distance=10) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + # $minDistance was added in MongoDB v2.6, but continued being buggy + # until v3.0; skip for older versions + @needs_mongodb_v3 + def test_near_and_min_distance(self): + """Ensure the "min_distance" operator works alongside the "near" + operator. + """ + event1, event2, event3 = self._create_event_data() + + # find events at least 10 degrees away of san francisco + point = [-122.415579, 37.7566023] + events = self.Event.objects(location__near=point, + location__min_distance=10) + self.assertEqual(events.count(), 2) + + def test_within_distance(self): + """Make sure the "within_distance" operator works.""" + event1, event2, event3 = self._create_event_data() + # find events within 5 degrees of pitchfork office, chicago point_and_distance = [[-87.67892, 41.9120459], 5] - events = Event.objects(location__within_distance=point_and_distance) + events = self.Event.objects( + location__within_distance=point_and_distance) self.assertEqual(events.count(), 2) events = list(events) self.assertTrue(event2 not in events) self.assertTrue(event1 in events) self.assertTrue(event3 in events) - # ensure ordering is respected by "near" - events = Event.objects(location__near=[-87.67892, 41.9120459]) - events = events.order_by("-date") - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event3, event1, event2]) - - # find events within 10 degrees of san francisco - point = [-122.415579, 37.7566023] - events = Event.objects(location__near=point, location__max_distance=10) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event2) - - # find events at least 10 degrees away of san francisco - point = [-122.415579, 37.7566023] - events = Event.objects(location__near=point, location__min_distance=10) - # The following real test passes on MongoDB 3 but minDistance seems - # buggy on older MongoDB versions - if get_connection().server_info()['versionArray'][0] > 2: - self.assertEqual(events.count(), 2) - else: - self.assertTrue(events.count() >= 2) - # find events within 10 degrees of san francisco point_and_distance = [[-122.415579, 37.7566023], 10] - events = Event.objects(location__within_distance=point_and_distance) + events = self.Event.objects( + location__within_distance=point_and_distance) self.assertEqual(events.count(), 1) self.assertEqual(events[0], event2) # find events within 1 degree of greenpoint, broolyn, nyc, ny point_and_distance = [[-73.9509714, 40.7237134], 1] - events = Event.objects(location__within_distance=point_and_distance) + events = self.Event.objects( + location__within_distance=point_and_distance) self.assertEqual(events.count(), 0) # ensure ordering is respected by "within_distance" point_and_distance = [[-87.67892, 41.9120459], 10] - events = Event.objects(location__within_distance=point_and_distance) + events = self.Event.objects( + location__within_distance=point_and_distance) events = events.order_by("-date") self.assertEqual(events.count(), 2) self.assertEqual(events[0], event3) + def test_within_box(self): + """Ensure the "within_box" operator works.""" + event1, event2, event3 = self._create_event_data() + # check that within_box works box = [(-125.0, 35.0), (-100.0, 40.0)] - events = Event.objects(location__within_box=box) + events = self.Event.objects(location__within_box=box) self.assertEqual(events.count(), 1) self.assertEqual(events[0].id, event2.id) + def test_within_polygon(self): + """Ensure the "within_polygon" operator works.""" + event1, event2, event3 = self._create_event_data() + polygon = [ (-87.694445, 41.912114), (-87.69084, 41.919395), @@ -107,7 +141,7 @@ class GeoQueriesTest(unittest.TestCase): (-87.654276, 41.911731), (-87.656164, 41.898061), ] - events = Event.objects(location__within_polygon=polygon) + events = self.Event.objects(location__within_polygon=polygon) self.assertEqual(events.count(), 1) self.assertEqual(events[0].id, event1.id) @@ -116,13 +150,151 @@ class GeoQueriesTest(unittest.TestCase): (-1.225891, 52.792797), (-4.40094, 53.389881) ] - events = Event.objects(location__within_polygon=polygon2) + events = self.Event.objects(location__within_polygon=polygon2) self.assertEqual(events.count(), 0) - def test_geo_spatial_embedded(self): + def test_2dsphere_near(self): + """Make sure the "near" operator works with a PointField, which + corresponds to a 2dsphere index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + # find all events "near" pitchfork office, chicago. + # note that "near" will show the san francisco event, too, + # although it sorts to last. + events = self.Event.objects(location__near=[-87.67892, 41.9120459]) + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event1, event3, event2]) + + # ensure ordering is respected by "near" + events = self.Event.objects(location__near=[-87.67892, 41.9120459]) + events = events.order_by("-date") + self.assertEqual(events.count(), 3) + self.assertEqual(list(events), [event3, event1, event2]) + + def test_2dsphere_near_and_max_distance(self): + """Ensure the "max_distance" operator works alongside the "near" + operator with a 2dsphere index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + + # find events within 10km of san francisco + point = [-122.415579, 37.7566023] + events = self.Event.objects(location__near=point, + location__max_distance=10000) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + # find events within 1km of greenpoint, broolyn, nyc, ny + events = self.Event.objects(location__near=[-73.9509714, 40.7237134], + location__max_distance=1000) + self.assertEqual(events.count(), 0) + + # ensure ordering is respected by "near" + events = self.Event.objects( + location__near=[-87.67892, 41.9120459], + location__max_distance=10000 + ).order_by("-date") + self.assertEqual(events.count(), 2) + self.assertEqual(events[0], event3) + + def test_2dsphere_geo_within_box(self): + """Ensure the "geo_within_box" operator works with a 2dsphere + index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + + # check that within_box works + box = [(-125.0, 35.0), (-100.0, 40.0)] + events = self.Event.objects(location__geo_within_box=box) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event2.id) + + def test_2dsphere_geo_within_polygon(self): + """Ensure the "geo_within_polygon" operator works with a + 2dsphere index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + + polygon = [ + (-87.694445, 41.912114), + (-87.69084, 41.919395), + (-87.681742, 41.927186), + (-87.654276, 41.911731), + (-87.656164, 41.898061), + ] + events = self.Event.objects(location__geo_within_polygon=polygon) + self.assertEqual(events.count(), 1) + self.assertEqual(events[0].id, event1.id) + + polygon2 = [ + (-1.742249, 54.033586), + (-1.225891, 52.792797), + (-4.40094, 53.389881) + ] + events = self.Event.objects(location__geo_within_polygon=polygon2) + self.assertEqual(events.count(), 0) + + # $minDistance was added in MongoDB v2.6, but continued being buggy + # until v3.0; skip for older versions + @needs_mongodb_v3 + def test_2dsphere_near_and_min_max_distance(self): + """Ensure "min_distace" and "max_distance" operators work well + together with the "near" operator in a 2dsphere index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + + # ensure min_distance and max_distance combine well + events = self.Event.objects( + location__near=[-87.67892, 41.9120459], + location__min_distance=1000, + location__max_distance=10000 + ).order_by("-date") + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event3) + + # ensure ordering is respected by "near" with "min_distance" + events = self.Event.objects( + location__near=[-87.67892, 41.9120459], + location__min_distance=10000 + ).order_by("-date") + self.assertEqual(events.count(), 1) + self.assertEqual(events[0], event2) + + def test_2dsphere_geo_within_center(self): + """Make sure the "geo_within_center" operator works with a + 2dsphere index. + """ + event1, event2, event3 = self._create_event_data( + point_field_class=PointField + ) + + # find events within 5 degrees of pitchfork office, chicago + point_and_distance = [[-87.67892, 41.9120459], 2] + events = self.Event.objects( + location__geo_within_center=point_and_distance) + self.assertEqual(events.count(), 2) + events = list(events) + self.assertTrue(event2 not in events) + self.assertTrue(event1 in events) + self.assertTrue(event3 in events) + + def _test_embedded(self, point_field_class): + """Helper test method ensuring given point field class works + well in an embedded document. + """ class Venue(EmbeddedDocument): - location = GeoPointField() + location = point_field_class() name = StringField() class Event(Document): @@ -148,16 +320,18 @@ class GeoQueriesTest(unittest.TestCase): self.assertEqual(events.count(), 3) self.assertEqual(list(events), [event1, event3, event2]) - def test_spherical_geospatial_operators(self): - """Ensure that spherical geospatial queries are working - """ - # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039 - connection = get_connection() - info = connection.test.command('buildInfo') - mongodb_version = tuple([int(i) for i in info['version'].split('.')]) - if mongodb_version < (2, 6, 4): - raise SkipTest("Need MongoDB version 2.6.4+") + def test_geo_spatial_embedded(self): + """Make sure GeoPointField works properly in an embedded document.""" + self._test_embedded(point_field_class=GeoPointField) + def test_2dsphere_point_embedded(self): + """Make sure PointField works properly in an embedded document.""" + self._test_embedded(point_field_class=PointField) + + # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039 + @needs_mongodb_v3 + def test_spherical_geospatial_operators(self): + """Ensure that spherical geospatial queries are working.""" class Point(Document): location = GeoPointField() @@ -177,7 +351,10 @@ class GeoQueriesTest(unittest.TestCase): # Same behavior for _within_spherical_distance points = Point.objects( - location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius] + location__within_spherical_distance=[ + [-122, 37.5], + 60 / earth_radius + ] ) self.assertEqual(points.count(), 2) @@ -194,14 +371,9 @@ class GeoQueriesTest(unittest.TestCase): # Test query works with min_distance, being farer from one point points = Point.objects(location__near_sphere=[-122, 37.8], location__min_distance=60 / earth_radius) - # The following real test passes on MongoDB 3 but minDistance seems - # buggy on older MongoDB versions - if get_connection().server_info()['versionArray'][0] > 2: - self.assertEqual(points.count(), 1) - far_point = points.first() - self.assertNotEqual(close_point, far_point) - else: - self.assertTrue(points.count() >= 1) + self.assertEqual(points.count(), 1) + far_point = points.first() + self.assertNotEqual(close_point, far_point) # Finds both points, but orders the north point first because it's # closer to the reference point to the north. @@ -220,141 +392,15 @@ class GeoQueriesTest(unittest.TestCase): # Finds only one point because only the first point is within 60km of # the reference point to the south. points = Point.objects( - location__within_spherical_distance=[[-122, 36.5], 60/earth_radius]) + location__within_spherical_distance=[ + [-122, 36.5], + 60 / earth_radius + ] + ) self.assertEqual(points.count(), 1) self.assertEqual(points[0].id, south_point.id) - def test_2dsphere_point(self): - - class Event(Document): - title = StringField() - date = DateTimeField() - location = PointField() - - def __unicode__(self): - return self.title - - Event.drop_collection() - - event1 = Event(title="Coltrane Motion @ Double Door", - date=datetime.now() - timedelta(days=1), - location=[-87.677137, 41.909889]) - event1.save() - event2 = Event(title="Coltrane Motion @ Bottom of the Hill", - date=datetime.now() - timedelta(days=10), - location=[-122.4194155, 37.7749295]).save() - event3 = Event(title="Coltrane Motion @ Empty Bottle", - date=datetime.now(), - location=[-87.686638, 41.900474]).save() - - # find all events "near" pitchfork office, chicago. - # note that "near" will show the san francisco event, too, - # although it sorts to last. - events = Event.objects(location__near=[-87.67892, 41.9120459]) - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event1, event3, event2]) - - # find events within 5 degrees of pitchfork office, chicago - point_and_distance = [[-87.67892, 41.9120459], 2] - events = Event.objects(location__geo_within_center=point_and_distance) - self.assertEqual(events.count(), 2) - events = list(events) - self.assertTrue(event2 not in events) - self.assertTrue(event1 in events) - self.assertTrue(event3 in events) - - # ensure ordering is respected by "near" - events = Event.objects(location__near=[-87.67892, 41.9120459]) - events = events.order_by("-date") - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event3, event1, event2]) - - # find events within 10km of san francisco - point = [-122.415579, 37.7566023] - events = Event.objects(location__near=point, location__max_distance=10000) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event2) - - # find events within 1km of greenpoint, broolyn, nyc, ny - events = Event.objects(location__near=[-73.9509714, 40.7237134], location__max_distance=1000) - self.assertEqual(events.count(), 0) - - # ensure ordering is respected by "near" - events = Event.objects(location__near=[-87.67892, 41.9120459], - location__max_distance=10000).order_by("-date") - self.assertEqual(events.count(), 2) - self.assertEqual(events[0], event3) - - # ensure min_distance and max_distance combine well - events = Event.objects(location__near=[-87.67892, 41.9120459], - location__min_distance=1000, - location__max_distance=10000).order_by("-date") - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event3) - - # ensure ordering is respected by "near" - events = Event.objects(location__near=[-87.67892, 41.9120459], - # location__min_distance=10000 - location__min_distance=10000).order_by("-date") - self.assertEqual(events.count(), 1) - self.assertEqual(events[0], event2) - - # check that within_box works - box = [(-125.0, 35.0), (-100.0, 40.0)] - events = Event.objects(location__geo_within_box=box) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0].id, event2.id) - - polygon = [ - (-87.694445, 41.912114), - (-87.69084, 41.919395), - (-87.681742, 41.927186), - (-87.654276, 41.911731), - (-87.656164, 41.898061), - ] - events = Event.objects(location__geo_within_polygon=polygon) - self.assertEqual(events.count(), 1) - self.assertEqual(events[0].id, event1.id) - - polygon2 = [ - (-1.742249, 54.033586), - (-1.225891, 52.792797), - (-4.40094, 53.389881) - ] - events = Event.objects(location__geo_within_polygon=polygon2) - self.assertEqual(events.count(), 0) - - def test_2dsphere_point_embedded(self): - - class Venue(EmbeddedDocument): - location = GeoPointField() - name = StringField() - - class Event(Document): - title = StringField() - venue = EmbeddedDocumentField(Venue) - - Event.drop_collection() - - venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889]) - venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295]) - - event1 = Event(title="Coltrane Motion @ Double Door", - venue=venue1).save() - event2 = Event(title="Coltrane Motion @ Bottom of the Hill", - venue=venue2).save() - event3 = Event(title="Coltrane Motion @ Empty Bottle", - venue=venue1).save() - - # find all events "near" pitchfork office, chicago. - # note that "near" will show the san francisco event, too, - # although it sorts to last. - events = Event.objects(venue__location__near=[-87.67892, 41.9120459]) - self.assertEqual(events.count(), 3) - self.assertEqual(list(events), [event1, event3, event2]) - def test_linestring(self): - class Road(Document): name = StringField() line = LineStringField() @@ -410,7 +456,6 @@ class GeoQueriesTest(unittest.TestCase): self.assertEqual(1, roads) def test_polygon(self): - class Road(Document): name = StringField() poly = PolygonField() @@ -507,5 +552,6 @@ class GeoQueriesTest(unittest.TestCase): loc = Location.objects.as_pymongo()[0] self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]}) + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c54fa13d..b496e04a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -19,6 +19,9 @@ from mongoengine.python_support import IS_PYMONGO_3 from mongoengine.queryset import (DoesNotExist, MultipleObjectsReturned, QuerySet, QuerySetManager, queryset_manager) +from tests.utils import needs_mongodb_v26, skip_pymongo3 + + __all__ = ("QuerySetTest",) @@ -32,37 +35,6 @@ class db_ops_tracker(query_counter): return list(self.db.system.profile.find(ignore_query)) -def skip_older_mongodb(f): - def _inner(*args, **kwargs): - connection = get_connection() - info = connection.test.command('buildInfo') - mongodb_version = tuple([int(i) for i in info['version'].split('.')]) - - if mongodb_version < (2, 6): - raise SkipTest("Need MongoDB version 2.6+") - - return f(*args, **kwargs) - - _inner.__name__ = f.__name__ - _inner.__doc__ = f.__doc__ - - return _inner - - -def skip_pymongo3(f): - def _inner(*args, **kwargs): - - if IS_PYMONGO_3: - raise SkipTest("Useless with PyMongo 3+") - - return f(*args, **kwargs) - - _inner.__name__ = f.__name__ - _inner.__doc__ = f.__doc__ - - return _inner - - class QuerySetTest(unittest.TestCase): def setUp(self): @@ -599,16 +571,23 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(post.comments[0].by, 'joe') self.assertEqual(post.comments[0].votes.score, 4) + @needs_mongodb_v26 def test_update_min_max(self): class Scores(Document): high_score = IntField() low_score = IntField() - scores = Scores(high_score=800, low_score=200) - scores.save() + + scores = Scores.objects.create(high_score=800, low_score=200) + Scores.objects(id=scores.id).update(min__low_score=150) - self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150) + self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150) Scores.objects(id=scores.id).update(min__low_score=250) - self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150) + self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150) + + Scores.objects(id=scores.id).update(max__high_score=1000) + self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000) + Scores.objects(id=scores.id).update(max__high_score=500) + self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000) def test_updates_can_have_match_operators(self): @@ -1012,7 +991,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(person.name, "User A") self.assertEqual(person.age, 20) - @skip_older_mongodb + @needs_mongodb_v26 @skip_pymongo3 def test_cursor_args(self): """Ensures the cursor args can be set as expected @@ -3129,7 +3108,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Foo.objects.distinct("bar"), [bar]) - @skip_older_mongodb + @needs_mongodb_v26 def test_text_indexes(self): class News(Document): title = StringField() @@ -3216,7 +3195,7 @@ class QuerySetTest(unittest.TestCase): 'brasil').order_by('$text_score').first() self.assertEqual(item.get_text_score(), max_text_score) - @skip_older_mongodb + @needs_mongodb_v26 def test_distinct_handles_references_to_alias(self): register_connection('testdb', 'mongoenginetest2') @@ -4891,6 +4870,7 @@ class QuerySetTest(unittest.TestCase): self.assertTrue(Person.objects._has_data(), 'Cursor has data and returned False') + @needs_mongodb_v26 def test_queryset_aggregation_framework(self): class Person(Document): name = StringField() @@ -4925,17 +4905,13 @@ class QuerySetTest(unittest.TestCase): {'_id': p1.pk, 'name': "ISABELLA LUANNA"} ]) - data = Person.objects( - age__gte=17, age__lte=40).order_by('-age').aggregate( - {'$group': { - '_id': None, - 'total': {'$sum': 1}, - 'avg': {'$avg': '$age'} - } - } - - ) - + data = Person.objects(age__gte=17, age__lte=40).order_by('-age').aggregate({ + '$group': { + '_id': None, + 'total': {'$sum': 1}, + 'avg': {'$avg': '$age'} + } + }) self.assertEqual(list(data), [ {'_id': None, 'avg': 29, 'total': 2} ]) @@ -4976,11 +4952,13 @@ class QuerySetTest(unittest.TestCase): self.assertEquals(Animal.objects(folded_ears=True).count(), 1) self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1) - def test_loop_via_invalid_id_does_not_crash(self): + def test_loop_over_invalid_id_does_not_crash(self): class Person(Document): name = StringField() - Person.objects.delete() - Person._get_collection().update({"name": "a"}, {"$set": {"_id": ""}}, upsert=True) + + Person.drop_collection() + + Person._get_collection().insert({'name': 'a', 'id': ''}) for p in Person.objects(): self.assertEqual(p.name, 'a') diff --git a/tests/test_connection.py b/tests/test_connection.py index a1d3bfb6..d74f68c7 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -35,8 +35,7 @@ class ConnectionTest(unittest.TestCase): mongoengine.connection._dbs = {} def test_connect(self): - """Ensure that the connect() method works properly. - """ + """Ensure that the connect() method works properly.""" connect('mongoenginetest') conn = get_connection() @@ -146,8 +145,7 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(expected_connection, actual_connection) def test_connect_uri(self): - """Ensure that the connect() method works properly with uri's - """ + """Ensure that the connect() method works properly with URIs.""" c = connect(db='mongoenginetest', alias='admin') c.admin.system.users.remove({}) c.mongoenginetest.system.users.remove({}) @@ -227,9 +225,8 @@ class ConnectionTest(unittest.TestCase): self.assertRaises(OperationFailure, get_db) def test_connect_uri_with_authsource(self): - """Ensure that the connect() method works well with - the option `authSource` in URI. - This feature was introduced in MongoDB 2.4 and removed in 2.6 + """Ensure that the connect() method works well with `authSource` + option in the URI. """ # Create users c = connect('mongoenginetest') @@ -238,30 +235,31 @@ class ConnectionTest(unittest.TestCase): # Authentication fails without "authSource" if IS_PYMONGO_3: - test_conn = connect('mongoenginetest', alias='test1', - host='mongodb://username2:password@localhost/mongoenginetest') + test_conn = connect( + 'mongoenginetest', alias='test1', + host='mongodb://username2:password@localhost/mongoenginetest' + ) self.assertRaises(OperationFailure, test_conn.server_info) else: self.assertRaises( - MongoEngineConnectionError, connect, 'mongoenginetest', - alias='test1', + MongoEngineConnectionError, + connect, 'mongoenginetest', alias='test1', host='mongodb://username2:password@localhost/mongoenginetest' ) self.assertRaises(MongoEngineConnectionError, get_db, 'test1') # Authentication succeeds with "authSource" - connect( + authd_conn = connect( 'mongoenginetest', alias='test2', host=('mongodb://username2:password@localhost/' 'mongoenginetest?authSource=admin') ) - # This will fail starting from MongoDB 2.6+ db = get_db('test2') self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'mongoenginetest') # Clear all users - c.admin.system.users.remove({}) + authd_conn.admin.system.users.remove({}) def test_register_connection(self): """Ensure that connections with different aliases may be registered. diff --git a/tests/utils.py b/tests/utils.py index 128bbff0..4566d864 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,11 @@ import unittest +from nose.plugins.skip import SkipTest + from mongoengine import connect -from mongoengine.connection import get_db +from mongoengine.connection import get_db, get_connection +from mongoengine.python_support import IS_PYMONGO_3 + MONGO_TEST_DB = 'mongoenginetest' @@ -20,3 +24,55 @@ class MongoDBTestCase(unittest.TestCase): @classmethod def tearDownClass(cls): cls._connection.drop_database(MONGO_TEST_DB) + + +def get_mongodb_version(): + """Return the version tuple of the MongoDB server that the default + connection is connected to. + """ + return tuple(get_connection().server_info()['versionArray']) + +def _decorated_with_ver_requirement(func, ver_tuple): + """Return a given function decorated with the version requirement + for a particular MongoDB version tuple. + """ + def _inner(*args, **kwargs): + mongodb_ver = get_mongodb_version() + if mongodb_ver >= ver_tuple: + return func(*args, **kwargs) + + raise SkipTest('Needs MongoDB v{}+'.format( + '.'.join([str(v) for v in ver_tuple]) + )) + + _inner.__name__ = func.__name__ + _inner.__doc__ = func.__doc__ + + return _inner + +def needs_mongodb_v26(func): + """Raise a SkipTest exception if we're working with MongoDB version + lower than v2.6. + """ + return _decorated_with_ver_requirement(func, (2, 6)) + +def needs_mongodb_v3(func): + """Raise a SkipTest exception if we're working with MongoDB version + lower than v3.0. + """ + return _decorated_with_ver_requirement(func, (3, 0)) + +def skip_pymongo3(f): + """Raise a SkipTest exception if we're running a test against + PyMongo v3.x. + """ + def _inner(*args, **kwargs): + if IS_PYMONGO_3: + raise SkipTest("Useless with PyMongo 3+") + return f(*args, **kwargs) + + _inner.__name__ = f.__name__ + _inner.__doc__ = f.__doc__ + + return _inner + diff --git a/tox.ini b/tox.ini index d6052edf..7f0d36e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py26,py27,py33,py34,py35,pypy,pypy3}-{mg27,mg28},flake8 +envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30} [testenv] commands = @@ -7,16 +7,7 @@ commands = deps = nose mg27: PyMongo<2.8 - mg28: PyMongo>=2.8,<3.0 + mg28: PyMongo>=2.8,<2.9 mg30: PyMongo>=3.0 - mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master setenv = PYTHON_EGG_CACHE = {envdir}/python-eggs -passenv = windir - -[testenv:flake8] -deps = - flake8 - flake8-import-order -commands = - flake8 From 627cf90de03dac4c172624b0c5571379589edaf7 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 26 Feb 2017 20:30:37 -0500 Subject: [PATCH 043/268] tutorial tweaks: better copy + use py3-friendly syntax --- docs/tutorial.rst | 65 ++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 34 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 311d2888..fc92ce0c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -3,11 +3,10 @@ Tutorial ======== This tutorial introduces **MongoEngine** by means of example --- we will walk -through how to create a simple **Tumblelog** application. A Tumblelog is a type -of blog where posts are not constrained to being conventional text-based posts. -As well as text-based entries, users may post images, links, videos, etc. For -simplicity's sake, we'll stick to text, image and link entries in our -application. As the purpose of this tutorial is to introduce MongoEngine, we'll +through how to create a simple **Tumblelog** application. A tumblelog is a +blog that supports mixed media content, including text, images, links, video, +audio, etc. For simplicity's sake, we'll stick to text, image, and link +entries. As the purpose of this tutorial is to introduce MongoEngine, we'll focus on the data-modelling side of the application, leaving out a user interface. @@ -16,14 +15,14 @@ Getting started Before we start, make sure that a copy of MongoDB is running in an accessible location --- running it locally will be easier, but if that is not an option -then it may be run on a remote server. If you haven't installed mongoengine, +then it may be run on a remote server. If you haven't installed MongoEngine, simply use pip to install it like so:: $ pip install mongoengine Before we can start using MongoEngine, we need to tell it how to connect to our instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect` -function. If running locally the only argument we need to provide is the name +function. If running locally, the only argument we need to provide is the name of the MongoDB database to use:: from mongoengine import * @@ -39,18 +38,18 @@ Defining our documents MongoDB is *schemaless*, which means that no schema is enforced by the database --- we may add and remove fields however we want and MongoDB won't complain. This makes life a lot easier in many regards, especially when there is a change -to the data model. However, defining schemata for our documents can help to -iron out bugs involving incorrect types or missing fields, and also allow us to +to the data model. However, defining schemas for our documents can help to iron +out bugs involving incorrect types or missing fields, and also allow us to define utility methods on our documents in the same way that traditional :abbr:`ORMs (Object-Relational Mappers)` do. In our Tumblelog application we need to store several different types of -information. We will need to have a collection of **users**, so that we may +information. We will need to have a collection of **users**, so that we may link posts to an individual. We also need to store our different types of **posts** (eg: text, image and link) in the database. To aid navigation of our Tumblelog, posts may have **tags** associated with them, so that the list of posts shown to the user may be limited to posts that have been assigned a -specific tag. Finally, it would be nice if **comments** could be added to +specific tag. Finally, it would be nice if **comments** could be added to posts. We'll start with **users**, as the other document models are slightly more involved. @@ -78,7 +77,7 @@ Now we'll think about how to store the rest of the information. If we were using a relational database, we would most likely have a table of **posts**, a table of **comments** and a table of **tags**. To associate the comments with individual posts, we would put a column in the comments table that contained a -foreign key to the posts table. We'd also need a link table to provide the +foreign key to the posts table. We'd also need a link table to provide the many-to-many relationship between posts and tags. Then we'd need to address the problem of storing the specialised post-types (text, image and link). There are several ways we can achieve this, but each of them have their problems --- none @@ -96,7 +95,7 @@ using* the new fields we need to support video posts. This fits with the Object-Oriented principle of *inheritance* nicely. We can think of :class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and :class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports -this kind of modelling out of the box --- all you need do is turn on inheritance +this kind of modeling out of the box --- all you need do is turn on inheritance by setting :attr:`allow_inheritance` to True in the :attr:`meta`:: class Post(Document): @@ -128,8 +127,8 @@ link table, we can just store a list of tags in each post. So, for both efficiency and simplicity's sake, we'll store the tags as strings directly within the post, rather than storing references to tags in a separate collection. Especially as tags are generally very short (often even shorter -than a document's id), this denormalisation won't impact very strongly on the -size of our database. So let's take a look that the code our modified +than a document's id), this denormalization won't impact the size of the +database very strongly. Let's take a look at the code of our modified :class:`Post` class:: class Post(Document): @@ -141,7 +140,7 @@ The :class:`~mongoengine.fields.ListField` object that is used to define a Post' takes a field object as its first argument --- this means that you can have lists of any type of field (including lists). -.. note:: We don't need to modify the specialised post types as they all +.. note:: We don't need to modify the specialized post types as they all inherit from :class:`Post`. Comments @@ -149,7 +148,7 @@ Comments A comment is typically associated with *one* post. In a relational database, to display a post with its comments, we would have to retrieve the post from the -database, then query the database again for the comments associated with the +database and then query the database again for the comments associated with the post. This works, but there is no real reason to be storing the comments separately from their associated posts, other than to work around the relational model. Using MongoDB we can store the comments as a list of @@ -219,8 +218,8 @@ Now that we've got our user in the database, let's add a couple of posts:: post2.tags = ['mongoengine'] post2.save() -.. note:: If you change a field on a object that has already been saved, then - call :meth:`save` again, the document will be updated. +.. note:: If you change a field on an object that has already been saved and + then call :meth:`save` again, the document will be updated. Accessing our data ================== @@ -232,17 +231,17 @@ used to access the documents in the database collection associated with that class. So let's see how we can get our posts' titles:: for post in Post.objects: - print post.title + print(post.title) Retrieving type-specific information ------------------------------------ -This will print the titles of our posts, one on each line. But What if we want +This will print the titles of our posts, one on each line. But what if we want to access the type-specific data (link_url, content, etc.)? One way is simply to use the :attr:`objects` attribute of a subclass of :class:`Post`:: for post in TextPost.objects: - print post.content + print(post.content) Using TextPost's :attr:`objects` attribute only returns documents that were created using :class:`TextPost`. Actually, there is a more general rule here: @@ -259,16 +258,14 @@ instances of :class:`Post` --- they were instances of the subclass of practice:: for post in Post.objects: - print post.title - print '=' * len(post.title) + print(post.title) + print('=' * len(post.title)) if isinstance(post, TextPost): - print post.content + print(post.content) if isinstance(post, LinkPost): - print 'Link:', post.link_url - - print + print('Link: {}'.format(post.link_url)) This would print the title of each post, followed by the content if it was a text post, and "Link: " if it was a link post. @@ -283,7 +280,7 @@ your query. Let's adjust our query so that only posts with the tag "mongodb" are returned:: for post in Post.objects(tags='mongodb'): - print post.title + print(post.title) There are also methods available on :class:`~mongoengine.queryset.QuerySet` objects that allow different results to be returned, for example, calling @@ -292,11 +289,11 @@ the first matched by the query you provide. Aggregation functions may also be used on :class:`~mongoengine.queryset.QuerySet` objects:: num_posts = Post.objects(tags='mongodb').count() - print 'Found %d posts with tag "mongodb"' % num_posts + print('Found {} posts with tag "mongodb"'.format(num_posts)) -Learning more about mongoengine +Learning more about MongoEngine ------------------------------- -If you got this far you've made a great start, so well done! The next step on -your mongoengine journey is the `full user guide `_, where you -can learn indepth about how to use mongoengine and mongodb. +If you got this far you've made a great start, so well done! The next step on +your MongoEngine journey is the `full user guide `_, where +you can learn in-depth about how to use MongoEngine and MongoDB. From 5f43c032f2778af7f9c13c32c8cec4e7756569d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 26 Feb 2017 21:29:06 -0500 Subject: [PATCH 044/268] revamp the "connecting" user guide and test more ways of connecting to a replica set (#1490) --- docs/guide/connecting.rst | 53 +++++++++++++++++++++------------------ docs/guide/installing.rst | 4 +-- tests/test_connection.py | 45 +++++++++++++++++++++++---------- 3 files changed, 62 insertions(+), 40 deletions(-) diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index 827e5a3c..f40ed4c5 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -42,13 +42,18 @@ the :attr:`host` to will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password. -ReplicaSets -=========== +Replica Sets +============ -MongoEngine supports -:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them, -please use an URI style connection and provide the ``replicaSet`` name -in the connection kwargs. +MongoEngine supports connecting to replica sets:: + + from mongoengine import connect + + # Regular connect + connect('dbname', replicaset='rs-name') + + # MongoDB URI-style connect + connect(host='mongodb://localhost/dbname?replicaSet=rs-name') Read preferences are supported through the connection or via individual queries by passing the read_preference :: @@ -59,76 +64,74 @@ queries by passing the read_preference :: Multiple Databases ================== -Multiple database support was added in MongoEngine 0.6. To use multiple -databases you can use :func:`~mongoengine.connect` and provide an `alias` name -for the connection - if no `alias` is provided then "default" is used. +To use multiple databases you can use :func:`~mongoengine.connect` and provide +an `alias` name for the connection - if no `alias` is provided then "default" +is used. In the background this uses :func:`~mongoengine.register_connection` to store the data and you can register all aliases up front if required. Individual documents can also support multiple databases by providing a -`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects -to point across databases and collections. Below is an example schema, using -3 different databases to store data:: +`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` +objects to point across databases and collections. Below is an example schema, +using 3 different databases to store data:: class User(Document): name = StringField() - meta = {"db_alias": "user-db"} + meta = {'db_alias': 'user-db'} class Book(Document): name = StringField() - meta = {"db_alias": "book-db"} + meta = {'db_alias': 'book-db'} class AuthorBooks(Document): author = ReferenceField(User) book = ReferenceField(Book) - meta = {"db_alias": "users-books-db"} + meta = {'db_alias': 'users-books-db'} Context Managers ================ -Sometimes you may want to switch the database or collection to query against -for a class. +Sometimes you may want to switch the database or collection to query against. For example, archiving older data into a separate database for performance reasons or writing functions that dynamically choose collections to write -document to. +a document to. Switch Database --------------- The :class:`~mongoengine.context_managers.switch_db` context manager allows you to change the database alias for a given class allowing quick and easy -access the same User document across databases:: +access to the same User document across databases:: from mongoengine.context_managers import switch_db class User(Document): name = StringField() - meta = {"db_alias": "user-db"} + meta = {'db_alias': 'user-db'} with switch_db(User, 'archive-user-db') as User: - User(name="Ross").save() # Saves the 'archive-user-db' + User(name='Ross').save() # Saves the 'archive-user-db' Switch Collection ----------------- The :class:`~mongoengine.context_managers.switch_collection` context manager allows you to change the collection for a given class allowing quick and easy -access the same Group document across collection:: +access to the same Group document across collection:: from mongoengine.context_managers import switch_collection class Group(Document): name = StringField() - Group(name="test").save() # Saves in the default db + Group(name='test').save() # Saves in the default db with switch_collection(Group, 'group2000') as Group: - Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection - + Group(name='hello Group 2000 collection!').save() # Saves in group2000 collection .. note:: Make sure any aliases have been registered with diff --git a/docs/guide/installing.rst b/docs/guide/installing.rst index e93f0485..b89d48f0 100644 --- a/docs/guide/installing.rst +++ b/docs/guide/installing.rst @@ -2,13 +2,13 @@ Installing MongoEngine ====================== -To use MongoEngine, you will need to download `MongoDB `_ +To use MongoEngine, you will need to download `MongoDB `_ and ensure it is running in an accessible location. You will also need `PyMongo `_ to use MongoEngine, but if you install MongoEngine using setuptools, then the dependencies will be handled for you. -MongoEngine is available on PyPI, so to use it you can use :program:`pip`: +MongoEngine is available on PyPI, so you can use :program:`pip`: .. code-block:: console diff --git a/tests/test_connection.py b/tests/test_connection.py index d74f68c7..cdcf1377 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -198,19 +198,6 @@ class ConnectionTest(unittest.TestCase): self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertEqual(db.name, 'test') - def test_connect_uri_with_replicaset(self): - """Ensure connect() works when specifying a replicaSet.""" - if IS_PYMONGO_3: - c = connect(host='mongodb://localhost/test?replicaSet=local-rs') - db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) - self.assertEqual(db.name, 'test') - else: - # PyMongo < v3.x raises an exception: - # "localhost:27017 is not a member of replica set local-rs" - with self.assertRaises(MongoEngineConnectionError): - c = connect(host='mongodb://localhost/test?replicaSet=local-rs') - def test_uri_without_credentials_doesnt_override_conn_settings(self): """Ensure connect() uses the username & password params if the URI doesn't explicitly specify them. @@ -332,6 +319,38 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(dict(conn1.write_concern), {'w': 1, 'j': True}) self.assertEqual(dict(conn2.write_concern), {'w': 1, 'j': True}) + def test_connect_with_replicaset_via_uri(self): + """Ensure connect() works when specifying a replicaSet via the + MongoDB URI. + """ + if IS_PYMONGO_3: + c = connect(host='mongodb://localhost/test?replicaSet=local-rs') + db = get_db() + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'test') + else: + # PyMongo < v3.x raises an exception: + # "localhost:27017 is not a member of replica set local-rs" + with self.assertRaises(MongoEngineConnectionError): + c = connect(host='mongodb://localhost/test?replicaSet=local-rs') + + def test_connect_with_replicaset_via_kwargs(self): + """Ensure connect() works when specifying a replicaSet via the + connection kwargs + """ + if IS_PYMONGO_3: + c = connect(replicaset='local-rs') + self.assertEqual(c._MongoClient__options.replica_set_name, + 'local-rs') + db = get_db() + self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertEqual(db.name, 'test') + else: + # PyMongo < v3.x raises an exception: + # "localhost:27017 is not a member of replica set local-rs" + with self.assertRaises(MongoEngineConnectionError): + c = connect(replicaset='local-rs') + def test_datetime(self): connect('mongoenginetest', tz_aware=True) d = datetime.datetime(2010, 5, 5, tzinfo=utc) From 398964945a3e99f7a224968b3d1177eaffed6484 Mon Sep 17 00:00:00 2001 From: Ephraim Berkovitch Date: Mon, 27 Feb 2017 16:42:44 +0200 Subject: [PATCH 045/268] Document.objects.create should raise NotUniqueError upon saving duplicate primary key (#1485) --- .gitignore | 1 + mongoengine/queryset/base.py | 2 +- tests/document/indexes.py | 28 +++++++++++++++------------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index b180e87e..048a2d19 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ tests/test_bugfix.py htmlcov/ venv venv3 +scratchpad diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7e485686..fde46ae6 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -286,7 +286,7 @@ class BaseQuerySet(object): .. versionadded:: 0.4 """ - return self._document(**kwargs).save() + return self._document(**kwargs).save(force_insert=True) def first(self): """Retrieve the first object matching the query.""" diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 5d219eda..58e09199 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -412,7 +412,6 @@ class IndexesTest(unittest.TestCase): User.ensure_indexes() info = User.objects._collection.index_information() self.assertEqual(sorted(info.keys()), ['_cls_1_user_guid_1', '_id_']) - User.drop_collection() def test_embedded_document_index(self): """Tests settings an index on an embedded document @@ -434,7 +433,6 @@ class IndexesTest(unittest.TestCase): info = BlogPost.objects._collection.index_information() self.assertEqual(sorted(info.keys()), ['_id_', 'date.yr_-1']) - BlogPost.drop_collection() def test_list_embedded_document_index(self): """Ensure list embedded documents can be indexed @@ -461,7 +459,6 @@ class IndexesTest(unittest.TestCase): post1 = BlogPost(title="Embedded Indexes tests in place", tags=[Tag(name="about"), Tag(name="time")]) post1.save() - BlogPost.drop_collection() def test_recursive_embedded_objects_dont_break_indexes(self): @@ -622,8 +619,6 @@ class IndexesTest(unittest.TestCase): post3 = BlogPost(title='test3', date=Date(year=2010), slug='test') self.assertRaises(OperationError, post3.save) - BlogPost.drop_collection() - def test_unique_embedded_document(self): """Ensure that uniqueness constraints are applied to fields on embedded documents. """ @@ -651,8 +646,6 @@ class IndexesTest(unittest.TestCase): sub=SubDocument(year=2010, slug='test')) self.assertRaises(NotUniqueError, post3.save) - BlogPost.drop_collection() - def test_unique_embedded_document_in_list(self): """ Ensure that the uniqueness constraints are applied to fields in @@ -683,8 +676,6 @@ class IndexesTest(unittest.TestCase): self.assertRaises(NotUniqueError, post2.save) - BlogPost.drop_collection() - def test_unique_with_embedded_document_and_embedded_unique(self): """Ensure that uniqueness constraints are applied to fields on embedded documents. And work with unique_with as well. @@ -718,8 +709,6 @@ class IndexesTest(unittest.TestCase): sub=SubDocument(year=2009, slug='test-1')) self.assertRaises(NotUniqueError, post3.save) - BlogPost.drop_collection() - def test_ttl_indexes(self): class Log(Document): @@ -759,13 +748,11 @@ class IndexesTest(unittest.TestCase): raise AssertionError("We saved a dupe!") except NotUniqueError: pass - Customer.drop_collection() def test_unique_and_primary(self): """If you set a field as primary, then unexpected behaviour can occur. You won't create a duplicate but you will update an existing document. """ - class User(Document): name = StringField(primary_key=True, unique=True) password = StringField() @@ -781,8 +768,23 @@ class IndexesTest(unittest.TestCase): self.assertEqual(User.objects.count(), 1) self.assertEqual(User.objects.get().password, 'secret2') + def test_unique_and_primary_create(self): + """Create a new record with a duplicate primary key + throws an exception + """ + class User(Document): + name = StringField(primary_key=True) + password = StringField() + User.drop_collection() + User.objects.create(name='huangz', password='secret') + with self.assertRaises(NotUniqueError): + User.objects.create(name='huangz', password='secret2') + + self.assertEqual(User.objects.count(), 1) + self.assertEqual(User.objects.get().password, 'secret') + def test_index_with_pk(self): """Ensure you can use `pk` as part of a query""" From 68109530148d87800cab0abe4bc288a0d9248550 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Mon, 27 Feb 2017 09:55:50 +0000 Subject: [PATCH 046/268] added OrderedDynamicField class to store data in the defined order because of #203 --- mongoengine/fields.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 11425095..4b6a1a9b 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,6 +5,7 @@ import re import time import uuid import warnings +from collections import OrderedDict from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -49,7 +50,7 @@ __all__ = ( 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', - 'MultiPolygonField', 'GeoJsonBaseField' + 'MultiPolygonField', 'GeoJsonBaseField', 'OrderedDynamicField' ) RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -644,7 +645,7 @@ class DynamicField(BaseField): is_list = True value = {k: v for k, v in enumerate(value)} - data = {} + data = self._container_type() if hasattr(self, '_container_type') else {} for k, v in value.iteritems(): data[k] = self.to_mongo(v, use_db_field, fields) @@ -675,6 +676,16 @@ class DynamicField(BaseField): value.validate(clean=clean) +class OrderedDynamicField(DynamicField): + """A field that wraps DynamicField. This uses OrderedDict class + to guarantee to store data in the defined order instead of dict. + """ + + def __init__(self, *args, **kwargs): + super(OrderedDynamicField, self).__init__(*args, **kwargs) + self._container_type = OrderedDict + + class ListField(ComplexBaseField): """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. From 84a8f1eb2b0c21f3e245469a469e14194c8c2d2a Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Tue, 28 Feb 2017 03:35:39 +0000 Subject: [PATCH 047/268] added OrderedDocument class to decode BSON data to OrderedDict for retrieving data in order --- mongoengine/document.py | 28 +++++++++++++++++++++++++++- mongoengine/queryset/base.py | 4 ++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index b79e5e97..a2a5e156 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,6 +1,7 @@ import re import warnings +from collections import OrderedDict from bson.dbref import DBRef import pymongo from pymongo.read_preferences import ReadPreference @@ -21,7 +22,8 @@ from mongoengine.queryset import (NotUniqueError, OperationError, __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument', 'OperationError', - 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument') + 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument', + 'OrderedDocument') def includes_cls(fields): @@ -1036,3 +1038,27 @@ class MapReduceDocument(object): self._key_object = self._document.objects.with_id(self.key) return self._key_object return self._key_object + + +class OrderedDocument(Document): + """A document that is almost same with Document except for returning + results in OrderedDict instead of dict. + """ + + # The __metaclass__ attribute is removed by 2to3 when running with Python3 + # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + + if IS_PYMONGO_3: + # returns collection object which is set OrderedDict class to be decoded from BSON document + from bson import CodecOptions + return collection.with_options(codec_options=CodecOptions(document_class=OrderedDict)) + else: + # set attribute to specify the class to be decoeded + cls.decoded_class = OrderedDict + return collection diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7e485686..4738f5f7 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1501,6 +1501,10 @@ class BaseQuerySet(object): cursor_args['read_preference'] = self._read_preference else: cursor_args['slave_okay'] = self._slave_okay + + # set decode format if needed + if hasattr(self._document, 'decoded_class'): + cursor_args['as_class'] = self._document.decoded_class else: fields_name = 'projection' # snapshot is not handled at all by PyMongo 3+ From e32a9777d794103a562ed6c087751dea665b7fdc Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Tue, 28 Feb 2017 03:35:53 +0000 Subject: [PATCH 048/268] added test for OrderedDynamicField and OrderedDocument --- tests/fields/fields.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 318c0c59..3516471f 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -7,6 +7,7 @@ import itertools import re from nose.plugins.skip import SkipTest +from collections import OrderedDict import six try: @@ -4499,6 +4500,40 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) + def test_ordered_dynamic_fields_class(self): + """ + Tests that OrderedDynamicFields interits features of the DynamicFields + and saves/retrieves data in order. + """ + class Member(Document): + name = StringField() + age = IntField() + + class Team(OrderedDocument): + members = OrderedDynamicField() + + Member.drop_collection() + Team.drop_collection() + + member_info = [ + ('Martin McFly', 17), + ('Emmett Brown', 65), + ('George McFly', 47) + ] + members = OrderedDict() + for name, age in member_info: + members[name] = Member(name=name, age=age) + members[name].save() + + Team(members=members).save() + + index = 0 + team = Team.objects.get() + for member in team.members: + print("%s == %s" % (member, member_info[index][0])) + self.assertEqual(member, member_info[index][0]) + index += 1 + if __name__ == '__main__': unittest.main() From 5957dc72eb209982a13821f6afe3e74ca39719a8 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 1 Mar 2017 09:20:57 +0000 Subject: [PATCH 049/268] To achive storing object data in order with minimum implementation, I changed followings. - added optional parameter `container_class` which enables to choose intermediate class at encoding Python data, instead of additional field class. - removed OrderedDocument class because the equivalent feature could be implemented by the outside of Mongoengine. --- mongoengine/document.py | 28 +---------------------- mongoengine/fields.py | 23 ++++++++----------- mongoengine/queryset/base.py | 4 ---- tests/fields/fields.py | 44 +++++++++++++++--------------------- 4 files changed, 29 insertions(+), 70 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index a2a5e156..b79e5e97 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1,7 +1,6 @@ import re import warnings -from collections import OrderedDict from bson.dbref import DBRef import pymongo from pymongo.read_preferences import ReadPreference @@ -22,8 +21,7 @@ from mongoengine.queryset import (NotUniqueError, OperationError, __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument', 'DynamicEmbeddedDocument', 'OperationError', - 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument', - 'OrderedDocument') + 'InvalidCollectionError', 'NotUniqueError', 'MapReduceDocument') def includes_cls(fields): @@ -1038,27 +1036,3 @@ class MapReduceDocument(object): self._key_object = self._document.objects.with_id(self.key) return self._key_object return self._key_object - - -class OrderedDocument(Document): - """A document that is almost same with Document except for returning - results in OrderedDict instead of dict. - """ - - # The __metaclass__ attribute is removed by 2to3 when running with Python3 - # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 - my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass - - @classmethod - def _get_collection(cls): - collection = super(OrderedDocument, cls)._get_collection() - - if IS_PYMONGO_3: - # returns collection object which is set OrderedDict class to be decoded from BSON document - from bson import CodecOptions - return collection.with_options(codec_options=CodecOptions(document_class=OrderedDict)) - else: - # set attribute to specify the class to be decoeded - cls.decoded_class = OrderedDict - return collection diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 4b6a1a9b..70061e08 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,7 +5,6 @@ import re import time import uuid import warnings -from collections import OrderedDict from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -50,7 +49,7 @@ __all__ = ( 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', 'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField', - 'MultiPolygonField', 'GeoJsonBaseField', 'OrderedDynamicField' + 'MultiPolygonField', 'GeoJsonBaseField' ) RECURSIVE_REFERENCE_CONSTANT = 'self' @@ -620,6 +619,14 @@ class DynamicField(BaseField): Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" + def __init__(self, container_class=dict, *args, **kwargs): + self._container_cls = container_class + if not issubclass(self._container_cls, dict): + self.error('The class that is specified in `container_class` parameter ' + 'must be a subclass of `dict`.') + + super(DynamicField, self).__init__(*args, **kwargs) + def to_mongo(self, value, use_db_field=True, fields=None): """Convert a Python type to a MongoDB compatible type. """ @@ -645,7 +652,7 @@ class DynamicField(BaseField): is_list = True value = {k: v for k, v in enumerate(value)} - data = self._container_type() if hasattr(self, '_container_type') else {} + data = self._container_cls() for k, v in value.iteritems(): data[k] = self.to_mongo(v, use_db_field, fields) @@ -676,16 +683,6 @@ class DynamicField(BaseField): value.validate(clean=clean) -class OrderedDynamicField(DynamicField): - """A field that wraps DynamicField. This uses OrderedDict class - to guarantee to store data in the defined order instead of dict. - """ - - def __init__(self, *args, **kwargs): - super(OrderedDynamicField, self).__init__(*args, **kwargs) - self._container_type = OrderedDict - - class ListField(ComplexBaseField): """A list field that wraps a standard field, allowing multiple instances of the field to be used as a list in the database. diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 4738f5f7..7e485686 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1501,10 +1501,6 @@ class BaseQuerySet(object): cursor_args['read_preference'] = self._read_preference else: cursor_args['slave_okay'] = self._slave_okay - - # set decode format if needed - if hasattr(self._document, 'decoded_class'): - cursor_args['as_class'] = self._document.decoded_class else: fields_name = 'projection' # snapshot is not handled at all by PyMongo 3+ diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 3516471f..b73eb0af 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4500,39 +4500,31 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) - def test_ordered_dynamic_fields_class(self): + def test_dynamicfield_with_container_class(self): """ - Tests that OrderedDynamicFields interits features of the DynamicFields - and saves/retrieves data in order. + Tests that object can be stored in order by DynamicField class + with container_class parameter. """ - class Member(Document): - name = StringField() - age = IntField() + raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] - class Team(OrderedDocument): - members = OrderedDynamicField() + class Doc(Document): + ordered_data = DynamicField(container_class=OrderedDict) + unordered_data = DynamicField() - Member.drop_collection() - Team.drop_collection() + Doc.drop_collection() - member_info = [ - ('Martin McFly', 17), - ('Emmett Brown', 65), - ('George McFly', 47) - ] - members = OrderedDict() - for name, age in member_info: - members[name] = Member(name=name, age=age) - members[name].save() + doc = Doc(ordered_data=OrderedDict(raw_data), + unordered_data=dict(raw_data)).save() - Team(members=members).save() + self.assertEqual(type(doc.ordered_data), OrderedDict) + self.assertEqual(type(doc.unordered_data), dict) + self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) + self.assertNotEqual([k for k,_ in doc.unordered_data.items()], ['d', 'c', 'b', 'a']) - index = 0 - team = Team.objects.get() - for member in team.members: - print("%s == %s" % (member, member_info[index][0])) - self.assertEqual(member, member_info[index][0]) - index += 1 + def test_dynamicfield_with_wrong_container_class(self): + with self.assertRaises(ValidationError): + class DocWithInvalidField: + data = DynamicField(container_class=list) if __name__ == '__main__': From 6aaf9ba470edcba89489b95f0302884730bf70ca Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 1 Mar 2017 09:32:28 +0000 Subject: [PATCH 050/268] removed a checking of dict order because this order is not cared (some implementation might be in ordered, but other one is not) --- tests/fields/fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index b73eb0af..2db37317 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4519,7 +4519,6 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertEqual(type(doc.ordered_data), OrderedDict) self.assertEqual(type(doc.unordered_data), dict) self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) - self.assertNotEqual([k for k,_ in doc.unordered_data.items()], ['d', 'c', 'b', 'a']) def test_dynamicfield_with_wrong_container_class(self): with self.assertRaises(ValidationError): From 741643af5f01b74067ece4f41c6a7982020ee6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Thu, 2 Mar 2017 00:05:10 -0500 Subject: [PATCH 051/268] clean up field unit tests (#1498) --- tests/fields/fields.py | 1240 +++++++++++++++++++--------------------- 1 file changed, 580 insertions(+), 660 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 318c0c59..ed7baa2b 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -16,7 +16,7 @@ except ImportError: from decimal import Decimal -from bson import Binary, DBRef, ObjectId +from bson import Binary, DBRef, ObjectId, SON try: from bson.int64 import Int64 except ImportError: @@ -35,7 +35,8 @@ __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") class FieldTest(MongoDBTestCase): def test_default_values_nothing_set(self): - """Ensure that default field values are used when creating a document. + """Ensure that default field values are used when creating + a document. """ class Person(Document): name = StringField() @@ -47,8 +48,9 @@ class FieldTest(MongoDBTestCase): # Confirm saving now would store values data_to_be_saved = sorted(person.to_mongo().keys()) - self.assertEqual( - data_to_be_saved, ['age', 'created', 'name', 'userid']) + self.assertEqual(data_to_be_saved, + ['age', 'created', 'name', 'userid'] + ) self.assertTrue(person.validate() is None) @@ -68,7 +70,8 @@ class FieldTest(MongoDBTestCase): data_to_be_saved, ['age', 'created', 'name', 'userid']) def test_default_values_set_to_None(self): - """Ensure that default field values are used when creating a document. + """Ensure that default field values are used even when + we explcitly initialize the doc with None values. """ class Person(Document): name = StringField() @@ -100,7 +103,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) def test_default_values_when_setting_to_None(self): - """Ensure that default field values are used when creating a document. + """Ensure that default field values are used when creating + a document. """ class Person(Document): name = StringField() @@ -120,10 +124,10 @@ class FieldTest(MongoDBTestCase): self.assertTrue(person.validate() is None) - self.assertEqual(person.name, person.name) - self.assertEqual(person.age, person.age) - self.assertEqual(person.userid, person.userid) - self.assertEqual(person.created, person.created) + self.assertEqual(person.name, None) + self.assertEqual(person.age, 30) + self.assertEqual(person.userid, 'test') + self.assertTrue(isinstance(person.created, datetime.datetime)) self.assertEqual(person._data['name'], person.name) self.assertEqual(person._data['age'], person.age) @@ -135,7 +139,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) def test_default_values_when_deleting_value(self): - """Ensure that default field values are used when creating a document. + """Ensure that default field values are used after non-default + values are explicitly deleted. """ class Person(Document): name = StringField() @@ -143,7 +148,8 @@ class FieldTest(MongoDBTestCase): userid = StringField(default=lambda: 'test', required=True) created = DateTimeField(default=datetime.datetime.utcnow) - person = Person(name="Ross") + person = Person(name="Ross", age=50, userid='different', + created=datetime.datetime(2014, 6, 12)) del person.name del person.age del person.userid @@ -154,10 +160,11 @@ class FieldTest(MongoDBTestCase): self.assertTrue(person.validate() is None) - self.assertEqual(person.name, person.name) - self.assertEqual(person.age, person.age) - self.assertEqual(person.userid, person.userid) - self.assertEqual(person.created, person.created) + self.assertEqual(person.name, None) + self.assertEqual(person.age, 30) + self.assertEqual(person.userid, 'test') + self.assertTrue(isinstance(person.created, datetime.datetime)) + self.assertNotEqual(person.created, datetime.datetime(2014, 6, 12)) self.assertEqual(person._data['name'], person.name) self.assertEqual(person._data['age'], person.age) @@ -169,8 +176,7 @@ class FieldTest(MongoDBTestCase): self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) def test_required_values(self): - """Ensure that required field constraints are enforced. - """ + """Ensure that required field constraints are enforced.""" class Person(Document): name = StringField(required=True) age = IntField(required=True) @@ -182,9 +188,9 @@ class FieldTest(MongoDBTestCase): self.assertRaises(ValidationError, person.validate) def test_not_required_handles_none_in_update(self): - """Ensure that every fields should accept None if required is False. + """Ensure that every fields should accept None if required is + False. """ - class HandleNoneFields(Document): str_fld = StringField() int_fld = IntField() @@ -236,23 +242,27 @@ class FieldTest(MongoDBTestCase): doc.com_dt_fld = datetime.datetime.utcnow() doc.save() - collection = self.db[HandleNoneFields._get_collection_name()] - obj = collection.update({"_id": doc.id}, {"$unset": { - "str_fld": 1, - "int_fld": 1, - "flt_fld": 1, - "comp_dt_fld": 1} + # Unset all the fields + obj = HandleNoneFields._get_collection().update({"_id": doc.id}, { + "$unset": { + "str_fld": 1, + "int_fld": 1, + "flt_fld": 1, + "comp_dt_fld": 1 + } }) # Retrive data from db and verify it. - ret = HandleNoneFields.objects.all()[0] - + ret = HandleNoneFields.objects.first() self.assertEqual(ret.str_fld, None) self.assertEqual(ret.int_fld, None) self.assertEqual(ret.flt_fld, None) - # Return current time if retrived value is None. + + # ComplexDateTimeField returns current time if retrived value is None. self.assertTrue(isinstance(ret.comp_dt_fld, datetime.datetime)) + # Retrieved object shouldn't pass validation when a re-save is + # attempted. self.assertRaises(ValidationError, ret.validate) def test_int_and_float_ne_operator(self): @@ -280,7 +290,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(1, TestDocument.objects(long_fld__ne=None).count()) def test_object_id_validation(self): - """Ensure that invalid values cannot be assigned to string fields. + """Ensure that invalid values cannot be assigned to an + ObjectIdField. """ class Person(Document): name = StringField() @@ -297,27 +308,8 @@ class FieldTest(MongoDBTestCase): person.id = '497ce96f395f2f052a494fd4' person.validate() - def test_db_field_validation(self): - """Ensure that db_field doesn't accept invalid values.""" - - # dot in the name - with self.assertRaises(ValueError): - class User(Document): - name = StringField(db_field='user.name') - - # name starting with $ - with self.assertRaises(ValueError): - class User(Document): - name = StringField(db_field='$name') - - # name containing a null character - with self.assertRaises(ValueError): - class User(Document): - name = StringField(db_field='name\0') - def test_string_validation(self): - """Ensure that invalid values cannot be assigned to string fields. - """ + """Ensure that invalid values cannot be assigned to string fields.""" class Person(Document): name = StringField(max_length=20) userid = StringField(r'[0-9a-z_]+$') @@ -490,10 +482,25 @@ class FieldTest(MongoDBTestCase): person_2 = Person(height='something invalid') self.assertRaises(ValidationError, person_2.validate) - Person.drop_collection() + def test_db_field_validation(self): + """Ensure that db_field doesn't accept invalid values.""" + + # dot in the name + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='user.name') + + # name starting with $ + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='$name') + + # name containing a null character + with self.assertRaises(ValueError): + class User(Document): + name = StringField(db_field='name\0') def test_decimal_comparison(self): - class Person(Document): money = DecimalField() @@ -546,7 +553,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(expected, actual) def test_boolean_validation(self): - """Ensure that invalid values cannot be assigned to boolean fields. + """Ensure that invalid values cannot be assigned to boolean + fields. """ class Person(Document): admin = BooleanField() @@ -586,8 +594,7 @@ class FieldTest(MongoDBTestCase): self.assertRaises(ValidationError, person.validate) def test_uuid_field_binary(self): - """Test UUID fields storing as Binary object - """ + """Test UUID fields storing as Binary object.""" class Person(Document): api_key = UUIDField(binary=True) @@ -611,7 +618,8 @@ class FieldTest(MongoDBTestCase): self.assertRaises(ValidationError, person.validate) def test_datetime_validation(self): - """Ensure that invalid values cannot be assigned to datetime fields. + """Ensure that invalid values cannot be assigned to datetime + fields. """ class LogEntry(Document): time = DateTimeField() @@ -675,8 +683,6 @@ class FieldTest(MongoDBTestCase): log.reload() self.assertEqual(log.date.date(), datetime.date.today()) - LogEntry.drop_collection() - # Post UTC - microseconds are rounded (down) nearest millisecond and # dropped d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) @@ -708,8 +714,6 @@ class FieldTest(MongoDBTestCase): self.assertNotEqual(log.date, d1) self.assertEqual(log.date, d2) - LogEntry.drop_collection() - def test_datetime_usage(self): """Tests for regular datetime fields""" class LogEntry(Document): @@ -731,48 +735,42 @@ class FieldTest(MongoDBTestCase): log1 = LogEntry.objects.get(date=d1.isoformat('T')) self.assertEqual(log, log1) - LogEntry.drop_collection() - - # create 60 log entries - for i in range(1950, 2010): + # 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(), 60) + self.assertEqual(LogEntry.objects.count(), 20) # Test ordering logs = LogEntry.objects.order_by("date") - count = logs.count() i = 0 - while i == count - 1: + while i < 19: self.assertTrue(logs[i].date <= logs[i + 1].date) i += 1 logs = LogEntry.objects.order_by("-date") - count = logs.count() i = 0 - while i == count - 1: + 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(), 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() + 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_complexdatetime_storage(self): - """Tests for complex datetime fields - which can handle microseconds - without rounding. + """Tests for complex datetime fields - which can handle + microseconds without rounding. """ class LogEntry(Document): date = ComplexDateTimeField() @@ -829,18 +827,16 @@ class FieldTest(MongoDBTestCase): 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) - LogEntry.drop_collection() - def test_complexdatetime_usage(self): - """Tests for complex datetime fields - which can handle microseconds - without rounding. + """Tests for complex datetime fields - which can handle + microseconds without rounding. """ class LogEntry(Document): date = ComplexDateTimeField() LogEntry.drop_collection() - d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) + d1 = datetime.datetime(1950, 1, 1, 0, 0, 1, 999) log = LogEntry() log.date = d1 log.save() @@ -848,10 +844,8 @@ class FieldTest(MongoDBTestCase): log1 = LogEntry.objects.get(date=d1) self.assertEqual(log, log1) - LogEntry.drop_collection() - - # create 60 log entries - for i in range(1950, 2010): + # 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() @@ -859,16 +853,14 @@ class FieldTest(MongoDBTestCase): # Test ordering logs = LogEntry.objects.order_by("date") - count = logs.count() i = 0 - while i == count - 1: + while i < 59: self.assertTrue(logs[i].date <= logs[i + 1].date) i += 1 logs = LogEntry.objects.order_by("-date") - count = logs.count() i = 0 - while i == count - 1: + while i < 59: self.assertTrue(logs[i].date >= logs[i + 1].date) i += 1 @@ -889,7 +881,9 @@ class FieldTest(MongoDBTestCase): # Test microsecond-level ordering/filtering for microsecond in (99, 999, 9999, 10000): - LogEntry(date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)).save() + 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): @@ -901,14 +895,12 @@ class FieldTest(MongoDBTestCase): 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)) + logs = LogEntry.objects.filter( + date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000)) self.assertEqual(logs.count(), 4) - LogEntry.drop_collection() - def test_list_validation(self): - """Ensure that a list field only accepts lists with valid elements. - """ + """Ensure that a list field only accepts lists with valid elements.""" class User(Document): pass @@ -922,6 +914,9 @@ class FieldTest(MongoDBTestCase): authors = ListField(ReferenceField(User)) generic = ListField(GenericReferenceField()) + User.drop_collection() + BlogPost.drop_collection() + post = BlogPost(content='Went for a walk today...') post.validate() @@ -967,9 +962,6 @@ class FieldTest(MongoDBTestCase): post.generic = [user] post.validate() - User.drop_collection() - BlogPost.drop_collection() - def test_sorted_list_sorting(self): """Ensure that a sorted list field properly sorts values. """ @@ -983,6 +975,8 @@ class FieldTest(MongoDBTestCase): ordering='order') tags = SortedListField(StringField()) + BlogPost.drop_collection() + post = BlogPost(content='Went for a walk today...') post.save() @@ -1007,8 +1001,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(post.comments[0].content, comment1.content) self.assertEqual(post.comments[1].content, comment2.content) - BlogPost.drop_collection() - def test_reverse_list_sorting(self): """Ensure that a reverse sorted list field properly sorts values""" @@ -1021,6 +1013,8 @@ class FieldTest(MongoDBTestCase): ordering='count', reverse=True) name = StringField() + CategoryList.drop_collection() + catlist = CategoryList(name="Top categories") cat1 = Category(name='posts', count=10) cat2 = Category(name='food', count=100) @@ -1033,11 +1027,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(catlist.categories[1].name, cat3.name) self.assertEqual(catlist.categories[2].name, cat1.name) - CategoryList.drop_collection() - def test_list_field(self): - """Ensure that list types work as expected. - """ + """Ensure that list types work as expected.""" class BlogPost(Document): info = ListField() @@ -1086,8 +1077,6 @@ class FieldTest(MongoDBTestCase): post.save() self.assertEqual(BlogPost.objects(info=['1', '2', '3', '4', '1', '2', '3', '4']).count(), 1) - BlogPost.drop_collection() - def test_list_field_manipulative_operators(self): """Ensure that ListField works with standard list operators that manipulate the list. """ @@ -1238,30 +1227,32 @@ class FieldTest(MongoDBTestCase): post.reload() self.assertEqual(post.info, ['5', '4', '3', '2', '1', '0']) - # 'sort': though this operator method does manipulate the list, it is tested in - # the 'test_list_field_lexicograpic_operators' function - BlogPost.drop_collection() + # 'sort': though this operator method does manipulate the list, it is + # tested in the 'test_list_field_lexicograpic_operators' function def test_list_field_invalid_operators(self): class BlogPost(Document): ref = StringField() info = ListField(StringField()) + post = BlogPost() post.ref = "1234" post.info = ['0', '1', '2', '3', '4', '5'] + # '__hash__' # aka 'hash(list)' - # # assert TypeError self.assertRaises(TypeError, lambda: hash(post.info)) def test_list_field_lexicographic_operators(self): - """Ensure that ListField works with standard list operators that do lexigraphic ordering. + """Ensure that ListField works with standard list operators that + do lexigraphic ordering. """ class BlogPost(Document): ref = StringField() text_info = ListField(StringField()) oid_info = ListField(ObjectIdField()) bool_info = ListField(BooleanField()) + BlogPost.drop_collection() blogSmall = BlogPost(ref="small") @@ -1286,28 +1277,35 @@ class FieldTest(MongoDBTestCase): blogLargeB.bool_info = [False, True] blogLargeB.save() blogLargeB.reload() + # '__eq__' aka '==' self.assertEqual(blogLargeA.text_info, blogLargeB.text_info) self.assertEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__ge__' aka '>=' self.assertGreaterEqual(blogLargeA.text_info, blogSmall.text_info) self.assertGreaterEqual(blogLargeA.text_info, blogLargeB.text_info) self.assertGreaterEqual(blogLargeA.bool_info, blogSmall.bool_info) self.assertGreaterEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__gt__' aka '>' self.assertGreaterEqual(blogLargeA.text_info, blogSmall.text_info) self.assertGreaterEqual(blogLargeA.bool_info, blogSmall.bool_info) + # '__le__' aka '<=' self.assertLessEqual(blogSmall.text_info, blogLargeB.text_info) self.assertLessEqual(blogLargeA.text_info, blogLargeB.text_info) self.assertLessEqual(blogSmall.bool_info, blogLargeB.bool_info) self.assertLessEqual(blogLargeA.bool_info, blogLargeB.bool_info) + # '__lt__' aka '<' self.assertLess(blogSmall.text_info, blogLargeB.text_info) self.assertLess(blogSmall.bool_info, blogLargeB.bool_info) + # '__ne__' aka '!=' self.assertNotEqual(blogSmall.text_info, blogLargeB.text_info) self.assertNotEqual(blogSmall.bool_info, blogLargeB.bool_info) + # 'sort' blogLargeB.bool_info = [True, False, True, False] blogLargeB.text_info.sort() @@ -1327,11 +1325,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(blogLargeB.oid_info, sorted_target_list) self.assertEqual(blogLargeB.bool_info, [False, False, True, True]) - BlogPost.drop_collection() - def test_list_assignment(self): - """Ensure that list field element assignment and slicing work - """ + """Ensure that list field element assignment and slicing work.""" class BlogPost(Document): info = ListField() @@ -1391,8 +1386,9 @@ class FieldTest(MongoDBTestCase): self.assertEqual(repr(foo.bars), '[]') def test_list_field_strict(self): - """Ensure that list field handles validation if provided a strict field type.""" - + """Ensure that list field handles validation if provided + a strict field type. + """ class Simple(Document): mapping = ListField(field=IntField()) @@ -1407,30 +1403,26 @@ class FieldTest(MongoDBTestCase): e.mapping = ["abc"] e.save() - Simple.drop_collection() - def test_list_field_rejects_strings(self): - """Strings aren't valid list field data types""" - + """Strings aren't valid list field data types.""" class Simple(Document): mapping = ListField() Simple.drop_collection() + e = Simple() e.mapping = 'hello world' - self.assertRaises(ValidationError, e.save) def test_complex_field_required(self): - """Ensure required cant be None / Empty""" - + """Ensure required cant be None / Empty.""" class Simple(Document): mapping = ListField(required=True) Simple.drop_collection() + e = Simple() e.mapping = [] - self.assertRaises(ValidationError, e.save) class Simple(Document): @@ -1439,18 +1431,17 @@ class FieldTest(MongoDBTestCase): Simple.drop_collection() e = Simple() e.mapping = {} - self.assertRaises(ValidationError, e.save) def test_complex_field_same_value_not_changed(self): - """ - If a complex field is set to the same value, it should not be marked as - changed. + """If a complex field is set to the same value, it should not + be marked as changed. """ class Simple(Document): mapping = ListField() Simple.drop_collection() + e = Simple().save() e.mapping = [] self.assertEqual([], e._changed_fields) @@ -1459,12 +1450,12 @@ class FieldTest(MongoDBTestCase): mapping = DictField() Simple.drop_collection() + e = Simple().save() e.mapping = {} self.assertEqual([], e._changed_fields) def test_slice_marks_field_as_changed(self): - class Simple(Document): widgets = ListField() @@ -1477,7 +1468,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(simple.widgets, [4]) def test_del_slice_marks_field_as_changed(self): - class Simple(Document): widgets = ListField() @@ -1490,7 +1480,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(simple.widgets, [4]) def test_list_field_with_negative_indices(self): - class Simple(Document): widgets = ListField() @@ -1504,7 +1493,6 @@ class FieldTest(MongoDBTestCase): def test_list_field_complex(self): """Ensure that the list fields can handle the complex types.""" - class SettingBase(EmbeddedDocument): meta = {'allow_inheritance': True} @@ -1518,6 +1506,7 @@ class FieldTest(MongoDBTestCase): mapping = ListField() Simple.drop_collection() + e = Simple() e.mapping.append(StringSetting(value='foo')) e.mapping.append(IntegerSetting(value=42)) @@ -1555,11 +1544,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual( Simple.objects.filter(mapping__2__list__1__value='Boo').count(), 1) - Simple.drop_collection() - def test_dict_field(self): - """Ensure that dict types work as expected. - """ + """Ensure that dict types work as expected.""" class BlogPost(Document): info = DictField() @@ -1621,11 +1607,8 @@ class FieldTest(MongoDBTestCase): post.reload() self.assertEqual([], post.info['authors']) - BlogPost.drop_collection() - def test_dictfield_dump_document(self): - """Ensure a DictField can handle another document's dump - """ + """Ensure a DictField can handle another document's dump.""" class Doc(Document): field = DictField() @@ -1663,7 +1646,6 @@ class FieldTest(MongoDBTestCase): def test_dictfield_strict(self): """Ensure that dict field handles validation if provided a strict field type.""" - class Simple(Document): mapping = DictField(field=IntField()) @@ -1678,11 +1660,8 @@ class FieldTest(MongoDBTestCase): e.mapping['somestring'] = "abc" e.save() - Simple.drop_collection() - def test_dictfield_complex(self): """Ensure that the dict field can handle the complex types.""" - class SettingBase(EmbeddedDocument): meta = {'allow_inheritance': True} @@ -1696,6 +1675,7 @@ class FieldTest(MongoDBTestCase): mapping = DictField() Simple.drop_collection() + e = Simple() e.mapping['somestring'] = StringSetting(value='foo') e.mapping['someint'] = IntegerSetting(value=42) @@ -1732,11 +1712,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual( Simple.objects.filter(mapping__nested_dict__list__1__value='Boo').count(), 1) - Simple.drop_collection() - def test_atomic_update_dict_field(self): """Ensure that the entire DictField can be atomically updated.""" - class Simple(Document): mapping = DictField(field=ListField(IntField(required=True))) @@ -1754,11 +1731,8 @@ class FieldTest(MongoDBTestCase): with self.assertRaises(ValueError): e.update(set__mapping={"somestrings": ["foo", "bar", ]}) - Simple.drop_collection() - def test_mapfield(self): """Ensure that the MapField handles the declared type.""" - class Simple(Document): mapping = MapField(IntField()) @@ -1776,11 +1750,8 @@ class FieldTest(MongoDBTestCase): class NoDeclaredType(Document): mapping = MapField() - Simple.drop_collection() - def test_complex_mapfield(self): """Ensure that the MapField can handle complex declared types.""" - class SettingBase(EmbeddedDocument): meta = {"allow_inheritance": True} @@ -1809,7 +1780,6 @@ class FieldTest(MongoDBTestCase): e.save() def test_embedded_mapfield_db_field(self): - class Embedded(EmbeddedDocument): number = IntField(default=0, db_field='i') @@ -1846,11 +1816,10 @@ class FieldTest(MongoDBTestCase): test.my_map['1'].name = 'test updated' test.save() - Test.drop_collection() - def test_map_field_lookup(self): - """Ensure MapField lookups succeed on Fields without a lookup method""" - + """Ensure MapField lookups succeed on Fields without a lookup + method. + """ class Action(EmbeddedDocument): operation = StringField() object = StringField() @@ -1872,7 +1841,6 @@ class FieldTest(MongoDBTestCase): actions__friends__object='beer').count()) def test_map_field_unicode(self): - class Info(EmbeddedDocument): description = StringField() value_list = ListField(field=StringField()) @@ -1890,12 +1858,12 @@ class FieldTest(MongoDBTestCase): tree.save() - self.assertEqual(BlogPost.objects.get(id=tree.id).info_dict[u"éééé"].description, u"VALUE: éééé") - - BlogPost.drop_collection() + self.assertEqual( + BlogPost.objects.get(id=tree.id).info_dict[u"éééé"].description, + u"VALUE: éééé" + ) def test_embedded_db_field(self): - class Embedded(EmbeddedDocument): number = IntField(default=0, db_field='i') @@ -1949,8 +1917,8 @@ class FieldTest(MongoDBTestCase): person.validate() def test_embedded_document_inheritance(self): - """Ensure that subclasses of embedded documents may be provided to - EmbeddedDocumentFields of the superclass' type. + """Ensure that subclasses of embedded documents may be provided + to EmbeddedDocumentFields of the superclass' type. """ class User(EmbeddedDocument): name = StringField() @@ -1976,7 +1944,6 @@ class FieldTest(MongoDBTestCase): """Ensure that nested list of subclassed embedded documents is handled correctly. """ - class Group(EmbeddedDocument): name = StringField() content = ListField(StringField()) @@ -1998,9 +1965,9 @@ class FieldTest(MongoDBTestCase): self.assertEqual(content, User.objects.first().groups[0].content) def test_reference_miss(self): - """Ensure an exception is raised when dereferencing unknown document + """Ensure an exception is raised when dereferencing an unknown + document. """ - class Foo(Document): pass @@ -2029,8 +1996,8 @@ class FieldTest(MongoDBTestCase): self.assertEqual(bar.generic_ref, {'_ref': expected, '_cls': 'Foo'}) def test_reference_validation(self): - """Ensure that invalid docment objects cannot be assigned to reference - fields. + """Ensure that invalid document objects cannot be assigned to + reference fields. """ class User(Document): name = StringField() @@ -2042,6 +2009,8 @@ class FieldTest(MongoDBTestCase): User.drop_collection() BlogPost.drop_collection() + # Make sure ReferenceField only accepts a document class or a string + # with a document class name. self.assertRaises(ValidationError, ReferenceField, EmbeddedDocument) user = User(name='Test User') @@ -2056,19 +2025,18 @@ class FieldTest(MongoDBTestCase): post1.author = post2 self.assertRaises(ValidationError, post1.validate) + # Make sure referencing a saved document of the right type works user.save() post1.author = user post1.save() + # Make sure referencing a saved document of the *wrong* type fails post2.save() post1.author = post2 self.assertRaises(ValidationError, post1.validate) - User.drop_collection() - BlogPost.drop_collection() - def test_dbref_reference_fields(self): - + """Make sure storing references as bson.dbref.DBRef works.""" class Person(Document): name = StringField() parent = ReferenceField('self', dbref=True) @@ -2078,410 +2046,31 @@ class FieldTest(MongoDBTestCase): p1 = Person(name="John").save() Person(name="Ross", parent=p1).save() - col = Person._get_collection() - data = col.find_one({'name': 'Ross'}) - self.assertEqual(data['parent'], DBRef('person', p1.pk)) + self.assertEqual( + Person._get_collection().find_one({'name': 'Ross'})['parent'], + DBRef('person', p1.pk) + ) p = Person.objects.get(name="Ross") self.assertEqual(p.parent, p1) def test_dbref_to_mongo(self): + """Make sure that calling to_mongo on a ReferenceField which + has dbref=False, but actually actually contains a DBRef returns + an ID of that DBRef. + """ class Person(Document): name = StringField() parent = ReferenceField('self', dbref=False) - p1 = Person._from_son({'name': "Yakxxx", - 'parent': "50a234ea469ac1eda42d347d"}) - mongoed = p1.to_mongo() - self.assertTrue(isinstance(mongoed['parent'], ObjectId)) - - def test_cached_reference_field_get_and_save(self): - """ - Tests #1047: CachedReferenceField creates DBRefs on to_python, but can't save them on to_mongo - """ - class Animal(Document): - name = StringField() - tag = StringField() - - class Ocorrence(Document): - person = StringField() - animal = CachedReferenceField(Animal) - - Animal.drop_collection() - Ocorrence.drop_collection() - - Ocorrence(person="testte", - animal=Animal(name="Leopard", tag="heavy").save()).save() - p = Ocorrence.objects.get() - p.person = 'new_testte' - p.save() - - def test_cached_reference_fields(self): - class Animal(Document): - name = StringField() - tag = StringField() - - class Ocorrence(Document): - person = StringField() - animal = CachedReferenceField( - Animal, fields=['tag']) - - Animal.drop_collection() - Ocorrence.drop_collection() - - a = Animal(name="Leopard", tag="heavy") - a.save() - - self.assertEqual(Animal._cached_reference_fields, [Ocorrence.animal]) - o = Ocorrence(person="teste", animal=a) - o.save() - - p = Ocorrence(person="Wilson") - p.save() - - self.assertEqual(Ocorrence.objects(animal=None).count(), 1) - - self.assertEqual( - a.to_mongo(fields=['tag']), {'tag': 'heavy', "_id": a.pk}) - - self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') - - # counts - Ocorrence(person="teste 2").save() - Ocorrence(person="teste 3").save() - - count = Ocorrence.objects(animal__tag='heavy').count() - self.assertEqual(count, 1) - - ocorrence = Ocorrence.objects(animal__tag='heavy').first() - self.assertEqual(ocorrence.person, "teste") - self.assertTrue(isinstance(ocorrence.animal, Animal)) - - def test_cached_reference_field_decimal(self): - class PersonAuto(Document): - name = StringField() - salary = DecimalField() - - class SocialTest(Document): - group = StringField() - person = CachedReferenceField( - PersonAuto, - fields=('salary',)) - - PersonAuto.drop_collection() - SocialTest.drop_collection() - - p = PersonAuto(name="Alberto", salary=Decimal('7000.00')) - p.save() - - s = SocialTest(group="dev", person=p) - s.save() - - self.assertEqual( - SocialTest.objects._collection.find_one({'person.salary': 7000.00}), { - '_id': s.pk, - 'group': s.group, - 'person': { - '_id': p.pk, - 'salary': 7000.00 - } - }) - - def test_cached_reference_field_reference(self): - class Group(Document): - name = StringField() - - class Person(Document): - name = StringField() - group = ReferenceField(Group) - - class SocialData(Document): - obs = StringField() - tags = ListField( - StringField()) - person = CachedReferenceField( - Person, - fields=('group',)) - - Group.drop_collection() - Person.drop_collection() - SocialData.drop_collection() - - g1 = Group(name='dev') - g1.save() - - g2 = Group(name="designers") - g2.save() - - p1 = Person(name="Alberto", group=g1) - p1.save() - - p2 = Person(name="Andre", group=g1) - p2.save() - - p3 = Person(name="Afro design", group=g2) - p3.save() - - s1 = SocialData(obs="testing 123", person=p1, tags=['tag1', 'tag2']) - s1.save() - - s2 = SocialData(obs="testing 321", person=p3, tags=['tag3', 'tag4']) - s2.save() - - self.assertEqual(SocialData.objects._collection.find_one( - {'tags': 'tag2'}), { - '_id': s1.pk, - 'obs': 'testing 123', - 'tags': ['tag1', 'tag2'], - 'person': { - '_id': p1.pk, - 'group': g1.pk - } - }) - - self.assertEqual(SocialData.objects(person__group=g2).count(), 1) - self.assertEqual(SocialData.objects(person__group=g2).first(), s2) - - def test_cached_reference_field_update_all(self): - class Person(Document): - TYPES = ( - ('pf', "PF"), - ('pj', "PJ") - ) - name = StringField() - tp = StringField( - choices=TYPES - ) - - father = CachedReferenceField('self', fields=('tp',)) - - Person.drop_collection() - - a1 = Person(name="Wilson Father", tp="pj") - a1.save() - - a2 = Person(name='Wilson Junior', tp='pf', father=a1) - a2.save() - - self.assertEqual(dict(a2.to_mongo()), { - "_id": a2.pk, - "name": u"Wilson Junior", - "tp": u"pf", - "father": { - "_id": a1.pk, - "tp": u"pj" - } - }) - - self.assertEqual(Person.objects(father=a1)._query, { - 'father._id': a1.pk - }) - self.assertEqual(Person.objects(father=a1).count(), 1) - - Person.objects.update(set__tp="pf") - Person.father.sync_all() - - a2.reload() - self.assertEqual(dict(a2.to_mongo()), { - "_id": a2.pk, - "name": u"Wilson Junior", - "tp": u"pf", - "father": { - "_id": a1.pk, - "tp": u"pf" - } - }) - - def test_cached_reference_fields_on_embedded_documents(self): - with self.assertRaises(InvalidDocumentError): - class Test(Document): - name = StringField() - - type('WrongEmbeddedDocument', ( - EmbeddedDocument,), { - 'test': CachedReferenceField(Test) - }) - - def test_cached_reference_auto_sync(self): - class Person(Document): - TYPES = ( - ('pf', "PF"), - ('pj', "PJ") - ) - name = StringField() - tp = StringField( - choices=TYPES - ) - - father = CachedReferenceField('self', fields=('tp',)) - - Person.drop_collection() - - a1 = Person(name="Wilson Father", tp="pj") - a1.save() - - a2 = Person(name='Wilson Junior', tp='pf', father=a1) - a2.save() - - a1.tp = 'pf' - a1.save() - - a2.reload() - self.assertEqual(dict(a2.to_mongo()), { - '_id': a2.pk, - 'name': 'Wilson Junior', - 'tp': 'pf', - 'father': { - '_id': a1.pk, - 'tp': 'pf' - } - }) - - def test_cached_reference_auto_sync_disabled(self): - class Persone(Document): - TYPES = ( - ('pf', "PF"), - ('pj', "PJ") - ) - name = StringField() - tp = StringField( - choices=TYPES - ) - - father = CachedReferenceField( - 'self', fields=('tp',), auto_sync=False) - - Persone.drop_collection() - - a1 = Persone(name="Wilson Father", tp="pj") - a1.save() - - a2 = Persone(name='Wilson Junior', tp='pf', father=a1) - a2.save() - - a1.tp = 'pf' - a1.save() - - self.assertEqual(Persone.objects._collection.find_one({'_id': a2.pk}), { - '_id': a2.pk, - 'name': 'Wilson Junior', - 'tp': 'pf', - 'father': { - '_id': a1.pk, - 'tp': 'pj' - } - }) - - def test_cached_reference_embedded_fields(self): - class Owner(EmbeddedDocument): - TPS = ( - ('n', "Normal"), - ('u', "Urgent") - ) - name = StringField() - tp = StringField( - verbose_name="Type", - db_field="t", - choices=TPS) - - class Animal(Document): - name = StringField() - tag = StringField() - - owner = EmbeddedDocumentField(Owner) - - class Ocorrence(Document): - person = StringField() - animal = CachedReferenceField( - Animal, fields=['tag', 'owner.tp']) - - Animal.drop_collection() - Ocorrence.drop_collection() - - a = Animal(name="Leopard", tag="heavy", - owner=Owner(tp='u', name="Wilson Júnior") - ) - a.save() - - o = Ocorrence(person="teste", animal=a) - o.save() - self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tp'])), { - '_id': a.pk, - 'tag': 'heavy', - 'owner': { - 't': 'u' - } - }) - self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') - self.assertEqual(o.to_mongo()['animal']['owner']['t'], 'u') - - # counts - Ocorrence(person="teste 2").save() - Ocorrence(person="teste 3").save() - - count = Ocorrence.objects( - animal__tag='heavy', animal__owner__tp='u').count() - self.assertEqual(count, 1) - - ocorrence = Ocorrence.objects( - animal__tag='heavy', - animal__owner__tp='u').first() - self.assertEqual(ocorrence.person, "teste") - self.assertTrue(isinstance(ocorrence.animal, Animal)) - - def test_cached_reference_embedded_list_fields(self): - class Owner(EmbeddedDocument): - name = StringField() - tags = ListField(StringField()) - - class Animal(Document): - name = StringField() - tag = StringField() - - owner = EmbeddedDocumentField(Owner) - - class Ocorrence(Document): - person = StringField() - animal = CachedReferenceField( - Animal, fields=['tag', 'owner.tags']) - - Animal.drop_collection() - Ocorrence.drop_collection() - - a = Animal(name="Leopard", tag="heavy", - owner=Owner(tags=['cool', 'funny'], - name="Wilson Júnior") - ) - a.save() - - o = Ocorrence(person="teste 2", animal=a) - o.save() - self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tags'])), { - '_id': a.pk, - 'tag': 'heavy', - 'owner': { - 'tags': ['cool', 'funny'] - } - }) - - self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') - self.assertEqual(o.to_mongo()['animal']['owner']['tags'], - ['cool', 'funny']) - - # counts - Ocorrence(person="teste 2").save() - Ocorrence(person="teste 3").save() - - query = Ocorrence.objects( - animal__tag='heavy', animal__owner__tags='cool')._query - self.assertEqual( - query, {'animal.owner.tags': 'cool', 'animal.tag': 'heavy'}) - - ocorrence = Ocorrence.objects( - animal__tag='heavy', - animal__owner__tags='cool').first() - self.assertEqual(ocorrence.person, "teste 2") - self.assertTrue(isinstance(ocorrence.animal, Animal)) + p = Person( + name='Steve', + parent=DBRef('person', 'abcdefghijklmnop') + ) + self.assertEqual(p.to_mongo(), SON([ + ('name', u'Steve'), + ('parent', 'abcdefghijklmnop') + ])) def test_objectid_reference_fields(self): @@ -2526,9 +2115,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(group_obj.members[0].name, user1.name) self.assertEqual(group_obj.members[1].name, user2.name) - User.drop_collection() - Group.drop_collection() - def test_recursive_reference(self): """Ensure that ReferenceFields can reference their own documents. """ @@ -2538,6 +2124,7 @@ class FieldTest(MongoDBTestCase): friends = ListField(ReferenceField('self')) Employee.drop_collection() + bill = Employee(name='Bill Lumbergh') bill.save() @@ -2567,8 +2154,8 @@ class FieldTest(MongoDBTestCase): children = ListField(EmbeddedDocumentField('TreeNode')) Tree.drop_collection() - tree = Tree(name="Tree") + tree = Tree(name="Tree") first_child = TreeNode(name="Child 1") tree.children.append(first_child) @@ -2681,9 +2268,6 @@ class FieldTest(MongoDBTestCase): post = BlogPost.objects(author=m2).first() self.assertEqual(post.id, post2.id) - Member.drop_collection() - BlogPost.drop_collection() - def test_reference_query_conversion_dbref(self): """Ensure that ReferenceFields can be queried using objects and values of the type of the primary key of the referenced object. @@ -2715,9 +2299,6 @@ class FieldTest(MongoDBTestCase): post = BlogPost.objects(author=m2).first() self.assertEqual(post.id, post2.id) - Member.drop_collection() - BlogPost.drop_collection() - def test_drop_abstract_document(self): """Ensure that an abstract document cannot be dropped given it has no underlying collection. @@ -2751,9 +2332,6 @@ class FieldTest(MongoDBTestCase): self.assertEquals(Brother.objects[0].sibling.name, sister.name) - Sister.drop_collection() - Brother.drop_collection() - def test_reference_abstract_class(self): """Ensure that an abstract class instance cannot be used in the reference of that abstract class. @@ -2775,9 +2353,6 @@ class FieldTest(MongoDBTestCase): brother = Brother(name="Bob", sibling=sister) self.assertRaises(ValidationError, brother.save) - Sister.drop_collection() - Brother.drop_collection() - def test_abstract_reference_base_type(self): """Ensure that an an abstract reference fails validation when given a Document that does not inherit from the abstract type. @@ -2800,9 +2375,6 @@ class FieldTest(MongoDBTestCase): brother = Brother(name="Bob", sibling=mother) self.assertRaises(ValidationError, brother.save) - Brother.drop_collection() - Mother.drop_collection() - def test_generic_reference(self): """Ensure that a GenericReferenceField properly dereferences items. """ @@ -2842,10 +2414,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(bm.bookmark_object, link_1) self.assertTrue(isinstance(bm.bookmark_object, Link)) - Link.drop_collection() - Post.drop_collection() - Bookmark.drop_collection() - def test_generic_reference_list(self): """Ensure that a ListField properly dereferences generic references. """ @@ -2876,10 +2444,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(user.bookmarks[0], post_1) self.assertEqual(user.bookmarks[1], link_1) - Link.drop_collection() - Post.drop_collection() - User.drop_collection() - def test_generic_reference_document_not_registered(self): """Ensure dereferencing out of the document registry throws a `NotRegistered` error. @@ -2910,9 +2474,6 @@ class FieldTest(MongoDBTestCase): except NotRegistered: pass - Link.drop_collection() - User.drop_collection() - def test_generic_reference_is_none(self): class Person(Document): @@ -2920,14 +2481,13 @@ class FieldTest(MongoDBTestCase): city = GenericReferenceField() Person.drop_collection() - Person(name="Wilson Jr").save() + Person(name="Wilson Jr").save() self.assertEqual(repr(Person.objects(city=None)), "[]") def test_generic_reference_choices(self): - """Ensure that a GenericReferenceField can handle choices - """ + """Ensure that a GenericReferenceField can handle choices.""" class Link(Document): title = StringField() @@ -3044,10 +2604,6 @@ class FieldTest(MongoDBTestCase): user = User.objects.first() self.assertEqual(user.bookmarks, [post_1]) - Link.drop_collection() - Post.drop_collection() - User.drop_collection() - def test_generic_reference_list_item_modification(self): """Ensure that modifications of related documents (through generic reference) don't influence on querying """ @@ -3076,9 +2632,6 @@ class FieldTest(MongoDBTestCase): self.assertNotEqual(user, None) self.assertEqual(user.bookmarks[0], post_1) - Post.drop_collection() - User.drop_collection() - def test_generic_reference_filter_by_dbref(self): """Ensure we can search for a specific generic reference by providing its ObjectId. @@ -3130,8 +2683,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(MIME_TYPE, attachment_1.content_type) self.assertEqual(BLOB, six.binary_type(attachment_1.blob)) - Attachment.drop_collection() - def test_binary_validation(self): """Ensure that invalid values cannot be assigned to binary fields. """ @@ -3164,12 +2715,7 @@ class FieldTest(MongoDBTestCase): attachment_size_limit.blob = six.b('\xe6\x00\xc4\xff') attachment_size_limit.validate() - Attachment.drop_collection() - AttachmentRequired.drop_collection() - AttachmentSizeLimit.drop_collection() - def test_binary_field_primary(self): - class Attachment(Document): id = BinaryField(primary_key=True) @@ -3178,13 +2724,10 @@ class FieldTest(MongoDBTestCase): att = Attachment(id=binary_id).save() self.assertEqual(1, Attachment.objects.count()) self.assertEqual(1, Attachment.objects.filter(id=att.id).count()) - # TODO use assertIsNotNone once Python 2.6 support is dropped - self.assertTrue(Attachment.objects.filter(id=att.id).first() is not None) att.delete() self.assertEqual(0, Attachment.objects.count()) def test_binary_field_primary_filter_by_binary_pk_as_str(self): - raise SkipTest("Querying by id as string is not currently supported") class Attachment(Document): @@ -3194,8 +2737,6 @@ class FieldTest(MongoDBTestCase): binary_id = uuid.uuid4().bytes att = Attachment(id=binary_id).save() self.assertEqual(1, Attachment.objects.filter(id=binary_id).count()) - # TODO use assertIsNotNone once Python 2.6 support is dropped - self.assertTrue(Attachment.objects.filter(id=binary_id).first() is not None) att.delete() self.assertEqual(0, Attachment.objects.count()) @@ -3363,8 +2904,6 @@ class FieldTest(MongoDBTestCase): shirt.size = "XS" self.assertRaises(ValidationError, shirt.validate) - Shirt.drop_collection() - def test_simple_choices_get_field_display(self): """Test dynamic helper for returning the display value of a choices field. @@ -3395,8 +2934,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(shirt.get_style_display(), 'Z') self.assertRaises(ValidationError, shirt.validate) - Shirt.drop_collection() - def test_simple_choices_validation_invalid_value(self): """Ensure that error messages are correct. """ @@ -3429,8 +2966,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(error_dict['size'], SIZE_MESSAGE) self.assertEqual(error_dict['color'], COLOR_MESSAGE) - Shirt.drop_collection() - def test_ensure_unique_default_instances(self): """Ensure that every field has it's own unique default instance.""" class D(Document): @@ -3581,7 +3116,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(a.counter, 2) def test_multiple_sequence_fields_on_docs(self): - class Animal(Document): id = SequenceField(primary_key=True) name = StringField() @@ -3737,8 +3271,7 @@ class FieldTest(MongoDBTestCase): self.assertTrue(isinstance(person.like, Dish)) def test_generic_embedded_document_choices(self): - """Ensure you can limit GenericEmbeddedDocument choices - """ + """Ensure you can limit GenericEmbeddedDocument choices.""" class Car(EmbeddedDocument): name = StringField() @@ -3763,8 +3296,8 @@ class FieldTest(MongoDBTestCase): self.assertTrue(isinstance(person.like, Dish)) def test_generic_list_embedded_document_choices(self): - """Ensure you can limit GenericEmbeddedDocument choices inside a list - field + """Ensure you can limit GenericEmbeddedDocument choices inside + a list field. """ class Car(EmbeddedDocument): name = StringField() @@ -3790,8 +3323,7 @@ class FieldTest(MongoDBTestCase): self.assertTrue(isinstance(person.likes[0], Dish)) def test_recursive_validation(self): - """Ensure that a validation result to_dict is available. - """ + """Ensure that a validation result to_dict is available.""" class Author(EmbeddedDocument): name = StringField(required=True) @@ -3868,9 +3400,8 @@ class FieldTest(MongoDBTestCase): self.assertTrue(user.validate() is None) def test_tuples_as_tuples(self): - """ - Ensure that tuples remain tuples when they are - inside a ComplexBaseField + """Ensure that tuples remain tuples when they are inside + a ComplexBaseField. """ class EnumField(BaseField): @@ -3887,6 +3418,7 @@ class FieldTest(MongoDBTestCase): items = ListField(EnumField()) TestDoc.drop_collection() + tuples = [(100, 'Testing')] doc = TestDoc() doc.items = tuples @@ -3898,7 +3430,6 @@ class FieldTest(MongoDBTestCase): self.assertTrue(x.items[0] in tuples) def test_dynamic_fields_class(self): - class Doc2(Document): field_1 = StringField(db_field='f') @@ -3921,7 +3452,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(doc.embed_me.field_1, "hello") def test_dynamic_fields_embedded_class(self): - class Embed(EmbeddedDocument): field_1 = StringField(db_field='f') @@ -3938,8 +3468,7 @@ class FieldTest(MongoDBTestCase): self.assertEqual(doc.embed_me.field_1, "hello") def test_dynamicfield_dump_document(self): - """Ensure a DynamicField can handle another document's dump - """ + """Ensure a DynamicField can handle another document's dump.""" class Doc(Document): field = DynamicField() @@ -4010,15 +3539,15 @@ class FieldTest(MongoDBTestCase): def test_sparse_field(self): class Doc(Document): name = StringField(required=False, unique=True, sparse=True) - try: - Doc().save() - Doc().save() - except Exception: - self.fail() + + # This would raise an exception in a non-sparse unique index + Doc().save() + Doc().save() def test_undefined_field_exception(self): - """Tests if a `FieldDoesNotExist` exception is raised when trying to - instanciate a document with a field that's not defined. + """Tests if a `FieldDoesNotExist` exception is raised when + trying to instantiate a document with a field that's not + defined. """ class Doc(Document): foo = StringField() @@ -4026,11 +3555,10 @@ class FieldTest(MongoDBTestCase): with self.assertRaises(FieldDoesNotExist): Doc(bar='test') - def test_undefined_field_exception_with_strict(self): - """Tests if a `FieldDoesNotExist` exception is raised when trying to - instanciate a document with a field that's not defined, - even when strict is set to False. + """Tests if a `FieldDoesNotExist` exception is raised when + trying to instantiate a document with a field that's not + defined, even when strict is set to False. """ class Doc(Document): foo = StringField() @@ -4041,8 +3569,8 @@ class FieldTest(MongoDBTestCase): def test_long_field_is_considered_as_int64(self): """ - Tests that long fields are stored as long in mongo, even if long value - is small enough to be an int. + Tests that long fields are stored as long in mongo, even if long + value is small enough to be an int. """ class TestLongFieldConsideredAsInt64(Document): some_long = LongField() @@ -4500,5 +4028,397 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) +class CachedReferenceFieldTest(MongoDBTestCase): + + def test_cached_reference_field_get_and_save(self): + """ + Tests #1047: CachedReferenceField creates DBRefs on to_python, + but can't save them on to_mongo. + """ + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocorrence(Document): + person = StringField() + animal = CachedReferenceField(Animal) + + Animal.drop_collection() + Ocorrence.drop_collection() + + Ocorrence(person="testte", + animal=Animal(name="Leopard", tag="heavy").save()).save() + p = Ocorrence.objects.get() + p.person = 'new_testte' + p.save() + + def test_cached_reference_fields(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocorrence(Document): + person = StringField() + animal = CachedReferenceField( + Animal, fields=['tag']) + + Animal.drop_collection() + Ocorrence.drop_collection() + + a = Animal(name="Leopard", tag="heavy") + a.save() + + self.assertEqual(Animal._cached_reference_fields, [Ocorrence.animal]) + o = Ocorrence(person="teste", animal=a) + o.save() + + p = Ocorrence(person="Wilson") + p.save() + + self.assertEqual(Ocorrence.objects(animal=None).count(), 1) + + self.assertEqual( + a.to_mongo(fields=['tag']), {'tag': 'heavy', "_id": a.pk}) + + self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') + + # counts + Ocorrence(person="teste 2").save() + Ocorrence(person="teste 3").save() + + count = Ocorrence.objects(animal__tag='heavy').count() + self.assertEqual(count, 1) + + ocorrence = Ocorrence.objects(animal__tag='heavy').first() + self.assertEqual(ocorrence.person, "teste") + self.assertTrue(isinstance(ocorrence.animal, Animal)) + + def test_cached_reference_field_decimal(self): + class PersonAuto(Document): + name = StringField() + salary = DecimalField() + + class SocialTest(Document): + group = StringField() + person = CachedReferenceField( + PersonAuto, + fields=('salary',)) + + PersonAuto.drop_collection() + SocialTest.drop_collection() + + p = PersonAuto(name="Alberto", salary=Decimal('7000.00')) + p.save() + + s = SocialTest(group="dev", person=p) + s.save() + + self.assertEqual( + SocialTest.objects._collection.find_one({'person.salary': 7000.00}), { + '_id': s.pk, + 'group': s.group, + 'person': { + '_id': p.pk, + 'salary': 7000.00 + } + }) + + def test_cached_reference_field_reference(self): + class Group(Document): + name = StringField() + + class Person(Document): + name = StringField() + group = ReferenceField(Group) + + class SocialData(Document): + obs = StringField() + tags = ListField( + StringField()) + person = CachedReferenceField( + Person, + fields=('group',)) + + Group.drop_collection() + Person.drop_collection() + SocialData.drop_collection() + + g1 = Group(name='dev') + g1.save() + + g2 = Group(name="designers") + g2.save() + + p1 = Person(name="Alberto", group=g1) + p1.save() + + p2 = Person(name="Andre", group=g1) + p2.save() + + p3 = Person(name="Afro design", group=g2) + p3.save() + + s1 = SocialData(obs="testing 123", person=p1, tags=['tag1', 'tag2']) + s1.save() + + s2 = SocialData(obs="testing 321", person=p3, tags=['tag3', 'tag4']) + s2.save() + + self.assertEqual(SocialData.objects._collection.find_one( + {'tags': 'tag2'}), { + '_id': s1.pk, + 'obs': 'testing 123', + 'tags': ['tag1', 'tag2'], + 'person': { + '_id': p1.pk, + 'group': g1.pk + } + }) + + self.assertEqual(SocialData.objects(person__group=g2).count(), 1) + self.assertEqual(SocialData.objects(person__group=g2).first(), s2) + + def test_cached_reference_field_update_all(self): + class Person(Document): + TYPES = ( + ('pf', "PF"), + ('pj', "PJ") + ) + name = StringField() + tp = StringField( + choices=TYPES + ) + + father = CachedReferenceField('self', fields=('tp',)) + + Person.drop_collection() + + a1 = Person(name="Wilson Father", tp="pj") + a1.save() + + a2 = Person(name='Wilson Junior', tp='pf', father=a1) + a2.save() + + self.assertEqual(dict(a2.to_mongo()), { + "_id": a2.pk, + "name": u"Wilson Junior", + "tp": u"pf", + "father": { + "_id": a1.pk, + "tp": u"pj" + } + }) + + self.assertEqual(Person.objects(father=a1)._query, { + 'father._id': a1.pk + }) + self.assertEqual(Person.objects(father=a1).count(), 1) + + Person.objects.update(set__tp="pf") + Person.father.sync_all() + + a2.reload() + self.assertEqual(dict(a2.to_mongo()), { + "_id": a2.pk, + "name": u"Wilson Junior", + "tp": u"pf", + "father": { + "_id": a1.pk, + "tp": u"pf" + } + }) + + def test_cached_reference_fields_on_embedded_documents(self): + with self.assertRaises(InvalidDocumentError): + class Test(Document): + name = StringField() + + type('WrongEmbeddedDocument', ( + EmbeddedDocument,), { + 'test': CachedReferenceField(Test) + }) + + def test_cached_reference_auto_sync(self): + class Person(Document): + TYPES = ( + ('pf', "PF"), + ('pj', "PJ") + ) + name = StringField() + tp = StringField( + choices=TYPES + ) + + father = CachedReferenceField('self', fields=('tp',)) + + Person.drop_collection() + + a1 = Person(name="Wilson Father", tp="pj") + a1.save() + + a2 = Person(name='Wilson Junior', tp='pf', father=a1) + a2.save() + + a1.tp = 'pf' + a1.save() + + a2.reload() + self.assertEqual(dict(a2.to_mongo()), { + '_id': a2.pk, + 'name': 'Wilson Junior', + 'tp': 'pf', + 'father': { + '_id': a1.pk, + 'tp': 'pf' + } + }) + + def test_cached_reference_auto_sync_disabled(self): + class Persone(Document): + TYPES = ( + ('pf', "PF"), + ('pj', "PJ") + ) + name = StringField() + tp = StringField( + choices=TYPES + ) + + father = CachedReferenceField( + 'self', fields=('tp',), auto_sync=False) + + Persone.drop_collection() + + a1 = Persone(name="Wilson Father", tp="pj") + a1.save() + + a2 = Persone(name='Wilson Junior', tp='pf', father=a1) + a2.save() + + a1.tp = 'pf' + a1.save() + + self.assertEqual(Persone.objects._collection.find_one({'_id': a2.pk}), { + '_id': a2.pk, + 'name': 'Wilson Junior', + 'tp': 'pf', + 'father': { + '_id': a1.pk, + 'tp': 'pj' + } + }) + + def test_cached_reference_embedded_fields(self): + class Owner(EmbeddedDocument): + TPS = ( + ('n', "Normal"), + ('u', "Urgent") + ) + name = StringField() + tp = StringField( + verbose_name="Type", + db_field="t", + choices=TPS) + + class Animal(Document): + name = StringField() + tag = StringField() + + owner = EmbeddedDocumentField(Owner) + + class Ocorrence(Document): + person = StringField() + animal = CachedReferenceField( + Animal, fields=['tag', 'owner.tp']) + + Animal.drop_collection() + Ocorrence.drop_collection() + + a = Animal(name="Leopard", tag="heavy", + owner=Owner(tp='u', name="Wilson Júnior") + ) + a.save() + + o = Ocorrence(person="teste", animal=a) + o.save() + self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tp'])), { + '_id': a.pk, + 'tag': 'heavy', + 'owner': { + 't': 'u' + } + }) + self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') + self.assertEqual(o.to_mongo()['animal']['owner']['t'], 'u') + + # counts + Ocorrence(person="teste 2").save() + Ocorrence(person="teste 3").save() + + count = Ocorrence.objects( + animal__tag='heavy', animal__owner__tp='u').count() + self.assertEqual(count, 1) + + ocorrence = Ocorrence.objects( + animal__tag='heavy', + animal__owner__tp='u').first() + self.assertEqual(ocorrence.person, "teste") + self.assertTrue(isinstance(ocorrence.animal, Animal)) + + def test_cached_reference_embedded_list_fields(self): + class Owner(EmbeddedDocument): + name = StringField() + tags = ListField(StringField()) + + class Animal(Document): + name = StringField() + tag = StringField() + + owner = EmbeddedDocumentField(Owner) + + class Ocorrence(Document): + person = StringField() + animal = CachedReferenceField( + Animal, fields=['tag', 'owner.tags']) + + Animal.drop_collection() + Ocorrence.drop_collection() + + a = Animal(name="Leopard", tag="heavy", + owner=Owner(tags=['cool', 'funny'], + name="Wilson Júnior") + ) + a.save() + + o = Ocorrence(person="teste 2", animal=a) + o.save() + self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tags'])), { + '_id': a.pk, + 'tag': 'heavy', + 'owner': { + 'tags': ['cool', 'funny'] + } + }) + + self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') + self.assertEqual(o.to_mongo()['animal']['owner']['tags'], + ['cool', 'funny']) + + # counts + Ocorrence(person="teste 2").save() + Ocorrence(person="teste 3").save() + + query = Ocorrence.objects( + animal__tag='heavy', animal__owner__tags='cool')._query + self.assertEqual( + query, {'animal.owner.tags': 'cool', 'animal.tag': 'heavy'}) + + ocorrence = Ocorrence.objects( + animal__tag='heavy', + animal__owner__tags='cool').first() + self.assertEqual(ocorrence.person, "teste 2") + self.assertTrue(isinstance(ocorrence.animal, Animal)) + + if __name__ == '__main__': unittest.main() From 30e8b8186f0ee84f186bc60982bd7e703e1e657d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Thu, 2 Mar 2017 00:25:56 -0500 Subject: [PATCH 052/268] clean up document instance tests --- tests/document/instance.py | 260 +++++++++++++------------------------ 1 file changed, 91 insertions(+), 169 deletions(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 9b52c809..78d326fc 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -28,8 +28,6 @@ TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), __all__ = ("InstanceTest",) - - class InstanceTest(unittest.TestCase): def setUp(self): @@ -72,8 +70,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(field._instance, instance) def test_capped_collection(self): - """Ensure that capped collections work properly. - """ + """Ensure that capped collections work properly.""" class Log(Document): date = DateTimeField(default=datetime.now) meta = { @@ -181,8 +178,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('', repr(doc)) def test_repr_none(self): - """Ensure None values handled correctly - """ + """Ensure None values are handled correctly.""" class Article(Document): title = StringField() @@ -190,25 +186,23 @@ class InstanceTest(unittest.TestCase): return None doc = Article(title=u'привет мир') - self.assertEqual('', repr(doc)) def test_queryset_resurrects_dropped_collection(self): self.Person.drop_collection() - self.assertEqual([], list(self.Person.objects())) + # Ensure works correctly with inhertited classes class Actor(self.Person): pass - # Ensure works correctly with inhertited classes Actor.objects() self.Person.drop_collection() self.assertEqual([], list(Actor.objects())) def test_polymorphic_references(self): - """Ensure that the correct subclasses are returned from a query when - using references / generic references + """Ensure that the correct subclasses are returned from a query + when using references / generic references """ class Animal(Document): meta = {'allow_inheritance': True} @@ -258,9 +252,6 @@ class InstanceTest(unittest.TestCase): classes = [a.__class__ for a in Zoo.objects.first().animals] self.assertEqual(classes, [Animal, Fish, Mammal, Dog, Human]) - Zoo.drop_collection() - Animal.drop_collection() - def test_reference_inheritance(self): class Stats(Document): created = DateTimeField(default=datetime.now) @@ -287,8 +278,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(list_stats, CompareStats.objects.first().stats) def test_db_field_load(self): - """Ensure we load data correctly - """ + """Ensure we load data correctly from the right db field.""" class Person(Document): name = StringField(required=True) _rank = StringField(required=False, db_field="rank") @@ -307,8 +297,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Fred").rank, "Private") def test_db_embedded_doc_field_load(self): - """Ensure we load embedded document data correctly - """ + """Ensure we load embedded document data correctly.""" class Rank(EmbeddedDocument): title = StringField(required=True) @@ -333,8 +322,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Person.objects.get(name="Fred").rank, "Private") def test_custom_id_field(self): - """Ensure that documents may be created with custom primary keys. - """ + """Ensure that documents may be created with custom primary keys.""" class User(Document): username = StringField(primary_key=True) name = StringField() @@ -382,10 +370,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(user_son['_id'], 'mongo') self.assertTrue('username' not in user_son['_id']) - User.drop_collection() - def test_document_not_registered(self): - class Place(Document): name = StringField() @@ -407,7 +392,6 @@ class InstanceTest(unittest.TestCase): list(Place.objects.all()) def test_document_registry_regressions(self): - class Location(Document): name = StringField() meta = {'allow_inheritance': True} @@ -421,18 +405,16 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Area, get_document("Location.Area")) def test_creation(self): - """Ensure that document may be created using keyword arguments. - """ + """Ensure that document may be created using keyword arguments.""" person = self.Person(name="Test User", age=30) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 30) def test_to_dbref(self): - """Ensure that you can get a dbref of a document""" + """Ensure that you can get a dbref of a document.""" person = self.Person(name="Test User", age=30) self.assertRaises(OperationError, person.to_dbref) person.save() - person.to_dbref() def test_save_abstract_document(self): @@ -445,8 +427,7 @@ class InstanceTest(unittest.TestCase): Doc(name='aaa').save() def test_reload(self): - """Ensure that attributes may be reloaded. - """ + """Ensure that attributes may be reloaded.""" person = self.Person(name="Test User", age=20) person.save() @@ -479,7 +460,6 @@ class InstanceTest(unittest.TestCase): doc = Animal(superphylum='Deuterostomia') doc.save() doc.reload() - Animal.drop_collection() def test_reload_sharded_nested(self): class SuperPhylum(EmbeddedDocument): @@ -493,11 +473,9 @@ class InstanceTest(unittest.TestCase): doc = Animal(superphylum=SuperPhylum(name='Deuterostomia')) doc.save() doc.reload() - Animal.drop_collection() def test_reload_referencing(self): - """Ensures reloading updates weakrefs correctly - """ + """Ensures reloading updates weakrefs correctly.""" class Embedded(EmbeddedDocument): dict_field = DictField() list_field = ListField() @@ -569,8 +547,7 @@ class InstanceTest(unittest.TestCase): self.assertFalse("Threw wrong exception") def test_reload_of_non_strict_with_special_field_name(self): - """Ensures reloading works for documents with meta strict == False - """ + """Ensures reloading works for documents with meta strict == False.""" class Post(Document): meta = { 'strict': False @@ -591,8 +568,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(post.items, ["more lorem", "even more ipsum"]) def test_dictionary_access(self): - """Ensure that dictionary-style field access works properly. - """ + """Ensure that dictionary-style field access works properly.""" person = self.Person(name='Test User', age=30, job=self.Job()) self.assertEqual(person['name'], 'Test User') @@ -634,8 +610,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(sub_doc.to_mongo().keys(), ['id']) def test_embedded_document(self): - """Ensure that embedded documents are set up correctly. - """ + """Ensure that embedded documents are set up correctly.""" class Comment(EmbeddedDocument): content = StringField() @@ -643,8 +618,7 @@ class InstanceTest(unittest.TestCase): self.assertFalse('id' in Comment._fields) def test_embedded_document_instance(self): - """Ensure that embedded documents can reference parent instance - """ + """Ensure that embedded documents can reference parent instance.""" class Embedded(EmbeddedDocument): string = StringField() @@ -652,6 +626,7 @@ class InstanceTest(unittest.TestCase): embedded_field = EmbeddedDocumentField(Embedded) Doc.drop_collection() + doc = Doc(embedded_field=Embedded(string="Hi")) self.assertHasInstance(doc.embedded_field, doc) @@ -661,7 +636,8 @@ class InstanceTest(unittest.TestCase): def test_embedded_document_complex_instance(self): """Ensure that embedded documents in complex fields can reference - parent instance""" + parent instance. + """ class Embedded(EmbeddedDocument): string = StringField() @@ -677,8 +653,7 @@ class InstanceTest(unittest.TestCase): self.assertHasInstance(doc.embedded_field[0], doc) def test_embedded_document_complex_instance_no_use_db_field(self): - """Ensure that use_db_field is propagated to list of Emb Docs - """ + """Ensure that use_db_field is propagated to list of Emb Docs.""" class Embedded(EmbeddedDocument): string = StringField(db_field='s') @@ -690,7 +665,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(d['embedded_field'], [{'string': 'Hi'}]) def test_instance_is_set_on_setattr(self): - class Email(EmbeddedDocument): email = EmailField() @@ -698,6 +672,7 @@ class InstanceTest(unittest.TestCase): email = EmbeddedDocumentField(Email) Account.drop_collection() + acc = Account() acc.email = Email(email='test@example.com') self.assertHasInstance(acc._data["email"], acc) @@ -707,7 +682,6 @@ class InstanceTest(unittest.TestCase): self.assertHasInstance(acc1._data["email"], acc1) def test_instance_is_set_on_setattr_on_embedded_document_list(self): - class Email(EmbeddedDocument): email = EmailField() @@ -853,32 +827,28 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) def test_save(self): - """Ensure that a document may be saved in the database. - """ + """Ensure that a document may be saved in the database.""" + # Create person object and save it to the database person = self.Person(name='Test User', age=30) person.save() + # Ensure that the object is in the database collection = self.db[self.Person._get_collection_name()] person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(person_obj['name'], 'Test User') self.assertEqual(person_obj['age'], 30) self.assertEqual(person_obj['_id'], person.id) - # Test skipping validation on save + # Test skipping validation on save class Recipient(Document): email = EmailField(required=True) recipient = Recipient(email='root@localhost') self.assertRaises(ValidationError, recipient.save) - - try: - recipient.save(validate=False) - except ValidationError: - self.fail() + recipient.save(validate=False) def test_save_to_a_value_that_equates_to_false(self): - class Thing(EmbeddedDocument): count = IntField() @@ -898,7 +868,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(user.thing.count, 0) def test_save_max_recursion_not_hit(self): - class Person(Document): name = StringField() parent = ReferenceField('self') @@ -924,7 +893,6 @@ class InstanceTest(unittest.TestCase): p0.save() def test_save_max_recursion_not_hit_with_file_field(self): - class Foo(Document): name = StringField() picture = FileField() @@ -948,7 +916,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(b.picture, b.bar.picture, b.bar.bar.picture) def test_save_cascades(self): - class Person(Document): name = StringField() parent = ReferenceField('self') @@ -971,7 +938,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(p1.name, p.parent.name) def test_save_cascade_kwargs(self): - class Person(Document): name = StringField() parent = ReferenceField('self') @@ -992,7 +958,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(p1.name, p2.parent.name) def test_save_cascade_meta_false(self): - class Person(Document): name = StringField() parent = ReferenceField('self') @@ -1021,7 +986,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(p1.name, p.parent.name) def test_save_cascade_meta_true(self): - class Person(Document): name = StringField() parent = ReferenceField('self') @@ -1046,7 +1010,6 @@ class InstanceTest(unittest.TestCase): self.assertNotEqual(p1.name, p.parent.name) def test_save_cascades_generically(self): - class Person(Document): name = StringField() parent = GenericReferenceField() @@ -1072,7 +1035,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(p1.name, p.parent.name) def test_save_atomicity_condition(self): - class Widget(Document): toggle = BooleanField(default=False) count = IntField(default=0) @@ -1150,7 +1112,8 @@ class InstanceTest(unittest.TestCase): def test_update(self): """Ensure that an existing document is updated instead of be - overwritten.""" + overwritten. + """ # Create person object and save it to the database person = self.Person(name='Test User', age=30) person.save() @@ -1254,7 +1217,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(2, self.Person.objects.count()) def test_can_save_if_not_included(self): - class EmbeddedDoc(EmbeddedDocument): pass @@ -1341,10 +1303,7 @@ class InstanceTest(unittest.TestCase): doc2.update(set__name=doc1.name) def test_embedded_update(self): - """ - Test update on `EmbeddedDocumentField` fields - """ - + """Test update on `EmbeddedDocumentField` fields.""" class Page(EmbeddedDocument): log_message = StringField(verbose_name="Log message", required=True) @@ -1365,11 +1324,9 @@ class InstanceTest(unittest.TestCase): self.assertEqual(site.page.log_message, "Error: Dummy message") def test_embedded_update_db_field(self): + """Test update on `EmbeddedDocumentField` fields when db_field + is other than default. """ - Test update on `EmbeddedDocumentField` fields when db_field is other - than default. - """ - class Page(EmbeddedDocument): log_message = StringField(verbose_name="Log message", db_field="page_log_message", @@ -1392,9 +1349,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(site.page.log_message, "Error: Dummy message") def test_save_only_changed_fields(self): - """Ensure save only sets / unsets changed fields - """ - + """Ensure save only sets / unsets changed fields.""" class User(self.Person): active = BooleanField(default=True) @@ -1514,8 +1469,8 @@ class InstanceTest(unittest.TestCase): self.assertEqual(q, 3) def test_set_unset_one_operation(self): - """Ensure that $set and $unset actions are performed in the same - operation. + """Ensure that $set and $unset actions are performed in the + same operation. """ class FooBar(Document): foo = StringField(default=None) @@ -1536,9 +1491,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(1, q) def test_save_only_changed_fields_recursive(self): - """Ensure save only sets / unsets changed fields - """ - + """Ensure save only sets / unsets changed fields.""" class Comment(EmbeddedDocument): published = BooleanField(default=True) @@ -1578,8 +1531,7 @@ class InstanceTest(unittest.TestCase): self.assertFalse(person.comments_dict['first_post'].published) def test_delete(self): - """Ensure that document may be deleted using the delete method. - """ + """Ensure that document may be deleted using the delete method.""" person = self.Person(name="Test User", age=30) person.save() self.assertEqual(self.Person.objects.count(), 1) @@ -1587,33 +1539,34 @@ class InstanceTest(unittest.TestCase): self.assertEqual(self.Person.objects.count(), 0) def test_save_custom_id(self): - """Ensure that a document may be saved with a custom _id. - """ + """Ensure that a document may be saved with a custom _id.""" + # Create person object and save it to the database person = self.Person(name='Test User', age=30, id='497ce96f395f2f052a494fd4') person.save() + # Ensure that the object is in the database with the correct _id collection = self.db[self.Person._get_collection_name()] person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') def test_save_custom_pk(self): - """ - Ensure that a document may be saved with a custom _id using pk alias. + """Ensure that a document may be saved with a custom _id using + pk alias. """ # Create person object and save it to the database person = self.Person(name='Test User', age=30, pk='497ce96f395f2f052a494fd4') person.save() + # Ensure that the object is in the database with the correct _id collection = self.db[self.Person._get_collection_name()] person_obj = collection.find_one({'name': 'Test User'}) self.assertEqual(str(person_obj['_id']), '497ce96f395f2f052a494fd4') def test_save_list(self): - """Ensure that a list field may be properly saved. - """ + """Ensure that a list field may be properly saved.""" class Comment(EmbeddedDocument): content = StringField() @@ -1636,8 +1589,6 @@ class InstanceTest(unittest.TestCase): for comment_obj, comment in zip(post_obj['comments'], comments): self.assertEqual(comment_obj['content'], comment['content']) - BlogPost.drop_collection() - def test_list_search_by_embedded(self): class User(Document): username = StringField(required=True) @@ -1697,8 +1648,8 @@ class InstanceTest(unittest.TestCase): list(Page.objects.filter(comments__user=u3))) def test_save_embedded_document(self): - """Ensure that a document with an embedded document field may be - saved in the database. + """Ensure that a document with an embedded document field may + be saved in the database. """ class EmployeeDetails(EmbeddedDocument): position = StringField() @@ -1717,13 +1668,13 @@ class InstanceTest(unittest.TestCase): employee_obj = collection.find_one({'name': 'Test Employee'}) self.assertEqual(employee_obj['name'], 'Test Employee') self.assertEqual(employee_obj['age'], 50) + # Ensure that the 'details' embedded object saved correctly self.assertEqual(employee_obj['details']['position'], 'Developer') def test_embedded_update_after_save(self): - """ - Test update of `EmbeddedDocumentField` attached to a newly saved - document. + """Test update of `EmbeddedDocumentField` attached to a newly + saved document. """ class Page(EmbeddedDocument): log_message = StringField(verbose_name="Log message", @@ -1744,8 +1695,8 @@ class InstanceTest(unittest.TestCase): self.assertEqual(site.page.log_message, "Error: Dummy message") def test_updating_an_embedded_document(self): - """Ensure that a document with an embedded document field may be - saved in the database. + """Ensure that a document with an embedded document field may + be saved in the database. """ class EmployeeDetails(EmbeddedDocument): position = StringField() @@ -1780,7 +1731,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(promoted_employee.details, None) def test_object_mixins(self): - class NameMixin(object): name = StringField() @@ -1819,9 +1769,9 @@ class InstanceTest(unittest.TestCase): self.assertEqual(t.count, 12) def test_save_reference(self): - """Ensure that a document reference field may be saved in the database. + """Ensure that a document reference field may be saved in the + database. """ - class BlogPost(Document): meta = {'collection': 'blogpost_1'} content = StringField() @@ -1852,8 +1802,6 @@ class InstanceTest(unittest.TestCase): author = list(self.Person.objects(name='Test User'))[-1] self.assertEqual(author.age, 25) - BlogPost.drop_collection() - def test_duplicate_db_fields_raise_invalid_document_error(self): """Ensure a InvalidDocumentError is thrown if duplicate fields declare the same db_field. @@ -1864,7 +1812,7 @@ class InstanceTest(unittest.TestCase): name2 = StringField(db_field='name') def test_invalid_son(self): - """Raise an error if loading invalid data""" + """Raise an error if loading invalid data.""" class Occurrence(EmbeddedDocument): number = IntField() @@ -1887,9 +1835,9 @@ class InstanceTest(unittest.TestCase): Word._from_son('this is not a valid SON dict') def test_reverse_delete_rule_cascade_and_nullify(self): - """Ensure that a referenced document is also deleted upon deletion. + """Ensure that a referenced document is also deleted upon + deletion. """ - class BlogPost(Document): content = StringField() author = ReferenceField(self.Person, reverse_delete_rule=CASCADE) @@ -1944,7 +1892,8 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Book.objects.count(), 0) def test_reverse_delete_rule_with_shared_id_among_collections(self): - """Ensure that cascade delete rule doesn't mix id among collections. + """Ensure that cascade delete rule doesn't mix id among + collections. """ class User(Document): id = IntField(primary_key=True) @@ -1975,10 +1924,9 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Book.objects.get(), book_2) def test_reverse_delete_rule_with_document_inheritance(self): - """Ensure that a referenced document is also deleted upon deletion - of a child document. + """Ensure that a referenced document is also deleted upon + deletion of a child document. """ - class Writer(self.Person): pass @@ -2010,10 +1958,9 @@ class InstanceTest(unittest.TestCase): self.assertEqual(BlogPost.objects.count(), 0) def test_reverse_delete_rule_cascade_and_nullify_complex_field(self): - """Ensure that a referenced document is also deleted upon deletion for - complex fields. + """Ensure that a referenced document is also deleted upon + deletion for complex fields. """ - class BlogPost(Document): content = StringField() authors = ListField(ReferenceField( @@ -2022,7 +1969,6 @@ class InstanceTest(unittest.TestCase): self.Person, reverse_delete_rule=NULLIFY)) self.Person.drop_collection() - BlogPost.drop_collection() author = self.Person(name='Test User') @@ -2046,10 +1992,10 @@ class InstanceTest(unittest.TestCase): self.assertEqual(BlogPost.objects.count(), 0) def test_reverse_delete_rule_cascade_triggers_pre_delete_signal(self): - """ ensure the pre_delete signal is triggered upon a cascading deletion - setup a blog post with content, an author and editor - delete the author which triggers deletion of blogpost via cascade - blog post's pre_delete signal alters an editor attribute + """Ensure the pre_delete signal is triggered upon a cascading + deletion setup a blog post with content, an author and editor + delete the author which triggers deletion of blogpost via + cascade blog post's pre_delete signal alters an editor attribute. """ class Editor(self.Person): review_queue = IntField(default=0) @@ -2077,6 +2023,7 @@ class InstanceTest(unittest.TestCase): # delete the author, the post is also deleted due to the CASCADE rule author.delete() + # the pre-delete signal should have decremented the editor's queue editor = Editor.objects(name='Max P.').get() self.assertEqual(editor.review_queue, 0) @@ -2085,7 +2032,6 @@ class InstanceTest(unittest.TestCase): """Ensure that Bi-Directional relationships work with reverse_delete_rule """ - class Bar(Document): content = StringField() foo = ReferenceField('Foo') @@ -2131,8 +2077,8 @@ class InstanceTest(unittest.TestCase): mother = ReferenceField('Person', reverse_delete_rule=DENY) def test_reverse_delete_rule_cascade_recurs(self): - """Ensure that a chain of documents is also deleted upon cascaded - deletion. + """Ensure that a chain of documents is also deleted upon + cascaded deletion. """ class BlogPost(Document): content = StringField() @@ -2162,15 +2108,10 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(Comment.objects.count(), 0) - self.Person.drop_collection() - BlogPost.drop_collection() - Comment.drop_collection() - def test_reverse_delete_rule_deny(self): - """Ensure that a document cannot be referenced if there are still - documents referring to it. + """Ensure that a document cannot be referenced if there are + still documents referring to it. """ - class BlogPost(Document): content = StringField() author = ReferenceField(self.Person, reverse_delete_rule=DENY) @@ -2198,11 +2139,7 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(self.Person.objects.count(), 1) - self.Person.drop_collection() - BlogPost.drop_collection() - def subclasses_and_unique_keys_works(self): - class A(Document): pass @@ -2218,12 +2155,9 @@ class InstanceTest(unittest.TestCase): self.assertEqual(A.objects.count(), 2) self.assertEqual(B.objects.count(), 1) - A.drop_collection() - B.drop_collection() def test_document_hash(self): - """Test document in list, dict, set - """ + """Test document in list, dict, set.""" class User(Document): pass @@ -2266,11 +2200,9 @@ class InstanceTest(unittest.TestCase): # in Set all_user_set = set(User.objects.all()) - self.assertTrue(u1 in all_user_set) def test_picklable(self): - pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleEmbedded() pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved @@ -2296,7 +2228,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(pickle_doc.lists, ["1", "2", "3"]) def test_regular_document_pickle(self): - pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved pickle_doc.save() @@ -2319,7 +2250,6 @@ class InstanceTest(unittest.TestCase): fixtures.PickleTest = PickleTest def test_dynamic_document_pickle(self): - pickle_doc = PickleDynamicTest( name="test", number=1, string="One", lists=['1', '2']) pickle_doc.embedded = PickleDynamicEmbedded(foo="Bar") @@ -2358,7 +2288,6 @@ class InstanceTest(unittest.TestCase): validate = DictField() def test_mutating_documents(self): - class B(EmbeddedDocument): field1 = StringField(default='field1') @@ -2366,6 +2295,7 @@ class InstanceTest(unittest.TestCase): b = EmbeddedDocumentField(B, default=lambda: B()) A.drop_collection() + a = A() a.save() a.reload() @@ -2389,12 +2319,13 @@ class InstanceTest(unittest.TestCase): self.assertEqual(a.b.field2.c_field, 'new value') def test_can_save_false_values(self): - """Ensures you can save False values on save""" + """Ensures you can save False values on save.""" class Doc(Document): foo = StringField() archived = BooleanField(default=False, required=True) Doc.drop_collection() + d = Doc() d.save() d.archived = False @@ -2403,11 +2334,12 @@ class InstanceTest(unittest.TestCase): self.assertEqual(Doc.objects(archived=False).count(), 1) def test_can_save_false_values_dynamic(self): - """Ensures you can save False values on dynamic docs""" + """Ensures you can save False values on dynamic docs.""" class Doc(DynamicDocument): foo = StringField() Doc.drop_collection() + d = Doc() d.save() d.archived = False @@ -2447,7 +2379,7 @@ class InstanceTest(unittest.TestCase): Collection.update = orig_update def test_db_alias_tests(self): - """ DB Alias tests """ + """DB Alias tests.""" # mongoenginetest - Is default connection alias from setUp() # Register Aliases register_connection('testdb-1', 'mongoenginetest2') @@ -2509,8 +2441,7 @@ class InstanceTest(unittest.TestCase): get_db("testdb-3")[AuthorBooks._get_collection_name()]) def test_db_alias_overrides(self): - """db_alias can be overriden - """ + """Test db_alias can be overriden.""" # Register a connection with db_alias testdb-2 register_connection('testdb-2', 'mongoenginetest2') @@ -2534,8 +2465,7 @@ class InstanceTest(unittest.TestCase): B._get_collection().database.name) def test_db_alias_propagates(self): - """db_alias propagates? - """ + """db_alias propagates?""" register_connection('testdb-1', 'mongoenginetest2') class A(Document): @@ -2548,8 +2478,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual('testdb-1', B._meta.get('db_alias')) def test_db_ref_usage(self): - """ DB Ref usage in dict_fields""" - + """DB Ref usage in dict_fields.""" class User(Document): name = StringField() @@ -2784,7 +2713,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(user.thing._data['data'], [1, 2, 3]) def test_spaces_in_keys(self): - class Embedded(DynamicEmbeddedDocument): pass @@ -2873,7 +2801,6 @@ class InstanceTest(unittest.TestCase): log.machine = "127.0.0.1" def test_kwargs_simple(self): - class Embedded(EmbeddedDocument): name = StringField() @@ -2893,7 +2820,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc._data, dict_doc._data) def test_kwargs_complex(self): - class Embedded(EmbeddedDocument): name = StringField() @@ -2916,36 +2842,35 @@ class InstanceTest(unittest.TestCase): self.assertEqual(classic_doc._data, dict_doc._data) def test_positional_creation(self): - """Ensure that document may be created using positional arguments. - """ + """Ensure that document may be created using positional arguments.""" person = self.Person("Test User", 42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) def test_mixed_creation(self): - """Ensure that document may be created using mixed arguments. - """ + """Ensure that document may be created using mixed arguments.""" person = self.Person("Test User", age=42) self.assertEqual(person.name, "Test User") self.assertEqual(person.age, 42) def test_positional_creation_embedded(self): - """Ensure that embedded document may be created using positional arguments. + """Ensure that embedded document may be created using positional + arguments. """ job = self.Job("Test Job", 4) self.assertEqual(job.name, "Test Job") self.assertEqual(job.years, 4) def test_mixed_creation_embedded(self): - """Ensure that embedded document may be created using mixed arguments. + """Ensure that embedded document may be created using mixed + arguments. """ job = self.Job("Test Job", years=4) self.assertEqual(job.name, "Test Job") self.assertEqual(job.years, 4) def test_mixed_creation_dynamic(self): - """Ensure that document may be created using mixed arguments. - """ + """Ensure that document may be created using mixed arguments.""" class Person(DynamicDocument): name = StringField() @@ -2954,14 +2879,14 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.age, 42) def test_bad_mixed_creation(self): - """Ensure that document gives correct error when duplicating arguments + """Ensure that document gives correct error when duplicating + arguments. """ with self.assertRaises(TypeError): return self.Person("Test User", 42, name="Bad User") def test_data_contains_id_field(self): - """Ensure that asking for _data returns 'id' - """ + """Ensure that asking for _data returns 'id'.""" class Person(Document): name = StringField() @@ -2973,7 +2898,6 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person._data.get('id'), person.id) def test_complex_nesting_document_and_embedded_document(self): - class Macro(EmbeddedDocument): value = DynamicField(default="UNDEFINED") @@ -3016,7 +2940,6 @@ class InstanceTest(unittest.TestCase): system.nodes["node"].parameters["param"].macros["test"].value) def test_embedded_document_equality(self): - class Test(Document): field = StringField(required=True) @@ -3202,8 +3125,7 @@ class InstanceTest(unittest.TestCase): self.assertEqual(idx, 2) def test_falsey_pk(self): - """Ensure that we can create and update a document with Falsey PK. - """ + """Ensure that we can create and update a document with Falsey PK.""" class Person(Document): age = IntField(primary_key=True) height = FloatField() From 19ef2be88b65d5f2103fded394b3c9b0b9b6973b Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 5 Mar 2017 00:05:33 -0500 Subject: [PATCH 053/268] fix #937 --- docs/guide/querying.rst | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 980947df..0bb19658 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -340,14 +340,19 @@ Javascript code that is executed on the database server. Counting results ---------------- -Just as with limiting and skipping results, there is a method on -:class:`~mongoengine.queryset.QuerySet` objects -- -:meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic -way of achieving this:: +Just as with limiting and skipping results, there is a method on a +:class:`~mongoengine.queryset.QuerySet` object -- +:meth:`~mongoengine.queryset.QuerySet.count`:: - num_users = len(User.objects) + num_users = User.objects.count() -Even if len() is the Pythonic way of counting results, keep in mind that if you concerned about performance, :meth:`~mongoengine.queryset.QuerySet.count` is the way to go since it only execute a server side count query, while len() retrieves the results, places them in cache, and finally counts them. If we compare the performance of the two operations, len() is much slower than :meth:`~mongoengine.queryset.QuerySet.count`. +You could technically use ``len(User.objects)`` to get the same result, but it +would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`. +When you execute a server-side count query, you let MongoDB do the heavy +lifting and you receive a single integer over the wire. Meanwhile, len() +retrieves all the results, places them in a local cache, and finally counts +them. If we compare the performance of the two operations, len() is much slower +than :meth:`~mongoengine.queryset.QuerySet.count`. Further aggregation ------------------- From e8ea29496401bc618c96339d43ae5480abb52851 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 5 Mar 2017 18:12:01 -0500 Subject: [PATCH 054/268] test negative indexes (closes #1119) --- tests/fields/fields.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ed7baa2b..8e0da410 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1173,6 +1173,14 @@ class FieldTest(MongoDBTestCase): post.reload() self.assertEqual(post.info, ['0', '1', '2', '3', 'a', '5']) + # __setitem__(index, value) with a negative index + reset_post() + post.info[-2] = 'a' + self.assertEqual(post.info, ['0', '1', '2', '3', 'a', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', '1', '2', '3', 'a', '5']) + # '__setitem__(slice(i, j), listB)' # aka 'listA[i:j] = listB' # aka 'setitem(listA, slice(i, j), listB)' @@ -1183,6 +1191,16 @@ class FieldTest(MongoDBTestCase): post.reload() self.assertEqual(post.info, ['0', 'h', 'e', 'l', 'l', 'o', '3', '4', '5']) + # '__setitem__(slice(i, j), listB)' with negative i and j + reset_post() + post.info[-5:-3] = ['h', 'e', 'l', 'l', 'o'] + self.assertEqual(post.info, ['0', 'h', 'e', 'l', 'l', 'o', '3', '4', '5']) + post.save() + post.reload() + self.assertEqual(post.info, ['0', 'h', 'e', 'l', 'l', 'o', '3', '4', '5']) + + # negative + # 'append' reset_post() post.info.append('h') From f9ccf635ca17e7f30efaeccba37b32c7ea062157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 5 Mar 2017 18:20:09 -0500 Subject: [PATCH 055/268] Respect db fields in multiple layers of embedded docs (#1501) --- mongoengine/base/document.py | 9 ++++++-- tests/fields/fields.py | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 9d366706..ce96837a 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -684,8 +684,13 @@ class BaseDocument(object): # class if unavailable class_name = son.get('_cls', cls._class_name) - # Convert SON to a dict, making sure each key is a string - data = {str(key): value for key, value in son.iteritems()} + # Convert SON to a data dict, making sure each key is a string and + # corresponds to the right db field. + data = {} + for key, value in son.iteritems(): + key = str(key) + key = cls._db_field_map.get(key, key) + data[key] = value # Return correct subclass for document type if class_name != cls._class_name: diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 8e0da410..2305f3af 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -1901,6 +1901,51 @@ class FieldTest(MongoDBTestCase): doc = self.db.test.find_one() self.assertEqual(doc['x']['i'], 2) + def test_double_embedded_db_field(self): + """Make sure multiple layers of embedded docs resolve db fields + properly and can be initialized using dicts. + """ + class C(EmbeddedDocument): + txt = StringField() + + class B(EmbeddedDocument): + c = EmbeddedDocumentField(C, db_field='fc') + + class A(Document): + b = EmbeddedDocumentField(B, db_field='fb') + + a = A( + b=B( + c=C(txt='hi') + ) + ) + a.validate() + + a = A(b={'c': {'txt': 'hi'}}) + a.validate() + + def test_double_embedded_db_field_from_son(self): + """Make sure multiple layers of embedded docs resolve db fields + from SON properly. + """ + class C(EmbeddedDocument): + txt = StringField() + + class B(EmbeddedDocument): + c = EmbeddedDocumentField(C, db_field='fc') + + class A(Document): + b = EmbeddedDocumentField(B, db_field='fb') + + a = A._from_son(SON([ + ('fb', SON([ + ('fc', SON([ + ('txt', 'hi') + ])) + ])) + ])) + self.assertEqual(a.b.c.txt, 'hi') + def test_embedded_document_validation(self): """Ensure that invalid embedded documents cannot be assigned to embedded document fields. From 49035543b996a7e802e95b36c5cef249904f6e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 5 Mar 2017 21:17:53 -0500 Subject: [PATCH 056/268] cleanup BaseQuerySet.__getitem__ (#1502) --- mongoengine/queryset/base.py | 57 ++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index fde46ae6..7c60a489 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -158,44 +158,49 @@ class BaseQuerySet(object): # self._cursor def __getitem__(self, key): - """Support skip and limit using getitem and slicing syntax.""" + """Return a document instance corresponding to a given index if + the key is an integer. If the key is a slice, translate its + bounds into a skip and a limit, and return a cloned queryset + with that skip/limit applied. For example: + + >>> User.objects[0] + + >>> User.objects[1:3] + [, ] + """ queryset = self.clone() - # Slice provided + # Handle a slice if isinstance(key, slice): - try: - queryset._cursor_obj = queryset._cursor[key] - queryset._skip, queryset._limit = key.start, key.stop - if key.start and key.stop: - queryset._limit = key.stop - key.start - except IndexError as err: - # PyMongo raises an error if key.start == key.stop, catch it, - # bin it, kill it. - start = key.start or 0 - if start >= 0 and key.stop >= 0 and key.step is None: - if start == key.stop: - queryset.limit(0) - queryset._skip = key.start - queryset._limit = key.stop - start - return queryset - raise err + queryset._cursor_obj = queryset._cursor[key] + queryset._skip, queryset._limit = key.start, key.stop + if key.start and key.stop: + queryset._limit = key.stop - key.start + # Allow further QuerySet modifications to be performed return queryset - # Integer index provided + + # Handle an index elif isinstance(key, int): if queryset._scalar: return queryset._get_scalar( - queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference, - only_fields=self.only_fields)) + queryset._document._from_son( + queryset._cursor[key], + _auto_dereference=self._auto_dereference, + only_fields=self.only_fields + ) + ) if queryset._as_pymongo: return queryset._get_as_pymongo(queryset._cursor[key]) - return queryset._document._from_son(queryset._cursor[key], - _auto_dereference=self._auto_dereference, - only_fields=self.only_fields) - raise AttributeError + return queryset._document._from_son( + queryset._cursor[key], + _auto_dereference=self._auto_dereference, + only_fields=self.only_fields + ) + + raise AttributeError('Provide a slice or an integer index') def __iter__(self): raise NotImplementedError From b78010aa940af31a4b36bd074965c037d78eee0d Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 5 Mar 2017 21:24:40 -0500 Subject: [PATCH 057/268] remove test_last_field_name_like_operator (it's a dupe of the same test in tests/queryset/transform.py) --- tests/queryset/queryset.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index b496e04a..3ae3fb42 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4962,20 +4962,6 @@ class QuerySetTest(unittest.TestCase): for p in Person.objects(): self.assertEqual(p.name, 'a') - def test_last_field_name_like_operator(self): - class EmbeddedItem(EmbeddedDocument): - type = StringField() - - class Doc(Document): - item = EmbeddedDocumentField(EmbeddedItem) - - Doc.drop_collection() - - doc = Doc(item=EmbeddedItem(type="axe")) - doc.save() - - self.assertEqual(1, Doc.objects(item__type__="axe").count()) - def test_len_during_iteration(self): """Tests that calling len on a queyset during iteration doesn't stop paging. From f2fe58c3c5d01e49accc7c48356adbb6694d8693 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 10:25:41 +0000 Subject: [PATCH 058/268] Added a condition to store data to ObjectDict when the items type is it Previous dereference implementation re-contains data as `dict` except for the predicted type. But the OrderedDict is not predicted, so the its data would be converted `dict` implicitly. As the result, the order of stored data get wrong. And this patch prevents it. --- mongoengine/dereference.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 59204d4d..c11449b9 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -1,3 +1,4 @@ +from collections import OrderedDict from bson import DBRef, SON import six @@ -201,6 +202,10 @@ class DeReference(object): as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] + elif type(items) == OrderedDict: + is_list = False + iterator = items.iteritems() + data = OrderedDict() else: is_list = False iterator = items.iteritems() From 9cd3dcdebf36728fc1ba8958bd69ccd3edff1d24 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 14:45:43 +0000 Subject: [PATCH 059/268] Added a test for the change of the condition in DeReference processing This checks DBRef conversion using DynamicField with the ordering guarantee. --- tests/test_dereference.py | 70 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 7f58a85b..9a976611 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -2,10 +2,15 @@ import unittest from bson import DBRef, ObjectId +from collections import OrderedDict from mongoengine import * from mongoengine.connection import get_db from mongoengine.context_managers import query_counter +from mongoengine.python_support import IS_PYMONGO_3 +from mongoengine.base import TopLevelDocumentMetaclass +if IS_PYMONGO_3: + from bson import CodecOptions class FieldTest(unittest.TestCase): @@ -1287,5 +1292,70 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) + def test_dynamic_field_dereference(self): + class Merchandise(Document): + name = StringField() + price = IntField() + + class Store(Document): + merchandises = DynamicField() + + Merchandise.drop_collection() + Store.drop_collection() + + merchandises = { + '#1': Merchandise(name='foo', price=100).save(), + '#2': Merchandise(name='bar', price=120).save(), + '#3': Merchandise(name='baz', price=110).save(), + } + Store(merchandises=merchandises).save() + + store = Store.objects().first() + for obj in store.merchandises.values(): + self.assertFalse(isinstance(obj, Merchandise)) + + store.select_related() + for obj in store.merchandises.values(): + self.assertTrue(isinstance(obj, Merchandise)) + + def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self): + # This is because 'codec_options' is supported on pymongo3 or later + if IS_PYMONGO_3: + class OrderedDocument(Document): + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + opts = CodecOptions(document_class=OrderedDict) + + return collection.with_options(codec_options=opts) + + class Merchandise(Document): + name = StringField() + price = IntField() + + class Store(OrderedDocument): + merchandises = DynamicField(container_class=OrderedDict) + + Merchandise.drop_collection() + Store.drop_collection() + + merchandises = OrderedDict() + merchandises['#1'] = Merchandise(name='foo', price=100).save() + merchandises['#2'] = Merchandise(name='bar', price=120).save() + merchandises['#3'] = Merchandise(name='baz', price=110).save() + + Store(merchandises=merchandises).save() + + store = Store.objects().first() + + store.select_related() + + # confirms that the load data order is same with the one at storing + self.assertTrue(type(store.merchandises), OrderedDict) + self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3') + if __name__ == '__main__': unittest.main() From ffbb2c96896a184032807ab00dbcc0ca89ece77a Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Wed, 8 Mar 2017 14:46:04 +0000 Subject: [PATCH 060/268] This is Additional tests for the container_class parameter of DynamicField This tests DynamicField dereference with ordering guarantee. --- tests/fields/fields.py | 50 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 2db37317..2c0d1228 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -5,6 +5,7 @@ import uuid import math import itertools import re +import pymongo from nose.plugins.skip import SkipTest from collections import OrderedDict @@ -26,9 +27,12 @@ except ImportError: from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, - _document_registry) + _document_registry, TopLevelDocumentMetaclass) -from tests.utils import MongoDBTestCase +from tests.utils import MongoDBTestCase, MONGO_TEST_DB +from mongoengine.python_support import IS_PYMONGO_3 +if IS_PYMONGO_3: + from bson import CodecOptions __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") @@ -4513,18 +4517,54 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): Doc.drop_collection() - doc = Doc(ordered_data=OrderedDict(raw_data), - unordered_data=dict(raw_data)).save() + doc = Doc(ordered_data=OrderedDict(raw_data), unordered_data=dict(raw_data)).save() + # checks that the data is in order self.assertEqual(type(doc.ordered_data), OrderedDict) self.assertEqual(type(doc.unordered_data), dict) - self.assertEqual([k for k,_ in doc.ordered_data.items()], ['d', 'c', 'b', 'a']) + self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') + + # checks that the data is stored to the database in order + pymongo_db = pymongo.MongoClient()[MONGO_TEST_DB] + if IS_PYMONGO_3: + codec_option = CodecOptions(document_class=OrderedDict) + db_doc = pymongo_db.doc.with_options(codec_options=codec_option).find_one() + else: + db_doc = pymongo_db.doc.find_one(as_class=OrderedDict) + + self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') def test_dynamicfield_with_wrong_container_class(self): with self.assertRaises(ValidationError): class DocWithInvalidField: data = DynamicField(container_class=list) + def test_dynamicfield_with_wrong_container_class_and_reload_docuemnt(self): + # This is because 'codec_options' is supported on pymongo3 or later + if IS_PYMONGO_3: + class OrderedDocument(Document): + my_metaclass = TopLevelDocumentMetaclass + __metaclass__ = TopLevelDocumentMetaclass + + @classmethod + def _get_collection(cls): + collection = super(OrderedDocument, cls)._get_collection() + opts = CodecOptions(document_class=OrderedDict) + + return collection.with_options(codec_options=opts) + + raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] + + class Doc(OrderedDocument): + data = DynamicField(container_class=OrderedDict) + + Doc.drop_collection() + + doc = Doc(data=OrderedDict(raw_data)).save() + doc.reload() + + self.assertEqual(type(doc.data), OrderedDict) + self.assertEqual(','.join(doc.data.keys()), 'd,c,b,a') if __name__ == '__main__': unittest.main() From 58f293fef33ce9fe3f3c536dd831d7f71b95e623 Mon Sep 17 00:00:00 2001 From: Richard Fortescue-Webb Date: Wed, 29 Mar 2017 10:34:50 +0100 Subject: [PATCH 061/268] Allow ReferenceFields to take ObjectIds --- mongoengine/fields.py | 4 ++-- tests/fields/fields.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 11425095..b67b385d 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -998,8 +998,8 @@ class ReferenceField(BaseField): def validate(self, value): - if not isinstance(value, (self.document_type, DBRef)): - self.error('A ReferenceField only accepts DBRef or documents') + if not isinstance(value, (self.document_type, DBRef, ObjectId)): + self.error('A ReferenceField only accepts DBRef, ObjectId or documents') if isinstance(value, Document) and value.id is None: self.error('You can only reference documents once they have been ' diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 2305f3af..5f49596a 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2088,6 +2088,12 @@ class FieldTest(MongoDBTestCase): post1.author = post2 self.assertRaises(ValidationError, post1.validate) + # Ensure ObjectID's are accepted as references + user_object_id = user.pk + post3 = BlogPost(content="Chips and curry sauce taste good.") + post3.author = user_object_id + post3.save() + # Make sure referencing a saved document of the right type works user.save() post1.author = user @@ -2098,6 +2104,20 @@ class FieldTest(MongoDBTestCase): post1.author = post2 self.assertRaises(ValidationError, post1.validate) + def test_objectid_reference_fields(self): + """Make sure storing Object ID references works.""" + class Person(Document): + name = StringField() + parent = ReferenceField('self') + + Person.drop_collection() + + p1 = Person(name="John").save() + Person(name="Ross", parent=p1).save() + + p = Person.objects.get(name="Ross") + self.assertEqual(p.parent, p1) + def test_dbref_reference_fields(self): """Make sure storing references as bson.dbref.DBRef works.""" class Person(Document): From 5713de896674110c73f90d0a2016d4410a79d6e3 Mon Sep 17 00:00:00 2001 From: Richard Fortescue-Webb Date: Wed, 29 Mar 2017 11:34:57 +0100 Subject: [PATCH 062/268] Use the objectid in the test --- tests/fields/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 5f49596a..f4ad0fa2 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2113,7 +2113,7 @@ class FieldTest(MongoDBTestCase): Person.drop_collection() p1 = Person(name="John").save() - Person(name="Ross", parent=p1).save() + Person(name="Ross", parent=p1.pk).save() p = Person.objects.get(name="Ross") self.assertEqual(p.parent, p1) From d8b238d5f172ec058952046b50921d879e7bc826 Mon Sep 17 00:00:00 2001 From: Hiroyasu OHYAMA Date: Thu, 6 Apr 2017 00:42:11 +0000 Subject: [PATCH 063/268] Refactored the implementation of DynamicField extension for storing data in order --- mongoengine/dereference.py | 2 +- mongoengine/fields.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index c11449b9..f30b2c15 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -202,7 +202,7 @@ class DeReference(object): as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] - elif type(items) == OrderedDict: + elif isinstance(items, OrderedDict): is_list = False iterator = items.iteritems() data = OrderedDict() diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 70061e08..a3b0f883 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,6 +5,7 @@ import re import time import uuid import warnings +from collections import Mapping from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -621,7 +622,7 @@ class DynamicField(BaseField): def __init__(self, container_class=dict, *args, **kwargs): self._container_cls = container_class - if not issubclass(self._container_cls, dict): + if not issubclass(self._container_cls, Mapping): self.error('The class that is specified in `container_class` parameter ' 'must be a subclass of `dict`.') From 888a6da4a58b0b0e6d62ba626af18e0993102f57 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Fri, 7 Apr 2017 10:18:39 -0400 Subject: [PATCH 064/268] update the changelog and bump the version to v0.12.0 --- docs/changelog.rst | 12 +++++++++++- docs/upgrade.rst | 3 +++ mongoengine/__init__.py | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0da97e90..8764b8e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,11 +5,21 @@ Changelog Development =========== - (Fill this out as you fix issues and develop your features). -- Fixed using sets in field choices #1481 + +Changes in 0.12.0 +================= - POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476 - POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476 - Fixed connecting to a replica set with PyMongo 2.x #1436 +- Fixed using sets in field choices #1481 +- Fixed deleting items from a `ListField` #1318 - Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237 +- Fixed behavior of a `dec` update operator #1450 +- Added a `rename` update operator #1454 +- Added validation for the `db_field` parameter #1448 +- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440 +- Fixed the error message displayed when validating unicode URLs #1486 +- Raise an error when trying to save an abstract document #1449 Changes in 0.11.0 ================= diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 17b1c4ac..a9a547e4 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -6,6 +6,9 @@ Development *********** (Fill this out whenever you introduce breaking changes to MongoEngine) + +0.12.0 +****** This release includes various fixes for the `BaseQuerySet` methods and how they are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size to an already-existing queryset wouldn't modify the underlying PyMongo cursor. diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index f8969592..0feb49e0 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 11, 0) +VERSION = (0, 12, 0) def get_version(): From b52d3e3a7ba36b65b18952a2ce114c4a07a42151 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Fri, 7 Apr 2017 10:34:04 -0400 Subject: [PATCH 065/268] added one more item to the v0.12.0 changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8764b8e9..2b07ed53 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ Changes in 0.12.0 ================= - POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476 - POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476 +- Fixed the way `Document.objects.create` works with duplicate IDs #1485 - Fixed connecting to a replica set with PyMongo 2.x #1436 - Fixed using sets in field choices #1481 - Fixed deleting items from a `ListField` #1318 From 466935e9a3d3d45551efa42927035eba9077bd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 16 Apr 2017 13:58:58 -0400 Subject: [PATCH 066/268] Unicode support in EmailField (#1527) --- mongoengine/fields.py | 105 +++++++++++++++++++++++++++++++++---- tests/document/instance.py | 2 +- tests/fields/fields.py | 101 ++++++++++++++++++++++++++++++----- 3 files changed, 183 insertions(+), 25 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 06c56f06..b631fc48 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -2,6 +2,7 @@ import datetime import decimal import itertools import re +import socket import time import uuid import warnings @@ -154,21 +155,105 @@ class EmailField(StringField): .. versionadded:: 0.4 """ - - EMAIL_REGEX = re.compile( - # dot-atom - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" - # quoted-string - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' - # domain (max length of an ICAAN TLD is 22 characters) - r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(? Date: Sun, 16 Apr 2017 14:08:42 -0400 Subject: [PATCH 067/268] bump version to v0.13.0 and fill in the changelog and the upgrade docs --- docs/changelog.rst | 5 +++++ docs/upgrade.rst | 11 +++++++++++ mongoengine/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2b07ed53..46d47b97 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,11 @@ Development =========== - (Fill this out as you fix issues and develop your features). +Changes in 0.13.0 +================= +- POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see + docs/upgrade.rst for details. + Changes in 0.12.0 ================= - POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476 diff --git a/docs/upgrade.rst b/docs/upgrade.rst index a9a547e4..d612df88 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -6,6 +6,17 @@ Development *********** (Fill this out whenever you introduce breaking changes to MongoEngine) +0.13.0 +****** +This release adds Unicode support to the `EmailField` and changes its +structure significantly. Previously, email addresses containing Unicode +characters didn't work at all. Starting with v0.13.0, domains with Unicode +characters are supported out of the box, meaning some emails that previously +didn't pass validation now do. Make sure the rest of your application can +accept such email addresses. Additionally, if you subclassed the `EmailField` +in your application and overrode `EmailField.EMAIL_REGEX`, you will have to +adjust your code to override `EmailField.USER_REGEX`, `EmailField.DOMAIN_REGEX`, +and potentially `EmailField.UTF8_USER_REGEX`. 0.12.0 ****** diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 0feb49e0..840c90d7 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 12, 0) +VERSION = (0, 13, 0) def get_version(): From 358b80d782e488a614b2e6ee125a5205dc1ed285 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 20 Apr 2017 08:11:38 -0500 Subject: [PATCH 068/268] Make the tutorial slightly more intuitive (#1530) --- docs/tutorial.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index fc92ce0c..cc5b647d 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -206,7 +206,10 @@ object:: ross.last_name = 'Lawley' ross.save() -Now that we've got our user in the database, let's add a couple of posts:: +Assign another user to a variable called ``john``, just like we did above with +``ross``. + +Now that we've got our users in the database, let's add a couple of posts:: post1 = TextPost(title='Fun with MongoEngine', author=john) post1.content = 'Took a look at MongoEngine today, looks pretty cool.' From bd4a603e16187478af6950a182cb2d1dabfeb025 Mon Sep 17 00:00:00 2001 From: Artemiy Date: Thu, 20 Apr 2017 17:27:37 +0300 Subject: [PATCH 069/268] Use De Morgan's laws to simplify an expression. (#1531) --- mongoengine/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index b631fc48..0010f818 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -608,9 +608,9 @@ class EmbeddedDocumentField(BaseField): """ def __init__(self, document_type, **kwargs): - if ( - not isinstance(document_type, six.string_types) and - not issubclass(document_type, EmbeddedDocument) + if not ( + isinstance(document_type, six.string_types) or + issubclass(document_type, EmbeddedDocument) ): self.error('Invalid embedded document class provided to an ' 'EmbeddedDocumentField') From 1a3c70ce1b32088a83ef40ae81b439a337e0441b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 30 Apr 2017 13:30:21 -0400 Subject: [PATCH 070/268] make EmbeddedDocument not hashable by default (#1528) --- mongoengine/base/document.py | 7 ------- mongoengine/document.py | 15 +++++++++++++++ tests/document/instance.py | 28 ++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index ce96837a..99c8af87 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -272,13 +272,6 @@ class BaseDocument(object): def __ne__(self, other): return not self.__eq__(other) - def __hash__(self): - if getattr(self, 'pk', None) is None: - # For new object - return super(BaseDocument, self).__hash__() - else: - return hash(self.pk) - def clean(self): """ Hook for doing document level data cleaning before validation is run. diff --git a/mongoengine/document.py b/mongoengine/document.py index b79e5e97..0b903d20 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -60,6 +60,12 @@ class EmbeddedDocument(BaseDocument): my_metaclass = DocumentMetaclass __metaclass__ = DocumentMetaclass + # A generic embedded document doesn't have any immutable properties + # that describe it uniquely, hence it shouldn't be hashable. You can + # define your own __hash__ method on a subclass if you need your + # embedded documents to be hashable. + __hash__ = None + def __init__(self, *args, **kwargs): super(EmbeddedDocument, self).__init__(*args, **kwargs) self._instance = None @@ -160,6 +166,15 @@ class Document(BaseDocument): """Set the primary key.""" return setattr(self, self._meta['id_field'], value) + def __hash__(self): + """Return the hash based on the PK of this document. If it's new + and doesn't have a PK yet, return the default object hash instead. + """ + if self.pk is None: + return super(BaseDocument, self).__hash__() + else: + return hash(self.pk) + @classmethod def _get_db(cls): """Some Model using other db_alias""" diff --git a/tests/document/instance.py b/tests/document/instance.py index c59de96f..c98b1405 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -2164,7 +2164,7 @@ class InstanceTest(unittest.TestCase): class BlogPost(Document): pass - # Clear old datas + # Clear old data User.drop_collection() BlogPost.drop_collection() @@ -2176,17 +2176,18 @@ class InstanceTest(unittest.TestCase): b1 = BlogPost.objects.create() b2 = BlogPost.objects.create() - # in List + # Make sure docs are properly identified in a list (__eq__ is used + # for the comparison). all_user_list = list(User.objects.all()) - self.assertTrue(u1 in all_user_list) self.assertTrue(u2 in all_user_list) self.assertTrue(u3 in all_user_list) - self.assertFalse(u4 in all_user_list) # New object - self.assertFalse(b1 in all_user_list) # Other object - self.assertFalse(b2 in all_user_list) # Other object + self.assertTrue(u4 not in all_user_list) # New object + self.assertTrue(b1 not in all_user_list) # Other object + self.assertTrue(b2 not in all_user_list) # Other object - # in Dict + # Make sure docs can be used as keys in a dict (__hash__ is used + # for hashing the docs). all_user_dic = {} for u in User.objects.all(): all_user_dic[u] = "OK" @@ -2198,9 +2199,20 @@ class InstanceTest(unittest.TestCase): self.assertEqual(all_user_dic.get(b1, False), False) # Other object self.assertEqual(all_user_dic.get(b2, False), False) # Other object - # in Set + # Make sure docs are properly identified in a set (__hash__ is used + # for hashing the docs). all_user_set = set(User.objects.all()) self.assertTrue(u1 in all_user_set) + self.assertTrue(u4 not in all_user_set) + self.assertTrue(b1 not in all_user_list) + self.assertTrue(b2 not in all_user_list) + + # Make sure duplicate docs aren't accepted in the set + self.assertEqual(len(all_user_set), 3) + all_user_set.add(u1) + all_user_set.add(u2) + all_user_set.add(u3) + self.assertEqual(len(all_user_set), 3) def test_picklable(self): pickle_doc = PickleTest(number=1, string="One", lists=['1', '2']) From de18e256cebd46e9412274dd73704c0b8fc18cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 30 Apr 2017 14:35:33 -0400 Subject: [PATCH 071/268] clean up the Document._get_collection code (#1540) --- mongoengine/document.py | 80 +++++++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 0b903d20..ad976216 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -182,45 +182,63 @@ class Document(BaseDocument): @classmethod def _get_collection(cls): - """Returns the collection for the document.""" - # TODO: use new get_collection() with PyMongo3 ? + """Return a PyMongo collection for the document.""" if not hasattr(cls, '_collection') or cls._collection is None: - db = cls._get_db() - collection_name = cls._get_collection_name() - # Create collection as a capped collection if specified - if cls._meta.get('max_size') or cls._meta.get('max_documents'): - # Get max document limit and max byte size from meta - max_size = cls._meta.get('max_size') or 10 * 2 ** 20 # 10MB default - max_documents = cls._meta.get('max_documents') - # Round up to next 256 bytes as MongoDB would do it to avoid exception - if max_size % 256: - max_size = (max_size // 256 + 1) * 256 - if collection_name in db.collection_names(): - cls._collection = db[collection_name] - # The collection already exists, check if its capped - # options match the specified capped options - options = cls._collection.options() - if options.get('max') != max_documents or \ - options.get('size') != max_size: - msg = (('Cannot create collection "%s" as a capped ' - 'collection as it already exists') - % cls._collection) - raise InvalidCollectionError(msg) - else: - # Create the collection as a capped collection - opts = {'capped': True, 'size': max_size} - if max_documents: - opts['max'] = max_documents - cls._collection = db.create_collection( - collection_name, **opts - ) + # Get the collection, either capped or regular. + if cls._meta.get('max_size') or cls._meta.get('max_documents'): + cls._collection = cls._get_capped_collection() else: + db = cls._get_db() + collection_name = cls._get_collection_name() cls._collection = db[collection_name] + + # Ensure indexes on the collection unless auto_create_index was + # set to False. if cls._meta.get('auto_create_index', True): cls.ensure_indexes() + return cls._collection + @classmethod + def _get_capped_collection(cls): + """Create a new or get an existing capped PyMongo collection.""" + db = cls._get_db() + collection_name = cls._get_collection_name() + + # Get max document limit and max byte size from meta. + max_size = cls._meta.get('max_size') or 10 * 2 ** 20 # 10MB default + max_documents = cls._meta.get('max_documents') + + # MongoDB will automatically raise the size to make it a multiple of + # 256 bytes. We raise it here ourselves to be able to reliably compare + # the options below. + if max_size % 256: + max_size = (max_size // 256 + 1) * 256 + + # If the collection already exists and has different options + # (i.e. isn't capped or has different max/size), raise an error. + if collection_name in db.collection_names(): + collection = db[collection_name] + options = collection.options() + if ( + options.get('max') != max_documents or + options.get('size') != max_size + ): + raise InvalidCollectionError( + 'Cannot create collection "{}" as a capped ' + 'collection as it already exists'.format(cls._collection) + ) + + return collection + + # Create a new capped collection. + opts = {'capped': True, 'size': max_size} + if max_documents: + opts['max'] = max_documents + + return db.create_collection(collection_name, **opts) + def to_mongo(self, *args, **kwargs): data = super(Document, self).to_mongo(*args, **kwargs) From 894e9818ac23ff8f5bd1a53e2afd880e4532fcf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 30 Apr 2017 15:38:21 -0400 Subject: [PATCH 072/268] use an external sphinx rtd theme (#1541) Externalize Sphinx RTD theme --- CONTRIBUTING.rst | 4 +- docs/_themes/sphinx_rtd_theme/__init__.py | 17 - .../_themes/sphinx_rtd_theme/breadcrumbs.html | 15 - docs/_themes/sphinx_rtd_theme/footer.html | 30 -- docs/_themes/sphinx_rtd_theme/layout.html | 142 ------- docs/_themes/sphinx_rtd_theme/layout_old.html | 205 --------- docs/_themes/sphinx_rtd_theme/search.html | 50 --- docs/_themes/sphinx_rtd_theme/searchbox.html | 5 - .../static/css/badge_only.css | 1 - .../sphinx_rtd_theme/static/css/theme.css | 1 - .../sphinx_rtd_theme/static/favicon.ico | Bin 6261 -> 0 bytes .../static/font/fontawesome_webfont.eot | Bin 37405 -> 0 bytes .../static/font/fontawesome_webfont.svg | 399 ------------------ .../static/font/fontawesome_webfont.ttf | Bin 79076 -> 0 bytes .../static/font/fontawesome_webfont.woff | Bin 43572 -> 0 bytes .../sphinx_rtd_theme/static/js/theme.js | 16 - docs/_themes/sphinx_rtd_theme/theme.conf | 8 - docs/_themes/sphinx_rtd_theme/versions.html | 37 -- docs/conf.py | 15 +- requirements.txt | 2 + 20 files changed, 13 insertions(+), 934 deletions(-) delete mode 100755 docs/_themes/sphinx_rtd_theme/__init__.py delete mode 100755 docs/_themes/sphinx_rtd_theme/breadcrumbs.html delete mode 100755 docs/_themes/sphinx_rtd_theme/footer.html delete mode 100755 docs/_themes/sphinx_rtd_theme/layout.html delete mode 100755 docs/_themes/sphinx_rtd_theme/layout_old.html delete mode 100755 docs/_themes/sphinx_rtd_theme/search.html delete mode 100755 docs/_themes/sphinx_rtd_theme/searchbox.html delete mode 100755 docs/_themes/sphinx_rtd_theme/static/css/badge_only.css delete mode 100755 docs/_themes/sphinx_rtd_theme/static/css/theme.css delete mode 100644 docs/_themes/sphinx_rtd_theme/static/favicon.ico delete mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot delete mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.svg delete mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf delete mode 100755 docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.woff delete mode 100755 docs/_themes/sphinx_rtd_theme/static/js/theme.js delete mode 100755 docs/_themes/sphinx_rtd_theme/theme.conf delete mode 100755 docs/_themes/sphinx_rtd_theme/versions.html diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 5707886b..573d7060 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -77,4 +77,6 @@ You might also use the github `Edit -
  • Docs »
  • -
  • {{ title }}
  • -
  • - {% if display_github %} - Edit on GitHub - {% elif display_bitbucket %} - Edit on Bitbucket - {% elif show_source and has_source and sourcename %} - View page source - {% endif %} -
  • - -
    - diff --git a/docs/_themes/sphinx_rtd_theme/footer.html b/docs/_themes/sphinx_rtd_theme/footer.html deleted file mode 100755 index 1fa05eaa..00000000 --- a/docs/_themes/sphinx_rtd_theme/footer.html +++ /dev/null @@ -1,30 +0,0 @@ -
    - {% if next or prev %} - - {% endif %} - -
    - -

    - {%- if show_copyright %} - {%- if hasdoc('copyright') %} - {% trans path=pathto('copyright'), copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} - {%- else %} - {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %} - {%- endif %} - {%- endif %} - - {%- if last_updated %} - {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} - {%- endif %} -

    - - {% trans %}Sphinx theme provided by Read the Docs{% endtrans %} -
    diff --git a/docs/_themes/sphinx_rtd_theme/layout.html b/docs/_themes/sphinx_rtd_theme/layout.html deleted file mode 100755 index febe8eb0..00000000 --- a/docs/_themes/sphinx_rtd_theme/layout.html +++ /dev/null @@ -1,142 +0,0 @@ -{# TEMPLATE VAR SETTINGS #} -{%- set url_root = pathto('', 1) %} -{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} -{%- if not embedded and docstitle %} - {%- set titlesuffix = " — "|safe + docstitle|e %} -{%- else %} - {%- set titlesuffix = "" %} -{%- endif %} - - - - - - - - {% block htmltitle %} - {{ title|striptags|e }}{{ titlesuffix }} - {% endblock %} - - {# FAVICON #} - {% if favicon %} - - {% endif %} - {# CANONICAL #} - {%- if theme_canonical_url %} - - {%- endif %} - - {# CSS #} - - - {# JS #} - {% if not embedded %} - - - {%- for scriptfile in script_files %} - - {%- endfor %} - - {% if use_opensearch %} - - {% endif %} - - {% endif %} - - {# RTD hosts these file themselves, so just load on non RTD builds #} - {% if not READTHEDOCS %} - - - {% endif %} - - {% for cssfile in css_files %} - - {% endfor %} - - {%- block linktags %} - {%- if hasdoc('about') %} - - {%- endif %} - {%- if hasdoc('genindex') %} - - {%- endif %} - {%- if hasdoc('search') %} - - {%- endif %} - {%- if hasdoc('copyright') %} - - {%- endif %} - - {%- if parents %} - - {%- endif %} - {%- if next %} - - {%- endif %} - {%- if prev %} - - {%- endif %} - {%- endblock %} - {%- block extrahead %} {% endblock %} - - - - - - - -
    - - {# SIDE NAV, TOGGLES ON MOBILE #} - - -
    - - {# MOBILE NAV, TRIGGLES SIDE NAV ON TOGGLE #} - - - - {# PAGE CONTENT #} -
    -
    - {% include "breadcrumbs.html" %} - {% block body %}{% endblock %} - {% include "footer.html" %} -
    -
    - -
    - -
    - {% include "versions.html" %} - - diff --git a/docs/_themes/sphinx_rtd_theme/layout_old.html b/docs/_themes/sphinx_rtd_theme/layout_old.html deleted file mode 100755 index deb8df2a..00000000 --- a/docs/_themes/sphinx_rtd_theme/layout_old.html +++ /dev/null @@ -1,205 +0,0 @@ -{# - basic/layout.html - ~~~~~~~~~~~~~~~~~ - - Master layout template for Sphinx themes. - - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{%- block doctype -%} - -{%- endblock %} -{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} -{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} -{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and - (sidebars != []) %} -{%- set url_root = pathto('', 1) %} -{# XXX necessary? #} -{%- if url_root == '#' %}{% set url_root = '' %}{% endif %} -{%- if not embedded and docstitle %} - {%- set titlesuffix = " — "|safe + docstitle|e %} -{%- else %} - {%- set titlesuffix = "" %} -{%- endif %} - -{%- macro relbar() %} - -{%- endmacro %} - -{%- macro sidebar() %} - {%- if render_sidebar %} -
    -
    - {%- block sidebarlogo %} - {%- if logo %} - - {%- endif %} - {%- endblock %} - {%- if sidebars != None %} - {#- new style sidebar: explicitly include/exclude templates #} - {%- for sidebartemplate in sidebars %} - {%- include sidebartemplate %} - {%- endfor %} - {%- else %} - {#- old style sidebars: using blocks -- should be deprecated #} - {%- block sidebartoc %} - {%- include "localtoc.html" %} - {%- endblock %} - {%- block sidebarrel %} - {%- include "relations.html" %} - {%- endblock %} - {%- block sidebarsourcelink %} - {%- include "sourcelink.html" %} - {%- endblock %} - {%- if customsidebar %} - {%- include customsidebar %} - {%- endif %} - {%- block sidebarsearch %} - {%- include "searchbox.html" %} - {%- endblock %} - {%- endif %} -
    -
    - {%- endif %} -{%- endmacro %} - -{%- macro script() %} - - {%- for scriptfile in script_files %} - - {%- endfor %} -{%- endmacro %} - -{%- macro css() %} - - - {%- for cssfile in css_files %} - - {%- endfor %} -{%- endmacro %} - - - - - {{ metatags }} - {%- block htmltitle %} - {{ title|striptags|e }}{{ titlesuffix }} - {%- endblock %} - {{ css() }} - {%- if not embedded %} - {{ script() }} - {%- if use_opensearch %} - - {%- endif %} - {%- if favicon %} - - {%- endif %} - {%- endif %} -{%- block linktags %} - {%- if hasdoc('about') %} - - {%- endif %} - {%- if hasdoc('genindex') %} - - {%- endif %} - {%- if hasdoc('search') %} - - {%- endif %} - {%- if hasdoc('copyright') %} - - {%- endif %} - - {%- if parents %} - - {%- endif %} - {%- if next %} - - {%- endif %} - {%- if prev %} - - {%- endif %} -{%- endblock %} -{%- block extrahead %} {% endblock %} - - -{%- block header %}{% endblock %} - -{%- block relbar1 %}{{ relbar() }}{% endblock %} - -{%- block content %} - {%- block sidebar1 %} {# possible location for sidebar #} {% endblock %} - -
    - {%- block document %} -
    - {%- if render_sidebar %} -
    - {%- endif %} -
    - {% block body %} {% endblock %} -
    - {%- if render_sidebar %} -
    - {%- endif %} -
    - {%- endblock %} - - {%- block sidebar2 %}{{ sidebar() }}{% endblock %} -
    -
    -{%- endblock %} - -{%- block relbar2 %}{{ relbar() }}{% endblock %} - -{%- block footer %} - -

    asdf asdf asdf asdf 22

    -{%- endblock %} - - - diff --git a/docs/_themes/sphinx_rtd_theme/search.html b/docs/_themes/sphinx_rtd_theme/search.html deleted file mode 100755 index d8bbe690..00000000 --- a/docs/_themes/sphinx_rtd_theme/search.html +++ /dev/null @@ -1,50 +0,0 @@ -{# - basic/search.html - ~~~~~~~~~~~~~~~~~ - - Template for the search page. - - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{%- extends "layout.html" %} -{% set title = _('Search') %} -{% set script_files = script_files + ['_static/searchtools.js'] %} -{% block extrahead %} - - {# this is used when loading the search index using $.ajax fails, - such as on Chrome for documents on localhost #} - - {{ super() }} -{% endblock %} -{% block body %} - - - {% if search_performed %} -

    {{ _('Search Results') }}

    - {% if not search_results %} -

    {{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}

    - {% endif %} - {% endif %} -
    - {% if search_results %} -
      - {% for href, caption, context in search_results %} -
    • - {{ caption }} -

      {{ context|e }}

      -
    • - {% endfor %} -
    - {% endif %} -
    -{% endblock %} diff --git a/docs/_themes/sphinx_rtd_theme/searchbox.html b/docs/_themes/sphinx_rtd_theme/searchbox.html deleted file mode 100755 index f62545ea..00000000 --- a/docs/_themes/sphinx_rtd_theme/searchbox.html +++ /dev/null @@ -1,5 +0,0 @@ -
    - - - -
    diff --git a/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css b/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css deleted file mode 100755 index 7fccc414..00000000 --- a/docs/_themes/sphinx_rtd_theme/static/css/badge_only.css +++ /dev/null @@ -1 +0,0 @@ -.font-smooth,.icon:before{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:fontawesome-webfont;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#fontawesome-webfont") format("svg")}.icon:before{display:inline-block;font-family:fontawesome-webfont;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .icon{display:inline-block;text-decoration:inherit}li .icon{display:inline-block}li .icon-large:before,li .icon-large:before{width:1.875em}ul.icons{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.icons li .icon{width:0.8em}ul.icons li .icon-large:before,ul.icons li .icon-large:before{vertical-align:baseline}.icon-book:before{content:"\f02d"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .icon{color:#fcfcfc}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}} diff --git a/docs/_themes/sphinx_rtd_theme/static/css/theme.css b/docs/_themes/sphinx_rtd_theme/static/css/theme.css deleted file mode 100755 index a37f8d8c..00000000 --- a/docs/_themes/sphinx_rtd_theme/static/css/theme.css +++ /dev/null @@ -1 +0,0 @@ -*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none}[hidden]{display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:hover,a:active{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:0}dfn{font-style:italic}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:20px 0;padding:0}ins{background:#ff9;color:#000;text-decoration:none}mark{background:#ff0;color:#000;font-style:italic;font-weight:bold}pre,code,.rst-content tt,kbd,samp{font-family:monospace,serif;_font-family:"courier new",monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:before,q:after{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}ul,ol,dl{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}legend{border:0;*margin-left:-7px;padding:0;white-space:normal}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*width:13px;*height:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:0.2em 0;background:#ccc;color:#000;padding:0.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none !important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{html,body,section{background:none !important}*{box-shadow:none !important;text-shadow:none !important;filter:none !important;-ms-filter:none !important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.font-smooth,.icon:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before,.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso,.btn,input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"],select,textarea,.wy-tag-input-group,.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a,.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a,.wy-nav-top a{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:fontawesome-webfont;font-weight:normal;font-style:normal;src:url("../font/fontawesome_webfont.eot");src:url("../font/fontawesome_webfont.eot?#iefix") format("embedded-opentype"),url("../font/fontawesome_webfont.woff") format("woff"),url("../font/fontawesome_webfont.ttf") format("truetype"),url("../font/fontawesome_webfont.svg#fontawesome-webfont") format("svg")}.icon:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before,.rst-content .admonition-title:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content dl dt .headerlink:before{display:inline-block;font-family:fontawesome-webfont;font-style:normal;font-weight:normal;line-height:1;text-decoration:inherit}a .icon,a .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success a .wy-input-context,a .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger a .wy-input-context,a .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning a .wy-input-context,a .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info a .wy-input-context,a .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag a .wy-tag-remove,a .rst-content .admonition-title,.rst-content a .admonition-title,a .rst-content h1 .headerlink,.rst-content h1 a .headerlink,a .rst-content h2 .headerlink,.rst-content h2 a .headerlink,a .rst-content h3 .headerlink,.rst-content h3 a .headerlink,a .rst-content h4 .headerlink,.rst-content h4 a .headerlink,a .rst-content h5 .headerlink,.rst-content h5 a .headerlink,a .rst-content h6 .headerlink,.rst-content h6 a .headerlink,a .rst-content dl dt .headerlink,.rst-content dl dt a .headerlink{display:inline-block;text-decoration:inherit}.icon-large:before{vertical-align:-10%;font-size:1.33333em}.btn .icon,.btn .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .wy-input-context,.btn .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .btn .wy-tag-remove,.btn .rst-content .admonition-title,.rst-content .btn .admonition-title,.btn .rst-content h1 .headerlink,.rst-content h1 .btn .headerlink,.btn .rst-content h2 .headerlink,.rst-content h2 .btn .headerlink,.btn .rst-content h3 .headerlink,.rst-content h3 .btn .headerlink,.btn .rst-content h4 .headerlink,.rst-content h4 .btn .headerlink,.btn .rst-content h5 .headerlink,.rst-content h5 .btn .headerlink,.btn .rst-content h6 .headerlink,.rst-content h6 .btn .headerlink,.btn .rst-content dl dt .headerlink,.rst-content dl dt .btn .headerlink,.nav .icon,.nav .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .wy-input-context,.nav .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .nav .wy-tag-remove,.nav .rst-content .admonition-title,.rst-content .nav .admonition-title,.nav .rst-content h1 .headerlink,.rst-content h1 .nav .headerlink,.nav .rst-content h2 .headerlink,.rst-content h2 .nav .headerlink,.nav .rst-content h3 .headerlink,.rst-content h3 .nav .headerlink,.nav .rst-content h4 .headerlink,.rst-content h4 .nav .headerlink,.nav .rst-content h5 .headerlink,.rst-content h5 .nav .headerlink,.nav .rst-content h6 .headerlink,.rst-content h6 .nav .headerlink,.nav .rst-content dl dt .headerlink,.rst-content dl dt .nav .headerlink{display:inline}.btn .icon.icon-large,.btn .wy-inline-validate.wy-inline-validate-success .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .icon-large.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .icon-large.wy-input-context,.btn .wy-tag-input-group .wy-tag .icon-large.wy-tag-remove,.wy-tag-input-group .wy-tag .btn .icon-large.wy-tag-remove,.btn .rst-content .icon-large.admonition-title,.rst-content .btn .icon-large.admonition-title,.btn .rst-content h1 .icon-large.headerlink,.rst-content h1 .btn .icon-large.headerlink,.btn .rst-content h2 .icon-large.headerlink,.rst-content h2 .btn .icon-large.headerlink,.btn .rst-content h3 .icon-large.headerlink,.rst-content h3 .btn .icon-large.headerlink,.btn .rst-content h4 .icon-large.headerlink,.rst-content h4 .btn .icon-large.headerlink,.btn .rst-content h5 .icon-large.headerlink,.rst-content h5 .btn .icon-large.headerlink,.btn .rst-content h6 .icon-large.headerlink,.rst-content h6 .btn .icon-large.headerlink,.btn .rst-content dl dt .icon-large.headerlink,.rst-content dl dt .btn .icon-large.headerlink,.nav .icon.icon-large,.nav .wy-inline-validate.wy-inline-validate-success .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .icon-large.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .icon-large.wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .icon-large.wy-input-context,.nav .wy-tag-input-group .wy-tag .icon-large.wy-tag-remove,.wy-tag-input-group .wy-tag .nav .icon-large.wy-tag-remove,.nav .rst-content .icon-large.admonition-title,.rst-content .nav .icon-large.admonition-title,.nav .rst-content h1 .icon-large.headerlink,.rst-content h1 .nav .icon-large.headerlink,.nav .rst-content h2 .icon-large.headerlink,.rst-content h2 .nav .icon-large.headerlink,.nav .rst-content h3 .icon-large.headerlink,.rst-content h3 .nav .icon-large.headerlink,.nav .rst-content h4 .icon-large.headerlink,.rst-content h4 .nav .icon-large.headerlink,.nav .rst-content h5 .icon-large.headerlink,.rst-content h5 .nav .icon-large.headerlink,.nav .rst-content h6 .icon-large.headerlink,.rst-content h6 .nav .icon-large.headerlink,.nav .rst-content dl dt .icon-large.headerlink,.rst-content dl dt .nav .icon-large.headerlink{line-height:0.9em}.btn .icon.icon-spin,.btn .wy-inline-validate.wy-inline-validate-success .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-success .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-danger .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-warning .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .btn .icon-spin.wy-input-context,.btn .wy-inline-validate.wy-inline-validate-info .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-info .btn .icon-spin.wy-input-context,.btn .wy-tag-input-group .wy-tag .icon-spin.wy-tag-remove,.wy-tag-input-group .wy-tag .btn .icon-spin.wy-tag-remove,.btn .rst-content .icon-spin.admonition-title,.rst-content .btn .icon-spin.admonition-title,.btn .rst-content h1 .icon-spin.headerlink,.rst-content h1 .btn .icon-spin.headerlink,.btn .rst-content h2 .icon-spin.headerlink,.rst-content h2 .btn .icon-spin.headerlink,.btn .rst-content h3 .icon-spin.headerlink,.rst-content h3 .btn .icon-spin.headerlink,.btn .rst-content h4 .icon-spin.headerlink,.rst-content h4 .btn .icon-spin.headerlink,.btn .rst-content h5 .icon-spin.headerlink,.rst-content h5 .btn .icon-spin.headerlink,.btn .rst-content h6 .icon-spin.headerlink,.rst-content h6 .btn .icon-spin.headerlink,.btn .rst-content dl dt .icon-spin.headerlink,.rst-content dl dt .btn .icon-spin.headerlink,.nav .icon.icon-spin,.nav .wy-inline-validate.wy-inline-validate-success .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-success .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-danger .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-danger .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-warning .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-warning .nav .icon-spin.wy-input-context,.nav .wy-inline-validate.wy-inline-validate-info .icon-spin.wy-input-context,.wy-inline-validate.wy-inline-validate-info .nav .icon-spin.wy-input-context,.nav .wy-tag-input-group .wy-tag .icon-spin.wy-tag-remove,.wy-tag-input-group .wy-tag .nav .icon-spin.wy-tag-remove,.nav .rst-content .icon-spin.admonition-title,.rst-content .nav .icon-spin.admonition-title,.nav .rst-content h1 .icon-spin.headerlink,.rst-content h1 .nav .icon-spin.headerlink,.nav .rst-content h2 .icon-spin.headerlink,.rst-content h2 .nav .icon-spin.headerlink,.nav .rst-content h3 .icon-spin.headerlink,.rst-content h3 .nav .icon-spin.headerlink,.nav .rst-content h4 .icon-spin.headerlink,.rst-content h4 .nav .icon-spin.headerlink,.nav .rst-content h5 .icon-spin.headerlink,.rst-content h5 .nav .icon-spin.headerlink,.nav .rst-content h6 .icon-spin.headerlink,.rst-content h6 .nav .icon-spin.headerlink,.nav .rst-content dl dt .icon-spin.headerlink,.rst-content dl dt .nav .icon-spin.headerlink{display:inline-block}.btn.icon:before,.wy-inline-validate.wy-inline-validate-success .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .btn.wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .btn.wy-input-context:before,.wy-tag-input-group .wy-tag .btn.wy-tag-remove:before,.rst-content .btn.admonition-title:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content dl dt .btn.headerlink:before{opacity:0.5;-webkit-transition:opacity 0.05s ease-in;-moz-transition:opacity 0.05s ease-in;transition:opacity 0.05s ease-in}.btn.icon:hover:before,.wy-inline-validate.wy-inline-validate-success .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-danger .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-warning .btn.wy-input-context:hover:before,.wy-inline-validate.wy-inline-validate-info .btn.wy-input-context:hover:before,.wy-tag-input-group .wy-tag .btn.wy-tag-remove:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content dl dt .btn.headerlink:hover:before{opacity:1}.btn-mini .icon:before,.btn-mini .wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-danger .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .btn-mini .wy-input-context:before,.btn-mini .wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .btn-mini .wy-input-context:before,.btn-mini .wy-tag-input-group .wy-tag .wy-tag-remove:before,.wy-tag-input-group .wy-tag .btn-mini .wy-tag-remove:before,.btn-mini .rst-content .admonition-title:before,.rst-content .btn-mini .admonition-title:before,.btn-mini .rst-content h1 .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.btn-mini .rst-content dl dt .headerlink:before,.rst-content dl dt .btn-mini .headerlink:before{font-size:14px;vertical-align:-15%}li .icon,li .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success li .wy-input-context,li .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger li .wy-input-context,li .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning li .wy-input-context,li .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info li .wy-input-context,li .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag li .wy-tag-remove,li .rst-content .admonition-title,.rst-content li .admonition-title,li .rst-content h1 .headerlink,.rst-content h1 li .headerlink,li .rst-content h2 .headerlink,.rst-content h2 li .headerlink,li .rst-content h3 .headerlink,.rst-content h3 li .headerlink,li .rst-content h4 .headerlink,.rst-content h4 li .headerlink,li .rst-content h5 .headerlink,.rst-content h5 li .headerlink,li .rst-content h6 .headerlink,.rst-content h6 li .headerlink,li .rst-content dl dt .headerlink,.rst-content dl dt li .headerlink{display:inline-block}li .icon-large:before,li .icon-large:before{width:1.875em}ul.icons{list-style-type:none;margin-left:2em;text-indent:-0.8em}ul.icons li .icon,ul.icons li .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning ul.icons li .wy-input-context,ul.icons li .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info ul.icons li .wy-input-context,ul.icons li .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag ul.icons li .wy-tag-remove,ul.icons li .rst-content .admonition-title,.rst-content ul.icons li .admonition-title,ul.icons li .rst-content h1 .headerlink,.rst-content h1 ul.icons li .headerlink,ul.icons li .rst-content h2 .headerlink,.rst-content h2 ul.icons li .headerlink,ul.icons li .rst-content h3 .headerlink,.rst-content h3 ul.icons li .headerlink,ul.icons li .rst-content h4 .headerlink,.rst-content h4 ul.icons li .headerlink,ul.icons li .rst-content h5 .headerlink,.rst-content h5 ul.icons li .headerlink,ul.icons li .rst-content h6 .headerlink,.rst-content h6 ul.icons li .headerlink,ul.icons li .rst-content dl dt .headerlink,.rst-content dl dt ul.icons li .headerlink{width:0.8em}ul.icons li .icon-large:before,ul.icons li .icon-large:before{vertical-align:baseline}.icon-glass:before{content:"\f000"}.icon-music:before{content:"\f001"}.icon-search:before{content:"\f002"}.icon-envelope-alt:before{content:"\f003"}.icon-heart:before{content:"\f004"}.icon-star:before{content:"\f005"}.icon-star-empty:before{content:"\f006"}.icon-user:before{content:"\f007"}.icon-film:before{content:"\f008"}.icon-th-large:before{content:"\f009"}.icon-th:before{content:"\f00a"}.icon-th-list:before{content:"\f00b"}.icon-ok:before{content:"\f00c"}.icon-remove:before,.wy-tag-input-group .wy-tag .wy-tag-remove:before{content:"\f00d"}.icon-zoom-in:before{content:"\f00e"}.icon-zoom-out:before{content:"\f010"}.icon-power-off:before,.icon-off:before{content:"\f011"}.icon-signal:before{content:"\f012"}.icon-gear:before,.icon-cog:before{content:"\f013"}.icon-trash:before{content:"\f014"}.icon-home:before{content:"\f015"}.icon-file-alt:before{content:"\f016"}.icon-time:before{content:"\f017"}.icon-road:before{content:"\f018"}.icon-download-alt:before{content:"\f019"}.icon-download:before{content:"\f01a"}.icon-upload:before{content:"\f01b"}.icon-inbox:before{content:"\f01c"}.icon-play-circle:before{content:"\f01d"}.icon-rotate-right:before,.icon-repeat:before{content:"\f01e"}.icon-refresh:before{content:"\f021"}.icon-list-alt:before{content:"\f022"}.icon-lock:before{content:"\f023"}.icon-flag:before{content:"\f024"}.icon-headphones:before{content:"\f025"}.icon-volume-off:before{content:"\f026"}.icon-volume-down:before{content:"\f027"}.icon-volume-up:before{content:"\f028"}.icon-qrcode:before{content:"\f029"}.icon-barcode:before{content:"\f02a"}.icon-tag:before{content:"\f02b"}.icon-tags:before{content:"\f02c"}.icon-book:before{content:"\f02d"}.icon-bookmark:before{content:"\f02e"}.icon-print:before{content:"\f02f"}.icon-camera:before{content:"\f030"}.icon-font:before{content:"\f031"}.icon-bold:before{content:"\f032"}.icon-italic:before{content:"\f033"}.icon-text-height:before{content:"\f034"}.icon-text-width:before{content:"\f035"}.icon-align-left:before{content:"\f036"}.icon-align-center:before{content:"\f037"}.icon-align-right:before{content:"\f038"}.icon-align-justify:before{content:"\f039"}.icon-list:before{content:"\f03a"}.icon-indent-left:before{content:"\f03b"}.icon-indent-right:before{content:"\f03c"}.icon-facetime-video:before{content:"\f03d"}.icon-picture:before{content:"\f03e"}.icon-pencil:before{content:"\f040"}.icon-map-marker:before{content:"\f041"}.icon-adjust:before{content:"\f042"}.icon-tint:before{content:"\f043"}.icon-edit:before{content:"\f044"}.icon-share:before{content:"\f045"}.icon-check:before{content:"\f046"}.icon-move:before{content:"\f047"}.icon-step-backward:before{content:"\f048"}.icon-fast-backward:before{content:"\f049"}.icon-backward:before{content:"\f04a"}.icon-play:before{content:"\f04b"}.icon-pause:before{content:"\f04c"}.icon-stop:before{content:"\f04d"}.icon-forward:before{content:"\f04e"}.icon-fast-forward:before{content:"\f050"}.icon-step-forward:before{content:"\f051"}.icon-eject:before{content:"\f052"}.icon-chevron-left:before{content:"\f053"}.icon-chevron-right:before{content:"\f054"}.icon-plus-sign:before{content:"\f055"}.icon-minus-sign:before{content:"\f056"}.icon-remove-sign:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:"\f057"}.icon-ok-sign:before{content:"\f058"}.icon-question-sign:before{content:"\f059"}.icon-info-sign:before{content:"\f05a"}.icon-screenshot:before{content:"\f05b"}.icon-remove-circle:before{content:"\f05c"}.icon-ok-circle:before{content:"\f05d"}.icon-ban-circle:before{content:"\f05e"}.icon-arrow-left:before{content:"\f060"}.icon-arrow-right:before{content:"\f061"}.icon-arrow-up:before{content:"\f062"}.icon-arrow-down:before{content:"\f063"}.icon-mail-forward:before,.icon-share-alt:before{content:"\f064"}.icon-resize-full:before{content:"\f065"}.icon-resize-small:before{content:"\f066"}.icon-plus:before{content:"\f067"}.icon-minus:before{content:"\f068"}.icon-asterisk:before{content:"\f069"}.icon-exclamation-sign:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.rst-content .admonition-title:before{content:"\f06a"}.icon-gift:before{content:"\f06b"}.icon-leaf:before{content:"\f06c"}.icon-fire:before{content:"\f06d"}.icon-eye-open:before{content:"\f06e"}.icon-eye-close:before{content:"\f070"}.icon-warning-sign:before{content:"\f071"}.icon-plane:before{content:"\f072"}.icon-calendar:before{content:"\f073"}.icon-random:before{content:"\f074"}.icon-comment:before{content:"\f075"}.icon-magnet:before{content:"\f076"}.icon-chevron-up:before{content:"\f077"}.icon-chevron-down:before{content:"\f078"}.icon-retweet:before{content:"\f079"}.icon-shopping-cart:before{content:"\f07a"}.icon-folder-close:before{content:"\f07b"}.icon-folder-open:before{content:"\f07c"}.icon-resize-vertical:before{content:"\f07d"}.icon-resize-horizontal:before{content:"\f07e"}.icon-bar-chart:before{content:"\f080"}.icon-twitter-sign:before{content:"\f081"}.icon-facebook-sign:before{content:"\f082"}.icon-camera-retro:before{content:"\f083"}.icon-key:before{content:"\f084"}.icon-gears:before,.icon-cogs:before{content:"\f085"}.icon-comments:before{content:"\f086"}.icon-thumbs-up-alt:before{content:"\f087"}.icon-thumbs-down-alt:before{content:"\f088"}.icon-star-half:before{content:"\f089"}.icon-heart-empty:before{content:"\f08a"}.icon-signout:before{content:"\f08b"}.icon-linkedin-sign:before{content:"\f08c"}.icon-pushpin:before{content:"\f08d"}.icon-external-link:before{content:"\f08e"}.icon-signin:before{content:"\f090"}.icon-trophy:before{content:"\f091"}.icon-github-sign:before{content:"\f092"}.icon-upload-alt:before{content:"\f093"}.icon-lemon:before{content:"\f094"}.icon-phone:before{content:"\f095"}.icon-unchecked:before,.icon-check-empty:before{content:"\f096"}.icon-bookmark-empty:before{content:"\f097"}.icon-phone-sign:before{content:"\f098"}.icon-twitter:before{content:"\f099"}.icon-facebook:before{content:"\f09a"}.icon-github:before{content:"\f09b"}.icon-unlock:before{content:"\f09c"}.icon-credit-card:before{content:"\f09d"}.icon-rss:before{content:"\f09e"}.icon-hdd:before{content:"\f0a0"}.icon-bullhorn:before{content:"\f0a1"}.icon-bell:before{content:"\f0a2"}.icon-certificate:before{content:"\f0a3"}.icon-hand-right:before{content:"\f0a4"}.icon-hand-left:before{content:"\f0a5"}.icon-hand-up:before{content:"\f0a6"}.icon-hand-down:before{content:"\f0a7"}.icon-circle-arrow-left:before{content:"\f0a8"}.icon-circle-arrow-right:before{content:"\f0a9"}.icon-circle-arrow-up:before{content:"\f0aa"}.icon-circle-arrow-down:before{content:"\f0ab"}.icon-globe:before{content:"\f0ac"}.icon-wrench:before{content:"\f0ad"}.icon-tasks:before{content:"\f0ae"}.icon-filter:before{content:"\f0b0"}.icon-briefcase:before{content:"\f0b1"}.icon-fullscreen:before{content:"\f0b2"}.icon-group:before{content:"\f0c0"}.icon-link:before{content:"\f0c1"}.icon-cloud:before{content:"\f0c2"}.icon-beaker:before{content:"\f0c3"}.icon-cut:before{content:"\f0c4"}.icon-copy:before{content:"\f0c5"}.icon-paperclip:before,.icon-paper-clip:before{content:"\f0c6"}.icon-save:before{content:"\f0c7"}.icon-sign-blank:before{content:"\f0c8"}.icon-reorder:before{content:"\f0c9"}.icon-list-ul:before{content:"\f0ca"}.icon-list-ol:before{content:"\f0cb"}.icon-strikethrough:before{content:"\f0cc"}.icon-underline:before{content:"\f0cd"}.icon-table:before{content:"\f0ce"}.icon-magic:before{content:"\f0d0"}.icon-truck:before{content:"\f0d1"}.icon-pinterest:before{content:"\f0d2"}.icon-pinterest-sign:before{content:"\f0d3"}.icon-google-plus-sign:before{content:"\f0d4"}.icon-google-plus:before{content:"\f0d5"}.icon-money:before{content:"\f0d6"}.icon-caret-down:before{content:"\f0d7"}.icon-caret-up:before{content:"\f0d8"}.icon-caret-left:before{content:"\f0d9"}.icon-caret-right:before{content:"\f0da"}.icon-columns:before{content:"\f0db"}.icon-sort:before{content:"\f0dc"}.icon-sort-down:before{content:"\f0dd"}.icon-sort-up:before{content:"\f0de"}.icon-envelope:before{content:"\f0e0"}.icon-linkedin:before{content:"\f0e1"}.icon-rotate-left:before,.icon-undo:before{content:"\f0e2"}.icon-legal:before{content:"\f0e3"}.icon-dashboard:before{content:"\f0e4"}.icon-comment-alt:before{content:"\f0e5"}.icon-comments-alt:before{content:"\f0e6"}.icon-bolt:before{content:"\f0e7"}.icon-sitemap:before{content:"\f0e8"}.icon-umbrella:before{content:"\f0e9"}.icon-paste:before{content:"\f0ea"}.icon-lightbulb:before{content:"\f0eb"}.icon-exchange:before{content:"\f0ec"}.icon-cloud-download:before{content:"\f0ed"}.icon-cloud-upload:before{content:"\f0ee"}.icon-user-md:before{content:"\f0f0"}.icon-stethoscope:before{content:"\f0f1"}.icon-suitcase:before{content:"\f0f2"}.icon-bell-alt:before{content:"\f0f3"}.icon-coffee:before{content:"\f0f4"}.icon-food:before{content:"\f0f5"}.icon-file-text-alt:before{content:"\f0f6"}.icon-building:before{content:"\f0f7"}.icon-hospital:before{content:"\f0f8"}.icon-ambulance:before{content:"\f0f9"}.icon-medkit:before{content:"\f0fa"}.icon-fighter-jet:before{content:"\f0fb"}.icon-beer:before{content:"\f0fc"}.icon-h-sign:before{content:"\f0fd"}.icon-plus-sign-alt:before{content:"\f0fe"}.icon-double-angle-left:before{content:"\f100"}.icon-double-angle-right:before{content:"\f101"}.icon-double-angle-up:before{content:"\f102"}.icon-double-angle-down:before{content:"\f103"}.icon-angle-left:before{content:"\f104"}.icon-angle-right:before{content:"\f105"}.icon-angle-up:before{content:"\f106"}.icon-angle-down:before{content:"\f107"}.icon-desktop:before{content:"\f108"}.icon-laptop:before{content:"\f109"}.icon-tablet:before{content:"\f10a"}.icon-mobile-phone:before{content:"\f10b"}.icon-circle-blank:before{content:"\f10c"}.icon-quote-left:before{content:"\f10d"}.icon-quote-right:before{content:"\f10e"}.icon-spinner:before{content:"\f110"}.icon-circle:before{content:"\f111"}.icon-mail-reply:before,.icon-reply:before{content:"\f112"}.icon-github-alt:before{content:"\f113"}.icon-folder-close-alt:before{content:"\f114"}.icon-folder-open-alt:before{content:"\f115"}.icon-expand-alt:before{content:"\f116"}.icon-collapse-alt:before{content:"\f117"}.icon-smile:before{content:"\f118"}.icon-frown:before{content:"\f119"}.icon-meh:before{content:"\f11a"}.icon-gamepad:before{content:"\f11b"}.icon-keyboard:before{content:"\f11c"}.icon-flag-alt:before{content:"\f11d"}.icon-flag-checkered:before{content:"\f11e"}.icon-terminal:before{content:"\f120"}.icon-code:before{content:"\f121"}.icon-reply-all:before{content:"\f122"}.icon-mail-reply-all:before{content:"\f122"}.icon-star-half-full:before,.icon-star-half-empty:before{content:"\f123"}.icon-location-arrow:before{content:"\f124"}.icon-crop:before{content:"\f125"}.icon-code-fork:before{content:"\f126"}.icon-unlink:before{content:"\f127"}.icon-question:before{content:"\f128"}.icon-info:before{content:"\f129"}.icon-exclamation:before{content:"\f12a"}.icon-superscript:before{content:"\f12b"}.icon-subscript:before{content:"\f12c"}.icon-eraser:before{content:"\f12d"}.icon-puzzle-piece:before{content:"\f12e"}.icon-microphone:before{content:"\f130"}.icon-microphone-off:before{content:"\f131"}.icon-shield:before{content:"\f132"}.icon-calendar-empty:before{content:"\f133"}.icon-fire-extinguisher:before{content:"\f134"}.icon-rocket:before{content:"\f135"}.icon-maxcdn:before{content:"\f136"}.icon-chevron-sign-left:before{content:"\f137"}.icon-chevron-sign-right:before{content:"\f138"}.icon-chevron-sign-up:before{content:"\f139"}.icon-chevron-sign-down:before{content:"\f13a"}.icon-html5:before{content:"\f13b"}.icon-css3:before{content:"\f13c"}.icon-anchor:before{content:"\f13d"}.icon-unlock-alt:before{content:"\f13e"}.icon-bullseye:before{content:"\f140"}.icon-ellipsis-horizontal:before{content:"\f141"}.icon-ellipsis-vertical:before{content:"\f142"}.icon-rss-sign:before{content:"\f143"}.icon-play-sign:before{content:"\f144"}.icon-ticket:before{content:"\f145"}.icon-minus-sign-alt:before{content:"\f146"}.icon-check-minus:before{content:"\f147"}.icon-level-up:before{content:"\f148"}.icon-level-down:before{content:"\f149"}.icon-check-sign:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:"\f14a"}.icon-edit-sign:before{content:"\f14b"}.icon-external-link-sign:before{content:"\f14c"}.icon-share-sign:before{content:"\f14d"}.icon-compass:before{content:"\f14e"}.icon-collapse:before{content:"\f150"}.icon-collapse-top:before{content:"\f151"}.icon-expand:before{content:"\f152"}.icon-euro:before,.icon-eur:before{content:"\f153"}.icon-gbp:before{content:"\f154"}.icon-dollar:before,.icon-usd:before{content:"\f155"}.icon-rupee:before,.icon-inr:before{content:"\f156"}.icon-yen:before,.icon-jpy:before{content:"\f157"}.icon-renminbi:before,.icon-cny:before{content:"\f158"}.icon-won:before,.icon-krw:before{content:"\f159"}.icon-bitcoin:before,.icon-btc:before{content:"\f15a"}.icon-file:before{content:"\f15b"}.icon-file-text:before{content:"\f15c"}.icon-sort-by-alphabet:before{content:"\f15d"}.icon-sort-by-alphabet-alt:before{content:"\f15e"}.icon-sort-by-attributes:before{content:"\f160"}.icon-sort-by-attributes-alt:before{content:"\f161"}.icon-sort-by-order:before{content:"\f162"}.icon-sort-by-order-alt:before{content:"\f163"}.icon-thumbs-up:before{content:"\f164"}.icon-thumbs-down:before{content:"\f165"}.icon-youtube-sign:before{content:"\f166"}.icon-youtube:before{content:"\f167"}.icon-xing:before{content:"\f168"}.icon-xing-sign:before{content:"\f169"}.icon-youtube-play:before{content:"\f16a"}.icon-dropbox:before{content:"\f16b"}.icon-stackexchange:before{content:"\f16c"}.icon-instagram:before{content:"\f16d"}.icon-flickr:before{content:"\f16e"}.icon-adn:before{content:"\f170"}.icon-bitbucket:before{content:"\f171"}.icon-bitbucket-sign:before{content:"\f172"}.icon-tumblr:before{content:"\f173"}.icon-tumblr-sign:before{content:"\f174"}.icon-long-arrow-down:before{content:"\f175"}.icon-long-arrow-up:before{content:"\f176"}.icon-long-arrow-left:before{content:"\f177"}.icon-long-arrow-right:before{content:"\f178"}.icon-apple:before{content:"\f179"}.icon-windows:before{content:"\f17a"}.icon-android:before{content:"\f17b"}.icon-linux:before{content:"\f17c"}.icon-dribbble:before{content:"\f17d"}.icon-skype:before{content:"\f17e"}.icon-foursquare:before{content:"\f180"}.icon-trello:before{content:"\f181"}.icon-female:before{content:"\f182"}.icon-male:before{content:"\f183"}.icon-gittip:before{content:"\f184"}.icon-sun:before{content:"\f185"}.icon-moon:before{content:"\f186"}.icon-archive:before{content:"\f187"}.icon-bug:before{content:"\f188"}.icon-vk:before{content:"\f189"}.icon-weibo:before{content:"\f18a"}.icon-renren:before{content:"\f18b"}.wy-alert,.rst-content .note,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .warning,.rst-content .seealso{padding:12px;line-height:24px;margin-bottom:24px}.wy-alert-title,.rst-content .admonition-title{color:#fff;font-weight:bold;display:block;color:#fff;background:transparent;margin:-12px;padding:6px 12px;margin-bottom:12px}.wy-alert.wy-alert-danger,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.rst-content .wy-alert-danger.seealso{background:#fdf3f2}.wy-alert.wy-alert-danger .wy-alert-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .danger .wy-alert-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .danger .admonition-title,.rst-content .error .admonition-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.seealso .admonition-title{background:#f29f97}.wy-alert.wy-alert-warning,.rst-content .wy-alert-warning.note,.rst-content .attention,.rst-content .caution,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.tip,.rst-content .warning,.rst-content .wy-alert-warning.seealso{background:#ffedcc}.wy-alert.wy-alert-warning .wy-alert-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .attention .wy-alert-title,.rst-content .caution .wy-alert-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .attention .admonition-title,.rst-content .caution .admonition-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .warning .admonition-title,.rst-content .wy-alert-warning.seealso .admonition-title{background:#f0b37e}.wy-alert.wy-alert-info,.rst-content .note,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.rst-content .seealso{background:#e7f2fa}.wy-alert.wy-alert-info .wy-alert-title,.rst-content .note .wy-alert-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .seealso .wy-alert-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.rst-content .note .admonition-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .seealso .admonition-title{background:#6ab0de}.wy-alert.wy-alert-success,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.warning,.rst-content .wy-alert-success.seealso{background:#dbfaf4}.wy-alert.wy-alert-success .wy-alert-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .hint .wy-alert-title,.rst-content .important .wy-alert-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .hint .admonition-title,.rst-content .important .admonition-title,.rst-content .tip .admonition-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.seealso .admonition-title{background:#1abc9c}.wy-alert.wy-alert-neutral,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.rst-content .wy-alert-neutral.seealso{background:#f3f6f6}.wy-alert.wy-alert-neutral strong,.rst-content .wy-alert-neutral.note strong,.rst-content .wy-alert-neutral.attention strong,.rst-content .wy-alert-neutral.caution strong,.rst-content .wy-alert-neutral.danger strong,.rst-content .wy-alert-neutral.error strong,.rst-content .wy-alert-neutral.hint strong,.rst-content .wy-alert-neutral.important strong,.rst-content .wy-alert-neutral.tip strong,.rst-content .wy-alert-neutral.warning strong,.rst-content .wy-alert-neutral.seealso strong{color:#404040}.wy-alert.wy-alert-neutral a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.rst-content .wy-alert-neutral.seealso a{color:#2980b9}.wy-tray-container{position:fixed;top:-50px;left:0;width:100%;-webkit-transition:top 0.2s ease-in;-moz-transition:top 0.2s ease-in;transition:top 0.2s ease-in}.wy-tray-container.on{top:0}.wy-tray-container li{display:none;width:100%;background:#343131;padding:12px 24px;color:#fff;margin-bottom:6px;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1),0px -1px 2px -1px rgba(255,255,255,0.5) inset}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.btn{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;font-size:100%;padding:6px 12px;color:#fff;border:1px solid rgba(0,0,0,0.1);border-bottom:solid 3px rgba(0,0,0,0.1);background-color:#27ae60;text-decoration:none;font-weight:500;box-shadow:0px 1px 2px -1px rgba(255,255,255,0.5) inset;-webkit-transition:all 0.1s linear;-moz-transition:all 0.1s linear;transition:all 0.1s linear;outline-none:false}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;color:#fff;outline:0}.btn:active{border-top:solid 3px rgba(0,0,0,0.1);border-bottom:solid 1px rgba(0,0,0,0.1);box-shadow:0px 1px 2px -1px rgba(0,0,0,0.5) inset}.btn[disabled]{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn-disabled:hover,.btn-disabled:focus,.btn-disabled:active{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:0.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9 !important}.btn-info:hover{background-color:#2e8ece !important}.btn-neutral{background-color:#f3f6f6 !important;color:#404040 !important}.btn-neutral:hover{background-color:#e5ebeb !important;color:#404040}.btn-danger{background-color:#e74c3c !important}.btn-danger:hover{background-color:#ea6153 !important}.btn-warning{background-color:#e67e22 !important}.btn-warning:hover{background-color:#e98b39 !important}.btn-invert{background-color:#343131}.btn-invert:hover{background-color:#413d3d !important}.btn-link{background-color:transparent !important;color:#2980b9;border-color:transparent}.btn-link:hover{background-color:transparent !important;color:#409ad5;border-color:transparent}.btn-link:active{background-color:transparent !important;border-color:transparent;border-top:solid 1px transparent;border-bottom:solid 3px transparent}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:before,.wy-btn-group:after{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown:hover .wy-dropdown-menu{display:block}.wy-dropdown .caret:after{font-family:fontawesome-webfont;content:"\f0d7";font-size:70%}.wy-dropdown-menu{position:absolute;top:100%;left:0;display:none;float:left;min-width:100%;background:#fcfcfc;z-index:100;border:solid 1px #cfd7dd;box-shadow:0 5px 5px 0 rgba(0,0,0,0.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:solid 1px #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type="search"]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned input,.wy-form-aligned textarea,.wy-form-aligned select,.wy-form-aligned .wy-help-inline,.wy-form-aligned label{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:0.5em 1em 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:0.5em}fieldset{border:0;margin:0;padding:0}legend{display:block;width:100%;border:0;padding:0;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label{display:block;margin:0 0 0.3125em 0;color:#999;font-size:90%}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button{-webkit-appearance:button;cursor:pointer;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}input[type="text"],input[type="password"],input[type="email"],input[type="url"],input[type="date"],input[type="month"],input[type="time"],input[type="datetime"],input[type="datetime-local"],input[type="week"],input[type="number"],input[type="search"],input[type="tel"],input[type="color"]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}input[type="datetime-local"]{padding:0.34375em 0.625em}input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0;margin-right:0.3125em;*height:13px;*width:13px}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input[type="text"]:focus,input[type="password"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus{outline:0;outline:thin dotted \9;border-color:#2980b9}input.no-focus:focus{border-color:#ccc !important}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type="text"][disabled],input[type="password"][disabled],input[type="email"][disabled],input[type="url"][disabled],input[type="date"][disabled],input[type="month"][disabled],input[type="time"][disabled],input[type="datetime"][disabled],input[type="datetime-local"][disabled],input[type="week"][disabled],input[type="number"][disabled],input[type="search"][disabled],input[type="tel"][disabled],input[type="color"][disabled]{cursor:not-allowed;background-color:#f3f6f6;color:#cad2d3}input:focus:invalid,textarea:focus:invalid,select:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,textarea:focus:invalid:focus,select:focus:invalid:focus{border-color:#e9322d}input[type="file"]:focus:invalid:focus,input[type="radio"]:focus:invalid:focus,input[type="checkbox"]:focus:invalid:focus{outline-color:#e9322d}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%}select,textarea{padding:0.5em 0.625em;display:inline-block;border:1px solid #ccc;font-size:0.8em;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#fff;color:#cad2d3;border-color:transparent}.wy-checkbox,.wy-radio{margin:0.5em 0;color:#404040 !important;display:block}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{padding:6px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:solid 1px #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:0.5em 0.625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.wy-control-group{margin-bottom:24px;*zoom:1}.wy-control-group:before,.wy-control-group:after{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type="text"],.wy-control-group.wy-control-group-error input[type="password"],.wy-control-group.wy-control-group-error input[type="email"],.wy-control-group.wy-control-group-error input[type="url"],.wy-control-group.wy-control-group-error input[type="date"],.wy-control-group.wy-control-group-error input[type="month"],.wy-control-group.wy-control-group-error input[type="time"],.wy-control-group.wy-control-group-error input[type="datetime"],.wy-control-group.wy-control-group-error input[type="datetime-local"],.wy-control-group.wy-control-group-error input[type="week"],.wy-control-group.wy-control-group-error input[type="number"],.wy-control-group.wy-control-group-error input[type="search"],.wy-control-group.wy-control-group-error input[type="tel"],.wy-control-group.wy-control-group-error input[type="color"]{border:solid 2px #e74c3c}.wy-control-group.wy-control-group-error textarea{border:solid 2px #e74c3c}.wy-control-group.fluid-input input[type="text"],.wy-control-group.fluid-input input[type="password"],.wy-control-group.fluid-input input[type="email"],.wy-control-group.fluid-input input[type="url"],.wy-control-group.fluid-input input[type="date"],.wy-control-group.fluid-input input[type="month"],.wy-control-group.fluid-input input[type="time"],.wy-control-group.fluid-input input[type="datetime"],.wy-control-group.fluid-input input[type="datetime-local"],.wy-control-group.fluid-input input[type="week"],.wy-control-group.fluid-input input[type="number"],.wy-control-group.fluid-input input[type="search"],.wy-control-group.fluid-input input[type="tel"],.wy-control-group.fluid-input input[type="color"]{width:100%}.wy-form-message-inline{display:inline-block;padding-left:0.3em;color:#666;vertical-align:middle;font-size:90%}.wy-form-message{display:block;color:#ccc;font-size:70%;margin-top:0.3125em;font-style:italic}.wy-tag-input-group{padding:4px 4px 0px 4px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border 0.3s linear;-moz-transition:border 0.3s linear;transition:border 0.3s linear}.wy-tag-input-group .wy-tag{display:inline-block;background-color:rgba(0,0,0,0.1);padding:0.5em 0.625em;border-radius:2px;position:relative;margin-bottom:4px}.wy-tag-input-group .wy-tag .wy-tag-remove{color:#ccc;margin-left:5px}.wy-tag-input-group .wy-tag .wy-tag-remove:hover{color:#e74c3c}.wy-tag-input-group label{margin-left:5px;display:inline-block;margin-bottom:0}.wy-tag-input-group input{border:none;font-size:100%;margin-bottom:4px;box-shadow:none}.wy-form-upload{border:solid 1px #ccc;border-bottom:solid 3px #ccc;background-color:#fff;padding:24px;display:inline-block;text-align:center;cursor:pointer;color:#404040;-webkit-transition:border-color 0.1s ease-in;-moz-transition:border-color 0.1s ease-in;transition:border-color 0.1s ease-in;*zoom:1}.wy-form-upload:before,.wy-form-upload:after{display:table;content:""}.wy-form-upload:after{clear:both}@media screen and (max-width: 480px){.wy-form-upload{width:100%}}.wy-form-upload .image-drop{display:none}.wy-form-upload .image-desktop{display:none}.wy-form-upload .image-loading{display:none}.wy-form-upload .wy-form-upload-icon{display:block;font-size:32px;color:#b3b3b3}.wy-form-upload .image-drop .wy-form-upload-icon{color:#27ae60}.wy-form-upload p{font-size:90%}.wy-form-upload .wy-form-upload-image{float:left;margin-right:24px}@media screen and (max-width: 480px){.wy-form-upload .wy-form-upload-image{width:100%;margin-bottom:24px}}.wy-form-upload img{max-width:125px;max-height:125px;opacity:0.9;-webkit-transition:opacity 0.1s ease-in;-moz-transition:opacity 0.1s ease-in;transition:opacity 0.1s ease-in}.wy-form-upload .wy-form-upload-content{float:left}@media screen and (max-width: 480px){.wy-form-upload .wy-form-upload-content{width:100%}}.wy-form-upload:hover{border-color:#b3b3b3;color:#404040}.wy-form-upload:hover .image-desktop{display:block}.wy-form-upload:hover .image-drag{display:none}.wy-form-upload:hover img{opacity:1}.wy-form-upload:active{border-top:solid 3px #ccc;border-bottom:solid 1px #ccc}.wy-form-upload.wy-form-upload-big{width:100%;text-align:center;padding:72px}.wy-form-upload.wy-form-upload-big .wy-form-upload-content{float:none}.wy-form-upload.wy-form-upload-file p{margin-bottom:0}.wy-form-upload.wy-form-upload-file .wy-form-upload-icon{display:inline-block;font-size:inherit}.wy-form-upload.wy-form-upload-drop{background-color:#ddf7e8}.wy-form-upload.wy-form-upload-drop .image-drop{display:block}.wy-form-upload.wy-form-upload-drop .image-desktop{display:none}.wy-form-upload.wy-form-upload-drop .image-drag{display:none}.wy-form-upload.wy-form-upload-loading .image-drag{display:none}.wy-form-upload.wy-form-upload-loading .image-desktop{display:none}.wy-form-upload.wy-form-upload-loading .image-loading{display:block}.wy-form-upload.wy-form-upload-loading .wy-input-prefix{display:none}.wy-form-upload.wy-form-upload-loading p{margin-bottom:0}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}.wy-form-gallery-manage{margin-left:-12px;margin-right:-12px}.wy-form-gallery-manage li{float:left;padding:12px;width:20%;cursor:pointer}@media screen and (max-width: 768px){.wy-form-gallery-manage li{width:25%}}@media screen and (max-width: 480px){.wy-form-gallery-manage li{width:50%}}.wy-form-gallery-manage li:active{cursor:move}.wy-form-gallery-manage li>a{padding:12px;background-color:#fff;border:solid 1px #e1e4e5;border-bottom:solid 3px #e1e4e5;display:inline-block;-webkit-transition:all 0.1s ease-in;-moz-transition:all 0.1s ease-in;transition:all 0.1s ease-in}.wy-form-gallery-manage li>a:active{border:solid 1px #ccc;border-top:solid 3px #ccc}.wy-form-gallery-manage img{width:100%;-webkit-transition:all 0.05s ease-in;-moz-transition:all 0.05s ease-in;transition:all 0.05s ease-in}li.wy-form-gallery-edit{position:relative;color:#fff;padding:24px;width:100%;display:block;background-color:#343131;border-radius:4px}li.wy-form-gallery-edit .arrow{position:absolute;display:block;top:-50px;left:50%;margin-left:-25px;z-index:500;height:0;width:0;border-color:transparent;border-style:solid;border-width:25px;border-bottom-color:#343131}@media only screen and (max-width: 480px){.wy-form button[type="submit"]{margin:0.7em 0 0}.wy-form input[type="text"],.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0.3em;display:block}.wy-form label{margin-bottom:0.3em;display:block}.wy-form input[type="password"],.wy-form input[type="email"],.wy-form input[type="url"],.wy-form input[type="date"],.wy-form input[type="month"],.wy-form input[type="time"],.wy-form input[type="datetime"],.wy-form input[type="datetime-local"],.wy-form input[type="week"],.wy-form input[type="number"],.wy-form input[type="search"],.wy-form input[type="tel"],.wy-form input[type="color"]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:0.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-controls{margin:1.5em 0 0 0}.wy-form .wy-help-inline,.wy-form-message-inline,.wy-form-message{display:block;font-size:80%;padding:0.2em 0 0.8em}}@media screen and (max-width: 768px){.tablet-hide{display:none}}@media screen and (max-width: 480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.wy-grid-one-col{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;max-width:1066px;margin-top:1.618em}.wy-grid-one-col:before,.wy-grid-one-col:after{display:table;content:""}.wy-grid-one-col:after{clear:both}.wy-grid-one-col section{display:block;float:left;margin-right:2.35765%;width:100%;background:#fcfcfc;padding:1.618em;margin-right:0}.wy-grid-one-col section:last-child{margin-right:0}.wy-grid-index-card{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;max-width:460px;margin-top:1.618em;background:#fcfcfc;padding:1.618em}.wy-grid-index-card:before,.wy-grid-index-card:after{display:table;content:""}.wy-grid-index-card:after{clear:both}.wy-grid-index-card header,.wy-grid-index-card section,.wy-grid-index-card aside{display:block;float:left;margin-right:2.35765%;width:100%}.wy-grid-index-card header:last-child,.wy-grid-index-card section:last-child,.wy-grid-index-card aside:last-child{margin-right:0}.wy-grid-index-card.twocol{max-width:768px}.wy-grid-index-card.twocol section{display:block;float:left;margin-right:2.35765%;width:48.82117%}.wy-grid-index-card.twocol section:last-child{margin-right:0}.wy-grid-index-card.twocol aside{display:block;float:left;margin-right:2.35765%;width:48.82117%}.wy-grid-index-card.twocol aside:last-child{margin-right:0}.wy-grid-search-filter{*zoom:1;max-width:68em;margin-left:auto;margin-right:auto;margin-bottom:24px}.wy-grid-search-filter:before,.wy-grid-search-filter:after{display:table;content:""}.wy-grid-search-filter:after{clear:both}.wy-grid-search-filter .wy-grid-search-filter-input{display:block;float:left;margin-right:2.35765%;width:74.41059%}.wy-grid-search-filter .wy-grid-search-filter-input:last-child{margin-right:0}.wy-grid-search-filter .wy-grid-search-filter-btn{display:block;float:left;margin-right:2.35765%;width:23.23176%}.wy-grid-search-filter .wy-grid-search-filter-btn:last-child{margin-right:0}.wy-table,.rst-content table.docutils,.rst-content table.field-list{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.wy-table caption,.rst-content table.docutils caption,.rst-content table.field-list caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td,.wy-table th,.rst-content table.docutils th,.rst-content table.field-list th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.wy-table td:first-child,.rst-content table.docutils td:first-child,.rst-content table.field-list td:first-child,.wy-table th:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list th:first-child{border-left-width:0}.wy-table thead,.rst-content table.docutils thead,.rst-content table.field-list thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.wy-table thead th,.rst-content table.docutils thead th,.rst-content table.field-list thead th{font-weight:bold;border-bottom:solid 2px #e1e4e5}.wy-table td,.rst-content table.docutils td,.rst-content table.field-list td{background-color:transparent;vertical-align:middle}.wy-table td p,.rst-content table.docutils td p,.rst-content table.field-list td p{line-height:18px;margin-bottom:0}.wy-table .wy-table-cell-min,.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min{width:1%;padding-right:0}.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox],.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:gray;font-size:90%}.wy-table-tertiary{color:gray;font-size:80%}.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td,.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td{background-color:#f3f6f6}.wy-table-backed{background-color:#f3f6f6}.wy-table-bordered-all,.rst-content table.docutils{border:1px solid #e1e4e5}.wy-table-bordered-all td,.rst-content table.docutils td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.wy-table-bordered-all tbody>tr:last-child td,.rst-content table.docutils tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0 !important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}html{height:100%;overflow-x:hidden}body{font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;font-weight:normal;color:#404040;min-height:100%;overflow-x:hidden;background:#edf0f2}a{color:#2980b9;text-decoration:none}a:hover{color:#3091d1}.link-danger{color:#e74c3c}.link-danger:hover{color:#d62c1a}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif}p{line-height:24px;margin:0;font-size:16px;margin-bottom:24px}h1{font-size:175%}h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}small{font-size:80%}code,.rst-content tt{white-space:nowrap;max-width:100%;background:#fff;border:solid 1px #e1e4e5;font-size:75%;padding:0 5px;font-family:"Incosolata","Consolata","Monaco",monospace;color:#e74c3c;overflow-x:auto}code.code-large,.rst-content tt.code-large{font-size:90%}.full-width{width:100%}.wy-plain-list-disc,.rst-content .section ul,.rst-content .toctree-wrapper ul{list-style:disc;line-height:24px;margin-bottom:24px}.wy-plain-list-disc li,.rst-content .section ul li,.rst-content .toctree-wrapper ul li{list-style:disc;margin-left:24px}.wy-plain-list-disc li ul,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li ul{margin-bottom:0}.wy-plain-list-disc li li,.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li{list-style:circle}.wy-plain-list-disc li li li,.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li{list-style:square}.wy-plain-list-decimal,.rst-content .section ol,.rst-content ol.arabic{list-style:decimal;line-height:24px;margin-bottom:24px}.wy-plain-list-decimal li,.rst-content .section ol li,.rst-content ol.arabic li{list-style:decimal;margin-left:24px}.wy-type-large{font-size:120%}.wy-type-normal{font-size:100%}.wy-type-small{font-size:100%}.wy-type-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22 !important}a.wy-text-warning:hover{color:#eb9950 !important}.wy-text-info{color:#2980b9 !important}a.wy-text-info:hover{color:#409ad5 !important}.wy-text-success{color:#27ae60 !important}a.wy-text-success:hover{color:#36d278 !important}.wy-text-danger{color:#e74c3c !important}a.wy-text-danger:hover{color:#ed7669 !important}.wy-text-neutral{color:#404040 !important}a.wy-text-neutral:hover{color:#595959 !important}.codeblock-example{border:1px solid #e1e4e5;border-bottom:none;padding:24px;padding-top:48px;font-weight:500;background:#fff;position:relative}.codeblock-example:after{content:"Example";position:absolute;top:0px;left:0px;background:#9b59b6;color:#fff;padding:6px 12px}.codeblock-example.prettyprint-example-only{border:1px solid #e1e4e5;margin-bottom:24px}.codeblock,.rst-content .literal-block,div[class^='highlight']{border:1px solid #e1e4e5;padding:0px;overflow-x:auto;background:#fff;margin:1px 0 24px 0}.codeblock div[class^='highlight'],.rst-content .literal-block div[class^='highlight'],div[class^='highlight'] div[class^='highlight']{border:none;background:none;margin:0}div[class^='highlight'] td.code{width:100%}.linenodiv pre{border-right:solid 1px #e6e9ea;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;color:#d9d9d9}div[class^='highlight'] pre{white-space:pre;margin:0;padding:12px 12px;font-family:"Incosolata","Consolata","Monaco",monospace;font-size:12px;line-height:1.5;display:block;overflow:auto;color:#404040}pre.literal-block{@extends .codeblock;}@media print{.codeblock,.rst-content .literal-block,div[class^='highlight'],div[class^='highlight'] pre{white-space:pre-wrap}}.hll{background-color:#ffc;margin:0 -12px;padding:0 12px;display:block}.c{color:#998;font-style:italic}.err{color:#a61717;background-color:#e3d2d2}.k{font-weight:bold}.o{font-weight:bold}.cm{color:#998;font-style:italic}.cp{color:#999;font-weight:bold}.c1{color:#998;font-style:italic}.cs{color:#999;font-weight:bold;font-style:italic}.gd{color:#000;background-color:#fdd}.gd .x{color:#000;background-color:#faa}.ge{font-style:italic}.gr{color:#a00}.gh{color:#999}.gi{color:#000;background-color:#dfd}.gi .x{color:#000;background-color:#afa}.go{color:#888}.gp{color:#555}.gs{font-weight:bold}.gu{color:purple;font-weight:bold}.gt{color:#a00}.kc{font-weight:bold}.kd{font-weight:bold}.kn{font-weight:bold}.kp{font-weight:bold}.kr{font-weight:bold}.kt{color:#458;font-weight:bold}.m{color:#099}.s{color:#d14}.n{color:#333}.na{color:teal}.nb{color:#0086b3}.nc{color:#458;font-weight:bold}.no{color:teal}.ni{color:purple}.ne{color:#900;font-weight:bold}.nf{color:#900;font-weight:bold}.nn{color:#555}.nt{color:navy}.nv{color:teal}.ow{font-weight:bold}.w{color:#bbb}.mf{color:#099}.mh{color:#099}.mi{color:#099}.mo{color:#099}.sb{color:#d14}.sc{color:#d14}.sd{color:#d14}.s2{color:#d14}.se{color:#d14}.sh{color:#d14}.si{color:#d14}.sx{color:#d14}.sr{color:#009926}.s1{color:#d14}.ss{color:#990073}.bp{color:#999}.vc{color:teal}.vg{color:teal}.vi{color:teal}.il{color:#099}.gc{color:#999;background-color:#eaf2f5}.wy-breadcrumbs li{display:inline-block}.wy-breadcrumbs li.wy-breadcrumbs-aside{float:right}.wy-breadcrumbs li a{display:inline-block;padding:5px}.wy-breadcrumbs li a:first-child{padding-left:0}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width: 480px){.wy-breadcrumbs-extra{display:none}.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:before,.wy-menu-horiz:after{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz ul,.wy-menu-horiz li{display:inline-block}.wy-menu-horiz li:hover{background:rgba(255,255,255,0.1)}.wy-menu-horiz li.divide-left{border-left:solid 1px #404040}.wy-menu-horiz li.divide-right{border-right:solid 1px #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical header{height:32px;display:inline-block;line-height:32px;padding:0 1.618em;display:block;font-weight:bold;text-transform:uppercase;font-size:80%;color:#2980b9;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:solid 1px #404040}.wy-menu-vertical li.divide-bottom{border-bottom:solid 1px #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:gray;border-right:solid 1px #c9c9c9;padding:0.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.wy-menu-vertical li.on a,.wy-menu-vertical li.current>a{color:#404040;padding:0.4045em 1.618em;font-weight:bold;position:relative;background:#fcfcfc;border:none;border-bottom:solid 1px #c9c9c9;border-top:solid 1px #c9c9c9;padding-left:1.618em -4px}.wy-menu-vertical li.on a:hover,.wy-menu-vertical li.current>a:hover{background:#fcfcfc}.wy-menu-vertical li.tocktree-l2.current>a{background:#c9c9c9}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical .local-toc li ul{display:block}.wy-menu-vertical li ul li a{margin-bottom:0;color:#b3b3b3;font-weight:normal}.wy-menu-vertical a{display:inline-block;line-height:18px;padding:0.4045em 1.618em;display:block;position:relative;font-size:90%;color:#b3b3b3}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-side-nav-search{z-index:200;background-color:#2980b9;text-align:center;padding:0.809em;display:block;color:#fcfcfc;margin-bottom:0.809em}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto 0.809em auto;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search>a,.wy-side-nav-search .wy-dropdown>a{color:#fcfcfc;font-size:100%;font-weight:bold;display:inline-block;padding:4px 6px;margin-bottom:0.809em}.wy-side-nav-search>a:hover,.wy-side-nav-search .wy-dropdown>a:hover{background:rgba(255,255,255,0.1)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all 0.2s ease-in;-moz-transition:all 0.2s ease-in;transition:all 0.2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:left repeat-y #fcfcfc;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDoxOERBMTRGRDBFMUUxMUUzODUwMkJCOThDMEVFNURFMCIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDoxOERBMTRGRTBFMUUxMUUzODUwMkJCOThDMEVFNURFMCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjE4REExNEZCMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjE4REExNEZDMEUxRTExRTM4NTAyQkI5OEMwRUU1REUwIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EwrlwAAAAA5JREFUeNpiMDU0BAgwAAE2AJgB9BnaAAAAAElFTkSuQmCC);background-size:300px 1px}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:absolute;top:0;left:0;width:300px;overflow:hidden;min-height:100%;background:#343131;z-index:200}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:0.4045em 0.809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:before,.wy-nav-top:after{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:bold}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,0.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:#999}footer p{margin-bottom:12px}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:before,.rst-footer-buttons:after{display:table;content:""}.rst-footer-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:solid 1px #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:solid 1px #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:gray;font-size:90%}@media screen and (max-width: 768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width: 1400px){.wy-nav-content-wrap{background:rgba(0,0,0,0.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.wy-nav-side{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;border-top:solid 10px #343131;font-family:"Lato","proxima-nova","Helvetica Neue",Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:before,.rst-versions .rst-current-version:after{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-success .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-danger .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-info .rst-versions .rst-current-version .wy-input-context,.rst-versions .rst-current-version .wy-tag-input-group .wy-tag .wy-tag-remove,.wy-tag-input-group .wy-tag .rst-versions .rst-current-version .wy-tag-remove,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-content dl dt .rst-versions .rst-current-version .headerlink{color:#fcfcfc}.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:gray;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:solid 1px #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px}.rst-versions.rst-badge .icon-book{float:none}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge .rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width: 768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}img{width:100%;height:auto}}.rst-content img{max-width:100%;height:auto !important}.rst-content .section>img{margin-bottom:24px}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content .note .last,.rst-content .attention .last,.rst-content .caution .last,.rst-content .danger .last,.rst-content .error .last,.rst-content .hint .last,.rst-content .important .last,.rst-content .tip .last,.rst-content .warning .last,.rst-content .seealso .last{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,0.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent !important;border-color:rgba(0,0,0,0.1) !important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha li{list-style:upper-alpha}.rst-content .section ol p,.rst-content .section ul p{margin-bottom:12px}.rst-content .line-block{margin-left:24px}.rst-content .topic-title{font-weight:bold;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0px 0px 24px 24px}.rst-content .align-left{float:left;margin:0px 24px 24px 0px}.rst-content .align-center{margin:auto;display:block}.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content dl dt .headerlink{display:none;visibility:hidden;font-size:14px}.rst-content h1 .headerlink:after,.rst-content h2 .headerlink:after,.rst-content h3 .headerlink:after,.rst-content h4 .headerlink:after,.rst-content h5 .headerlink:after,.rst-content h6 .headerlink:after,.rst-content dl dt .headerlink:after{visibility:visible;content:"\f0c1";font-family:fontawesome-webfont;display:inline-block}.rst-content h1:hover .headerlink,.rst-content h2:hover .headerlink,.rst-content h3:hover .headerlink,.rst-content h4:hover .headerlink,.rst-content h5:hover .headerlink,.rst-content h6:hover .headerlink,.rst-content dl dt:hover .headerlink{display:inline-block}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:solid 1px #e1e4e5}.rst-content .sidebar p,.rst-content .sidebar ul,.rst-content .sidebar dl{font-size:90%}.rst-content .sidebar .last{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:"Roboto Slab","ff-tisa-web-pro","Georgia",Arial,sans-serif;font-weight:bold;background:#e1e4e5;padding:6px 12px;margin:-24px;margin-bottom:24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;display:inline-block;font-weight:bold;padding:0 6px}.rst-content .footnote-reference,.rst-content .citation-reference{vertical-align:super;font-size:90%}.rst-content table.docutils.citation,.rst-content table.docutils.footnote{background:none;border:none;color:#999}.rst-content table.docutils.citation td,.rst-content table.docutils.citation tr,.rst-content table.docutils.footnote td,.rst-content table.docutils.footnote tr{border:none;background-color:transparent !important;white-space:normal}.rst-content table.docutils.citation td.label,.rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}.rst-content table.field-list{border:none}.rst-content table.field-list td{border:none}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left;padding-left:0}.rst-content tt{color:#000}.rst-content tt big,.rst-content tt em{font-size:100% !important;line-height:normal}.rst-content tt .xref,a .rst-content tt{font-weight:bold}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:bold}.rst-content dl p,.rst-content dl table,.rst-content dl ul,.rst-content dl ol{margin-bottom:12px !important}.rst-content dl dd{margin:0 0 12px 24px}.rst-content dl:not(.docutils){margin-bottom:24px}.rst-content dl:not(.docutils) dt{display:inline-block;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:solid 3px #6ab0de;padding:6px;position:relative}.rst-content dl:not(.docutils) dt:before{color:#6ab0de}.rst-content dl:not(.docutils) dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dl dt{margin-bottom:6px;border:none;border-left:solid 3px #ccc;background:#f0f0f0;color:gray}.rst-content dl:not(.docutils) dl dt .headerlink{color:#404040;font-size:100% !important}.rst-content dl:not(.docutils) dt:first-child{margin-top:0}.rst-content dl:not(.docutils) tt{font-weight:bold}.rst-content dl:not(.docutils) tt.descname,.rst-content dl:not(.docutils) tt.descclassname{background-color:transparent;border:none;padding:0;font-size:100% !important}.rst-content dl:not(.docutils) tt.descname{font-weight:bold}.rst-content dl:not(.docutils) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:bold}.rst-content dl:not(.docutils) .property{display:inline-block;padding-right:8px}.rst-content .viewcode-link,.rst-content .viewcode-back{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}@media screen and (max-width: 480px){.rst-content .sidebar{width:100%}}span[id*='MathJax-Span']{color:#404040} diff --git a/docs/_themes/sphinx_rtd_theme/static/favicon.ico b/docs/_themes/sphinx_rtd_theme/static/favicon.ico deleted file mode 100644 index 6970cfde25f4158dcde472becc0e56d7eb26d10c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6261 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 diff --git a/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot b/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.eot deleted file mode 100755 index 0662cb96bfb78cb2603df4bc9995314bd6806312..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37405 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�-A&iY%khEsGVMaq&$BJA^QAs8x>7-g_ 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 k3oQ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf b/docs/_themes/sphinx_rtd_theme/static/font/fontawesome_webfont.ttf deleted file mode 100755 index d3659246915cacb0c9204271f1f9fc5f77049eac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79076 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 diff --git a/docs/_themes/sphinx_rtd_theme/static/js/theme.js b/docs/_themes/sphinx_rtd_theme/static/js/theme.js deleted file mode 100755 index 58e514c0..00000000 --- a/docs/_themes/sphinx_rtd_theme/static/js/theme.js +++ /dev/null @@ -1,16 +0,0 @@ -$( document ).ready(function() { - // Shift nav in mobile when clicking the menu. - $("[data-toggle='wy-nav-top']").click(function() { - $("[data-toggle='wy-nav-shift']").toggleClass("shift"); - $("[data-toggle='rst-versions']").toggleClass("shift"); - }); - // Close menu when you click a link. - $(".wy-menu-vertical .current ul li a").click(function() { - $("[data-toggle='wy-nav-shift']").removeClass("shift"); - $("[data-toggle='rst-versions']").toggleClass("shift"); - }); - $("[data-toggle='rst-current-version']").click(function() { - $("[data-toggle='rst-versions']").toggleClass("shift-up"); - }); - $("table.docutils:not(.field-list").wrap("

    "); -}); diff --git a/docs/_themes/sphinx_rtd_theme/theme.conf b/docs/_themes/sphinx_rtd_theme/theme.conf deleted file mode 100755 index 173ca698..00000000 --- a/docs/_themes/sphinx_rtd_theme/theme.conf +++ /dev/null @@ -1,8 +0,0 @@ -[theme] -inherit = basic -stylesheet = css/theme.css - -[options] -typekit_id = hiw1hhg -analytics_id = -canonical_url = \ No newline at end of file diff --git a/docs/_themes/sphinx_rtd_theme/versions.html b/docs/_themes/sphinx_rtd_theme/versions.html deleted file mode 100755 index 93319be8..00000000 --- a/docs/_themes/sphinx_rtd_theme/versions.html +++ /dev/null @@ -1,37 +0,0 @@ -{% if READTHEDOCS %} -{# Add rst-badge after rst-versions for small badge style. #} -
    - - Read the Docs - v: {{ current_version }} - - -
    -
    -
    Versions
    - {% for slug, url in versions %} -
    {{ slug }}
    - {% endfor %} -
    -
    -
    Downloads
    - {% for type, url in downloads %} -
    {{ type }}
    - {% endfor %} -
    -
    -
    On Read the Docs
    -
    - Project Home -
    -
    - Builds -
    -
    -
    - Free document hosting provided by Read the Docs. - -
    -
    -{% endif %} - diff --git a/docs/conf.py b/docs/conf.py index cddd35db..468e71e0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,10 @@ import sys, os +import sphinx_rtd_theme + +import mongoengine + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -44,7 +48,6 @@ copyright = u'2009, MongoEngine Authors' # |version| and |release|, also used in various other places throughout the # built documents. # -import mongoengine # The short X.Y version. version = mongoengine.get_version() # The full version, including alpha/beta/rc tags. @@ -97,10 +100,12 @@ html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +html_theme_options = { + 'canonical_url': 'http://docs.mongoengine.org/en/latest/' +} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -199,7 +204,3 @@ latex_documents = [ #latex_use_modindex = True autoclass_content = 'both' - -html_theme_options = dict( - canonical_url='http://docs.mongoengine.org/en/latest/' -) diff --git a/requirements.txt b/requirements.txt index 854ed26d..4e3ea940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ pymongo>=2.7.1 six==1.10.0 flake8 flake8-import-order +Sphinx==1.5.5 +sphinx-rtd-theme==0.2.4 From e2a0b42d03a00f1295d1313abe05fdc4f9e66dad Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 30 Apr 2017 18:29:17 -0400 Subject: [PATCH 073/268] clarify test_get_changed_fields_query_count --- tests/queryset/queryset.py | 50 ++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 3ae3fb42..7b12625c 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -917,7 +917,9 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Blog.objects.count(), 3) def test_get_changed_fields_query_count(self): - + """Make sure we don't perform unnecessary db operations when + none of document's fields were updated. + """ class Person(Document): name = StringField() owns = ListField(ReferenceField('Organization')) @@ -925,8 +927,8 @@ class QuerySetTest(unittest.TestCase): class Organization(Document): name = StringField() - owner = ReferenceField('Person') - employees = ListField(ReferenceField('Person')) + owner = ReferenceField(Person) + employees = ListField(ReferenceField(Person)) class Project(Document): name = StringField() @@ -945,35 +947,35 @@ class QuerySetTest(unittest.TestCase): with query_counter() as q: self.assertEqual(q, 0) - fresh_o1 = Organization.objects.get(id=o1.id) - self.assertEqual(1, q) - fresh_o1._get_changed_fields() - self.assertEqual(1, q) - - with query_counter() as q: - self.assertEqual(q, 0) - - fresh_o1 = Organization.objects.get(id=o1.id) - fresh_o1.save() # No changes, does nothing - + # Fetching a document should result in a query. + org = Organization.objects.get(id=o1.id) self.assertEqual(q, 1) - with query_counter() as q: - self.assertEqual(q, 0) - - fresh_o1 = Organization.objects.get(id=o1.id) - fresh_o1.save(cascade=False) # No changes, does nothing - + # Checking changed fields of a newly fetched document should not + # result in a query. + org._get_changed_fields() self.assertEqual(q, 1) + # Saving a doc without changing any of its fields should not result + # in a query (with or without cascade=False). + org = Organization.objects.get(id=o1.id) with query_counter() as q: + org.save() self.assertEqual(q, 0) - fresh_o1 = Organization.objects.get(id=o1.id) - fresh_o1.employees.append(p2) # Dereferences - fresh_o1.save(cascade=False) # Saves + org = Organization.objects.get(id=o1.id) + with query_counter() as q: + org.save(cascade=False) + self.assertEqual(q, 0) - self.assertEqual(q, 3) + # Saving a doc after you append a reference to it should result in + # two db operations (a query for the reference and an update). + # TODO dereferencing of p2 shouldn't be necessary. + org = Organization.objects.get(id=o1.id) + with query_counter() as q: + org.employees.append(p2) # dereferences p2 + org.save() # saves the org + self.assertEqual(q, 2) @skip_pymongo3 def test_slave_okay(self): From 2cf23e33e343ab4b717f220dd6400078b81c51c8 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 19:26:10 -0400 Subject: [PATCH 074/268] Document._get_update_doc helper method --- mongoengine/document.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index ad976216..4babd1b9 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -300,7 +300,7 @@ class Document(BaseDocument): created. :param force_insert: only try to create a new document, don't allow - updates of existing documents + updates of existing documents. :param validate: validates the document; set to ``False`` to skip. :param clean: call the document clean method, requires `validate` to be True. @@ -441,6 +441,21 @@ class Document(BaseDocument): return object_id + def _get_update_doc(self): + """Return a dict containing all the $set and $unset operations + that should be sent to MongoDB based on the changes made to this + Document. + """ + updates, removals = self._delta() + + update_doc = {} + if updates: + update_doc['$set'] = updates + if removals: + update_doc['$unset'] = removals + + return update_doc + def _save_update(self, doc, save_condition, write_concern): """Update an existing document. @@ -466,15 +481,10 @@ class Document(BaseDocument): val = val[ak] select_dict['.'.join(actual_key)] = val - updates, removals = self._delta() - update_query = {} - if updates: - update_query['$set'] = updates - if removals: - update_query['$unset'] = removals - if updates or removals: + update_doc = self._get_update_doc() + if update_doc: upsert = save_condition is None - last_error = collection.update(select_dict, update_query, + last_error = collection.update(select_dict, update_doc, upsert=upsert, **write_concern) if not upsert and last_error['n'] == 0: raise SaveConditionError('Race condition preventing' From 944d1c0a4ab817a9f0dcd124bbf8456520367e8b Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 19:54:58 -0400 Subject: [PATCH 075/268] use a newer pypy3 (https://github.com/travis-ci/travis-ci/issues/6277) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47448950..bb6cd721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,8 @@ python: - 2.7 - 3.5 - pypy -- pypy3 +- pypy3.3-5.2-alpha1 + env: - MONGODB=2.6 PYMONGO=2.7 From c00914bea2951d53a9b8021e3facf406e8d9109e Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 20:32:52 -0400 Subject: [PATCH 076/268] fix tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 7f0d36e4..54f0cc2e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30} +envlist = {py27,py35,pypy,pypy33-52-alpha1}-{mg27,mg28,mg30} [testenv] commands = From 03ff61d11339ac2dccead188a0afaaf1ea6b7066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Sun, 7 May 2017 21:11:14 -0400 Subject: [PATCH 077/268] better db_field validation (#1547) --- mongoengine/base/fields.py | 9 ++++++++- tests/document/instance.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 5658b185..e2b5d321 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -81,7 +81,14 @@ class BaseField(object): self.sparse = sparse self._owner_document = None - # Validate the db_field + # Make sure db_field is a string (if it's explicitly defined). + if ( + self.db_field is not None and + not isinstance(self.db_field, six.string_types) + ): + raise TypeError('db_field should be a string.') + + # Make sure db_field doesn't contain any forbidden characters. if isinstance(self.db_field, six.string_types) and ( '.' in self.db_field or '\0' in self.db_field or diff --git a/tests/document/instance.py b/tests/document/instance.py index c98b1405..37bbe337 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -242,7 +242,7 @@ class InstanceTest(unittest.TestCase): Zoo.drop_collection() class Zoo(Document): - animals = ListField(GenericReferenceField(Animal)) + animals = ListField(GenericReferenceField()) # Save a reference to each animal zoo = Zoo(animals=Animal.objects) From 009059def407b63db38c317258db1df88084e2eb Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 21:29:13 -0400 Subject: [PATCH 078/268] revert #1497 --- mongoengine/dereference.py | 5 --- mongoengine/fields.py | 11 +----- tests/fields/fields.py | 70 ++------------------------------------ tests/test_dereference.py | 70 -------------------------------------- 4 files changed, 3 insertions(+), 153 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index f30b2c15..59204d4d 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -1,4 +1,3 @@ -from collections import OrderedDict from bson import DBRef, SON import six @@ -202,10 +201,6 @@ class DeReference(object): as_tuple = isinstance(items, tuple) iterator = enumerate(items) data = [] - elif isinstance(items, OrderedDict): - is_list = False - iterator = items.iteritems() - data = OrderedDict() else: is_list = False iterator = items.iteritems() diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0010f818..c34fe1b1 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -6,7 +6,6 @@ import socket import time import uuid import warnings -from collections import Mapping from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -705,14 +704,6 @@ class DynamicField(BaseField): Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data""" - def __init__(self, container_class=dict, *args, **kwargs): - self._container_cls = container_class - if not issubclass(self._container_cls, Mapping): - self.error('The class that is specified in `container_class` parameter ' - 'must be a subclass of `dict`.') - - super(DynamicField, self).__init__(*args, **kwargs) - def to_mongo(self, value, use_db_field=True, fields=None): """Convert a Python type to a MongoDB compatible type. """ @@ -738,7 +729,7 @@ class DynamicField(BaseField): is_list = True value = {k: v for k, v in enumerate(value)} - data = self._container_cls() + data = {} for k, v in value.iteritems(): data[k] = self.to_mongo(v, use_db_field, fields) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 80606256..60c8370a 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -5,11 +5,9 @@ import uuid import math import itertools import re -import pymongo import sys from nose.plugins.skip import SkipTest -from collections import OrderedDict import six try: @@ -28,12 +26,9 @@ except ImportError: from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, - _document_registry, TopLevelDocumentMetaclass) + _document_registry) -from tests.utils import MongoDBTestCase, MONGO_TEST_DB -from mongoengine.python_support import IS_PYMONGO_3 -if IS_PYMONGO_3: - from bson import CodecOptions +from tests.utils import MongoDBTestCase __all__ = ("FieldTest", "EmbeddedDocumentListFieldTestCase") @@ -4188,67 +4183,6 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertTrue(hasattr(CustomData.c_field, 'custom_data')) self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) - def test_dynamicfield_with_container_class(self): - """ - Tests that object can be stored in order by DynamicField class - with container_class parameter. - """ - raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] - - class Doc(Document): - ordered_data = DynamicField(container_class=OrderedDict) - unordered_data = DynamicField() - - Doc.drop_collection() - - doc = Doc(ordered_data=OrderedDict(raw_data), unordered_data=dict(raw_data)).save() - - # checks that the data is in order - self.assertEqual(type(doc.ordered_data), OrderedDict) - self.assertEqual(type(doc.unordered_data), dict) - self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') - - # checks that the data is stored to the database in order - pymongo_db = pymongo.MongoClient()[MONGO_TEST_DB] - if IS_PYMONGO_3: - codec_option = CodecOptions(document_class=OrderedDict) - db_doc = pymongo_db.doc.with_options(codec_options=codec_option).find_one() - else: - db_doc = pymongo_db.doc.find_one(as_class=OrderedDict) - - self.assertEqual(','.join(doc.ordered_data.keys()), 'd,c,b,a') - - def test_dynamicfield_with_wrong_container_class(self): - with self.assertRaises(ValidationError): - class DocWithInvalidField: - data = DynamicField(container_class=list) - - def test_dynamicfield_with_wrong_container_class_and_reload_docuemnt(self): - # This is because 'codec_options' is supported on pymongo3 or later - if IS_PYMONGO_3: - class OrderedDocument(Document): - my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass - - @classmethod - def _get_collection(cls): - collection = super(OrderedDocument, cls)._get_collection() - opts = CodecOptions(document_class=OrderedDict) - - return collection.with_options(codec_options=opts) - - raw_data = [('d', 1), ('c', 2), ('b', 3), ('a', 4)] - - class Doc(OrderedDocument): - data = DynamicField(container_class=OrderedDict) - - Doc.drop_collection() - - doc = Doc(data=OrderedDict(raw_data)).save() - doc.reload() - - self.assertEqual(type(doc.data), OrderedDict) - self.assertEqual(','.join(doc.data.keys()), 'd,c,b,a') class CachedReferenceFieldTest(MongoDBTestCase): diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 9a976611..7f58a85b 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -2,15 +2,10 @@ import unittest from bson import DBRef, ObjectId -from collections import OrderedDict from mongoengine import * from mongoengine.connection import get_db from mongoengine.context_managers import query_counter -from mongoengine.python_support import IS_PYMONGO_3 -from mongoengine.base import TopLevelDocumentMetaclass -if IS_PYMONGO_3: - from bson import CodecOptions class FieldTest(unittest.TestCase): @@ -1292,70 +1287,5 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) - def test_dynamic_field_dereference(self): - class Merchandise(Document): - name = StringField() - price = IntField() - - class Store(Document): - merchandises = DynamicField() - - Merchandise.drop_collection() - Store.drop_collection() - - merchandises = { - '#1': Merchandise(name='foo', price=100).save(), - '#2': Merchandise(name='bar', price=120).save(), - '#3': Merchandise(name='baz', price=110).save(), - } - Store(merchandises=merchandises).save() - - store = Store.objects().first() - for obj in store.merchandises.values(): - self.assertFalse(isinstance(obj, Merchandise)) - - store.select_related() - for obj in store.merchandises.values(): - self.assertTrue(isinstance(obj, Merchandise)) - - def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self): - # This is because 'codec_options' is supported on pymongo3 or later - if IS_PYMONGO_3: - class OrderedDocument(Document): - my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass - - @classmethod - def _get_collection(cls): - collection = super(OrderedDocument, cls)._get_collection() - opts = CodecOptions(document_class=OrderedDict) - - return collection.with_options(codec_options=opts) - - class Merchandise(Document): - name = StringField() - price = IntField() - - class Store(OrderedDocument): - merchandises = DynamicField(container_class=OrderedDict) - - Merchandise.drop_collection() - Store.drop_collection() - - merchandises = OrderedDict() - merchandises['#1'] = Merchandise(name='foo', price=100).save() - merchandises['#2'] = Merchandise(name='bar', price=120).save() - merchandises['#3'] = Merchandise(name='baz', price=110).save() - - Store(merchandises=merchandises).save() - - store = Store.objects().first() - - store.select_related() - - # confirms that the load data order is same with the one at storing - self.assertTrue(type(store.merchandises), OrderedDict) - self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3') - if __name__ == '__main__': unittest.main() From b82d026f394daffaa62641af3b73e8c2ae8b1fcc Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 21:37:05 -0400 Subject: [PATCH 079/268] Revert "fix tox.ini" This reverts commit c00914bea2951d53a9b8021e3facf406e8d9109e. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 54f0cc2e..7f0d36e4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py35,pypy,pypy33-52-alpha1}-{mg27,mg28,mg30} +envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30} [testenv] commands = From 689fe4ed9ab58980a9d6cc4ef7a8be045004aa47 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 21:37:14 -0400 Subject: [PATCH 080/268] Revert "use a newer pypy3 (https://github.com/travis-ci/travis-ci/issues/6277)" This reverts commit 944d1c0a4ab817a9f0dcd124bbf8456520367e8b. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bb6cd721..47448950 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,7 @@ python: - 2.7 - 3.5 - pypy -- pypy3.3-5.2-alpha1 - +- pypy3 env: - MONGODB=2.6 PYMONGO=2.7 From 33e9ef21067d887013b9ab7bfd0571c19f6d06f1 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 7 May 2017 21:37:38 -0400 Subject: [PATCH 081/268] dont test pypy3 temporarily --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 47448950..8f846ab1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,6 @@ python: - 2.7 - 3.5 - pypy -- pypy3 env: - MONGODB=2.6 PYMONGO=2.7 From 3b88712402a5c4c7ddab45a1853ed37696942dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20W=C3=B3jcik?= Date: Mon, 8 May 2017 00:02:42 -0400 Subject: [PATCH 082/268] Cleaner as_pymongo (#1549) --- mongoengine/queryset/base.py | 88 +++++++++++------------------------- tests/queryset/queryset.py | 87 +++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 75 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 7c60a489..f7c32d20 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -67,7 +67,6 @@ class BaseQuerySet(object): self._scalar = [] self._none = False self._as_pymongo = False - self._as_pymongo_coerce = False self._search_text = None # If inheritance is allowed, only return instances and instances of @@ -728,11 +727,12 @@ class BaseQuerySet(object): '%s is not a subclass of BaseQuerySet' % new_qs.__name__) copy_props = ('_mongo_query', '_initial_query', '_none', '_query_obj', - '_where_clause', '_loaded_fields', '_ordering', '_snapshot', - '_timeout', '_class_check', '_slave_okay', '_read_preference', - '_iter', '_scalar', '_as_pymongo', '_as_pymongo_coerce', + '_where_clause', '_loaded_fields', '_ordering', + '_snapshot', '_timeout', '_class_check', '_slave_okay', + '_read_preference', '_iter', '_scalar', '_as_pymongo', '_limit', '_skip', '_hint', '_auto_dereference', - '_search_text', 'only_fields', '_max_time_ms', '_comment') + '_search_text', 'only_fields', '_max_time_ms', + '_comment') for prop in copy_props: val = getattr(self, prop) @@ -939,7 +939,8 @@ class BaseQuerySet(object): posts = BlogPost.objects(...).fields(slice__comments=5) - :param kwargs: A set keywors arguments identifying what to include. + :param kwargs: A set of keyword arguments identifying what to + include, exclude, or slice. .. versionadded:: 0.5 """ @@ -1128,16 +1129,15 @@ class BaseQuerySet(object): """An alias for scalar""" return self.scalar(*fields) - def as_pymongo(self, coerce_types=False): + def as_pymongo(self): """Instead of returning Document instances, return raw values from pymongo. - :param coerce_types: Field types (if applicable) would be use to - coerce types. + This method is particularly useful if you don't need dereferencing + and care primarily about the speed of data retrieval. """ queryset = self.clone() queryset._as_pymongo = True - queryset._as_pymongo_coerce = coerce_types return queryset def max_time_ms(self, ms): @@ -1799,59 +1799,25 @@ class BaseQuerySet(object): return tuple(data) - def _get_as_pymongo(self, row): - # Extract which fields paths we should follow if .fields(...) was - # used. If not, handle all fields. - if not getattr(self, '__as_pymongo_fields', None): - self.__as_pymongo_fields = [] + def _get_as_pymongo(self, doc): + """Clean up a PyMongo doc, removing fields that were only fetched + for the sake of MongoEngine's implementation, and return it. + """ + # Always remove _cls as a MongoEngine's implementation detail. + if '_cls' in doc: + del doc['_cls'] - for field in self._loaded_fields.fields - set(['_cls']): - self.__as_pymongo_fields.append(field) - while '.' in field: - field, _ = field.rsplit('.', 1) - self.__as_pymongo_fields.append(field) + # If the _id was not included in a .only or was excluded in a .exclude, + # remove it from the doc (we always fetch it so that we can properly + # construct documents). + fields = self._loaded_fields + if fields and '_id' in doc and ( + (fields.value == QueryFieldList.ONLY and '_id' not in fields.fields) or + (fields.value == QueryFieldList.EXCLUDE and '_id' in fields.fields) + ): + del doc['_id'] - all_fields = not self.__as_pymongo_fields - - def clean(data, path=None): - path = path or '' - - if isinstance(data, dict): - new_data = {} - for key, value in data.iteritems(): - new_path = '%s.%s' % (path, key) if path else key - - if all_fields: - include_field = True - elif self._loaded_fields.value == QueryFieldList.ONLY: - include_field = new_path in self.__as_pymongo_fields - else: - include_field = new_path not in self.__as_pymongo_fields - - if include_field: - new_data[key] = clean(value, path=new_path) - data = new_data - elif isinstance(data, list): - data = [clean(d, path=path) for d in data] - else: - if self._as_pymongo_coerce: - # If we need to coerce types, we need to determine the - # type of this field and use the corresponding - # .to_python(...) - EmbeddedDocumentField = _import_class('EmbeddedDocumentField') - - obj = self._document - for chunk in path.split('.'): - obj = getattr(obj, chunk, None) - if obj is None: - break - elif isinstance(obj, EmbeddedDocumentField): - obj = obj.document_type - if obj and data is not None: - data = obj.to_python(data) - return data - - return clean(row) + return doc def _sub_js_fields(self, code): """When fields are specified with [~fieldname] syntax, where diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7b12625c..d97b307d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4047,6 +4047,35 @@ class QuerySetTest(unittest.TestCase): plist = list(Person.objects.scalar('name', 'state')) self.assertEqual(plist, [(u'Wilson JR', s1)]) + def test_generic_reference_field_with_only_and_as_pymongo(self): + class TestPerson(Document): + name = StringField() + + class TestActivity(Document): + name = StringField() + owner = GenericReferenceField() + + TestPerson.drop_collection() + TestActivity.drop_collection() + + person = TestPerson(name='owner') + person.save() + + a1 = TestActivity(name='a1', owner=person) + a1.save() + + activity = TestActivity.objects(owner=person).scalar('id', 'owner').no_dereference().first() + self.assertEqual(activity[0], a1.pk) + self.assertEqual(activity[1]['_ref'], DBRef('test_person', person.pk)) + + activity = TestActivity.objects(owner=person).only('id', 'owner')[0] + self.assertEqual(activity.pk, a1.pk) + self.assertEqual(activity.owner, person) + + activity = TestActivity.objects(owner=person).only('id', 'owner').as_pymongo().first() + self.assertEqual(activity['_id'], a1.pk) + self.assertTrue(activity['owner']['_ref'], DBRef('test_person', person.pk)) + def test_scalar_db_field(self): class TestDoc(Document): @@ -4392,21 +4421,44 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(doc_objects, Doc.objects.from_json(json_data)) def test_as_pymongo(self): - from decimal import Decimal + class LastLogin(EmbeddedDocument): + location = StringField() + ip = StringField() + class User(Document): id = ObjectIdField('_id') name = StringField() age = IntField() price = DecimalField() + last_login = EmbeddedDocumentField(LastLogin) User.drop_collection() - User(name="Bob Dole", age=89, price=Decimal('1.11')).save() - User(name="Barack Obama", age=51, price=Decimal('2.22')).save() + + User.objects.create(name="Bob Dole", age=89, price=Decimal('1.11')) + User.objects.create( + name="Barack Obama", + age=51, + price=Decimal('2.22'), + last_login=LastLogin( + location='White House', + ip='104.107.108.116' + ) + ) + + results = User.objects.as_pymongo() + self.assertEqual( + set(results[0].keys()), + set(['_id', 'name', 'age', 'price']) + ) + self.assertEqual( + set(results[1].keys()), + set(['_id', 'name', 'age', 'price', 'last_login']) + ) results = User.objects.only('id', 'name').as_pymongo() - self.assertEqual(sorted(results[0].keys()), sorted(['_id', 'name'])) + self.assertEqual(set(results[0].keys()), set(['_id', 'name'])) users = User.objects.only('name', 'price').as_pymongo() results = list(users) @@ -4417,16 +4469,20 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(results[1]['name'], 'Barack Obama') self.assertEqual(results[1]['price'], 2.22) - # Test coerce_types - users = User.objects.only( - 'name', 'price').as_pymongo(coerce_types=True) + users = User.objects.only('name', 'last_login').as_pymongo() results = list(users) self.assertTrue(isinstance(results[0], dict)) self.assertTrue(isinstance(results[1], dict)) - self.assertEqual(results[0]['name'], 'Bob Dole') - self.assertEqual(results[0]['price'], Decimal('1.11')) - self.assertEqual(results[1]['name'], 'Barack Obama') - self.assertEqual(results[1]['price'], Decimal('2.22')) + self.assertEqual(results[0], { + 'name': 'Bob Dole' + }) + self.assertEqual(results[1], { + 'name': 'Barack Obama', + 'last_login': { + 'location': 'White House', + 'ip': '104.107.108.116' + } + }) def test_as_pymongo_json_limit_fields(self): @@ -4590,7 +4646,6 @@ class QuerySetTest(unittest.TestCase): def test_no_cache(self): """Ensure you can add meta data to file""" - class Noddy(Document): fields = DictField() @@ -4608,15 +4663,19 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(len(list(docs)), 100) + # Can't directly get a length of a no-cache queryset. with self.assertRaises(TypeError): len(docs) + # Another iteration over the queryset should result in another db op. with query_counter() as q: - self.assertEqual(q, 0) list(docs) self.assertEqual(q, 1) + + # ... and another one to double-check. + with query_counter() as q: list(docs) - self.assertEqual(q, 2) + self.assertEqual(q, 1) def test_nested_queryset_iterator(self): # Try iterating the same queryset twice, nested. From e52603b4a7ab540aaa74e3fafcbe432d1c15fb70 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 8 May 2017 00:12:26 -0400 Subject: [PATCH 083/268] ver bump to v0.14.0 + changelog/upgrade docs update --- docs/changelog.rst | 6 ++++++ docs/upgrade.rst | 12 ++++++++++++ mongoengine/__init__.py | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 46d47b97..b50b1b32 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,12 @@ Development =========== - (Fill this out as you fix issues and develop your features). +Changes in 0.14.0 +================= +- BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo`. +- POTENTIAL BREAKING CHANGE: Made EmbeddedDocument not hashable by default #1528 +- Improved code quality #1531, #1540, #1541, #1547 + Changes in 0.13.0 ================= - POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see diff --git a/docs/upgrade.rst b/docs/upgrade.rst index d612df88..a433db7d 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -6,6 +6,18 @@ Development *********** (Fill this out whenever you introduce breaking changes to MongoEngine) +0.14.0 +****** +This release includes a few bug fixes and another significant code cleanup. +The most important change is that `QuerySet.as_pymongo` no longer supports a +`coerce_types` mode. If you used it in the past, a) please let us know of your +use case, b) you'll need to override `as_pymongo` to get the desired outcome. + +This release also makes the EmbeddedDocument not hashable by default. If you +use embedded documents in sets or dictionaries, you might have to override +`__hash__` and implement a hashing logic specific to your use case. See #1528 +for the reason behind this change. + 0.13.0 ****** This release adds Unicode support to the `EmailField` and changes its diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 840c90d7..b41e87e7 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 13, 0) +VERSION = (0, 14, 0) def get_version(): From 0bc7aa52d8a3a8ec324f47b2daac79688578001e Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Mon, 8 May 2017 00:14:42 -0400 Subject: [PATCH 084/268] more docs tweaks [ci skip] --- docs/changelog.rst | 2 +- docs/upgrade.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b50b1b32..c60bbf09 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,7 @@ Development Changes in 0.14.0 ================= -- BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo`. +- BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo` #1549 - POTENTIAL BREAKING CHANGE: Made EmbeddedDocument not hashable by default #1528 - Improved code quality #1531, #1540, #1541, #1547 diff --git a/docs/upgrade.rst b/docs/upgrade.rst index a433db7d..65d13359 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -8,8 +8,8 @@ Development 0.14.0 ****** -This release includes a few bug fixes and another significant code cleanup. -The most important change is that `QuerySet.as_pymongo` no longer supports a +This release includes a few bug fixes and a significant code cleanup. The most +important change is that `QuerySet.as_pymongo` no longer supports a `coerce_types` mode. If you used it in the past, a) please let us know of your use case, b) you'll need to override `as_pymongo` to get the desired outcome. From 24d15d427482f39f7cfa0e9ac0a8b44393aa9ddf Mon Sep 17 00:00:00 2001 From: lanf0n Date: Thu, 11 May 2017 22:06:36 +0800 Subject: [PATCH 085/268] fix typo in the save() method's docstring (#1551) --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 4babd1b9..f1622934 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -320,7 +320,7 @@ class Document(BaseDocument): :param save_condition: only perform save if matching record in db satisfies condition(s) (e.g. version number). Raises :class:`OperationError` if the conditions are not satisfied - :parm signal_kwargs: (optional) kwargs dictionary to be passed to + :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. .. versionchanged:: 0.5 From 2f1fe5468edaa0922e67ce659e4cf75b2cf35df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Enrique=20Carrillo=20Pino?= Date: Fri, 12 May 2017 11:59:14 -0500 Subject: [PATCH 086/268] Fix empty string casted to datetime today in DateTimeField (#1533) --- mongoengine/fields.py | 4 ++++ tests/fields/fields.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index c34fe1b1..0d402712 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -483,6 +483,10 @@ class DateTimeField(BaseField): if not isinstance(value, six.string_types): return None + value = value.strip() + if not value: + return None + # Attempt to parse a datetime: if dateutil: try: diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 60c8370a..7a0ccc25 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -35,6 +35,28 @@ __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_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_values_nothing_set(self): """Ensure that default field values are used when creating a document. From b9e922c658f6275fc8446bf1ffdedf688f708f25 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 12 Jun 2017 04:50:13 +0000 Subject: [PATCH 087/268] support multiple operator #1510 --- AUTHORS | 1 + mongoengine/base/common.py | 7 ++++--- tests/queryset/queryset.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1d724718..88d4bbe1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -243,3 +243,4 @@ that much better: * Victor Varvaryuk * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) + * Erdenezul Batmunkh (https://github.com/erdenezul) diff --git a/mongoengine/base/common.py b/mongoengine/base/common.py index b9971ff7..f80471ef 100644 --- a/mongoengine/base/common.py +++ b/mongoengine/base/common.py @@ -3,9 +3,10 @@ from mongoengine.errors import NotRegistered __all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry') -UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push', - 'push_all', 'pull', 'pull_all', 'add_to_set', - 'set_on_insert', 'min', 'max', 'rename']) +UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'mul', + 'pop', 'push', 'push_all', 'pull', + 'pull_all', 'add_to_set', 'set_on_insert', + 'min', 'max', 'rename']) _document_registry = {} diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d97b307d..7666d030 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -589,6 +589,20 @@ class QuerySetTest(unittest.TestCase): Scores.objects(id=scores.id).update(max__high_score=500) self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000) + @needs_mongodb_v26 + def test_update_multiple(self): + class Product(Document): + item = StringField() + price = FloatField() + + product = Product.objects.create(item='ABC', price=10.99) + product = Product.objects.create(item='ABC', price=10.99) + Product.objects(id=product.id).update(mul__price=1.25) + self.assertEqual(Product.objects.get(id=product.id).price, 13.7375) + unknown_product = Product.objects.create(item='Unknown') + Product.objects(id=unknown_product.id).update(mul__price=100) + self.assertEqual(Product.objects.get(id=unknown_product.id).price, 0) + def test_updates_can_have_match_operators(self): class Comment(EmbeddedDocument): From 6903eed4e770bcb605df948728121ce75149d0fa Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Thu, 15 Jun 2017 06:08:40 +0000 Subject: [PATCH 088/268] support position in 'push' #1565 --- mongoengine/queryset/transform.py | 16 +++++++++++++--- tests/queryset/queryset.py | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index bb04ee37..2c1b7fdc 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -284,9 +284,11 @@ def update(_doc_cls=None, **update): if isinstance(field, GeoJsonBaseField): value = field.to_mongo(value) - if op in (None, 'set', 'push', 'pull'): + if op == 'push' and isinstance(value, (list, tuple, set)): + value = [field.prepare_query_value(op, v) for v in value] + elif op in (None, 'set', 'push', 'pull'): if field.required or value is not None: - value = field.prepare_query_value(op, value) + value = field.prepare_query_value(op, value) elif op in ('pushAll', 'pullAll'): value = [field.prepare_query_value(op, v) for v in value] elif op in ('addToSet', 'setOnInsert'): @@ -302,6 +304,10 @@ def update(_doc_cls=None, **update): value = {match: value} key = '.'.join(parts) + position = None + if parts[-1].isdigit() and isinstance(value, (list, tuple, set)): + key = parts[0] + position = int(parts[-1]) if not op: raise InvalidQueryError('Updates must supply an operation ' @@ -333,10 +339,14 @@ def update(_doc_cls=None, **update): value = {key: value} elif op == 'addToSet' and isinstance(value, list): value = {key: {'$each': value}} + elif op == 'push' and isinstance(value, list): + if position is not None: + value = {key: {'$each': value, '$position': position}} + else: + value = {key: {'$each': value}} else: value = {key: value} key = '$' + op - if key not in mongo_update: mongo_update[key] = value elif key in mongo_update and isinstance(mongo_update[key], dict): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d97b307d..9341a214 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1903,6 +1903,29 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + def test_update_push_with_position(self): + """Ensure that the 'push' update with position works properly. + """ + class BlogPost(Document): + slug = StringField() + tags = ListField(StringField()) + + BlogPost.drop_collection() + + post = BlogPost(slug="test") + post.save() + + BlogPost.objects.filter(id=post.id).update(push__tags="code") + BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"]) + post.reload() + self.assertEqual(post.tags[0], "mongodb") + self.assertEqual(post.tags[1], "python") + self.assertEqual(post.tags[2], "code") + + BlogPost.objects.filter(id=post.id).update(set__tags__2="java") + post.reload() + self.assertEqual(post.tags[2], "java") + def test_update_push_and_pull_add_to_set(self): """Ensure that the 'pull' update operation works correctly. """ From f63ad2dd69c40d87962fe05137e618c09418ed5e Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Thu, 15 Jun 2017 07:36:14 +0000 Subject: [PATCH 089/268] dont test in mongoDB v2.4 #1565 --- tests/queryset/queryset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 9341a214..7be3f8d7 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1903,6 +1903,7 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() + @needs_mongodb_v26 def test_update_push_with_position(self): """Ensure that the 'push' update with position works properly. """ From 1d4b1870cfb50e3b91b0fe187874c31d5373ea65 Mon Sep 17 00:00:00 2001 From: Danil Date: Mon, 19 Jun 2017 05:04:46 +0500 Subject: [PATCH 090/268] to_db_fields fix (#1553) --- mongoengine/queryset/base.py | 4 ++-- tests/queryset/field_list.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index f7c32d20..41a10ace 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1731,14 +1731,14 @@ class BaseQuerySet(object): for x in document._subclasses][1:] for field in fields: try: - field = '.'.join(f.db_field for f in + field = '.'.join(f if isinstance(f, six.string_types) else f.db_field for f in document._lookup_field(field.split('.'))) ret.append(field) except LookUpError as err: found = False for subdoc in subclasses: try: - subfield = '.'.join(f.db_field for f in + subfield = '.'.join(f if isinstance(f, six.string_types) else f.db_field for f in subdoc._lookup_field(field.split('.'))) ret.append(subfield) found = True diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index d1277e06..c07cec3e 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -197,14 +197,18 @@ class OnlyExcludeAllTest(unittest.TestCase): title = StringField() text = StringField() + class VariousData(EmbeddedDocument): + some = BooleanField() + class BlogPost(Document): content = StringField() author = EmbeddedDocumentField(User) comments = ListField(EmbeddedDocumentField(Comment)) + various = MapField(field=EmbeddedDocumentField(VariousData)) BlogPost.drop_collection() - post = BlogPost(content='Had a good coffee today...') + post = BlogPost(content='Had a good coffee today...', various={'test_dynamic':{'some': True}}) post.author = User(name='Test User') post.comments = [Comment(title='I aggree', text='Great post!'), Comment(title='Coffee', text='I hate coffee')] post.save() @@ -215,6 +219,9 @@ class OnlyExcludeAllTest(unittest.TestCase): self.assertEqual(obj.author.name, 'Test User') self.assertEqual(obj.comments, []) + obj = BlogPost.objects.only('various.test_dynamic.some').get() + self.assertEqual(obj.various["test_dynamic"].some, True) + obj = BlogPost.objects.only('content', 'comments.title',).get() self.assertEqual(obj.content, 'Had a good coffee today...') self.assertEqual(obj.author, None) From a8d6e59a7a0434edebcb52ca63e40be5466548fa Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 18 Jun 2017 17:25:39 -0700 Subject: [PATCH 091/268] minor tweaks to code quality in _fields_to_dbfields --- mongoengine/queryset/base.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 41a10ace..6f9c372c 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1722,25 +1722,33 @@ class BaseQuerySet(object): return frequencies def _fields_to_dbfields(self, fields): - """Translate fields paths to its db equivalents""" - ret = [] + """Translate fields' paths to their db equivalents.""" subclasses = [] - document = self._document - if document._meta['allow_inheritance']: + if self._document._meta['allow_inheritance']: subclasses = [get_document(x) - for x in document._subclasses][1:] + for x in self._document._subclasses][1:] + + db_field_paths = [] for field in fields: + field_parts = field.split('.') try: - field = '.'.join(f if isinstance(f, six.string_types) else f.db_field for f in - document._lookup_field(field.split('.'))) - ret.append(field) + field = '.'.join( + f if isinstance(f, six.string_types) else f.db_field + for f in self._document._lookup_field(field_parts) + ) + db_field_paths.append(field) except LookUpError as err: found = False + + # If a field path wasn't found on the main document, go + # through its subclasses and see if it exists on any of them. for subdoc in subclasses: try: - subfield = '.'.join(f if isinstance(f, six.string_types) else f.db_field for f in - subdoc._lookup_field(field.split('.'))) - ret.append(subfield) + subfield = '.'.join( + f if isinstance(f, six.string_types) else f.db_field + for f in subdoc._lookup_field(field_parts) + ) + db_field_paths.append(subfield) found = True break except LookUpError: @@ -1748,7 +1756,8 @@ class BaseQuerySet(object): if not found: raise err - return ret + + return db_field_paths def _get_order_by(self, keys): """Given a list of MongoEngine-style sort keys, return a list From f3ee4a5dac9a6bcccdd11f9be3afc702e344e8a2 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 19 Jun 2017 02:59:17 +0000 Subject: [PATCH 092/268] add tests for push operator #1565 --- AUTHORS | 1 + tests/document/instance.py | 41 ++++++++++++++++++++++++++++++++++++++ tests/queryset/modify.py | 25 ++++++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 1d724718..88d4bbe1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -243,3 +243,4 @@ that much better: * Victor Varvaryuk * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) + * Erdenezul Batmunkh (https://github.com/erdenezul) diff --git a/tests/document/instance.py b/tests/document/instance.py index 37bbe337..1456f0a1 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -22,6 +22,8 @@ from mongoengine.queryset import NULLIFY, Q from mongoengine.context_managers import switch_db, query_counter from mongoengine import signals +from tests.utils import needs_mongodb_v26 + TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), '../fields/mongoengine.png') @@ -775,6 +777,7 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(doc.to_mongo())]) + def test_modify_invalid_query(self): doc1 = self.Person(name="bob", age=10).save() doc2 = self.Person(name="jim", age=20).save() @@ -826,6 +829,27 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) + @needs_mongodb_v26 + def test_modity_push_position(self): + class BlogPost(Document): + slug = StringField() + tags = ListField(StringField()) + + other_blog = BlogPost(slug="ABC", tags=["code", "java", "python"]).save() + + blog = BlogPost(slug="ABC", tags=["python"]).save() + blog_copy = blog._from_son(blog.to_mongo()) + + assert blog.modify(push__tags__0=["code", "java"]) + blog_copy.tags = ["code", "java", "python"] + assert blog.to_json() == blog_copy.to_json() + assert blog._get_changed_fields() == [] + + docs = [dict(other_blog.to_mongo()), dict(blog.to_mongo())] + self.assertEqual( + list(BlogPost._get_collection().find().sort("id")), + sorted(docs, key=lambda doc: doc["_id"])) + def test_save(self): """Ensure that a document may be saved in the database.""" @@ -3149,6 +3173,23 @@ class InstanceTest(unittest.TestCase): person.update(set__height=2.0) + @needs_mongodb_v26 + def test_push_with_position(self): + """Ensure that push with position works properly for an instance.""" + class BlogPost(Document): + slug = StringField() + tags = ListField(StringField()) + + blog = BlogPost() + blog.slug = "ABC" + blog.tags = ["python"] + blog.save() + + blog.update(push__tags__0=["mongodb", "code"]) + blog.reload() + self.assertEqual(blog.tags[0], "mongodb") + self.assertEqual(blog.tags[2], "python") + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py index 607937f6..4bda9718 100644 --- a/tests/queryset/modify.py +++ b/tests/queryset/modify.py @@ -1,6 +1,6 @@ import unittest -from mongoengine import connect, Document, IntField +from mongoengine import connect, Document, IntField, StringField, ListField __all__ = ("FindAndModifyTest",) @@ -94,6 +94,29 @@ class FindAndModifyTest(unittest.TestCase): self.assertEqual(old_doc.to_mongo(), {"_id": 1}) self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + def test_modify_with_push(self): + class BlogPost(Document): + id = StringField(primary_key=True) + tags = ListField(StringField()) + + BlogPost.drop_collection() + + BlogPost(id="ABC").save() + BlogPost(id="BCD").save() + blog = BlogPost.objects(id="ABC").modify(push__tags="code") + + self.assertEqual(blog.to_mongo(), {"_id": "ABC", "tags": []}) + docs = [{"_id": "ABC", "tags":["code"]}, {"_id": "BCD", "tags":[]}] + self.assertEqual(list(BlogPost._collection.find().sort("id")), docs) + + another_blog = BlogPost.objects(id="BCD").modify(push__tags="java") + self.assertEqual(another_blog.to_mongo(), {"_id": "BCD", "tags": []}) + another_blog = BlogPost.objects(id="BCD").modify(push__tags__0=["python"]) + self.assertEqual(another_blog.to_mongo(), {"_id": "BCD", "tags": ["java"]}) + docs = [{"_id": "ABC", "tags":["code"]}, + {"_id": "BCD", "tags":["python", "java"]}] + self.assertEqual(list(BlogPost._collection.find().sort("id")), docs) + if __name__ == '__main__': unittest.main() From 7782aa7379306c8223edd33aeec6b47f2f145b83 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 19 Jun 2017 03:11:59 +0000 Subject: [PATCH 093/268] do not test position push in mongodb_v2.4 #1565 --- tests/document/instance.py | 1 - tests/queryset/modify.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/document/instance.py b/tests/document/instance.py index 1456f0a1..26ab0d97 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -777,7 +777,6 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(doc.to_mongo())]) - def test_modify_invalid_query(self): doc1 = self.Person(name="bob", age=10).save() doc2 = self.Person(name="jim", age=20).save() diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py index 4bda9718..44bdc3ff 100644 --- a/tests/queryset/modify.py +++ b/tests/queryset/modify.py @@ -2,6 +2,8 @@ import unittest from mongoengine import connect, Document, IntField, StringField, ListField +from tests.utils import needs_mongodb_v26 + __all__ = ("FindAndModifyTest",) @@ -94,6 +96,7 @@ class FindAndModifyTest(unittest.TestCase): self.assertEqual(old_doc.to_mongo(), {"_id": 1}) self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + @needs_mongodb_v26 def test_modify_with_push(self): class BlogPost(Document): id = StringField(primary_key=True) From fb00b79d1945a05c08f642aa35efbba5e92558a8 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 19 Jun 2017 03:28:34 +0000 Subject: [PATCH 094/268] add docs for positional push operator #1565 --- docs/guide/querying.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst index 0bb19658..f1594dd2 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -565,6 +565,15 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`:: >>> post.tags ['database', 'mongodb'] +From MongoDB version 2.6, push operator supports $position value which allows +to push values with index. + >>> post = BlogPost(title="Test", tags=["mongo"]) + >>> post.save() + >>> post.update(push__tags__0=["database", "code"]) + >>> post.reload() + >>> post.tags + ['database', 'code', 'mongo'] + .. note:: Currently only top level lists are handled, future versions of mongodb / pymongo plan to support nested positional operators. See `The $ positional From 71c3c632d7efb673aad2f1e79e564537df202e7b Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 19 Jun 2017 06:00:21 +0000 Subject: [PATCH 095/268] add test case for reverse_delete_rule with pull #1519 --- tests/document/instance.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/document/instance.py b/tests/document/instance.py index 37bbe337..f9f4285e 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1866,6 +1866,25 @@ class InstanceTest(unittest.TestCase): author.delete() self.assertEqual(BlogPost.objects.count(), 0) + def test_reverse_delete_rule_pull(self): + """Ensure that a referenced document is also deleted with + pull. + """ + class Record(Document): + name = StringField() + children = ListField(ReferenceField('self', reverse_delete_rule=PULL)) + + Record.drop_collection() + + parent_record = Record(name='parent').save() + child_record = Record(name='child').save() + parent_record.children.append(child_record) + parent_record.save() + + 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. From 0bc6507df34710de0a8fb16b0b45f48d8d6aec5b Mon Sep 17 00:00:00 2001 From: Benoit Larroque Date: Tue, 27 Jun 2017 00:43:41 +0200 Subject: [PATCH 096/268] Make queryset aggregates obey read_preference --- mongoengine/queryset/base.py | 4 ++++ tests/queryset/queryset.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6f9c372c..090ed906 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1182,6 +1182,10 @@ class BaseQuerySet(object): pipeline = initial_pipeline + list(pipeline) + if IS_PYMONGO_3 and self._read_preference is not None: + return self._collection.with_options(read_preference=self._read_preference) \ + .aggregate(pipeline, cursor={}, **kwargs) + return self._collection.aggregate(pipeline, cursor={}, **kwargs) # JS functionality diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d97b307d..e0eb7d38 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4353,6 +4353,26 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(bars._cursor._Cursor__read_preference, ReadPreference.SECONDARY_PREFERRED) + @needs_mongodb_v26 + def test_read_preference_aggregation_framework(self): + class Bar(Document): + txt = StringField() + + meta = { + 'indexes': ['txt'] + } + # Aggregates with read_preference + bars = Bar.objects \ + .read_preference(ReadPreference.SECONDARY_PREFERRED) \ + .aggregate() + if IS_PYMONGO_3: + self.assertEqual(bars._CommandCursor__collection.read_preference, + ReadPreference.SECONDARY_PREFERRED) + else: + self.assertNotEqual(bars._CommandCursor__collection.read_preference, + ReadPreference.SECONDARY_PREFERRED) + + def test_json_simple(self): class Embedded(EmbeddedDocument): From e6a30f899c9bb6cdb28e2302967e6dc96ceac055 Mon Sep 17 00:00:00 2001 From: "Bo.Yi" Date: Thu, 6 Jul 2017 14:57:03 +0800 Subject: [PATCH 097/268] [fix]validation list field with multi choice values --- mongoengine/base/document.py | 6 +++++- mongoengine/base/fields.py | 6 ++++-- tests/fields/fields.py | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 99c8af87..43158133 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1080,5 +1080,9 @@ class BaseDocument(object): """Return the display value for a choice field""" value = getattr(self, field.name) if field.choices and isinstance(field.choices[0], (list, tuple)): - return dict(field.choices).get(value, value) + sep = getattr(field, 'display_sep', u' ') + values = value if field.__name__ == 'ListField' else [value] + return sep.join([ + dict(field.choices).get(val, val) + for val in values]) return value diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index e2b5d321..69034d5d 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -213,8 +213,10 @@ class BaseField(object): ) ) # Choices which are types other than Documents - elif value not in choice_list: - self.error('Value must be one of %s' % six.text_type(choice_list)) + else: + values = value if isinstance(value, (list, tuple)) else [value] + if len(set(values) - set(choice_list)): + self.error('Value must be one of %s' % six.text_type(choice_list)) def _validate(self, value, **kwargs): # Check the Choices Constraint diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7a0ccc25..b6628ab0 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -920,6 +920,12 @@ class FieldTest(MongoDBTestCase): def test_list_validation(self): """Ensure that a list field only accepts lists with valid elements.""" + AccessLevelChoices = ( + ('a', u'Administrator'), + ('b', u'Manager'), + ('c', u'Staff'), + ) + class User(Document): pass @@ -932,6 +938,7 @@ class FieldTest(MongoDBTestCase): tags = ListField(StringField()) authors = ListField(ReferenceField(User)) generic = ListField(GenericReferenceField()) + access_list = ListField(required=False, default=[], choices=AccessLevelChoices, display_sep=u',') User.drop_collection() BlogPost.drop_collection() @@ -949,6 +956,17 @@ class FieldTest(MongoDBTestCase): post.tags = ('fun', 'leisure') post.validate() + post.access_list = 'a,b' + self.assertRaises(ValidationError, post.validate()) + + post.access_list = ['c', 'd'] + self.assertRaises(ValidationError, post.validate()) + + post.access_list = ['a', 'b'] + post.validate() + + self.assertEqual(post.get_access_list_display(), u'Administrator,Manager') + post.comments = ['a'] self.assertRaises(ValidationError, post.validate) post.comments = 'yay' From 820b5cbb86a0cc9dff65db35b0f806fb811a512b Mon Sep 17 00:00:00 2001 From: "Bo.Yi" Date: Thu, 6 Jul 2017 16:07:51 +0800 Subject: [PATCH 098/268] [fix]pass test case and fix field type error --- mongoengine/base/document.py | 6 ++++-- tests/fields/fields.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 43158133..42a8bb4b 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1080,9 +1080,11 @@ class BaseDocument(object): """Return the display value for a choice field""" value = getattr(self, field.name) if field.choices and isinstance(field.choices[0], (list, tuple)): + if value is None: + return None sep = getattr(field, 'display_sep', u' ') - values = value if field.__name__ == 'ListField' else [value] + values = value if field.__class__.__name__ == 'ListField' else [value] return sep.join([ dict(field.choices).get(val, val) - for val in values]) + for val in values or []]) return value diff --git a/tests/fields/fields.py b/tests/fields/fields.py index b6628ab0..336e116c 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -957,10 +957,10 @@ class FieldTest(MongoDBTestCase): post.validate() post.access_list = 'a,b' - self.assertRaises(ValidationError, post.validate()) + self.assertRaises(ValidationError, post.validate) post.access_list = ['c', 'd'] - self.assertRaises(ValidationError, post.validate()) + self.assertRaises(ValidationError, post.validate) post.access_list = ['a', 'b'] post.validate() From 437b11af9ac3f2a160868265337a22eb6fcb6ff5 Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Mon, 10 Jul 2017 16:43:24 -0400 Subject: [PATCH 099/268] docs: use explicit register_delete_rule example The previous example of creating bi-directional delete rules was vague since the example defined only one class and the relationship between "Foo" and "Bar" wasn't clear. I added a more explicit example where the relationship between the two classes is explicit. --- mongoengine/fields.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0d402712..0029d68b 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -971,11 +971,13 @@ class ReferenceField(BaseField): .. code-block:: python - class Bar(Document): - content = StringField() - foo = ReferenceField('Foo') + class Org(Document): + owner = ReferenceField('User') - Foo.register_delete_rule(Bar, 'foo', NULLIFY) + class User(Document): + org = ReferenceField('Org', reverse_delete_rule=CASCADE) + + User.register_delete_rule(Org, 'owner', DENY) .. versionchanged:: 0.5 added `reverse_delete_rule` """ From a7cab513695ba1814d9316719429d63249fb97dc Mon Sep 17 00:00:00 2001 From: Davidrjx <1058960881@qq.com> Date: Thu, 13 Jul 2017 18:07:36 +0800 Subject: [PATCH 100/268] Use a set literal in _clean_settings (#1585) --- AUTHORS | 1 + mongoengine/connection.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index 1d724718..96a7850e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -243,3 +243,4 @@ that much better: * Victor Varvaryuk * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) + * Renjianxin (https://github.com/Davidrjx) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 7eae810f..34ff4dc3 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -146,13 +146,14 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): raise MongoEngineConnectionError(msg) def _clean_settings(settings_dict): - irrelevant_fields = set([ - 'name', 'username', 'password', 'authentication_source', - 'authentication_mechanism' - ]) + # set literal more efficient than calling set function + irrelevant_fields_set = { + 'name', 'username', 'password', + 'authentication_source', 'authentication_mechanism' + } return { k: v for k, v in settings_dict.items() - if k not in irrelevant_fields + if k not in irrelevant_fields_set } # Retrieve a copy of the connection settings associated with the requested From 3dcc9bc1433e550a3151ade5f31efef7e39f2be3 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Thu, 13 Jul 2017 22:59:21 +0800 Subject: [PATCH 101/268] use explicit tests and fix unneccessary indent #1565 --- mongoengine/queryset/transform.py | 2 +- tests/document/instance.py | 26 ++++++++++---------------- tests/queryset/queryset.py | 6 ++---- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 2c1b7fdc..b9c8b130 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -288,7 +288,7 @@ def update(_doc_cls=None, **update): value = [field.prepare_query_value(op, v) for v in value] elif op in (None, 'set', 'push', 'pull'): if field.required or value is not None: - value = field.prepare_query_value(op, value) + value = field.prepare_query_value(op, value) elif op in ('pushAll', 'pullAll'): value = [field.prepare_query_value(op, v) for v in value] elif op in ('addToSet', 'setOnInsert'): diff --git a/tests/document/instance.py b/tests/document/instance.py index 26ab0d97..609bc900 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -829,25 +829,20 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) @needs_mongodb_v26 - def test_modity_push_position(self): + def test_modify_with_positional_push(self): class BlogPost(Document): - slug = StringField() tags = ListField(StringField()) - other_blog = BlogPost(slug="ABC", tags=["code", "java", "python"]).save() + post = BlogPost.objects.create(tags=['python']) + self.assertEqual(post.tags, ['python']) + post.modify(push__tags__0=['code', 'mongo']) + self.assertEqual(post.tags, ['code', 'mongo', 'python']) - blog = BlogPost(slug="ABC", tags=["python"]).save() - blog_copy = blog._from_son(blog.to_mongo()) - - assert blog.modify(push__tags__0=["code", "java"]) - blog_copy.tags = ["code", "java", "python"] - assert blog.to_json() == blog_copy.to_json() - assert blog._get_changed_fields() == [] - - docs = [dict(other_blog.to_mongo()), dict(blog.to_mongo())] + # Assert same order of the list items is maintained in the db self.assertEqual( - list(BlogPost._get_collection().find().sort("id")), - sorted(docs, key=lambda doc: doc["_id"])) + BlogPost._get_collection().find_one({'_id': post.pk})['tags'], + ['code', 'mongo', 'python'] + ) def test_save(self): """Ensure that a document may be saved in the database.""" @@ -3186,8 +3181,7 @@ class InstanceTest(unittest.TestCase): blog.update(push__tags__0=["mongodb", "code"]) blog.reload() - self.assertEqual(blog.tags[0], "mongodb") - self.assertEqual(blog.tags[2], "python") + self.assertEqual(blog.tags, ['mongodb', 'code', 'python']) if __name__ == '__main__': diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 7be3f8d7..f4e4670d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1919,13 +1919,11 @@ class QuerySetTest(unittest.TestCase): BlogPost.objects.filter(id=post.id).update(push__tags="code") BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"]) post.reload() - self.assertEqual(post.tags[0], "mongodb") - self.assertEqual(post.tags[1], "python") - self.assertEqual(post.tags[2], "code") + self.assertEqual(post.tags, ['mongodb', 'python', 'code']) BlogPost.objects.filter(id=post.id).update(set__tags__2="java") post.reload() - self.assertEqual(post.tags[2], "java") + self.assertEqual(post.tags, ['mongodb', 'python', 'java']) def test_update_push_and_pull_add_to_set(self): """Ensure that the 'pull' update operation works correctly. From 9f02f71c52f5f9c02cbd3db797ae80aea36d0639 Mon Sep 17 00:00:00 2001 From: "Bo.Yi" Date: Sun, 16 Jul 2017 18:47:20 +0800 Subject: [PATCH 102/268] [fix]fix some personal hobby --- mongoengine/base/document.py | 4 ++-- tests/fields/fields.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 42a8bb4b..bb8d3a16 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1082,8 +1082,8 @@ class BaseDocument(object): if field.choices and isinstance(field.choices[0], (list, tuple)): if value is None: return None - sep = getattr(field, 'display_sep', u' ') - values = value if field.__class__.__name__ == 'ListField' else [value] + sep = getattr(field, 'display_sep', ' ') + values = value if field.__class__.__name__ in ('ListField', 'SortedListField') else [value] return sep.join([ dict(field.choices).get(val, val) for val in values or []]) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 336e116c..1dc25d12 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -921,7 +921,7 @@ class FieldTest(MongoDBTestCase): def test_list_validation(self): """Ensure that a list field only accepts lists with valid elements.""" AccessLevelChoices = ( - ('a', u'Administrator'), + ('a', u'Administration'), ('b', u'Manager'), ('c', u'Staff'), ) @@ -938,7 +938,7 @@ class FieldTest(MongoDBTestCase): tags = ListField(StringField()) authors = ListField(ReferenceField(User)) generic = ListField(GenericReferenceField()) - access_list = ListField(required=False, default=[], choices=AccessLevelChoices, display_sep=u',') + access_list = ListField(choices=AccessLevelChoices, display_sep=', ') User.drop_collection() BlogPost.drop_collection() @@ -965,7 +965,7 @@ class FieldTest(MongoDBTestCase): post.access_list = ['a', 'b'] post.validate() - self.assertEqual(post.get_access_list_display(), u'Administrator,Manager') + self.assertEqual(post.get_access_list_display(), u'Administration, Manager') post.comments = ['a'] self.assertRaises(ValidationError, post.validate) From 433f10ef93cbfa2d2f0a8cb704e8333704ebf362 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 31 Jul 2017 05:15:23 +0000 Subject: [PATCH 103/268] position support singular value #1565 --- mongoengine/queryset/transform.py | 17 ++++++++++------- tests/queryset/queryset.py | 5 +++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index b9c8b130..b01d3d41 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -304,10 +304,6 @@ def update(_doc_cls=None, **update): value = {match: value} key = '.'.join(parts) - position = None - if parts[-1].isdigit() and isinstance(value, (list, tuple, set)): - key = parts[0] - position = int(parts[-1]) if not op: raise InvalidQueryError('Updates must supply an operation ' @@ -339,11 +335,18 @@ def update(_doc_cls=None, **update): value = {key: value} elif op == 'addToSet' and isinstance(value, list): value = {key: {'$each': value}} - elif op == 'push' and isinstance(value, list): - if position is not None: + elif op == 'push': + if parts[-1].isdigit() and op == 'push': + key = parts[0] + position = int(parts[-1]) + # position modifier must appear with each. + if not isinstance(value, (set, tuple, list)): + value = [value] value = {key: {'$each': value, '$position': position}} - else: + elif isinstance(value, list): value = {key: {'$each': value}} + else: + value = {key: value} else: value = {key: value} key = '$' + op diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index f4e4670d..fe97b765 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1925,6 +1925,11 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertEqual(post.tags, ['mongodb', 'python', 'java']) + #test push with singular value + BlogPost.objects.filter(id=post.id).update(push__tags__0='scala') + post.reload() + self.assertEqual(post.tags, ['scala', 'mongodb', 'python', 'java']) + def test_update_push_and_pull_add_to_set(self): """Ensure that the 'pull' update operation works correctly. """ From 34fca9d6f5cb8ea09af7dc63a480354c8ecbc06d Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 31 Jul 2017 18:32:34 +0800 Subject: [PATCH 104/268] Add clear comment and tests for positional push #1565 --- mongoengine/queryset/transform.py | 5 +++-- tests/queryset/modify.py | 33 +++++++++++++++++++------------ tests/queryset/queryset.py | 3 +-- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index b01d3d41..a9907ada 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -336,10 +336,11 @@ def update(_doc_cls=None, **update): elif op == 'addToSet' and isinstance(value, list): value = {key: {'$each': value}} elif op == 'push': - if parts[-1].isdigit() and op == 'push': + if parts[-1].isdigit(): key = parts[0] position = int(parts[-1]) - # position modifier must appear with each. + # $position expects an iterable. If pushing a single value, + # wrap it in a list. if not isinstance(value, (set, tuple, list)): value = [value] value = {key: {'$each': value, '$position': position}} diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py index 44bdc3ff..fe410d19 100644 --- a/tests/queryset/modify.py +++ b/tests/queryset/modify.py @@ -99,26 +99,33 @@ class FindAndModifyTest(unittest.TestCase): @needs_mongodb_v26 def test_modify_with_push(self): class BlogPost(Document): - id = StringField(primary_key=True) tags = ListField(StringField()) BlogPost.drop_collection() BlogPost(id="ABC").save() - BlogPost(id="BCD").save() - blog = BlogPost.objects(id="ABC").modify(push__tags="code") - self.assertEqual(blog.to_mongo(), {"_id": "ABC", "tags": []}) - docs = [{"_id": "ABC", "tags":["code"]}, {"_id": "BCD", "tags":[]}] - self.assertEqual(list(BlogPost._collection.find().sort("id")), docs) + # Push a new tag via modify with new=False (default). + blog = BlogPost(pk='ABC').modify(push__tags='code') + self.assertEqual(blog.tags, []) + blog.reload() + self.assertEqual(blog.tags, ['code']) - another_blog = BlogPost.objects(id="BCD").modify(push__tags="java") - self.assertEqual(another_blog.to_mongo(), {"_id": "BCD", "tags": []}) - another_blog = BlogPost.objects(id="BCD").modify(push__tags__0=["python"]) - self.assertEqual(another_blog.to_mongo(), {"_id": "BCD", "tags": ["java"]}) - docs = [{"_id": "ABC", "tags":["code"]}, - {"_id": "BCD", "tags":["python", "java"]}] - self.assertEqual(list(BlogPost._collection.find().sort("id")), docs) + # Push a new tag via modify with new=True. + blog = BlogPost.objects(pk='ABC').modify(push__tags='java', new=True) + self.assertEqual(blog.tags, ['code', 'java']) + + # Push a new tag with a positional argument. + blog = BlogPost.objects(pk='ABC').modify( + push__tags__0='python', + new=True) + self.assertEqual(blog.tags, ['python', 'code', 'java']) + + # Push multiple new tags with a positional argument. + blog = BlogPost.objects(pk='ABC').modify( + push__tags__1=['go', 'rust'], + new=True) + self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java']) if __name__ == '__main__': diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index fe97b765..c78ed985 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1913,8 +1913,7 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() - post = BlogPost(slug="test") - post.save() + post = BlogPost.objects.create(slug="test") BlogPost.objects.filter(id=post.id).update(push__tags="code") BlogPost.objects.filter(id=post.id).update(push__tags__0=["mongodb", "python"]) From f09256a24e83623b194792234f6a9e4c6944e2a6 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 31 Jul 2017 18:49:52 +0800 Subject: [PATCH 105/268] Fix modify tests #1565 --- tests/queryset/modify.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py index fe410d19..b37f9b73 100644 --- a/tests/queryset/modify.py +++ b/tests/queryset/modify.py @@ -103,26 +103,26 @@ class FindAndModifyTest(unittest.TestCase): BlogPost.drop_collection() - BlogPost(id="ABC").save() + blog = BlogPost.objects.create() # Push a new tag via modify with new=False (default). - blog = BlogPost(pk='ABC').modify(push__tags='code') + BlogPost(id=blog.id).modify(push__tags='code') self.assertEqual(blog.tags, []) blog.reload() self.assertEqual(blog.tags, ['code']) # Push a new tag via modify with new=True. - blog = BlogPost.objects(pk='ABC').modify(push__tags='java', new=True) + blog = BlogPost.objects(id=blog.id).modify(push__tags='java', new=True) self.assertEqual(blog.tags, ['code', 'java']) # Push a new tag with a positional argument. - blog = BlogPost.objects(pk='ABC').modify( + blog = BlogPost.objects(id=blog.id).modify( push__tags__0='python', new=True) self.assertEqual(blog.tags, ['python', 'code', 'java']) # Push multiple new tags with a positional argument. - blog = BlogPost.objects(pk='ABC').modify( + blog = BlogPost.objects(id=blog.id).modify( push__tags__1=['go', 'rust'], new=True) self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java']) From 2a795e91381053294cc9bf312f785b5ed9f1ab49 Mon Sep 17 00:00:00 2001 From: Ali Date: Fri, 4 Aug 2017 11:31:29 +0100 Subject: [PATCH 106/268] QuerySet limit function now returns all docs in cursor when 0 is passed --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 7 ++++--- tests/queryset/queryset.py | 5 +++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c60bbf09..aa8584a7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Development =========== +- QuerySet limit function behaviour: Passing 0 as parameter will return all the documents in the cursor #1611 - (Fill this out as you fix issues and develop your features). Changes in 0.14.0 diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6f9c372c..be4b9d66 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -384,7 +384,7 @@ class BaseQuerySet(object): :meth:`skip` that has been applied to this cursor into account when getting the count """ - if self._limit == 0 and with_limit_and_skip or self._none: + if self._limit == 0 and with_limit_and_skip is False or self._none: return 0 return self._cursor.count(with_limit_and_skip=with_limit_and_skip) @@ -759,10 +759,11 @@ class BaseQuerySet(object): """Limit the number of returned documents to `n`. This may also be achieved using array-slicing syntax (e.g. ``User.objects[:5]``). - :param n: the maximum number of objects to return + :param n: the maximum number of objects to return if n is greater than 0. + When 0 is passed, returns all the documents in the cursor """ queryset = self.clone() - queryset._limit = n if n != 0 else 1 + queryset._limit = n # If a cursor object has already been created, apply the limit to it. if queryset._cursor_obj: diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c78ed985..ac545629 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -124,6 +124,11 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(len(people2), 1) self.assertEqual(people2[0], user_a) + # Test limit with 0 as parameter + people = self.Person.objects.limit(0) + self.assertEqual(people.count(with_limit_and_skip=True), 2) + self.assertEqual(len(people), 2) + # Test chaining of only after limit person = self.Person.objects().limit(1).only('name').first() self.assertEqual(person, user_a) From ee5686e91ab9dec45c0d98d22d20c17864f02e0f Mon Sep 17 00:00:00 2001 From: Ali Turki Date: Mon, 7 Aug 2017 11:12:15 +0100 Subject: [PATCH 107/268] test case for #1602 --- tests/queryset/queryset.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index ac545629..57126ac0 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -5133,6 +5133,16 @@ class QuerySetTest(unittest.TestCase): # in a way we'd expect) should raise a TypeError, too self.assertRaises(TypeError, BlogPost.objects(authors__in=author).count) + def test_create_count(self): + self.Person.drop_collection() + self.Person.objects.create(name="Foo") + self.Person.objects.create(name="Bar") + self.Person.objects.create(name="Baz") + self.assertEqual(self.Person.objects.count(with_limit_and_skip=True), 3) + + newPerson = self.Person.objects.create(name="Foo_1") + self.assertEqual(self.Person.objects.count(with_limit_and_skip=True), 4) + if __name__ == '__main__': unittest.main() From c4de879b207e4e7087b099459ff801b8897620f1 Mon Sep 17 00:00:00 2001 From: Paulo Matos Date: Fri, 11 Aug 2017 09:09:33 +0200 Subject: [PATCH 108/268] Clarify comment in validation example --- docs/guide/document-instances.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst index 0e9fcef6..64f17c08 100644 --- a/docs/guide/document-instances.rst +++ b/docs/guide/document-instances.rst @@ -57,7 +57,8 @@ document values for example:: def clean(self): """Ensures that only published essays have a `pub_date` and - automatically sets the pub_date if published and not set""" + automatically sets `pub_date` if essay is published and `pub_date` + is not set""" if self.status == 'Draft' and self.pub_date is not None: msg = 'Draft entries should not have a publication date.' raise ValidationError(msg) From b9f3991d03fcd78ad5b81011d8420ab0287b29bd Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Fri, 18 Aug 2017 09:31:26 +0800 Subject: [PATCH 109/268] added as_pymongo test for PointField #1311 --- tests/queryset/geo.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 51a32382..38c0377e 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -510,6 +510,24 @@ class GeoQueriesTest(MongoDBTestCase): roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count() self.assertEqual(1, roads) + def test_aspymongo_with_only(self): + """Ensure as_pymongo works with only""" + class Place(Document): + location = PointField() + + Place.drop_collection() + p = Place(location=[24.946861267089844, 60.16311983618494]) + p.save() + qs = Place.objects().only('location') + self.assertDictEqual( + qs.as_pymongo()[0]['location'], + {u'type': u'Point', + u'coordinates': [ + 24.946861267089844, + 60.16311983618494] + } + ) + def test_2dsphere_point_sets_correctly(self): class Location(Document): loc = PointField() From 21d1faa7938e4585fa12325037f0695e3572ee80 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Thu, 24 Aug 2017 18:03:08 +0200 Subject: [PATCH 110/268] Fix .install_mongodb_on_travis.sh script (all credit to @erdenezul) --- .install_mongodb_on_travis.sh | 6 +++++- .travis.yml | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.install_mongodb_on_travis.sh b/.install_mongodb_on_travis.sh index 8563ae74..f2018411 100644 --- a/.install_mongodb_on_travis.sh +++ b/.install_mongodb_on_travis.sh @@ -1,5 +1,6 @@ #!/bin/bash +sudo apt-get remove mongodb-org-server sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 if [ "$MONGODB" = "2.4" ]; then @@ -13,7 +14,7 @@ elif [ "$MONGODB" = "2.6" ]; then sudo apt-get install mongodb-org-server=2.6.12 # service should be started automatically elif [ "$MONGODB" = "3.0" ]; then - echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list + echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list sudo apt-get update sudo apt-get install mongodb-org-server=3.0.14 # service should be started automatically @@ -21,3 +22,6 @@ else echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0." exit 1 fi; + +mkdir db +1>db/logs mongod --dbpath=db & diff --git a/.travis.yml b/.travis.yml index 8f846ab1..5e90660d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,8 @@ matrix: before_install: - bash .install_mongodb_on_travis.sh +- sleep 15 # https://docs.travis-ci.com/user/database-setup/#MongoDB-does-not-immediately-accept-connections +- mongo --eval 'db.version();' install: - sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev From 4f59c7f77fdda587386ec3a03b4de9caf1b0dadf Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 25 Jul 2017 16:26:07 +0200 Subject: [PATCH 111/268] Expose to user mongoengine.base.NON_FIELD_ERRORS This variable is used to set the field containing the errors raised in a clean function. Given those function are user-defined, users should be able to get the name of the field to easily retreive their custom errors. --- mongoengine/base/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 99c8af87..d05c7695 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -19,7 +19,7 @@ from mongoengine.common import _import_class from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, LookUpError, OperationError, ValidationError) -__all__ = ('BaseDocument',) +__all__ = ('BaseDocument', 'NON_FIELD_ERRORS') NON_FIELD_ERRORS = '__all__' From bce859569f924fe051441b99c36d11795b5953a9 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 25 Jul 2017 19:37:21 +0200 Subject: [PATCH 112/268] Remove SemiStrictDict to improve perfs --- docs/changelog.rst | 2 +- mongoengine/base/datastructures.py | 39 ---------------------------- mongoengine/base/document.py | 5 ++-- tests/test_datastructures.py | 41 +----------------------------- 4 files changed, 4 insertions(+), 83 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c60bbf09..1dcbfb51 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog Development =========== -- (Fill this out as you fix issues and develop your features). +- Improve performances by removing SemiStrictDict Changes in 0.14.0 ================= diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index b9aca8fa..cd6ac641 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -445,42 +445,3 @@ class StrictDict(object): cls._classes[allowed_keys] = SpecificStrictDict return cls._classes[allowed_keys] - - -class SemiStrictDict(StrictDict): - __slots__ = ('_extras', ) - _classes = {} - - def __getattr__(self, attr): - try: - super(SemiStrictDict, self).__getattr__(attr) - except AttributeError: - try: - return self.__getattribute__('_extras')[attr] - except KeyError as e: - raise AttributeError(e) - - def __setattr__(self, attr, value): - try: - super(SemiStrictDict, self).__setattr__(attr, value) - except AttributeError: - try: - self._extras[attr] = value - except AttributeError: - self._extras = {attr: value} - - def __delattr__(self, attr): - try: - super(SemiStrictDict, self).__delattr__(attr) - except AttributeError: - try: - del self._extras[attr] - except KeyError as e: - raise AttributeError(e) - - def __iter__(self): - try: - extras_iter = iter(self.__getattribute__('_extras')) - except AttributeError: - extras_iter = () - return itertools.chain(super(SemiStrictDict, self).__iter__(), extras_iter) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index d05c7695..f8ab73d0 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -13,7 +13,7 @@ from mongoengine import signals from mongoengine.base.common import get_document from mongoengine.base.datastructures import (BaseDict, BaseList, EmbeddedDocumentList, - SemiStrictDict, StrictDict) + StrictDict) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, @@ -79,8 +79,7 @@ class BaseDocument(object): if self.STRICT and not self._dynamic: self._data = StrictDict.create(allowed_keys=self._fields_ordered)() else: - self._data = SemiStrictDict.create( - allowed_keys=self._fields_ordered)() + self._data = {} self._dynamic_fields = SON() diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 6830a188..79381c5a 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,6 @@ import unittest -from mongoengine.base.datastructures import StrictDict, SemiStrictDict +from mongoengine.base.datastructures import StrictDict class TestStrictDict(unittest.TestCase): @@ -76,44 +76,5 @@ class TestStrictDict(unittest.TestCase): assert dict(**d) == {'a': 1, 'b': 2} -class TestSemiSrictDict(TestStrictDict): - def strict_dict_class(self, *args, **kwargs): - return SemiStrictDict.create(*args, **kwargs) - - def test_init_fails_on_nonexisting_attrs(self): - # disable irrelevant test - pass - - def test_setattr_raises_on_nonexisting_attr(self): - # disable irrelevant test - pass - - def test_setattr_getattr_nonexisting_attr_succeeds(self): - d = self.dtype() - d.x = 1 - self.assertEqual(d.x, 1) - - def test_init_succeeds_with_nonexisting_attrs(self): - d = self.dtype(a=1, b=1, c=1, x=2) - self.assertEqual((d.a, d.b, d.c, d.x), (1, 1, 1, 2)) - - def test_iter_with_nonexisting_attrs(self): - d = self.dtype(a=1, b=1, c=1, x=2) - self.assertEqual(list(d), ['a', 'b', 'c', 'x']) - - def test_iteritems_with_nonexisting_attrs(self): - d = self.dtype(a=1, b=1, c=1, x=2) - self.assertEqual(list(d.iteritems()), [('a', 1), ('b', 1), ('c', 1), ('x', 2)]) - - def tets_cmp_with_strict_dicts(self): - d = self.dtype(a=1, b=1, c=1) - dd = StrictDict.create(("a", "b", "c"))(a=1, b=1, c=1) - self.assertEqual(d, dd) - - def test_cmp_with_strict_dict_with_nonexisting_attrs(self): - d = self.dtype(a=1, b=1, c=1, x=2) - dd = StrictDict.create(("a", "b", "c", "x"))(a=1, b=1, c=1, x=2) - self.assertEqual(d, dd) - if __name__ == '__main__': unittest.main() From 66429ce331822c29e69770386785dfa0412dd02b Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Sun, 6 Aug 2017 13:35:01 +0530 Subject: [PATCH 113/268] Add python 3.6 support --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5e90660d..9362dcc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ language: python python: - 2.7 - 3.5 +- 3.6 - pypy env: @@ -39,6 +40,10 @@ matrix: env: MONGODB=2.4 PYMONGO=3.0 - python: 3.5 env: MONGODB=3.0 PYMONGO=3.0 + - python: 3.6 + env: MONGODB=2.4 PYMONGO=3.0 + - python: 3.6 + env: MONGODB=3.0 PYMONGO=3.0 before_install: - bash .install_mongodb_on_travis.sh From 7c0cfb1da2dd4bdcd24332a3ca08d561fa1e93da Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Sun, 6 Aug 2017 13:23:47 +0530 Subject: [PATCH 114/268] Add six.moves.range instead of xrange --- mongoengine/base/datastructures.py | 2 +- mongoengine/queryset/queryset.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index b9aca8fa..c727ec78 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -127,7 +127,7 @@ class BaseList(list): return value def __iter__(self): - for i in xrange(self.__len__()): + for i in six.moves.range(self.__len__()): yield self[i] def __setitem__(self, key, value, *args, **kwargs): diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index b5d2765b..cf913b01 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -1,3 +1,5 @@ +import six + from mongoengine.errors import OperationError from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING, NULLIFY, PULL) @@ -112,7 +114,7 @@ class QuerySet(BaseQuerySet): # Pull in ITER_CHUNK_SIZE docs from the database and store them in # the result cache. try: - for _ in xrange(ITER_CHUNK_SIZE): + for _ in six.moves.range(ITER_CHUNK_SIZE): self._result_cache.append(self.next()) except StopIteration: # Getting this exception means there are no more docs in the @@ -166,7 +168,7 @@ class QuerySetNoCache(BaseQuerySet): return '.. queryset mid-iteration ..' data = [] - for _ in xrange(REPR_OUTPUT_SIZE + 1): + for _ in six.moves.range(REPR_OUTPUT_SIZE + 1): try: data.append(self.next()) except StopIteration: From 9b3fe09508e6bd5fc7fe8ed6fff97f515841bede Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Fri, 18 Aug 2017 09:31:26 +0800 Subject: [PATCH 115/268] added as_pymongo test for PointField #1311 --- tests/queryset/geo.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 51a32382..38c0377e 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -510,6 +510,24 @@ class GeoQueriesTest(MongoDBTestCase): roads = Road.objects.filter(poly__geo_intersects={"$geometry": polygon}).count() self.assertEqual(1, roads) + def test_aspymongo_with_only(self): + """Ensure as_pymongo works with only""" + class Place(Document): + location = PointField() + + Place.drop_collection() + p = Place(location=[24.946861267089844, 60.16311983618494]) + p.save() + qs = Place.objects().only('location') + self.assertDictEqual( + qs.as_pymongo()[0]['location'], + {u'type': u'Point', + u'coordinates': [ + 24.946861267089844, + 60.16311983618494] + } + ) + def test_2dsphere_point_sets_correctly(self): class Location(Document): loc = PointField() From dc8a64fa7d00364956560d47a7b753aee84fa561 Mon Sep 17 00:00:00 2001 From: Srinivas Reddy Thatiparthy Date: Fri, 25 Aug 2017 22:02:47 +0530 Subject: [PATCH 116/268] Add missing dunder method - __ne__ to the class GridFSProxy class --- mongoengine/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0d402712..fffba7ac 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1465,6 +1465,9 @@ class GridFSProxy(object): else: return False + def __ne__(self, other): + return not self == other + @property def fs(self): if not self._fs: From 1eae97731f2a384cb7b27404aa9b59741155f7bc Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 30 Aug 2017 12:04:04 +0800 Subject: [PATCH 117/268] Fix Document.modify fail on sharded collection #1569 --- mongoengine/document.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/document.py b/mongoengine/document.py index f1622934..71929cf1 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -280,6 +280,9 @@ class Document(BaseDocument): elif query[id_field] != self.pk: raise InvalidQueryError('Invalid document modify query: it must modify only this document.') + # Need to add shard key to query, or you get an error + query.update(self._object_key) + updated = self._qs(**query).modify(new=True, **update) if updated is None: return False From 44732a5dd904da5224ae1ab7ab45ca4a247d82c1 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Sat, 2 Sep 2017 02:05:27 +0900 Subject: [PATCH 118/268] add fix for reload(fields) affect changed fields #1371 --- mongoengine/document.py | 5 +++-- tests/document/instance.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index f1622934..cb901c82 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -702,7 +702,6 @@ class Document(BaseDocument): obj = obj[0] else: raise self.DoesNotExist('Document does not exist') - for field in obj._data: if not fields or field in fields: try: @@ -718,7 +717,9 @@ class Document(BaseDocument): # i.e. obj.update(unset__field=1) followed by obj.reload() delattr(self, field) - self._changed_fields = obj._changed_fields + self._changed_fields = list( + set(self._changed_fields) - set(fields) + ) if fields else obj._changed_fields self._created = False return self diff --git a/tests/document/instance.py b/tests/document/instance.py index 609bc900..2a746709 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -476,6 +476,24 @@ class InstanceTest(unittest.TestCase): doc.save() doc.reload() + def test_reload_with_changed_fields(self): + """Ensures reloading will not affect changed fields""" + class User(Document): + name = StringField() + number = IntField() + User.drop_collection() + + user = User(name="Bob", number=1).save() + user.name = "John" + user.number = 2 + + self.assertEqual(user._get_changed_fields(), ['name', 'number']) + user.reload('number') + self.assertEqual(user._get_changed_fields(), ['name']) + user.save() + user.reload() + self.assertEqual(user.name, "John") + def test_reload_referencing(self): """Ensures reloading updates weakrefs correctly.""" class Embedded(EmbeddedDocument): @@ -521,7 +539,7 @@ class InstanceTest(unittest.TestCase): doc.save() doc.dict_field['extra'] = 1 doc = doc.reload(10, 'list_field') - self.assertEqual(doc._get_changed_fields(), []) + self.assertEqual(doc._get_changed_fields(), ['dict_field.extra']) self.assertEqual(len(doc.list_field), 5) self.assertEqual(len(doc.dict_field), 3) self.assertEqual(len(doc.embedded_field.list_field), 4) From 02733e6e5849bd60cd938fc05db2f73f8a561036 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Sat, 2 Sep 2017 12:00:57 +0900 Subject: [PATCH 119/268] fix flake8 error #1371 --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index cb901c82..a30dd0df 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -719,7 +719,7 @@ class Document(BaseDocument): self._changed_fields = list( set(self._changed_fields) - set(fields) - ) if fields else obj._changed_fields + ) if fields else obj._changed_fields self._created = False return self From 70088704e202c38ed7265cbb1929a38d383c8a3d Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 10 Sep 2017 01:37:17 +0900 Subject: [PATCH 120/268] add tests to increase code coverage --- tests/fields/fields.py | 10 +++++++++- tests/queryset/geo.py | 4 ++++ tests/queryset/queryset.py | 6 ++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7a0ccc25..dbf148e1 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -334,7 +334,7 @@ class FieldTest(MongoDBTestCase): def test_string_validation(self): """Ensure that invalid values cannot be assigned to string fields.""" class Person(Document): - name = StringField(max_length=20) + name = StringField(max_length=20, min_length=5) userid = StringField(r'[0-9a-z_]+$') person = Person(name=34) @@ -352,6 +352,10 @@ class FieldTest(MongoDBTestCase): person = Person(name='Name that is more than twenty characters') self.assertRaises(ValidationError, person.validate) + # Test max length validation on name + person = Person(name='aa') + self.assertRaises(ValidationError, person.validate) + person.name = 'Shorter name' person.validate() @@ -437,6 +441,10 @@ class FieldTest(MongoDBTestCase): doc.age = 'ten' self.assertRaises(ValidationError, doc.validate) + # Test max_value validation + doc.value = 200 + self.assertRaises(ValidationError, doc.validate) + def test_float_validation(self): """Ensure that invalid values cannot be assigned to float fields. """ diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 38c0377e..acfd9364 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -429,6 +429,10 @@ class GeoQueriesTest(MongoDBTestCase): roads = Road.objects.filter(line__geo_within=polygon).count() self.assertEqual(1, roads) + sphere = [[-1, 42,], 2] + roads = Road.objects.filter(line__geo_within_sphere=sphere).count() + self.assertEqual(0, roads) + roads = Road.objects.filter(line__geo_within={"$geometry": polygon}).count() self.assertEqual(1, roads) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c78ed985..5fc73a54 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1882,6 +1882,12 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertTrue('mongo' in post.tags) + # Push with arrays + BlogPost.objects.update(push__tags=['python', 'scala']) + post.reload() + self.assertTrue('python' in post.tags) + self.assertTrue('scala' in post.tags) + BlogPost.objects.update_one(push_all__tags=['db', 'nosql']) post.reload() self.assertTrue('db' in post.tags and 'nosql' in post.tags) From ba99190f535ba16ed3c9d6fe5acfc4836c64b48d Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 10 Sep 2017 13:09:20 +0900 Subject: [PATCH 121/268] add tests to increase coverage --- tests/fields/fields.py | 26 ++++++++++++++++++++++++++ tests/queryset/queryset.py | 4 ++++ 2 files changed, 30 insertions(+) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index dbf148e1..d25f2e25 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -312,6 +312,27 @@ class FieldTest(MongoDBTestCase): self.assertEqual(1, TestDocument.objects(long_fld__ne=None).count()) + def test_callable_validation(self): + """Ensure that callable validation works""" + def check_even(value): + return value % 2 == 0 + + class Order(Document): + number = IntField(validation=check_even) + + Order.drop_collection() + + order = Order(number=3) + self.assertRaises(ValidationError, order.validate) + + class User(Document): + name = StringField(validation=1) + + User.drop_collection() + + user = User(name='test') + self.assertRaises(ValidationError, user.validate) + def test_object_id_validation(self): """Ensure that invalid values cannot be assigned to an ObjectIdField. @@ -527,6 +548,11 @@ class FieldTest(MongoDBTestCase): class User(Document): name = StringField(db_field='name\0') + # db field should be a string + with self.assertRaises(TypeError): + class User(Document): + name = StringField(db_field=1) + def test_decimal_comparison(self): class Person(Document): money = DecimalField() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5fc73a54..9c61513a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1861,6 +1861,10 @@ class QuerySetTest(unittest.TestCase): post = BlogPost(name="Test Post", hits=5, tags=['test']) post.save() + BlogPost.objects.update(hits=11) + post.reload() + self.assertEqual(post.hits, 11) + BlogPost.objects.update(set__hits=10) post.reload() self.assertEqual(post.hits, 10) From be8f1b9fdd522888527f871c1fbc2a0f6f0af279 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Thu, 14 Sep 2017 22:24:27 +0900 Subject: [PATCH 122/268] add failing test for generic_emdedded_document query #1651 --- tests/queryset/queryset.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c78ed985..4bc5fef6 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4790,6 +4790,30 @@ class QuerySetTest(unittest.TestCase): for obj in C.objects.no_sub_classes(): self.assertEqual(obj.__class__, C) + def test_query_generic_embedded_document(self): + """Ensure that querying sub field on generic_embedded_field works + """ + class A(EmbeddedDocument): + a_name = StringField() + + class B(EmbeddedDocument): + b_name = StringField() + + class Doc(Document): + document = GenericEmbeddedDocumentField(choices=(A, B)) + + Doc.drop_collection() + Doc(document=A(a_name='A doc')).save() + Doc(document=B(b_name='B doc')).save() + + # Using raw in filter working fine + self.assertEqual(Doc.objects( + __raw__={'document.a_name': 'A doc'}).count(), 1) + self.assertEqual(Doc.objects( + __raw__={'document.b_name': 'B doc'}).count(), 1) + self.assertEqual(Doc.objects(document__a_name='A doc').count(), 1) + self.assertEqual(Doc.objects(document__b_name='B doc').count(), 1) + def test_query_reference_to_custom_pk_doc(self): class A(Document): From e90f6a2fa3766872b44edad29a21527659984ae3 Mon Sep 17 00:00:00 2001 From: Andy Yankovsky Date: Thu, 14 Sep 2017 20:28:15 +0300 Subject: [PATCH 123/268] Fix update via pull__something__in=[] --- mongoengine/queryset/transform.py | 46 +++++++++++++++++++------------ tests/queryset/transform.py | 7 +++++ 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index a9907ada..1f70a48b 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -101,21 +101,8 @@ def query(_doc_cls=None, **kwargs): value = value['_id'] elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict): - # Raise an error if the in/nin/all/near param is not iterable. We need a - # special check for BaseDocument, because - although it's iterable - using - # it as such in the context of this method is most definitely a mistake. - BaseDocument = _import_class('BaseDocument') - if isinstance(value, BaseDocument): - raise TypeError("When using the `in`, `nin`, `all`, or " - "`near`-operators you can\'t use a " - "`Document`, you must wrap your object " - "in a list (object -> [object]).") - elif not hasattr(value, '__iter__'): - raise TypeError("The `in`, `nin`, `all`, or " - "`near`-operators must be applied to an " - "iterable (e.g. a list).") - else: - value = [field.prepare_query_value(op, v) for v in value] + # Raise an error if the in/nin/all/near param is not iterable. + value = _prepare_query_for_iterable(field, op, value) # If we're querying a GenericReferenceField, we need to alter the # key depending on the value: @@ -284,9 +271,15 @@ def update(_doc_cls=None, **update): if isinstance(field, GeoJsonBaseField): value = field.to_mongo(value) - if op == 'push' and isinstance(value, (list, tuple, set)): + if op == 'pull': + if field.required or value is not None: + if match == 'in' and not isinstance(value, dict): + value = _prepare_query_for_iterable(field, op, value) + else: + value = field.prepare_query_value(op, value) + elif op == 'push' and isinstance(value, (list, tuple, set)): value = [field.prepare_query_value(op, v) for v in value] - elif op in (None, 'set', 'push', 'pull'): + elif op in (None, 'set', 'push'): if field.required or value is not None: value = field.prepare_query_value(op, value) elif op in ('pushAll', 'pullAll'): @@ -439,3 +432,22 @@ def _infer_geometry(value): raise InvalidQueryError('Invalid $geometry data. Can be either a ' 'dictionary or (nested) lists of coordinate(s)') + + +def _prepare_query_for_iterable(field, op, value): + # We need a special check for BaseDocument, because - although it's iterable - using + # it as such in the context of this method is most definitely a mistake. + BaseDocument = _import_class('BaseDocument') + + if isinstance(value, BaseDocument): + raise TypeError("When using the `in`, `nin`, `all`, or " + "`near`-operators you can\'t use a " + "`Document`, you must wrap your object " + "in a list (object -> [object]).") + + if not hasattr(value, '__iter__'): + raise TypeError("The `in`, `nin`, `all`, or " + "`near`-operators must be applied to an " + "iterable (e.g. a list).") + + return [field.prepare_query_value(op, v) for v in value] diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 20ab0b3f..a043a647 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -28,12 +28,16 @@ class TransformTest(unittest.TestCase): {'name': {'$exists': True}}) def test_transform_update(self): + class LisDoc(Document): + foo = ListField(StringField()) + class DicDoc(Document): dictField = DictField() class Doc(Document): pass + LisDoc.drop_collection() DicDoc.drop_collection() Doc.drop_collection() @@ -51,6 +55,9 @@ class TransformTest(unittest.TestCase): update = transform.update(DicDoc, pull__dictField__test=doc) self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict)) + update = transform.update(LisDoc, pull__foo__in=['a']) + self.assertEqual(update, {'$pull': {'foo': {'$in': ['a']}}}) + def test_query_field_name(self): """Ensure that the correct field name is used when querying. """ From 2f4e2bde6bb1d892ac22928870e01889fa983833 Mon Sep 17 00:00:00 2001 From: Andy Yankovsky Date: Thu, 14 Sep 2017 21:02:53 +0300 Subject: [PATCH 124/268] Update AUTHORS --- AUTHORS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 4eac5eb2..2e7b56fc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -244,4 +244,5 @@ that much better: * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) * Renjianxin (https://github.com/Davidrjx) - * Erdenezul Batmunkh (https://github.com/erdenezul) \ No newline at end of file + * Erdenezul Batmunkh (https://github.com/erdenezul) + * Andy Yankovsky (https://github.com/werat) From aa4996ef28bec2614cb11c055b5d184fee6b9549 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Fri, 15 Sep 2017 11:18:24 +0800 Subject: [PATCH 125/268] fix bug query subfield in generic_embedded_document #1651 --- mongoengine/fields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index fffba7ac..cc66008b 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -692,6 +692,14 @@ class GenericEmbeddedDocumentField(BaseField): value.validate(clean=clean) + def lookup_member(self, member_name): + if self.choices: + for choice in self.choices: + field = choice._fields.get(member_name) + if field: + return field + return None + def to_mongo(self, document, use_db_field=True, fields=None): if document is None: return None From 091a02f73737ccd028aa256e2f50cc30d84621a7 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 1 Oct 2017 16:09:10 -0400 Subject: [PATCH 126/268] minor .travis.yml comment correction [ci skip] --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9362dcc2..78a9f787 100644 --- a/.travis.yml +++ b/.travis.yml @@ -97,7 +97,7 @@ deploy: distributions: "sdist bdist_wheel" # only deploy on tagged commits (aka GitHub releases) and only for the - # parent repo's builds running Python 2.7 along with dev PyMongo (we run + # parent repo's builds running Python 2.7 along with PyMongo v3.0 (we run # Travis against many different Python and PyMongo versions and we don't # want the deploy to occur multiple times). on: From 01526a7b373b954ef053238331f84ef1193bb564 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 1 Oct 2017 16:32:02 -0400 Subject: [PATCH 127/268] v0.14.1 version bump + updated changelog --- docs/changelog.rst | 10 +++++++++- mongoengine/__init__.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1dcbfb51..f04ab314 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,15 @@ Changelog Development =========== -- Improve performances by removing SemiStrictDict +- (Fill this out as you fix issues and develop your features). + +Changes in 0.14.1 +================= +- Removed SemiStrictDict and started using a regular dict for `BaseDocument._data` #1630 +- Added support for the `$position` param in the `$push` operator #1566 +- Fixed `DateTimeField` interpreting an empty string as today #1533 +- Added a missing `__ne__` method to the `GridFSProxy` class #1632 +- Fixed `BaseQuerySet._fields_to_db_fields` #1553 Changes in 0.14.0 ================= diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index b41e87e7..4b60f1b7 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 14, 0) +VERSION = (0, 14, 1) def get_version(): From d79ab5ffeb421abe4c145af77633283f4d20d942 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 1 Oct 2017 17:05:28 -0400 Subject: [PATCH 128/268] remove {nospam} from author_email & maintainer_email (PyPI doesnt validate those anymore) --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index fa682d20..98964d19 100644 --- a/setup.py +++ b/setup.py @@ -70,9 +70,9 @@ setup( name='mongoengine', version=VERSION, author='Harry Marr', - author_email='harry.marr@{nospam}gmail.com', - maintainer="Ross Lawley", - maintainer_email="ross.lawley@{nospam}gmail.com", + author_email='harry.marr@gmail.com', + maintainer="Stefan Wojcik", + maintainer_email="wojcikstefan@gmail.com", url='http://mongoengine.org/', download_url='https://github.com/MongoEngine/mongoengine/tarball/master', license='MIT', From a1494c4c93cce608d8d9450c9ef76c07559248c9 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Sun, 1 Oct 2017 17:31:10 -0400 Subject: [PATCH 129/268] v0.14.3 version bump --- mongoengine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index 4b60f1b7..c7c6f707 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 14, 1) +VERSION = (0, 14, 3) def get_version(): From 2f075be6f8857d297d9e939d9150417158d93d5a Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 2 Oct 2017 22:46:27 +0800 Subject: [PATCH 130/268] parse read_preference from conn_host #1665 --- mongoengine/connection.py | 11 +++++++++++ tests/test_connection.py | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 34ff4dc3..ef815343 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -103,6 +103,17 @@ def register_connection(alias, name=None, host=None, port=None, conn_settings['authentication_source'] = uri_options['authsource'] if 'authmechanism' in uri_options: conn_settings['authentication_mechanism'] = uri_options['authmechanism'] + if 'readpreference' in uri_options: + read_preferences = (ReadPreference.NEAREST, + ReadPreference.PRIMARY, + ReadPreference.PRIMARY_PREFERRED, + ReadPreference.SECONDARY, + ReadPreference.SECONDARY_PREFERRED) + read_pf_mode = uri_options['readpreference'] + for preference in read_preferences: + if preference.mode == read_pf_mode: + conn_settings['read_preference'] = preference + break else: resolved_hosts.append(entity) conn_settings['host'] = resolved_hosts diff --git a/tests/test_connection.py b/tests/test_connection.py index cdcf1377..f0c272e4 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -364,6 +364,11 @@ class ConnectionTest(unittest.TestCase): date_doc = DateDoc.objects.first() self.assertEqual(d, date_doc.the_date) + def test_read_preference_from_parse(self): + from pymongo import ReadPreference + conn = connect(host="mongodb://a1.vpc,a2.vpc,a3.vpc/prod?readPreference=secondaryPreferred") + self.assertEqual(conn.read_preference, ReadPreference.SECONDARY_PREFERRED) + def test_multiple_connection_settings(self): connect('mongoenginetest', alias='t1', host="localhost") From 416486c370cbe565128b04e9fadcc65e643d12d9 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 2 Oct 2017 23:13:25 +0800 Subject: [PATCH 131/268] use read_preference only pymongo3.x #1665 --- mongoengine/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index ef815343..419af6bc 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -103,7 +103,7 @@ def register_connection(alias, name=None, host=None, port=None, conn_settings['authentication_source'] = uri_options['authsource'] if 'authmechanism' in uri_options: conn_settings['authentication_mechanism'] = uri_options['authmechanism'] - if 'readpreference' in uri_options: + if IS_PYMONGO_3 and 'readpreference' in uri_options: read_preferences = (ReadPreference.NEAREST, ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, From 5c4ce8754e29d435a621a1780ef96c3a6bb60ebf Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 2 Oct 2017 23:15:37 +0800 Subject: [PATCH 132/268] run tests only pymongo3 #1565 --- tests/test_connection.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_connection.py b/tests/test_connection.py index f0c272e4..f58b1a3e 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -365,9 +365,10 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(d, date_doc.the_date) def test_read_preference_from_parse(self): - from pymongo import ReadPreference - conn = connect(host="mongodb://a1.vpc,a2.vpc,a3.vpc/prod?readPreference=secondaryPreferred") - self.assertEqual(conn.read_preference, ReadPreference.SECONDARY_PREFERRED) + if IS_PYMONGO_3: + from pymongo import ReadPreference + conn = connect(host="mongodb://a1.vpc,a2.vpc,a3.vpc/prod?readPreference=secondaryPreferred") + self.assertEqual(conn.read_preference, ReadPreference.SECONDARY_PREFERRED) def test_multiple_connection_settings(self): connect('mongoenginetest', alias='t1', host="localhost") From 6e2db1ced6a8621e35cf38f27993751fe5ea1e6b Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Tue, 3 Oct 2017 09:23:17 +0800 Subject: [PATCH 133/268] read_preference from parse_uri only PYMONGO_3 #1665 --- mongoengine/connection.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 419af6bc..feba0b58 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -104,14 +104,15 @@ def register_connection(alias, name=None, host=None, port=None, if 'authmechanism' in uri_options: conn_settings['authentication_mechanism'] = uri_options['authmechanism'] if IS_PYMONGO_3 and 'readpreference' in uri_options: - read_preferences = (ReadPreference.NEAREST, - ReadPreference.PRIMARY, - ReadPreference.PRIMARY_PREFERRED, - ReadPreference.SECONDARY, - ReadPreference.SECONDARY_PREFERRED) - read_pf_mode = uri_options['readpreference'] + read_preferences = ( + ReadPreference.NEAREST, + ReadPreference.PRIMARY, + ReadPreference.PRIMARY_PREFERRED, + ReadPreference.SECONDARY, + ReadPreference.SECONDARY_PREFERRED) + read_pf_mode = uri_options['readpreference'].lower() for preference in read_preferences: - if preference.mode == read_pf_mode: + if preference.name.lower() == read_pf_mode: conn_settings['read_preference'] = preference break else: From 9ab856e186b438bc28f12cae8274ea1bb2ff3614 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Tue, 10 Oct 2017 10:34:34 +0800 Subject: [PATCH 134/268] use each modifier only with #1673 --- mongoengine/queryset/transform.py | 2 -- tests/document/instance.py | 11 +++++++++++ tests/queryset/queryset.py | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index a9907ada..3eadaf64 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -344,8 +344,6 @@ def update(_doc_cls=None, **update): if not isinstance(value, (set, tuple, list)): value = [value] value = {key: {'$each': value, '$position': position}} - elif isinstance(value, list): - value = {key: {'$each': value}} else: value = {key: value} else: diff --git a/tests/document/instance.py b/tests/document/instance.py index 609bc900..721aa95f 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -3183,6 +3183,17 @@ class InstanceTest(unittest.TestCase): blog.reload() self.assertEqual(blog.tags, ['mongodb', 'code', 'python']) + def test_push_nested_list(self): + """Ensure that push update works in nested list""" + class BlogPost(Document): + slug = StringField() + tags = ListField() + + blog = BlogPost(slug="test").save() + blog.update(push__tags=["value1", 123]) + blog.reload() + self.assertEqual(blog.tags, [["value1", 123]]) + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c78ed985..d633b8b2 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1929,6 +1929,21 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertEqual(post.tags, ['scala', 'mongodb', 'python', 'java']) + def test_update_push_list_of_list(self): + """Ensure that the 'push' update operation works in the list of list + """ + class BlogPost(Document): + slug = StringField() + tags = ListField() + + BlogPost.drop_collection() + + post = BlogPost(slug="test").save() + + BlogPost.objects.filter(slug="test").update(push__tags=["value1", 123]) + post.reload() + self.assertEqual(post.tags, [["value1", 123]]) + def test_update_push_and_pull_add_to_set(self): """Ensure that the 'pull' update operation works correctly. """ From 15451ff42b7cc7b5a9a76190b60e96d062ecff81 Mon Sep 17 00:00:00 2001 From: Mandeep Singh Date: Tue, 17 Oct 2017 00:11:47 +0530 Subject: [PATCH 135/268] return instead of raising StopIteration --- mongoengine/queryset/base.py | 2 +- mongoengine/queryset/queryset.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6f9c372c..2dcfdbea 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1461,7 +1461,7 @@ class BaseQuerySet(object): """Wrap the result in a :class:`~mongoengine.Document` object. """ if self._limit == 0 or self._none: - raise StopIteration + return raw_doc = self._cursor.next() diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index cf913b01..1aadfb76 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -89,10 +89,10 @@ class QuerySet(BaseQuerySet): yield self._result_cache[pos] pos += 1 - # Raise StopIteration if we already established there were no more + # return if we already established there were no more # docs in the db cursor. if not self._has_more: - raise StopIteration + return # Otherwise, populate more of the cache and repeat. if len(self._result_cache) <= pos: From e6c0280b40f18074c0a6267b29f2a77f21c1c779 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 30 Oct 2017 18:15:51 +0100 Subject: [PATCH 136/268] Add LazyReferenceField --- docs/changelog.rst | 6 +- mongoengine/base/__init__.py | 2 +- mongoengine/base/datastructures.py | 42 ++++- mongoengine/fields.py | 139 ++++++++++++++- tests/fields/fields.py | 276 ++++++++++++++++++++++++++++- 5 files changed, 456 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f04ab314..834fbee2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,9 +2,9 @@ Changelog ========= -Development -=========== -- (Fill this out as you fix issues and develop your features). +Changes in 0.15.0 +================= +- Add LazyReferenceField to address #1230 Changes in 0.14.1 ================= diff --git a/mongoengine/base/__init__.py b/mongoengine/base/__init__.py index da31b922..e069a147 100644 --- a/mongoengine/base/__init__.py +++ b/mongoengine/base/__init__.py @@ -15,7 +15,7 @@ __all__ = ( 'UPDATE_OPERATORS', '_document_registry', 'get_document', # datastructures - 'BaseDict', 'BaseList', 'EmbeddedDocumentList', + 'BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference', # document 'BaseDocument', diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 14fe95e9..043df471 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -2,11 +2,12 @@ import itertools import weakref import six +from bson import DBRef from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned -__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList') +__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference') class BaseDict(dict): @@ -445,3 +446,42 @@ class StrictDict(object): cls._classes[allowed_keys] = SpecificStrictDict return cls._classes[allowed_keys] + + +class LazyReference(DBRef): + __slots__ = ('_cached_doc', 'passthrough', 'document_type') + + def fetch(self, force=False): + if not self._cached_doc or force: + self._cached_doc = self.document_type.objects.get(pk=self.pk) + if not self._cached_doc: + raise DoesNotExist('Trying to dereference unknown document %s' % (self)) + return self._cached_doc + + @property + def pk(self): + return self.id + + def __init__(self, document_type, pk, cached_doc=None, passthrough=False): + self.document_type = document_type + self._cached_doc = cached_doc + self.passthrough = passthrough + super(LazyReference, self).__init__(self.document_type._get_collection_name(), pk) + + def __getitem__(self, name): + if not self.passthrough: + raise KeyError() + document = self.fetch() + return document[name] + + def __getattr__(self, name): + if not object.__getattribute__(self, 'passthrough'): + raise AttributeError() + document = self.fetch() + try: + return document[name] + except KeyError: + raise AttributeError() + + def __repr__(self): + return "" % (self.document_type, self.pk) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index fffba7ac..73a62bc5 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -26,7 +26,8 @@ except ImportError: Int64 = long from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, - GeoJsonBaseField, ObjectIdField, get_document) + GeoJsonBaseField, ObjectIdField, get_document, + LazyReference) from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError @@ -46,6 +47,8 @@ __all__ = ( 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', 'SortedListField', 'EmbeddedDocumentListField', 'DictField', 'MapField', 'ReferenceField', 'CachedReferenceField', + 'LazyReferenceField', + # 'GenericLazyReferenceField', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', @@ -953,6 +956,15 @@ class ReferenceField(BaseField): """A reference to a document that will be automatically dereferenced on access (lazily). + Note this means you will get a database I/O access everytime you access + this field. This is necessary because the field returns a :class:`~mongoengine.Document` + which precise type can depend of the value of the `_cls` field present in the + document in database. + In short, using this type of field can lead to poor performances (especially + if you access this field only to retrieve it `pk` field which is already + known before dereference). To solve this you should consider using the + :class:`~mongoengine.fields.LazyReferenceField`. + Use the `reverse_delete_rule` to handle what should happen if the document the field is referencing is deleted. EmbeddedDocuments, DictFields and MapFields does not support reverse_delete_rule and an `InvalidDocumentError` @@ -1087,8 +1099,8 @@ class ReferenceField(BaseField): def validate(self, value): - if not isinstance(value, (self.document_type, DBRef, ObjectId)): - self.error('A ReferenceField only accepts DBRef, ObjectId or documents') + if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)): + self.error('A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents') if isinstance(value, Document) and value.id is None: self.error('You can only reference documents once they have been ' @@ -2141,3 +2153,124 @@ class MultiPolygonField(GeoJsonBaseField): .. versionadded:: 0.9 """ _type = 'MultiPolygon' + + +class LazyReferenceField(BaseField): + """A really lazy reference to a document. + Unlike the :class:`~mongoengine.fields.ReferenceField` it must be manually + dereferenced using it ``fetch()`` method. + """ + + def __init__(self, document_type, passthrough=False, dbref=False, + reverse_delete_rule=DO_NOTHING, **kwargs): + """Initialises the Reference Field. + + :param dbref: Store the reference as :class:`~pymongo.dbref.DBRef` + or as the :class:`~pymongo.objectid.ObjectId`.id . + :param reverse_delete_rule: Determines what to do when the referring + object is deleted + :param passthrough: When trying to access unknown fields, the + :class:`~mongoengine.base.datastructure.LazyReference` instance will + automatically call `fetch()` and try to retrive the field on the fetched + document. Note this only work getting field (not setting or deleting). + """ + if ( + not isinstance(document_type, six.string_types) and + not issubclass(document_type, Document) + ): + self.error('Argument to LazyReferenceField constructor must be a ' + 'document class or a string') + + self.dbref = dbref + self.passthrough = passthrough + self.document_type_obj = document_type + self.reverse_delete_rule = reverse_delete_rule + super(LazyReferenceField, self).__init__(**kwargs) + + @property + def document_type(self): + if isinstance(self.document_type_obj, six.string_types): + if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: + self.document_type_obj = self.owner_document + else: + self.document_type_obj = get_document(self.document_type_obj) + return self.document_type_obj + + def __get__(self, instance, owner): + """Descriptor to allow lazy dereferencing.""" + if instance is None: + # Document class being used rather than a document object + return self + + value = instance._data.get(self.name) + if isinstance(value, LazyReference): + if value.passthrough != self.passthrough: + instance._data[self.name] = LazyReference( + value.document_type, value.pk, passthrough=self.passthrough) + elif value is not None: + if isinstance(value, self.document_type): + value = LazyReference(self.document_type, value.pk, passthrough=self.passthrough) + elif isinstance(value, DBRef): + value = LazyReference(self.document_type, value.id, passthrough=self.passthrough) + else: + # value is the primary key of the referenced document + value = LazyReference(self.document_type, value, passthrough=self.passthrough) + instance._data[self.name] = value + + return super(LazyReferenceField, self).__get__(instance, owner) + + def to_mongo(self, value): + if isinstance(value, LazyReference): + pk = value.pk + elif isinstance(value, self.document_type): + pk = value.pk + elif isinstance(value, DBRef): + pk = value.id + else: + # value is the primary key of the referenced document + pk = value + id_field_name = self.document_type._meta['id_field'] + id_field = self.document_type._fields[id_field_name] + pk = id_field.to_mongo(pk) + if self.dbref: + return DBRef(self.document_type._get_collection_name(), pk) + else: + return pk + + def validate(self, value): + if isinstance(value, LazyReference): + if not issubclass(value.document_type, self.document_type): + self.error('Reference must be on a `%s` document.' % self.document_type) + pk = value.pk + elif isinstance(value, self.document_type): + pk = value.pk + elif isinstance(value, DBRef): + # TODO: check collection ? + collection = self.document_type._get_collection_name() + if value.collection != collection: + self.error("DBRef on bad collection (must be on `%s`)" % collection) + pk = value.id + else: + # value is the primary key of the referenced document + id_field_name = self.document_type._meta['id_field'] + id_field = getattr(self.document_type, id_field_name) + pk = value + try: + id_field.validate(pk) + except ValidationError: + self.error("value should be `{0}` document, LazyReference or DBRef on `{0}` " + "or `{0}`'s primary key (i.e. `{1}`)".format( + self.document_type.__name__, type(id_field).__name__)) + + if pk is None: + self.error('You can only reference documents once they have been ' + 'saved to the database') + + def prepare_query_value(self, op, value): + if value is None: + return None + super(LazyReferenceField, self).prepare_query_value(op, value) + return self.to_mongo(value) + + def lookup_member(self, member_name): + return self.document_type._fields.get(member_name) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7a0ccc25..84156622 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -26,7 +26,7 @@ except ImportError: from mongoengine import * from mongoengine.connection import get_db from mongoengine.base import (BaseDict, BaseField, EmbeddedDocumentList, - _document_registry) + _document_registry, LazyReference) from tests.utils import MongoDBTestCase @@ -931,7 +931,9 @@ class FieldTest(MongoDBTestCase): comments = ListField(EmbeddedDocumentField(Comment)) tags = ListField(StringField()) authors = ListField(ReferenceField(User)) + authors_as_lazy = ListField(LazyReferenceField(User)) generic = ListField(GenericReferenceField()) + # generic_as_lazy = ListField(LazyGenericReferenceField()) User.drop_collection() BlogPost.drop_collection() @@ -969,6 +971,15 @@ class FieldTest(MongoDBTestCase): post.authors = [user] post.validate() + post.authors_as_lazy = [Comment()] + self.assertRaises(ValidationError, post.validate) + + post.authors_as_lazy = [User()] + self.assertRaises(ValidationError, post.validate) + + post.authors_as_lazy = [user] + post.validate() + post.generic = [1, 2] self.assertRaises(ValidationError, post.validate) @@ -981,6 +992,18 @@ class FieldTest(MongoDBTestCase): post.generic = [user] post.validate() + # post.generic_as_lazy = [1, 2] + # self.assertRaises(ValidationError, post.validate) + + # post.generic_as_lazy = [User(), Comment()] + # self.assertRaises(ValidationError, post.validate) + + # post.generic_as_lazy = [Comment()] + # self.assertRaises(ValidationError, post.validate) + + # post.generic_as_lazy = [user] + # post.validate() + def test_sorted_list_sorting(self): """Ensure that a sorted list field properly sorts values. """ @@ -4598,5 +4621,256 @@ class CachedReferenceFieldTest(MongoDBTestCase): self.assertTrue(isinstance(ocorrence.animal, Animal)) +class LazyReferenceFieldTest(MongoDBTestCase): + def test_lazy_reference_config(self): + # Make sure ReferenceField only accepts a document class or a string + # with a document class name. + self.assertRaises(ValidationError, LazyReferenceField, EmbeddedDocument) + + def test_lazy_reference_simple(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal(name="Leopard", tag="heavy").save() + Ocurrence(person="test", animal=animal).save() + p = Ocurrence.objects.get() + self.assertIsInstance(p.animal, LazyReference) + fetched_animal = p.animal.fetch() + self.assertEqual(fetched_animal, animal) + # `fetch` keep cache on referenced document by default... + animal.tag = "not so heavy" + animal.save() + double_fetch = p.animal.fetch() + self.assertIs(fetched_animal, double_fetch) + self.assertEqual(double_fetch.tag, "heavy") + # ...unless specified otherwise + fetch_force = p.animal.fetch(force=True) + self.assertIsNot(fetch_force, fetched_animal) + self.assertEqual(fetch_force.tag, "not so heavy") + + def test_lazy_reference_fetch_invalid_ref(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal(name="Leopard", tag="heavy").save() + Ocurrence(person="test", animal=animal).save() + animal.delete() + p = Ocurrence.objects.get() + self.assertIsInstance(p.animal, LazyReference) + with self.assertRaises(DoesNotExist): + p.animal.fetch() + + def test_lazy_reference_set(self): + class Animal(Document): + meta = {'allow_inheritance': True} + + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + class SubAnimal(Animal): + nick = StringField() + + animal = Animal(name="Leopard", tag="heavy").save() + sub_animal = SubAnimal(nick='doggo', name='dog').save() + for ref in ( + animal, + animal.pk, + DBRef(animal._get_collection_name(), animal.pk), + LazyReference(Animal, animal.pk), + + sub_animal, + sub_animal.pk, + DBRef(sub_animal._get_collection_name(), sub_animal.pk), + LazyReference(SubAnimal, sub_animal.pk), + ): + p = Ocurrence(person="test", animal=ref).save() + p.reload() + self.assertIsInstance(p.animal, LazyReference) + p.animal.fetch() + + def test_lazy_reference_bad_set(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + class BadDoc(Document): + pass + + animal = Animal(name="Leopard", tag="heavy").save() + baddoc = BadDoc().save() + for bad in ( + 42, + 'foo', + baddoc, + DBRef(baddoc._get_collection_name(), animal.pk), + LazyReference(BadDoc, animal.pk) + ): + with self.assertRaises(ValidationError): + p = Ocurrence(person="test", animal=bad).save() + + def test_lazy_reference_query_conversion(self): + """Ensure that LazyReferenceFields can be queried using objects and values + of the type of the primary key of the referenced object. + """ + class Member(Document): + user_num = IntField(primary_key=True) + + class BlogPost(Document): + title = StringField() + author = LazyReferenceField(Member, dbref=False) + + Member.drop_collection() + BlogPost.drop_collection() + + m1 = Member(user_num=1) + m1.save() + m2 = Member(user_num=2) + m2.save() + + post1 = BlogPost(title='post 1', author=m1) + post1.save() + + post2 = BlogPost(title='post 2', author=m2) + post2.save() + + post = BlogPost.objects(author=m1).first() + self.assertEqual(post.id, post1.id) + + post = BlogPost.objects(author=m2).first() + self.assertEqual(post.id, post2.id) + + # Same thing by passing a LazyReference instance + post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() + self.assertEqual(post.id, post2.id) + + def test_lazy_reference_query_conversion_dbref(self): + """Ensure that LazyReferenceFields can be queried using objects and values + of the type of the primary key of the referenced object. + """ + class Member(Document): + user_num = IntField(primary_key=True) + + class BlogPost(Document): + title = StringField() + author = LazyReferenceField(Member, dbref=True) + + Member.drop_collection() + BlogPost.drop_collection() + + m1 = Member(user_num=1) + m1.save() + m2 = Member(user_num=2) + m2.save() + + post1 = BlogPost(title='post 1', author=m1) + post1.save() + + post2 = BlogPost(title='post 2', author=m2) + post2.save() + + post = BlogPost.objects(author=m1).first() + self.assertEqual(post.id, post1.id) + + post = BlogPost.objects(author=m2).first() + self.assertEqual(post.id, post2.id) + + # Same thing by passing a LazyReference instance + post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() + self.assertEqual(post.id, post2.id) + + def test_lazy_reference_passthrough(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + animal = LazyReferenceField(Animal, passthrough=False) + animal_passthrough = LazyReferenceField(Animal, passthrough=True) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal(name="Leopard", tag="heavy").save() + Ocurrence(animal=animal, animal_passthrough=animal).save() + p = Ocurrence.objects.get() + self.assertIsInstance(p.animal, LazyReference) + with self.assertRaises(KeyError): + p.animal['name'] + with self.assertRaises(AttributeError): + p.animal.name + self.assertEqual(p.animal.pk, animal.pk) + + self.assertEqual(p.animal_passthrough.name, "Leopard") + self.assertEqual(p.animal_passthrough['name'], "Leopard") + + # Should not be able to access referenced document's methods + with self.assertRaises(AttributeError): + p.animal.save + with self.assertRaises(KeyError): + p.animal['save'] + + def test_lazy_reference_not_set(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + Ocurrence(person='foo').save() + p = Ocurrence.objects.get() + self.assertIs(p.animal, None) + + def test_lazy_reference_equality(self): + class Animal(Document): + name = StringField() + tag = StringField() + + Animal.drop_collection() + + animal = Animal(name="Leopard", tag="heavy").save() + animalref = LazyReference(Animal, animal.pk) + self.assertEqual(animal, animalref) + self.assertEqual(animalref, animal) + + other_animalref = LazyReference(Animal, ObjectId("54495ad94c934721ede76f90")) + self.assertNotEqual(animal, other_animalref) + self.assertNotEqual(other_animalref, animal) + + if __name__ == '__main__': unittest.main() From 35d04582280c189ce7433864686fda20ae859b02 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 6 Nov 2017 12:17:31 +0100 Subject: [PATCH 137/268] Add GenericLazyReferenceField --- docs/changelog.rst | 2 +- mongoengine/fields.py | 73 ++++++++++++++- tests/fields/fields.py | 198 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 261 insertions(+), 12 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 834fbee2..1e9ac7fc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,7 @@ Changelog Changes in 0.15.0 ================= -- Add LazyReferenceField to address #1230 +- Add LazyReferenceField and GenericLazyReferenceField to address #1230 Changes in 0.14.1 ================= diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 73a62bc5..61e0cb69 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -47,8 +47,7 @@ __all__ = ( 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', 'SortedListField', 'EmbeddedDocumentListField', 'DictField', 'MapField', 'ReferenceField', 'CachedReferenceField', - 'LazyReferenceField', - # 'GenericLazyReferenceField', + 'LazyReferenceField', 'GenericLazyReferenceField', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', @@ -1275,6 +1274,12 @@ class GenericReferenceField(BaseField): """A reference to *any* :class:`~mongoengine.document.Document` subclass that will be automatically dereferenced on access (lazily). + Note this field works the same way as :class:`~mongoengine.document.ReferenceField`, + doing database I/O access the first time it is accessed (even if it's to access + it ``pk`` or ``id`` field). + To solve this you should consider using the + :class:`~mongoengine.fields.GenericLazyReferenceField`. + .. note :: * Any documents used as a generic reference must be registered in the document registry. Importing the model will automatically register @@ -2159,6 +2164,8 @@ class LazyReferenceField(BaseField): """A really lazy reference to a document. Unlike the :class:`~mongoengine.fields.ReferenceField` it must be manually dereferenced using it ``fetch()`` method. + + .. versionadded:: 0.15 """ def __init__(self, document_type, passthrough=False, dbref=False, @@ -2274,3 +2281,65 @@ class LazyReferenceField(BaseField): def lookup_member(self, member_name): return self.document_type._fields.get(member_name) + + +class GenericLazyReferenceField(GenericReferenceField): + """A reference to *any* :class:`~mongoengine.document.Document` subclass + that will be automatically dereferenced on access (lazily). + Unlike the :class:`~mongoengine.fields.GenericReferenceField` it must be + manually dereferenced using it ``fetch()`` method. + + .. note :: + * Any documents used as a generic reference must be registered in the + document registry. Importing the model will automatically register + it. + + * You can use the choices param to limit the acceptable Document types + + .. versionadded:: 0.15 + """ + + def __init__(self, *args, **kwargs): + self.passthrough = kwargs.pop('passthrough', False) + super(GenericLazyReferenceField, self).__init__(*args, **kwargs) + + def _validate_choices(self, value): + if isinstance(value, LazyReference): + value = value.document_type + super(GenericLazyReferenceField, self)._validate_choices(value) + + def __get__(self, instance, owner): + if instance is None: + return self + + value = instance._data.get(self.name) + if isinstance(value, LazyReference): + if value.passthrough != self.passthrough: + instance._data[self.name] = LazyReference( + value.document_type, value.pk, passthrough=self.passthrough) + elif value is not None: + if isinstance(value, (dict, SON)): + value = LazyReference(get_document(value['_cls']), value['_ref'].id, passthrough=self.passthrough) + elif isinstance(value, Document): + value = LazyReference(type(value), value.pk, passthrough=self.passthrough) + instance._data[self.name] = value + + return super(GenericLazyReferenceField, self).__get__(instance, owner) + + def validate(self, value): + if isinstance(value, LazyReference) and value.pk is None: + self.error('You can only reference documents once they have been' + ' saved to the database') + return super(GenericLazyReferenceField, self).validate(value) + + def to_mongo(self, document): + if document is None: + return None + + if isinstance(document, LazyReference): + return SON(( + ('_cls', document.document_type._class_name), + ('_ref', document) + )) + else: + return super(GenericLazyReferenceField, self).to_mongo(document) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 84156622..632f5404 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -933,7 +933,7 @@ class FieldTest(MongoDBTestCase): authors = ListField(ReferenceField(User)) authors_as_lazy = ListField(LazyReferenceField(User)) generic = ListField(GenericReferenceField()) - # generic_as_lazy = ListField(LazyGenericReferenceField()) + generic_as_lazy = ListField(GenericLazyReferenceField()) User.drop_collection() BlogPost.drop_collection() @@ -992,17 +992,17 @@ class FieldTest(MongoDBTestCase): post.generic = [user] post.validate() - # post.generic_as_lazy = [1, 2] - # self.assertRaises(ValidationError, post.validate) + post.generic_as_lazy = [1, 2] + self.assertRaises(ValidationError, post.validate) - # post.generic_as_lazy = [User(), Comment()] - # self.assertRaises(ValidationError, post.validate) + post.generic_as_lazy = [User(), Comment()] + self.assertRaises(ValidationError, post.validate) - # post.generic_as_lazy = [Comment()] - # self.assertRaises(ValidationError, post.validate) + post.generic_as_lazy = [Comment()] + self.assertRaises(ValidationError, post.validate) - # post.generic_as_lazy = [user] - # post.validate() + post.generic_as_lazy = [user] + post.validate() def test_sorted_list_sorting(self): """Ensure that a sorted list field properly sorts values. @@ -4872,5 +4872,185 @@ class LazyReferenceFieldTest(MongoDBTestCase): self.assertNotEqual(other_animalref, animal) +class GenericLazyReferenceFieldTest(MongoDBTestCase): + def test_generic_lazy_reference_simple(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = GenericLazyReferenceField() + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal(name="Leopard", tag="heavy").save() + Ocurrence(person="test", animal=animal).save() + p = Ocurrence.objects.get() + self.assertIsInstance(p.animal, LazyReference) + fetched_animal = p.animal.fetch() + self.assertEqual(fetched_animal, animal) + # `fetch` keep cache on referenced document by default... + animal.tag = "not so heavy" + animal.save() + double_fetch = p.animal.fetch() + self.assertIs(fetched_animal, double_fetch) + self.assertEqual(double_fetch.tag, "heavy") + # ...unless specified otherwise + fetch_force = p.animal.fetch(force=True) + self.assertIsNot(fetch_force, fetched_animal) + self.assertEqual(fetch_force.tag, "not so heavy") + + def test_generic_lazy_reference_choices(self): + class Animal(Document): + name = StringField() + + class Vegetal(Document): + name = StringField() + + class Mineral(Document): + name = StringField() + + class Ocurrence(Document): + living_thing = GenericLazyReferenceField(choices=[Animal, Vegetal]) + thing = GenericLazyReferenceField() + + Animal.drop_collection() + Vegetal.drop_collection() + Mineral.drop_collection() + Ocurrence.drop_collection() + + animal = Animal(name="Leopard").save() + vegetal = Vegetal(name="Oak").save() + mineral = Mineral(name="Granite").save() + + occ_animal = Ocurrence(living_thing=animal, thing=animal).save() + occ_vegetal = Ocurrence(living_thing=vegetal, thing=vegetal).save() + with self.assertRaises(ValidationError): + Ocurrence(living_thing=mineral).save() + + occ = Ocurrence.objects.get(living_thing=animal) + self.assertEqual(occ, occ_animal) + self.assertIsInstance(occ.thing, LazyReference) + self.assertIsInstance(occ.living_thing, LazyReference) + + occ.thing = vegetal + occ.living_thing = vegetal + occ.save() + + occ.thing = mineral + occ.living_thing = mineral + with self.assertRaises(ValidationError): + occ.save() + + def test_generic_lazy_reference_set(self): + class Animal(Document): + meta = {'allow_inheritance': True} + + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = GenericLazyReferenceField() + + Animal.drop_collection() + Ocurrence.drop_collection() + + class SubAnimal(Animal): + nick = StringField() + + animal = Animal(name="Leopard", tag="heavy").save() + sub_animal = SubAnimal(nick='doggo', name='dog').save() + for ref in ( + animal, + LazyReference(Animal, animal.pk), + {'_cls': 'Animal', '_ref': DBRef(animal._get_collection_name(), animal.pk)}, + + sub_animal, + LazyReference(SubAnimal, sub_animal.pk), + {'_cls': 'SubAnimal', '_ref': DBRef(sub_animal._get_collection_name(), sub_animal.pk)}, + ): + p = Ocurrence(person="test", animal=ref).save() + p.reload() + self.assertIsInstance(p.animal, (LazyReference, Document)) + p.animal.fetch() + + def test_generic_lazy_reference_bad_set(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = GenericLazyReferenceField(choices=['Animal']) + + Animal.drop_collection() + Ocurrence.drop_collection() + + class BadDoc(Document): + pass + + animal = Animal(name="Leopard", tag="heavy").save() + baddoc = BadDoc().save() + for bad in ( + 42, + 'foo', + baddoc, + LazyReference(BadDoc, animal.pk) + ): + with self.assertRaises(ValidationError): + p = Ocurrence(person="test", animal=bad).save() + + def test_generic_lazy_reference_query_conversion(self): + class Member(Document): + user_num = IntField(primary_key=True) + + class BlogPost(Document): + title = StringField() + author = GenericLazyReferenceField() + + Member.drop_collection() + BlogPost.drop_collection() + + m1 = Member(user_num=1) + m1.save() + m2 = Member(user_num=2) + m2.save() + + post1 = BlogPost(title='post 1', author=m1) + post1.save() + + post2 = BlogPost(title='post 2', author=m2) + post2.save() + + post = BlogPost.objects(author=m1).first() + self.assertEqual(post.id, post1.id) + + post = BlogPost.objects(author=m2).first() + self.assertEqual(post.id, post2.id) + + # Same thing by passing a LazyReference instance + post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() + self.assertEqual(post.id, post2.id) + + def test_generic_lazy_reference_not_set(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = GenericLazyReferenceField() + + Animal.drop_collection() + Ocurrence.drop_collection() + + Ocurrence(person='foo').save() + p = Ocurrence.objects.get() + self.assertIs(p.animal, None) + + if __name__ == '__main__': unittest.main() From da33cb54fe3e80490e55957da914fec6bbde92ba Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 6 Nov 2017 14:11:11 +0100 Subject: [PATCH 138/268] Correct style --- mongoengine/base/datastructures.py | 2 +- mongoengine/fields.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 043df471..43f32810 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -1,8 +1,8 @@ import itertools import weakref -import six from bson import DBRef +import six from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 61e0cb69..62d9b941 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -26,8 +26,8 @@ except ImportError: Int64 = long from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, - GeoJsonBaseField, ObjectIdField, get_document, - LazyReference) + GeoJsonBaseField, LazyReference, ObjectIdField, + get_document) from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError @@ -2265,9 +2265,10 @@ class LazyReferenceField(BaseField): try: id_field.validate(pk) except ValidationError: - self.error("value should be `{0}` document, LazyReference or DBRef on `{0}` " - "or `{0}`'s primary key (i.e. `{1}`)".format( - self.document_type.__name__, type(id_field).__name__)) + self.error( + "value should be `{0}` document, LazyReference or DBRef on `{0}` " + "or `{0}`'s primary key (i.e. `{1}`)".format( + self.document_type.__name__, type(id_field).__name__)) if pk is None: self.error('You can only reference documents once they have been ' From ea25972257fd75ddf6a5cee674e1b9ed67f80ad7 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 6 Nov 2017 14:39:10 +0100 Subject: [PATCH 139/268] Bump version to 0.15.0 --- mongoengine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index c7c6f707..a1b7d682 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 14, 3) +VERSION = (0, 15, 0) def get_version(): From 4f5b0634ad944534012a7752f26dfbe541160a2c Mon Sep 17 00:00:00 2001 From: Ryan Scott Date: Tue, 7 Nov 2017 16:26:01 -0500 Subject: [PATCH 140/268] Add documentation for auto_create_index to the Indexing section of the Docs --- docs/guide/defining-documents.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d41ae7e6..5ff7a56b 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -526,8 +526,9 @@ There are a few top level defaults for all indexes that can be set:: meta = { 'index_options': {}, 'index_background': True, + 'index_cls': False, + 'auto_create_index': True, 'index_drop_dups': True, - 'index_cls': False } @@ -540,6 +541,12 @@ There are a few top level defaults for all indexes that can be set:: :attr:`index_cls` (Optional) A way to turn off a specific index for _cls. +:attr:`auto_create_index` (Optional) + When this is True (default), MongoEngine will ensure that the correct + indexes exist in MongoDB each time a command is run. This can be disabled + in systems where indexes are managed separately. Disabling this will improve + performance. + :attr:`index_drop_dups` (Optional) Set the default value for if an index should drop duplicates From 6b38ef3c9f0a69420fe4305dda226a0faf9f0528 Mon Sep 17 00:00:00 2001 From: kcarretto Date: Sat, 11 Nov 2017 03:36:28 -0500 Subject: [PATCH 141/268] Fixed format string issue in mongoengine error message. --- mongoengine/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 62d9b941..8692e3a7 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1108,7 +1108,7 @@ class ReferenceField(BaseField): if self.document_type._meta.get('abstract') and \ not isinstance(value, self.document_type): self.error( - '%s is not an instance of abstract reference type %s' % ( + '%s is not an instance of abstract reference type %s' % (value, self.document_type._class_name) ) From 47c7cb932741098e264dc9000e1ddf4fe3ed76fb Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 15 Nov 2017 20:44:36 +0100 Subject: [PATCH 142/268] Ignore style I202 rule (see https://github.com/PyCQA/flake8-import-order/issues/122) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index eabe3271..46edff3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ tests=tests cover-package=mongoengine [flake8] -ignore=E501,F401,F403,F405,I201 +ignore=E501,F401,F403,F405,I201,I202 exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests max-complexity=47 application-import-names=mongoengine,tests From c1c09fa6b4c832b99afeea83d19d290468689fe4 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Tue, 21 Nov 2017 21:56:26 +0800 Subject: [PATCH 143/268] fix validatione error for invalid embedded document instance #1067 --- mongoengine/fields.py | 6 +++++- tests/queryset/queryset.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 6c4a06c9..0aa51b2d 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -688,6 +688,11 @@ class GenericEmbeddedDocumentField(BaseField): return value def validate(self, value, clean=True): + if self.choices and isinstance(value, SON): + for choice in self.choices: + if value['_cls'] == choice._class_name: + return True + if not isinstance(value, EmbeddedDocument): self.error('Invalid embedded document instance provided to an ' 'GenericEmbeddedDocumentField') @@ -705,7 +710,6 @@ class GenericEmbeddedDocumentField(BaseField): def to_mongo(self, document, use_db_field=True, fields=None): if document is None: return None - data = document.to_mongo(use_db_field, fields) if '_cls' not in data: data['_cls'] = document._class_name diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 28e84af4..43800fff 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2086,6 +2086,23 @@ class QuerySetTest(unittest.TestCase): Site.objects(id=s.id).update_one( pull_all__collaborators__helpful__user=['Ross']) + def test_pull_in_genericembedded_field(self): + + class Foo(EmbeddedDocument): + name = StringField() + + class Bar(Document): + foos = ListField(GenericEmbeddedDocumentField( + choices=[Foo, ])) + + Bar.drop_collection() + + foo = Foo(name="bar") + bar = Bar(foos=[foo]).save() + Bar.objects(id=bar.id).update(pull__foos=foo) + bar.reload() + self.assertEqual(len(bar.foos), 0) + def test_update_one_pop_generic_reference(self): class BlogTag(Document): @@ -2179,6 +2196,24 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(message.authors[1].name, "Ross") self.assertEqual(message.authors[2].name, "Adam") + def test_set_generic_embedded_documents(self): + + class Bar(EmbeddedDocument): + name = StringField() + + class User(Document): + username = StringField() + bar = GenericEmbeddedDocumentField(choices=[Bar,]) + + User.drop_collection() + + User(username='abc').save() + User.objects(username='abc').update( + set__bar=Bar(name='test'), upsert=True) + + user = User.objects(username='abc').first() + self.assertEqual(user.bar.name, "test") + def test_reload_embedded_docs_instance(self): class SubDoc(EmbeddedDocument): From e74f65901565f64b39a51edc22a1564d8f9fed9a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 8 Nov 2017 18:05:30 +0100 Subject: [PATCH 144/268] Improve LazyReferenceField and GenericLazyReferenceField with nested fields --- mongoengine/base/document.py | 3 +- mongoengine/dereference.py | 9 +++- mongoengine/fields.py | 56 +++++++++++++++-------- setup.cfg | 2 +- tests/fields/fields.py | 86 ++++++++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 22 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index f8ab73d0..658d0c79 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -13,6 +13,7 @@ from mongoengine import signals from mongoengine.base.common import get_document from mongoengine.base.datastructures import (BaseDict, BaseList, EmbeddedDocumentList, + LazyReference, StrictDict) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class @@ -488,7 +489,7 @@ class BaseDocument(object): else: data = getattr(data, part, None) - if hasattr(data, '_changed_fields'): + if not isinstance(data, LazyReference) and hasattr(data, '_changed_fields'): if getattr(data, '_is_document', False): continue diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 59204d4d..7fe34e43 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -3,6 +3,7 @@ import six from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList, TopLevelDocumentMetaclass, get_document) +from mongoengine.base.datastructures import LazyReference from mongoengine.connection import get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.fields import DictField, ListField, MapField, ReferenceField @@ -99,7 +100,10 @@ class DeReference(object): if isinstance(item, (Document, EmbeddedDocument)): for field_name, field in item._fields.iteritems(): v = item._data.get(field_name, None) - if isinstance(v, DBRef): + if isinstance(v, LazyReference): + # LazyReference inherits DBRef but should not be dereferenced here ! + continue + elif isinstance(v, DBRef): reference_map.setdefault(field.document_type, set()).add(v.id) elif isinstance(v, (dict, SON)) and '_ref' in v: reference_map.setdefault(get_document(v['_cls']), set()).add(v['_ref'].id) @@ -110,6 +114,9 @@ class DeReference(object): if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)): key = field_cls reference_map.setdefault(key, set()).update(refs) + elif isinstance(item, LazyReference): + # LazyReference inherits DBRef but should not be dereferenced here ! + continue elif isinstance(item, DBRef): reference_map.setdefault(item.collection, set()).add(item.id) elif isinstance(item, (dict, SON)) and '_ref' in item: diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 6c4a06c9..8ca2b17f 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -28,6 +28,7 @@ except ImportError: from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField, LazyReference, ObjectIdField, get_document) +from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError @@ -789,6 +790,17 @@ class ListField(ComplexBaseField): kwargs.setdefault('default', lambda: []) super(ListField, self).__init__(**kwargs) + def __get__(self, instance, owner): + if instance is None: + # Document class being used rather than a document object + return self + value = instance._data.get(self.name) + LazyReferenceField = _import_class('LazyReferenceField') + GenericLazyReferenceField = _import_class('GenericLazyReferenceField') + if isinstance(self.field, (LazyReferenceField, GenericLazyReferenceField)) and value: + instance._data[self.name] = [self.field.build_lazyref(x) for x in value] + return super(ListField, self).__get__(instance, owner) + def validate(self, value): """Make sure that a list of valid fields is being used.""" if (not isinstance(value, (list, tuple, QuerySet)) or @@ -2211,17 +2223,10 @@ class LazyReferenceField(BaseField): self.document_type_obj = get_document(self.document_type_obj) return self.document_type_obj - def __get__(self, instance, owner): - """Descriptor to allow lazy dereferencing.""" - if instance is None: - # Document class being used rather than a document object - return self - - value = instance._data.get(self.name) + def build_lazyref(self, value): if isinstance(value, LazyReference): if value.passthrough != self.passthrough: - instance._data[self.name] = LazyReference( - value.document_type, value.pk, passthrough=self.passthrough) + value = LazyReference(value.document_type, value.pk, passthrough=self.passthrough) elif value is not None: if isinstance(value, self.document_type): value = LazyReference(self.document_type, value.pk, passthrough=self.passthrough) @@ -2230,6 +2235,16 @@ class LazyReferenceField(BaseField): else: # value is the primary key of the referenced document value = LazyReference(self.document_type, value, passthrough=self.passthrough) + return value + + def __get__(self, instance, owner): + """Descriptor to allow lazy dereferencing.""" + if instance is None: + # Document class being used rather than a document object + return self + + value = self.build_lazyref(instance._data.get(self.name)) + if value: instance._data[self.name] = value return super(LazyReferenceField, self).__get__(instance, owner) @@ -2254,7 +2269,7 @@ class LazyReferenceField(BaseField): def validate(self, value): if isinstance(value, LazyReference): - if not issubclass(value.document_type, self.document_type): + if value.collection != self.document_type._get_collection_name(): self.error('Reference must be on a `%s` document.' % self.document_type) pk = value.pk elif isinstance(value, self.document_type): @@ -2314,23 +2329,26 @@ class GenericLazyReferenceField(GenericReferenceField): def _validate_choices(self, value): if isinstance(value, LazyReference): - value = value.document_type + value = value.document_type._class_name super(GenericLazyReferenceField, self)._validate_choices(value) - def __get__(self, instance, owner): - if instance is None: - return self - - value = instance._data.get(self.name) + def build_lazyref(self, value): if isinstance(value, LazyReference): if value.passthrough != self.passthrough: - instance._data[self.name] = LazyReference( - value.document_type, value.pk, passthrough=self.passthrough) + value = LazyReference(value.document_type, value.pk, passthrough=self.passthrough) elif value is not None: if isinstance(value, (dict, SON)): value = LazyReference(get_document(value['_cls']), value['_ref'].id, passthrough=self.passthrough) elif isinstance(value, Document): value = LazyReference(type(value), value.pk, passthrough=self.passthrough) + return value + + def __get__(self, instance, owner): + if instance is None: + return self + + value = self.build_lazyref(instance._data.get(self.name)) + if value: instance._data[self.name] = value return super(GenericLazyReferenceField, self).__get__(instance, owner) @@ -2348,7 +2366,7 @@ class GenericLazyReferenceField(GenericReferenceField): if isinstance(document, LazyReference): return SON(( ('_cls', document.document_type._class_name), - ('_ref', document) + ('_ref', DBRef(document.document_type._get_collection_name(), document.pk)) )) else: return super(GenericLazyReferenceField, self).to_mongo(document) diff --git a/setup.cfg b/setup.cfg index 46edff3b..fd6192b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [nosetests] verbosity=2 detailed-errors=1 -tests=tests +#tests=tests cover-package=mongoengine [flake8] diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 632f5404..ffee25e6 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4871,6 +4871,48 @@ class LazyReferenceFieldTest(MongoDBTestCase): self.assertNotEqual(animal, other_animalref) self.assertNotEqual(other_animalref, animal) + def test_lazy_reference_embedded(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class EmbeddedOcurrence(EmbeddedDocument): + in_list = ListField(LazyReferenceField(Animal)) + direct = LazyReferenceField(Animal) + + class Ocurrence(Document): + in_list = ListField(LazyReferenceField(Animal)) + in_embedded = EmbeddedDocumentField(EmbeddedOcurrence) + direct = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal1 = Animal('doggo').save() + animal2 = Animal('cheeta').save() + + def check_fields_type(occ): + self.assertIsInstance(occ.direct, LazyReference) + for elem in occ.in_list: + self.assertIsInstance(elem, LazyReference) + self.assertIsInstance(occ.in_embedded.direct, LazyReference) + for elem in occ.in_embedded.in_list: + self.assertIsInstance(elem, LazyReference) + + occ = Ocurrence( + in_list=[animal1, animal2], + in_embedded={'in_list': [animal1, animal2], 'direct': animal1}, + direct=animal1 + ).save() + check_fields_type(occ) + occ.reload() + check_fields_type(occ) + occ.direct = animal1.id + occ.in_list = [animal1.id, animal2.id] + occ.in_embedded.direct = animal1.id + occ.in_embedded.in_list = [animal1.id, animal2.id] + check_fields_type(occ) + class GenericLazyReferenceFieldTest(MongoDBTestCase): def test_generic_lazy_reference_simple(self): @@ -5051,6 +5093,50 @@ class GenericLazyReferenceFieldTest(MongoDBTestCase): p = Ocurrence.objects.get() self.assertIs(p.animal, None) + def test_generic_lazy_reference_embedded(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class EmbeddedOcurrence(EmbeddedDocument): + in_list = ListField(GenericLazyReferenceField()) + direct = GenericLazyReferenceField() + + class Ocurrence(Document): + in_list = ListField(GenericLazyReferenceField()) + in_embedded = EmbeddedDocumentField(EmbeddedOcurrence) + direct = GenericLazyReferenceField() + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal1 = Animal('doggo').save() + animal2 = Animal('cheeta').save() + + def check_fields_type(occ): + self.assertIsInstance(occ.direct, LazyReference) + for elem in occ.in_list: + self.assertIsInstance(elem, LazyReference) + self.assertIsInstance(occ.in_embedded.direct, LazyReference) + for elem in occ.in_embedded.in_list: + self.assertIsInstance(elem, LazyReference) + + occ = Ocurrence( + in_list=[animal1, animal2], + in_embedded={'in_list': [animal1, animal2], 'direct': animal1}, + direct=animal1 + ).save() + check_fields_type(occ) + occ.reload() + check_fields_type(occ) + animal1_ref = {'_cls': 'Animal', '_ref': DBRef(animal1._get_collection_name(), animal1.pk)} + animal2_ref = {'_cls': 'Animal', '_ref': DBRef(animal2._get_collection_name(), animal2.pk)} + occ.direct = animal1_ref + occ.in_list = [animal1_ref, animal2_ref] + occ.in_embedded.direct = animal1_ref + occ.in_embedded.in_list = [animal1_ref, animal2_ref] + check_fields_type(occ) + if __name__ == '__main__': unittest.main() From 60758dd76b4ae1831e4f3f389c71b95b0e77abd2 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 18:56:12 +0800 Subject: [PATCH 145/268] add changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e9ac7fc..b4852178 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,7 @@ Changelog Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 +- Fix validation error for invalid embedded document instance #1067 Changes in 0.14.1 ================= From 79486e33936d6302e1cb898c38a9535648563343 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 19:27:35 +0800 Subject: [PATCH 146/268] change description in changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b4852178..249d4659 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,7 @@ Changelog Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 -- Fix validation error for invalid embedded document instance #1067 +- Fix validation error instance in GenericEmbeddedDocumentField #1067 Changes in 0.14.1 ================= From 0ce081323fd66ad00b00eb4bc9edc6b888a5fa2d Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 22:19:50 +0800 Subject: [PATCH 147/268] move changes to development --- docs/changelog.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 249d4659..f96fc11b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,10 +2,13 @@ Changelog ========= +Development +=========== +- Fix validation error instance in GenericEmbeddedDocumentField #1067 + Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 -- Fix validation error instance in GenericEmbeddedDocumentField #1067 Changes in 0.14.1 ================= From c45dfacb411a062dc75acd27e784693fb6c0898a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 29 Nov 2017 16:41:42 +0100 Subject: [PATCH 148/268] Update changelog --- docs/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1e9ac7fc..151fdd08 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,13 @@ Changelog ========= +dev +=== +- 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 + + Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 From b35efb9f7205ab9b07d5b21c5cdb26d4c6d07201 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Tue, 21 Nov 2017 21:56:26 +0800 Subject: [PATCH 149/268] fix validatione error for invalid embedded document instance #1067 --- mongoengine/fields.py | 6 +++++- tests/queryset/queryset.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 8ca2b17f..4e941c4e 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -689,6 +689,11 @@ class GenericEmbeddedDocumentField(BaseField): return value def validate(self, value, clean=True): + if self.choices and isinstance(value, SON): + for choice in self.choices: + if value['_cls'] == choice._class_name: + return True + if not isinstance(value, EmbeddedDocument): self.error('Invalid embedded document instance provided to an ' 'GenericEmbeddedDocumentField') @@ -706,7 +711,6 @@ class GenericEmbeddedDocumentField(BaseField): def to_mongo(self, document, use_db_field=True, fields=None): if document is None: return None - data = document.to_mongo(use_db_field, fields) if '_cls' not in data: data['_cls'] = document._class_name diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 28e84af4..43800fff 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2086,6 +2086,23 @@ class QuerySetTest(unittest.TestCase): Site.objects(id=s.id).update_one( pull_all__collaborators__helpful__user=['Ross']) + def test_pull_in_genericembedded_field(self): + + class Foo(EmbeddedDocument): + name = StringField() + + class Bar(Document): + foos = ListField(GenericEmbeddedDocumentField( + choices=[Foo, ])) + + Bar.drop_collection() + + foo = Foo(name="bar") + bar = Bar(foos=[foo]).save() + Bar.objects(id=bar.id).update(pull__foos=foo) + bar.reload() + self.assertEqual(len(bar.foos), 0) + def test_update_one_pop_generic_reference(self): class BlogTag(Document): @@ -2179,6 +2196,24 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(message.authors[1].name, "Ross") self.assertEqual(message.authors[2].name, "Adam") + def test_set_generic_embedded_documents(self): + + class Bar(EmbeddedDocument): + name = StringField() + + class User(Document): + username = StringField() + bar = GenericEmbeddedDocumentField(choices=[Bar,]) + + User.drop_collection() + + User(username='abc').save() + User.objects(username='abc').update( + set__bar=Bar(name='test'), upsert=True) + + user = User.objects(username='abc').first() + self.assertEqual(user.bar.name, "test") + def test_reload_embedded_docs_instance(self): class SubDoc(EmbeddedDocument): From ce9ea7baad2870145203a51e069f6a6f57526518 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 18:56:12 +0800 Subject: [PATCH 150/268] add changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 151fdd08..69e57e0b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ dev Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 +- Fix validation error for invalid embedded document instance #1067 Changes in 0.14.1 ================= From 08a4deca17b511974125e26a7151e165231b58de Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 19:27:35 +0800 Subject: [PATCH 151/268] change description in changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 69e57e0b..c44f8432 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,7 +12,7 @@ dev Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 -- Fix validation error for invalid embedded document instance #1067 +- Fix validation error instance in GenericEmbeddedDocumentField #1067 Changes in 0.14.1 ================= From 2cbebf9c99dd5ebd368ca775b60fc25149ea5634 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 22 Nov 2017 22:19:50 +0800 Subject: [PATCH 152/268] move changes to development --- docs/changelog.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c44f8432..efd2e0f6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,12 +7,11 @@ dev - 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 - +- Fix validation error instance in GenericEmbeddedDocumentField #1067 Changes in 0.15.0 ================= - Add LazyReferenceField and GenericLazyReferenceField to address #1230 -- Fix validation error instance in GenericEmbeddedDocumentField #1067 Changes in 0.14.1 ================= From 9e0ca51c2f47618b539c2724ecfe656e454eb062 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Fri, 1 Dec 2017 08:40:51 +0800 Subject: [PATCH 153/268] remove merge conflict after rebase #1067 --- docs/changelog.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1bd932d6..efd2e0f6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,7 +2,6 @@ Changelog ========= -<<<<<<< HEAD dev === - Subfield resolve error in generic_emdedded_document query #1651 #1652 From 7674dc9b344f99e47a6df3b1c44d72faf57ff137 Mon Sep 17 00:00:00 2001 From: Esmail Date: Tue, 5 Dec 2017 15:14:15 -0500 Subject: [PATCH 154/268] One-character typo fix ("that" -> "than") --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index cc5b647d..ea1a04c1 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -153,7 +153,7 @@ post. This works, but there is no real reason to be storing the comments separately from their associated posts, other than to work around the relational model. Using MongoDB we can store the comments as a list of *embedded documents* directly on a post document. An embedded document should -be treated no differently that a regular document; it just doesn't have its own +be treated no differently than a regular document; it just doesn't have its own collection in the database. Using MongoEngine, we can define the structure of embedded documents, along with utility methods, in exactly the same way we do with regular documents:: From 22a8ad2fde14b0c5eb2cf3cbc0fd4e0de98d67c5 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:17:54 +0800 Subject: [PATCH 155/268] update fields argument when given #1172 --- mongoengine/fields.py | 6 +++++- tests/fields/fields.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 8ca2b17f..17003974 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1256,7 +1256,11 @@ class CachedReferenceField(BaseField): if value.pk is None: self.error('You can only reference documents once they have' ' been saved to the database') - return {'_id': value.pk} + value_dict = {'_id': value.pk} + for field in self.fields: + value_dict.update({field: value[field]}) + + return value_dict raise NotImplementedError diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ffee25e6..f86ffdb4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4379,6 +4379,51 @@ class CachedReferenceFieldTest(MongoDBTestCase): self.assertEqual(SocialData.objects(person__group=g2).count(), 1) self.assertEqual(SocialData.objects(person__group=g2).first(), s2) + def test_cached_reference_field_push_with_fields(self): + class Product(Document): + name = StringField() + + Product.drop_collection() + + class Basket(Document): + products = ListField(CachedReferenceField(Product, fields=['name'])) + + Basket.drop_collection() + product1 = Product(name='abc').save() + product2 = Product(name='def').save() + basket = Basket(products=[product1]).save() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + } + ] + } + ) + # push to list + basket.update(push__products=product2) + basket.reload() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + }, + { + '_id': product2.pk, + 'name': product2.name + } + ] + } + ) + def test_cached_reference_field_update_all(self): class Person(Document): TYPES = ( From 6621c318db9fc5d3395d9ceb9d0900cc0fef0e68 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:28:33 +0800 Subject: [PATCH 156/268] add changelog #1712 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 151fdd08..7b937822 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ dev - 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 +- Update cached fields when fields argument is given #1712 Changes in 0.15.0 From 18a5fba42b2af5e0835293b1e7201747100014c9 Mon Sep 17 00:00:00 2001 From: erdenezul Date: Fri, 22 Dec 2017 20:19:21 +0800 Subject: [PATCH 157/268] Revert "add tests to increase code coverage" --- tests/fields/fields.py | 36 +----------------------------------- tests/queryset/geo.py | 4 ---- tests/queryset/queryset.py | 10 ---------- 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 4360f298..ffee25e6 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -312,27 +312,6 @@ class FieldTest(MongoDBTestCase): self.assertEqual(1, TestDocument.objects(long_fld__ne=None).count()) - def test_callable_validation(self): - """Ensure that callable validation works""" - def check_even(value): - return value % 2 == 0 - - class Order(Document): - number = IntField(validation=check_even) - - Order.drop_collection() - - order = Order(number=3) - self.assertRaises(ValidationError, order.validate) - - class User(Document): - name = StringField(validation=1) - - User.drop_collection() - - user = User(name='test') - self.assertRaises(ValidationError, user.validate) - def test_object_id_validation(self): """Ensure that invalid values cannot be assigned to an ObjectIdField. @@ -355,7 +334,7 @@ class FieldTest(MongoDBTestCase): def test_string_validation(self): """Ensure that invalid values cannot be assigned to string fields.""" class Person(Document): - name = StringField(max_length=20, min_length=5) + name = StringField(max_length=20) userid = StringField(r'[0-9a-z_]+$') person = Person(name=34) @@ -373,10 +352,6 @@ class FieldTest(MongoDBTestCase): person = Person(name='Name that is more than twenty characters') self.assertRaises(ValidationError, person.validate) - # Test max length validation on name - person = Person(name='aa') - self.assertRaises(ValidationError, person.validate) - person.name = 'Shorter name' person.validate() @@ -462,10 +437,6 @@ class FieldTest(MongoDBTestCase): doc.age = 'ten' self.assertRaises(ValidationError, doc.validate) - # Test max_value validation - doc.value = 200 - self.assertRaises(ValidationError, doc.validate) - def test_float_validation(self): """Ensure that invalid values cannot be assigned to float fields. """ @@ -548,11 +519,6 @@ class FieldTest(MongoDBTestCase): class User(Document): name = StringField(db_field='name\0') - # db field should be a string - with self.assertRaises(TypeError): - class User(Document): - name = StringField(db_field=1) - def test_decimal_comparison(self): class Person(Document): money = DecimalField() diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index acfd9364..38c0377e 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -429,10 +429,6 @@ class GeoQueriesTest(MongoDBTestCase): roads = Road.objects.filter(line__geo_within=polygon).count() self.assertEqual(1, roads) - sphere = [[-1, 42,], 2] - roads = Road.objects.filter(line__geo_within_sphere=sphere).count() - self.assertEqual(0, roads) - roads = Road.objects.filter(line__geo_within={"$geometry": polygon}).count() self.assertEqual(1, roads) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 984314ed..43800fff 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1861,10 +1861,6 @@ class QuerySetTest(unittest.TestCase): post = BlogPost(name="Test Post", hits=5, tags=['test']) post.save() - BlogPost.objects.update(hits=11) - post.reload() - self.assertEqual(post.hits, 11) - BlogPost.objects.update(set__hits=10) post.reload() self.assertEqual(post.hits, 10) @@ -1886,12 +1882,6 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertTrue('mongo' in post.tags) - # Push with arrays - BlogPost.objects.update(push__tags=['python', 'scala']) - post.reload() - self.assertTrue('python' in post.tags) - self.assertTrue('scala' in post.tags) - BlogPost.objects.update_one(push_all__tags=['db', 'nosql']) post.reload() self.assertTrue('db' in post.tags and 'nosql' in post.tags) From 12b846586c4e72d712920d32e595146911c4f9e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 22 Dec 2017 13:23:03 +0100 Subject: [PATCH 158/268] Fix travis tests with mongodb 2.4 & pymongo 3 --- .travis.yml | 14 +++++++------- tox.ini | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 78a9f787..a70c711e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,7 +21,7 @@ python: env: - MONGODB=2.6 PYMONGO=2.7 - MONGODB=2.6 PYMONGO=2.8 -- MONGODB=2.6 PYMONGO=3.0 +- MONGODB=2.6 PYMONGO=3.x matrix: # Finish the build as soon as one job fails @@ -31,19 +31,19 @@ matrix: - python: 2.7 env: MONGODB=2.4 PYMONGO=2.7 - python: 2.7 - env: MONGODB=2.4 PYMONGO=3.0 + env: MONGODB=2.4 PYMONGO=3.5 - python: 2.7 - env: MONGODB=3.0 PYMONGO=3.0 + env: MONGODB=3.0 PYMONGO=3.x - python: 3.5 env: MONGODB=2.4 PYMONGO=2.7 - python: 3.5 - env: MONGODB=2.4 PYMONGO=3.0 + env: MONGODB=2.4 PYMONGO=3.5 - python: 3.5 - env: MONGODB=3.0 PYMONGO=3.0 + env: MONGODB=3.0 PYMONGO=3.x - python: 3.6 - env: MONGODB=2.4 PYMONGO=3.0 + env: MONGODB=2.4 PYMONGO=3.5 - python: 3.6 - env: MONGODB=3.0 PYMONGO=3.0 + env: MONGODB=3.0 PYMONGO=3.x before_install: - bash .install_mongodb_on_travis.sh diff --git a/tox.ini b/tox.ini index 7f0d36e4..9bb0c5ec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30} +envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg35,mg3x} [testenv] commands = @@ -8,6 +8,7 @@ deps = nose mg27: PyMongo<2.8 mg28: PyMongo>=2.8,<2.9 - mg30: PyMongo>=3.0 + mg35: PyMongo==3.5 + mg3x: PyMongo>=3.0 setenv = PYTHON_EGG_CACHE = {envdir}/python-eggs From aa5510531df9eed361e34c5130bc19ce15b1185e Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:17:54 +0800 Subject: [PATCH 159/268] update fields argument when given #1172 --- mongoengine/fields.py | 6 +++++- tests/fields/fields.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 4e941c4e..7932f73a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1260,7 +1260,11 @@ class CachedReferenceField(BaseField): if value.pk is None: self.error('You can only reference documents once they have' ' been saved to the database') - return {'_id': value.pk} + value_dict = {'_id': value.pk} + for field in self.fields: + value_dict.update({field: value[field]}) + + return value_dict raise NotImplementedError diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ffee25e6..f86ffdb4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4379,6 +4379,51 @@ class CachedReferenceFieldTest(MongoDBTestCase): self.assertEqual(SocialData.objects(person__group=g2).count(), 1) self.assertEqual(SocialData.objects(person__group=g2).first(), s2) + def test_cached_reference_field_push_with_fields(self): + class Product(Document): + name = StringField() + + Product.drop_collection() + + class Basket(Document): + products = ListField(CachedReferenceField(Product, fields=['name'])) + + Basket.drop_collection() + product1 = Product(name='abc').save() + product2 = Product(name='def').save() + basket = Basket(products=[product1]).save() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + } + ] + } + ) + # push to list + basket.update(push__products=product2) + basket.reload() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + }, + { + '_id': product2.pk, + 'name': product2.name + } + ] + } + ) + def test_cached_reference_field_update_all(self): class Person(Document): TYPES = ( From b66621f9c648d6fffbfd770688d918559bc6b7fb Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:28:33 +0800 Subject: [PATCH 160/268] add changelog #1712 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index efd2e0f6..219d60df 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ dev - use each modifier only with $position #1673 #1675 - Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704 - Fix validation error instance in GenericEmbeddedDocumentField #1067 +- Update cached fields when fields argument is given #1712 Changes in 0.15.0 ================= From 101947da8bec04b532b55c7a252dfff5921b3f0f Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:17:54 +0800 Subject: [PATCH 161/268] update fields argument when given #1172 --- mongoengine/fields.py | 6 +++++- tests/fields/fields.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 4e941c4e..7932f73a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1260,7 +1260,11 @@ class CachedReferenceField(BaseField): if value.pk is None: self.error('You can only reference documents once they have' ' been saved to the database') - return {'_id': value.pk} + value_dict = {'_id': value.pk} + for field in self.fields: + value_dict.update({field: value[field]}) + + return value_dict raise NotImplementedError diff --git a/tests/fields/fields.py b/tests/fields/fields.py index ffee25e6..f86ffdb4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -4379,6 +4379,51 @@ class CachedReferenceFieldTest(MongoDBTestCase): self.assertEqual(SocialData.objects(person__group=g2).count(), 1) self.assertEqual(SocialData.objects(person__group=g2).first(), s2) + def test_cached_reference_field_push_with_fields(self): + class Product(Document): + name = StringField() + + Product.drop_collection() + + class Basket(Document): + products = ListField(CachedReferenceField(Product, fields=['name'])) + + Basket.drop_collection() + product1 = Product(name='abc').save() + product2 = Product(name='def').save() + basket = Basket(products=[product1]).save() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + } + ] + } + ) + # push to list + basket.update(push__products=product2) + basket.reload() + self.assertEqual( + Basket.objects._collection.find_one(), + { + '_id': basket.pk, + 'products': [ + { + '_id': product1.pk, + 'name': product1.name + }, + { + '_id': product2.pk, + 'name': product2.name + } + ] + } + ) + def test_cached_reference_field_update_all(self): class Person(Document): TYPES = ( From 19b18d3d0ab8be238d16a2c41734b6d5dba4adb6 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Wed, 20 Dec 2017 15:28:33 +0800 Subject: [PATCH 162/268] add changelog #1712 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index efd2e0f6..219d60df 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ dev - use each modifier only with $position #1673 #1675 - Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704 - Fix validation error instance in GenericEmbeddedDocumentField #1067 +- Update cached fields when fields argument is given #1712 Changes in 0.15.0 ================= From 919f221be9738dc278f08c31c7421267bfd4f2de Mon Sep 17 00:00:00 2001 From: Chuan-Heng Hsiao Date: Thu, 11 Jan 2018 07:28:25 -0500 Subject: [PATCH 163/268] defensive programming for v as an instance of DBRef when accessing v.collection in dereference --- mongoengine/dereference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 7fe34e43..18b365cc 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -237,7 +237,7 @@ class DeReference(object): elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: item_name = '%s.%s' % (name, k) if name else name data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name) - elif hasattr(v, 'id'): + elif isinstance(v, DBRef) and hasattr(v, 'id'): data[k] = self.object_map.get((v.collection, v.id), v) if instance and name: From 22e75c1691e67a487d90166c37f3cf08531226ac Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Fri, 26 Jan 2018 10:55:44 +0300 Subject: [PATCH 164/268] Insert null values fix https://stackoverflow.com/questions/42601950/how-to-store-a-null-value-in-mongodb-via-mongoengine --- mongoengine/base/document.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 658d0c79..956e9b0e 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -337,11 +337,10 @@ class BaseDocument(object): value = field.generate() self._data[field_name] = value - if value is not None: - if use_db_field: - data[field.db_field] = value - else: - data[field.name] = value + if use_db_field: + data[field.db_field] = value + else: + data[field.name] = value # Only add _cls if allow_inheritance is True if not self._meta.get('allow_inheritance'): From fb213f6e7432e2b9aca4eb4ce619a9db0c521f28 Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Fri, 26 Jan 2018 11:12:02 +0300 Subject: [PATCH 165/268] Update document.py --- mongoengine/base/document.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 956e9b0e..2115a252 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -147,6 +147,7 @@ class BaseDocument(object): if not hasattr(self, name) and not name.startswith('_'): DynamicField = _import_class('DynamicField') + field = DynamicField(db_field=name,null=True) field = DynamicField(db_field=name) field.name = name self._dynamic_fields[name] = field @@ -337,10 +338,11 @@ class BaseDocument(object): value = field.generate() self._data[field_name] = value - if use_db_field: - data[field.db_field] = value - else: - data[field.name] = value + if (value is not None) or (field.null): + if use_db_field: + data[field.db_field] = value + else: + data[field.name] = value # Only add _cls if allow_inheritance is True if not self._meta.get('allow_inheritance'): From 7e8c62104a31bc7168d24e6030437338664340cd Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Fri, 26 Jan 2018 11:15:12 +0300 Subject: [PATCH 166/268] null=True now usefull --- mongoengine/document.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongoengine/document.py b/mongoengine/document.py index f1622934..d635d62e 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1010,6 +1010,7 @@ class DynamicDocument(Document): field_name = args[0] if field_name in self._dynamic_fields: setattr(self, field_name, None) + self._dynamic_fields[field_name].null = False else: super(DynamicDocument, self).__delattr__(*args, **kwargs) From fdda27abd1ce957d0bb0e3053027ff0a5ce7b9de Mon Sep 17 00:00:00 2001 From: Esmail Date: Thu, 1 Feb 2018 12:58:10 -0500 Subject: [PATCH 167/268] Update `post_save` signal documentation to reflect #594 --- docs/guide/signals.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index 30277966..eed382c4 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -43,10 +43,10 @@ Available signals include: has taken place but before saving. `post_save` - Called within :meth:`~mongoengine.Document.save` after all actions - (validation, insert/update, cascades, clearing dirty flags) have completed - successfully. Passed the additional boolean keyword argument `created` to - indicate if the save was an insert or an update. + Called within :meth:`~mongoengine.Document.save` after most actions + (validation, insert/update, and cascades, but not clearing dirty flags) have + completed successfully. Passed the additional boolean keyword argument + `created` to indicate if the save was an insert or an update. `pre_delete` Called within :meth:`~mongoengine.Document.delete` prior to From 8f6c0796e3c66de67110b9f6cf6ac7e4e92e8915 Mon Sep 17 00:00:00 2001 From: Arto Jantunen Date: Fri, 2 Feb 2018 08:20:53 +0200 Subject: [PATCH 168/268] Add db parameter to register_connection This is done to make it compatible with the connect function. --- docs/changelog.rst | 1 + mongoengine/connection.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 219d60df..29471463 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,7 @@ dev - Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704 - Fix validation error instance in GenericEmbeddedDocumentField #1067 - Update cached fields when fields argument is given #1712 +- Add a db parameter to register_connection for compatibility with connect Changes in 0.15.0 ================= diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 34ff4dc3..705dc25b 100644 --- a/mongoengine/connection.py +++ b/mongoengine/connection.py @@ -28,7 +28,7 @@ _connections = {} _dbs = {} -def register_connection(alias, name=None, host=None, port=None, +def register_connection(alias, db=None, name=None, host=None, port=None, read_preference=READ_PREFERENCE, username=None, password=None, authentication_source=None, @@ -39,6 +39,7 @@ def register_connection(alias, name=None, host=None, port=None, :param alias: the name that will be used to refer to this connection throughout MongoEngine :param name: the name of the specific database to use + :param db: the name of the database to use, for compatibility with connect :param host: the host name of the :program:`mongod` instance to connect to :param port: the port that the :program:`mongod` instance is running on :param read_preference: The read preference for the collection @@ -58,7 +59,7 @@ def register_connection(alias, name=None, host=None, port=None, .. versionchanged:: 0.10.6 - added mongomock support """ conn_settings = { - 'name': name or 'test', + 'name': name or db or 'test', 'host': host or 'localhost', 'port': port or 27017, 'read_preference': read_preference, From 9e80da705a8dae9336be7df02169f36607769fb8 Mon Sep 17 00:00:00 2001 From: Calgary Michael Date: Fri, 2 Feb 2018 21:47:04 -0600 Subject: [PATCH 169/268] removed usage of 'pushAll' operator --- mongoengine/queryset/transform.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 3eadaf64..a9874ddf 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -335,7 +335,7 @@ def update(_doc_cls=None, **update): value = {key: value} elif op == 'addToSet' and isinstance(value, list): value = {key: {'$each': value}} - elif op == 'push': + elif op in ('push', 'pushAll'): if parts[-1].isdigit(): key = parts[0] position = int(parts[-1]) @@ -345,7 +345,13 @@ def update(_doc_cls=None, **update): value = [value] value = {key: {'$each': value, '$position': position}} else: - value = {key: value} + if op == 'pushAll': + op = 'push' # convert to non-deprecated keyword + if not isinstance(value, (set, tuple, list)): + value = [value] + value = {key: {'$each': value}} + else: + value = {key: value} else: value = {key: value} key = '$' + op From 4d5c6d11ab8665f77aef8ac6e9b2e58843188b75 Mon Sep 17 00:00:00 2001 From: Calgary Michael Date: Fri, 2 Feb 2018 22:04:30 -0600 Subject: [PATCH 170/268] removed deprecated warning for 'update' method --- mongoengine/context_managers.py | 11 ++++++++++- mongoengine/queryset/base.py | 17 ++++++++++------- tests/queryset/queryset.py | 11 ++++++----- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index c477575e..e6295570 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,9 +1,11 @@ from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db +from pymongo.write_concern import WriteConcern +from contextlib import contextmanager __all__ = ('switch_db', 'switch_collection', 'no_dereference', - 'no_sub_classes', 'query_counter') + 'no_sub_classes', 'query_counter', 'set_write_concern') class switch_db(object): @@ -215,3 +217,10 @@ class query_counter(object): count = self.db.system.profile.find(ignore_query).count() - self.counter self.counter += 1 return count + + +@contextmanager +def set_write_concern(collection, write_concerns): + yield collection.with_options(write_concern=WriteConcern( + **dict(collection.write_concern.document.items()), + **write_concerns)) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6f9c372c..c6e2137c 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -18,7 +18,7 @@ from mongoengine import signals from mongoengine.base import get_document from mongoengine.common import _import_class from mongoengine.connection import get_db -from mongoengine.context_managers import switch_db +from mongoengine.context_managers import switch_db, set_write_concern from mongoengine.errors import (InvalidQueryError, LookUpError, NotUniqueError, OperationError) from mongoengine.python_support import IS_PYMONGO_3 @@ -510,12 +510,15 @@ class BaseQuerySet(object): else: update['$set'] = {'_cls': queryset._document._class_name} try: - result = queryset._collection.update(query, update, multi=multi, - upsert=upsert, **write_concern) + with set_write_concern(queryset._collection, write_concern) as collection: + update_func = collection.update_one + if multi: + update_func = collection.update_many + result = update_func(query, update, upsert=upsert) if full_result: return result - elif result: - return result['n'] + elif result.raw_result: + return result.raw_result['n'] except pymongo.errors.DuplicateKeyError as err: raise NotUniqueError(u'Update failed (%s)' % six.text_type(err)) except pymongo.errors.OperationFailure as err: @@ -544,10 +547,10 @@ class BaseQuerySet(object): write_concern=write_concern, full_result=True, **update) - if atomic_update['updatedExisting']: + if atomic_update.raw_result['updatedExisting']: document = self.get() else: - document = self._document.objects.with_id(atomic_update['upserted']) + document = self._document.objects.with_id(atomic_update.upserted_id) return document def update_one(self, upsert=False, write_concern=None, **update): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 43800fff..848fe35d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -9,6 +9,7 @@ from nose.plugins.skip import SkipTest import pymongo from pymongo.errors import ConfigurationError from pymongo.read_preferences import ReadPreference +from pymongo.results import UpdateResult import six from mongoengine import * @@ -656,14 +657,14 @@ class QuerySetTest(unittest.TestCase): result = self.Person(name="Bob", age=25).update( upsert=True, full_result=True) - self.assertTrue(isinstance(result, dict)) - self.assertTrue("upserted" in result) - self.assertFalse(result["updatedExisting"]) + self.assertTrue(isinstance(result, UpdateResult)) + self.assertTrue("upserted" in result.raw_result) + self.assertFalse(result.raw_result["updatedExisting"]) bob = self.Person.objects.first() result = bob.update(set__age=30, full_result=True) - self.assertTrue(isinstance(result, dict)) - self.assertTrue(result["updatedExisting"]) + self.assertTrue(isinstance(result, UpdateResult)) + self.assertTrue(result.raw_result["updatedExisting"]) self.Person(name="Bob", age=20).save() result = self.Person.objects(name="Bob").update( From fa38bfd4e8884fe2d6c641fa185c90641c5a24c8 Mon Sep 17 00:00:00 2001 From: Calgary Michael Date: Fri, 2 Feb 2018 22:30:06 -0600 Subject: [PATCH 171/268] made set_write_concern python2.7 compatible --- mongoengine/context_managers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index e6295570..cc35cdd7 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -221,6 +221,6 @@ class query_counter(object): @contextmanager def set_write_concern(collection, write_concerns): - yield collection.with_options(write_concern=WriteConcern( - **dict(collection.write_concern.document.items()), - **write_concerns)) + old_concerns = dict(collection.write_concern.document.items()) + combined_concerns = old_concerns.update(write_concerns) + yield collection.with_options(write_concern=WriteConcern(**combined_concerns)) From 6835c15d9b0a0379036417e642516f7cdef76840 Mon Sep 17 00:00:00 2001 From: Calgary Michael Date: Fri, 2 Feb 2018 22:41:07 -0600 Subject: [PATCH 172/268] fixing bug in previous commit --- mongoengine/context_managers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index cc35cdd7..c6d0d40f 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -221,6 +221,6 @@ class query_counter(object): @contextmanager def set_write_concern(collection, write_concerns): - old_concerns = dict(collection.write_concern.document.items()) - combined_concerns = old_concerns.update(write_concerns) + combined_concerns = dict(collection.write_concern.document.items()) + combined_concerns.update(write_concerns) yield collection.with_options(write_concern=WriteConcern(**combined_concerns)) From 38fdf264051430babc8e324f878dc265471b4dea Mon Sep 17 00:00:00 2001 From: Calgary Michael Date: Sun, 4 Feb 2018 10:23:50 -0600 Subject: [PATCH 173/268] added tests for push and push_all --- tests/queryset/transform.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 20ab0b3f..20a0c278 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -51,6 +51,17 @@ class TransformTest(unittest.TestCase): update = transform.update(DicDoc, pull__dictField__test=doc) self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict)) + def test_transform_update_push(self): + """Ensure the differences in behvaior between 'push' and 'push_all'""" + class BlogPost(Document): + tags = ListField(StringField()) + + update = transform.update(BlogPost, push__tags=['mongo', 'db']) + self.assertEqual(update, {'$push': {'tags': ['mongo', 'db']}}) + + update = transform.update(BlogPost, push_all__tags=['mongo', 'db']) + self.assertEqual(update, {'$push': {'tags': {'$each': ['mongo', 'db']}}}) + def test_query_field_name(self): """Ensure that the correct field name is used when querying. """ From 0d854ce906abf87d99ec0a102540570cf800237a Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Mon, 5 Feb 2018 03:24:53 +0300 Subject: [PATCH 174/268] style fix --- mongoengine/base/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 2115a252..172e052f 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -147,7 +147,7 @@ class BaseDocument(object): if not hasattr(self, name) and not name.startswith('_'): DynamicField = _import_class('DynamicField') - field = DynamicField(db_field=name,null=True) + field = DynamicField(db_field=name, null=True) field = DynamicField(db_field=name) field.name = name self._dynamic_fields[name] = field From 6b04ddfad1b2b6574c03ed842e8020feb361d9ce Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Mon, 5 Feb 2018 04:24:03 +0300 Subject: [PATCH 175/268] >< --- mongoengine/base/document.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 172e052f..f31c22ce 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -147,7 +147,6 @@ class BaseDocument(object): if not hasattr(self, name) and not name.startswith('_'): DynamicField = _import_class('DynamicField') - field = DynamicField(db_field=name, null=True) field = DynamicField(db_field=name) field.name = name self._dynamic_fields[name] = field From de360c61dd239ef44e2d2abd9c4ce835f1144cf8 Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Mon, 5 Feb 2018 04:26:25 +0300 Subject: [PATCH 176/268] removed useless lines --- mongoengine/document.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index d635d62e..f1622934 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1010,7 +1010,6 @@ class DynamicDocument(Document): field_name = args[0] if field_name in self._dynamic_fields: setattr(self, field_name, None) - self._dynamic_fields[field_name].null = False else: super(DynamicDocument, self).__delattr__(*args, **kwargs) From d69808c20414d316fcc6f5a1a6e0d8f751b04a9b Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Mon, 5 Feb 2018 12:33:58 +0300 Subject: [PATCH 177/268] oh, ok... --- mongoengine/base/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index f31c22ce..348ee977 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -147,7 +147,7 @@ class BaseDocument(object): if not hasattr(self, name) and not name.startswith('_'): DynamicField = _import_class('DynamicField') - field = DynamicField(db_field=name) + field = DynamicField(db_field=name, null=True) field.name = name self._dynamic_fields[name] = field self._fields_ordered += (name,) From 7efa67e7e6a6bf32d208c6628b3b43a4c0d89bab Mon Sep 17 00:00:00 2001 From: Ivan Pogrebkov Date: Mon, 5 Feb 2018 12:35:06 +0300 Subject: [PATCH 178/268] reverse to 'style fix' --- mongoengine/document.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongoengine/document.py b/mongoengine/document.py index f1622934..d635d62e 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1010,6 +1010,7 @@ class DynamicDocument(Document): field_name = args[0] if field_name in self._dynamic_fields: setattr(self, field_name, None) + self._dynamic_fields[field_name].null = False else: super(DynamicDocument, self).__delattr__(*args, **kwargs) From 7d8916b6e9f60995f905885beac32be321417f1a Mon Sep 17 00:00:00 2001 From: Sangmin In Date: Sun, 11 Feb 2018 00:59:47 +0900 Subject: [PATCH 179/268] fix case inconsistent casing of "MongoDB" (#1744) --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ea1a04c1..bcd0d17f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -86,7 +86,7 @@ of them stand out as particularly intuitive solutions. Posts ^^^^^ -Happily mongoDB *isn't* a relational database, so we're not going to do it that +Happily MongoDB *isn't* a relational database, so we're not going to do it that way. As it turns out, we can use MongoDB's schemaless nature to provide us with a much nicer solution. We will store all of the posts in *one collection* and each post type will only store the fields it needs. If we later want to add From 0bd2103a8cb10c63371b2d4cefc8809a5d0adc55 Mon Sep 17 00:00:00 2001 From: Andy Yankovsky Date: Tue, 20 Feb 2018 00:02:12 +0300 Subject: [PATCH 180/268] Add test for document update --- tests/document/instance.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/document/instance.py b/tests/document/instance.py index 609bc900..22c44ffa 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -1341,6 +1341,23 @@ class InstanceTest(unittest.TestCase): site = Site.objects.first() self.assertEqual(site.page.log_message, "Error: Dummy message") + def test_update_list_field(self): + """Test update on `ListField` with $pull + $in. + """ + class Doc(Document): + foo = ListField(StringField()) + + Doc.drop_collection() + doc = Doc(foo=['a', 'b', 'c']) + doc.save() + + # Update + doc = Doc.objects.first() + doc.update(pull__foo__in=['a', 'c']) + + doc = Doc.objects.first() + self.assertEqual(doc.foo, ['b']) + def test_embedded_update_db_field(self): """Test update on `EmbeddedDocumentField` fields when db_field is other than default. From 2d76aebb8e9d1ee995eb8945968ca13a2b1b2777 Mon Sep 17 00:00:00 2001 From: KCarretto Date: Wed, 21 Feb 2018 05:02:35 -0500 Subject: [PATCH 181/268] Fixed formatting issue --- mongoengine/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 8692e3a7..d75286f0 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1108,8 +1108,8 @@ class ReferenceField(BaseField): if self.document_type._meta.get('abstract') and \ not isinstance(value, self.document_type): self.error( - '%s is not an instance of abstract reference type %s' % (value, - self.document_type._class_name) + '%s is not an instance of abstract reference type %s' % ( + value, self.document_type._class_name) ) def lookup_member(self, member_name): From aa683226416dac3fa4d5fd757091e2c795cf2ff7 Mon Sep 17 00:00:00 2001 From: estein-de Date: Tue, 27 Feb 2018 08:43:09 -0600 Subject: [PATCH 182/268] MongoDB wants dates stored in UTC, but the functions used in this documentation to generate datetime objects would use server's local timezone - fix it! (#1662) --- docs/guide/defining-documents.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d41ae7e6..33b5292f 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -22,7 +22,7 @@ objects** as class attributes to the document class:: class Page(Document): title = StringField(max_length=200, required=True) - date_modified = DateTimeField(default=datetime.datetime.now) + date_modified = DateTimeField(default=datetime.datetime.utcnow) As BSON (the binary format for storing data in mongodb) is order dependent, documents are serialized based on their field order. @@ -224,7 +224,7 @@ store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate user = ReferenceField(User) answers = DictField() - survey_response = SurveyResponse(date=datetime.now(), user=request.user) + survey_response = SurveyResponse(date=datetime.utcnow(), user=request.user) response_form = ResponseForm(request.POST) survey_response.answers = response_form.cleaned_data() survey_response.save() @@ -618,7 +618,7 @@ collection after a given period. See the official documentation for more information. A common usecase might be session data:: class Session(Document): - created = DateTimeField(default=datetime.now) + created = DateTimeField(default=datetime.utcnow) meta = { 'indexes': [ {'fields': ['created'], 'expireAfterSeconds': 3600} From a34fd9ac89c3ebd294f57d4b471fb6f5b31e1e91 Mon Sep 17 00:00:00 2001 From: Thomas Erker <35792856+th-erker@users.noreply.github.com> Date: Thu, 8 Mar 2018 11:25:36 +0000 Subject: [PATCH 183/268] Add testcase for #1751 --- tests/document/class_methods.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index dd3addb7..8701b4b2 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -187,6 +187,19 @@ class ClassMethodsTest(unittest.TestCase): self.assertEqual(BlogPostWithTags.compare_indexes(), { 'missing': [], 'extra': [] }) self.assertEqual(BlogPostWithCustomField.compare_indexes(), { 'missing': [], 'extra': [] }) + def test_compare_indexes_for_text_indexes(self): + """ Ensure that compare_indexes behaves correctly for text indexes """ + + class Doc(Document): + a = StringField() + meta = { 'indexes': ['$a']} + + Doc.drop_collection() + Doc.ensure_indexes() + actual = Doc.compare_indexes() + expected = {'missing': [], 'extra': []} + self.assertEqual(actual, expected) + def test_list_indexes_inheritance(self): """ ensure that all of the indexes are listed regardless of the super- or sub-class that we call it from From a0947d0c544a58516650a48b73b84d171e693ffc Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sat, 10 Mar 2018 23:24:04 -0600 Subject: [PATCH 184/268] Edit EmbeddedDocumentListField update() doc --- mongoengine/base/datastructures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 43f32810..fddd945a 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -351,7 +351,8 @@ class EmbeddedDocumentList(BaseList): def update(self, **update): """ - Updates the embedded documents with the given update values. + Updates the embedded documents with the given replacement values. This + function does not support mongoDB update operators such as ``inc__``. .. note:: The embedded document changes are not automatically saved From dabe8c1bb7e189d0dc8bdceb54eb1ca982a1f6b5 Mon Sep 17 00:00:00 2001 From: Stefan Wojcik Date: Wed, 14 Mar 2018 14:26:06 -0400 Subject: [PATCH 185/268] highlight places where ValidationError is raised outside of validate() method --- mongoengine/fields.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 7932f73a..f169f0f1 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -614,6 +614,7 @@ class EmbeddedDocumentField(BaseField): """ def __init__(self, document_type, **kwargs): + # XXX ValidationError raised outside of the "validate" method. if not ( isinstance(document_type, six.string_types) or issubclass(document_type, EmbeddedDocument) @@ -919,8 +920,11 @@ class DictField(ComplexBaseField): self.field = field self._auto_dereference = False self.basecls = basecls or BaseField + + # XXX ValidationError raised outside of the "validate" method. if not issubclass(self.basecls, BaseField): self.error('DictField only accepts dict values') + kwargs.setdefault('default', lambda: {}) super(DictField, self).__init__(*args, **kwargs) @@ -969,6 +973,7 @@ class MapField(DictField): """ def __init__(self, field=None, *args, **kwargs): + # XXX ValidationError raised outside of the "validate" method. if not isinstance(field, BaseField): self.error('Argument to MapField constructor must be a valid ' 'field') @@ -1028,6 +1033,7 @@ class ReferenceField(BaseField): A reference to an abstract document type is always stored as a :class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`. """ + # XXX ValidationError raised outside of the "validate" method. if ( not isinstance(document_type, six.string_types) and not issubclass(document_type, Document) @@ -1082,6 +1088,8 @@ class ReferenceField(BaseField): if isinstance(document, Document): # We need the id from the saved object to create the DBRef id_ = document.pk + + # XXX ValidationError raised outside of the "validate" method. if id_ is None: self.error('You can only reference documents once they have' ' been saved to the database') @@ -1121,7 +1129,6 @@ class ReferenceField(BaseField): return self.to_mongo(value) def validate(self, value): - if not isinstance(value, (self.document_type, LazyReference, DBRef, ObjectId)): self.error('A ReferenceField only accepts DBRef, LazyReference, ObjectId or documents') @@ -1129,11 +1136,14 @@ class ReferenceField(BaseField): self.error('You can only reference documents once they have been ' 'saved to the database') - if self.document_type._meta.get('abstract') and \ - not isinstance(value, self.document_type): + if ( + self.document_type._meta.get('abstract') and + not isinstance(value, self.document_type) + ): self.error( '%s is not an instance of abstract reference type %s' % ( - self.document_type._class_name) + self.document_type._class_name + ) ) def lookup_member(self, member_name): @@ -1156,6 +1166,7 @@ class CachedReferenceField(BaseField): if fields is None: fields = [] + # XXX ValidationError raised outside of the "validate" method. if ( not isinstance(document_type, six.string_types) and not issubclass(document_type, Document) @@ -1230,6 +1241,7 @@ class CachedReferenceField(BaseField): id_field_name = self.document_type._meta['id_field'] id_field = self.document_type._fields[id_field_name] + # XXX ValidationError raised outside of the "validate" method. if isinstance(document, Document): # We need the id from the saved object to create the DBRef id_ = document.pk @@ -1238,7 +1250,6 @@ class CachedReferenceField(BaseField): ' been saved to the database') else: self.error('Only accept a document object') - # TODO: should raise here or will fail next statement value = SON(( ('_id', id_field.to_mongo(id_)), @@ -1256,6 +1267,7 @@ class CachedReferenceField(BaseField): if value is None: return None + # XXX ValidationError raised outside of the "validate" method. if isinstance(value, Document): if value.pk is None: self.error('You can only reference documents once they have' @@ -1269,7 +1281,6 @@ class CachedReferenceField(BaseField): raise NotImplementedError def validate(self, value): - if not isinstance(value, self.document_type): self.error('A CachedReferenceField only accepts documents') @@ -1330,6 +1341,8 @@ class GenericReferenceField(BaseField): elif isinstance(choice, type) and issubclass(choice, Document): self.choices.append(choice._class_name) else: + # XXX ValidationError raised outside of the "validate" + # method. self.error('Invalid choices provided: must be a list of' 'Document subclasses and/or six.string_typess') @@ -1393,6 +1406,7 @@ class GenericReferenceField(BaseField): # We need the id from the saved object to create the DBRef id_ = document.id if id_ is None: + # XXX ValidationError raised outside of the "validate" method. self.error('You can only reference documents once they have' ' been saved to the database') else: @@ -2209,6 +2223,7 @@ class LazyReferenceField(BaseField): automatically call `fetch()` and try to retrive the field on the fetched document. Note this only work getting field (not setting or deleting). """ + # XXX ValidationError raised outside of the "validate" method. if ( not isinstance(document_type, six.string_types) and not issubclass(document_type, Document) From e46779f87b8c6cc154d90939f10d35ffd06469ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malthe=20J=C3=B8rgensen?= Date: Tue, 27 Mar 2018 14:14:08 +0200 Subject: [PATCH 186/268] Docs, queryset.update: `full_result`-arg not clearly described The documentation for the `full_result`-argument to `queryset.update()` can be read as returning the update documents/objects, whereas it's really returning just the full "PyMongo result dictionary". This commit adds some wording and an example dictionary, to make it clear what the behavior is. --- mongoengine/queryset/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6f9c372c..bf8a5b55 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -486,8 +486,9 @@ 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 rather than just the number - updated. + :param full_result: Return the full result dictionary rather than just the number + updated, e.g. return + `{u'n': 2, u'nModified': 2, u'ok': 1.0, 'updatedExisting': True}`. :param update: Django-style update keyword arguments .. versionadded:: 0.2 From 727778b7305cd01c81e3b198fd20079fcb66b0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Malthe=20J=C3=B8rgensen?= Date: Fri, 30 Mar 2018 20:41:39 +0200 Subject: [PATCH 187/268] Docs, queryset.update(): Fix backtick mistake Code should be marked with double backticks --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index bf8a5b55..e5611226 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -488,7 +488,7 @@ class BaseQuerySet(object): 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 - `{u'n': 2, u'nModified': 2, u'ok': 1.0, 'updatedExisting': True}`. + ``{'n': 2, 'nModified': 2, 'ok': 1.0, 'updatedExisting': True}``. :param update: Django-style update keyword arguments .. versionadded:: 0.2 From c6f0d5e4785571980cd8a53c71c27172cb63c7d0 Mon Sep 17 00:00:00 2001 From: Kushal Mitruka Date: Sun, 1 Apr 2018 20:11:22 +0530 Subject: [PATCH 188/268] fixed pull queries for embeddeddocumentlistfields Updated mongoengine.queryset.transform.update method to handle EmbeddedDocuementListField during pull operations in DB using mongoegning ORM fixed : .udpate(pull__emb_doc__emb_doc_list=doc) --- mongoengine/queryset/transform.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 05721850..f96f993c 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -314,11 +314,17 @@ def update(_doc_cls=None, **update): field_classes = [c.__class__ for c in cleaned_fields] field_classes.reverse() ListField = _import_class('ListField') - if ListField in field_classes: - # Join all fields via dot notation to the last ListField + EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField') + if ListField in field_classes or EmbeddedDocumentListField in field_classes: + # Join all fields via dot notation to the last ListField or EmbeddedDocumentListField # Then process as normal + if ListField in field_classes: + _check_field = ListField + else: + _check_field = EmbeddedDocumentListField + last_listField = len( - cleaned_fields) - field_classes.index(ListField) + cleaned_fields) - field_classes.index(_check_field) key = '.'.join(parts[:last_listField]) parts = parts[last_listField:] parts.insert(0, key) From 806a80cef16ea08b5a33b4ae663b573e7f9a20c4 Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 14 Sep 2017 20:31:50 +0300 Subject: [PATCH 189/268] Fixes #1641 There's no need to explicitly raise StopIteration as that's what a bare return statement does for a generator function - so yes they're the same. As of late 2014 return is correct and raise StopIteration for ending a generator is on a depreciation schedule. See PEP 479 for full details. https://stackoverflow.com/q/14183803/248296 https://www.python.org/dev/peps/pep-0479/ --- mongoengine/queryset/queryset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index cf913b01..60de19a2 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -92,7 +92,7 @@ class QuerySet(BaseQuerySet): # Raise StopIteration if we already established there were no more # docs in the db cursor. if not self._has_more: - raise StopIteration + return # Otherwise, populate more of the cache and repeat. if len(self._result_cache) <= pos: From 49bff5d544c30ba0a8433b07cda8f7d4e50c9d73 Mon Sep 17 00:00:00 2001 From: Benjamin Chrobot Date: Thu, 12 Apr 2018 10:47:52 -0400 Subject: [PATCH 190/268] Add documentation for LazyReference and GenericLazyReference fields. --- docs/apireference.rst | 2 ++ docs/guide/defining-documents.rst | 2 ++ mongoengine/fields.py | 17 +++++++++++------ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/apireference.rst b/docs/apireference.rst index 625d4a8b..05ba3f73 100644 --- a/docs/apireference.rst +++ b/docs/apireference.rst @@ -87,7 +87,9 @@ Fields .. autoclass:: mongoengine.fields.DictField .. autoclass:: mongoengine.fields.MapField .. autoclass:: mongoengine.fields.ReferenceField +.. autoclass:: mongoengine.fields.LazyReferenceField .. autoclass:: mongoengine.fields.GenericReferenceField +.. autoclass:: mongoengine.fields.GenericLazyReferenceField .. autoclass:: mongoengine.fields.CachedReferenceField .. autoclass:: mongoengine.fields.BinaryField .. autoclass:: mongoengine.fields.FileField diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 33b5292f..3ced284e 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -80,6 +80,7 @@ are as follows: * :class:`~mongoengine.fields.FloatField` * :class:`~mongoengine.fields.GenericEmbeddedDocumentField` * :class:`~mongoengine.fields.GenericReferenceField` +* :class:`~mongoengine.fields.GenericLazyReferenceField` * :class:`~mongoengine.fields.GeoPointField` * :class:`~mongoengine.fields.ImageField` * :class:`~mongoengine.fields.IntField` @@ -87,6 +88,7 @@ are as follows: * :class:`~mongoengine.fields.MapField` * :class:`~mongoengine.fields.ObjectIdField` * :class:`~mongoengine.fields.ReferenceField` +* :class:`~mongoengine.fields.LazyReferenceField` * :class:`~mongoengine.fields.SequenceField` * :class:`~mongoengine.fields.SortedListField` * :class:`~mongoengine.fields.StringField` diff --git a/mongoengine/fields.py b/mongoengine/fields.py index f169f0f1..a661874a 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -2204,8 +2204,11 @@ class MultiPolygonField(GeoJsonBaseField): class LazyReferenceField(BaseField): """A really lazy reference to a document. - Unlike the :class:`~mongoengine.fields.ReferenceField` it must be manually - dereferenced using it ``fetch()`` method. + Unlike the :class:`~mongoengine.fields.ReferenceField` it will + **not** be automatically (lazily) dereferenced on access. + Instead, access will return a :class:`~mongoengine.base.LazyReference` class + instance, allowing access to `pk` or manual dereference by using + ``fetch()`` method. .. versionadded:: 0.15 """ @@ -2331,10 +2334,12 @@ class LazyReferenceField(BaseField): class GenericLazyReferenceField(GenericReferenceField): - """A reference to *any* :class:`~mongoengine.document.Document` subclass - that will be automatically dereferenced on access (lazily). - Unlike the :class:`~mongoengine.fields.GenericReferenceField` it must be - manually dereferenced using it ``fetch()`` method. + """A reference to *any* :class:`~mongoengine.document.Document` subclass. + Unlike the :class:`~mongoengine.fields.GenericReferenceField` it will + **not** be automatically (lazily) dereferenced on access. + Instead, access will return a :class:`~mongoengine.base.LazyReference` class + instance, allowing access to `pk` or manual dereference by using + ``fetch()`` method. .. note :: * Any documents used as a generic reference must be registered in the From faca8512c53102c201cc9b13519ca2ef3c5eb0c5 Mon Sep 17 00:00:00 2001 From: Emmanuel Nosa Evbuomwan Date: Sun, 29 Apr 2018 17:32:03 +0100 Subject: [PATCH 191/268] Updated text-indexes.rst The search statement under the `Ordering by text score` section uses `search` on the QuerySet object instead of `search_text` and thus raised an `AttributeError` AttributeError: 'QuerySet' object has no attribute 'search' --- docs/guide/text-indexes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/text-indexes.rst b/docs/guide/text-indexes.rst index 725ad369..92a4471a 100644 --- a/docs/guide/text-indexes.rst +++ b/docs/guide/text-indexes.rst @@ -48,4 +48,4 @@ Ordering by text score :: - objects = News.objects.search('mongo').order_by('$text_score') + objects = News.objects.search_text('mongo').order_by('$text_score') From 65e4fea4efbb2f888bcf15850777457aafbd5720 Mon Sep 17 00:00:00 2001 From: Kushal Mitruka Date: Tue, 1 May 2018 20:32:38 +0530 Subject: [PATCH 192/268] added test cases for update pull queries --- tests/queryset/transform.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index a043a647..a2636122 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -247,7 +247,31 @@ class TransformTest(unittest.TestCase): events = Event.objects(location__within=box) with self.assertRaises(InvalidQueryError): events.count() + + def test_update_pull_for_list_fields(self): + """ + Test added to check pull operation in update for + EmbeddedDocumentListField which is inside a EmbeddedDocumentField + """ + class Word(EmbeddedDocument): + word = StringField() + index = IntField() + + class SubDoc(EmbeddedDocument): + heading = ListField(StringField()) + text = EmbeddedDocumentListField(Word) + + class MainDoc(Document): + title = StringField() + content = EmbeddedDocumentField(SubDoc) + + word = Word(word='abc', index=1) + update = transform.update(MainDoc, pull__content__text=word) + self.assertEqual(update, {'$pull': {'content.text': SON([('word', u'abc'), ('index', 1)])}}) - + update = transform.update(MainDoc, pull__content__heading='xyz') + self.assertEqual(update, {'$pull': {'content.heading': 'xyz'}}) + + if __name__ == '__main__': unittest.main() From b1f62a27353a1c2bd8d69b4697dab16caa8056cf Mon Sep 17 00:00:00 2001 From: Kushal Mitruka Date: Tue, 1 May 2018 21:08:43 +0530 Subject: [PATCH 193/268] added import in tests/queryset/transform.py for SON object --- tests/queryset/transform.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index a2636122..a1f060d5 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -1,5 +1,7 @@ import unittest +from bson.son import SON + from mongoengine import * from mongoengine.queryset import Q, transform From 5a6d4387ea7cbc6eea6176d7c3d67cdcc64c2b31 Mon Sep 17 00:00:00 2001 From: Andy Yankovsky Date: Mon, 7 May 2018 23:17:12 +0300 Subject: [PATCH 194/268] Restore comment from cached value after cursor copy --- mongoengine/queryset/base.py | 3 +++ tests/queryset/queryset.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index e5611226..b1d3342b 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1579,6 +1579,9 @@ class BaseQuerySet(object): if self._batch_size is not None: self._cursor_obj.batch_size(self._batch_size) + if self._comment is not None: + self._cursor_obj.comment(self._comment) + return self._cursor_obj def __deepcopy__(self, memo): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 43800fff..c9b87eeb 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -2383,14 +2383,19 @@ class QuerySetTest(unittest.TestCase): age = IntField() with db_ops_tracker() as q: - adult = (User.objects.filter(age__gte=18) + adult1 = (User.objects.filter(age__gte=18) .comment('looking for an adult') .first()) + + adult2 = (User.objects.comment('looking for an adult') + .filter(age__gte=18) + .first()) + ops = q.get_ops() - self.assertEqual(len(ops), 1) - op = ops[0] - self.assertEqual(op['query']['$query'], {'age': {'$gte': 18}}) - self.assertEqual(op['query']['$comment'], 'looking for an adult') + self.assertEqual(len(ops), 2) + for op in ops: + self.assertEqual(op['query']['$query'], {'age': {'$gte': 18}}) + self.assertEqual(op['query']['$comment'], 'looking for an adult') def test_map_reduce(self): """Ensure map/reduce is both mapping and reducing. From 85d621846d521e2662d420983393b49737a22ae3 Mon Sep 17 00:00:00 2001 From: Benjamin Jiang Date: Wed, 16 May 2018 11:37:32 +0800 Subject: [PATCH 195/268] Fix typo --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index c948dac2..a8061749 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -579,7 +579,7 @@ class Document(BaseDocument): """Delete the :class:`~mongoengine.Document` from the database. This will only take effect if the document has been previously saved. - :parm signal_kwargs: (optional) kwargs dictionary to be passed to + :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. :param write_concern: Extra keyword arguments are passed down which will be used as options for the resultant From 08b64338439a33a1b869ef0c77f09fd9b50bae2c Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 11:03:13 +0800 Subject: [PATCH 196/268] fix compare_indexes for text indexes #1751 --- mongoengine/document.py | 12 ++++++++++-- tests/document/class_methods.py | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index c948dac2..18c52d9d 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -967,8 +967,16 @@ class Document(BaseDocument): """ required = cls.list_indexes() - existing = [info['key'] - for info in cls._get_collection().index_information().values()] + + existing = [] + for info in cls._get_collection().index_information().values(): + if '_fts' in info['key'][0]: + index_type = info['key'][0][1] + text_index_fields = info.get('weights').keys() + existing.append( + [(key, index_type) for key in text_index_fields]) + else: + existing.append(info['key']) missing = [index for index in required if index not in existing] extra = [index for index in existing if index not in required] diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index 8701b4b2..3f052d45 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -192,7 +192,13 @@ class ClassMethodsTest(unittest.TestCase): class Doc(Document): a = StringField() - meta = { 'indexes': ['$a']} + b = StringField() + meta = {'indexes': [ + {'fields': ['$a', "$b"], + 'default_language': 'english', + 'weights': {'a': 10, 'b': 2} + } + ]} Doc.drop_collection() Doc.ensure_indexes() From e50d66b3032df29331f72f6ce238b3813ae35b50 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 11:26:30 +0800 Subject: [PATCH 197/268] skip mongodb 2.4 --- tests/document/class_methods.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index 3f052d45..2fab1f72 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -5,6 +5,7 @@ from mongoengine import * from mongoengine.queryset import NULLIFY, PULL from mongoengine.connection import get_db +from tests.utils import needs_mongodb_v26 __all__ = ("ClassMethodsTest", ) @@ -187,6 +188,7 @@ class ClassMethodsTest(unittest.TestCase): self.assertEqual(BlogPostWithTags.compare_indexes(), { 'missing': [], 'extra': [] }) self.assertEqual(BlogPostWithCustomField.compare_indexes(), { 'missing': [], 'extra': [] }) + @needs_mongodb_v26 def test_compare_indexes_for_text_indexes(self): """ Ensure that compare_indexes behaves correctly for text indexes """ From 257a43298b93785d9280d3942745aab7149bf627 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 12:31:27 +0800 Subject: [PATCH 198/268] use MongoClient.is_mongos in ensure indexes #1759 --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2de0b1a3..865bb063 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -854,7 +854,7 @@ class Document(BaseDocument): collection = cls._get_collection() # 746: when connection is via mongos, the read preference is not necessarily an indication that # this code runs on a secondary - if not collection.is_mongos and collection.read_preference > 1: + if not collection.database.client.is_mongos and collection.read_preference > 1: return # determine if an index which we are creating includes From fd02d77c59aa6c427581c9903d158c1d77cbfa26 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 12:43:24 +0800 Subject: [PATCH 199/268] drop pymongo 2.x support in update --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index a70c711e..29a72d1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,8 +19,6 @@ python: - pypy env: -- MONGODB=2.6 PYMONGO=2.7 -- MONGODB=2.6 PYMONGO=2.8 - MONGODB=2.6 PYMONGO=3.x matrix: @@ -28,14 +26,10 @@ matrix: fast_finish: true include: - - python: 2.7 - env: MONGODB=2.4 PYMONGO=2.7 - python: 2.7 env: MONGODB=2.4 PYMONGO=3.5 - python: 2.7 env: MONGODB=3.0 PYMONGO=3.x - - python: 3.5 - env: MONGODB=2.4 PYMONGO=2.7 - python: 3.5 env: MONGODB=2.4 PYMONGO=3.5 - python: 3.5 From f605eb14e8b9e4c02d8279a54035f1c00791cf22 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 12:54:24 +0800 Subject: [PATCH 200/268] fix style --- mongoengine/context_managers.py | 4 ++-- mongoengine/document.py | 2 +- tox.ini | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index c6d0d40f..150f9657 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,7 +1,7 @@ from mongoengine.common import _import_class -from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db -from pymongo.write_concern import WriteConcern from contextlib import contextmanager +from pymongo.write_concern import WriteConcern +from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db __all__ = ('switch_db', 'switch_collection', 'no_dereference', diff --git a/mongoengine/document.py b/mongoengine/document.py index 865bb063..2de0b1a3 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -854,7 +854,7 @@ class Document(BaseDocument): collection = cls._get_collection() # 746: when connection is via mongos, the read preference is not necessarily an indication that # this code runs on a secondary - if not collection.database.client.is_mongos and collection.read_preference > 1: + if not collection.is_mongos and collection.read_preference > 1: return # determine if an index which we are creating includes diff --git a/tox.ini b/tox.ini index 9bb0c5ec..2f2b1757 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,11 @@ [tox] -envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg35,mg3x} +envlist = {py27,py35,pypy,pypy3}-{mg35,mg3x} [testenv] commands = python setup.py nosetests {posargs} deps = nose - mg27: PyMongo<2.8 - mg28: PyMongo>=2.8,<2.9 mg35: PyMongo==3.5 mg3x: PyMongo>=3.0 setenv = From c83c6350676af155a8c204a0593e74ee8b674e6b Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 13:04:51 +0800 Subject: [PATCH 201/268] fix import order --- mongoengine/context_managers.py | 2 +- mongoengine/queryset/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index 150f9657..ec2e9e8b 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,6 +1,6 @@ -from mongoengine.common import _import_class from contextlib import contextmanager from pymongo.write_concern import WriteConcern +from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 2a1bf3ad..f78ee882 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -18,7 +18,7 @@ from mongoengine import signals from mongoengine.base import get_document from mongoengine.common import _import_class from mongoengine.connection import get_db -from mongoengine.context_managers import switch_db, set_write_concern +from mongoengine.context_managers import set_write_concern, switch_db from mongoengine.errors import (InvalidQueryError, LookUpError, NotUniqueError, OperationError) from mongoengine.python_support import IS_PYMONGO_3 From 843fc03bf490b6c6346a22f8599046f718631beb Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 13:16:25 +0800 Subject: [PATCH 202/268] add changelog for update_one,update_many --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 29471463..cb31da21 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ dev - Fix validation error instance in GenericEmbeddedDocumentField #1067 - Update cached fields when fields argument is given #1712 - Add a db parameter to register_connection for compatibility with connect +- Use new update_one, update_many on document/queryset update #1491 Changes in 0.15.0 ================= From 3e0d84383e4a0f9c52d6840ff1d4a882e7eeb6cb Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 13:41:20 +0800 Subject: [PATCH 203/268] use insert_one, insert_many and remove deprecated one #1491 --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 29471463..4dec42ce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ dev - Fix validation error instance in GenericEmbeddedDocumentField #1067 - Update cached fields when fields argument is given #1712 - Add a db parameter to register_connection for compatibility with connect +- Use insert_one, insert_many in Document.insert #1491 Changes in 0.15.0 ================= diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index b1d3342b..95b25d06 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -350,8 +350,14 @@ class BaseQuerySet(object): documents=docs, **signal_kwargs) raw = [doc.to_mongo() for doc in docs] + insert_func = self._collection.insert_many + if return_one: + raw = raw[0] + insert_func = self._collection.insert_one + try: - ids = self._collection.insert(raw, **write_concern) + inserted_result = insert_func(raw, **write_concern) + ids = inserted_result.inserted_id if return_one else inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) From f9a887c8c6665a77dda33ca785acf2ece8158337 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 14:33:12 +0800 Subject: [PATCH 204/268] fix inserted_ids --- mongoengine/queryset/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 95b25d06..eec7df18 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -357,7 +357,8 @@ class BaseQuerySet(object): try: inserted_result = insert_func(raw, **write_concern) - ids = inserted_result.inserted_id if return_one else inserted_result.inserted_ids + ids = return_one and inserted_result.inserted_id or\ + inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) @@ -374,7 +375,6 @@ class BaseQuerySet(object): signals.post_bulk_insert.send( self._document, documents=docs, loaded=False, **signal_kwargs) return return_one and ids[0] or ids - documents = self.in_bulk(ids) results = [] for obj_id in ids: From 1d3f20b666bfe0ef8dcdea06a2341e460d13c486 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 14:41:25 +0800 Subject: [PATCH 205/268] fix style and ids need to be an array --- mongoengine/queryset/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 2f2d38dd..0634dceb 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -357,8 +357,7 @@ class BaseQuerySet(object): try: inserted_result = insert_func(raw, **write_concern) - ids = return_one and inserted_result.inserted_id or\ - inserted_result.inserted_ids + ids = return_one and [inserted_result.inserted_id] or inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) From 1aebc9514564a9ba1c58cb52fa4552ace6d2066a Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 13:41:20 +0800 Subject: [PATCH 206/268] use insert_one, insert_many and remove deprecated one #1491 --- docs/changelog.rst | 1 + mongoengine/queryset/base.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cb31da21..e3d366b3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ dev - Update cached fields when fields argument is given #1712 - Add a db parameter to register_connection for compatibility with connect - Use new update_one, update_many on document/queryset update #1491 +- Use insert_one, insert_many in Document.insert #1491 Changes in 0.15.0 ================= diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index f78ee882..ff7afa96 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -350,8 +350,14 @@ class BaseQuerySet(object): documents=docs, **signal_kwargs) raw = [doc.to_mongo() for doc in docs] + insert_func = self._collection.insert_many + if return_one: + raw = raw[0] + insert_func = self._collection.insert_one + try: - ids = self._collection.insert(raw, **write_concern) + inserted_result = insert_func(raw, **write_concern) + ids = inserted_result.inserted_id if return_one else inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) From 5c0bd8a81090aa39a74b347adb7afde9edb202b6 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 14:33:12 +0800 Subject: [PATCH 207/268] fix inserted_ids --- mongoengine/queryset/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index ff7afa96..2f2d38dd 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -357,7 +357,8 @@ class BaseQuerySet(object): try: inserted_result = insert_func(raw, **write_concern) - ids = inserted_result.inserted_id if return_one else inserted_result.inserted_ids + ids = return_one and inserted_result.inserted_id or\ + inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) @@ -374,7 +375,6 @@ class BaseQuerySet(object): signals.post_bulk_insert.send( self._document, documents=docs, loaded=False, **signal_kwargs) return return_one and ids[0] or ids - documents = self.in_bulk(ids) results = [] for obj_id in ids: From 0fc55451c2aaad6a612d28ead0b58898a2236e40 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 14:41:25 +0800 Subject: [PATCH 208/268] fix style and ids need to be an array --- mongoengine/queryset/base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 2f2d38dd..0634dceb 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -357,8 +357,7 @@ class BaseQuerySet(object): try: inserted_result = insert_func(raw, **write_concern) - ids = return_one and inserted_result.inserted_id or\ - inserted_result.inserted_ids + ids = return_one and [inserted_result.inserted_id] or inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) From acba86993dd92b04fff289a151cb8882913bbf20 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 15:43:19 +0800 Subject: [PATCH 209/268] set_write_concern pymongo3 --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 0634dceb..0a3f65fb 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -356,7 +356,7 @@ class BaseQuerySet(object): insert_func = self._collection.insert_one try: - inserted_result = insert_func(raw, **write_concern) + inserted_result = insert_func(raw, set_write_concern(write_concern)) ids = return_one and [inserted_result.inserted_id] or inserted_result.inserted_ids except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' From fa4ac95ecc245c59040664905c935d3c85b49429 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 17:49:49 +0800 Subject: [PATCH 210/268] catch bulkwriteerror --- mongoengine/queryset/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 008e7f15..391cc819 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -371,6 +371,11 @@ class BaseQuerySet(object): message = u'Tried to save duplicate unique keys (%s)' raise NotUniqueError(message % six.text_type(err)) raise OperationError(message % six.text_type(err)) + except pymongo.error.BulkWriteError as err: + # inserting documents that already have an _id field will + # give huge performance debt or raise + message = u'Document must not have _id value before bulk write (%s)' + raise NotUniqueError(message % sx.text_type(err)) if not load_bulk: signals.post_bulk_insert.send( From 78601d90c9fd13b20d1e6cfe2342ddb23a520e33 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 17:54:13 +0800 Subject: [PATCH 211/268] fix typo --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 391cc819..4c8309a7 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -375,7 +375,7 @@ class BaseQuerySet(object): # inserting documents that already have an _id field will # give huge performance debt or raise message = u'Document must not have _id value before bulk write (%s)' - raise NotUniqueError(message % sx.text_type(err)) + raise NotUniqueError(message % six.text_type(err)) if not load_bulk: signals.post_bulk_insert.send( From 94cda90a6ead7a64059328c58dafa6154436c951 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 17:56:19 +0800 Subject: [PATCH 212/268] fix syntax --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 4c8309a7..bba63da0 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -371,7 +371,7 @@ class BaseQuerySet(object): message = u'Tried to save duplicate unique keys (%s)' raise NotUniqueError(message % six.text_type(err)) raise OperationError(message % six.text_type(err)) - except pymongo.error.BulkWriteError as err: + except pymongo.errors.BulkWriteError as err: # inserting documents that already have an _id field will # give huge performance debt or raise message = u'Document must not have _id value before bulk write (%s)' From 088fd6334bfe8316c2df2466ad93ee568e3013c6 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 18:06:59 +0800 Subject: [PATCH 213/268] bulkwriteerror does not trigger --- mongoengine/queryset/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index bba63da0..647eafc6 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -363,6 +363,11 @@ class BaseQuerySet(object): except pymongo.errors.DuplicateKeyError as err: message = 'Could not save document (%s)' raise NotUniqueError(message % six.text_type(err)) + except pymongo.errors.BulkWriteError as err: + # inserting documents that already have an _id field will + # give huge performance debt or raise + message = u'Document must not have _id value before bulk write (%s)' + raise NotUniqueError(message % six.text_type(err)) except pymongo.errors.OperationFailure as err: message = 'Could not save document (%s)' if re.match('^E1100[01] duplicate key', six.text_type(err)): @@ -371,11 +376,6 @@ class BaseQuerySet(object): message = u'Tried to save duplicate unique keys (%s)' raise NotUniqueError(message % six.text_type(err)) raise OperationError(message % six.text_type(err)) - except pymongo.errors.BulkWriteError as err: - # inserting documents that already have an _id field will - # give huge performance debt or raise - message = u'Document must not have _id value before bulk write (%s)' - raise NotUniqueError(message % six.text_type(err)) if not load_bulk: signals.post_bulk_insert.send( From 506168ab83a11dc22c706dd308f77f6233543d57 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 18:25:50 +0800 Subject: [PATCH 214/268] use write_concern class --- tests/queryset/queryset.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 5c3f179f..4c0085d0 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -927,8 +927,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Blog.objects.count(), 2) - Blog.objects.insert([blog2, blog3], - write_concern={"w": 0, 'continue_on_error': True}) + Blog.objects.insert([blog2, blog3], write_concern={"w": 0}) self.assertEqual(Blog.objects.count(), 3) def test_get_changed_fields_query_count(self): From fc5d9ae10033e7e083931d9a64b7137feadc6eb6 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Sun, 20 May 2018 18:39:22 +0800 Subject: [PATCH 215/268] pymongo3 does not support continue_on_error --- tests/queryset/queryset.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 4c0085d0..3fa86a58 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -927,9 +927,6 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Blog.objects.count(), 2) - Blog.objects.insert([blog2, blog3], write_concern={"w": 0}) - self.assertEqual(Blog.objects.count(), 3) - def test_get_changed_fields_query_count(self): """Make sure we don't perform unnecessary db operations when none of document's fields were updated. From 2adb640821123439c9b611375d01aba1d86b3b26 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 09:19:03 +0800 Subject: [PATCH 216/268] modify bulk_insert test for pymongo3 --- tests/queryset/queryset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 3fa86a58..50825e5c 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -846,10 +846,10 @@ class QuerySetTest(unittest.TestCase): Blog.objects.insert(blogs, load_bulk=False) if mongodb_version < (2, 6): - self.assertEqual(q, 1) + self.assertEqual(q, 99) else: # profiling logs each doc now in the bulk op - self.assertEqual(q, 99) + self.assertEqual(q, 1) Blog.drop_collection() Blog.ensure_indexes() @@ -859,7 +859,7 @@ class QuerySetTest(unittest.TestCase): Blog.objects.insert(blogs) if mongodb_version < (2, 6): - self.assertEqual(q, 2) # 1 for insert, and 1 for in bulk fetch + self.assertEqual(q, 101) # 100 for insert, and 1 for in bulk fetch else: # 99 for insert, and 1 for in bulk fetch self.assertEqual(q, 100) From e4451ccaf8616e27c3479e97bcb75daf76396488 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 09:22:33 +0800 Subject: [PATCH 217/268] insert_many uses only one insert --- tests/queryset/queryset.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 50825e5c..8e91feae 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -845,11 +845,8 @@ class QuerySetTest(unittest.TestCase): blogs.append(Blog(title="post %s" % i, posts=[post1, post2])) Blog.objects.insert(blogs, load_bulk=False) - if mongodb_version < (2, 6): - self.assertEqual(q, 99) - else: - # profiling logs each doc now in the bulk op - self.assertEqual(q, 1) + # profiling logs each doc now in the bulk op + self.assertEqual(q, 99) Blog.drop_collection() Blog.ensure_indexes() @@ -861,8 +858,8 @@ class QuerySetTest(unittest.TestCase): if mongodb_version < (2, 6): self.assertEqual(q, 101) # 100 for insert, and 1 for in bulk fetch else: - # 99 for insert, and 1 for in bulk fetch - self.assertEqual(q, 100) + # 1 for insert, and 1 for in bulk fetch + self.assertEqual(q, 2) Blog.drop_collection() From f7a3acfaf45673ee379fc09de86f4fa484e907bc Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 09:34:44 +0800 Subject: [PATCH 218/268] query profiler test fix --- tests/queryset/queryset.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 8e91feae..9c7ac0e4 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -855,11 +855,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(q, 0) Blog.objects.insert(blogs) - if mongodb_version < (2, 6): - self.assertEqual(q, 101) # 100 for insert, and 1 for in bulk fetch - else: - # 1 for insert, and 1 for in bulk fetch - self.assertEqual(q, 2) + self.assertEqual(q, 100) # 99 for insert 1 for fetch Blog.drop_collection() From 3f9ff7254f206e0926c6ab31cc3a1a1efe39ad2d Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 09:46:23 +0800 Subject: [PATCH 219/268] fix queryset tests --- tests/queryset/queryset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 9c7ac0e4..dea5b110 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -970,7 +970,10 @@ class QuerySetTest(unittest.TestCase): org = Organization.objects.get(id=o1.id) with query_counter() as q: org.save(cascade=False) - self.assertEqual(q, 0) + if mongodb_version >= (3, 0): + self.assertEqual(q, 1) + else: + self.assertEqual(q, 0) # Saving a doc after you append a reference to it should result in # two db operations (a query for the reference and an update). From 6fb5c312c3a8d753c4a49a6e65bbc9db3d84f5bb Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 09:54:19 +0800 Subject: [PATCH 220/268] fix test error --- tests/queryset/queryset.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index dea5b110..2b12d261 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -967,6 +967,11 @@ class QuerySetTest(unittest.TestCase): org.save() self.assertEqual(q, 0) + # get MongoDB version info + connection = get_connection() + info = connection.test.command('buildInfo') + mongodb_version = tuple([int(i) for i in info['version'].split('.')]) + org = Organization.objects.get(id=o1.id) with query_counter() as q: org.save(cascade=False) From 9bd328e147122133a64f00a71c5829c8803217ed Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 10:04:59 +0800 Subject: [PATCH 221/268] query_counter fix --- tests/queryset/queryset.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 2b12d261..9c7ac0e4 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -967,18 +967,10 @@ class QuerySetTest(unittest.TestCase): org.save() self.assertEqual(q, 0) - # get MongoDB version info - connection = get_connection() - info = connection.test.command('buildInfo') - mongodb_version = tuple([int(i) for i in info['version'].split('.')]) - org = Organization.objects.get(id=o1.id) with query_counter() as q: org.save(cascade=False) - if mongodb_version >= (3, 0): - self.assertEqual(q, 1) - else: - self.assertEqual(q, 0) + self.assertEqual(q, 0) # Saving a doc after you append a reference to it should result in # two db operations (a query for the reference and an update). From 290b821a3ae2e130192a90bb62ad5ae8d5461005 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Sat, 2 Sep 2017 02:05:27 +0900 Subject: [PATCH 222/268] add fix for reload(fields) affect changed fields #1371 --- mongoengine/document.py | 5 +++-- tests/document/instance.py | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 2de0b1a3..182733c7 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -705,7 +705,6 @@ class Document(BaseDocument): obj = obj[0] else: raise self.DoesNotExist('Document does not exist') - for field in obj._data: if not fields or field in fields: try: @@ -721,7 +720,9 @@ class Document(BaseDocument): # i.e. obj.update(unset__field=1) followed by obj.reload() delattr(self, field) - self._changed_fields = obj._changed_fields + self._changed_fields = list( + set(self._changed_fields) - set(fields) + ) if fields else obj._changed_fields self._created = False return self diff --git a/tests/document/instance.py b/tests/document/instance.py index 38c7fcaf..b255e8a6 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -476,6 +476,24 @@ class InstanceTest(unittest.TestCase): doc.save() doc.reload() + def test_reload_with_changed_fields(self): + """Ensures reloading will not affect changed fields""" + class User(Document): + name = StringField() + number = IntField() + User.drop_collection() + + user = User(name="Bob", number=1).save() + user.name = "John" + user.number = 2 + + self.assertEqual(user._get_changed_fields(), ['name', 'number']) + user.reload('number') + self.assertEqual(user._get_changed_fields(), ['name']) + user.save() + user.reload() + self.assertEqual(user.name, "John") + def test_reload_referencing(self): """Ensures reloading updates weakrefs correctly.""" class Embedded(EmbeddedDocument): @@ -521,7 +539,7 @@ class InstanceTest(unittest.TestCase): doc.save() doc.dict_field['extra'] = 1 doc = doc.reload(10, 'list_field') - self.assertEqual(doc._get_changed_fields(), []) + self.assertEqual(doc._get_changed_fields(), ['dict_field.extra']) self.assertEqual(len(doc.list_field), 5) self.assertEqual(len(doc.dict_field), 3) self.assertEqual(len(doc.embedded_field.list_field), 4) From d424583cbf26de5c9b4f3ac47ec9b4cb52549fd5 Mon Sep 17 00:00:00 2001 From: Erdenezul Date: Sat, 2 Sep 2017 12:00:57 +0900 Subject: [PATCH 223/268] fix flake8 error #1371 --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 182733c7..109f8f82 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -722,7 +722,7 @@ class Document(BaseDocument): self._changed_fields = list( set(self._changed_fields) - set(fields) - ) if fields else obj._changed_fields + ) if fields else obj._changed_fields self._created = False return self From 784386fddcc3d171791fb710e332d72d29c009a5 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 21 May 2018 16:07:08 +0800 Subject: [PATCH 224/268] add changelog #1371 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d8765d17..08e5a490 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,7 @@ dev - Use insert_one, insert_many in Document.insert #1491 - Use new update_one, update_many on document/queryset update #1491 - Use insert_one, insert_many in Document.insert #1491 +- Fix reload(fields) affect changed fields #1371 Changes in 0.15.0 ================= From 446c1010185bd7ca836e686d6513f8d7c07828c5 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 23 May 2018 15:53:30 +0800 Subject: [PATCH 225/268] dont call ensure_indexes on slave #1338 --- mongoengine/document.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 109f8f82..7d03bd60 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -195,7 +195,9 @@ class Document(BaseDocument): # Ensure indexes on the collection unless auto_create_index was # set to False. - if cls._meta.get('auto_create_index', True): + # Also there is no need to ensure indexes on slave. + if cls._meta.get('auto_create_index', True) and\ + db.client.is_primary: cls.ensure_indexes() return cls._collection From e8e47c39d78b25a6af9e6c77da1c2ce6fb5b88f7 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 23 May 2018 15:54:44 +0800 Subject: [PATCH 226/268] add changelog #1338 --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 08e5a490..d5ee2b23 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,7 @@ dev - Use new update_one, update_many on document/queryset update #1491 - Use insert_one, insert_many in Document.insert #1491 - Fix reload(fields) affect changed fields #1371 +- Fix Read-only access to database fails when trying to create indexes #1338 Changes in 0.15.0 ================= From 8739ab9c66e2cf84c3c206b95a5468263967b9d9 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Wed, 23 May 2018 15:59:18 +0800 Subject: [PATCH 227/268] fix syntax #1338 --- mongoengine/document.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mongoengine/document.py b/mongoengine/document.py index 7d03bd60..0d471c3a 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -196,6 +196,7 @@ class Document(BaseDocument): # Ensure indexes on the collection unless auto_create_index was # set to False. # Also there is no need to ensure indexes on slave. + db = cls._get_db() if cls._meta.get('auto_create_index', True) and\ db.client.is_primary: cls.ensure_indexes() From 080226dd7289bb3973c46b76eba15174dd8c6977 Mon Sep 17 00:00:00 2001 From: Tal Yalon Date: Fri, 22 Jun 2018 14:16:17 +0300 Subject: [PATCH 228/268] Fix issue #1286 and #844.: when building a query set from filters that reference the same field several times, do not assume each value is a dict --- mongoengine/queryset/transform.py | 2 +- tests/queryset/queryset.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 5f777f41..f450c8a3 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -147,7 +147,7 @@ def query(_doc_cls=None, **kwargs): if op is None or key not in mongo_query: mongo_query[key] = value elif key in mongo_query: - if isinstance(mongo_query[key], dict): + if isinstance(mongo_query[key], dict) and isinstance(value, dict): mongo_query[key].update(value) # $max/minDistance needs to come last - convert to SON value_dict = mongo_query[key] diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 497a0d23..9b1b3256 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -1202,6 +1202,14 @@ class QuerySetTest(unittest.TestCase): BlogPost.drop_collection() Blog.drop_collection() + def test_filter_chaining_with_regex(self): + person = self.Person(name='Guido van Rossum') + person.save() + + people = self.Person.objects + people = people.filter(name__startswith='Gui').filter(name__not__endswith='tum') + self.assertEqual(people.count(), 1) + def assertSequence(self, qs, expected): qs = list(qs) expected = list(expected) From d0741946c7f982286cb709c23850f95769414ec6 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 28 Jun 2018 10:19:48 -0500 Subject: [PATCH 229/268] Fix a typo (#1813) an -> a --- mongoengine/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index 0d471c3a..c8532e5b 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -715,7 +715,7 @@ class Document(BaseDocument): except (KeyError, AttributeError): try: # If field is a special field, e.g. items is stored as _reserved_items, - # an KeyError is thrown. So try to retrieve the field from _data + # a KeyError is thrown. So try to retrieve the field from _data setattr(self, field, self._reload(field, obj._data.get(field))) except KeyError: # If field is removed from the database while the object From b070e7de0765f5d364474c832fb28d91e04a884e Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 29 Jun 2018 15:47:28 +0200 Subject: [PATCH 230/268] Fix travis's pypi release trigger --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 29a72d1c..381f7385 100644 --- a/.travis.yml +++ b/.travis.yml @@ -91,11 +91,11 @@ deploy: distributions: "sdist bdist_wheel" # only deploy on tagged commits (aka GitHub releases) and only for the - # parent repo's builds running Python 2.7 along with PyMongo v3.0 (we run + # parent repo's builds running Python 2.7 along with PyMongo v3.x (we run # Travis against many different Python and PyMongo versions and we don't # want the deploy to occur multiple times). on: tags: true repo: MongoEngine/mongoengine - condition: "$PYMONGO = 3.0" + condition: "$PYMONGO = 3.x" python: 2.7 From fdba648afb4141a7dbd768435a3847c265713cbe Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 16 Jul 2018 09:19:50 +0800 Subject: [PATCH 231/268] fix pypi trigger --- mongoengine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index a1b7d682..e6dc6b9d 100644 --- a/mongoengine/__init__.py +++ b/mongoengine/__init__.py @@ -23,7 +23,7 @@ __all__ = (list(document.__all__) + list(fields.__all__) + list(signals.__all__) + list(errors.__all__)) -VERSION = (0, 15, 0) +VERSION = (0, 15, 3) def get_version(): From 1241a902e3a0d5b1fac919d77dc191fd47a4d966 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 16 Jul 2018 09:49:16 +0800 Subject: [PATCH 232/268] fix changelog --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d5ee2b23..7eaed75a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,8 +2,8 @@ Changelog ========= -dev -=== +Changes in 0.15.3 +================= - 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 From ae783d4f45bc65daeae9f2fbbf1364ac4161e0e6 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Mon, 16 Jul 2018 17:25:29 +0800 Subject: [PATCH 233/268] tweak tox.ini to pass tests --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2f2b1757..815d2acc 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,6 @@ commands = deps = nose mg35: PyMongo==3.5 - mg3x: PyMongo>=3.0 + mg3x: PyMongo>=3.0,<3.7 setenv = PYTHON_EGG_CACHE = {envdir}/python-eggs From 62eadbc17468b079b2da40ef151b190202e98b1d Mon Sep 17 00:00:00 2001 From: Gram Date: Fri, 20 Jul 2018 17:21:57 +0300 Subject: [PATCH 234/268] +date field --- mongoengine/fields.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index a661874a..d631e882 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -525,6 +525,22 @@ class DateTimeField(BaseField): return super(DateTimeField, self).prepare_query_value(op, self.to_mongo(value)) +class DateField(DateTimeField): + def to_mongo(self, value): + value = super(DateField, self).to_mongo(value) + # drop hours, minutes, seconds + if isinstance(value, datetime.datetime): + value = datetime.datetime(value.year, value.month, value.day) + return value + + def to_python(self, value): + value = super(DateField, self).to_python(value) + # convert datetime to date + if isinstance(value, datetime.datetime): + value = datetime.date(value.year, value.month, value.day) + return value + + class ComplexDateTimeField(StringField): """ ComplexDateTimeField handles microseconds exactly instead of rounding From 19a6e324c45a2bb4307802d7b40e3805084b567d Mon Sep 17 00:00:00 2001 From: Gram Date: Fri, 20 Jul 2018 17:37:23 +0300 Subject: [PATCH 235/268] +tests for date field --- mongoengine/fields.py | 2 +- tests/fields/fields.py | 145 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index d631e882..f9622b31 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -43,7 +43,7 @@ except ImportError: __all__ = ( 'StringField', 'URLField', 'EmailField', 'IntField', 'LongField', - 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', + 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'DateField', 'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField', 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', 'SortedListField', 'EmbeddedDocumentListField', 'DictField', diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 0b9710c3..7352d242 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -46,6 +46,17 @@ class FieldTest(MongoDBTestCase): 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 @@ -57,6 +68,17 @@ class FieldTest(MongoDBTestCase): 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. @@ -66,13 +88,14 @@ class FieldTest(MongoDBTestCase): age = IntField(default=30, required=False) userid = StringField(default=lambda: 'test', required=True) created = DateTimeField(default=datetime.datetime.utcnow) + day = DateField(default=datetime.date.today) person = Person(name="Ross") # Confirm saving now would store values data_to_be_saved = sorted(person.to_mongo().keys()) self.assertEqual(data_to_be_saved, - ['age', 'created', 'name', 'userid'] + ['age', 'created', 'day', 'name', 'userid'] ) self.assertTrue(person.validate() is None) @@ -81,16 +104,18 @@ class FieldTest(MongoDBTestCase): self.assertEqual(person.age, person.age) self.assertEqual(person.userid, person.userid) self.assertEqual(person.created, person.created) + self.assertEqual(person.day, person.day) self.assertEqual(person._data['name'], person.name) self.assertEqual(person._data['age'], person.age) self.assertEqual(person._data['userid'], person.userid) self.assertEqual(person._data['created'], person.created) + self.assertEqual(person._data['day'], person.day) # Confirm introspection changes nothing data_to_be_saved = sorted(person.to_mongo().keys()) self.assertEqual( - data_to_be_saved, ['age', 'created', 'name', 'userid']) + data_to_be_saved, ['age', 'created', 'day', 'name', 'userid']) def test_default_values_set_to_None(self): """Ensure that default field values are used even when @@ -662,6 +687,32 @@ class FieldTest(MongoDBTestCase): 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 @@ -733,6 +784,51 @@ class FieldTest(MongoDBTestCase): 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): @@ -787,6 +883,51 @@ class FieldTest(MongoDBTestCase): ) 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_complexdatetime_storage(self): """Tests for complex datetime fields - which can handle microseconds without rounding. From 6da27e59760d8498344ddb1628b7f5d3d5ca3d5f Mon Sep 17 00:00:00 2001 From: Gram Date: Fri, 27 Jul 2018 12:31:27 +0300 Subject: [PATCH 236/268] +changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7eaed75a..9d9fa976 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,10 @@ Changelog ========= +Changes in 0.15.4 +================= +- Added `DateField` #513 + Changes in 0.15.3 ================= - Subfield resolve error in generic_emdedded_document query #1651 #1652 From 3d45cdc339a08717267b94c7cfe47cb99c2afec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 12 Jun 2018 20:59:12 +0200 Subject: [PATCH 237/268] Implemented lazy regex compiling in Field classes to improve 'import mongoengine' performance --- mongoengine/base/utils.py | 22 ++++++++++++++++++++ mongoengine/common.py | 1 + mongoengine/fields.py | 10 ++++----- mongoengine/python_support.py | 6 +----- tests/test_utils.py | 38 +++++++++++++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 mongoengine/base/utils.py create mode 100644 tests/test_utils.py diff --git a/mongoengine/base/utils.py b/mongoengine/base/utils.py new file mode 100644 index 00000000..288c2f3e --- /dev/null +++ b/mongoengine/base/utils.py @@ -0,0 +1,22 @@ +import re + + +class LazyRegexCompiler(object): + """Descriptor to allow lazy compilation of regex""" + + def __init__(self, pattern, flags=0): + self._pattern = pattern + self._flags = flags + self._compiled_regex = None + + @property + def compiled_regex(self): + if self._compiled_regex is None: + self._compiled_regex = re.compile(self._pattern, self._flags) + return self._compiled_regex + + def __get__(self, obj, objtype): + return self.compiled_regex + + def __set__(self, instance, value): + raise AttributeError("Can not set attribute LazyRegexCompiler") diff --git a/mongoengine/common.py b/mongoengine/common.py index bde7e78c..2e64130f 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -56,3 +56,4 @@ def _import_class(cls_name): _class_registry_cache[cls] = getattr(module, cls) return _class_registry_cache.get(cls_name) + diff --git a/mongoengine/fields.py b/mongoengine/fields.py index f9622b31..28ab25c7 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -5,7 +5,6 @@ import re import socket import time import uuid -import warnings from operator import itemgetter from bson import Binary, DBRef, ObjectId, SON @@ -28,6 +27,7 @@ except ImportError: from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField, LazyReference, ObjectIdField, get_document) +from mongoengine.base.utils import LazyRegexCompiler from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.document import Document, EmbeddedDocument @@ -123,7 +123,7 @@ class URLField(StringField): .. versionadded:: 0.3 """ - _URL_REGEX = re.compile( + _URL_REGEX = LazyRegexCompiler( r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(?= 3 # six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3. StringIO = six.BytesIO diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..562cc1ff --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,38 @@ +import unittest +import re + +from mongoengine.base.utils import LazyRegexCompiler + +signal_output = [] + + +class LazyRegexCompilerTest(unittest.TestCase): + + def test_lazy_regex_compiler_verify_laziness_of_descriptor(self): + class UserEmail(object): + EMAIL_REGEX = LazyRegexCompiler('@', flags=32) + + descriptor = UserEmail.__dict__['EMAIL_REGEX'] + self.assertIsNone(descriptor._compiled_regex) + + regex = UserEmail.EMAIL_REGEX + self.assertEqual(regex, re.compile('@', flags=32)) + self.assertEqual(regex.search('user@domain.com').group(), '@') + + user_email = UserEmail() + self.assertIs(user_email.EMAIL_REGEX, UserEmail.EMAIL_REGEX) + + def test_lazy_regex_compiler_verify_cannot_set_descriptor_on_instance(self): + class UserEmail(object): + EMAIL_REGEX = LazyRegexCompiler('@') + + user_email = UserEmail() + with self.assertRaises(AttributeError): + user_email.EMAIL_REGEX = re.compile('@') + + def test_lazy_regex_compiler_verify_can_override_class_attr(self): + class UserEmail(object): + EMAIL_REGEX = LazyRegexCompiler('@') + + UserEmail.EMAIL_REGEX = re.compile('cookies') + self.assertEqual(UserEmail.EMAIL_REGEX.search('Cake & cookies').group(), 'cookies') From 727866f090668881e425b8a90491be1f23563737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sun, 5 Aug 2018 22:30:51 +0200 Subject: [PATCH 238/268] fix styling flake8 error from CI --- mongoengine/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mongoengine/common.py b/mongoengine/common.py index 2e64130f..bde7e78c 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -56,4 +56,3 @@ def _import_class(cls_name): _class_registry_cache[cls] = getattr(module, cls) return _class_registry_cache.get(cls_name) - From 820f7b4d9375d35bdadb1d6b49b0b0429faea3b5 Mon Sep 17 00:00:00 2001 From: JoschSan Date: Tue, 7 Aug 2018 14:29:15 -0400 Subject: [PATCH 239/268] Update connecting.rst My suggest is append the parameter in authentication_source in authentication example, because was included in https://github.com/MongoEngine/mongoengine/pull/590/files --- docs/guide/connecting.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst index f40ed4c5..5dac6ae9 100644 --- a/docs/guide/connecting.rst +++ b/docs/guide/connecting.rst @@ -18,10 +18,10 @@ provide the :attr:`host` and :attr:`port` arguments to connect('project1', host='192.168.1.35', port=12345) -If the database requires authentication, :attr:`username` and :attr:`password` -arguments should be provided:: +If the database requires authentication, :attr:`username`, :attr:`password` +and :attr:`authentication_source` arguments should be provided:: - connect('project1', username='webapp', password='pwd123') + connect('project1', username='webapp', password='pwd123', authentication_source='admin') URI style connections are also supported -- just supply the URI as the :attr:`host` to From 1a2b1f283bc4dac8cd853fc6207d63185be3c012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Mon, 13 Aug 2018 21:21:33 +0200 Subject: [PATCH 240/268] Update signals doc - clarification on EmbeddedDocument Since there is a .save() method on EmbeddedDocument, you could be tempted to attach a pre_save event to an EmbeddedDocument (#1720). This update is an attempt to make this clearer. --- docs/guide/signals.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index eed382c4..06bccb3b 100644 --- a/docs/guide/signals.rst +++ b/docs/guide/signals.rst @@ -113,6 +113,10 @@ handlers within your subclass:: signals.pre_save.connect(Author.pre_save, sender=Author) signals.post_save.connect(Author.post_save, sender=Author) +.. warning:: + + Note that EmbeddedDocument only supports pre/post_init signals. pre/post_save, etc should be attached to Document's class only. Attaching pre_save to an EmbeddedDocument is ignored silently. + Finally, you can also use this small decorator to quickly create a number of signals and attach them to your :class:`~mongoengine.Document` or :class:`~mongoengine.EmbeddedDocument` subclasses as class decorators:: From 364813dd73f7deecaf197bbc5c25a1b63833f655 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Mon, 13 Aug 2018 22:34:37 +0200 Subject: [PATCH 241/268] Clarify that you should use python 2 when developing on mongoengine #1837 --- CONTRIBUTING.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 573d7060..f7b15c85 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,8 +22,11 @@ Supported Interpreters MongoEngine supports CPython 2.7 and newer. Language features not supported by all interpreters can not be used. -Please also ensure that your code is properly converted by -`2to3 `_ for Python 3 support. +The codebase is written in python 2 so you must be using python 2 +when developing new features. Compatibility of the library with Python 3 +relies on the 2to3 package that gets executed as part of the installation +build. You should ensure that your code is properly converted by +`2to3 `_. Style Guide ----------- From 1bcdcce93abdbf1c20410dcde395e20b6987cc2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sat, 18 Aug 2018 00:03:17 +0200 Subject: [PATCH 242/268] remove dead code --- mongoengine/base/document.py | 4 +--- mongoengine/document.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 4181bcd5..f03ce848 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -100,13 +100,11 @@ class BaseDocument(object): for key, value in values.iteritems(): if key in self._fields or key == '_id': setattr(self, key, value) - elif self._dynamic: + else: dynamic_data[key] = value else: FileField = _import_class('FileField') for key, value in values.iteritems(): - if key == '__auto_convert': - continue key = self._reverse_db_field_map.get(key, key) if key in self._fields or key in ('id', 'pk', '_cls'): if __auto_convert and value is not None: diff --git a/mongoengine/document.py b/mongoengine/document.py index c8532e5b..b9db8bc6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -1000,7 +1000,7 @@ class Document(BaseDocument): class DynamicDocument(Document): """A Dynamic Document class allowing flexible, expandable and uncontrolled schemas. As a :class:`~mongoengine.Document` subclass, acts in the same - way as an ordinary document but has expando style properties. Any data + way as an ordinary document but has expanded style properties. Any data passed or set against the :class:`~mongoengine.DynamicDocument` that is not a field is automatically converted into a :class:`~mongoengine.fields.DynamicField` and data can be attributed to that From 461b789515306d2e339310aef691367c52204c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sun, 19 Aug 2018 22:54:37 +0200 Subject: [PATCH 243/268] relates to (#710) - Update gridfs.rst to make it clearer that you should save the Document hosting the GridFSProxy after calling .delete() or .replace() on the GridFSProxy - updated GridFSProxy.__str__ so that it would always print both the filename and the grid_id. This should improve debugging --- docs/guide/gridfs.rst | 6 ++++-- mongoengine/fields.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/guide/gridfs.rst b/docs/guide/gridfs.rst index 68e7a6d2..f7380e89 100644 --- a/docs/guide/gridfs.rst +++ b/docs/guide/gridfs.rst @@ -53,7 +53,8 @@ Deletion Deleting stored files is achieved with the :func:`delete` method:: - marmot.photo.delete() + marmot.photo.delete() # Deletes the GridFS document + marmot.save() # Saves the GridFS reference (being None) contained in the marmot instance .. warning:: @@ -71,4 +72,5 @@ Files can be replaced with the :func:`replace` method. This works just like the :func:`put` method so even metadata can (and should) be replaced:: another_marmot = open('another_marmot.png', 'rb') - marmot.photo.replace(another_marmot, content_type='image/png') + marmot.photo.replace(another_marmot, content_type='image/png') # Replaces the GridFS document + marmot.save() # Replaces the GridFS reference contained in marmot instance diff --git a/mongoengine/fields.py b/mongoengine/fields.py index f9622b31..9648bb0b 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -1528,9 +1528,9 @@ class GridFSProxy(object): return '<%s: %s>' % (self.__class__.__name__, self.grid_id) def __str__(self): - name = getattr( - self.get(), 'filename', self.grid_id) if self.get() else '(no file)' - return '<%s: %s>' % (self.__class__.__name__, name) + gridout = self.get() + filename = getattr(gridout, 'filename') if gridout else '' + return '<%s: %s (%s)>' % (self.__class__.__name__, filename, self.grid_id) def __eq__(self, other): if isinstance(other, GridFSProxy): From 48a85ee6e000d744490faf5128e069d4094a4cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Mon, 20 Aug 2018 00:10:27 +0200 Subject: [PATCH 244/268] update related tests --- tests/fields/file_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 8364d5ef..841e7c7d 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -54,7 +54,7 @@ class FileTest(MongoDBTestCase): result = PutFile.objects.first() self.assertTrue(putfile == result) - self.assertEqual("%s" % result.the_file, "") + self.assertEqual("%s" % result.the_file, "" % result.the_file.grid_id) self.assertEqual(result.the_file.read(), text) self.assertEqual(result.the_file.content_type, content_type) result.the_file.delete() # Remove file from GridFS From a7658c75735a5593a99e4b59895d09ecee30cb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 21 Aug 2018 18:01:12 +0200 Subject: [PATCH 245/268] fix BaseList.__iter__ operator (#1305) + minor improvements --- AUTHORS | 1 + mongoengine/base/datastructures.py | 8 ++++---- tests/test_datastructures.py | 17 ++++++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/AUTHORS b/AUTHORS index 2e7b56fc..b38825dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -246,3 +246,4 @@ that much better: * Renjianxin (https://github.com/Davidrjx) * Erdenezul Batmunkh (https://github.com/erdenezul) * Andy Yankovsky (https://github.com/werat) + * Bastien Gérard (https://github.com/bagerard) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index fddd945a..8948243e 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -128,8 +128,8 @@ class BaseList(list): return value def __iter__(self): - for i in six.moves.range(self.__len__()): - yield self[i] + for v in super(BaseList, self).__iter__(): + yield v def __setitem__(self, key, value, *args, **kwargs): if isinstance(key, slice): @@ -138,7 +138,7 @@ class BaseList(list): self._mark_as_changed(key) return super(BaseList, self).__setitem__(key, value) - def __delitem__(self, key, *args, **kwargs): + def __delitem__(self, key): self._mark_as_changed() return super(BaseList, self).__delitem__(key) @@ -187,7 +187,7 @@ class BaseList(list): self._mark_as_changed() return super(BaseList, self).remove(*args, **kwargs) - def reverse(self, *args, **kwargs): + def reverse(self): self._mark_as_changed() return super(BaseList, self).reverse() diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 79381c5a..1ea562a5 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,21 @@ import unittest -from mongoengine.base.datastructures import StrictDict +from mongoengine.base.datastructures import StrictDict, BaseList + + +class TestBaseList(unittest.TestCase): + + def test_iter_simple(self): + values = [True, False, True, False] + base_list = BaseList(values, instance=None, name='my_name') + self.assertEqual(values, list(base_list)) + + def test_iter_allow_modification_while_iterating_withou_error(self): + # regular list allows for this, thus this subclass must comply to that + base_list = BaseList([True, False, True, False], instance=None, name='my_name') + for idx, val in enumerate(base_list): + if val: + base_list.pop(idx) class TestStrictDict(unittest.TestCase): From 86548fc7bfcd73d75f5cf6fbc8cba690294d8253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 28 Aug 2018 22:42:51 +0200 Subject: [PATCH 246/268] Document the attribute of .from_json --- mongoengine/base/document.py | 10 +++- tests/document/instance.py | 92 ++++++++++++++++++++++++++++-------- tests/queryset/queryset.py | 1 - 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index f03ce848..85906a3e 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -404,7 +404,15 @@ class BaseDocument(object): @classmethod def from_json(cls, json_data, created=False): - """Converts json data to an unsaved document instance""" + """Converts json data to a Document instance + + :param json_data: The json data to load into the Document + :param created: If True, the document will be considered as a brand new document + If False and an id is provided, it will consider that the data being + loaded corresponds to what's already in the database (This has an impact of subsequent call to .save()) + If False and no id is provided, it will consider the data as a new document + (default ``False``) + """ return cls._from_son(json_util.loads(json_data), created=created) def __expand_dynamic_values(self, name, value): diff --git a/tests/document/instance.py b/tests/document/instance.py index b255e8a6..ca0a5ad0 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -550,21 +550,14 @@ class InstanceTest(unittest.TestCase): pass f = Foo() - try: + with self.assertRaises(Foo.DoesNotExist): f.reload() - except Foo.DoesNotExist: - pass - except Exception: - self.assertFalse("Threw wrong exception") f.save() f.delete() - try: + + with self.assertRaises(Foo.DoesNotExist): f.reload() - except Foo.DoesNotExist: - pass - except Exception: - self.assertFalse("Threw wrong exception") def test_reload_of_non_strict_with_special_field_name(self): """Ensures reloading works for documents with meta strict == False.""" @@ -734,12 +727,12 @@ class InstanceTest(unittest.TestCase): t = TestDocument(status="draft", pub_date=datetime.now()) - try: + with self.assertRaises(ValidationError) as cm: t.save() - except ValidationError as e: - expect_msg = "Draft entries may not have a publication date." - self.assertTrue(expect_msg in e.message) - self.assertEqual(e.to_dict(), {'__all__': expect_msg}) + + expected_msg = "Draft entries may not have a publication date." + self.assertIn(expected_msg, cm.exception.message) + self.assertEqual(cm.exception.to_dict(), {'__all__': expected_msg}) t = TestDocument(status="published") t.save(clean=False) @@ -773,12 +766,13 @@ class InstanceTest(unittest.TestCase): TestDocument.drop_collection() t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25, z=15)) - try: + + with self.assertRaises(ValidationError) as cm: t.save() - except ValidationError as e: - expect_msg = "Value of z != x + y" - self.assertTrue(expect_msg in e.message) - self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}}) + + expected_msg = "Value of z != x + y" + self.assertIn(expected_msg, cm.exception.message) + self.assertEqual(cm.exception.to_dict(), {'doc': {'__all__': expected_msg}}) t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25)).save() self.assertEqual(t.doc.z, 35) @@ -3148,6 +3142,64 @@ class InstanceTest(unittest.TestCase): self.assertEquals(p.id, None) p.id = "12345" # in case it is not working: "OperationError: Shard Keys are immutable..." will be raised here + def test_from_son_created_False_without_id(self): + class MyPerson(Document): + name = StringField() + + MyPerson.objects.delete() + + p = MyPerson.from_json('{"name": "a_fancy_name"}', created=False) + self.assertFalse(p._created) + self.assertIsNone(p.id) + p.save() + self.assertIsNotNone(p.id) + saved_p = MyPerson.objects.get(id=p.id) + self.assertEqual(saved_p.name, 'a_fancy_name') + + def test_from_son_created_False_with_id(self): + # 1854 + class MyPerson(Document): + name = StringField() + + MyPerson.objects.delete() + + p = MyPerson.from_json('{"_id": "5b85a8b04ec5dc2da388296e", "name": "a_fancy_name"}', created=False) + self.assertFalse(p._created) + self.assertEqual(p._changed_fields, []) + self.assertEqual(p.name, 'a_fancy_name') + self.assertEqual(p.id, ObjectId('5b85a8b04ec5dc2da388296e')) + p.save() + + with self.assertRaises(DoesNotExist): + # Since created=False and we gave an id in the json and _changed_fields is empty + # mongoengine assumes that the document exits with that structure already + # and calling .save() didn't save anything + MyPerson.objects.get(id=p.id) + + self.assertFalse(p._created) + p.name = 'a new fancy name' + self.assertEqual(p._changed_fields, ['name']) + p.save() + saved_p = MyPerson.objects.get(id=p.id) + self.assertEqual(saved_p.name, p.name) + + def test_from_son_created_True_with_an_id(self): + class MyPerson(Document): + name = StringField() + + MyPerson.objects.delete() + + p = MyPerson.from_json('{"_id": "5b85a8b04ec5dc2da388296e", "name": "a_fancy_name"}', created=True) + self.assertTrue(p._created) + self.assertEqual(p._changed_fields, []) + self.assertEqual(p.name, 'a_fancy_name') + self.assertEqual(p.id, ObjectId('5b85a8b04ec5dc2da388296e')) + p.save() + + saved_p = MyPerson.objects.get(id=p.id) + self.assertEqual(saved_p, p) + self.assertEqual(p.name, 'a_fancy_name') + def test_null_field(self): # 734 class User(Document): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 497a0d23..a405e892 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4457,7 +4457,6 @@ class QuerySetTest(unittest.TestCase): self.assertNotEqual(bars._CommandCursor__collection.read_preference, ReadPreference.SECONDARY_PREFERRED) - def test_json_simple(self): class Embedded(EmbeddedDocument): From 576f23d5fb371315e2b54b6798915d8a103c3e7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 28 Aug 2018 22:58:13 +0200 Subject: [PATCH 247/268] Fix .delete doc of **write_concern as suggested by #1779 --- mongoengine/document.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mongoengine/document.py b/mongoengine/document.py index b9db8bc6..25af273d 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -585,9 +585,8 @@ class Document(BaseDocument): :param signal_kwargs: (optional) kwargs dictionary to be passed to the signal calls. :param write_concern: Extra keyword arguments are passed down which - will be used as options for the resultant - ``getLastError`` command. For example, - ``save(..., write_concern={w: 2, fsync: True}, ...)`` will + will be used as options for the resultant ``getLastError`` command. + For example, ``save(..., w: 2, fsync: True)`` will wait until at least two servers have recorded the write and will force an fsync on the primary server. From b4860de34dd922936b290d98c95e575c12c713d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Wed, 29 Aug 2018 23:04:18 +0200 Subject: [PATCH 248/268] Fix index creation error that was swallowed by hasattr under python2 (#1688) --- mongoengine/dereference.py | 7 ++++++- tests/document/instance.py | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 18b365cc..40bc72b2 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -133,7 +133,12 @@ class DeReference(object): """ object_map = {} for collection, dbrefs in self.reference_map.iteritems(): - if hasattr(collection, 'objects'): # We have a document class for the refs + + # we use getattr instead of hasattr because as hasattr swallows any exception under python2 + # so it could hide nasty things without raising exceptions (cfr bug #1688)) + ref_document_cls_exists = (getattr(collection, 'objects', None) is not None) + + if ref_document_cls_exists: col_name = collection._get_collection_name() refs = [dbref for dbref in dbrefs if (col_name, dbref) not in object_map] diff --git a/tests/document/instance.py b/tests/document/instance.py index b255e8a6..c933eaf4 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -8,9 +8,12 @@ import weakref from datetime import datetime from bson import DBRef, ObjectId +from pymongo.errors import DuplicateKeyError + from tests import fixtures from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, PickleDynamicEmbedded, PickleDynamicTest) +from tests.utils import MongoDBTestCase from mongoengine import * from mongoengine.base import get_document, _document_registry @@ -30,12 +33,9 @@ TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), __all__ = ("InstanceTest",) -class InstanceTest(unittest.TestCase): +class InstanceTest(MongoDBTestCase): def setUp(self): - connect(db='mongoenginetest') - self.db = get_db() - class Job(EmbeddedDocument): name = StringField() years = IntField() @@ -3248,6 +3248,23 @@ class InstanceTest(unittest.TestCase): blog.reload() self.assertEqual(blog.tags, [["value1", 123]]) + def test_accessing_objects_with_indexes_error(self): + insert_result = self.db.company.insert_many([{'name': 'Foo'}, + {'name': 'Foo'}]) # Force 2 doc with same name + REF_OID = insert_result.inserted_ids[0] + self.db.user.insert_one({'company': REF_OID}) # Force 2 doc with same name + + class Company(Document): + name = StringField(unique=True) + + class User(Document): + company = ReferenceField(Company) + + + # Ensure index creation exception aren't swallowed (#1688) + with self.assertRaises(DuplicateKeyError): + User.objects().select_related() + if __name__ == '__main__': unittest.main() From 4779106139306257fb9dfd8203ed7cc7c3310385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 30 Aug 2018 21:50:03 +0200 Subject: [PATCH 249/268] Improve doc of EmbeddedDocumentList.filter and clarify that it does not supports operators like __lte, __gte, etc --- mongoengine/base/datastructures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index 8948243e..db292f14 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -234,6 +234,9 @@ class EmbeddedDocumentList(BaseList): Filters the list by only including embedded documents with the given keyword arguments. + This method only supports simple comparison (e.g: .filter(name='John Doe')) + and does not support operators like __gte, __lte, __icontains like queryset.filter does + :param kwargs: The keyword arguments corresponding to the fields to filter on. *Multiple arguments are treated as if they are ANDed together.* From 5dbee2a2708722a4e9835ac5738991ba394954c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 30 Aug 2018 16:03:16 +0200 Subject: [PATCH 250/268] Ensures EmbeddedDocumentField does not accepts references to Document classes in its constructor --- mongoengine/fields.py | 12 +++++++++-- tests/fields/fields.py | 47 ++++++++++++++++++++++++++++++++++++++++++ tests/utils.py | 8 +++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 16f3185f..a54d3a52 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -645,9 +645,17 @@ class EmbeddedDocumentField(BaseField): def document_type(self): if isinstance(self.document_type_obj, six.string_types): if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT: - self.document_type_obj = self.owner_document + resolved_document_type = self.owner_document else: - self.document_type_obj = get_document(self.document_type_obj) + resolved_document_type = get_document(self.document_type_obj) + + if not issubclass(resolved_document_type, EmbeddedDocument): + # Due to the late resolution of the document_type + # There is a chance that it won't be an EmbeddedDocument (#1661) + self.error('Invalid embedded document class provided to an ' + 'EmbeddedDocumentField') + self.document_type_obj = resolved_document_type + return self.document_type_obj def to_python(self, value): diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 7352d242..362acec4 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -2147,6 +2147,15 @@ class FieldTest(MongoDBTestCase): ])) self.assertEqual(a.b.c.txt, 'hi') + def test_embedded_document_field_cant_reference_using_a_str_if_it_does_not_exist_yet(self): + raise SkipTest("Using a string reference in an EmbeddedDocumentField does not work if the class isnt registerd yet") + + class MyDoc2(Document): + emb = EmbeddedDocumentField('MyDoc') + + class MyDoc(EmbeddedDocument): + name = StringField() + def test_embedded_document_validation(self): """Ensure that invalid embedded documents cannot be assigned to embedded document fields. @@ -4388,6 +4397,44 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.assertEqual(custom_data['a'], CustomData.c_field.custom_data['a']) +class TestEmbeddedDocumentField(MongoDBTestCase): + def test___init___(self): + class MyDoc(EmbeddedDocument): + name = StringField() + + field = EmbeddedDocumentField(MyDoc) + self.assertEqual(field.document_type_obj, MyDoc) + + field2 = EmbeddedDocumentField('MyDoc') + self.assertEqual(field2.document_type_obj, 'MyDoc') + + def test___init___throw_error_if_document_type_is_not_EmbeddedDocument(self): + with self.assertRaises(ValidationError): + EmbeddedDocumentField(dict) + + def test_document_type_throw_error_if_not_EmbeddedDocument_subclass(self): + + class MyDoc(Document): + name = StringField() + + emb = EmbeddedDocumentField('MyDoc') + with self.assertRaises(ValidationError) as ctx: + emb.document_type + self.assertIn('Invalid embedded document class provided to an EmbeddedDocumentField', str(ctx.exception)) + + def test_embedded_document_field_only_allow_subclasses_of_embedded_document(self): + # Relates to #1661 + class MyDoc(Document): + name = StringField() + + with self.assertRaises(ValidationError): + class MyFailingDoc(Document): + emb = EmbeddedDocumentField(MyDoc) + + with self.assertRaises(ValidationError): + class MyFailingdoc2(Document): + emb = EmbeddedDocumentField('MyDoc') + class CachedReferenceFieldTest(MongoDBTestCase): def test_cached_reference_field_get_and_save(self): diff --git a/tests/utils.py b/tests/utils.py index 4566d864..acd318c5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,12 +7,12 @@ from mongoengine.connection import get_db, get_connection from mongoengine.python_support import IS_PYMONGO_3 -MONGO_TEST_DB = 'mongoenginetest' +MONGO_TEST_DB = 'mongoenginetest' # standard name for the test database class MongoDBTestCase(unittest.TestCase): """Base class for tests that need a mongodb connection - db is being dropped automatically + It ensures that the db is clean at the beginning and dropped at the end automatically """ @classmethod @@ -32,6 +32,7 @@ def get_mongodb_version(): """ return tuple(get_connection().server_info()['versionArray']) + def _decorated_with_ver_requirement(func, ver_tuple): """Return a given function decorated with the version requirement for a particular MongoDB version tuple. @@ -50,18 +51,21 @@ def _decorated_with_ver_requirement(func, ver_tuple): return _inner + def needs_mongodb_v26(func): """Raise a SkipTest exception if we're working with MongoDB version lower than v2.6. """ return _decorated_with_ver_requirement(func, (2, 6)) + def needs_mongodb_v3(func): """Raise a SkipTest exception if we're working with MongoDB version lower than v3.0. """ return _decorated_with_ver_requirement(func, (3, 0)) + def skip_pymongo3(f): """Raise a SkipTest exception if we're running a test against PyMongo v3.x. From d9fce49b082d774163f355d17ad432f0627ae086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 30 Aug 2018 22:46:37 +0200 Subject: [PATCH 251/268] minor improvement to DateTimeField doc --- mongoengine/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 9648bb0b..ead86291 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -462,6 +462,8 @@ class DateTimeField(BaseField): installed you can utilise it to convert varying types of date formats into valid python datetime objects. + Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) + Note: Microseconds are rounded to the nearest millisecond. Pre UTC microsecond support is effectively broken. Use :class:`~mongoengine.fields.ComplexDateTimeField` if you From bd524d2e1e51f2c9ea4bc971003eeb34e8e7a090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 30 Aug 2018 23:13:10 +0200 Subject: [PATCH 252/268] Documented that it is possible to specify a name when using a dict to define an index --- docs/guide/defining-documents.rst | 3 +++ mongoengine/context_managers.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 2a8d5418..366d12c7 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -513,6 +513,9 @@ If a dictionary is passed then the following options are available: Allows you to automatically expire data from a collection by setting the time in seconds to expire the a field. +:attr:`name` (Optional) + Allows you to specify a name for the index + .. note:: Inheritance adds extra fields indices see: :ref:`document-inheritance`. diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index ec2e9e8b..0343e163 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -215,7 +215,7 @@ class query_counter(object): """Get the number of queries.""" ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}} count = self.db.system.profile.find(ignore_query).count() - self.counter - self.counter += 1 + self.counter += 1 # Account for the query we just fired return count From a7852a89cc5d8a9bf0f976c99cd42eacade4ef85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sat, 1 Sep 2018 23:30:50 +0200 Subject: [PATCH 253/268] Fixes 2 bugs in no_subclasses context mgr (__exit__ swallows exception + repair feature) --- mongoengine/context_managers.py | 9 +++---- tests/test_context_managers.py | 44 +++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index ec2e9e8b..cfc0cdd4 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -145,18 +145,17 @@ class no_sub_classes(object): :param cls: the class to turn querying sub classes on """ self.cls = cls + self.cls_initial_subclasses = None def __enter__(self): """Change the objects default and _auto_dereference values.""" - self.cls._all_subclasses = self.cls._subclasses - self.cls._subclasses = (self.cls,) + self.cls_initial_subclasses = self.cls._subclasses + self.cls._subclasses = (self.cls._class_name,) return self.cls def __exit__(self, t, value, traceback): """Reset the default and _auto_dereference values.""" - self.cls._subclasses = self.cls._all_subclasses - delattr(self.cls, '_all_subclasses') - return self.cls + self.cls._subclasses = self.cls_initial_subclasses class query_counter(object): diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index 0f6bf815..8c96016c 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -140,8 +140,6 @@ class ContextManagersTest(unittest.TestCase): def test_no_sub_classes(self): class A(Document): x = IntField() - y = IntField() - meta = {'allow_inheritance': True} class B(A): @@ -152,29 +150,29 @@ class ContextManagersTest(unittest.TestCase): A.drop_collection() - A(x=10, y=20).save() - A(x=15, y=30).save() - B(x=20, y=40).save() - B(x=30, y=50).save() - C(x=40, y=60).save() + A(x=10).save() + A(x=15).save() + B(x=20).save() + B(x=30).save() + C(x=40).save() self.assertEqual(A.objects.count(), 5) self.assertEqual(B.objects.count(), 3) self.assertEqual(C.objects.count(), 1) - with no_sub_classes(A) as A: + with no_sub_classes(A): self.assertEqual(A.objects.count(), 2) for obj in A.objects: self.assertEqual(obj.__class__, A) - with no_sub_classes(B) as B: + with no_sub_classes(B): self.assertEqual(B.objects.count(), 2) for obj in B.objects: self.assertEqual(obj.__class__, B) - with no_sub_classes(C) as C: + with no_sub_classes(C): self.assertEqual(C.objects.count(), 1) for obj in C.objects: @@ -185,6 +183,32 @@ class ContextManagersTest(unittest.TestCase): self.assertEqual(B.objects.count(), 3) self.assertEqual(C.objects.count(), 1) + def test_no_sub_classes_modification_to_document_class_are_temporary(self): + class A(Document): + x = IntField() + meta = {'allow_inheritance': True} + + class B(A): + z = IntField() + + self.assertEqual(A._subclasses, ('A', 'A.B')) + with no_sub_classes(A): + self.assertEqual(A._subclasses, ('A',)) + self.assertEqual(A._subclasses, ('A', 'A.B')) + + self.assertEqual(B._subclasses, ('A.B',)) + with no_sub_classes(B): + self.assertEqual(B._subclasses, ('A.B',)) + self.assertEqual(B._subclasses, ('A.B',)) + + def test_no_subclass_context_manager_does_not_swallow_exception(self): + class User(Document): + name = StringField() + + with self.assertRaises(TypeError): + with no_sub_classes(User): + raise TypeError() + def test_query_counter(self): connect('mongoenginetest') db = get_db() From 8ff82996fb76314f799e9a153f32a4adc37485d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Mon, 3 Sep 2018 11:11:15 +0200 Subject: [PATCH 254/268] Fix few things related to query_counter context manager: - Improve doc - Fix the fact that the context was modifying the initial profiling_level in case it was != 0 - Ignores 'killcursors' to fix flaky test that were impacted by killcursors queries (#1869) --- mongoengine/context_managers.py | 56 +++++++++++++------- tests/queryset/queryset.py | 29 +++++++---- tests/test_context_managers.py | 91 +++++++++++++++++++++++++++++++-- 3 files changed, 143 insertions(+), 33 deletions(-) diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index 67c83581..ee1f5e01 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -159,51 +159,69 @@ class no_sub_classes(object): class query_counter(object): - """Query_counter context manager to get the number of queries.""" + """Query_counter context manager to get the number of queries. + This works by updating the `profiling_level` of the database so that all queries get logged, + resetting the db.system.profile collection at the beginnig of the context and counting the new entries. + + This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes + can interfere with it + + Be aware that: + - Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of + documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches) + - Some queries are ignored by default by the counter (killcursors, db.system.indexes) + """ def __init__(self): - """Construct the query_counter.""" - self.counter = 0 + """Construct the query_counter + """ self.db = get_db() + self.initial_profiling_level = None + self._ctx_query_counter = 0 # number of queries issued by the context - def __enter__(self): - """On every with block we need to drop the profile collection.""" + self._ignored_query = { + 'ns': + {'$ne': '%s.system.indexes' % self.db.name}, + 'op': + {'$ne': 'killcursors'} + } + + def _turn_on_profiling(self): + self.initial_profiling_level = self.db.profiling_level() self.db.set_profiling_level(0) self.db.system.profile.drop() self.db.set_profiling_level(2) + + def _resets_profiling(self): + self.db.set_profiling_level(self.initial_profiling_level) + + def __enter__(self): + self._turn_on_profiling() return self def __exit__(self, t, value, traceback): - """Reset the profiling level.""" - self.db.set_profiling_level(0) + self._resets_profiling() def __eq__(self, value): - """== Compare querycounter.""" counter = self._get_count() return value == counter def __ne__(self, value): - """!= Compare querycounter.""" return not self.__eq__(value) def __lt__(self, value): - """< Compare querycounter.""" return self._get_count() < value def __le__(self, value): - """<= Compare querycounter.""" return self._get_count() <= value def __gt__(self, value): - """> Compare querycounter.""" return self._get_count() > value def __ge__(self, value): - """>= Compare querycounter.""" return self._get_count() >= value def __int__(self): - """int representation.""" return self._get_count() def __repr__(self): @@ -211,10 +229,12 @@ class query_counter(object): return u"%s" % self._get_count() def _get_count(self): - """Get the number of queries.""" - ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}} - count = self.db.system.profile.find(ignore_query).count() - self.counter - self.counter += 1 # Account for the query we just fired + """Get the number of queries by counting the current number of entries in db.system.profile + and substracting the queries issued by this context. In fact everytime this is called, 1 query is + issued so we need to balance that + """ + count = self.db.system.profile.find(self._ignored_query).count() - self._ctx_query_counter + self._ctx_query_counter += 1 # Account for the query we just issued to gather the information return count diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a405e892..ff5407df 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -4714,18 +4714,27 @@ class QuerySetTest(unittest.TestCase): for i in range(100): Person(name="No: %s" % i).save() - with query_counter() as q: - self.assertEqual(q, 0) - people = Person.objects.no_cache() + with query_counter() as q: + try: + self.assertEqual(q, 0) + people = Person.objects.no_cache() - [x for x in people] - self.assertEqual(q, 1) + [x for x in people] + self.assertEqual(q, 1) - list(people) - self.assertEqual(q, 2) + list(people) + self.assertEqual(q, 2) + + people.count() + self.assertEqual(q, 3) + except AssertionError as exc: + db = get_db() + msg = '' + for q in list(db.system.profile.find())[-50:]: + msg += str([q['ts'], q['ns'], q.get('query'), q['op']])+'\n' + msg += str(q) + raise AssertionError(str(exc) + '\n'+msg) - people.count() - self.assertEqual(q, 3) def test_cache_not_cloned(self): @@ -5053,7 +5062,7 @@ class QuerySetTest(unittest.TestCase): {"$ne": "%s.system.indexes" % q.db.name}})[0] self.assertFalse('$orderby' in op['query'], - 'BaseQuerySet must remove orderby from meta in boolen test') + 'BaseQuerySet must remove orderby from meta in boolean test') self.assertEqual(Person.objects.first().name, 'A') self.assertTrue(Person.objects._has_data(), diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index 8c96016c..2b265cbd 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -209,18 +209,99 @@ class ContextManagersTest(unittest.TestCase): with no_sub_classes(User): raise TypeError() + def test_query_counter_does_not_swallow_exception(self): + + with self.assertRaises(TypeError): + with query_counter() as q: + raise TypeError() + + def test_query_counter_temporarily_modifies_profiling_level(self): + connect('mongoenginetest') + db = get_db() + + initial_profiling_level = db.profiling_level() + + try: + NEW_LEVEL = 1 + db.set_profiling_level(NEW_LEVEL) + self.assertEqual(db.profiling_level(), NEW_LEVEL) + with query_counter() as q: + self.assertEqual(db.profiling_level(), 2) + self.assertEqual(db.profiling_level(), NEW_LEVEL) + except Exception: + db.set_profiling_level(initial_profiling_level) # Ensures it gets reseted no matter the outcome of the test + raise + def test_query_counter(self): connect('mongoenginetest') db = get_db() - db.test.find({}) + + collection = db.query_counter + collection.drop() + + def issue_1_count_query(): + collection.find({}).count() + + def issue_1_insert_query(): + collection.insert_one({'test': 'garbage'}) + + def issue_1_find_query(): + collection.find_one() + + counter = 0 + with query_counter() as q: + self.assertEqual(q, counter) + self.assertEqual(q, counter) # Ensures previous count query did not get counted + + for _ in range(10): + issue_1_insert_query() + counter += 1 + self.assertEqual(q, counter) + + for _ in range(4): + issue_1_find_query() + counter += 1 + self.assertEqual(q, counter) + + for _ in range(3): + issue_1_count_query() + counter += 1 + self.assertEqual(q, counter) + + def test_query_counter_counts_getmore_queries(self): + connect('mongoenginetest') + db = get_db() + + collection = db.query_counter + collection.drop() + + many_docs = [{'test': 'garbage %s' % i} for i in range(150)] + collection.insert_many(many_docs) # first batch of documents contains 101 documents with query_counter() as q: - self.assertEqual(0, q) + self.assertEqual(q, 0) + list(collection.find()) + self.assertEqual(q, 2) # 1st select + 1 getmore - for i in range(1, 51): - db.test.find({}).count() + def test_query_counter_ignores_particular_queries(self): + connect('mongoenginetest') + db = get_db() - self.assertEqual(50, q) + collection = db.query_counter + collection.insert_many([{'test': 'garbage %s' % i} for i in range(10)]) + + with query_counter() as q: + self.assertEqual(q, 0) + cursor = collection.find() + self.assertEqual(q, 0) # cursor wasn't opened yet + _ = next(cursor) # opens the cursor and fires the find query + self.assertEqual(q, 1) + + cursor.close() # issues a `killcursors` query that is ignored by the context + self.assertEqual(q, 1) + + _ = db.system.indexes.find_one() # queries on db.system.indexes are ignored as well + self.assertEqual(q, 1) if __name__ == '__main__': unittest.main() From 408274152baf75485f17eee9cc0550fd7bb82960 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Tue, 4 Sep 2018 20:24:34 +0800 Subject: [PATCH 255/268] reduce cycle complexity using logic map --- mongoengine/queryset/transform.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 5f777f41..555be6f9 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -214,17 +214,20 @@ def update(_doc_cls=None, **update): if parts[0] in UPDATE_OPERATORS: op = parts.pop(0) # Convert Pythonic names to Mongo equivalents - if op in ('push_all', 'pull_all'): - op = op.replace('_all', 'All') - elif op == 'dec': + operator_map = { + 'push_all': 'pushAll', + 'pull_all': 'pullAll', + 'dec': 'inc', + 'add_to_set': 'addToSet', + 'set_on_insert': 'setOnInsert' + } + # If operator doesn't found from operator map, op value will stay + # unchanged + op = operator_map.get(op, op) + if op == 'dec': # Support decrement by flipping a positive value's sign # and using 'inc' - op = 'inc' value = -value - elif op == 'add_to_set': - op = 'addToSet' - elif op == 'set_on_insert': - op = 'setOnInsert' match = None if parts[-1] in COMPARISON_OPERATORS: From e83b529f1c8cc032968797057d7791169fc9bba5 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Tue, 4 Sep 2018 20:38:42 +0800 Subject: [PATCH 256/268] flip value before changing op to inc --- mongoengine/queryset/transform.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 555be6f9..a8670543 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -221,13 +221,13 @@ def update(_doc_cls=None, **update): 'add_to_set': 'addToSet', 'set_on_insert': 'setOnInsert' } - # If operator doesn't found from operator map, op value will stay - # unchanged - op = operator_map.get(op, op) if op == 'dec': # Support decrement by flipping a positive value's sign # and using 'inc' value = -value + # If operator doesn't found from operator map, op value will stay + # unchanged + op = operator_map.get(op, op) match = None if parts[-1] in COMPARISON_OPERATORS: From b65478e7d9c8f09d915a102a367e8da52ad6bdf4 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Tue, 4 Sep 2018 20:44:44 +0800 Subject: [PATCH 257/268] trigger ci --- mongoengine/queryset/transform.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index a8670543..6021d464 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -225,8 +225,8 @@ def update(_doc_cls=None, **update): # Support decrement by flipping a positive value's sign # and using 'inc' value = -value - # If operator doesn't found from operator map, op value will stay - # unchanged + # If the operator doesn't found from operator map, the op value + # will stay unchanged op = operator_map.get(op, op) match = None From ab08e67eaf3f6b809a58740c0cfbbb24e1a3ef0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 30 Aug 2018 23:57:16 +0200 Subject: [PATCH 258/268] fix inc/dec operator with decimal --- mongoengine/fields.py | 7 ++- mongoengine/queryset/transform.py | 6 +++ tests/queryset/queryset.py | 79 ++++++++++++++++++++++++++----- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 89b901e7..d8eaec4e 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -364,7 +364,8 @@ class FloatField(BaseField): class DecimalField(BaseField): - """Fixed-point decimal number field. + """Fixed-point decimal number field. Stores the value as a float by default unless `force_string` is used. + If using floats, beware of Decimal to float conversion (potential precision loss) .. versionchanged:: 0.8 .. versionadded:: 0.3 @@ -375,7 +376,9 @@ class DecimalField(BaseField): """ :param min_value: Validation rule for the minimum acceptable value. :param max_value: Validation rule for the maximum acceptable value. - :param force_string: Store as a string. + :param force_string: Store the value as a string (instead of a float). + Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied) + and some query operator won't work (e.g: inc, dec) :param precision: Number of decimal places to store. :param rounding: The rounding rule from the python decimal library: diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 6021d464..25bd68e0 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -201,14 +201,18 @@ def update(_doc_cls=None, **update): format. """ mongo_update = {} + for key, value in update.items(): if key == '__raw__': mongo_update.update(value) continue + parts = key.split('__') + # if there is no operator, default to 'set' if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS: parts.insert(0, 'set') + # Check for an operator and transform to mongo-style if there is op = None if parts[0] in UPDATE_OPERATORS: @@ -294,6 +298,8 @@ def update(_doc_cls=None, **update): value = field.prepare_query_value(op, value) elif op == 'unset': value = 1 + elif op == 'inc': + value = field.prepare_query_value(op, value) if match: match = '$' + match diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index a405e892..b0dd354d 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3,6 +3,7 @@ import datetime import unittest import uuid +from decimal import Decimal from bson import DBRef, ObjectId from nose.plugins.skip import SkipTest @@ -1851,21 +1852,16 @@ class QuerySetTest(unittest.TestCase): self.assertEqual( 1, BlogPost.objects(author__in=["%s" % me.pk]).count()) - def test_update(self): - """Ensure that atomic updates work properly. - """ + def test_update_intfield_operator(self): class BlogPost(Document): - name = StringField() - title = StringField() hits = IntField() - tags = ListField(StringField()) BlogPost.drop_collection() - post = BlogPost(name="Test Post", hits=5, tags=['test']) + post = BlogPost(hits=5) post.save() - BlogPost.objects.update(set__hits=10) + BlogPost.objects.update_one(set__hits=10) post.reload() self.assertEqual(post.hits, 10) @@ -1882,6 +1878,55 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertEqual(post.hits, 11) + def test_update_decimalfield_operator(self): + class BlogPost(Document): + review = DecimalField() + + BlogPost.drop_collection() + + post = BlogPost(review=3.5) + post.save() + + BlogPost.objects.update_one(inc__review=0.1) # test with floats + post.reload() + self.assertEqual(float(post.review), 3.6) + + BlogPost.objects.update_one(dec__review=0.1) + post.reload() + self.assertEqual(float(post.review), 3.5) + + BlogPost.objects.update_one(inc__review=Decimal(0.12)) # test with Decimal + post.reload() + self.assertEqual(float(post.review), 3.62) + + BlogPost.objects.update_one(dec__review=Decimal(0.12)) + post.reload() + self.assertEqual(float(post.review), 3.5) + + def test_update_decimalfield_operator_not_working_with_force_string(self): + class BlogPost(Document): + review = DecimalField(force_string=True) + + BlogPost.drop_collection() + + post = BlogPost(review=3.5) + post.save() + + with self.assertRaises(OperationError): + BlogPost.objects.update_one(inc__review=0.1) # test with floats + + def test_update_listfield_operator(self): + """Ensure that atomic updates work properly. + """ + class BlogPost(Document): + tags = ListField(StringField()) + + BlogPost.drop_collection() + + post = BlogPost(tags=['test']) + post.save() + + # ListField operator BlogPost.objects.update(push__tags='mongo') post.reload() self.assertTrue('mongo' in post.tags) @@ -1900,13 +1945,23 @@ class QuerySetTest(unittest.TestCase): post.reload() self.assertEqual(post.tags.count('unique'), 1) - self.assertNotEqual(post.hits, None) - BlogPost.objects.update_one(unset__hits=1) - post.reload() - self.assertEqual(post.hits, None) + BlogPost.drop_collection() + + def test_update_unset(self): + class BlogPost(Document): + title = StringField() BlogPost.drop_collection() + post = BlogPost(title='garbage').save() + + self.assertNotEqual(post.title, None) + BlogPost.objects.update_one(unset__title=1) + post.reload() + self.assertEqual(post.title, None) + pymongo_doc = BlogPost.objects.as_pymongo().first() + self.assertNotIn('title', pymongo_doc) + @needs_mongodb_v26 def test_update_push_with_position(self): """Ensure that the 'push' update with position works properly. From cf9df548ca1280a34f6bf5bbce1971400bb07656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 4 Sep 2018 19:18:40 +0200 Subject: [PATCH 259/268] reverted back to the StopIteration in queryset.next that one didnt have to be changed (test stalled) --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index a4e64018..2c80218c 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -1481,7 +1481,7 @@ class BaseQuerySet(object): """Wrap the result in a :class:`~mongoengine.Document` object. """ if self._limit == 0 or self._none: - return + raise StopIteration raw_doc = self._cursor.next() From a25d127f368b7dfba1f697d34d27ee0b54a29df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 4 Sep 2018 20:51:06 +0200 Subject: [PATCH 260/268] Remove DictField.basecls related code, it is useless --- mongoengine/fields.py | 9 ++------- tests/queryset/queryset.py | 27 --------------------------- 2 files changed, 2 insertions(+), 34 deletions(-) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 6994b315..91604a16 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -945,14 +945,9 @@ class DictField(ComplexBaseField): .. versionchanged:: 0.5 - Can now handle complex / varying types of data """ - def __init__(self, basecls=None, field=None, *args, **kwargs): + def __init__(self, field=None, *args, **kwargs): self.field = field self._auto_dereference = False - self.basecls = basecls or BaseField - - # XXX ValidationError raised outside of the "validate" method. - if not issubclass(self.basecls, BaseField): - self.error('DictField only accepts dict values') kwargs.setdefault('default', lambda: {}) super(DictField, self).__init__(*args, **kwargs) @@ -972,7 +967,7 @@ class DictField(ComplexBaseField): super(DictField, self).validate(value) def lookup_member(self, member_name): - return DictField(basecls=self.basecls, db_field=member_name) + return DictField(db_field=member_name) def prepare_query_value(self, op, value): match_operators = ['contains', 'icontains', 'startswith', diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 77308c26..c6aa3594 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3618,33 +3618,6 @@ class QuerySetTest(unittest.TestCase): Group.drop_collection() - def test_dict_with_custom_baseclass(self): - """Ensure DictField working with custom base clases. - """ - class Test(Document): - testdict = DictField() - - Test.drop_collection() - - t = Test(testdict={'f': 'Value'}) - t.save() - - self.assertEqual( - Test.objects(testdict__f__startswith='Val').count(), 1) - self.assertEqual(Test.objects(testdict__f='Value').count(), 1) - Test.drop_collection() - - class Test(Document): - testdict = DictField(basecls=StringField) - - t = Test(testdict={'f': 'Value'}) - t.save() - - self.assertEqual(Test.objects(testdict__f='Value').count(), 1) - self.assertEqual( - Test.objects(testdict__f__startswith='Val').count(), 1) - Test.drop_collection() - def test_bulk(self): """Ensure bulk querying by object id returns a proper dict. """ From 282b83ac08a3b4cf680b6f81627187b7a08a2889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Tue, 4 Sep 2018 23:48:07 +0200 Subject: [PATCH 261/268] Fix default value of ComplexDateTime + fixed descriptor .__get__ for class attribute --- mongoengine/base/fields.py | 3 +- mongoengine/fields.py | 20 ++- tests/fields/fields.py | 327 +++++++++++++++++++++---------------- 3 files changed, 199 insertions(+), 151 deletions(-) diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index 69034d5d..d25d4305 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -55,7 +55,7 @@ class BaseField(object): field. Generally this is deprecated in favour of the `FIELD.validate` method :param choices: (optional) The valid choices - :param null: (optional) Is the field value can be null. If no and there is a default value + :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 sparse: (optional) `sparse=True` combined with `unique=True` and `required=False` means that uniqueness won't be enforced for `None` values @@ -130,7 +130,6 @@ class BaseField(object): def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document. """ - # If setting to None and there is a default # Then set the value to the default value if value is None: diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 6994b315..bc632ac9 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -562,11 +562,15 @@ class ComplexDateTimeField(StringField): The `,` as the separator can be easily modified by passing the `separator` keyword when initializing the field. + Note: To default the field to the current datetime, use: DateTimeField(default=datetime.utcnow) + .. versionadded:: 0.5 """ def __init__(self, separator=',', **kwargs): - self.names = ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'] + """ + :param separator: Allows to customize the separator used for storage (default ``,``) + """ self.separator = separator self.format = separator.join(['%Y', '%m', '%d', '%H', '%M', '%S', '%f']) super(ComplexDateTimeField, self).__init__(**kwargs) @@ -597,16 +601,20 @@ class ComplexDateTimeField(StringField): return datetime.datetime(*values) def __get__(self, instance, owner): + if instance is None: + return self + data = super(ComplexDateTimeField, self).__get__(instance, owner) - if data is None: - return None if self.null else datetime.datetime.now() - if isinstance(data, datetime.datetime): + + if isinstance(data, datetime.datetime) or data is None: return data return self._convert_from_string(data) def __set__(self, instance, value): - value = self._convert_from_datetime(value) if value else value - return super(ComplexDateTimeField, self).__set__(instance, value) + super(ComplexDateTimeField, self).__set__(instance, value) + value = instance._data[self.name] + if value is not None: + instance._data[self.name] = self._convert_from_datetime(value) def validate(self, value): value = self.to_python(value) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 362acec4..4bfd7c74 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -264,12 +264,11 @@ class FieldTest(MongoDBTestCase): # Retrive data from db and verify it. ret = HandleNoneFields.objects.all()[0] - self.assertEqual(ret.str_fld, None) - self.assertEqual(ret.int_fld, None) - self.assertEqual(ret.flt_fld, None) + self.assertIsNone(ret.str_fld) + self.assertIsNone(ret.int_fld) + self.assertIsNone(ret.flt_fld) - # Return current time if retrived value is None. - self.assertTrue(isinstance(ret.comp_dt_fld, datetime.datetime)) + self.assertIsNone(ret.comp_dt_fld) def test_not_required_handles_none_from_database(self): """Ensure that every field can handle null values from the @@ -287,7 +286,7 @@ class FieldTest(MongoDBTestCase): doc.str_fld = u'spam ham egg' doc.int_fld = 42 doc.flt_fld = 4.2 - doc.com_dt_fld = datetime.datetime.utcnow() + doc.comp_dt_fld = datetime.datetime.utcnow() doc.save() # Unset all the fields @@ -302,12 +301,10 @@ class FieldTest(MongoDBTestCase): # Retrive data from db and verify it. ret = HandleNoneFields.objects.first() - self.assertEqual(ret.str_fld, None) - self.assertEqual(ret.int_fld, None) - self.assertEqual(ret.flt_fld, None) - - # ComplexDateTimeField returns current time if retrived value is None. - self.assertTrue(isinstance(ret.comp_dt_fld, datetime.datetime)) + self.assertIsNone(ret.str_fld) + self.assertIsNone(ret.int_fld) + self.assertIsNone(ret.flt_fld) + self.assertIsNone(ret.comp_dt_fld) # Retrieved object shouldn't pass validation when a re-save is # attempted. @@ -928,137 +925,6 @@ class FieldTest(MongoDBTestCase): logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) self.assertEqual(logs.count(), 10) - 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_list_validation(self): """Ensure that a list field only accepts lists with valid elements.""" AccessLevelChoices = ( @@ -5389,5 +5255,180 @@ 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() From b15673c52566dbf1fb1f29005901fda35bd97373 Mon Sep 17 00:00:00 2001 From: Sergey Tereschenko Date: Wed, 5 Sep 2018 11:53:15 +0300 Subject: [PATCH 262/268] fixed TypeError on translated choices join expect strings, but if we use django ugettext_lazy like this: choices=[(1, _("One")), (2, _("Two"))] there will be TypeError: sequence item 0: expected string, __proxy__ found this commif fixes error by converting label to string --- mongoengine/base/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 85906a3e..aaf99ace 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1091,6 +1091,6 @@ class BaseDocument(object): sep = getattr(field, 'display_sep', ' ') values = value if field.__class__.__name__ in ('ListField', 'SortedListField') else [value] return sep.join([ - dict(field.choices).get(val, val) + six.text_type(dict(field.choices).get(val, val)) for val in values or []]) return value From 003827e91600dc49c4504eba4cf4e419b406a820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 6 Sep 2018 21:47:06 +0200 Subject: [PATCH 263/268] rewrote some poorly written assertions like: assertTrue(isinstance(a, b)) assertTrue(a==b) assertTrue(a!=b) assertTrue(a in b) --- tests/document/class_methods.py | 8 +- tests/document/delta.py | 4 +- tests/document/dynamic.py | 6 +- tests/document/indexes.py | 36 ++++----- tests/document/inheritance.py | 12 +-- tests/document/instance.py | 95 ++++++++++++----------- tests/document/validation.py | 28 +++---- tests/fields/fields.py | 67 ++++++++-------- tests/fields/file_tests.py | 18 ++--- tests/fields/geo.py | 16 ++-- tests/queryset/field_list.py | 2 +- tests/queryset/geo.py | 12 +-- tests/queryset/queryset.py | 131 ++++++++++++++++---------------- tests/queryset/transform.py | 34 ++++----- tests/queryset/visitor.py | 2 +- tests/test_connection.py | 60 +++++++-------- tests/test_context_managers.py | 24 +++--- tests/test_dereference.py | 48 ++++++------ 18 files changed, 298 insertions(+), 305 deletions(-) diff --git a/tests/document/class_methods.py b/tests/document/class_methods.py index 2fab1f72..5289e483 100644 --- a/tests/document/class_methods.py +++ b/tests/document/class_methods.py @@ -66,10 +66,10 @@ class ClassMethodsTest(unittest.TestCase): """ collection_name = 'person' self.Person(name='Test').save() - self.assertTrue(collection_name in self.db.collection_names()) + self.assertIn(collection_name, self.db.collection_names()) self.Person.drop_collection() - self.assertFalse(collection_name in self.db.collection_names()) + self.assertNotIn(collection_name, self.db.collection_names()) def test_register_delete_rule(self): """Ensure that register delete rule adds a delete rule to the document @@ -340,7 +340,7 @@ class ClassMethodsTest(unittest.TestCase): meta = {'collection': collection_name} Person(name="Test User").save() - self.assertTrue(collection_name in self.db.collection_names()) + self.assertIn(collection_name, self.db.collection_names()) user_obj = self.db[collection_name].find_one() self.assertEqual(user_obj['name'], "Test User") @@ -349,7 +349,7 @@ class ClassMethodsTest(unittest.TestCase): self.assertEqual(user_obj.name, "Test User") Person.drop_collection() - self.assertFalse(collection_name in self.db.collection_names()) + self.assertNotIn(collection_name, self.db.collection_names()) def test_collection_name_and_primary(self): """Ensure that a collection with a specified name may be used. diff --git a/tests/document/delta.py b/tests/document/delta.py index add4fe8d..30296956 100644 --- a/tests/document/delta.py +++ b/tests/document/delta.py @@ -694,7 +694,7 @@ class DeltaTest(unittest.TestCase): organization.employees.append(person) updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertTrue('employees' in updates) + self.assertIn('employees', updates) def test_delta_with_dbref_false(self): person, organization, employee = self.circular_reference_deltas_2(Document, Document, False) @@ -709,7 +709,7 @@ class DeltaTest(unittest.TestCase): organization.employees.append(person) updates, removals = organization._delta() self.assertEqual({}, removals) - self.assertTrue('employees' in updates) + self.assertIn('employees', updates) def test_nested_nested_fields_mark_as_changed(self): class EmbeddedDoc(EmbeddedDocument): diff --git a/tests/document/dynamic.py b/tests/document/dynamic.py index a478df42..94cea134 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -174,8 +174,8 @@ class DynamicTest(unittest.TestCase): Employee.drop_collection() - self.assertTrue('name' in Employee._fields) - self.assertTrue('salary' in Employee._fields) + self.assertIn('name', Employee._fields) + self.assertIn('salary', Employee._fields) self.assertEqual(Employee._get_collection_name(), self.Person._get_collection_name()) @@ -189,7 +189,7 @@ class DynamicTest(unittest.TestCase): self.assertEqual(1, Employee.objects(age=20).count()) joe_bloggs = self.Person.objects.first() - self.assertTrue(isinstance(joe_bloggs, Employee)) + self.assertIsInstance(joe_bloggs, Employee) def test_embedded_dynamic_document(self): """Test dynamic embedded documents""" diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 58e09199..1cbb4ec3 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -70,7 +70,7 @@ class IndexesTest(unittest.TestCase): self.assertEqual(len(info), 4) info = [value['key'] for key, value in info.iteritems()] for expected in expected_specs: - self.assertTrue(expected['fields'] in info) + self.assertIn(expected['fields'], info) def _index_test_inheritance(self, InheritFrom): @@ -102,7 +102,7 @@ class IndexesTest(unittest.TestCase): self.assertEqual(len(info), 4) info = [value['key'] for key, value in info.iteritems()] for expected in expected_specs: - self.assertTrue(expected['fields'] in info) + self.assertIn(expected['fields'], info) class ExtendedBlogPost(BlogPost): title = StringField() @@ -117,7 +117,7 @@ class IndexesTest(unittest.TestCase): info = ExtendedBlogPost.objects._collection.index_information() info = [value['key'] for key, value in info.iteritems()] for expected in expected_specs: - self.assertTrue(expected['fields'] in info) + self.assertIn(expected['fields'], info) def test_indexes_document_inheritance(self): """Ensure that indexes are used when meta[indexes] is specified for @@ -226,7 +226,7 @@ class IndexesTest(unittest.TestCase): list(Person.objects) info = Person.objects._collection.index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('rank.title', 1)] in info) + self.assertIn([('rank.title', 1)], info) def test_explicit_geo2d_index(self): """Ensure that geo2d indexes work when created via meta[indexes] @@ -246,7 +246,7 @@ class IndexesTest(unittest.TestCase): Place.ensure_indexes() info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('location.point', '2d')] in info) + self.assertIn([('location.point', '2d')], info) def test_explicit_geo2d_index_embedded(self): """Ensure that geo2d indexes work when created via meta[indexes] @@ -269,7 +269,7 @@ class IndexesTest(unittest.TestCase): Place.ensure_indexes() info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('current.location.point', '2d')] in info) + self.assertIn([('current.location.point', '2d')], info) def test_explicit_geosphere_index(self): """Ensure that geosphere indexes work when created via meta[indexes] @@ -289,7 +289,7 @@ class IndexesTest(unittest.TestCase): Place.ensure_indexes() info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('location.point', '2dsphere')] in info) + self.assertIn([('location.point', '2dsphere')], info) def test_explicit_geohaystack_index(self): """Ensure that geohaystack indexes work when created via meta[indexes] @@ -311,7 +311,7 @@ class IndexesTest(unittest.TestCase): Place.ensure_indexes() info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('location.point', 'geoHaystack')] in info) + self.assertIn([('location.point', 'geoHaystack')], info) def test_create_geohaystack_index(self): """Ensure that geohaystack indexes can be created @@ -323,7 +323,7 @@ class IndexesTest(unittest.TestCase): Place.create_index({'fields': (')location.point', 'name')}, bucketSize=10) info = Place._get_collection().index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('location.point', 'geoHaystack'), ('name', 1)] in info) + self.assertIn([('location.point', 'geoHaystack'), ('name', 1)], info) def test_dictionary_indexes(self): """Ensure that indexes are used when meta[indexes] contains @@ -356,7 +356,7 @@ class IndexesTest(unittest.TestCase): value.get('unique', False), value.get('sparse', False)) for key, value in info.iteritems()] - self.assertTrue(([('addDate', -1)], True, True) in info) + self.assertIn(([('addDate', -1)], True, True), info) BlogPost.drop_collection() @@ -803,7 +803,7 @@ class IndexesTest(unittest.TestCase): info = BlogPost.objects._collection.index_information() info = [value['key'] for key, value in info.iteritems()] index_item = [('_id', 1), ('comments.comment_id', 1)] - self.assertTrue(index_item in info) + self.assertIn(index_item, info) def test_compound_key_embedded(self): @@ -850,8 +850,8 @@ class IndexesTest(unittest.TestCase): info = MyDoc.objects._collection.index_information() info = [value['key'] for key, value in info.iteritems()] - self.assertTrue([('provider_ids.foo', 1)] in info) - self.assertTrue([('provider_ids.bar', 1)] in info) + self.assertIn([('provider_ids.foo', 1)], info) + self.assertIn([('provider_ids.bar', 1)], info) def test_sparse_compound_indexes(self): @@ -876,9 +876,9 @@ class IndexesTest(unittest.TestCase): } indexes = Book.objects._collection.index_information() - self.assertTrue("title_text" in indexes) + self.assertIn("title_text", indexes) key = indexes["title_text"]["key"] - self.assertTrue(('_fts', 'text') in key) + self.assertIn(('_fts', 'text'), key) def test_hashed_indexes(self): @@ -889,8 +889,8 @@ class IndexesTest(unittest.TestCase): } indexes = Book.objects._collection.index_information() - self.assertTrue("ref_id_hashed" in indexes) - self.assertTrue(('ref_id', 'hashed') in indexes["ref_id_hashed"]["key"]) + self.assertIn("ref_id_hashed", indexes) + self.assertIn(('ref_id', 'hashed'), indexes["ref_id_hashed"]["key"]) def test_indexes_after_database_drop(self): """ @@ -1013,7 +1013,7 @@ class IndexesTest(unittest.TestCase): TestDoc.ensure_indexes() index_info = TestDoc._get_collection().index_information() - self.assertTrue('shard_1_1__cls_1_txt_1_1' in index_info) + self.assertIn('shard_1_1__cls_1_txt_1_1', index_info) if __name__ == '__main__': diff --git a/tests/document/inheritance.py b/tests/document/inheritance.py index 2897e1d1..b2ab1b52 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -268,7 +268,7 @@ class InheritanceTest(unittest.TestCase): collection = self.db[Animal._get_collection_name()] obj = collection.find_one() - self.assertFalse('_cls' in obj) + self.assertNotIn('_cls', obj) def test_cant_turn_off_inheritance_on_subclass(self): """Ensure if inheritance is on in a subclass you cant turn it off. @@ -298,7 +298,7 @@ class InheritanceTest(unittest.TestCase): # Check that _cls isn't present in simple documents doc = Animal(name='dog') - self.assertFalse('_cls' in doc.to_mongo()) + self.assertNotIn('_cls', doc.to_mongo()) def test_abstract_handle_ids_in_metaclass_properly(self): @@ -374,14 +374,14 @@ class InheritanceTest(unittest.TestCase): pass doc = Comment(content='test') - self.assertFalse('_cls' in doc.to_mongo()) + self.assertNotIn('_cls', doc.to_mongo()) class Comment(EmbeddedDocument): content = StringField() meta = {'allow_inheritance': True} doc = Comment(content='test') - self.assertTrue('_cls' in doc.to_mongo()) + self.assertIn('_cls', doc.to_mongo()) def test_document_inheritance(self): """Ensure mutliple inheritance of abstract documents @@ -434,8 +434,8 @@ class InheritanceTest(unittest.TestCase): for cls in [Animal, Fish, Guppy]: self.assertEqual(cls._meta[k], v) - self.assertFalse('collection' in Animal._meta) - self.assertFalse('collection' in Mammal._meta) + self.assertNotIn('collection', Animal._meta) + self.assertNotIn('collection', Mammal._meta) self.assertEqual(Animal._get_collection_name(), None) self.assertEqual(Mammal._get_collection_name(), None) diff --git a/tests/document/instance.py b/tests/document/instance.py index cffe4f30..e637b3e6 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -357,7 +357,7 @@ class InstanceTest(MongoDBTestCase): user_son = User.objects._collection.find_one() self.assertEqual(user_son['_id'], 'test') - self.assertTrue('username' not in user_son['_id']) + self.assertNotIn('username', user_son['_id']) User.drop_collection() @@ -370,7 +370,7 @@ class InstanceTest(MongoDBTestCase): user_son = User.objects._collection.find_one() self.assertEqual(user_son['_id'], 'mongo') - self.assertTrue('username' not in user_son['_id']) + self.assertNotIn('username', user_son['_id']) def test_document_not_registered(self): class Place(Document): @@ -594,10 +594,10 @@ class InstanceTest(MongoDBTestCase): # Length = length(assigned fields + id) self.assertEqual(len(person), 5) - self.assertTrue('age' in person) + self.assertIn('age', person) person.age = None - self.assertFalse('age' in person) - self.assertFalse('nationality' in person) + self.assertNotIn('age', person) + self.assertNotIn('nationality', person) def test_embedded_document_to_mongo(self): class Person(EmbeddedDocument): @@ -627,8 +627,8 @@ class InstanceTest(MongoDBTestCase): class Comment(EmbeddedDocument): content = StringField() - self.assertTrue('content' in Comment._fields) - self.assertFalse('id' in Comment._fields) + self.assertIn('content', Comment._fields) + self.assertNotIn('id', Comment._fields) def test_embedded_document_instance(self): """Ensure that embedded documents can reference parent instance.""" @@ -1455,9 +1455,9 @@ class InstanceTest(MongoDBTestCase): user = User.objects.first() # Even if stored as ObjectId's internally mongoengine uses DBRefs # As ObjectId's aren't automatically derefenced - self.assertTrue(isinstance(user._data['orgs'][0], DBRef)) - self.assertTrue(isinstance(user.orgs[0], Organization)) - self.assertTrue(isinstance(user._data['orgs'][0], Organization)) + self.assertIsInstance(user._data['orgs'][0], DBRef) + self.assertIsInstance(user.orgs[0], Organization) + self.assertIsInstance(user._data['orgs'][0], Organization) # Changing a value with query_counter() as q: @@ -1837,9 +1837,8 @@ class InstanceTest(MongoDBTestCase): post_obj = BlogPost.objects.first() # Test laziness - self.assertTrue(isinstance(post_obj._data['author'], - bson.DBRef)) - self.assertTrue(isinstance(post_obj.author, self.Person)) + self.assertIsInstance(post_obj._data['author'], bson.DBRef) + self.assertIsInstance(post_obj.author, self.Person) self.assertEqual(post_obj.author.name, 'Test User') # Ensure that the dereferenced object may be changed and saved @@ -2245,12 +2244,12 @@ class InstanceTest(MongoDBTestCase): # Make sure docs are properly identified in a list (__eq__ is used # for the comparison). all_user_list = list(User.objects.all()) - self.assertTrue(u1 in all_user_list) - self.assertTrue(u2 in all_user_list) - self.assertTrue(u3 in all_user_list) - self.assertTrue(u4 not in all_user_list) # New object - self.assertTrue(b1 not in all_user_list) # Other object - self.assertTrue(b2 not in all_user_list) # Other object + self.assertIn(u1, all_user_list) + self.assertIn(u2, all_user_list) + self.assertIn(u3, all_user_list) + self.assertNotIn(u4, all_user_list) # New object + self.assertNotIn(b1, all_user_list) # Other object + self.assertNotIn(b2, all_user_list) # Other object # Make sure docs can be used as keys in a dict (__hash__ is used # for hashing the docs). @@ -2268,10 +2267,10 @@ class InstanceTest(MongoDBTestCase): # Make sure docs are properly identified in a set (__hash__ is used # for hashing the docs). all_user_set = set(User.objects.all()) - self.assertTrue(u1 in all_user_set) - self.assertTrue(u4 not in all_user_set) - self.assertTrue(b1 not in all_user_list) - self.assertTrue(b2 not in all_user_list) + self.assertIn(u1, all_user_set) + self.assertNotIn(u4, all_user_set) + self.assertNotIn(b1, all_user_list) + self.assertNotIn(b2, all_user_list) # Make sure duplicate docs aren't accepted in the set self.assertEqual(len(all_user_set), 3) @@ -2972,7 +2971,7 @@ class InstanceTest(MongoDBTestCase): Person(name="Harry Potter").save() person = Person.objects.first() - self.assertTrue('id' in person._data.keys()) + self.assertIn('id', person._data.keys()) self.assertEqual(person._data.get('id'), person.id) def test_complex_nesting_document_and_embedded_document(self): @@ -3064,36 +3063,36 @@ class InstanceTest(MongoDBTestCase): dbref2 = f._data['test2'] obj2 = f.test2 - self.assertTrue(isinstance(dbref2, DBRef)) - self.assertTrue(isinstance(obj2, Test2)) - self.assertTrue(obj2.id == dbref2.id) - self.assertTrue(obj2 == dbref2) - self.assertTrue(dbref2 == obj2) + self.assertIsInstance(dbref2, DBRef) + self.assertIsInstance(obj2, Test2) + self.assertEqual(obj2.id, dbref2.id) + self.assertEqual(obj2, dbref2) + self.assertEqual(dbref2, obj2) dbref3 = f._data['test3'] obj3 = f.test3 - self.assertTrue(isinstance(dbref3, DBRef)) - self.assertTrue(isinstance(obj3, Test3)) - self.assertTrue(obj3.id == dbref3.id) - self.assertTrue(obj3 == dbref3) - self.assertTrue(dbref3 == obj3) + self.assertIsInstance(dbref3, DBRef) + self.assertIsInstance(obj3, Test3) + self.assertEqual(obj3.id, dbref3.id) + self.assertEqual(obj3, dbref3) + self.assertEqual(dbref3, obj3) - self.assertTrue(obj2.id == obj3.id) - self.assertTrue(dbref2.id == dbref3.id) - self.assertFalse(dbref2 == dbref3) - self.assertFalse(dbref3 == dbref2) - self.assertTrue(dbref2 != dbref3) - self.assertTrue(dbref3 != dbref2) + self.assertEqual(obj2.id, obj3.id) + self.assertEqual(dbref2.id, dbref3.id) + self.assertNotEqual(dbref2, dbref3) + self.assertNotEqual(dbref3, dbref2) + self.assertNotEqual(dbref2, dbref3) + self.assertNotEqual(dbref3, dbref2) - self.assertFalse(obj2 == dbref3) - self.assertFalse(dbref3 == obj2) - self.assertTrue(obj2 != dbref3) - self.assertTrue(dbref3 != obj2) + self.assertNotEqual(obj2, dbref3) + self.assertNotEqual(dbref3, obj2) + self.assertNotEqual(obj2, dbref3) + self.assertNotEqual(dbref3, obj2) - self.assertFalse(obj3 == dbref2) - self.assertFalse(dbref2 == obj3) - self.assertTrue(obj3 != dbref2) - self.assertTrue(dbref2 != obj3) + self.assertNotEqual(obj3, dbref2) + self.assertNotEqual(dbref2, obj3) + self.assertNotEqual(obj3, dbref2) + self.assertNotEqual(dbref2, obj3) def test_default_values(self): class Person(Document): diff --git a/tests/document/validation.py b/tests/document/validation.py index 105bc8b0..30a285b2 100644 --- a/tests/document/validation.py +++ b/tests/document/validation.py @@ -20,16 +20,16 @@ class ValidatorErrorTest(unittest.TestCase): # 1st level error schema error.errors = {'1st': ValidationError('bad 1st'), } - self.assertTrue('1st' in error.to_dict()) + self.assertIn('1st', error.to_dict()) self.assertEqual(error.to_dict()['1st'], 'bad 1st') # 2nd level error schema error.errors = {'1st': ValidationError('bad 1st', errors={ '2nd': ValidationError('bad 2nd'), })} - self.assertTrue('1st' in error.to_dict()) - self.assertTrue(isinstance(error.to_dict()['1st'], dict)) - self.assertTrue('2nd' in error.to_dict()['1st']) + self.assertIn('1st', error.to_dict()) + self.assertIsInstance(error.to_dict()['1st'], dict) + self.assertIn('2nd', error.to_dict()['1st']) self.assertEqual(error.to_dict()['1st']['2nd'], 'bad 2nd') # moar levels @@ -40,10 +40,10 @@ class ValidatorErrorTest(unittest.TestCase): }), }), })} - self.assertTrue('1st' in error.to_dict()) - self.assertTrue('2nd' in error.to_dict()['1st']) - self.assertTrue('3rd' in error.to_dict()['1st']['2nd']) - self.assertTrue('4th' in error.to_dict()['1st']['2nd']['3rd']) + self.assertIn('1st', error.to_dict()) + self.assertIn('2nd', error.to_dict()['1st']) + self.assertIn('3rd', error.to_dict()['1st']['2nd']) + self.assertIn('4th', error.to_dict()['1st']['2nd']['3rd']) self.assertEqual(error.to_dict()['1st']['2nd']['3rd']['4th'], 'Inception') @@ -58,7 +58,7 @@ class ValidatorErrorTest(unittest.TestCase): try: User().validate() except ValidationError as e: - self.assertTrue("User:None" in e.message) + self.assertIn("User:None", e.message) self.assertEqual(e.to_dict(), { 'username': 'Field is required', 'name': 'Field is required'}) @@ -68,7 +68,7 @@ class ValidatorErrorTest(unittest.TestCase): try: user.save() except ValidationError as e: - self.assertTrue("User:RossC0" in e.message) + self.assertIn("User:RossC0", e.message) self.assertEqual(e.to_dict(), { 'name': 'Field is required'}) @@ -116,7 +116,7 @@ class ValidatorErrorTest(unittest.TestCase): try: Doc(id="bad").validate() except ValidationError as e: - self.assertTrue("SubDoc:None" in e.message) + self.assertIn("SubDoc:None", e.message) self.assertEqual(e.to_dict(), { "e": {'val': 'OK could not be converted to int'}}) @@ -127,14 +127,14 @@ class ValidatorErrorTest(unittest.TestCase): doc = Doc.objects.first() keys = doc._data.keys() self.assertEqual(2, len(keys)) - self.assertTrue('e' in keys) - self.assertTrue('id' in keys) + self.assertIn('e', keys) + self.assertIn('id', keys) doc.e.val = "OK" try: doc.save() except ValidationError as e: - self.assertTrue("Doc:test" in e.message) + self.assertIn("Doc:test", e.message) self.assertEqual(e.to_dict(), { "e": {'val': 'OK could not be converted to int'}}) diff --git a/tests/fields/fields.py b/tests/fields/fields.py index 4bfd7c74..d6df061d 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -175,7 +175,7 @@ class FieldTest(MongoDBTestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, 30) self.assertEqual(person.userid, 'test') - self.assertTrue(isinstance(person.created, datetime.datetime)) + self.assertIsInstance(person.created, datetime.datetime) self.assertEqual(person._data['name'], person.name) self.assertEqual(person._data['age'], person.age) @@ -211,7 +211,7 @@ class FieldTest(MongoDBTestCase): self.assertEqual(person.name, None) self.assertEqual(person.age, 30) self.assertEqual(person.userid, 'test') - self.assertTrue(isinstance(person.created, datetime.datetime)) + self.assertIsInstance(person.created, datetime.datetime) self.assertNotEqual(person.created, datetime.datetime(2014, 6, 12)) self.assertEqual(person._data['name'], person.name) @@ -1602,8 +1602,8 @@ class FieldTest(MongoDBTestCase): e.save() e2 = Simple.objects.get(id=e.id) - self.assertTrue(isinstance(e2.mapping[0], StringSetting)) - self.assertTrue(isinstance(e2.mapping[1], IntegerSetting)) + self.assertIsInstance(e2.mapping[0], StringSetting) + self.assertIsInstance(e2.mapping[1], IntegerSetting) # Test querying self.assertEqual( @@ -1772,8 +1772,8 @@ class FieldTest(MongoDBTestCase): e.save() e2 = Simple.objects.get(id=e.id) - self.assertTrue(isinstance(e2.mapping['somestring'], StringSetting)) - self.assertTrue(isinstance(e2.mapping['someint'], IntegerSetting)) + self.assertIsInstance(e2.mapping['somestring'], StringSetting) + self.assertIsInstance(e2.mapping['someint'], IntegerSetting) # Test querying self.assertEqual( @@ -1857,8 +1857,8 @@ class FieldTest(MongoDBTestCase): e.save() e2 = Extensible.objects.get(id=e.id) - self.assertTrue(isinstance(e2.mapping['somestring'], StringSetting)) - self.assertTrue(isinstance(e2.mapping['someint'], IntegerSetting)) + self.assertIsInstance(e2.mapping['somestring'], StringSetting) + self.assertIsInstance(e2.mapping['someint'], IntegerSetting) with self.assertRaises(ValidationError): e.mapping['someint'] = 123 @@ -2563,7 +2563,7 @@ class FieldTest(MongoDBTestCase): bm = Bookmark.objects(bookmark_object=post_1).first() self.assertEqual(bm.bookmark_object, post_1) - self.assertTrue(isinstance(bm.bookmark_object, Post)) + self.assertIsInstance(bm.bookmark_object, Post) bm.bookmark_object = link_1 bm.save() @@ -2571,7 +2571,7 @@ class FieldTest(MongoDBTestCase): bm = Bookmark.objects(bookmark_object=link_1).first() self.assertEqual(bm.bookmark_object, link_1) - self.assertTrue(isinstance(bm.bookmark_object, Link)) + self.assertIsInstance(bm.bookmark_object, Link) def test_generic_reference_list(self): """Ensure that a ListField properly dereferences generic references. @@ -2818,7 +2818,7 @@ class FieldTest(MongoDBTestCase): doc1 = Doc.objects.create() doc2 = Doc.objects.create(ref=doc1) - self.assertTrue(isinstance(doc1.pk, ObjectId)) + self.assertIsInstance(doc1.pk, ObjectId) doc = Doc.objects.get(ref=doc1.pk) self.assertEqual(doc, doc2) @@ -3421,13 +3421,13 @@ class FieldTest(MongoDBTestCase): person.save() person = Person.objects.first() - self.assertTrue(isinstance(person.like, Car)) + self.assertIsInstance(person.like, Car) person.like = Dish(food="arroz", number=15) person.save() person = Person.objects.first() - self.assertTrue(isinstance(person.like, Dish)) + self.assertIsInstance(person.like, Dish) def test_generic_embedded_document_choices(self): """Ensure you can limit GenericEmbeddedDocument choices.""" @@ -3452,7 +3452,7 @@ class FieldTest(MongoDBTestCase): person.save() person = Person.objects.first() - self.assertTrue(isinstance(person.like, Dish)) + self.assertIsInstance(person.like, Dish) def test_generic_list_embedded_document_choices(self): """Ensure you can limit GenericEmbeddedDocument choices inside @@ -3479,7 +3479,7 @@ class FieldTest(MongoDBTestCase): person.save() person = Person.objects.first() - self.assertTrue(isinstance(person.likes[0], Dish)) + self.assertIsInstance(person.likes[0], Dish) def test_recursive_validation(self): """Ensure that a validation result to_dict is available.""" @@ -3505,18 +3505,17 @@ class FieldTest(MongoDBTestCase): except ValidationError as error: # ValidationError.errors property self.assertTrue(hasattr(error, 'errors')) - self.assertTrue(isinstance(error.errors, dict)) - self.assertTrue('comments' in error.errors) - self.assertTrue(1 in error.errors['comments']) - self.assertTrue(isinstance(error.errors['comments'][1]['content'], - ValidationError)) + self.assertIsInstance(error.errors, dict) + self.assertIn('comments', error.errors) + self.assertIn(1, error.errors['comments']) + self.assertIsInstance(error.errors['comments'][1]['content'], ValidationError) # ValidationError.schema property error_dict = error.to_dict() - self.assertTrue(isinstance(error_dict, dict)) - self.assertTrue('comments' in error_dict) - self.assertTrue(1 in error_dict['comments']) - self.assertTrue('content' in error_dict['comments'][1]) + self.assertIsInstance(error_dict, dict) + self.assertIn('comments', error_dict) + self.assertIn(1, error_dict['comments']) + self.assertIn('content', error_dict['comments'][1]) self.assertEqual(error_dict['comments'][1]['content'], u'Field is required') @@ -3632,7 +3631,7 @@ class FieldTest(MongoDBTestCase): # Passes regex validation user = User(email='me@example.com') - self.assertTrue(user.validate() is None) + self.assertIsNone(user.validate()) def test_tuples_as_tuples(self): """Ensure that tuples remain tuples when they are inside @@ -3659,10 +3658,10 @@ class FieldTest(MongoDBTestCase): doc.items = tuples doc.save() x = TestDoc.objects().get() - self.assertTrue(x is not None) - self.assertTrue(len(x.items) == 1) - self.assertTrue(tuple(x.items[0]) in tuples) - self.assertTrue(x.items[0] in tuples) + self.assertIsNotNone(x) + self.assertEqual(len(x.items), 1) + self.assertIn(tuple(x.items[0]), tuples) + self.assertIn(x.items[0], tuples) def test_dynamic_fields_class(self): class Doc2(Document): @@ -3812,8 +3811,8 @@ class FieldTest(MongoDBTestCase): doc = TestLongFieldConsideredAsInt64(some_long=42).save() db = get_db() - self.assertTrue(isinstance(db.test_long_field_considered_as_int64.find()[0]['some_long'], Int64)) - self.assertTrue(isinstance(doc.some_long, six.integer_types)) + self.assertIsInstance(db.test_long_field_considered_as_int64.find()[0]['some_long'], Int64) + self.assertIsInstance(doc.some_long, six.integer_types) class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): @@ -4364,7 +4363,7 @@ class CachedReferenceFieldTest(MongoDBTestCase): ocorrence = Ocorrence.objects(animal__tag='heavy').first() self.assertEqual(ocorrence.person, "teste") - self.assertTrue(isinstance(ocorrence.animal, Animal)) + self.assertIsInstance(ocorrence.animal, Animal) def test_cached_reference_field_decimal(self): class PersonAuto(Document): @@ -4681,7 +4680,7 @@ class CachedReferenceFieldTest(MongoDBTestCase): animal__tag='heavy', animal__owner__tp='u').first() self.assertEqual(ocorrence.person, "teste") - self.assertTrue(isinstance(ocorrence.animal, Animal)) + self.assertIsInstance(ocorrence.animal, Animal) def test_cached_reference_embedded_list_fields(self): class Owner(EmbeddedDocument): @@ -4735,7 +4734,7 @@ class CachedReferenceFieldTest(MongoDBTestCase): animal__tag='heavy', animal__owner__tags='cool').first() self.assertEqual(ocorrence.person, "teste 2") - self.assertTrue(isinstance(ocorrence.animal, Animal)) + self.assertIsInstance(ocorrence.animal, Animal) class LazyReferenceFieldTest(MongoDBTestCase): diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 841e7c7d..213e889c 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -53,7 +53,7 @@ class FileTest(MongoDBTestCase): putfile.save() result = PutFile.objects.first() - self.assertTrue(putfile == result) + self.assertEqual(putfile, result) self.assertEqual("%s" % result.the_file, "" % result.the_file.grid_id) self.assertEqual(result.the_file.read(), text) self.assertEqual(result.the_file.content_type, content_type) @@ -71,7 +71,7 @@ class FileTest(MongoDBTestCase): putfile.save() result = PutFile.objects.first() - self.assertTrue(putfile == result) + self.assertEqual(putfile, result) self.assertEqual(result.the_file.read(), text) self.assertEqual(result.the_file.content_type, content_type) result.the_file.delete() @@ -96,7 +96,7 @@ class FileTest(MongoDBTestCase): streamfile.save() result = StreamFile.objects.first() - self.assertTrue(streamfile == result) + self.assertEqual(streamfile, result) self.assertEqual(result.the_file.read(), text + more_text) self.assertEqual(result.the_file.content_type, content_type) result.the_file.seek(0) @@ -132,7 +132,7 @@ class FileTest(MongoDBTestCase): streamfile.save() result = StreamFile.objects.first() - self.assertTrue(streamfile == result) + self.assertEqual(streamfile, result) self.assertEqual(result.the_file.read(), text + more_text) # self.assertEqual(result.the_file.content_type, content_type) result.the_file.seek(0) @@ -161,7 +161,7 @@ class FileTest(MongoDBTestCase): setfile.save() result = SetFile.objects.first() - self.assertTrue(setfile == result) + self.assertEqual(setfile, result) self.assertEqual(result.the_file.read(), text) # Try replacing file with new one @@ -169,7 +169,7 @@ class FileTest(MongoDBTestCase): result.save() result = SetFile.objects.first() - self.assertTrue(setfile == result) + self.assertEqual(setfile, result) self.assertEqual(result.the_file.read(), more_text) result.the_file.delete() @@ -231,8 +231,8 @@ class FileTest(MongoDBTestCase): test_file_dupe = TestFile() data = test_file_dupe.the_file.read() # Should be None - self.assertTrue(test_file.name != test_file_dupe.name) - self.assertTrue(test_file.the_file.read() != data) + self.assertNotEqual(test_file.name, test_file_dupe.name) + self.assertNotEqual(test_file.the_file.read(), data) TestFile.drop_collection() @@ -291,7 +291,7 @@ class FileTest(MongoDBTestCase): the_file = FileField() test_file = TestFile() - self.assertFalse(test_file.the_file in [{"test": 1}]) + self.assertNotIn(test_file.the_file, [{"test": 1}]) def test_file_disk_space(self): """ Test disk space usage when we delete/replace a file """ diff --git a/tests/fields/geo.py b/tests/fields/geo.py index 1c5bccc0..754f4203 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -298,9 +298,9 @@ class GeoFieldTest(unittest.TestCase): polygon = PolygonField() geo_indicies = Event._geo_indices() - self.assertTrue({'fields': [('line', '2dsphere')]} in geo_indicies) - self.assertTrue({'fields': [('polygon', '2dsphere')]} in geo_indicies) - self.assertTrue({'fields': [('point', '2dsphere')]} in geo_indicies) + self.assertIn({'fields': [('line', '2dsphere')]}, geo_indicies) + self.assertIn({'fields': [('polygon', '2dsphere')]}, geo_indicies) + self.assertIn({'fields': [('point', '2dsphere')]}, geo_indicies) def test_indexes_2dsphere_embedded(self): """Ensure that indexes are created automatically for GeoPointFields. @@ -316,9 +316,9 @@ class GeoFieldTest(unittest.TestCase): venue = EmbeddedDocumentField(Venue) geo_indicies = Event._geo_indices() - self.assertTrue({'fields': [('venue.line', '2dsphere')]} in geo_indicies) - self.assertTrue({'fields': [('venue.polygon', '2dsphere')]} in geo_indicies) - self.assertTrue({'fields': [('venue.point', '2dsphere')]} in geo_indicies) + self.assertIn({'fields': [('venue.line', '2dsphere')]}, geo_indicies) + self.assertIn({'fields': [('venue.polygon', '2dsphere')]}, geo_indicies) + self.assertIn({'fields': [('venue.point', '2dsphere')]}, geo_indicies) def test_geo_indexes_recursion(self): @@ -335,9 +335,9 @@ class GeoFieldTest(unittest.TestCase): Parent(name='Berlin').save() info = Parent._get_collection().index_information() - self.assertFalse('location_2d' in info) + self.assertNotIn('location_2d', info) info = Location._get_collection().index_information() - self.assertTrue('location_2d' in info) + self.assertIn('location_2d', info) self.assertEqual(len(Parent._geo_indices()), 0) self.assertEqual(len(Location._geo_indices()), 1) diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index c07cec3e..b111238a 100644 --- a/tests/queryset/field_list.py +++ b/tests/queryset/field_list.py @@ -181,7 +181,7 @@ class OnlyExcludeAllTest(unittest.TestCase): employee.save() obj = self.Person.objects(id=employee.id).only('age').get() - self.assertTrue(isinstance(obj, Employee)) + self.assertIsInstance(obj, Employee) # Check field names are looked up properly obj = Employee.objects(id=employee.id).only('salary').get() diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 38c0377e..fea225b2 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -95,9 +95,9 @@ class GeoQueriesTest(MongoDBTestCase): location__within_distance=point_and_distance) self.assertEqual(events.count(), 2) events = list(events) - self.assertTrue(event2 not in events) - self.assertTrue(event1 in events) - self.assertTrue(event3 in events) + self.assertNotIn(event2, events) + self.assertIn(event1, events) + self.assertIn(event3, events) # find events within 10 degrees of san francisco point_and_distance = [[-122.415579, 37.7566023], 10] @@ -285,9 +285,9 @@ class GeoQueriesTest(MongoDBTestCase): location__geo_within_center=point_and_distance) self.assertEqual(events.count(), 2) events = list(events) - self.assertTrue(event2 not in events) - self.assertTrue(event1 in events) - self.assertTrue(event3 in events) + self.assertNotIn(event2, events) + self.assertIn(event1, events) + self.assertIn(event3, events) def _test_embedded(self, point_field_class): """Helper test method ensuring given point field class works diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index c6aa3594..16268cbf 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -59,11 +59,10 @@ class QuerySetTest(unittest.TestCase): def test_initialisation(self): """Ensure that a QuerySet is correctly initialised by QuerySetManager. """ - self.assertTrue(isinstance(self.Person.objects, QuerySet)) + self.assertIsInstance(self.Person.objects, QuerySet) self.assertEqual(self.Person.objects._collection.name, self.Person._get_collection_name()) - self.assertTrue(isinstance(self.Person.objects._collection, - pymongo.collection.Collection)) + self.assertIsInstance(self.Person.objects._collection, pymongo.collection.Collection) def test_cannot_perform_joins_references(self): @@ -89,8 +88,8 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(people.count(), 2) results = list(people) - self.assertTrue(isinstance(results[0], self.Person)) - self.assertTrue(isinstance(results[0].id, (ObjectId, str, unicode))) + self.assertIsInstance(results[0], self.Person) + self.assertIsInstance(results[0].id, (ObjectId, str, unicode)) self.assertEqual(results[0], user_a) self.assertEqual(results[0].name, 'User A') @@ -225,7 +224,7 @@ class QuerySetTest(unittest.TestCase): # Retrieve the first person from the database person = self.Person.objects.first() - self.assertTrue(isinstance(person, self.Person)) + self.assertIsInstance(person, self.Person) self.assertEqual(person.name, "User A") self.assertEqual(person.age, 20) @@ -672,13 +671,13 @@ class QuerySetTest(unittest.TestCase): result = self.Person(name="Bob", age=25).update( upsert=True, full_result=True) - self.assertTrue(isinstance(result, UpdateResult)) - self.assertTrue("upserted" in result.raw_result) + self.assertIsInstance(result, UpdateResult) + self.assertIn("upserted", result.raw_result) self.assertFalse(result.raw_result["updatedExisting"]) bob = self.Person.objects.first() result = bob.update(set__age=30, full_result=True) - self.assertTrue(isinstance(result, UpdateResult)) + self.assertIsInstance(result, UpdateResult) self.assertTrue(result.raw_result["updatedExisting"]) self.Person(name="Bob", age=20).save() @@ -994,7 +993,7 @@ class QuerySetTest(unittest.TestCase): # Retrieve the first person from the database person = self.Person.objects.slave_okay(True).first() - self.assertTrue(isinstance(person, self.Person)) + self.assertIsInstance(person, self.Person) self.assertEqual(person.name, "User A") self.assertEqual(person.age, 20) @@ -1061,10 +1060,10 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(docs.count(), 1000) docs_string = "%s" % docs - self.assertTrue("Doc: 0" in docs_string) + self.assertIn("Doc: 0", docs_string) self.assertEqual(docs.count(), 1000) - self.assertTrue('(remaining elements truncated)' in "%s" % docs) + self.assertIn('(remaining elements truncated)', "%s" % docs) # Limit and skip docs = docs[1:4] @@ -1281,7 +1280,7 @@ class QuerySetTest(unittest.TestCase): with db_ops_tracker() as q: BlogPost.objects.filter(title='whatever').order_by().first() self.assertEqual(len(q.get_ops()), 1) - self.assertFalse('$orderby' in q.get_ops()[0]['query']) + self.assertNotIn('$orderby', q.get_ops()[0]['query']) # calling an explicit order_by should use a specified sort with db_ops_tracker() as q: @@ -1297,7 +1296,7 @@ class QuerySetTest(unittest.TestCase): qs = BlogPost.objects.filter(title='whatever').order_by('published_date') qs.order_by().first() self.assertEqual(len(q.get_ops()), 1) - self.assertFalse('$orderby' in q.get_ops()[0]['query']) + self.assertNotIn('$orderby', q.get_ops()[0]['query']) def test_no_ordering_for_get(self): """ Ensure that Doc.objects.get doesn't use any ordering. @@ -1316,13 +1315,13 @@ class QuerySetTest(unittest.TestCase): with db_ops_tracker() as q: BlogPost.objects.get(title='whatever') self.assertEqual(len(q.get_ops()), 1) - self.assertFalse('$orderby' in q.get_ops()[0]['query']) + self.assertNotIn('$orderby', q.get_ops()[0]['query']) # Ordering should be ignored for .get even if we set it explicitly with db_ops_tracker() as q: BlogPost.objects.order_by('-title').get(title='whatever') self.assertEqual(len(q.get_ops()), 1) - self.assertFalse('$orderby' in q.get_ops()[0]['query']) + self.assertNotIn('$orderby', q.get_ops()[0]['query']) def test_find_embedded(self): """Ensure that an embedded document is properly returned from @@ -1344,15 +1343,15 @@ class QuerySetTest(unittest.TestCase): ) result = BlogPost.objects.first() - self.assertTrue(isinstance(result.author, User)) + self.assertIsInstance(result.author, User) self.assertEqual(result.author.name, 'Test User') result = BlogPost.objects.get(author__name=user.name) - self.assertTrue(isinstance(result.author, User)) + self.assertIsInstance(result.author, User) self.assertEqual(result.author.name, 'Test User') result = BlogPost.objects.get(author={'name': user.name}) - self.assertTrue(isinstance(result.author, User)) + self.assertIsInstance(result.author, User) self.assertEqual(result.author.name, 'Test User') # Fails, since the string is not a type that is able to represent the @@ -1470,7 +1469,7 @@ class QuerySetTest(unittest.TestCase): code_chunks = ['doc["cmnts"];', 'doc["doc-name"],', 'doc["cmnts"][i]["body"]'] for chunk in code_chunks: - self.assertTrue(chunk in sub_code) + self.assertIn(chunk, sub_code) results = BlogPost.objects.exec_js(code) expected_results = [ @@ -1937,11 +1936,12 @@ class QuerySetTest(unittest.TestCase): # ListField operator BlogPost.objects.update(push__tags='mongo') post.reload() - self.assertTrue('mongo' in post.tags) + self.assertIn('mongo', post.tags) BlogPost.objects.update_one(push_all__tags=['db', 'nosql']) post.reload() - self.assertTrue('db' in post.tags and 'nosql' in post.tags) + self.assertIn('db', post.tags) + self.assertIn('nosql', post.tags) tags = post.tags[:-1] BlogPost.objects.update(pop__tags=1) @@ -3274,8 +3274,8 @@ class QuerySetTest(unittest.TestCase): News.drop_collection() info = News.objects._collection.index_information() - self.assertTrue('title_text_content_text' in info) - self.assertTrue('textIndexVersion' in info['title_text_content_text']) + self.assertIn('title_text_content_text', info) + self.assertIn('textIndexVersion', info['title_text_content_text']) News(title="Neymar quebrou a vertebra", content="O Brasil sofre com a perda de Neymar").save() @@ -3309,15 +3309,15 @@ class QuerySetTest(unittest.TestCase): '$search': 'dilma', '$language': 'pt'}, 'is_active': False}) - self.assertEqual(new.is_active, False) - self.assertTrue('dilma' in new.content) - self.assertTrue('planejamento' in new.title) + self.assertFalse(new.is_active) + self.assertIn('dilma', new.content) + self.assertIn('planejamento', new.title) query = News.objects.search_text("candidata") self.assertEqual(query._search_text, "candidata") new = query.first() - self.assertTrue(isinstance(new.get_text_score(), float)) + self.assertIsInstance(new.get_text_score(), float) # count query = News.objects.search_text('brasil').order_by('$text_score') @@ -3612,7 +3612,7 @@ class QuerySetTest(unittest.TestCase): Group.objects(id=group.id).update(set__members=[user1, user2]) group.reload() - self.assertTrue(len(group.members) == 2) + self.assertEqual(len(group.members), 2) self.assertEqual(group.members[0].name, user1.name) self.assertEqual(group.members[1].name, user2.name) @@ -3643,13 +3643,13 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(len(objects), 3) - self.assertTrue(post_1.id in objects) - self.assertTrue(post_2.id in objects) - self.assertTrue(post_5.id in objects) + self.assertIn(post_1.id, objects) + self.assertIn(post_2.id, objects) + self.assertIn(post_5.id, objects) - self.assertTrue(objects[post_1.id].title == post_1.title) - self.assertTrue(objects[post_2.id].title == post_2.title) - self.assertTrue(objects[post_5.id].title == post_5.title) + self.assertEqual(objects[post_1.id].title, post_1.title) + self.assertEqual(objects[post_2.id].title, post_2.title) + self.assertEqual(objects[post_5.id].title, post_5.title) BlogPost.drop_collection() @@ -3669,7 +3669,7 @@ class QuerySetTest(unittest.TestCase): Post.drop_collection() - self.assertTrue(isinstance(Post.objects, CustomQuerySet)) + self.assertIsInstance(Post.objects, CustomQuerySet) self.assertFalse(Post.objects.not_empty()) Post().save() @@ -3694,7 +3694,7 @@ class QuerySetTest(unittest.TestCase): Post.drop_collection() - self.assertTrue(isinstance(Post.objects, CustomQuerySet)) + self.assertIsInstance(Post.objects, CustomQuerySet) self.assertFalse(Post.objects.not_empty()) Post().save() @@ -3741,7 +3741,7 @@ class QuerySetTest(unittest.TestCase): pass Post.drop_collection() - self.assertTrue(isinstance(Post.objects, CustomQuerySet)) + self.assertIsInstance(Post.objects, CustomQuerySet) self.assertFalse(Post.objects.not_empty()) Post().save() @@ -3769,7 +3769,7 @@ class QuerySetTest(unittest.TestCase): pass Post.drop_collection() - self.assertTrue(isinstance(Post.objects, CustomQuerySet)) + self.assertIsInstance(Post.objects, CustomQuerySet) self.assertFalse(Post.objects.not_empty()) Post().save() @@ -3860,17 +3860,17 @@ class QuerySetTest(unittest.TestCase): test = Number.objects test2 = test.clone() - self.assertFalse(test == test2) + self.assertNotEqual(test, test2) self.assertEqual(test.count(), test2.count()) test = test.filter(n__gt=11) test2 = test.clone() - self.assertFalse(test == test2) + self.assertNotEqual(test, test2) self.assertEqual(test.count(), test2.count()) test = test.limit(10) test2 = test.clone() - self.assertFalse(test == test2) + self.assertNotEqual(test, test2) self.assertEqual(test.count(), test2.count()) Number.drop_collection() @@ -3960,7 +3960,7 @@ class QuerySetTest(unittest.TestCase): value.get('unique', False), value.get('sparse', False)) for key, value in info.iteritems()] - self.assertTrue(([('_cls', 1), ('message', 1)], False, False) in info) + self.assertIn(([('_cls', 1), ('message', 1)], False, False), info) def test_where(self): """Ensure that where clauses work. @@ -3984,13 +3984,13 @@ class QuerySetTest(unittest.TestCase): 'this["fielda"] >= this["fieldb"]', query._where_clause) results = list(query) self.assertEqual(2, len(results)) - self.assertTrue(a in results) - self.assertTrue(c in results) + self.assertIn(a, results) + self.assertIn(c, results) query = IntPair.objects.where('this[~fielda] == this[~fieldb]') results = list(query) self.assertEqual(1, len(results)) - self.assertTrue(a in results) + self.assertIn(a, results) query = IntPair.objects.where( 'function() { return this[~fielda] >= this[~fieldb] }') @@ -3998,8 +3998,8 @@ class QuerySetTest(unittest.TestCase): 'function() { return this["fielda"] >= this["fieldb"] }', query._where_clause) results = list(query) self.assertEqual(2, len(results)) - self.assertTrue(a in results) - self.assertTrue(c in results) + self.assertIn(a, results) + self.assertIn(c, results) with self.assertRaises(TypeError): list(IntPair.objects.where(fielda__gte=3)) @@ -4381,7 +4381,7 @@ class QuerySetTest(unittest.TestCase): Test.drop_collection() Test.objects(test='foo').update_one(upsert=True, set__test='foo') - self.assertFalse('_cls' in Test._collection.find_one()) + self.assertNotIn('_cls', Test._collection.find_one()) class Test(Document): meta = {'allow_inheritance': True} @@ -4390,7 +4390,7 @@ class QuerySetTest(unittest.TestCase): Test.drop_collection() Test.objects(test='foo').update_one(upsert=True, set__test='foo') - self.assertTrue('_cls' in Test._collection.find_one()) + self.assertIn('_cls', Test._collection.find_one()) def test_update_upsert_looks_like_a_digit(self): class MyDoc(DynamicDocument): @@ -4602,8 +4602,8 @@ class QuerySetTest(unittest.TestCase): users = User.objects.only('name', 'price').as_pymongo() results = list(users) - self.assertTrue(isinstance(results[0], dict)) - self.assertTrue(isinstance(results[1], dict)) + self.assertIsInstance(results[0], dict) + self.assertIsInstance(results[1], dict) self.assertEqual(results[0]['name'], 'Bob Dole') self.assertEqual(results[0]['price'], 1.11) self.assertEqual(results[1]['name'], 'Barack Obama') @@ -4611,8 +4611,8 @@ class QuerySetTest(unittest.TestCase): users = User.objects.only('name', 'last_login').as_pymongo() results = list(users) - self.assertTrue(isinstance(results[0], dict)) - self.assertTrue(isinstance(results[1], dict)) + self.assertIsInstance(results[0], dict) + self.assertIsInstance(results[1], dict) self.assertEqual(results[0], { 'name': 'Bob Dole' }) @@ -4669,12 +4669,10 @@ class QuerySetTest(unittest.TestCase): User(name="Bob Dole", organization=whitehouse).save() qs = User.objects() - self.assertTrue(isinstance(qs.first().organization, Organization)) - self.assertFalse(isinstance(qs.no_dereference().first().organization, - Organization)) - self.assertFalse(isinstance(qs.no_dereference().get().organization, - Organization)) - self.assertTrue(isinstance(qs.first().organization, Organization)) + self.assertIsInstance(qs.first().organization, Organization) + self.assertNotIsInstance(qs.no_dereference().first().organization, Organization) + self.assertNotIsInstance(qs.no_dereference().get().organization, Organization) + self.assertIsInstance(qs.first().organization, Organization) def test_no_dereference_embedded_doc(self): @@ -4707,9 +4705,9 @@ class QuerySetTest(unittest.TestCase): result = Organization.objects().no_dereference().first() - self.assertTrue(isinstance(result.admin[0], (DBRef, ObjectId))) - self.assertTrue(isinstance(result.member.user, (DBRef, ObjectId))) - self.assertTrue(isinstance(result.members[0].user, (DBRef, ObjectId))) + self.assertIsInstance(result.admin[0], (DBRef, ObjectId)) + self.assertIsInstance(result.member.user, (DBRef, ObjectId)) + self.assertIsInstance(result.members[0].user, (DBRef, ObjectId)) def test_cached_queryset(self): class Person(Document): @@ -5052,7 +5050,7 @@ class QuerySetTest(unittest.TestCase): op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertFalse('$orderby' in op['query'], + self.assertNotIn('$orderby', op['query'], 'BaseQuerySet cannot use orderby in if stmt') with query_counter() as p: @@ -5063,8 +5061,7 @@ class QuerySetTest(unittest.TestCase): op = p.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertTrue('$orderby' in op['query'], - 'BaseQuerySet cannot remove orderby in for loop') + self.assertIn('$orderby', op['query'], 'BaseQuerySet cannot remove orderby in for loop') def test_bool_with_ordering_from_meta_dict(self): @@ -5088,7 +5085,7 @@ class QuerySetTest(unittest.TestCase): op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertFalse('$orderby' in op['query'], + self.assertNotIn('$orderby', op['query'], 'BaseQuerySet must remove orderby from meta in boolen test') self.assertEqual(Person.objects.first().name, 'A') diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 38098432..8064f09c 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -48,15 +48,15 @@ class TransformTest(unittest.TestCase): for k, v in (("set", "$set"), ("set_on_insert", "$setOnInsert"), ("push", "$push")): update = transform.update(DicDoc, **{"%s__dictField__test" % k: doc}) - self.assertTrue(isinstance(update[v]["dictField.test"], dict)) + self.assertIsInstance(update[v]["dictField.test"], dict) # Update special cases update = transform.update(DicDoc, unset__dictField__test=doc) self.assertEqual(update["$unset"]["dictField.test"], 1) update = transform.update(DicDoc, pull__dictField__test=doc) - self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict)) - + self.assertIsInstance(update["$pull"]["dictField"]["test"], dict) + update = transform.update(LisDoc, pull__foo__in=['a']) self.assertEqual(update, {'$pull': {'foo': {'$in': ['a']}}}) @@ -88,17 +88,15 @@ class TransformTest(unittest.TestCase): post = BlogPost(**data) post.save() - self.assertTrue('postTitle' in - BlogPost.objects(title=data['title'])._query) + self.assertIn('postTitle', BlogPost.objects(title=data['title'])._query) self.assertFalse('title' in BlogPost.objects(title=data['title'])._query) self.assertEqual(BlogPost.objects(title=data['title']).count(), 1) - self.assertTrue('_id' in BlogPost.objects(pk=post.id)._query) + self.assertIn('_id', BlogPost.objects(pk=post.id)._query) self.assertEqual(BlogPost.objects(pk=post.id).count(), 1) - self.assertTrue('postComments.commentContent' in - BlogPost.objects(comments__content='test')._query) + self.assertIn('postComments.commentContent', BlogPost.objects(comments__content='test')._query) self.assertEqual(BlogPost.objects(comments__content='test').count(), 1) BlogPost.drop_collection() @@ -116,8 +114,8 @@ class TransformTest(unittest.TestCase): post = BlogPost(**data) post.save() - self.assertTrue('_id' in BlogPost.objects(pk=data['title'])._query) - self.assertTrue('_id' in BlogPost.objects(title=data['title'])._query) + self.assertIn('_id', BlogPost.objects(pk=data['title'])._query) + self.assertIn('_id', BlogPost.objects(title=data['title'])._query) self.assertEqual(BlogPost.objects(pk=data['title']).count(), 1) BlogPost.drop_collection() @@ -260,31 +258,31 @@ class TransformTest(unittest.TestCase): events = Event.objects(location__within=box) with self.assertRaises(InvalidQueryError): events.count() - + def test_update_pull_for_list_fields(self): - """ - Test added to check pull operation in update for + """ + Test added to check pull operation in update for EmbeddedDocumentListField which is inside a EmbeddedDocumentField """ class Word(EmbeddedDocument): word = StringField() index = IntField() - + class SubDoc(EmbeddedDocument): heading = ListField(StringField()) text = EmbeddedDocumentListField(Word) - + class MainDoc(Document): title = StringField() content = EmbeddedDocumentField(SubDoc) - + word = Word(word='abc', index=1) update = transform.update(MainDoc, pull__content__text=word) self.assertEqual(update, {'$pull': {'content.text': SON([('word', u'abc'), ('index', 1)])}}) update = transform.update(MainDoc, pull__content__heading='xyz') self.assertEqual(update, {'$pull': {'content.heading': 'xyz'}}) - - + + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/visitor.py b/tests/queryset/visitor.py index 6f020e88..7b68cfb0 100644 --- a/tests/queryset/visitor.py +++ b/tests/queryset/visitor.py @@ -196,7 +196,7 @@ class QTest(unittest.TestCase): test2 = test.clone() self.assertEqual(test2.count(), 3) - self.assertFalse(test2 == test) + self.assertNotEqual(test2, test) test3 = test2.filter(x=6) self.assertEqual(test3.count(), 1) diff --git a/tests/test_connection.py b/tests/test_connection.py index f58b1a3e..88d63cdb 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -39,15 +39,15 @@ class ConnectionTest(unittest.TestCase): connect('mongoenginetest') conn = get_connection() - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'mongoenginetest') connect('mongoenginetest2', alias='testdb') conn = get_connection('testdb') - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) def test_connect_in_mocking(self): """Ensure that the connect() method works properly in mocking. @@ -59,31 +59,31 @@ class ConnectionTest(unittest.TestCase): connect('mongoenginetest', host='mongomock://localhost') conn = get_connection() - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect('mongoenginetest2', host='mongomock://localhost', alias='testdb2') conn = get_connection('testdb2') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect('mongoenginetest3', host='mongodb://localhost', is_mock=True, alias='testdb3') conn = get_connection('testdb3') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect('mongoenginetest4', is_mock=True, alias='testdb4') conn = get_connection('testdb4') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host='mongodb://localhost:27017/mongoenginetest5', is_mock=True, alias='testdb5') conn = get_connection('testdb5') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host='mongomock://localhost:27017/mongoenginetest6', alias='testdb6') conn = get_connection('testdb6') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host='mongomock://localhost:27017/mongoenginetest7', is_mock=True, alias='testdb7') conn = get_connection('testdb7') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) def test_connect_with_host_list(self): """Ensure that the connect() method works when host is a list @@ -97,27 +97,27 @@ class ConnectionTest(unittest.TestCase): connect(host=['mongomock://localhost']) conn = get_connection() - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host=['mongodb://localhost'], is_mock=True, alias='testdb2') conn = get_connection('testdb2') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host=['localhost'], is_mock=True, alias='testdb3') conn = get_connection('testdb3') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host=['mongomock://localhost:27017', 'mongomock://localhost:27018'], alias='testdb4') conn = get_connection('testdb4') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host=['mongodb://localhost:27017', 'mongodb://localhost:27018'], is_mock=True, alias='testdb5') conn = get_connection('testdb5') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) connect(host=['localhost:27017', 'localhost:27018'], is_mock=True, alias='testdb6') conn = get_connection('testdb6') - self.assertTrue(isinstance(conn, mongomock.MongoClient)) + self.assertIsInstance(conn, mongomock.MongoClient) def test_disconnect(self): """Ensure that the disconnect() method works properly @@ -163,10 +163,10 @@ class ConnectionTest(unittest.TestCase): connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest') conn = get_connection() - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'mongoenginetest') c.admin.system.users.remove({}) @@ -179,10 +179,10 @@ class ConnectionTest(unittest.TestCase): connect("mongoenginetest", host='mongodb://localhost/') conn = get_connection() - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'mongoenginetest') def test_connect_uri_default_db(self): @@ -192,10 +192,10 @@ class ConnectionTest(unittest.TestCase): connect(host='mongodb://localhost/') conn = get_connection() - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'test') def test_uri_without_credentials_doesnt_override_conn_settings(self): @@ -242,7 +242,7 @@ class ConnectionTest(unittest.TestCase): 'mongoenginetest?authSource=admin') ) db = get_db('test2') - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'mongoenginetest') # Clear all users @@ -255,10 +255,10 @@ class ConnectionTest(unittest.TestCase): self.assertRaises(MongoEngineConnectionError, get_connection) conn = get_connection('testdb') - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) db = get_db('testdb') - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'mongoenginetest2') def test_register_connection_defaults(self): @@ -267,7 +267,7 @@ class ConnectionTest(unittest.TestCase): register_connection('testdb', 'mongoenginetest', host=None, port=None) conn = get_connection('testdb') - self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) + self.assertIsInstance(conn, pymongo.mongo_client.MongoClient) def test_connection_kwargs(self): """Ensure that connection kwargs get passed to pymongo.""" @@ -326,7 +326,7 @@ class ConnectionTest(unittest.TestCase): if IS_PYMONGO_3: c = connect(host='mongodb://localhost/test?replicaSet=local-rs') db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'test') else: # PyMongo < v3.x raises an exception: @@ -343,7 +343,7 @@ class ConnectionTest(unittest.TestCase): self.assertEqual(c._MongoClient__options.replica_set_name, 'local-rs') db = get_db() - self.assertTrue(isinstance(db, pymongo.database.Database)) + self.assertIsInstance(db, pymongo.database.Database) self.assertEqual(db.name, 'test') else: # PyMongo < v3.x raises an exception: @@ -377,8 +377,8 @@ class ConnectionTest(unittest.TestCase): mongo_connections = mongoengine.connection._connections self.assertEqual(len(mongo_connections.items()), 2) - self.assertTrue('t1' in mongo_connections.keys()) - self.assertTrue('t2' in mongo_connections.keys()) + self.assertIn('t1', mongo_connections.keys()) + self.assertIn('t2', mongo_connections.keys()) if not IS_PYMONGO_3: self.assertEqual(mongo_connections['t1'].host, 'localhost') self.assertEqual(mongo_connections['t2'].host, '127.0.0.1') diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index 8c96016c..3c93b96f 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -89,15 +89,15 @@ class ContextManagersTest(unittest.TestCase): with no_dereference(Group) as Group: group = Group.objects.first() - self.assertTrue(all([not isinstance(m, User) - for m in group.members])) - self.assertFalse(isinstance(group.ref, User)) - self.assertFalse(isinstance(group.generic, User)) + for m in group.members: + self.assertNotIsInstance(m, User) + self.assertNotIsInstance(group.ref, User) + self.assertNotIsInstance(group.generic, User) - self.assertTrue(all([isinstance(m, User) - for m in group.members])) - self.assertTrue(isinstance(group.ref, User)) - self.assertTrue(isinstance(group.generic, User)) + for m in group.members: + self.assertIsInstance(m, User) + self.assertIsInstance(group.ref, User) + self.assertIsInstance(group.generic, User) def test_no_dereference_context_manager_dbref(self): """Ensure that DBRef items in ListFields aren't dereferenced. @@ -129,13 +129,13 @@ class ContextManagersTest(unittest.TestCase): group = Group.objects.first() self.assertTrue(all([not isinstance(m, User) for m in group.members])) - self.assertFalse(isinstance(group.ref, User)) - self.assertFalse(isinstance(group.generic, User)) + self.assertNotIsInstance(group.ref, User) + self.assertNotIsInstance(group.generic, User) self.assertTrue(all([isinstance(m, User) for m in group.members])) - self.assertTrue(isinstance(group.ref, User)) - self.assertTrue(isinstance(group.generic, User)) + self.assertIsInstance(group.ref, User) + self.assertIsInstance(group.generic, User) def test_no_sub_classes(self): class A(Document): diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 7f58a85b..8b8bcfb2 100644 --- a/tests/test_dereference.py +++ b/tests/test_dereference.py @@ -200,8 +200,8 @@ class FieldTest(unittest.TestCase): group = Group(author=user, members=[user]).save() raw_data = Group._get_collection().find_one() - self.assertTrue(isinstance(raw_data['author'], DBRef)) - self.assertTrue(isinstance(raw_data['members'][0], DBRef)) + self.assertIsInstance(raw_data['author'], DBRef) + self.assertIsInstance(raw_data['members'][0], DBRef) group = Group.objects.first() self.assertEqual(group.author, user) @@ -224,8 +224,8 @@ class FieldTest(unittest.TestCase): self.assertEqual(group.members, [user]) raw_data = Group._get_collection().find_one() - self.assertTrue(isinstance(raw_data['author'], ObjectId)) - self.assertTrue(isinstance(raw_data['members'][0], ObjectId)) + self.assertIsInstance(raw_data['author'], ObjectId) + self.assertIsInstance(raw_data['members'][0], ObjectId) def test_recursive_reference(self): """Ensure that ReferenceFields can reference their own documents. @@ -469,7 +469,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Document select_related with query_counter() as q: @@ -485,7 +485,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Queryset select_related with query_counter() as q: @@ -502,7 +502,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) UserA.drop_collection() UserB.drop_collection() @@ -560,7 +560,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Document select_related with query_counter() as q: @@ -576,7 +576,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Queryset select_related with query_counter() as q: @@ -593,7 +593,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for m in group_obj.members: - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) UserA.drop_collection() UserB.drop_collection() @@ -633,7 +633,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, User)) + self.assertIsInstance(m, User) # Document select_related with query_counter() as q: @@ -646,7 +646,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, User)) + self.assertIsInstance(m, User) # Queryset select_related with query_counter() as q: @@ -660,7 +660,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, User)) + self.assertIsInstance(m, User) User.drop_collection() Group.drop_collection() @@ -715,7 +715,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Document select_related with query_counter() as q: @@ -731,7 +731,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Queryset select_related with query_counter() as q: @@ -748,7 +748,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) Group.objects.delete() Group().save() @@ -806,7 +806,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, UserA)) + self.assertIsInstance(m, UserA) # Document select_related with query_counter() as q: @@ -822,7 +822,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, UserA)) + self.assertIsInstance(m, UserA) # Queryset select_related with query_counter() as q: @@ -839,7 +839,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 2) for k, m in group_obj.members.iteritems(): - self.assertTrue(isinstance(m, UserA)) + self.assertIsInstance(m, UserA) UserA.drop_collection() Group.drop_collection() @@ -894,7 +894,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Document select_related with query_counter() as q: @@ -910,7 +910,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) # Queryset select_related with query_counter() as q: @@ -927,7 +927,7 @@ class FieldTest(unittest.TestCase): self.assertEqual(q, 4) for k, m in group_obj.members.iteritems(): - self.assertTrue('User' in m.__class__.__name__) + self.assertIn('User', m.__class__.__name__) Group.objects.delete() Group().save() @@ -1209,10 +1209,10 @@ class FieldTest(unittest.TestCase): # Can't use query_counter across databases - so test the _data object book = Book.objects.first() - self.assertFalse(isinstance(book._data['author'], User)) + self.assertNotIsInstance(book._data['author'], User) book.select_related() - self.assertTrue(isinstance(book._data['author'], User)) + self.assertIsInstance(book._data['author'], User) def test_non_ascii_pk(self): """ From a86092fb6450df0911aea761230451d88027ae60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Thu, 6 Sep 2018 22:33:24 +0200 Subject: [PATCH 264/268] fix doc for meta index_options --- docs/guide/defining-documents.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 366d12c7..62cbcd67 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -529,7 +529,7 @@ There are a few top level defaults for all indexes that can be set:: title = StringField() rating = StringField() meta = { - 'index_options': {}, + 'index_opts': {}, 'index_background': True, 'index_cls': False, 'auto_create_index': True, @@ -537,7 +537,7 @@ There are a few top level defaults for all indexes that can be set:: } -:attr:`index_options` (Optional) +:attr:`index_opts` (Optional) Set any default index options - see the `full options list `_ :attr:`index_background` (Optional) From 7fc5ced3af3380ffec60787a508a7a049180d194 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Thu, 6 Sep 2018 16:50:31 -0400 Subject: [PATCH 265/268] Update link to index options --- docs/guide/defining-documents.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 366d12c7..06666022 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -538,7 +538,7 @@ There are a few top level defaults for all indexes that can be set:: :attr:`index_options` (Optional) - Set any default index options - see the `full options list `_ + Set any default index options - see the `full options list `_ :attr:`index_background` (Optional) Set the default value for if an index should be indexed in the background From aa49283fa9394ede764c26a0329ccae3c62b5375 Mon Sep 17 00:00:00 2001 From: Erdenezul Batmunkh Date: Fri, 7 Sep 2018 15:39:55 +0800 Subject: [PATCH 266/268] fix trailing whitespace --- mongoengine/queryset/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index eb0fd669..6fddc381 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -775,7 +775,7 @@ class BaseQuerySet(object): """Limit the number of returned documents to `n`. This may also be achieved using array-slicing syntax (e.g. ``User.objects[:5]``). - :param n: the maximum number of objects to return if n is greater than 0. + :param n: the maximum number of objects to return if n is greater than 0. When 0 is passed, returns all the documents in the cursor """ queryset = self.clone() From d6e39b362b7837aa4ca3f84f5b9eccb3adf3e808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Fri, 7 Sep 2018 22:51:17 +0200 Subject: [PATCH 267/268] Updating inheritance doc Fixes #429 --- docs/guide/defining-documents.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index 62cbcd67..bf74ad8c 100644 --- a/docs/guide/defining-documents.rst +++ b/docs/guide/defining-documents.rst @@ -736,6 +736,9 @@ document.:: .. note:: From 0.8 onwards :attr:`allow_inheritance` defaults to False, meaning you must set it to True to use inheritance. + + Setting :attr:`allow_inheritance` to True should also be used in + :class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it Working with existing data -------------------------- From 4314fa883fffbbfee056028a4846f98333748e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Sun, 9 Sep 2018 15:50:48 +0200 Subject: [PATCH 268/268] improve 2-3 codebase compatibility --- docs/code/tumblelog.py | 26 +++++++++++++------------- mongoengine/base/common.py | 8 ++++---- mongoengine/base/datastructures.py | 2 +- mongoengine/base/document.py | 4 ++-- mongoengine/base/fields.py | 2 +- mongoengine/dereference.py | 2 +- mongoengine/document.py | 12 ++++-------- mongoengine/fields.py | 13 +++++++++++-- mongoengine/queryset/base.py | 17 ++++++++--------- mongoengine/queryset/field_list.py | 4 +++- mongoengine/queryset/manager.py | 2 +- mongoengine/queryset/queryset.py | 4 ++-- setup.py | 3 +-- tests/__init__.py | 8 ++++---- tests/document/__init__.py | 16 ++++++++-------- tests/fields/__init__.py | 6 +++--- tests/queryset/__init__.py | 12 ++++++------ 17 files changed, 73 insertions(+), 68 deletions(-) diff --git a/docs/code/tumblelog.py b/docs/code/tumblelog.py index c10160ea..796336e6 100644 --- a/docs/code/tumblelog.py +++ b/docs/code/tumblelog.py @@ -45,27 +45,27 @@ post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs' post2.tags = ['mongoengine'] post2.save() -print 'ALL POSTS' -print +print('ALL POSTS') +print() for post in Post.objects: - print post.title + print(post.title) #print '=' * post.title.count() - print "=" * 20 + print("=" * 20) if isinstance(post, TextPost): - print post.content + print(post.content) if isinstance(post, LinkPost): - print 'Link:', post.link_url + print('Link:', post.link_url) - print -print + print() +print() -print 'POSTS TAGGED \'MONGODB\'' -print +print('POSTS TAGGED \'MONGODB\'') +print() for post in Post.objects(tags='mongodb'): - print post.title -print + print(post.title) +print() num_posts = Post.objects(tags='mongodb').count() -print 'Found %d posts with tag "mongodb"' % num_posts +print('Found %d posts with tag "mongodb"' % num_posts) diff --git a/mongoengine/base/common.py b/mongoengine/base/common.py index f80471ef..dd177920 100644 --- a/mongoengine/base/common.py +++ b/mongoengine/base/common.py @@ -3,10 +3,10 @@ from mongoengine.errors import NotRegistered __all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry') -UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'mul', - 'pop', 'push', 'push_all', 'pull', - 'pull_all', 'add_to_set', 'set_on_insert', - 'min', 'max', 'rename']) +UPDATE_OPERATORS = {'set', 'unset', 'inc', 'dec', 'mul', + 'pop', 'push', 'push_all', 'pull', + 'pull_all', 'add_to_set', 'set_on_insert', + 'min', 'max', 'rename'} _document_registry = {} diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index db292f14..0197ad10 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -377,7 +377,7 @@ class EmbeddedDocumentList(BaseList): class StrictDict(object): __slots__ = () - _special_fields = set(['get', 'pop', 'iteritems', 'items', 'keys', 'create']) + _special_fields = {'get', 'pop', 'iteritems', 'items', 'keys', 'create'} _classes = {} def __init__(self, **kwargs): diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 85906a3e..fdebab23 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -302,7 +302,7 @@ class BaseDocument(object): data['_cls'] = self._class_name # only root fields ['test1.a', 'test2'] => ['test1', 'test2'] - root_fields = set([f.split('.')[0] for f in fields]) + root_fields = {f.split('.')[0] for f in fields} for field_name in self: if root_fields and field_name not in root_fields: @@ -567,7 +567,7 @@ class BaseDocument(object): continue elif isinstance(field, SortedListField) and field._ordering: # if ordering is affected whole list is changed - if any(map(lambda d: field._ordering in d._changed_fields, data)): + if any(field._ordering in d._changed_fields for d in data): changed_fields.append(db_field_name) continue diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index d25d4305..a0726aa6 100644 --- a/mongoengine/base/fields.py +++ b/mongoengine/base/fields.py @@ -501,7 +501,7 @@ class GeoJsonBaseField(BaseField): def validate(self, value): """Validate the GeoJson object based on its type.""" if isinstance(value, dict): - if set(value.keys()) == set(['type', 'coordinates']): + if set(value.keys()) == {'type', 'coordinates'}: if value['type'] != self._type: self.error('%s type must be "%s"' % (self._name, self._type)) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 40bc72b2..6c993223 100644 --- a/mongoengine/dereference.py +++ b/mongoengine/dereference.py @@ -146,7 +146,7 @@ class DeReference(object): for key, doc in references.iteritems(): object_map[(col_name, key)] = doc else: # Generic reference: use the refs data to convert to document - if isinstance(doc_type, (ListField, DictField, MapField,)): + if isinstance(doc_type, (ListField, DictField, MapField)): continue refs = [dbref for dbref in dbrefs diff --git a/mongoengine/document.py b/mongoengine/document.py index 25af273d..cdeed4c6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -39,7 +39,7 @@ class InvalidCollectionError(Exception): pass -class EmbeddedDocument(BaseDocument): +class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)): """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 @@ -58,7 +58,6 @@ class EmbeddedDocument(BaseDocument): # The __metaclass__ attribute is removed by 2to3 when running with Python3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = DocumentMetaclass - __metaclass__ = DocumentMetaclass # A generic embedded document doesn't have any immutable properties # that describe it uniquely, hence it shouldn't be hashable. You can @@ -95,7 +94,7 @@ class EmbeddedDocument(BaseDocument): self._instance.reload(*args, **kwargs) -class Document(BaseDocument): +class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)): """The base class used for defining the structure and properties of collections of documents stored in MongoDB. Inherit from this class, and add fields as class attributes to define a document's structure. @@ -150,7 +149,6 @@ class Document(BaseDocument): # The __metaclass__ attribute is removed by 2to3 when running with Python3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass __slots__ = ('__objects',) @@ -996,7 +994,7 @@ class Document(BaseDocument): return {'missing': missing, 'extra': extra} -class DynamicDocument(Document): +class DynamicDocument(six.with_metaclass(TopLevelDocumentMetaclass, Document)): """A Dynamic Document class allowing flexible, expandable and uncontrolled schemas. As a :class:`~mongoengine.Document` subclass, acts in the same way as an ordinary document but has expanded style properties. Any data @@ -1013,7 +1011,6 @@ class DynamicDocument(Document): # The __metaclass__ attribute is removed by 2to3 when running with Python3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = TopLevelDocumentMetaclass - __metaclass__ = TopLevelDocumentMetaclass _dynamic = True @@ -1029,7 +1026,7 @@ class DynamicDocument(Document): super(DynamicDocument, self).__delattr__(*args, **kwargs) -class DynamicEmbeddedDocument(EmbeddedDocument): +class DynamicEmbeddedDocument(six.with_metaclass(DocumentMetaclass, EmbeddedDocument)): """A Dynamic Embedded Document class allowing flexible, expandable and uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more information about dynamic documents. @@ -1038,7 +1035,6 @@ class DynamicEmbeddedDocument(EmbeddedDocument): # The __metaclass__ attribute is removed by 2to3 when running with Python3 # my_metaclass is defined so that metaclass can be queried in Python 2 & 3 my_metaclass = DocumentMetaclass - __metaclass__ = DocumentMetaclass _dynamic = True diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 244551d2..f794edff 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -24,6 +24,7 @@ try: except ImportError: Int64 = long + from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField, LazyReference, ObjectIdField, get_document) @@ -41,6 +42,12 @@ except ImportError: Image = None ImageOps = None +if six.PY3: + # Useless as long as 2to3 gets executed + # as it turns `long` into `int` blindly + long = int + + __all__ = ( 'StringField', 'URLField', 'EmailField', 'IntField', 'LongField', 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'DateField', @@ -597,7 +604,7 @@ class ComplexDateTimeField(StringField): >>> ComplexDateTimeField()._convert_from_string(a) datetime.datetime(2011, 6, 8, 20, 26, 24, 92284) """ - values = map(int, data.split(self.separator)) + values = [int(d) for d in data.split(self.separator)] return datetime.datetime(*values) def __get__(self, instance, owner): @@ -1525,9 +1532,11 @@ class GridFSProxy(object): def __get__(self, instance, value): return self - def __nonzero__(self): + def __bool__(self): return bool(self.grid_id) + __nonzero__ = __bool__ # For Py2 support + def __getstate__(self): self_dict = self.__dict__ self_dict['_fs'] = None diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index 6fddc381..0be48654 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -2,7 +2,6 @@ from __future__ import absolute_import import copy import itertools -import operator import pprint import re import warnings @@ -209,14 +208,12 @@ class BaseQuerySet(object): queryset = self.order_by() return False if queryset.first() is None else True - def __nonzero__(self): - """Avoid to open all records in an if stmt in Py2.""" - return self._has_data() - def __bool__(self): """Avoid to open all records in an if stmt in Py3.""" return self._has_data() + __nonzero__ = __bool__ # For Py2 support + # Core functions def all(self): @@ -269,13 +266,13 @@ class BaseQuerySet(object): queryset = queryset.filter(*q_objs, **query) try: - result = queryset.next() + result = six.next(queryset) except StopIteration: msg = ('%s matching query does not exist.' % queryset._document._class_name) raise queryset._document.DoesNotExist(msg) try: - queryset.next() + six.next(queryset) except StopIteration: return result @@ -1478,13 +1475,13 @@ class BaseQuerySet(object): # Iterator helpers - def next(self): + def __next__(self): """Wrap the result in a :class:`~mongoengine.Document` object. """ if self._limit == 0 or self._none: raise StopIteration - raw_doc = self._cursor.next() + raw_doc = six.next(self._cursor) if self._as_pymongo: return self._get_as_pymongo(raw_doc) @@ -1498,6 +1495,8 @@ class BaseQuerySet(object): return doc + next = __next__ # For Python2 support + def rewind(self): """Rewind the cursor to its unevaluated state. diff --git a/mongoengine/queryset/field_list.py b/mongoengine/queryset/field_list.py index 0524c3bb..dba724af 100644 --- a/mongoengine/queryset/field_list.py +++ b/mongoengine/queryset/field_list.py @@ -63,9 +63,11 @@ class QueryFieldList(object): self._only_called = True return self - def __nonzero__(self): + def __bool__(self): return bool(self.fields) + __nonzero__ = __bool__ # For Py2 support + def as_dict(self): field_list = {field: self.value for field in self.fields} if self.slice: diff --git a/mongoengine/queryset/manager.py b/mongoengine/queryset/manager.py index 199205e9..f93dbb43 100644 --- a/mongoengine/queryset/manager.py +++ b/mongoengine/queryset/manager.py @@ -36,7 +36,7 @@ class QuerySetManager(object): queryset_class = owner._meta.get('queryset_class', self.default) queryset = queryset_class(owner, owner._get_collection()) if self.get_queryset: - arg_count = self.get_queryset.func_code.co_argcount + arg_count = self.get_queryset.__code__.co_argcount if arg_count == 1: queryset = self.get_queryset(queryset) elif arg_count == 2: diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py index 1aadfb76..f9fed7b7 100644 --- a/mongoengine/queryset/queryset.py +++ b/mongoengine/queryset/queryset.py @@ -115,7 +115,7 @@ class QuerySet(BaseQuerySet): # the result cache. try: for _ in six.moves.range(ITER_CHUNK_SIZE): - self._result_cache.append(self.next()) + self._result_cache.append(six.next(self)) except StopIteration: # Getting this exception means there are no more docs in the # db cursor. Set _has_more to False so that we can use that @@ -170,7 +170,7 @@ class QuerySetNoCache(BaseQuerySet): data = [] for _ in six.moves.range(REPR_OUTPUT_SIZE + 1): try: - data.append(self.next()) + data.append(six.next(self)) except StopIteration: break diff --git a/setup.py b/setup.py index 98964d19..c7632ce3 100644 --- a/setup.py +++ b/setup.py @@ -44,9 +44,8 @@ CLASSIFIERS = [ "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", 'Topic :: Database', diff --git a/tests/__init__.py b/tests/__init__.py index eab0ddc7..08db7186 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ -from all_warnings import AllWarnings -from document import * -from queryset import * -from fields import * +from .all_warnings import AllWarnings +from .document import * +from .queryset import * +from .fields import * diff --git a/tests/document/__init__.py b/tests/document/__init__.py index f71376ea..dc35c969 100644 --- a/tests/document/__init__.py +++ b/tests/document/__init__.py @@ -1,13 +1,13 @@ import unittest -from class_methods import * -from delta import * -from dynamic import * -from indexes import * -from inheritance import * -from instance import * -from json_serialisation import * -from validation import * +from .class_methods import * +from .delta import * +from .dynamic import * +from .indexes import * +from .inheritance import * +from .instance import * +from .json_serialisation import * +from .validation import * if __name__ == '__main__': unittest.main() diff --git a/tests/fields/__init__.py b/tests/fields/__init__.py index 8e0640db..4994d0c6 100644 --- a/tests/fields/__init__.py +++ b/tests/fields/__init__.py @@ -1,3 +1,3 @@ -from fields import * -from file_tests import * -from geo import * +from .fields import * +from .file_tests import * +from .geo import * diff --git a/tests/queryset/__init__.py b/tests/queryset/__init__.py index c36b2684..31016966 100644 --- a/tests/queryset/__init__.py +++ b/tests/queryset/__init__.py @@ -1,6 +1,6 @@ -from transform import * -from field_list import * -from queryset import * -from visitor import * -from geo import * -from modify import * \ No newline at end of file +from .transform import * +from .field_list import * +from .queryset import * +from .visitor import * +from .geo import * +from .modify import *