diff --git a/.install_mongodb_on_travis.sh b/.install_mongodb_on_travis.sh index 6ac2e364..f1073333 100644 --- a/.install_mongodb_on_travis.sh +++ b/.install_mongodb_on_travis.sh @@ -25,8 +25,14 @@ elif [ "$MONGODB" = "3.4" ]; then sudo apt-get update sudo apt-get install mongodb-org-server=3.4.17 # service should be started automatically +elif [ "$MONGODB" = "3.6" ]; then + sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 + echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list + sudo apt-get update + sudo apt-get install mongodb-org-server=3.6.12 + # service should be started automatically else - echo "Invalid MongoDB version, expected 2.6, 3.0, 3.2 or 3.4." + echo "Invalid MongoDB version, expected 2.6, 3.0, 3.2, 3.4 or 3.6." exit 1 fi; diff --git a/.travis.yml b/.travis.yml index b943024a..909183c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,6 +35,8 @@ matrix: env: MONGODB=3.2 PYMONGO=3.x - python: 3.6 env: MONGODB=3.4 PYMONGO=3.x + - python: 3.6 + env: MONGODB=3.6 PYMONGO=3.x before_install: - bash .install_mongodb_on_travis.sh diff --git a/README.rst b/README.rst index 12d9df0e..cb279d2c 100644 --- a/README.rst +++ b/README.rst @@ -26,10 +26,10 @@ an `API reference `_. Supported MongoDB Versions ========================== -MongoEngine is currently tested against MongoDB v2.6, v3.0, v3.2 and v3.4. Future +MongoEngine is currently tested against MongoDB v2.6, v3.0, v3.2, v3.4 and v3.6. 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.4+. +problems with MongoDB v3.6+. Installation ============ diff --git a/docs/changelog.rst b/docs/changelog.rst index b875ffcd..6ac8da93 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,7 @@ Changelog Development =========== +- Add support for MongoDB 3.6 and Python3.7 in travis - Fix querying on List(EmbeddedDocument) subclasses fields #1961 #1492 - Fix querying on (Generic)EmbeddedDocument subclasses fields #475 - expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all` diff --git a/mongoengine/base/metaclasses.py b/mongoengine/base/metaclasses.py index a1970825..6f507eaa 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -184,9 +184,6 @@ class DocumentMetaclass(type): if issubclass(new_class, EmbeddedDocument): raise InvalidDocumentError('CachedReferenceFields is not ' 'allowed in EmbeddedDocuments') - if not f.document_type: - raise InvalidDocumentError( - 'Document is not available to sync') if f.auto_sync: f.start_listener() diff --git a/mongoengine/common.py b/mongoengine/common.py index bde7e78c..bcdea194 100644 --- a/mongoengine/common.py +++ b/mongoengine/common.py @@ -31,7 +31,6 @@ def _import_class(cls_name): field_classes = _field_list_cache - queryset_classes = ('OperationError',) deref_classes = ('DeReference',) if cls_name == 'BaseDocument': @@ -43,14 +42,11 @@ def _import_class(cls_name): elif cls_name in field_classes: from mongoengine import fields as module import_classes = field_classes - elif cls_name in queryset_classes: - from mongoengine import queryset as module - import_classes = queryset_classes elif cls_name in deref_classes: from mongoengine import dereference as module import_classes = deref_classes else: - raise ValueError('No import set for: ' % cls_name) + raise ValueError('No import set for: %s' % cls_name) for cls in import_classes: _class_registry_cache[cls] = getattr(module, cls) diff --git a/mongoengine/errors.py b/mongoengine/errors.py index 0e92a8c4..b0009cbc 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -110,9 +110,6 @@ class ValidationError(AssertionError): def build_dict(source): errors_dict = {} - if not source: - return errors_dict - if isinstance(source, dict): for field_name, error in iteritems(source): errors_dict[field_name] = build_dict(error) diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 7e119721..1cd6be11 100644 --- a/mongoengine/fields.py +++ b/mongoengine/fields.py @@ -152,12 +152,10 @@ class URLField(StringField): scheme = value.split('://')[0].lower() if scheme not in self.schemes: self.error(u'Invalid scheme {} in URL: {}'.format(scheme, value)) - return # Then check full URL if not self.url_regex.match(value): self.error(u'Invalid URL: {}'.format(value)) - return class EmailField(StringField): @@ -259,10 +257,10 @@ class EmailField(StringField): try: domain_part = domain_part.encode('idna').decode('ascii') except UnicodeError: - self.error(self.error_msg % value) + self.error("%s %s" % (self.error_msg % value, "(domain failed IDN encoding)")) else: if not self.validate_domain_part(domain_part): - self.error(self.error_msg % value) + self.error("%s %s" % (self.error_msg % value, "(domain validation failed)")) class IntField(BaseField): diff --git a/mongoengine/mongodb_support.py b/mongoengine/mongodb_support.py index 717a3d81..8e414075 100644 --- a/mongoengine/mongodb_support.py +++ b/mongoengine/mongodb_support.py @@ -6,6 +6,7 @@ from mongoengine.connection import get_connection # Constant that can be used to compare the version retrieved with # get_mongodb_version() +MONGODB_36 = (3, 6) MONGODB_34 = (3, 4) MONGODB_32 = (3, 2) MONGODB_3 = (3, 0) diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index bfbfbbe0..c6244825 100644 --- a/mongoengine/queryset/base.py +++ b/mongoengine/queryset/base.py @@ -194,7 +194,7 @@ class BaseQuerySet(object): only_fields=self.only_fields ) - raise AttributeError('Provide a slice or an integer index') + raise TypeError('Provide a slice or an integer index') def __iter__(self): raise NotImplementedError diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index 48c5f682..128a4e44 100644 --- a/mongoengine/queryset/transform.py +++ b/mongoengine/queryset/transform.py @@ -86,18 +86,10 @@ def query(_doc_cls=None, **kwargs): singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not'] singular_ops += STRING_OPERATORS if op in singular_ops: - if isinstance(field, six.string_types): - if (op in STRING_OPERATORS and - isinstance(value, six.string_types)): - StringField = _import_class('StringField') - value = StringField.prepare_query_value(op, value) - else: - value = field - else: - value = field.prepare_query_value(op, value) + value = field.prepare_query_value(op, value) - if isinstance(field, CachedReferenceField) and value: - value = value['_id'] + if isinstance(field, CachedReferenceField) and value: + 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. @@ -277,7 +269,7 @@ def update(_doc_cls=None, **update): if op == 'pull': if field.required or value is not None: - if match == 'in' and not isinstance(value, dict): + if match in ('in', 'nin') and not isinstance(value, dict): value = _prepare_query_for_iterable(field, op, value) else: value = field.prepare_query_value(op, value) @@ -304,10 +296,6 @@ def update(_doc_cls=None, **update): key = '.'.join(parts) - if not op: - raise InvalidQueryError('Updates must supply an operation ' - 'eg: set__FIELD=value') - if 'pull' in op and '.' in key: # Dot operators don't work on pull operations # unless they point to a list field diff --git a/tests/document/indexes.py b/tests/document/indexes.py index be344d32..b0b78923 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -593,8 +593,9 @@ class IndexesTest(unittest.TestCase): # Two posts with the same slug is not allowed post2 = BlogPost(title='test2', slug='test') self.assertRaises(NotUniqueError, post2.save) + self.assertRaises(NotUniqueError, BlogPost.objects.insert, post2) - # Ensure backwards compatibilty for errors + # Ensure backwards compatibility for errors self.assertRaises(OperationError, post2.save) @requires_mongodb_gte_34 @@ -826,6 +827,18 @@ class IndexesTest(unittest.TestCase): self.assertEqual(3600, info['created_1']['expireAfterSeconds']) + def test_index_drop_dups_silently_ignored(self): + class Customer(Document): + cust_id = IntField(unique=True, required=True) + meta = { + 'indexes': ['cust_id'], + 'index_drop_dups': True, + 'allow_inheritance': False, + } + + Customer.drop_collection() + Customer.objects.first() + def test_unique_and_indexes(self): """Ensure that 'unique' constraints aren't overridden by meta.indexes. @@ -842,11 +855,16 @@ class IndexesTest(unittest.TestCase): cust.save() cust_dupe = Customer(cust_id=1) - try: + with self.assertRaises(NotUniqueError): cust_dupe.save() - raise AssertionError("We saved a dupe!") - except NotUniqueError: - pass + + cust = Customer(cust_id=2) + cust.save() + + # duplicate key on update + with self.assertRaises(NotUniqueError): + cust.cust_id = 1 + cust.save() def test_primary_save_duplicate_update_existing_object(self): """If you set a field as primary, then unexpected behaviour can occur. diff --git a/tests/document/instance.py b/tests/document/instance.py index 9b28f1b4..e1379a5d 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -420,6 +420,12 @@ class InstanceTest(MongoDBTestCase): person.save() person.to_dbref() + def test_key_like_attribute_access(self): + person = self.Person(age=30) + self.assertEqual(person['age'], 30) + with self.assertRaises(KeyError): + person['unknown_attr'] + def test_save_abstract_document(self): """Saving an abstract document should fail.""" class Doc(Document): diff --git a/tests/fields/geo.py b/tests/fields/geo.py index 754f4203..37ed97f5 100644 --- a/tests/fields/geo.py +++ b/tests/fields/geo.py @@ -40,6 +40,11 @@ class GeoFieldTest(unittest.TestCase): expected = "Both values (%s) in point must be float or int" % repr(coord) self._test_for_expected_error(Location, coord, expected) + invalid_coords = [21, 4, 'a'] + for coord in invalid_coords: + expected = "GeoPointField can only accept tuples or lists of (x, y)" + self._test_for_expected_error(Location, coord, expected) + def test_point_validation(self): class Location(Document): loc = PointField() diff --git a/tests/fields/test_cached_reference_field.py b/tests/fields/test_cached_reference_field.py index 989cea6d..470ecc5d 100644 --- a/tests/fields/test_cached_reference_field.py +++ b/tests/fields/test_cached_reference_field.py @@ -208,10 +208,7 @@ class TestCachedReferenceField(MongoDBTestCase): ('pj', "PJ") ) name = StringField() - tp = StringField( - choices=TYPES - ) - + tp = StringField(choices=TYPES) father = CachedReferenceField('self', fields=('tp',)) Person.drop_collection() @@ -222,6 +219,9 @@ class TestCachedReferenceField(MongoDBTestCase): a2 = Person(name='Wilson Junior', tp='pf', father=a1) a2.save() + a2 = Person.objects.with_id(a2.id) + self.assertEqual(a2.father.tp, a1.tp) + self.assertEqual(dict(a2.to_mongo()), { "_id": a2.pk, "name": u"Wilson Junior", @@ -374,6 +374,9 @@ class TestCachedReferenceField(MongoDBTestCase): self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy') self.assertEqual(o.to_mongo()['animal']['owner']['t'], 'u') + # Check to_mongo with fields + self.assertNotIn('animal', o.to_mongo(fields=['person'])) + # counts Ocorrence(person="teste 2").save() Ocorrence(person="teste 3").save() diff --git a/tests/fields/test_datetime_field.py b/tests/fields/test_datetime_field.py index c6253043..5af6a011 100644 --- a/tests/fields/test_datetime_field.py +++ b/tests/fields/test_datetime_field.py @@ -172,6 +172,9 @@ class TestDateTimeField(MongoDBTestCase): log.time = datetime.datetime.now().isoformat(' ') log.validate() + log.time = '2019-05-16 21:42:57.897847' + log.validate() + if dateutil: log.time = datetime.datetime.now().isoformat('T') log.validate() @@ -180,6 +183,12 @@ class TestDateTimeField(MongoDBTestCase): self.assertRaises(ValidationError, log.validate) log.time = 'ABC' self.assertRaises(ValidationError, log.validate) + log.time = '2019-05-16 21:GARBAGE:12' + self.assertRaises(ValidationError, log.validate) + log.time = '2019-05-16 21:42:57.GARBAGE' + self.assertRaises(ValidationError, log.validate) + log.time = '2019-05-16 21:42:57.123.456' + self.assertRaises(ValidationError, log.validate) class TestDateTimeTzAware(MongoDBTestCase): diff --git a/tests/fields/test_email_field.py b/tests/fields/test_email_field.py index d8410354..3ce49d62 100644 --- a/tests/fields/test_email_field.py +++ b/tests/fields/test_email_field.py @@ -75,6 +75,16 @@ class TestEmailField(MongoDBTestCase): user = User(email='me@localhost') user.validate() + def test_email_domain_validation_fails_if_invalid_idn(self): + class User(Document): + email = EmailField() + + invalid_idn = '.google.com' + user = User(email='me@%s' % invalid_idn) + with self.assertRaises(ValidationError) as ctx_err: + user.validate() + self.assertIn("domain failed IDN encoding", str(ctx_err.exception)) + def test_email_field_ip_domain(self): class User(Document): email = EmailField() diff --git a/tests/fields/test_lazy_reference_field.py b/tests/fields/test_lazy_reference_field.py index a72e8cbe..b10506e7 100644 --- a/tests/fields/test_lazy_reference_field.py +++ b/tests/fields/test_lazy_reference_field.py @@ -13,6 +13,35 @@ class TestLazyReferenceField(MongoDBTestCase): # with a document class name. self.assertRaises(ValidationError, LazyReferenceField, EmbeddedDocument) + def test___repr__(self): + class Animal(Document): + pass + + class Ocurrence(Document): + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal() + oc = Ocurrence(animal=animal) + self.assertIn('LazyReference', repr(oc.animal)) + + def test___getattr___unknown_attr_raises_attribute_error(self): + class Animal(Document): + pass + + class Ocurrence(Document): + animal = LazyReferenceField(Animal) + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal().save() + oc = Ocurrence(animal=animal) + with self.assertRaises(AttributeError): + oc.animal.not_exist + def test_lazy_reference_simple(self): class Animal(Document): name = StringField() @@ -479,6 +508,23 @@ class TestGenericLazyReferenceField(MongoDBTestCase): p = Ocurrence.objects.get() self.assertIs(p.animal, None) + def test_generic_lazy_reference_accepts_string_instead_of_class(self): + class Animal(Document): + name = StringField() + tag = StringField() + + class Ocurrence(Document): + person = StringField() + animal = GenericLazyReferenceField('Animal') + + Animal.drop_collection() + Ocurrence.drop_collection() + + animal = Animal().save() + Ocurrence(animal=animal).save() + p = Ocurrence.objects.get() + self.assertEqual(p.animal, animal) + def test_generic_lazy_reference_embedded(self): class Animal(Document): name = StringField() diff --git a/tests/fields/test_long_field.py b/tests/fields/test_long_field.py index 4ab7403d..3f307809 100644 --- a/tests/fields/test_long_field.py +++ b/tests/fields/test_long_field.py @@ -39,9 +39,9 @@ class TestLongField(MongoDBTestCase): doc.value = -1 self.assertRaises(ValidationError, doc.validate) - doc.age = 120 + doc.value = 120 self.assertRaises(ValidationError, doc.validate) - doc.age = 'ten' + doc.value = 'ten' self.assertRaises(ValidationError, doc.validate) def test_long_ne_operator(self): diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index 039834f7..04042350 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -6,7 +6,6 @@ import uuid from decimal import Decimal from bson import DBRef, ObjectId -from nose.plugins.skip import SkipTest import pymongo from pymongo.errors import ConfigurationError from pymongo.read_preferences import ReadPreference @@ -18,7 +17,7 @@ from mongoengine import * from mongoengine.connection import get_connection, get_db from mongoengine.context_managers import query_counter, switch_db from mongoengine.errors import InvalidQueryError -from mongoengine.mongodb_support import get_mongodb_version, MONGODB_32 +from mongoengine.mongodb_support import get_mongodb_version, MONGODB_32, MONGODB_36 from mongoengine.queryset import (DoesNotExist, MultipleObjectsReturned, QuerySet, QuerySetManager, queryset_manager) from tests.utils import requires_mongodb_gte_26 @@ -32,6 +31,12 @@ class db_ops_tracker(query_counter): return list(self.db.system.profile.find(ignore_query)) +def get_key_compat(mongo_ver): + ORDER_BY_KEY = 'sort' if mongo_ver >= MONGODB_32 else '$orderby' + CMD_QUERY_KEY = 'command' if mongo_ver >= MONGODB_36 else 'query' + return ORDER_BY_KEY, CMD_QUERY_KEY + + class QuerySetTest(unittest.TestCase): def setUp(self): @@ -157,6 +162,11 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(person.name, 'User B') self.assertEqual(person.age, None) + def test___getitem___invalid_index(self): + """Ensure slicing a queryset works as expected.""" + with self.assertRaises(TypeError): + self.Person.objects()['a'] + def test_slice(self): """Ensure slicing a queryset works as expected.""" user_a = self.Person.objects.create(name='User A', age=20) @@ -985,6 +995,29 @@ class QuerySetTest(unittest.TestCase): inserted_comment_id = Comment.objects.insert(comment, load_bulk=False) self.assertEqual(comment.id, inserted_comment_id) + def test_bulk_insert_accepts_doc_with_ids(self): + class Comment(Document): + id = IntField(primary_key=True) + + Comment.drop_collection() + + com1 = Comment(id=0) + com2 = Comment(id=1) + Comment.objects.insert([com1, com2]) + + def test_insert_raise_if_duplicate_in_constraint(self): + class Comment(Document): + id = IntField(primary_key=True) + + Comment.drop_collection() + + com1 = Comment(id=0) + + Comment.objects.insert(com1) + + with self.assertRaises(NotUniqueError): + Comment.objects.insert(com1) + 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. @@ -1280,8 +1313,7 @@ class QuerySetTest(unittest.TestCase): """Ensure that the default ordering can be cleared by calling order_by() w/o any arguments. """ - MONGO_VER = self.mongodb_version - ORDER_BY_KEY = 'sort' if MONGO_VER >= MONGODB_32 else '$orderby' + ORDER_BY_KEY, CMD_QUERY_KEY = get_key_compat(self.mongodb_version) class BlogPost(Document): title = StringField() @@ -1298,7 +1330,7 @@ class QuerySetTest(unittest.TestCase): BlogPost.objects.filter(title='whatever').first() self.assertEqual(len(q.get_ops()), 1) self.assertEqual( - q.get_ops()[0]['query'][ORDER_BY_KEY], + q.get_ops()[0][CMD_QUERY_KEY][ORDER_BY_KEY], {'published_date': -1} ) @@ -1306,14 +1338,14 @@ 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.assertNotIn(ORDER_BY_KEY, q.get_ops()[0]['query']) + self.assertNotIn(ORDER_BY_KEY, q.get_ops()[0][CMD_QUERY_KEY]) # 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'][ORDER_BY_KEY], + q.get_ops()[0][CMD_QUERY_KEY][ORDER_BY_KEY], {'published_date': 1} ) @@ -1322,13 +1354,12 @@ 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.assertNotIn(ORDER_BY_KEY, q.get_ops()[0]['query']) + self.assertNotIn(ORDER_BY_KEY, q.get_ops()[0][CMD_QUERY_KEY]) def test_no_ordering_for_get(self): """ Ensure that Doc.objects.get doesn't use any ordering. """ - MONGO_VER = self.mongodb_version - ORDER_BY_KEY = 'sort' if MONGO_VER == MONGODB_32 else '$orderby' + ORDER_BY_KEY, CMD_QUERY_KEY = get_key_compat(self.mongodb_version) class BlogPost(Document): title = StringField() @@ -1344,13 +1375,13 @@ class QuerySetTest(unittest.TestCase): with db_ops_tracker() as q: BlogPost.objects.get(title='whatever') self.assertEqual(len(q.get_ops()), 1) - self.assertNotIn(ORDER_BY_KEY, q.get_ops()[0]['query']) + self.assertNotIn(ORDER_BY_KEY, q.get_ops()[0][CMD_QUERY_KEY]) # 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.assertNotIn(ORDER_BY_KEY, q.get_ops()[0]['query']) + self.assertNotIn(ORDER_BY_KEY, q.get_ops()[0][CMD_QUERY_KEY]) def test_find_embedded(self): """Ensure that an embedded document is properly returned from @@ -2150,6 +2181,40 @@ class QuerySetTest(unittest.TestCase): Site.objects(id=s.id).update_one( pull_all__collaborators__helpful__name=['Ross']) + def test_pull_from_nested_embedded_using_in_nin(self): + """Ensure that the 'pull' update operation works on embedded documents using 'in' and 'nin' operators. + """ + + class User(EmbeddedDocument): + name = StringField() + + def __unicode__(self): + return '%s' % self.name + + class Collaborator(EmbeddedDocument): + helpful = ListField(EmbeddedDocumentField(User)) + unhelpful = ListField(EmbeddedDocumentField(User)) + + class Site(Document): + name = StringField(max_length=75, unique=True, required=True) + collaborators = EmbeddedDocumentField(Collaborator) + + Site.drop_collection() + + a = User(name='Esteban') + b = User(name='Frank') + x = User(name='Harry') + y = User(name='John') + + s = Site(name="test", collaborators=Collaborator( + helpful=[a, b], unhelpful=[x, y])).save() + + Site.objects(id=s.id).update_one(pull__collaborators__helpful__name__in=['Esteban']) # Pull a + self.assertEqual(Site.objects.first().collaborators['helpful'], [b]) + + Site.objects(id=s.id).update_one(pull__collaborators__unhelpful__name__nin=['John']) # Pull x + self.assertEqual(Site.objects.first().collaborators['unhelpful'], [y]) + def test_pull_from_nested_mapfield(self): class Collaborator(EmbeddedDocument): @@ -2489,6 +2554,7 @@ class QuerySetTest(unittest.TestCase): def test_comment(self): """Make sure adding a comment to the query gets added to the query""" MONGO_VER = self.mongodb_version + _, CMD_QUERY_KEY = get_key_compat(MONGO_VER) QUERY_KEY = 'filter' if MONGO_VER >= MONGODB_32 else '$query' COMMENT_KEY = 'comment' if MONGO_VER >= MONGODB_32 else '$comment' @@ -2507,8 +2573,8 @@ class QuerySetTest(unittest.TestCase): ops = q.get_ops() self.assertEqual(len(ops), 2) for op in ops: - self.assertEqual(op['query'][QUERY_KEY], {'age': {'$gte': 18}}) - self.assertEqual(op['query'][COMMENT_KEY], 'looking for an adult') + self.assertEqual(op[CMD_QUERY_KEY][QUERY_KEY], {'age': {'$gte': 18}}) + self.assertEqual(op[CMD_QUERY_KEY][COMMENT_KEY], 'looking for an adult') def test_map_reduce(self): """Ensure map/reduce is both mapping and reducing. @@ -3524,6 +3590,11 @@ class QuerySetTest(unittest.TestCase): opts = {"deleted": False} return qryset(**opts) + @queryset_manager + def objects_1_arg(qryset): + opts = {"deleted": False} + return qryset(**opts) + @queryset_manager def music_posts(doc_cls, queryset, deleted=False): return queryset(tags='music', @@ -3538,6 +3609,8 @@ class QuerySetTest(unittest.TestCase): self.assertEqual([p.id for p in BlogPost.objects()], [post1.id, post2.id, post3.id]) + self.assertEqual([p.id for p in BlogPost.objects_1_arg()], + [post1.id, post2.id, post3.id]) self.assertEqual([p.id for p in BlogPost.music_posts()], [post1.id, post2.id]) @@ -4914,6 +4987,38 @@ class QuerySetTest(unittest.TestCase): people.count() self.assertEqual(q, 3) + def test_no_cached_queryset__repr__(self): + class Person(Document): + name = StringField() + + Person.drop_collection() + qs = Person.objects.no_cache() + self.assertEqual(repr(qs), '[]') + + def test_no_cached_on_a_cached_queryset_raise_error(self): + class Person(Document): + name = StringField() + + Person.drop_collection() + Person(name='a').save() + qs = Person.objects() + _ = list(qs) + with self.assertRaises(OperationError) as ctx_err: + qs.no_cache() + self.assertEqual("QuerySet already cached", str(ctx_err.exception)) + + def test_no_cached_queryset_no_cache_back_to_cache(self): + class Person(Document): + name = StringField() + + Person.drop_collection() + qs = Person.objects() + self.assertIsInstance(qs, QuerySet) + qs = qs.no_cache() + self.assertIsInstance(qs, QuerySetNoCache) + qs = qs.cache() + self.assertIsInstance(qs, QuerySet) + def test_cache_not_cloned(self): class User(Document): @@ -5186,8 +5291,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(op['nreturned'], 1) def test_bool_with_ordering(self): - MONGO_VER = self.mongodb_version - ORDER_BY_KEY = 'sort' if MONGO_VER >= MONGODB_32 else '$orderby' + ORDER_BY_KEY, CMD_QUERY_KEY = get_key_compat(self.mongodb_version) class Person(Document): name = StringField() @@ -5206,21 +5310,22 @@ class QuerySetTest(unittest.TestCase): op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertNotIn(ORDER_BY_KEY, op['query']) + self.assertNotIn(ORDER_BY_KEY, op[CMD_QUERY_KEY]) # Check that normal query uses orderby qs2 = Person.objects.order_by('name') - with query_counter() as p: + with query_counter() as q: for x in qs2: pass - op = p.db.system.profile.find({"ns": + op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertIn(ORDER_BY_KEY, op['query']) + self.assertIn(ORDER_BY_KEY, op[CMD_QUERY_KEY]) def test_bool_with_ordering_from_meta_dict(self): + ORDER_BY_KEY, CMD_QUERY_KEY = get_key_compat(self.mongodb_version) class Person(Document): name = StringField() @@ -5242,7 +5347,7 @@ class QuerySetTest(unittest.TestCase): op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertNotIn('$orderby', op['query'], + self.assertNotIn('$orderby', op[CMD_QUERY_KEY], '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 8064f09c..2c2d018c 100644 --- a/tests/queryset/transform.py +++ b/tests/queryset/transform.py @@ -71,6 +71,14 @@ class TransformTest(unittest.TestCase): update = transform.update(BlogPost, push_all__tags=['mongo', 'db']) self.assertEqual(update, {'$push': {'tags': {'$each': ['mongo', 'db']}}}) + def test_transform_update_no_operator_default_to_set(self): + """Ensure the differences in behvaior between 'push' and 'push_all'""" + class BlogPost(Document): + tags = ListField(StringField()) + + update = transform.update(BlogPost, tags=['mongo', 'db']) + self.assertEqual(update, {'$set': {'tags': ['mongo', 'db']}}) + def test_query_field_name(self): """Ensure that the correct field name is used when querying. """ @@ -283,6 +291,11 @@ class TransformTest(unittest.TestCase): update = transform.update(MainDoc, pull__content__heading='xyz') self.assertEqual(update, {'$pull': {'content.heading': 'xyz'}}) + update = transform.update(MainDoc, pull__content__text__word__in=['foo', 'bar']) + self.assertEqual(update, {'$pull': {'content.text': {'word': {'$in': ['foo', 'bar']}}}}) + + update = transform.update(MainDoc, pull__content__text__word__nin=['foo', 'bar']) + self.assertEqual(update, {'$pull': {'content.text': {'word': {'$nin': ['foo', 'bar']}}}}) if __name__ == '__main__': unittest.main() diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 00000000..04ad5b34 --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,15 @@ +import unittest + +from mongoengine.common import _import_class +from mongoengine import Document + + +class TestCommon(unittest.TestCase): + + def test__import_class(self): + doc_cls = _import_class("Document") + self.assertIs(doc_cls, Document) + + def test__import_class_raise_if_not_known(self): + with self.assertRaises(ValueError): + _import_class("UnknownClass") diff --git a/tests/test_context_managers.py b/tests/test_context_managers.py index 22c33b01..529032fe 100644 --- a/tests/test_context_managers.py +++ b/tests/test_context_managers.py @@ -270,6 +270,14 @@ class ContextManagersTest(unittest.TestCase): counter += 1 self.assertEqual(q, counter) + self.assertEqual(int(q), counter) # test __int__ + self.assertEqual(repr(q), str(int(q))) # test __repr__ + self.assertGreater(q, -1) # test __gt__ + self.assertGreaterEqual(q, int(q)) # test __gte__ + self.assertNotEqual(q, -1) + self.assertLess(q, 1000) + self.assertLessEqual(q, int(q)) + def test_query_counter_counts_getmore_queries(self): connect('mongoenginetest') db = get_db() diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 4fb21d21..a9ef98e7 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,4 +1,5 @@ import unittest +from six import iterkeys from mongoengine import Document from mongoengine.base.datastructures import StrictDict, BaseList, BaseDict @@ -368,6 +369,20 @@ class TestStrictDict(unittest.TestCase): d = self.dtype(a=1, b=1, c=1) self.assertEqual((d.a, d.b, d.c), (1, 1, 1)) + def test_iterkeys(self): + d = self.dtype(a=1) + self.assertEqual(list(iterkeys(d)), ['a']) + + def test_len(self): + d = self.dtype(a=1) + self.assertEqual(len(d), 1) + + def test_pop(self): + d = self.dtype(a=1) + self.assertIn('a', d) + d.pop('a') + self.assertNotIn('a', d) + def test_repr(self): d = self.dtype(a=1, b=2, c=3) self.assertEqual(repr(d), '{"a": 1, "b": 2, "c": 3}')