diff --git a/.install_mongodb_on_travis.sh b/.install_mongodb_on_travis.sh index 8563ae74..057ccf74 100644 --- a/.install_mongodb_on_travis.sh +++ b/.install_mongodb_on_travis.sh @@ -1,23 +1,28 @@ #!/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 - 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 +if [ "$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 + 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 +elif [ "$MONGODB" = "3.2" ]; then + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv EA312927 + echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list + sudo apt-get update + sudo apt-get install mongodb-org-server=3.2.20 + # service should be started automatically else - echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0." + echo "Invalid MongoDB version, expected 2.6, 3.0, or 3.2" exit 1 fi; + +mkdir db +1>db/logs mongod --dbpath=db & diff --git a/.travis.yml b/.travis.yml index 8f846ab1..4f77f4e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,10 @@ # 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. -# +# v3.5, v3.6, PyPy, and PyMongo v3.x. +# * MongoDB v3.0 & v3.2 are tested against Python v2.7, v3.5 & v3.6 +# and Pymongo v3.5 & v3.x # Reminder: Update README.rst if you change MongoDB versions we test. language: python @@ -15,12 +13,11 @@ language: python python: - 2.7 - 3.5 +- 3.6 - pypy 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 @@ -28,20 +25,22 @@ matrix: include: - python: 2.7 - env: MONGODB=2.4 PYMONGO=2.7 + env: MONGODB=3.0 PYMONGO=3.5 - python: 2.7 - env: MONGODB=2.4 PYMONGO=3.0 - - python: 2.7 - env: MONGODB=3.0 PYMONGO=3.0 + env: MONGODB=3.2 PYMONGO=3.x - python: 3.5 - env: MONGODB=2.4 PYMONGO=2.7 + env: MONGODB=3.0 PYMONGO=3.5 - python: 3.5 - env: MONGODB=2.4 PYMONGO=3.0 - - python: 3.5 - env: MONGODB=3.0 PYMONGO=3.0 + env: MONGODB=3.2 PYMONGO=3.x + - python: 3.6 + env: MONGODB=3.0 PYMONGO=3.5 + - python: 3.6 + env: MONGODB=3.2 PYMONGO=3.x 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 @@ -90,11 +89,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 dev PyMongo (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 diff --git a/AUTHORS b/AUTHORS index 1d724718..880dfad1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -243,3 +243,9 @@ that much better: * Victor Varvaryuk * Stanislav Kaledin (https://github.com/sallyruthstruik) * Dmitry Yantsen (https://github.com/mrTable) + * Renjianxin (https://github.com/Davidrjx) + * Erdenezul Batmunkh (https://github.com/erdenezul) + * Andy Yankovsky (https://github.com/werat) + * Bastien Gérard (https://github.com/bagerard) + * Trevor Hall (https://github.com/tjhall13) + * Gleb Voropaev (https://github.com/buggyspace) \ No newline at end of file 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 ----------- diff --git a/README.rst b/README.rst index e1e2aef6..4e186a85 100644 --- a/README.rst +++ b/README.rst @@ -26,19 +26,21 @@ an `API reference `_. Supported MongoDB Versions ========================== -MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future +MongoEngine is currently tested against MongoDB v2.6, v3.0 and v3.2. 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+. +problems with MongoDB v3.4+. 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 -source from `GitHub `_ and run ``python -setup.py install``. +and thus you can use ``easy_install -U mongoengine``. Another option is +`pipenv `_. You can then use ``pipenv install mongoengine`` +to both create the virtual environment and install the package. Otherwise, you can +download the source from `GitHub `_ and +run ``python setup.py install``. Dependencies ============ 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/changelog.rst b/docs/changelog.rst index c60bbf09..dbd328d8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,90 @@ Changelog Development =========== - (Fill this out as you fix issues and develop your features). +- Fix .only() working improperly after using .count() of the same instance of QuerySet +- POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (_cls, _id) when using `QuerySet.as_pymongo` #1976 +- Fix InvalidStringData error when using modify on a BinaryField #1127 +- DEPRECATION: `EmbeddedDocument.save` & `.reload` are marked as deprecated and will be removed in a next version of mongoengine #1552 + +================= +Changes in 0.16.3 +================= +- Fix $push with $position operator not working with lists in embedded document #1965 + +================= +Changes in 0.16.2 +================= +- Fix .save() that fails when called with write_concern=None (regression of 0.16.1) #1958 + +================= +Changes in 0.16.1 +================= +- Fix `_cls` that is not set properly in Document constructor (regression) #1950 +- Fix bug in _delta method - Update of a ListField depends on an unrelated dynamic field update #1733 +- Remove deprecated `save()` method and used `insert_one()` #1899 + +================= +Changes in 0.16.0 +================= +- Various improvements to the doc +- Improvement to code quality +- POTENTIAL BREAKING CHANGES: + - EmbeddedDocumentField will no longer accept references to Document classes in its constructor #1661 + - Get rid of the `basecls` parameter from the DictField constructor (dead code) #1876 + - default value of ComplexDateTime is now None (and no longer the current datetime) #1368 +- Fix unhashable TypeError when referencing a Document with a compound key in an EmbeddedDocument #1685 +- Fix bug where an EmbeddedDocument with the same id as its parent would not be tracked for changes #1768 +- Fix the fact that bulk `insert()` was not setting primary keys of inserted documents instances #1919 +- Fix bug when referencing the abstract class in a ReferenceField #1920 +- Allow modification to the document made in pre_save_post_validation to be taken into account #1202 +- Replaced MongoDB 2.4 tests in CI by MongoDB 3.2 #1903 +- Fix side effects of using queryset.`no_dereference` on other documents #1677 +- Fix TypeError when using lazy django translation objects as translated choices #1879 +- Improve 2-3 codebase compatibility #1889 +- Fix the support for changing the default value of ComplexDateTime #1368 +- Improves error message in case an EmbeddedDocumentListField receives an EmbeddedDocument instance + instead of a list #1877 +- Fix the Decimal operator inc/dec #1517 #1320 +- Ignore killcursors queries in `query_counter` context manager #1869 +- Fix the fact that `query_counter` was modifying the initial profiling_level in case it was != 0 #1870 +- Repaired the `no_sub_classes` context manager + fix the fact that it was swallowing exceptions #1865 +- Fix index creation error that was swallowed by hasattr under python2 #1688 +- QuerySet limit function behaviour: Passing 0 as parameter will return all the documents in the cursor #1611 +- bulk insert updates the ids of the input documents instances #1919 +- Fix an harmless bug related to GenericReferenceField where modifications in the generic-referenced document + were tracked in the parent #1934 +- Improve validator of BinaryField #273 +- Implemented lazy regex compiling in Field classes to improve 'import mongoengine' performance #1806 +- Updated GridFSProxy.__str__ so that it would always print both the filename and grid_id #710 +- Add __repr__ to Q and QCombination #1843 +- fix bug in BaseList.__iter__ operator (was occuring when modifying a BaseList while iterating over it) #1676 +- Added field `DateField`#513 + +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 +- 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 +- 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 +================= +- Add LazyReferenceField and GenericLazyReferenceField to address #1230 + +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/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/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 diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst index d41ae7e6..911de36d 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. @@ -80,13 +80,16 @@ 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` * :class:`~mongoengine.fields.ListField` +* :class:`~mongoengine.fields.LongField` * :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` @@ -153,7 +156,7 @@ arguments can be set on all fields: 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 + Can either be nested tuples of value (stored in mongo) and a human readable key :: SIZE = (('S', 'Small'), @@ -224,7 +227,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() @@ -490,7 +493,9 @@ the field name with a **#**:: ] } -If a dictionary is passed then the following options are available: +If a dictionary is passed then additional options become available. Valid options include, +but are not limited to: + :attr:`fields` (Default: None) The fields to index. Specified in the same format as described above. @@ -511,8 +516,15 @@ 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 + +:attr:`collation` (Optional) + Allows to create case insensitive indexes (MongoDB v3.4+ only) + .. note:: + Additional options are forwarded as **kwargs to pymongo's create_index method. Inheritance adds extra fields indices see: :ref:`document-inheritance`. Global index default options @@ -524,15 +536,16 @@ 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, 'index_drop_dups': True, - 'index_cls': False } -:attr:`index_options` (Optional) - Set any default index options - see the `full options list `_ +:attr:`index_opts` (Optional) + 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 @@ -540,10 +553,15 @@ 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 - -.. note:: Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning + Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning and has no effect @@ -618,7 +636,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} @@ -725,6 +743,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 -------------------------- As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and 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) 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/docs/guide/querying.rst b/docs/guide/querying.rst index 0bb19658..08987835 100644 --- a/docs/guide/querying.rst +++ b/docs/guide/querying.rst @@ -456,14 +456,14 @@ data. To turn off dereferencing of the results of a query use :func:`~mongoengine.queryset.QuerySet.no_dereference` on the queryset like so:: post = Post.objects.no_dereference().first() - assert(isinstance(post.author, ObjectId)) + assert(isinstance(post.author, DBRef)) You can also turn off all dereferencing for a fixed period by using the :class:`~mongoengine.context_managers.no_dereference` context manager:: with no_dereference(Post) as Post: post = Post.objects.first() - assert(isinstance(post.author, ObjectId)) + assert(isinstance(post.author, DBRef)) # Outside the context manager dereferencing occurs. assert(isinstance(post.author, User)) @@ -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 diff --git a/docs/guide/signals.rst b/docs/guide/signals.rst index 30277966..06bccb3b 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 @@ -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:: 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') diff --git a/docs/tutorial.rst b/docs/tutorial.rst index cc5b647d..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 @@ -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:: diff --git a/docs/upgrade.rst b/docs/upgrade.rst index 65d13359..082dbadc 100644 --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -6,6 +6,11 @@ Development *********** (Fill this out whenever you introduce breaking changes to MongoEngine) +URLField's constructor no longer takes `verify_exists` + +0.15.0 +****** + 0.14.0 ****** This release includes a few bug fixes and a significant code cleanup. The most diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py index b41e87e7..2b78d4e6 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, 16, 3) def get_version(): 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/common.py b/mongoengine/base/common.py index b9971ff7..d747c8cc 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', 'unset', 'inc', 'dec', 'mul', + 'pop', 'push', 'push_all', 'pull', + 'pull_all', 'add_to_set', 'set_on_insert', + 'min', 'max', 'rename'} _document_registry = {} @@ -18,7 +19,7 @@ def get_document(name): # Possible old style name single_end = name.split('.')[-1] compound_end = '.%s' % single_end - possible_match = [k for k in _document_registry.keys() + possible_match = [k for k in _document_registry if k.endswith(compound_end) or k == single_end] if len(possible_match) == 1: doc = _document_registry.get(possible_match.pop(), None) diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py index b9aca8fa..808332b9 100644 --- a/mongoengine/base/datastructures.py +++ b/mongoengine/base/datastructures.py @@ -1,12 +1,30 @@ -import itertools import weakref +from bson import DBRef import six from mongoengine.common import _import_class from mongoengine.errors import DoesNotExist, MultipleObjectsReturned -__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList') +__all__ = ('BaseDict', 'StrictDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference') + + +def mark_as_changed_wrapper(parent_method): + """Decorators that ensures _mark_as_changed method gets called""" + def wrapper(self, *args, **kwargs): + result = parent_method(self, *args, **kwargs) # Can't use super() in the decorator + self._mark_as_changed() + return result + return wrapper + + +def mark_key_as_changed_wrapper(parent_method): + """Decorators that ensures _mark_as_changed method gets called with the key argument""" + def wrapper(self, key, *args, **kwargs): + result = parent_method(self, key, *args, **kwargs) # Can't use super() in the decorator + self._mark_as_changed(key) + return result + return wrapper class BaseDict(dict): @@ -17,46 +35,36 @@ class BaseDict(dict): _name = None def __init__(self, dict_items, instance, name): - Document = _import_class('Document') - EmbeddedDocument = _import_class('EmbeddedDocument') + BaseDocument = _import_class('BaseDocument') - if isinstance(instance, (Document, EmbeddedDocument)): + if isinstance(instance, BaseDocument): self._instance = weakref.proxy(instance) self._name = name super(BaseDict, self).__init__(dict_items) - def __getitem__(self, key, *args, **kwargs): + def get(self, key, default=None): + # get does not use __getitem__ by default so we must override it as well + try: + return self.__getitem__(key) + except KeyError: + return default + + def __getitem__(self, key): value = super(BaseDict, self).__getitem__(key) EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance - elif not isinstance(value, BaseDict) and isinstance(value, dict): + elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, None, '%s.%s' % (self._name, key)) super(BaseDict, self).__setitem__(key, value) value._instance = self._instance - elif not isinstance(value, BaseList) and isinstance(value, list): + elif isinstance(value, list) and not isinstance(value, BaseList): value = BaseList(value, None, '%s.%s' % (self._name, key)) super(BaseDict, self).__setitem__(key, value) value._instance = self._instance return value - def __setitem__(self, key, value, *args, **kwargs): - self._mark_as_changed(key) - return super(BaseDict, self).__setitem__(key, value) - - def __delete__(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).__delete__(*args, **kwargs) - - def __delitem__(self, key, *args, **kwargs): - self._mark_as_changed(key) - return super(BaseDict, self).__delitem__(key) - - def __delattr__(self, key, *args, **kwargs): - self._mark_as_changed(key) - return super(BaseDict, self).__delattr__(key) - def __getstate__(self): self.instance = None self._dereferenced = False @@ -66,25 +74,14 @@ class BaseDict(dict): self = state return self - def clear(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).clear() - - def pop(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).pop(*args, **kwargs) - - def popitem(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).popitem() - - def setdefault(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).setdefault(*args, **kwargs) - - def update(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseDict, self).update(*args, **kwargs) + __setitem__ = mark_key_as_changed_wrapper(dict.__setitem__) + __delattr__ = mark_key_as_changed_wrapper(dict.__delattr__) + __delitem__ = mark_key_as_changed_wrapper(dict.__delitem__) + pop = mark_as_changed_wrapper(dict.pop) + clear = mark_as_changed_wrapper(dict.clear) + update = mark_as_changed_wrapper(dict.update) + popitem = mark_as_changed_wrapper(dict.popitem) + setdefault = mark_as_changed_wrapper(dict.setdefault) def _mark_as_changed(self, key=None): if hasattr(self._instance, '_mark_as_changed'): @@ -102,52 +99,39 @@ class BaseList(list): _name = None def __init__(self, list_items, instance, name): - Document = _import_class('Document') - EmbeddedDocument = _import_class('EmbeddedDocument') + BaseDocument = _import_class('BaseDocument') - if isinstance(instance, (Document, EmbeddedDocument)): + if isinstance(instance, BaseDocument): self._instance = weakref.proxy(instance) self._name = name super(BaseList, self).__init__(list_items) - def __getitem__(self, key, *args, **kwargs): + def __getitem__(self, key): value = super(BaseList, self).__getitem__(key) + if isinstance(key, slice): + # When receiving a slice operator, we don't convert the structure and bind + # to parent's instance. This is buggy for now but would require more work to be handled properly + return value + EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance - elif not isinstance(value, BaseDict) and isinstance(value, dict): + elif isinstance(value, dict) and not isinstance(value, BaseDict): + # Replace dict by BaseDict value = BaseDict(value, None, '%s.%s' % (self._name, key)) super(BaseList, self).__setitem__(key, value) value._instance = self._instance - elif not isinstance(value, BaseList) and isinstance(value, list): + elif isinstance(value, list) and not isinstance(value, BaseList): + # Replace list by BaseList value = BaseList(value, None, '%s.%s' % (self._name, key)) super(BaseList, self).__setitem__(key, value) value._instance = self._instance return value def __iter__(self): - for i in xrange(self.__len__()): - yield self[i] - - def __setitem__(self, key, value, *args, **kwargs): - if isinstance(key, slice): - self._mark_as_changed() - else: - self._mark_as_changed(key) - return super(BaseList, self).__setitem__(key, value) - - def __delitem__(self, key, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).__delitem__(key) - - def __setslice__(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).__setslice__(*args, **kwargs) - - def __delslice__(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).__delslice__(*args, **kwargs) + for v in super(BaseList, self).__iter__(): + yield v def __getstate__(self): self.instance = None @@ -158,41 +142,40 @@ class BaseList(list): self = state return self - def __iadd__(self, other): - self._mark_as_changed() - return super(BaseList, self).__iadd__(other) + def __setitem__(self, key, value): + changed_key = key + if isinstance(key, slice): + # In case of slice, we don't bother to identify the exact elements being updated + # instead, we simply marks the whole list as changed + changed_key = None - def __imul__(self, other): - self._mark_as_changed() - return super(BaseList, self).__imul__(other) + result = super(BaseList, self).__setitem__(key, value) + self._mark_as_changed(changed_key) + return result - def append(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).append(*args, **kwargs) + append = mark_as_changed_wrapper(list.append) + extend = mark_as_changed_wrapper(list.extend) + insert = mark_as_changed_wrapper(list.insert) + pop = mark_as_changed_wrapper(list.pop) + remove = mark_as_changed_wrapper(list.remove) + reverse = mark_as_changed_wrapper(list.reverse) + sort = mark_as_changed_wrapper(list.sort) + __delitem__ = mark_as_changed_wrapper(list.__delitem__) + __iadd__ = mark_as_changed_wrapper(list.__iadd__) + __imul__ = mark_as_changed_wrapper(list.__imul__) - def extend(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).extend(*args, **kwargs) + if six.PY2: + # Under py3 __setslice__, __delslice__ and __getslice__ + # are replaced by __setitem__, __delitem__ and __getitem__ with a slice as parameter + # so we mimic this under python 2 + def __setslice__(self, i, j, sequence): + return self.__setitem__(slice(i, j), sequence) - def insert(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).insert(*args, **kwargs) + def __delslice__(self, i, j): + return self.__delitem__(slice(i, j)) - def pop(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).pop(*args, **kwargs) - - def remove(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).remove(*args, **kwargs) - - def reverse(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).reverse() - - def sort(self, *args, **kwargs): - self._mark_as_changed() - return super(BaseList, self).sort(*args, **kwargs) + def __getslice__(self, i, j): + return self.__getitem__(slice(i, j)) def _mark_as_changed(self, key=None): if hasattr(self._instance, '_mark_as_changed'): @@ -206,6 +189,10 @@ class BaseList(list): class EmbeddedDocumentList(BaseList): + def __init__(self, list_items, instance, name): + super(EmbeddedDocumentList, self).__init__(list_items, instance, name) + self._instance = instance + @classmethod def __match_all(cls, embedded_doc, kwargs): """Return True if a given embedded doc matches all the filter @@ -224,15 +211,14 @@ class EmbeddedDocumentList(BaseList): return embedded_docs return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)] - def __init__(self, list_items, instance, name): - super(EmbeddedDocumentList, self).__init__(list_items, instance, name) - self._instance = instance - def filter(self, **kwargs): """ 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.* @@ -350,7 +336,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 @@ -372,7 +359,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): @@ -447,40 +434,40 @@ class StrictDict(object): return cls._classes[allowed_keys] -class SemiStrictDict(StrictDict): - __slots__ = ('_extras', ) - _classes = {} +class LazyReference(DBRef): + __slots__ = ('_cached_doc', 'passthrough', 'document_type') - 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 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 - 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} + @property + def pk(self): + return self.id - 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 __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 __iter__(self): + 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: - extras_iter = iter(self.__getattribute__('_extras')) - except AttributeError: - extras_iter = () - return itertools.chain(super(SemiStrictDict, self).__iter__(), extras_iter) + return document[name] + except KeyError: + raise AttributeError() + + def __repr__(self): + return "" % (self.document_type, self.pk) diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py index 99c8af87..6a4c6bd9 100644 --- a/mongoengine/base/document.py +++ b/mongoengine/base/document.py @@ -1,11 +1,8 @@ import copy import numbers -from collections import Hashable from functools import partial -from bson import ObjectId, json_util -from bson.dbref import DBRef -from bson.son import SON +from bson import DBRef, ObjectId, SON, json_util import pymongo import six @@ -13,13 +10,15 @@ from mongoengine import signals from mongoengine.base.common import get_document from mongoengine.base.datastructures import (BaseDict, BaseList, EmbeddedDocumentList, - SemiStrictDict, StrictDict) + LazyReference, + StrictDict) from mongoengine.base.fields import ComplexBaseField from mongoengine.common import _import_class from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError, LookUpError, OperationError, ValidationError) +from mongoengine.python_support import Hashable -__all__ = ('BaseDocument',) +__all__ = ('BaseDocument', 'NON_FIELD_ERRORS') NON_FIELD_ERRORS = '__all__' @@ -79,8 +78,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() @@ -100,13 +98,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: @@ -147,7 +143,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,) @@ -304,7 +300,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: @@ -337,7 +333,7 @@ class BaseDocument(object): value = field.generate() self._data[field_name] = value - if value is not None: + if (value is not None) or (field.null): if use_db_field: data[field.db_field] = value else: @@ -406,7 +402,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): @@ -489,7 +493,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 @@ -497,7 +501,13 @@ class BaseDocument(object): self._changed_fields = [] - def _nestable_types_changed_fields(self, changed_fields, key, data, inspected): + def _nestable_types_changed_fields(self, changed_fields, base_key, data): + """Inspect nested data for changed fields + + :param changed_fields: Previously collected changed fields + :param base_key: The base key that must be used to prepend changes to this data + :param data: data to inspect for changes + """ # Loop list / dict fields as they contain documents # Determine the iterator to use if not hasattr(data, 'items'): @@ -505,68 +515,60 @@ class BaseDocument(object): else: iterator = data.iteritems() - for index, value in iterator: - list_key = '%s%s.' % (key, index) + for index_or_key, value in iterator: + item_key = '%s%s.' % (base_key, index_or_key) # don't check anything lower if this key is already marked # as changed. - if list_key[:-1] in changed_fields: + if item_key[:-1] in changed_fields: continue + if hasattr(value, '_get_changed_fields'): - changed = value._get_changed_fields(inspected) - changed_fields += ['%s%s' % (list_key, k) - for k in changed if k] + changed = value._get_changed_fields() + changed_fields += ['%s%s' % (item_key, k) for k in changed if k] elif isinstance(value, (list, tuple, dict)): self._nestable_types_changed_fields( - changed_fields, list_key, value, inspected) + changed_fields, item_key, value) - def _get_changed_fields(self, inspected=None): + def _get_changed_fields(self): """Return a list of all fields that have explicitly been changed. """ EmbeddedDocument = _import_class('EmbeddedDocument') - DynamicEmbeddedDocument = _import_class('DynamicEmbeddedDocument') ReferenceField = _import_class('ReferenceField') + GenericReferenceField = _import_class('GenericReferenceField') SortedListField = _import_class('SortedListField') changed_fields = [] changed_fields += getattr(self, '_changed_fields', []) - inspected = inspected or set() - if hasattr(self, 'id') and isinstance(self.id, Hashable): - if self.id in inspected: - return changed_fields - inspected.add(self.id) - for field_name in self._fields_ordered: db_field_name = self._db_field_map.get(field_name, field_name) key = '%s.' % db_field_name data = self._data.get(field_name, None) field = self._fields.get(field_name) - if hasattr(data, 'id'): - if data.id in inspected: - continue - if isinstance(field, ReferenceField): + if db_field_name in changed_fields: + # Whole field already marked as changed, no need to go further continue - elif ( - isinstance(data, (EmbeddedDocument, DynamicEmbeddedDocument)) and - db_field_name not in changed_fields - ): + + if isinstance(field, ReferenceField): # Don't follow referenced documents + continue + + if isinstance(data, EmbeddedDocument): # Find all embedded fields that have been changed - changed = data._get_changed_fields(inspected) + changed = data._get_changed_fields() changed_fields += ['%s%s' % (key, k) for k in changed if k] - elif (isinstance(data, (list, tuple, dict)) and - db_field_name not in changed_fields): + elif isinstance(data, (list, tuple, dict)): if (hasattr(field, 'field') and - isinstance(field.field, ReferenceField)): + isinstance(field.field, (ReferenceField, GenericReferenceField))): 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 self._nestable_types_changed_fields( - changed_fields, key, data, inspected) + changed_fields, key, data) return changed_fields def _delta(self): @@ -578,7 +580,6 @@ class BaseDocument(object): set_fields = self._get_changed_fields() unset_data = {} - parts = [] if hasattr(self, '_changed_fields'): set_data = {} # Fetch each set item from its path @@ -588,15 +589,13 @@ class BaseDocument(object): new_path = [] for p in parts: if isinstance(d, (ObjectId, DBRef)): + # Don't dig in the references break - elif isinstance(d, list) and p.lstrip('-').isdigit(): - if p[0] == '-': - p = str(len(d) + int(p)) - try: - d = d[int(p)] - except IndexError: - d = None + elif isinstance(d, list) and p.isdigit(): + # An item of a list (identified by its index) is updated + d = d[int(p)] elif hasattr(d, 'get'): + # dict-like (dict, embedded document) d = d.get(p) new_path.append(p) path = '.'.join(new_path) @@ -608,26 +607,26 @@ class BaseDocument(object): # Determine if any changed items were actually unset. for path, value in set_data.items(): - if value or isinstance(value, (numbers.Number, bool)): + if value or isinstance(value, (numbers.Number, bool)): # Account for 0 and True that are truthy continue - # If we've set a value that ain't the default value don't unset it. - default = None + parts = path.split('.') + if (self._dynamic and len(parts) and parts[0] in self._dynamic_fields): del set_data[path] unset_data[path] = 1 continue - elif path in self._fields: + + # If we've set a value that ain't the default value don't unset it. + default = None + if path in self._fields: default = self._fields[path].default else: # Perform a full lookup for lists / embedded lookups d = self - parts = path.split('.') db_field_name = parts.pop() for p in parts: - if isinstance(d, list) and p.lstrip('-').isdigit(): - if p[0] == '-': - p = str(len(d) + int(p)) + if isinstance(d, list) and p.isdigit(): d = d[int(p)] elif (hasattr(d, '__getattribute__') and not isinstance(d, dict)): @@ -645,10 +644,9 @@ class BaseDocument(object): default = None if default is not None: - if callable(default): - default = default() + default = default() if callable(default) else default - if default != value: + if value != default: continue del set_data[path] @@ -694,7 +692,7 @@ class BaseDocument(object): fields = cls._fields if not _auto_dereference: - fields = copy.copy(fields) + fields = copy.deepcopy(fields) for field_name, field in fields.iteritems(): field._auto_dereference = _auto_dereference @@ -1080,5 +1078,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)): - return dict(field.choices).get(value, value) + if value is None: + return None + sep = getattr(field, 'display_sep', ' ') + values = value if field.__class__.__name__ in ('ListField', 'SortedListField') else [value] + return sep.join([ + six.text_type(dict(field.choices).get(val, val)) + for val in values or []]) return value diff --git a/mongoengine/base/fields.py b/mongoengine/base/fields.py index e2b5d321..a32544d8 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: @@ -213,8 +212,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 @@ -265,13 +266,15 @@ class ComplexBaseField(BaseField): ReferenceField = _import_class('ReferenceField') GenericReferenceField = _import_class('GenericReferenceField') EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField') - dereference = (self._auto_dereference and + + auto_dereference = instance._fields[self.name]._auto_dereference + + dereference = (auto_dereference and (self.field is None or isinstance(self.field, (GenericReferenceField, ReferenceField)))) _dereference = _import_class('DeReference')() - self._auto_dereference = instance._fields[self.name]._auto_dereference if instance._initialised and dereference and instance._data.get(self.name): instance._data[self.name] = _dereference( instance._data.get(self.name), max_depth=1, instance=instance, @@ -292,7 +295,7 @@ class ComplexBaseField(BaseField): value = BaseDict(value, instance, self.name) instance._data[self.name] = value - if (self._auto_dereference and instance._initialised and + if (auto_dereference and instance._initialised and isinstance(value, (BaseList, BaseDict)) and not value._dereferenced): value = _dereference( @@ -311,11 +314,16 @@ class ComplexBaseField(BaseField): if hasattr(value, 'to_python'): return value.to_python() + BaseDocument = _import_class('BaseDocument') + if isinstance(value, BaseDocument): + # Something is wrong, return the value as it is + return value + is_list = False if not hasattr(value, 'items'): try: is_list = True - value = {k: v for k, v in enumerate(value)} + value = {idx: v for idx, v in enumerate(value)} except TypeError: # Not iterable return the value return value @@ -500,7 +508,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/base/metaclasses.py b/mongoengine/base/metaclasses.py index 481408bf..8eb10008 100644 --- a/mongoengine/base/metaclasses.py +++ b/mongoengine/base/metaclasses.py @@ -18,14 +18,14 @@ class DocumentMetaclass(type): """Metaclass for all documents.""" # TODO lower complexity of this method - def __new__(cls, name, bases, attrs): - flattened_bases = cls._get_bases(bases) - super_new = super(DocumentMetaclass, cls).__new__ + def __new__(mcs, name, bases, attrs): + flattened_bases = mcs._get_bases(bases) + super_new = super(DocumentMetaclass, mcs).__new__ # If a base class just call super metaclass = attrs.get('my_metaclass') if metaclass and issubclass(metaclass, DocumentMetaclass): - return super_new(cls, name, bases, attrs) + return super_new(mcs, name, bases, attrs) attrs['_is_document'] = attrs.get('_is_document', False) attrs['_cached_reference_fields'] = [] @@ -121,7 +121,8 @@ class DocumentMetaclass(type): # inheritance of classes where inheritance is set to False allow_inheritance = base._meta.get('allow_inheritance') if not allow_inheritance and not base._meta.get('abstract'): - raise ValueError('Document %s may not be subclassed' % + raise ValueError('Document %s may not be subclassed. ' + 'To enable inheritance, use the "allow_inheritance" meta attribute.' % base.__name__) # Get superclasses from last base superclass @@ -138,7 +139,7 @@ class DocumentMetaclass(type): attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types # Create the new_class - new_class = super_new(cls, name, bases, attrs) + new_class = super_new(mcs, name, bases, attrs) # Set _subclasses for base in document_bases: @@ -147,7 +148,7 @@ class DocumentMetaclass(type): base._types = base._subclasses # TODO depreciate _types (Document, EmbeddedDocument, DictField, - CachedReferenceField) = cls._import_classes() + CachedReferenceField) = mcs._import_classes() if issubclass(new_class, Document): new_class._collection = None @@ -219,29 +220,26 @@ class DocumentMetaclass(type): return new_class - def add_to_class(self, name, value): - setattr(self, name, value) - @classmethod - def _get_bases(cls, bases): + def _get_bases(mcs, bases): if isinstance(bases, BasesTuple): return bases seen = [] - bases = cls.__get_bases(bases) + bases = mcs.__get_bases(bases) unique_bases = (b for b in bases if not (b in seen or seen.append(b))) return BasesTuple(unique_bases) @classmethod - def __get_bases(cls, bases): + def __get_bases(mcs, bases): for base in bases: if base is object: continue yield base - for child_base in cls.__get_bases(base.__bases__): + for child_base in mcs.__get_bases(base.__bases__): yield child_base @classmethod - def _import_classes(cls): + def _import_classes(mcs): Document = _import_class('Document') EmbeddedDocument = _import_class('EmbeddedDocument') DictField = _import_class('DictField') @@ -254,9 +252,9 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): collection in the database. """ - def __new__(cls, name, bases, attrs): - flattened_bases = cls._get_bases(bases) - super_new = super(TopLevelDocumentMetaclass, cls).__new__ + def __new__(mcs, name, bases, attrs): + flattened_bases = mcs._get_bases(bases) + super_new = super(TopLevelDocumentMetaclass, mcs).__new__ # Set default _meta data if base class, otherwise get user defined meta if attrs.get('my_metaclass') == TopLevelDocumentMetaclass: @@ -319,7 +317,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): not parent_doc_cls._meta.get('abstract', False)): msg = 'Abstract document cannot have non-abstract base' raise ValueError(msg) - return super_new(cls, name, bases, attrs) + return super_new(mcs, name, bases, attrs) # Merge base class metas. # Uses a special MetaDict that handles various merging rules @@ -360,7 +358,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): attrs['_meta'] = meta # Call super and get the new class - new_class = super_new(cls, name, bases, attrs) + new_class = super_new(mcs, name, bases, attrs) meta = new_class._meta @@ -394,7 +392,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): '_auto_id_field', False) if not new_class._meta.get('id_field'): # After 0.10, find not existing names, instead of overwriting - id_name, id_db_name = cls.get_auto_id_names(new_class) + id_name, id_db_name = mcs.get_auto_id_names(new_class) new_class._auto_id_field = True new_class._meta['id_field'] = id_name new_class._fields[id_name] = ObjectIdField(db_field=id_db_name) @@ -419,7 +417,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): return new_class @classmethod - def get_auto_id_names(cls, new_class): + def get_auto_id_names(mcs, new_class): id_name, id_db_name = ('id', '_id') if id_name not in new_class._fields and \ id_db_name not in (v.db_field for v in new_class._fields.values()): diff --git a/mongoengine/base/utils.py b/mongoengine/base/utils.py new file mode 100644 index 00000000..8f27ee14 --- /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, instance, owner): + return self.compiled_regex + + def __set__(self, instance, value): + raise AttributeError("Can not set attribute LazyRegexCompiler") diff --git a/mongoengine/connection.py b/mongoengine/connection.py index 7eae810f..38ebb243 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, @@ -103,6 +104,18 @@ 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 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'].lower() + for preference in read_preferences: + if preference.name.lower() == read_pf_mode: + conn_settings['read_preference'] = preference + break else: resolved_hosts.append(entity) conn_settings['host'] = resolved_hosts @@ -146,13 +159,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 diff --git a/mongoengine/context_managers.py b/mongoengine/context_managers.py index c477575e..c26b0a79 100644 --- a/mongoengine/context_managers.py +++ b/mongoengine/context_managers.py @@ -1,9 +1,11 @@ +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 __all__ = ('switch_db', 'switch_collection', 'no_dereference', - 'no_sub_classes', 'query_counter') + 'no_sub_classes', 'query_counter', 'set_write_concern') class switch_db(object): @@ -143,66 +145,85 @@ 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): - """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': # MONGODB < 3.2 + {'$ne': 'killcursors'}, + 'command.killCursors': # MONGODB >= 3.2 + {'$exists': False} + } + + 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): @@ -210,8 +231,17 @@ 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 + """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 + + +@contextmanager +def set_write_concern(collection, write_concerns): + combined_concerns = dict(collection.write_concern.document.items()) + combined_concerns.update(write_concerns) + yield collection.with_options(write_concern=WriteConcern(**combined_concerns)) diff --git a/mongoengine/dereference.py b/mongoengine/dereference.py index 59204d4d..619b5d1f 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 @@ -51,26 +52,40 @@ class DeReference(object): [i.__class__ == doc_type for i in items.values()]): return items elif not field.dbref: + # We must turn the ObjectIds into DBRefs + + # Recursively dig into the sub items of a list/dict + # to turn the ObjectIds into DBRefs + def _get_items_from_list(items): + new_items = [] + for v in items: + value = v + if isinstance(v, dict): + value = _get_items_from_dict(v) + elif isinstance(v, list): + value = _get_items_from_list(v) + elif not isinstance(v, (DBRef, Document)): + value = field.to_python(v) + new_items.append(value) + return new_items + + def _get_items_from_dict(items): + new_items = {} + for k, v in items.iteritems(): + value = v + if isinstance(v, list): + value = _get_items_from_list(v) + elif isinstance(v, dict): + value = _get_items_from_dict(v) + elif not isinstance(v, (DBRef, Document)): + value = field.to_python(v) + new_items[k] = value + return new_items + if not hasattr(items, 'items'): - - def _get_items(items): - new_items = [] - for v in items: - if isinstance(v, list): - new_items.append(_get_items(v)) - elif not isinstance(v, (DBRef, Document)): - new_items.append(field.to_python(v)) - else: - new_items.append(v) - return new_items - - items = _get_items(items) + items = _get_items_from_list(items) else: - items = { - k: (v if isinstance(v, (DBRef, Document)) - else field.to_python(v)) - for k, v in items.iteritems() - } + items = _get_items_from_dict(items) self.reference_map = self._find_references(items) self.object_map = self._fetch_objects(doc_type=doc_type) @@ -99,7 +114,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 +128,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: @@ -126,7 +147,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 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] @@ -134,7 +160,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 @@ -230,7 +256,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: diff --git a/mongoengine/document.py b/mongoengine/document.py index 2efb5c5c..57364ae6 100644 --- a/mongoengine/document.py +++ b/mongoengine/document.py @@ -12,7 +12,9 @@ from mongoengine.base import (BaseDict, BaseDocument, BaseList, TopLevelDocumentMetaclass, get_document) from mongoengine.common import _import_class from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db -from mongoengine.context_managers import switch_collection, switch_db +from mongoengine.context_managers import (set_write_concern, + switch_collection, + switch_db) from mongoengine.errors import (InvalidDocumentError, InvalidQueryError, SaveConditionError) from mongoengine.python_support import IS_PYMONGO_3 @@ -39,7 +41,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 +60,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 @@ -88,11 +89,20 @@ class EmbeddedDocument(BaseDocument): return data + def save(self, *args, **kwargs): + warnings.warn("EmbeddedDocument.save is deprecated and will be removed in a next version of mongoengine." + "Use the parent document's .save() or ._instance.save()", + DeprecationWarning, stacklevel=2) + self._instance.save(*args, **kwargs) + def reload(self, *args, **kwargs): + warnings.warn("EmbeddedDocument.reload is deprecated and will be removed in a next version of mongoengine." + "Use the parent document's .reload() or ._instance.reload()", + DeprecationWarning, stacklevel=2) self._instance.reload(*args, **kwargs) -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. @@ -147,7 +157,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',) @@ -169,8 +178,8 @@ class Document(BaseDocument): """ if self.pk is None: return super(BaseDocument, self).__hash__() - else: - return hash(self.pk) + + return hash(self.pk) @classmethod def _get_db(cls): @@ -192,7 +201,10 @@ 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. + db = cls._get_db() + if cls._meta.get('auto_create_index', True) and\ + db.client.is_primary: cls.ensure_indexes() return cls._collection @@ -277,6 +289,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 @@ -361,6 +376,8 @@ class Document(BaseDocument): signals.pre_save_post_validation.send(self.__class__, document=self, created=created, **signal_kwargs) + # it might be refreshed by the pre_save_post_validation hook, e.g., for etag generation + doc = self.to_mongo() if self._meta.get('auto_create_index', True): self.ensure_indexes() @@ -420,11 +437,18 @@ class Document(BaseDocument): Helper method, should only be used inside save(). """ collection = self._get_collection() + with set_write_concern(collection, write_concern) as wc_collection: + if force_insert: + return wc_collection.insert_one(doc).inserted_id + # insert_one will provoke UniqueError alongside save does not + # therefore, it need to catch and call replace_one. + if '_id' in doc: + raw_object = wc_collection.find_one_and_replace( + {'_id': doc['_id']}, doc) + if raw_object: + return doc['_id'] - if force_insert: - return collection.insert(doc, **write_concern) - - object_id = collection.save(doc, **write_concern) + object_id = wc_collection.insert_one(doc).inserted_id # 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 @@ -573,12 +597,11 @@ 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 - ``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. @@ -699,7 +722,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: @@ -707,7 +729,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 @@ -715,7 +737,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 @@ -961,8 +985,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] @@ -979,10 +1011,10 @@ 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 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 @@ -990,13 +1022,12 @@ class DynamicDocument(Document): .. note:: - There is one caveat on Dynamic Documents: fields cannot start with `_` + There is one caveat on Dynamic Documents: undeclared fields cannot start with `_` """ # 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 @@ -1007,11 +1038,12 @@ 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) -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. @@ -1020,7 +1052,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/errors.py b/mongoengine/errors.py index 131596d1..986ebf73 100644 --- a/mongoengine/errors.py +++ b/mongoengine/errors.py @@ -71,6 +71,7 @@ class ValidationError(AssertionError): _message = None def __init__(self, message='', **kwargs): + super(ValidationError, self).__init__(message) self.errors = kwargs.get('errors', {}) self.field_name = kwargs.get('field_name') self.message = message diff --git a/mongoengine/fields.py b/mongoengine/fields.py index 0d402712..0055bcab 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 @@ -25,13 +24,18 @@ try: except ImportError: Int64 = long + from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField, - GeoJsonBaseField, ObjectIdField, get_document) + 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 from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError from mongoengine.python_support import StringIO -from mongoengine.queryset import DO_NOTHING, QuerySet +from mongoengine.queryset import DO_NOTHING +from mongoengine.queryset.base import BaseQuerySet try: from PIL import Image, ImageOps @@ -39,13 +43,20 @@ 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', + 'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField', 'DateField', 'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField', 'GenericEmbeddedDocumentField', 'DynamicField', 'ListField', 'SortedListField', 'EmbeddedDocumentListField', 'DictField', 'MapField', 'ReferenceField', 'CachedReferenceField', + 'LazyReferenceField', 'GenericLazyReferenceField', 'GenericReferenceField', 'BinaryField', 'GridFSError', 'GridFSProxy', 'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField', 'GeoPointField', 'PointField', 'LineStringField', 'PolygonField', @@ -120,9 +131,9 @@ 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,}(?>> 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): + 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) @@ -611,6 +650,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) @@ -625,9 +665,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): @@ -686,16 +734,28 @@ 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') 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 - data = document.to_mongo(use_db_field, fields) if '_cls' not in data: data['_cls'] = document._class_name @@ -779,10 +839,20 @@ 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 - isinstance(value, six.string_types)): + if not isinstance(value, (list, tuple, BaseQuerySet)): self.error('Only lists and tuples may be used in a list field') super(ListField, self).validate(value) @@ -874,7 +944,7 @@ def key_has_dot_or_dollar(d): dictionary contains a dot or a dollar sign. """ for k, v in d.items(): - if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)): + if ('.' in k or k.startswith('$')) or (isinstance(v, dict) and key_has_dot_or_dollar(v)): return True @@ -889,12 +959,10 @@ 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 - if not issubclass(self.basecls, BaseField): - self.error('DictField only accepts dict values') + kwargs.setdefault('default', lambda: {}) super(DictField, self).__init__(*args, **kwargs) @@ -909,11 +977,11 @@ class DictField(ComplexBaseField): self.error(msg) if key_has_dot_or_dollar(value): self.error('Invalid dictionary key name - keys may not contain "."' - ' or "$" characters') + ' or startswith "$" characters') 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', @@ -923,7 +991,7 @@ class DictField(ComplexBaseField): if op in match_operators and isinstance(value, six.string_types): return StringField().prepare_query_value(op, value) - if hasattr(self.field, 'field'): + if hasattr(self.field, 'field'): # Used for instance when using DictField(ListField(IntField())) if op in ('set', 'unset') and isinstance(value, dict): return { k: self.field.prepare_query_value(op, v) @@ -943,6 +1011,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') @@ -953,6 +1022,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` @@ -971,11 +1049,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` """ @@ -993,6 +1073,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) @@ -1022,9 +1103,9 @@ class ReferenceField(BaseField): # Get value from document instance if available value = instance._data.get(self.name) - self._auto_dereference = instance._fields[self.name]._auto_dereference + auto_dereference = instance._fields[self.name]._auto_dereference # Dereference DBRefs - if self._auto_dereference and isinstance(value, DBRef): + if auto_dereference and isinstance(value, DBRef): if hasattr(value, 'cls'): # Dereference using the class type specified in the reference cls = get_document(value.cls) @@ -1047,6 +1128,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') @@ -1086,21 +1169,13 @@ class ReferenceField(BaseField): return self.to_mongo(value) 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 ' 'saved to the database') - 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) - ) - def lookup_member(self, member_name): return self.document_type._fields.get(member_name) @@ -1121,6 +1196,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) @@ -1180,9 +1256,10 @@ class CachedReferenceField(BaseField): # Get value from document instance if available value = instance._data.get(self.name) - self._auto_dereference = instance._fields[self.name]._auto_dereference + auto_dereference = instance._fields[self.name]._auto_dereference + # Dereference DBRefs - if self._auto_dereference and isinstance(value, DBRef): + if auto_dereference and isinstance(value, DBRef): dereferenced = self.document_type._get_db().dereference(value) if dereferenced is None: raise DoesNotExist('Trying to dereference unknown document %s' % value) @@ -1195,6 +1272,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 @@ -1203,7 +1281,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_)), @@ -1221,16 +1298,20 @@ 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' ' 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 def validate(self, value): - if not isinstance(value, self.document_type): self.error('A CachedReferenceField only accepts documents') @@ -1263,6 +1344,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 @@ -1285,6 +1372,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') @@ -1303,8 +1392,8 @@ class GenericReferenceField(BaseField): value = instance._data.get(self.name) - self._auto_dereference = instance._fields[self.name]._auto_dereference - if self._auto_dereference and isinstance(value, (dict, SON)): + auto_dereference = instance._fields[self.name]._auto_dereference + if auto_dereference and isinstance(value, (dict, SON)): dereferenced = self.dereference(value) if dereferenced is None: raise DoesNotExist('Trying to dereference unknown document %s' % value) @@ -1348,6 +1437,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: @@ -1385,14 +1475,20 @@ class BinaryField(BaseField): return Binary(value) def validate(self, value): - if not isinstance(value, (six.binary_type, six.text_type, Binary)): + if not isinstance(value, (six.binary_type, Binary)): self.error('BinaryField only accepts instances of ' '(%s, %s, Binary)' % ( - six.binary_type.__name__, six.text_type.__name__)) + six.binary_type.__name__, Binary.__name__)) if self.max_bytes is not None and len(value) > self.max_bytes: self.error('Binary value is too long') + def prepare_query_value(self, op, value): + if value is None: + return value + return super(BinaryField, self).prepare_query_value( + op, self.to_mongo(value)) + class GridFSError(Exception): pass @@ -1433,9 +1529,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 @@ -1453,9 +1551,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): @@ -1465,6 +1563,9 @@ class GridFSProxy(object): else: return False + def __ne__(self, other): + return not self == other + @property def fs(self): if not self._fs: @@ -1772,12 +1873,9 @@ class ImageField(FileField): """ A Image File storage field. - @size (width, height, force): - max size to store images, if larger will be automatically resized - ex: size=(800, 600, True) - - @thumbnail (width, height, force): - size to generate a thumbnail + :param size: max size to store images, provided as (width, height, force) + if larger, it will be automatically resized (ex: size=(800, 600, True)) + :param thumbnail_size: size to generate a thumbnail, provided as (width, height, force) .. versionadded:: 0.6 """ @@ -1848,8 +1946,7 @@ class SequenceField(BaseField): self.collection_name = collection_name or self.COLLECTION_NAME self.db_alias = db_alias or DEFAULT_CONNECTION_NAME self.sequence_name = sequence_name - self.value_decorator = (callable(value_decorator) and - value_decorator or self.VALUE_DECORATOR) + self.value_decorator = value_decorator if callable(value_decorator) else self.VALUE_DECORATOR super(SequenceField, self).__init__(*args, **kwargs) def generate(self): @@ -1958,7 +2055,7 @@ class UUIDField(BaseField): if not isinstance(value, six.string_types): value = six.text_type(value) return uuid.UUID(value) - except Exception: + except (ValueError, TypeError, AttributeError): return original_value return value @@ -1980,7 +2077,7 @@ class UUIDField(BaseField): value = str(value) try: uuid.UUID(value) - except Exception as exc: + except (ValueError, TypeError, AttributeError) as exc: self.error('Could not convert to UUID: %s' % exc) @@ -2138,3 +2235,201 @@ 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 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 + """ + + 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). + """ + # XXX ValidationError raised outside of the "validate" method. + 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 build_lazyref(self, value): + if isinstance(value, LazyReference): + if value.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) + 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) + 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) + + 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 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): + 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) + + +class GenericLazyReferenceField(GenericReferenceField): + """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 + 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._class_name + super(GenericLazyReferenceField, self)._validate_choices(value) + + def build_lazyref(self, value): + if isinstance(value, LazyReference): + if value.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) + + 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', DBRef(document.document_type._get_collection_name(), document.pk)) + )) + else: + return super(GenericLazyReferenceField, self).to_mongo(document) diff --git a/mongoengine/python_support.py b/mongoengine/python_support.py index e51e1bc9..7e8e108f 100644 --- a/mongoengine/python_support.py +++ b/mongoengine/python_support.py @@ -6,11 +6,7 @@ import pymongo import six -if pymongo.version_tuple[0] < 3: - IS_PYMONGO_3 = False -else: - IS_PYMONGO_3 = True - +IS_PYMONGO_3 = pymongo.version_tuple[0] >= 3 # six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3. StringIO = six.BytesIO @@ -23,3 +19,10 @@ if not six.PY3: pass else: StringIO = cStringIO.StringIO + + +if six.PY3: + from collections.abc import Hashable +else: + # raises DeprecationWarnings in Python >=3.7 + from collections import Hashable diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py index f7c32d20..0ebeafa6 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 @@ -18,7 +17,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 set_write_concern, switch_db from mongoengine.errors import (InvalidQueryError, LookUpError, NotUniqueError, OperationError) from mongoengine.python_support import IS_PYMONGO_3 @@ -39,8 +38,6 @@ CASCADE = 2 DENY = 3 PULL = 4 -RE_TYPE = type(re.compile('')) - class BaseQuerySet(object): """A set of results returned from a query. Wraps a MongoDB cursor, @@ -191,7 +188,7 @@ class BaseQuerySet(object): ) if queryset._as_pymongo: - return queryset._get_as_pymongo(queryset._cursor[key]) + return queryset._cursor[key] return queryset._document._from_son( queryset._cursor[key], @@ -209,18 +206,16 @@ 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): - """Returns all documents.""" + """Returns a copy of the current QuerySet.""" return self.__call__() def filter(self, *q_objs, **query): @@ -269,13 +264,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 @@ -350,11 +345,24 @@ class BaseQuerySet(object): documents=docs, **signal_kwargs) raw = [doc.to_mongo() for doc in docs] + + with set_write_concern(self._collection, write_concern) as collection: + insert_func = collection.insert_many + if return_one: + raw = raw[0] + insert_func = collection.insert_one + try: - ids = self._collection.insert(raw, **write_concern) + inserted_result = insert_func(raw) + 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)) + 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)): @@ -364,18 +372,20 @@ class BaseQuerySet(object): raise NotUniqueError(message % six.text_type(err)) raise OperationError(message % six.text_type(err)) + # Apply inserted_ids to documents + for doc, doc_id in zip(docs, ids): + doc.pk = doc_id + if not load_bulk: signals.post_bulk_insert.send( self._document, documents=docs, loaded=False, **signal_kwargs) - return return_one and ids[0] or ids + return ids[0] if return_one else ids documents = self.in_bulk(ids) - results = [] - for obj_id in ids: - results.append(documents.get(obj_id)) + results = [documents.get(obj_id) for obj_id in ids] signals.post_bulk_insert.send( self._document, documents=results, loaded=True, **signal_kwargs) - return return_one and results[0] or results + return results[0] if return_one else results def count(self, with_limit_and_skip=False): """Count the selected elements in the query. @@ -384,9 +394,11 @@ 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) + count = self._cursor.count(with_limit_and_skip=with_limit_and_skip) + self._cursor_obj = None + return count def delete(self, write_concern=None, _from_doc_delete=False, cascade_refs=None): @@ -486,8 +498,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 + ``{'n': 2, 'nModified': 2, 'ok': 1.0, 'updatedExisting': True}``. :param update: Django-style update keyword arguments .. versionadded:: 0.2 @@ -510,12 +523,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 +560,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): @@ -674,7 +690,7 @@ class BaseQuerySet(object): self._document._from_son(doc, only_fields=self.only_fields)) elif self._as_pymongo: for doc in docs: - doc_map[doc['_id']] = self._get_as_pymongo(doc) + doc_map[doc['_id']] = doc else: for doc in docs: doc_map[doc['_id']] = self._document._from_son( @@ -759,10 +775,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: @@ -960,11 +977,10 @@ class BaseQuerySet(object): # explicitly included, and then more complicated operators such as # $slice. def _sort_key(field_tuple): - key, value = field_tuple - if isinstance(value, (int)): + _, value = field_tuple + if isinstance(value, int): return value # 0 for exclusion, 1 for inclusion - else: - return 2 # so that complex values appear last + return 2 # so that complex values appear last fields = sorted(cleaned_fields, key=_sort_key) @@ -1182,6 +1198,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 @@ -1457,16 +1477,16 @@ 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) + return raw_doc doc = self._document._from_son( raw_doc, _auto_dereference=self._auto_dereference, @@ -1477,6 +1497,8 @@ class BaseQuerySet(object): return doc + next = __next__ # For Python2 support + def rewind(self): """Rewind the cursor to its unevaluated state. @@ -1578,6 +1600,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): @@ -1722,25 +1747,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.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.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 +1781,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 @@ -1799,26 +1833,6 @@ class BaseQuerySet(object): return tuple(data) - 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'] - - # 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'] - - return doc - def _sub_js_fields(self, code): """When fields are specified with [~fieldname] syntax, where *fieldname* is the Python name of a field, *fieldname* will be @@ -1840,8 +1854,8 @@ class BaseQuerySet(object): # Substitute the correct name for the field into the javascript return '.'.join([f.db_field for f in fields]) - code = re.sub(u'\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code) - code = re.sub(u'\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}', field_path_sub, + code = re.sub(r'\[\s*~([A-z_][A-z_0-9.]+?)\s*\]', field_sub, code) + code = re.sub(r'\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}', field_path_sub, code) return code 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 b5d2765b..c7c593b1 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) @@ -87,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: @@ -112,8 +114,8 @@ 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): - self._result_cache.append(self.next()) + for _ in six.moves.range(ITER_CHUNK_SIZE): + 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 @@ -166,9 +168,9 @@ 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()) + data.append(six.next(self)) except StopIteration: break @@ -184,10 +186,3 @@ class QuerySetNoCache(BaseQuerySet): queryset = self.clone() queryset.rewind() return queryset - - -class QuerySetNoDeRef(QuerySet): - """Special no_dereference QuerySet""" - - def __dereference(items, max_depth=1, instance=None, name=None): - return items diff --git a/mongoengine/queryset/transform.py b/mongoengine/queryset/transform.py index bb04ee37..2d22c350 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: @@ -160,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] @@ -214,30 +201,37 @@ 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: 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 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' + # If the operator doesn't found from operator map, the op value + # will stay unchanged + op = operator_map.get(op, op) match = None if parts[-1] in COMPARISON_OPERATORS: @@ -284,7 +278,15 @@ def update(_doc_cls=None, **update): if isinstance(field, GeoJsonBaseField): value = field.to_mongo(value) - if op in (None, 'set', 'push', 'pull'): + 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'): if field.required or value is not None: value = field.prepare_query_value(op, value) elif op in ('pushAll', 'pullAll'): @@ -296,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 @@ -319,11 +323,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) @@ -333,10 +343,26 @@ def update(_doc_cls=None, **update): value = {key: value} elif op == 'addToSet' and isinstance(value, list): value = {key: {'$each': value}} + elif op in ('push', 'pushAll'): + if parts[-1].isdigit(): + key = '.'.join(parts[0:-1]) + position = int(parts[-1]) + # $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}} + else: + 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 - if key not in mongo_update: mongo_update[key] = value elif key in mongo_update and isinstance(mongo_update[key], dict): @@ -403,7 +429,6 @@ def _infer_geometry(value): 'type and coordinates keys') elif isinstance(value, (list, set)): # TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon? - # TODO: should both TypeError and IndexError be alike interpreted? try: value[0][0][0] @@ -425,3 +450,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/mongoengine/queryset/visitor.py b/mongoengine/queryset/visitor.py index bcf93a13..9d97094b 100644 --- a/mongoengine/queryset/visitor.py +++ b/mongoengine/queryset/visitor.py @@ -3,7 +3,7 @@ import copy from mongoengine.errors import InvalidQueryError from mongoengine.queryset import transform -__all__ = ('Q',) +__all__ = ('Q', 'QNode') class QNodeVisitor(object): @@ -131,6 +131,10 @@ class QCombination(QNode): else: self.children.append(node) + def __repr__(self): + op = ' & ' if self.operation is self.AND else ' | ' + return '(%s)' % op.join([repr(node) for node in self.children]) + def accept(self, visitor): for i in range(len(self.children)): if isinstance(self.children[i], QNode): @@ -151,6 +155,9 @@ class Q(QNode): def __init__(self, **query): self.query = query + def __repr__(self): + return 'Q(**%s)' % repr(self.query) + def accept(self, visitor): return visitor.visit_query(self) diff --git a/setup.cfg b/setup.cfg index eabe3271..84086601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,11 +1,11 @@ [nosetests] verbosity=2 detailed-errors=1 -tests=tests +#tests=tests cover-package=mongoengine [flake8] -ignore=E501,F401,F403,F405,I201 +ignore=E501,F401,F403,F405,I201,I202,W504, W605 exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests max-complexity=47 application-import-names=mongoengine,tests diff --git a/setup.py b/setup.py index fa682d20..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', @@ -70,9 +69,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', 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/document/class_methods.py b/tests/document/class_methods.py index dd3addb7..2632d38f 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 requires_mongodb_gte_26 __all__ = ("ClassMethodsTest", ) @@ -65,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 @@ -187,6 +188,26 @@ class ClassMethodsTest(unittest.TestCase): self.assertEqual(BlogPostWithTags.compare_indexes(), { 'missing': [], 'extra': [] }) self.assertEqual(BlogPostWithCustomField.compare_indexes(), { 'missing': [], 'extra': [] }) + @requires_mongodb_gte_26 + def test_compare_indexes_for_text_indexes(self): + """ Ensure that compare_indexes behaves correctly for text indexes """ + + class Doc(Document): + a = StringField() + b = StringField() + meta = {'indexes': [ + {'fields': ['$a', "$b"], + 'default_language': 'english', + 'weights': {'a': 10, 'b': 2} + } + ]} + + 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 @@ -319,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") @@ -328,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..44548d27 100644 --- a/tests/document/dynamic.py +++ b/tests/document/dynamic.py @@ -1,16 +1,15 @@ import unittest from mongoengine import * -from mongoengine.connection import get_db +from tests.utils import MongoDBTestCase -__all__ = ("DynamicTest", ) +__all__ = ("TestDynamicDocument", ) -class DynamicTest(unittest.TestCase): +class TestDynamicDocument(MongoDBTestCase): def setUp(self): - connect(db='mongoenginetest') - self.db = get_db() + super(TestDynamicDocument, self).setUp() class Person(DynamicDocument): name = StringField() @@ -98,6 +97,72 @@ class DynamicTest(unittest.TestCase): self.assertEqual(len(p._data), 4) self.assertEqual(sorted(p._data.keys()), ['_cls', 'age', 'id', 'name']) + def test_fields_without_underscore(self): + """Ensure we can query dynamic fields""" + Person = self.Person + + p = self.Person(name='Dean') + p.save() + + raw_p = Person.objects.as_pymongo().get(id=p.id) + self.assertEqual( + raw_p, + { + '_cls': u'Person', + '_id': p.id, + 'name': u'Dean' + } + ) + + p.name = 'OldDean' + p.newattr = 'garbage' + p.save() + raw_p = Person.objects.as_pymongo().get(id=p.id) + self.assertEqual( + raw_p, + { + '_cls': u'Person', + '_id': p.id, + 'name': 'OldDean', + 'newattr': u'garbage' + } + ) + + def test_fields_containing_underscore(self): + """Ensure we can query dynamic fields""" + class WeirdPerson(DynamicDocument): + name = StringField() + _name = StringField() + + WeirdPerson.drop_collection() + + p = WeirdPerson(name='Dean', _name='Dean') + p.save() + + raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id) + self.assertEqual( + raw_p, + { + '_id': p.id, + '_name': u'Dean', + 'name': u'Dean' + } + ) + + p.name = 'OldDean' + p._name = 'NewDean' + p._newattr1 = 'garbage' # Unknown fields won't be added + p.save() + raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id) + self.assertEqual( + raw_p, + { + '_id': p.id, + '_name': u'NewDean', + 'name': u'OldDean', + } + ) + def test_dynamic_document_queries(self): """Ensure we can query dynamic fields""" p = self.Person() @@ -174,8 +239,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 +254,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""" @@ -369,5 +434,6 @@ class DynamicTest(unittest.TestCase): person.save() self.assertEqual(Person.objects.first().age, 35) + if __name__ == '__main__': unittest.main() diff --git a/tests/document/indexes.py b/tests/document/indexes.py index 58e09199..757d8037 100644 --- a/tests/document/indexes.py +++ b/tests/document/indexes.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- import unittest -import sys +from datetime import datetime from nose.plugins.skip import SkipTest -from datetime import datetime +from pymongo.errors import OperationFailure import pymongo from mongoengine import * from mongoengine.connection import get_db - -from tests.utils import get_mongodb_version, needs_mongodb_v26 +from tests.utils import get_mongodb_version, requires_mongodb_gte_26, MONGODB_32, MONGODB_3 __all__ = ("IndexesTest", ) @@ -19,6 +18,7 @@ class IndexesTest(unittest.TestCase): def setUp(self): self.connection = connect(db='mongoenginetest') self.db = get_db() + self.mongodb_version = get_mongodb_version() class Person(Document): name = StringField() @@ -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() @@ -491,7 +491,7 @@ class IndexesTest(unittest.TestCase): obj = Test(a=1) obj.save() - IS_MONGODB_3 = get_mongodb_version()[0] >= 3 + IS_MONGODB_3 = get_mongodb_version() >= MONGODB_3 # Need to be explicit about covered indexes as mongoDB doesn't know if # the documents returned might have more keys in that here. @@ -541,19 +541,24 @@ class IndexesTest(unittest.TestCase): [('categories', 1), ('_id', 1)]) def test_hint(self): + MONGO_VER = self.mongodb_version + TAGS_INDEX_NAME = 'tags_1' class BlogPost(Document): tags = ListField(StringField()) meta = { 'indexes': [ - 'tags', + { + 'fields': ['tags'], + 'name': TAGS_INDEX_NAME + } ], } BlogPost.drop_collection() - for i in range(0, 10): - tags = [("tag %i" % n) for n in range(0, i % 2)] + for i in range(10): + tags = [("tag %i" % n) for n in range(i % 2)] BlogPost(tags=tags).save() self.assertEqual(BlogPost.objects.count(), 10) @@ -563,18 +568,18 @@ class IndexesTest(unittest.TestCase): if pymongo.version != '3.0': self.assertEqual(BlogPost.objects.hint([('tags', 1)]).count(), 10) + if MONGO_VER == MONGODB_32: + # Mongo32 throws an error if an index exists (i.e `tags` in our case) + # and you use hint on an index name that does not exist + with self.assertRaises(OperationFailure): + BlogPost.objects.hint([('ZZ', 1)]).count() + else: self.assertEqual(BlogPost.objects.hint([('ZZ', 1)]).count(), 10) - if pymongo.version >= '2.8': - self.assertEqual(BlogPost.objects.hint('tags').count(), 10) - else: - def invalid_index(): - BlogPost.objects.hint('tags').next() - self.assertRaises(TypeError, invalid_index) + self.assertEqual(BlogPost.objects.hint(TAGS_INDEX_NAME ).count(), 10) - def invalid_index_2(): - return BlogPost.objects.hint(('tags', 1)).next() - self.assertRaises(Exception, invalid_index_2) + with self.assertRaises(Exception): + BlogPost.objects.hint(('tags', 1)).next() def test_unique(self): """Ensure that uniqueness constraints are applied to fields. @@ -749,7 +754,7 @@ class IndexesTest(unittest.TestCase): except NotUniqueError: pass - def test_unique_and_primary(self): + def test_primary_save_duplicate_update_existing_object(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. """ @@ -803,7 +808,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 +855,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): @@ -867,7 +872,7 @@ 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 + @requires_mongodb_gte_26 def test_text_indexes(self): class Book(Document): title = DictField() @@ -876,9 +881,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 +894,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 +1018,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..32e3ed29 100644 --- a/tests/document/inheritance.py +++ b/tests/document/inheritance.py @@ -2,14 +2,11 @@ import unittest import warnings -from datetime import datetime - -from tests.fixtures import Base - -from mongoengine import Document, EmbeddedDocument, connect +from mongoengine import (BooleanField, Document, EmbeddedDocument, + EmbeddedDocumentField, GenericReferenceField, + IntField, ReferenceField, StringField, connect) from mongoengine.connection import get_db -from mongoengine.fields import (BooleanField, GenericReferenceField, - IntField, StringField) +from tests.fixtures import Base __all__ = ('InheritanceTest', ) @@ -26,6 +23,27 @@ class InheritanceTest(unittest.TestCase): continue self.db.drop_collection(collection) + def test_constructor_cls(self): + # Ensures _cls is properly set during construction + # and when object gets reloaded (prevent regression of #1950) + class EmbedData(EmbeddedDocument): + data = StringField() + meta = {'allow_inheritance': True} + + class DataDoc(Document): + name = StringField() + embed = EmbeddedDocumentField(EmbedData) + meta = {'allow_inheritance': True} + + test_doc = DataDoc(name='test', embed=EmbedData(data='data')) + assert test_doc._cls == 'DataDoc' + assert test_doc.embed._cls == 'EmbedData' + test_doc.save() + saved_doc = DataDoc.objects.with_id(test_doc.id) + assert test_doc._cls == saved_doc._cls + assert test_doc.embed._cls == saved_doc.embed._cls + test_doc.delete() + def test_superclasses(self): """Ensure that the correct list of superclasses is assembled. """ @@ -258,9 +276,10 @@ class InheritanceTest(unittest.TestCase): name = StringField() # can't inherit because Animal didn't explicitly allow inheritance - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as cm: class Dog(Animal): pass + self.assertIn("Document Animal may not be subclassed", str(cm.exception)) # Check that _cls etc aren't present on simple documents dog = Animal(name='dog').save() @@ -268,7 +287,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. @@ -277,9 +296,10 @@ class InheritanceTest(unittest.TestCase): name = StringField() meta = {'allow_inheritance': True} - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as cm: class Mammal(Animal): meta = {'allow_inheritance': False} + self.assertEqual(str(cm.exception), 'Only direct subclasses of Document may set "allow_inheritance" to False') def test_allow_inheritance_abstract_document(self): """Ensure that abstract documents can set inheritance rules and that @@ -292,13 +312,48 @@ class InheritanceTest(unittest.TestCase): class Animal(FinalDocument): name = StringField() - with self.assertRaises(ValueError): + with self.assertRaises(ValueError) as cm: class Mammal(Animal): pass # 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_using_abstract_class_in_reference_field(self): + # Ensures no regression of #1920 + class AbstractHuman(Document): + meta = {'abstract': True} + + class Dad(AbstractHuman): + name = StringField() + + class Home(Document): + dad = ReferenceField(AbstractHuman) # Referencing the abstract class + address = StringField() + + dad = Dad(name='5').save() + Home(dad=dad, address='street').save() + + home = Home.objects.first() + home.address = 'garbage' + home.save() # Was failing with ValidationError + + def test_abstract_class_referencing_self(self): + # Ensures no regression of #1920 + class Human(Document): + meta = {'abstract': True} + creator = ReferenceField('self', dbref=True) + + class User(Human): + name = StringField() + + user = User(name='John').save() + user2 = User(name='Foo', creator=user).save() + + user2 = User.objects.with_id(user2.id) + user2.name = 'Bar' + user2.save() # Was failing with ValidationError def test_abstract_handle_ids_in_metaclass_properly(self): @@ -358,11 +413,11 @@ class InheritanceTest(unittest.TestCase): meta = {'abstract': True, 'allow_inheritance': False} - bkk = City(continent='asia') - self.assertEqual(None, bkk.pk) + city = City(continent='asia') + self.assertEqual(None, city.pk) # TODO: expected error? Shouldn't we create a new error type? with self.assertRaises(KeyError): - setattr(bkk, 'pk', 1) + setattr(city, 'pk', 1) def test_allow_inheritance_embedded_document(self): """Ensure embedded documents respect inheritance.""" @@ -374,14 +429,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 +489,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 37bbe337..39e47524 100644 --- a/tests/document/instance.py +++ b/tests/document/instance.py @@ -7,10 +7,15 @@ import uuid import weakref from datetime import datetime + +import warnings 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 @@ -22,18 +27,17 @@ from mongoengine.queryset import NULLIFY, Q from mongoengine.context_managers import switch_db, query_counter from mongoengine import signals +from tests.utils import requires_mongodb_gte_26 + TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), '../fields/mongoengine.png') __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() @@ -355,7 +359,7 @@ class InstanceTest(unittest.TestCase): 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() @@ -368,7 +372,7 @@ class InstanceTest(unittest.TestCase): 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): @@ -474,6 +478,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): @@ -519,7 +541,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) @@ -530,21 +552,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.""" @@ -581,10 +596,10 @@ class InstanceTest(unittest.TestCase): # 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): @@ -614,8 +629,8 @@ class InstanceTest(unittest.TestCase): 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.""" @@ -714,12 +729,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) @@ -753,12 +768,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) @@ -826,6 +842,38 @@ class InstanceTest(unittest.TestCase): self.assertDbEqual([dict(other_doc.to_mongo()), dict(doc.to_mongo())]) + @requires_mongodb_gte_26 + def test_modify_with_positional_push(self): + class Content(EmbeddedDocument): + keywords = ListField(StringField()) + + class BlogPost(Document): + tags = ListField(StringField()) + content = EmbeddedDocumentField(Content) + + post = BlogPost.objects.create( + tags=['python'], content=Content(keywords=['ipsum'])) + + self.assertEqual(post.tags, ['python']) + post.modify(push__tags__0=['code', 'mongo']) + self.assertEqual(post.tags, ['code', 'mongo', 'python']) + + # Assert same order of the list items is maintained in the db + self.assertEqual( + BlogPost._get_collection().find_one({'_id': post.pk})['tags'], + ['code', 'mongo', 'python'] + ) + + self.assertEqual(post.content.keywords, ['ipsum']) + post.modify(push__content__keywords__0=['lorem']) + self.assertEqual(post.content.keywords, ['lorem', 'ipsum']) + + # Assert same order of the list items is maintained in the db + self.assertEqual( + BlogPost._get_collection().find_one({'_id': post.pk})['content']['keywords'], + ['lorem', 'ipsum'] + ) + def test_save(self): """Ensure that a document may be saved in the database.""" @@ -1323,6 +1371,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. @@ -1375,6 +1440,60 @@ class InstanceTest(unittest.TestCase): self.assertEqual(person.age, 21) self.assertEqual(person.active, False) + def test__get_changed_fields_same_ids_reference_field_does_not_enters_infinite_loop(self): + # Refers to Issue #1685 + class EmbeddedChildModel(EmbeddedDocument): + id = DictField(primary_key=True) + + class ParentModel(Document): + child = EmbeddedDocumentField( + EmbeddedChildModel) + + emb = EmbeddedChildModel(id={'1': [1]}) + ParentModel(children=emb)._get_changed_fields() + + def test__get_changed_fields_same_ids_reference_field_does_not_enters_infinite_loop(self): + class User(Document): + id = IntField(primary_key=True) + name = StringField() + + class Message(Document): + id = IntField(primary_key=True) + author = ReferenceField(User) + + Message.drop_collection() + + # All objects share the same id, but each in a different collection + user = User(id=1, name='user-name').save() + message = Message(id=1, author=user).save() + + message.author.name = 'tutu' + self.assertEqual(message._get_changed_fields(), []) + self.assertEqual(user._get_changed_fields(), ['name']) + + def test__get_changed_fields_same_ids_embedded(self): + # Refers to Issue #1768 + class User(EmbeddedDocument): + id = IntField() + name = StringField() + + class Message(Document): + id = IntField(primary_key=True) + author = EmbeddedDocumentField(User) + + Message.drop_collection() + + # All objects share the same id, but each in a different collection + user = User(id=1, name='user-name') # .save() + message = Message(id=1, author=user).save() + + message.author.name = 'tutu' + self.assertEqual(message._get_changed_fields(), ['author.name']) + message.save() + + message_fetched = Message.objects.with_id(message.id) + self.assertEqual(message_fetched.author.name, 'tutu') + def test_query_count_when_saving(self): """Ensure references don't cause extra fetches when saving""" class Organization(Document): @@ -1408,9 +1527,9 @@ class InstanceTest(unittest.TestCase): 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: @@ -1790,9 +1909,8 @@ class InstanceTest(unittest.TestCase): 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 @@ -1866,6 +1984,24 @@ 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. @@ -2179,12 +2315,12 @@ class InstanceTest(unittest.TestCase): # 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). @@ -2202,10 +2338,10 @@ class InstanceTest(unittest.TestCase): # 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) @@ -2906,7 +3042,7 @@ class InstanceTest(unittest.TestCase): 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): @@ -2951,6 +3087,24 @@ class InstanceTest(unittest.TestCase): "UNDEFINED", system.nodes["node"].parameters["param"].macros["test"].value) + def test_embedded_document_save_reload_warning(self): + """Relates to #1570""" + class Embedded(EmbeddedDocument): + pass + + class Doc(Document): + emb = EmbeddedDocumentField(Embedded) + + doc = Doc(emb=Embedded()).save() + doc.emb.save() # Make sure its still working + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + with self.assertRaises(DeprecationWarning): + doc.emb.save() + + with self.assertRaises(DeprecationWarning): + doc.emb.reload() + def test_embedded_document_equality(self): class Test(Document): field = StringField(required=True) @@ -2998,36 +3152,36 @@ class InstanceTest(unittest.TestCase): 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): @@ -3076,6 +3230,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): @@ -3149,6 +3361,49 @@ class InstanceTest(unittest.TestCase): person.update(set__height=2.0) + @requires_mongodb_gte_26 + 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, ['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]]) + + 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() 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/__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/fields/fields.py b/tests/fields/fields.py index 7a0ccc25..05810f2c 100644 --- a/tests/fields/fields.py +++ b/tests/fields/fields.py @@ -17,7 +17,7 @@ except ImportError: from decimal import Decimal -from bson import Binary, DBRef, ObjectId, SON +from bson import DBRef, ObjectId, SON try: from bson.int64 import Int64 except ImportError: @@ -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 @@ -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 @@ -150,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) @@ -161,6 +186,31 @@ class FieldTest(MongoDBTestCase): data_to_be_saved = sorted(person.to_mongo().keys()) self.assertEqual(data_to_be_saved, ['age', 'created', 'userid']) + def test_default_value_is_not_used_when_changing_value_to_empty_list_for_strict_doc(self): + """List field with default can be set to the empty list (strict)""" + # Issue #1733 + class Doc(Document): + x = ListField(IntField(), default=lambda: [42]) + + doc = Doc(x=[1]).save() + doc.x = [] + doc.save() + reloaded = Doc.objects.get(id=doc.id) + self.assertEqual(reloaded.x, []) + + def test_default_value_is_not_used_when_changing_value_to_empty_list_for_dyn_doc(self): + """List field with default can be set to the empty list (dynamic)""" + # Issue #1733 + class Doc(DynamicDocument): + x = ListField(IntField(), default=lambda: [42]) + + doc = Doc(x=[1]).save() + doc.x = [] + doc.y = 2 # Was triggering the bug + doc.save() + reloaded = Doc.objects.get(id=doc.id) + self.assertEqual(reloaded.x, []) + def test_default_values_when_deleting_value(self): """Ensure that default field values are used after non-default values are explicitly deleted. @@ -186,7 +236,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) @@ -239,12 +289,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 @@ -262,7 +311,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 @@ -277,12 +326,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. @@ -403,6 +450,16 @@ class FieldTest(MongoDBTestCase): scheme_link.url = 'ws://google.com' scheme_link.validate() + def test_url_allowed_domains(self): + """Allow underscore in domain names. + """ + class Link(Document): + url = URLField() + + link = Link() + link.url = 'https://san_leandro-ca.geebo.com' + link.validate() + def test_int_validation(self): """Ensure that invalid values cannot be assigned to int fields. """ @@ -586,6 +643,8 @@ class FieldTest(MongoDBTestCase): self.assertRaises(ValidationError, person.validate) person.admin = 'Yes' self.assertRaises(ValidationError, person.validate) + person.admin = 'False' + self.assertRaises(ValidationError, person.validate) def test_uuid_field_string(self): """Test UUID fields storing as String @@ -662,6 +721,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 +818,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,139 +917,59 @@ class FieldTest(MongoDBTestCase): ) self.assertEqual(logs.count(), 5) - def test_complexdatetime_storage(self): - """Tests for complex datetime fields - which can handle - microseconds without rounding. - """ + def test_date_usage(self): + """Tests for regular datetime fields""" class LogEntry(Document): - date = ComplexDateTimeField() - date_with_dots = ComplexDateTimeField(separator='.') + date = DateField() 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) + d1 = datetime.datetime(1970, 1, 1, 0, 0, 1) log = LogEntry() log.date = d1 + log.validate() 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) + for query in (d1, d1.isoformat(' ')): + log1 = LogEntry.objects.get(date=query) 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] + if dateutil: + log1 = LogEntry.objects.get(date=d1.isoformat('T')) + self.assertEqual(log, log1) - 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) + # 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") i = 0 - while i < 59: + while i < 19: self.assertTrue(logs[i].date <= logs[i + 1].date) i += 1 logs = LogEntry.objects.order_by("-date") i = 0 - while i < 59: + 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() - - # 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 = ( + ('a', u'Administration'), + ('b', u'Manager'), + ('c', u'Staff'), + ) + class User(Document): pass @@ -931,7 +981,10 @@ 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(GenericLazyReferenceField()) + access_list = ListField(choices=AccessLevelChoices, display_sep=', ') User.drop_collection() BlogPost.drop_collection() @@ -949,6 +1002,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'Administration, Manager') + post.comments = ['a'] self.assertRaises(ValidationError, post.validate) post.comments = 'yay' @@ -969,6 +1033,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 +1054,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. """ @@ -1129,7 +1214,7 @@ class FieldTest(MongoDBTestCase): # aka 'del list[index]' # aka 'operator.delitem(list, index)' reset_post() - del post.info[2] # del from middle ('2') + del post.info[2] # del from middle ('2') self.assertEqual(post.info, ['0', '1', '3', '4', '5']) post.save() post.reload() @@ -1139,7 +1224,7 @@ class FieldTest(MongoDBTestCase): # aka 'del list[i:j]' # aka 'operator.delitem(list, slice(i,j))' reset_post() - del post.info[1:3] # removes '1', '2' + del post.info[1:3] # removes '1', '2' self.assertEqual(post.info, ['0', '3', '4', '5']) post.save() post.reload() @@ -1554,8 +1639,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( @@ -1613,6 +1698,10 @@ class FieldTest(MongoDBTestCase): post.info = {'title': 'test'} post.save() + post = BlogPost() + post.info = {'title': 'dollar_sign', 'details': {'te$t': 'test'}} + post.save() + post = BlogPost() post.info = {'details': {'test': 'test'}} post.save() @@ -1621,12 +1710,15 @@ class FieldTest(MongoDBTestCase): post.info = {'details': {'test': 3}} post.save() - self.assertEqual(BlogPost.objects.count(), 3) + self.assertEqual(BlogPost.objects.count(), 4) self.assertEqual( BlogPost.objects.filter(info__title__exact='test').count(), 1) self.assertEqual( BlogPost.objects.filter(info__details__test__exact='test').count(), 1) + post = BlogPost.objects.filter(info__title__exact='dollar_sign').first() + self.assertIn('te$t', post['info']['details']) + # Confirm handles non strings or non existing keys self.assertEqual( BlogPost.objects.filter(info__details__test__exact=5).count(), 0) @@ -1724,8 +1816,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( @@ -1768,6 +1860,48 @@ class FieldTest(MongoDBTestCase): with self.assertRaises(ValueError): e.update(set__mapping={"somestrings": ["foo", "bar", ]}) + def test_dictfield_with_referencefield_complex_nesting_cases(self): + """Ensure complex nesting inside DictField handles dereferencing of ReferenceField(dbref=True | False)""" + # Relates to Issue #1453 + class Doc(Document): + s = StringField() + + class Simple(Document): + mapping0 = DictField(ReferenceField(Doc, dbref=True)) + mapping1 = DictField(ReferenceField(Doc, dbref=False)) + mapping2 = DictField(ListField(ReferenceField(Doc, dbref=True))) + mapping3 = DictField(ListField(ReferenceField(Doc, dbref=False))) + mapping4 = DictField(DictField(field=ReferenceField(Doc, dbref=True))) + mapping5 = DictField(DictField(field=ReferenceField(Doc, dbref=False))) + mapping6 = DictField(ListField(DictField(ReferenceField(Doc, dbref=True)))) + mapping7 = DictField(ListField(DictField(ReferenceField(Doc, dbref=False)))) + mapping8 = DictField(ListField(DictField(ListField(ReferenceField(Doc, dbref=True))))) + mapping9 = DictField(ListField(DictField(ListField(ReferenceField(Doc, dbref=False))))) + + Doc.drop_collection() + Simple.drop_collection() + + d = Doc(s='aa').save() + e = Simple() + e.mapping0['someint'] = e.mapping1['someint'] = d + e.mapping2['someint'] = e.mapping3['someint'] = [d] + e.mapping4['someint'] = e.mapping5['someint'] = {'d': d} + e.mapping6['someint'] = e.mapping7['someint'] = [{'d': d}] + e.mapping8['someint'] = e.mapping9['someint'] = [{'d': [d]}] + e.save() + + s = Simple.objects.first() + self.assertIsInstance(s.mapping0['someint'], Doc) + self.assertIsInstance(s.mapping1['someint'], Doc) + self.assertIsInstance(s.mapping2['someint'][0], Doc) + self.assertIsInstance(s.mapping3['someint'][0], Doc) + self.assertIsInstance(s.mapping4['someint']['d'], Doc) + self.assertIsInstance(s.mapping5['someint']['d'], Doc) + self.assertIsInstance(s.mapping6['someint'][0]['d'], Doc) + self.assertIsInstance(s.mapping7['someint'][0]['d'], Doc) + self.assertIsInstance(s.mapping8['someint'][0]['d'][0], Doc) + self.assertIsInstance(s.mapping9['someint'][0]['d'][0], Doc) + def test_mapfield(self): """Ensure that the MapField handles the declared type.""" class Simple(Document): @@ -1809,8 +1943,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 @@ -1965,6 +2099,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. @@ -2506,7 +2649,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() @@ -2514,7 +2657,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. @@ -2749,7 +2892,32 @@ class FieldTest(MongoDBTestCase): doc = Doc.objects.get(ref=DBRef('doc', doc1.pk)) self.assertEqual(doc, doc2) - def test_generic_reference_filter_by_objectid(self): + def test_generic_reference_is_not_tracked_in_parent_doc(self): + """Ensure that modifications of related documents (through generic reference) don't influence + the owner changed fields (#1934) + """ + class Doc1(Document): + name = StringField() + + class Doc2(Document): + ref = GenericReferenceField() + refs = ListField(GenericReferenceField()) + + Doc1.drop_collection() + Doc2.drop_collection() + + doc1 = Doc1(name='garbage1').save() + doc11 = Doc1(name='garbage11').save() + doc2 = Doc2(ref=doc1, refs=[doc11]).save() + + doc2.ref.name = 'garbage2' + self.assertEqual(doc2._get_changed_fields(), []) + + doc2.refs[0].name = 'garbage3' + self.assertEqual(doc2._get_changed_fields(), []) + self.assertEqual(doc2._delta(), ({}, {})) + + def test_generic_reference_field(self): """Ensure we can search for a specific generic reference by providing its DBRef. """ @@ -2761,87 +2929,11 @@ 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) - def test_binary_fields(self): - """Ensure that binary fields can be stored and retrieved. - """ - class Attachment(Document): - content_type = StringField() - blob = BinaryField() - - BLOB = six.b('\xe6\x00\xc4\xff\x07') - MIME_TYPE = 'application/octet-stream' - - Attachment.drop_collection() - - attachment = Attachment(content_type=MIME_TYPE, blob=BLOB) - attachment.save() - - attachment_1 = Attachment.objects().first() - self.assertEqual(MIME_TYPE, attachment_1.content_type) - self.assertEqual(BLOB, six.binary_type(attachment_1.blob)) - - def test_binary_validation(self): - """Ensure that invalid values cannot be assigned to binary fields. - """ - class Attachment(Document): - blob = BinaryField() - - class AttachmentRequired(Document): - blob = BinaryField(required=True) - - class AttachmentSizeLimit(Document): - blob = BinaryField(max_bytes=4) - - Attachment.drop_collection() - AttachmentRequired.drop_collection() - AttachmentSizeLimit.drop_collection() - - attachment = Attachment() - attachment.validate() - attachment.blob = 2 - self.assertRaises(ValidationError, attachment.validate) - - attachment_required = AttachmentRequired() - self.assertRaises(ValidationError, attachment_required.validate) - attachment_required.blob = Binary(six.b('\xe6\x00\xc4\xff\x07')) - attachment_required.validate() - - attachment_size_limit = AttachmentSizeLimit( - blob=six.b('\xe6\x00\xc4\xff\x07')) - self.assertRaises(ValidationError, attachment_size_limit.validate) - attachment_size_limit.blob = six.b('\xe6\x00\xc4\xff') - attachment_size_limit.validate() - - def test_binary_field_primary(self): - class Attachment(Document): - id = BinaryField(primary_key=True) - - Attachment.drop_collection() - binary_id = uuid.uuid4().bytes - att = Attachment(id=binary_id).save() - self.assertEqual(1, Attachment.objects.count()) - self.assertEqual(1, Attachment.objects.filter(id=att.id).count()) - 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): - id = BinaryField(primary_key=True) - - Attachment.drop_collection() - binary_id = uuid.uuid4().bytes - att = Attachment(id=binary_id).save() - self.assertEqual(1, Attachment.objects.filter(id=binary_id).count()) - att.delete() - self.assertEqual(0, Attachment.objects.count()) - def test_choices_allow_using_sets_as_choices(self): """Ensure that sets can be used when setting choices """ @@ -3364,13 +3456,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.""" @@ -3395,7 +3487,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 @@ -3422,7 +3514,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.""" @@ -3448,18 +3540,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') @@ -3575,7 +3666,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 @@ -3602,10 +3693,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): @@ -3677,7 +3768,7 @@ class FieldTest(MongoDBTestCase): assert isinstance(doc.field, ToEmbedChild) assert doc.field == to_embed_child - def test_invalid_dict_value(self): + def test_dict_field_invalid_dict_value(self): class DictFieldTest(Document): dictionary = DictField(required=True) @@ -3691,6 +3782,22 @@ class FieldTest(MongoDBTestCase): test.dictionary # Just access to test getter self.assertRaises(ValidationError, test.validate) + def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self): + class DictFieldTest(Document): + dictionary = DictField(required=True) + + DictFieldTest.drop_collection() + + class Embedded(EmbeddedDocument): + name = StringField() + + embed = Embedded(name='garbage') + doc = DictFieldTest(dictionary=embed) + with self.assertRaises(ValidationError) as ctx_err: + doc.validate() + self.assertIn("'dictionary'", str(ctx_err.exception)) + self.assertIn('Only dictionaries may be used in a DictField', str(ctx_err.exception)) + def test_cls_field(self): class Animal(Document): meta = {'allow_inheritance': True} @@ -3755,8 +3862,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): @@ -3789,6 +3896,28 @@ class EmbeddedDocumentListFieldTestCase(MongoDBTestCase): self.Comments(author='user3', message='message1') ]).save() + def test_fails_upon_validate_if_provide_a_doc_instead_of_a_list_of_doc(self): + # Relates to Issue #1464 + comment = self.Comments(author='John') + + class Title(Document): + content = StringField() + + # Test with an embeddedDocument instead of a list(embeddedDocument) + # It's an edge case but it used to fail with a vague error, making it difficult to troubleshoot it + post = self.BlogPost(comments=comment) + with self.assertRaises(ValidationError) as ctx_err: + post.validate() + self.assertIn("'comments'", str(ctx_err.exception)) + self.assertIn('Only lists and tuples may be used in a list field', str(ctx_err.exception)) + + # Test with a Document + post = self.BlogPost(comments=Title(content='garbage')) + with self.assertRaises(ValidationError) as e: + post.validate() + self.assertIn("'comments'", str(ctx_err.exception)) + self.assertIn('Only lists and tuples may be used in a list field', str(ctx_err.exception)) + def test_no_keyword_filter(self): """ Tests the filter method of a List of Embedded Documents @@ -4206,6 +4335,45 @@ 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): @@ -4269,7 +4437,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): @@ -4356,6 +4524,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 = ( @@ -4541,7 +4754,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): @@ -4595,7 +4808,699 @@ 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): + 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) + + 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): + 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) + + 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) + + +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__': diff --git a/tests/fields/file_tests.py b/tests/fields/file_tests.py index 8364d5ef..213e889c 100644 --- a/tests/fields/file_tests.py +++ b/tests/fields/file_tests.py @@ -53,8 +53,8 @@ class FileTest(MongoDBTestCase): putfile.save() result = PutFile.objects.first() - self.assertTrue(putfile == result) - self.assertEqual("%s" % result.the_file, "") + 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) result.the_file.delete() # Remove file from GridFS @@ -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/fields/test_binary_field.py b/tests/fields/test_binary_field.py new file mode 100644 index 00000000..8af75d4e --- /dev/null +++ b/tests/fields/test_binary_field.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +import uuid + +from nose.plugins.skip import SkipTest +import six + +from bson import Binary + +from mongoengine import * +from tests.utils import MongoDBTestCase + +BIN_VALUE = six.b('\xa9\xf3\x8d(\xd7\x03\x84\xb4k[\x0f\xe3\xa2\x19\x85p[J\xa3\xd2>\xde\xe6\x87\xb1\x7f\xc6\xe6\xd9r\x18\xf5') + + +class TestBinaryField(MongoDBTestCase): + def test_binary_fields(self): + """Ensure that binary fields can be stored and retrieved. + """ + class Attachment(Document): + content_type = StringField() + blob = BinaryField() + + BLOB = six.b('\xe6\x00\xc4\xff\x07') + MIME_TYPE = 'application/octet-stream' + + Attachment.drop_collection() + + attachment = Attachment(content_type=MIME_TYPE, blob=BLOB) + attachment.save() + + attachment_1 = Attachment.objects().first() + self.assertEqual(MIME_TYPE, attachment_1.content_type) + self.assertEqual(BLOB, six.binary_type(attachment_1.blob)) + + def test_validation_succeeds(self): + """Ensure that valid values can be assigned to binary fields. + """ + class AttachmentRequired(Document): + blob = BinaryField(required=True) + + class AttachmentSizeLimit(Document): + blob = BinaryField(max_bytes=4) + + attachment_required = AttachmentRequired() + self.assertRaises(ValidationError, attachment_required.validate) + attachment_required.blob = Binary(six.b('\xe6\x00\xc4\xff\x07')) + attachment_required.validate() + + _5_BYTES = six.b('\xe6\x00\xc4\xff\x07') + _4_BYTES = six.b('\xe6\x00\xc4\xff') + self.assertRaises(ValidationError, AttachmentSizeLimit(blob=_5_BYTES).validate) + AttachmentSizeLimit(blob=_4_BYTES).validate() + + def test_validation_fails(self): + """Ensure that invalid values cannot be assigned to binary fields.""" + + class Attachment(Document): + blob = BinaryField() + + for invalid_data in (2, u'Im_a_unicode', ['some_str']): + self.assertRaises(ValidationError, Attachment(blob=invalid_data).validate) + + def test__primary(self): + class Attachment(Document): + id = BinaryField(primary_key=True) + + Attachment.drop_collection() + binary_id = uuid.uuid4().bytes + att = Attachment(id=binary_id).save() + self.assertEqual(1, Attachment.objects.count()) + self.assertEqual(1, Attachment.objects.filter(id=att.id).count()) + att.delete() + self.assertEqual(0, Attachment.objects.count()) + + def test_primary_filter_by_binary_pk_as_str(self): + raise SkipTest("Querying by id as string is not currently supported") + + class Attachment(Document): + id = BinaryField(primary_key=True) + + Attachment.drop_collection() + binary_id = uuid.uuid4().bytes + att = Attachment(id=binary_id).save() + self.assertEqual(1, Attachment.objects.filter(id=binary_id).count()) + att.delete() + self.assertEqual(0, Attachment.objects.count()) + + def test_match_querying_with_bytes(self): + class MyDocument(Document): + bin_field = BinaryField() + + MyDocument.drop_collection() + + doc = MyDocument(bin_field=BIN_VALUE).save() + matched_doc = MyDocument.objects(bin_field=BIN_VALUE).first() + self.assertEqual(matched_doc.id, doc.id) + + def test_match_querying_with_binary(self): + class MyDocument(Document): + bin_field = BinaryField() + + MyDocument.drop_collection() + + doc = MyDocument(bin_field=BIN_VALUE).save() + + matched_doc = MyDocument.objects(bin_field=Binary(BIN_VALUE)).first() + self.assertEqual(matched_doc.id, doc.id) + + def test_modify_operation__set(self): + """Ensures no regression of bug #1127""" + class MyDocument(Document): + some_field = StringField() + bin_field = BinaryField() + + MyDocument.drop_collection() + + doc = MyDocument.objects(some_field='test').modify( + upsert=True, new=True, + set__bin_field=BIN_VALUE + ) + self.assertEqual(doc.some_field, 'test') + if six.PY3: + self.assertEqual(doc.bin_field, BIN_VALUE) + else: + self.assertEqual(doc.bin_field, Binary(BIN_VALUE)) + + def test_update_one(self): + """Ensures no regression of bug #1127""" + class MyDocument(Document): + bin_field = BinaryField() + + MyDocument.drop_collection() + + bin_data = six.b('\xe6\x00\xc4\xff\x07') + doc = MyDocument(bin_field=bin_data).save() + + n_updated = MyDocument.objects(bin_field=bin_data).update_one(bin_field=BIN_VALUE) + self.assertEqual(n_updated, 1) + fetched = MyDocument.objects.with_id(doc.id) + if six.PY3: + self.assertEqual(fetched.bin_field, BIN_VALUE) + else: + self.assertEqual(fetched.bin_field, Binary(BIN_VALUE)) 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 * diff --git a/tests/queryset/field_list.py b/tests/queryset/field_list.py index d1277e06..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() @@ -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) diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py index 51a32382..fd8c9b0f 100644 --- a/tests/queryset/geo.py +++ b/tests/queryset/geo.py @@ -3,7 +3,7 @@ import unittest from mongoengine import * -from tests.utils import MongoDBTestCase, needs_mongodb_v3 +from tests.utils import MongoDBTestCase, requires_mongodb_gte_3 __all__ = ("GeoQueriesTest",) @@ -72,7 +72,7 @@ class GeoQueriesTest(MongoDBTestCase): # $minDistance was added in MongoDB v2.6, but continued being buggy # until v3.0; skip for older versions - @needs_mongodb_v3 + @requires_mongodb_gte_3 def test_near_and_min_distance(self): """Ensure the "min_distance" operator works alongside the "near" operator. @@ -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] @@ -245,7 +245,7 @@ class GeoQueriesTest(MongoDBTestCase): # $minDistance was added in MongoDB v2.6, but continued being buggy # until v3.0; skip for older versions - @needs_mongodb_v3 + @requires_mongodb_gte_3 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. @@ -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 @@ -329,7 +329,7 @@ class GeoQueriesTest(MongoDBTestCase): self._test_embedded(point_field_class=PointField) # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039 - @needs_mongodb_v3 + @requires_mongodb_gte_3 def test_spherical_geospatial_operators(self): """Ensure that spherical geospatial queries are working.""" class Point(Document): @@ -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() diff --git a/tests/queryset/modify.py b/tests/queryset/modify.py index 607937f6..4b7c3da2 100644 --- a/tests/queryset/modify.py +++ b/tests/queryset/modify.py @@ -1,6 +1,8 @@ import unittest -from mongoengine import connect, Document, IntField +from mongoengine import connect, Document, IntField, StringField, ListField + +from tests.utils import requires_mongodb_gte_26 __all__ = ("FindAndModifyTest",) @@ -94,6 +96,37 @@ class FindAndModifyTest(unittest.TestCase): self.assertEqual(old_doc.to_mongo(), {"_id": 1}) self.assertDbEqual([{"_id": 0, "value": 0}, {"_id": 1, "value": -1}]) + @requires_mongodb_gte_26 + def test_modify_with_push(self): + class BlogPost(Document): + tags = ListField(StringField()) + + BlogPost.drop_collection() + + blog = BlogPost.objects.create() + + # Push a new tag via modify with new=False (default). + 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(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(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(id=blog.id).modify( + push__tags__1=['go', 'rust'], + new=True) + self.assertEqual(blog.tags, ['python', 'go', 'rust', 'code', 'java']) + if __name__ == '__main__': unittest.main() diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py index d97b307d..d3a2418a 100644 --- a/tests/queryset/queryset.py +++ b/tests/queryset/queryset.py @@ -3,12 +3,14 @@ import datetime import unittest 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 +from pymongo.results import UpdateResult import six from mongoengine import * @@ -19,8 +21,7 @@ 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 - +from tests.utils import requires_mongodb_gte_26, skip_pymongo3, get_mongodb_version, MONGODB_32 __all__ = ("QuerySetTest",) @@ -28,10 +29,8 @@ __all__ = ("QuerySetTest",) class db_ops_tracker(query_counter): def get_ops(self): - ignore_query = { - 'ns': {'$ne': '%s.system.indexes' % self.db.name}, - 'command.count': {'$ne': 'system.profile'} - } + ignore_query = dict(self._ignored_query) + ignore_query['command.count'] = {'$ne': 'system.profile'} # Ignore the query issued by query_counter return list(self.db.system.profile.find(ignore_query)) @@ -54,14 +53,15 @@ class QuerySetTest(unittest.TestCase): self.PersonMeta = PersonMeta self.Person = Person + self.mongodb_version = get_mongodb_version() + 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): @@ -87,8 +87,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') @@ -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) @@ -223,7 +228,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) @@ -395,13 +400,17 @@ class QuerySetTest(unittest.TestCase): self.Person.drop_collection() write_concern = {"fsync": True} - author = self.Person.objects.create(name='Test User') author.save(write_concern=write_concern) + # Ensure no regression of #1958 + author = self.Person(name='Test User2') + author.save(write_concern=None) # will default to {w: 1} + result = self.Person.objects.update( set__name='Ross', write_concern={"w": 1}) - self.assertEqual(result, 1) + + self.assertEqual(result, 2) result = self.Person.objects.update( set__name='Ross', write_concern={"w": 0}) self.assertEqual(result, None) @@ -571,7 +580,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(post.comments[0].by, 'joe') self.assertEqual(post.comments[0].votes.score, 4) - @needs_mongodb_v26 + @requires_mongodb_gte_26 def test_update_min_max(self): class Scores(Document): high_score = IntField() @@ -589,6 +598,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) + @requires_mongodb_gte_26 + 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): @@ -656,14 +679,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.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, dict)) - self.assertTrue(result["updatedExisting"]) + self.assertIsInstance(result, UpdateResult) + self.assertTrue(result.raw_result["updatedExisting"]) self.Person(name="Bob", age=20).save() result = self.Person.objects(name="Bob").update( @@ -685,38 +708,38 @@ class QuerySetTest(unittest.TestCase): self.assertRaises(ValidationError, Doc.objects().update, ed_f__str_f=1, upsert=True) def test_update_related_models(self): - class TestPerson(Document): - name = StringField() + class TestPerson(Document): + name = StringField() - class TestOrganization(Document): - name = StringField() - owner = ReferenceField(TestPerson) + class TestOrganization(Document): + name = StringField() + owner = ReferenceField(TestPerson) - TestPerson.drop_collection() - TestOrganization.drop_collection() + TestPerson.drop_collection() + TestOrganization.drop_collection() - p = TestPerson(name='p1') - p.save() - o = TestOrganization(name='o1') - o.save() + p = TestPerson(name='p1') + p.save() + o = TestOrganization(name='o1') + o.save() - o.owner = p - p.name = 'p2' + o.owner = p + p.name = 'p2' - self.assertEqual(o._get_changed_fields(), ['owner']) - self.assertEqual(p._get_changed_fields(), ['name']) + self.assertEqual(o._get_changed_fields(), ['owner']) + self.assertEqual(p._get_changed_fields(), ['name']) - o.save() + o.save() - self.assertEqual(o._get_changed_fields(), []) - self.assertEqual(p._get_changed_fields(), ['name']) # Fails; it's empty + self.assertEqual(o._get_changed_fields(), []) + self.assertEqual(p._get_changed_fields(), ['name']) # Fails; it's empty - # This will do NOTHING at all, even though we changed the name - p.save() + # This will do NOTHING at all, even though we changed the name + p.save() - p.reload() + p.reload() - self.assertEqual(p.name, 'p2') # Fails; it's still `p1` + self.assertEqual(p.name, 'p2') # Fails; it's still `p1` def test_upsert(self): self.Person.drop_collection() @@ -793,8 +816,8 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(record.embed.field, 2) def test_bulk_insert(self): - """Ensure that bulk insert works - """ + """Ensure that bulk insert works""" + MONGO_VER = self.mongodb_version class Comment(EmbeddedDocument): name = StringField() @@ -812,42 +835,43 @@ class QuerySetTest(unittest.TestCase): # get MongoDB version info connection = get_connection() info = connection.test.command('buildInfo') - mongodb_version = tuple([int(i) for i in info['version'].split('.')]) # Recreates the collection self.assertEqual(0, Blog.objects.count()) + comment1 = Comment(name='testa') + comment2 = Comment(name='testb') + post1 = Post(comments=[comment1, comment2]) + post2 = Post(comments=[comment2, comment2]) + + # Check bulk insert using load_bulk=False + blogs = [Blog(title="%s" % i, posts=[post1, post2]) + for i in range(99)] with query_counter() as q: self.assertEqual(q, 0) - - comment1 = Comment(name='testa') - comment2 = Comment(name='testb') - post1 = Post(comments=[comment1, comment2]) - post2 = Post(comments=[comment2, comment2]) - - blogs = [] - for i in range(1, 100): - 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, 1) + + if MONGO_VER == MONGODB_32: + self.assertEqual(q, 1) # 1 entry containing the list of inserts else: - # profiling logs each doc now in the bulk op - self.assertEqual(q, 99) + self.assertEqual(q, len(blogs)) # 1 entry per doc inserted + + self.assertEqual(Blog.objects.count(), len(blogs)) Blog.drop_collection() Blog.ensure_indexes() + # Check bulk insert using load_bulk=True + blogs = [Blog(title="%s" % i, posts=[post1, post2]) + for i in range(99)] with query_counter() as q: self.assertEqual(q, 0) - Blog.objects.insert(blogs) - if mongodb_version < (2, 6): - self.assertEqual(q, 2) # 1 for insert, and 1 for in bulk fetch + + if MONGO_VER == MONGODB_32: + self.assertEqual(q, 2) # 1 for insert 1 for fetch else: - # 99 for insert, and 1 for in bulk fetch - self.assertEqual(q, 100) + self.assertEqual(q, len(blogs)+1) # + 1 to fetch all docs Blog.drop_collection() @@ -864,30 +888,21 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Blog.objects.count(), 2) # test inserting an existing document (shouldn't be allowed) - with self.assertRaises(OperationError): + with self.assertRaises(OperationError) as cm: blog = Blog.objects.first() Blog.objects.insert(blog) + self.assertEqual(str(cm.exception), 'Some documents have ObjectIds use doc.update() instead') # test inserting a query set - with self.assertRaises(OperationError): - blogs = Blog.objects - Blog.objects.insert(blogs) + with self.assertRaises(OperationError) as cm: + blogs_qs = Blog.objects + Blog.objects.insert(blogs_qs) + self.assertEqual(str(cm.exception), 'Some documents have ObjectIds use doc.update() instead') - # insert a new doc + # insert 1 new doc new_post = Blog(title="code123", id=ObjectId()) Blog.objects.insert(new_post) - class Author(Document): - pass - - # try inserting a different document class - with self.assertRaises(OperationError): - Blog.objects.insert(Author()) - - # try inserting a non-document - with self.assertRaises(OperationError): - Blog.objects.insert("HELLO WORLD") - Blog.drop_collection() blog1 = Blog(title="code", posts=[post1, post2]) @@ -898,23 +913,69 @@ class QuerySetTest(unittest.TestCase): Blog.drop_collection() blog1 = Blog(title="code", posts=[post1, post2]) obj_id = Blog.objects.insert(blog1, load_bulk=False) - self.assertEqual(obj_id.__class__.__name__, 'ObjectId') + self.assertIsInstance(obj_id, ObjectId) Blog.drop_collection() post3 = Post(comments=[comment1, comment1]) blog1 = Blog(title="foo", posts=[post1, post2]) blog2 = Blog(title="bar", posts=[post2, post3]) - blog3 = Blog(title="baz", posts=[post1, post2]) Blog.objects.insert([blog1, blog2]) with self.assertRaises(NotUniqueError): - Blog.objects.insert([blog2, blog3]) + Blog.objects.insert(Blog(title=blog2.title)) self.assertEqual(Blog.objects.count(), 2) - Blog.objects.insert([blog2, blog3], - write_concern={"w": 0, 'continue_on_error': True}) - self.assertEqual(Blog.objects.count(), 3) + def test_bulk_insert_different_class_fails(self): + class Blog(Document): + pass + + class Author(Document): + pass + + # try inserting a different document class + with self.assertRaises(OperationError): + Blog.objects.insert(Author()) + + def test_bulk_insert_with_wrong_type(self): + class Blog(Document): + name = StringField() + + Blog.drop_collection() + Blog(name='test').save() + + with self.assertRaises(OperationError): + Blog.objects.insert("HELLO WORLD") + + with self.assertRaises(OperationError): + Blog.objects.insert({'name': 'garbage'}) + + def test_bulk_insert_update_input_document_ids(self): + class Comment(Document): + idx = IntField() + + Comment.drop_collection() + + # Test with bulk + comments = [Comment(idx=idx) for idx in range(20)] + for com in comments: + self.assertIsNone(com.id) + + returned_comments = Comment.objects.insert(comments, load_bulk=True) + + for com in comments: + self.assertIsInstance(com.id, ObjectId) + + input_mapping = {com.id: com.idx for com in comments} + saved_mapping = {com.id: com.idx for com in returned_comments} + self.assertEqual(input_mapping, saved_mapping) + + Comment.drop_collection() + + # Test with just one + comment = Comment(idx=0) + inserted_comment_id = Comment.objects.insert(comment, load_bulk=False) + self.assertEqual(comment.id, inserted_comment_id) def test_get_changed_fields_query_count(self): """Make sure we don't perform unnecessary db operations when @@ -989,11 +1050,11 @@ 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) - @needs_mongodb_v26 + @requires_mongodb_gte_26 @skip_pymongo3 def test_cursor_args(self): """Ensures the cursor args can be set as expected @@ -1056,10 +1117,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] @@ -1198,6 +1259,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) @@ -1245,6 +1314,9 @@ 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' + class BlogPost(Document): title = StringField() published_date = DateTimeField() @@ -1260,7 +1332,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']['$orderby'], + q.get_ops()[0]['query'][ORDER_BY_KEY], {'published_date': -1} ) @@ -1268,14 +1340,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.assertFalse('$orderby' in q.get_ops()[0]['query']) + self.assertNotIn(ORDER_BY_KEY, 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'], + q.get_ops()[0]['query'][ORDER_BY_KEY], {'published_date': 1} ) @@ -1284,11 +1356,14 @@ 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(ORDER_BY_KEY, q.get_ops()[0]['query']) 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' + class BlogPost(Document): title = StringField() published_date = DateTimeField() @@ -1303,13 +1378,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(ORDER_BY_KEY, 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(ORDER_BY_KEY, q.get_ops()[0]['query']) def test_find_embedded(self): """Ensure that an embedded document is properly returned from @@ -1331,15 +1406,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 @@ -1457,7 +1532,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 = [ @@ -1847,21 +1922,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) @@ -1878,13 +1948,63 @@ 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) + 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) @@ -1896,13 +2016,64 @@ 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) + + @requires_mongodb_gte_26 + 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.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"]) + post.reload() + self.assertEqual(post.tags, ['mongodb', 'python', 'code']) + + BlogPost.objects.filter(id=post.id).update(set__tags__2="java") + 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_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. """ @@ -2045,6 +2216,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): @@ -2138,6 +2326,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): @@ -2302,19 +2508,28 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(names, ['User A', 'User B', 'User C']) def test_comment(self): - """Make sure adding a comment to the query works.""" + """Make sure adding a comment to the query gets added to the query""" + MONGO_VER = self.mongodb_version + QUERY_KEY = 'filter' if MONGO_VER == MONGODB_32 else '$query' + COMMENT_KEY = 'comment' if MONGO_VER == MONGODB_32 else '$comment' + class User(Document): 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_KEY], {'age': {'$gte': 18}}) + self.assertEqual(op['query'][COMMENT_KEY], 'looking for an adult') def test_map_reduce(self): """Ensure map/reduce is both mapping and reducing. @@ -3110,7 +3325,7 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(Foo.objects.distinct("bar"), [bar]) - @needs_mongodb_v26 + @requires_mongodb_gte_26 def test_text_indexes(self): class News(Document): title = StringField() @@ -3126,8 +3341,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() @@ -3161,15 +3376,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') @@ -3197,7 +3412,7 @@ class QuerySetTest(unittest.TestCase): 'brasil').order_by('$text_score').first() self.assertEqual(item.get_text_score(), max_text_score) - @needs_mongodb_v26 + @requires_mongodb_gte_26 def test_distinct_handles_references_to_alias(self): register_connection('testdb', 'mongoenginetest2') @@ -3464,39 +3679,12 @@ 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) 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. """ @@ -3522,13 +3710,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() @@ -3548,7 +3736,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() @@ -3573,7 +3761,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() @@ -3620,7 +3808,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() @@ -3648,7 +3836,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() @@ -3739,17 +3927,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() @@ -3839,7 +4027,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. @@ -3863,13 +4051,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] }') @@ -3877,8 +4065,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)) @@ -4260,7 +4448,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} @@ -4269,7 +4457,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): @@ -4353,6 +4541,25 @@ class QuerySetTest(unittest.TestCase): self.assertEqual(bars._cursor._Cursor__read_preference, ReadPreference.SECONDARY_PREFERRED) + @requires_mongodb_gte_26 + 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): @@ -4421,14 +4628,12 @@ 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') + id = StringField(primary_key=True) name = StringField() age = IntField() price = DecimalField() @@ -4436,9 +4641,10 @@ class QuerySetTest(unittest.TestCase): User.drop_collection() - User.objects.create(name="Bob Dole", age=89, price=Decimal('1.11')) + User.objects.create(id='Bob', name="Bob Dole", age=89, price=Decimal('1.11')) User.objects.create( - name="Barack Obama", + id='Barak', + name="Barak Obama", age=51, price=Decimal('2.22'), last_login=LastLogin( @@ -4462,28 +4668,48 @@ 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') + self.assertEqual(results[1]['name'], 'Barak Obama') self.assertEqual(results[1]['price'], 2.22) 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], { + '_id': 'Bob', 'name': 'Bob Dole' }) self.assertEqual(results[1], { - 'name': 'Barack Obama', + '_id': 'Barak', + 'name': 'Barak Obama', 'last_login': { 'location': 'White House', 'ip': '104.107.108.116' } }) + def test_as_pymongo_returns_cls_attribute_when_using_inheritance(self): + class User(Document): + name = StringField() + meta = {'allow_inheritance': True} + + User.drop_collection() + + user = User(name="Bob Dole").save() + result = User.objects.as_pymongo().first() + self.assertEqual( + result, + { + '_cls': 'User', + '_id': user.id, + 'name': 'Bob Dole' + } + ) + def test_as_pymongo_json_limit_fields(self): class User(Document): @@ -4499,19 +4725,49 @@ class QuerySetTest(unittest.TestCase): serialized_user = User.objects.exclude( 'password_salt', 'password_hash').as_pymongo()[0] - self.assertEqual(set(['_id', 'email']), set(serialized_user.keys())) + self.assertEqual({'_id', 'email'}, set(serialized_user.keys())) serialized_user = User.objects.exclude( 'id', 'password_salt', 'password_hash').to_json() self.assertEqual('[{"email": "ross@example.com"}]', serialized_user) - serialized_user = User.objects.exclude( - 'password_salt').only('email').as_pymongo()[0] - self.assertEqual(set(['email']), set(serialized_user.keys())) + serialized_user = User.objects.only('email').as_pymongo()[0] + self.assertEqual({'_id', 'email'}, set(serialized_user.keys())) serialized_user = User.objects.exclude( - 'password_salt').only('email').to_json() - self.assertEqual('[{"email": "ross@example.com"}]', serialized_user) + 'password_salt').only('email').as_pymongo()[0] + self.assertEqual({'_id', 'email'}, set(serialized_user.keys())) + + serialized_user = User.objects.exclude( + 'password_salt', 'id').only('email').as_pymongo()[0] + self.assertEqual({'email'}, set(serialized_user.keys())) + + serialized_user = User.objects.exclude( + 'password_salt', 'id').only('email').to_json() + self.assertEqual('[{"email": "ross@example.com"}]', + serialized_user) + + def test_only_after_count(self): + """Test that only() works after count()""" + + class User(Document): + name = StringField() + age = IntField() + address = StringField() + User.drop_collection() + user = User(name="User", age=50, + address="Moscow, Russia").save() + + user_queryset = User.objects(age=50) + + result = user_queryset.only("name", "age").as_pymongo().first() + self.assertEqual(result, {"_id": user.id, "name": "User", "age": 50}) + + result = user_queryset.count() + self.assertEqual(result, 1) + + result = user_queryset.only("name", "age").as_pymongo().first() + self.assertEqual(result, {"_id": user.id, "name": "User", "age": 50}) def test_no_dereference(self): @@ -4529,12 +4785,76 @@ 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)) + qs_user = qs.first() + + self.assertIsInstance(qs.first().organization, Organization) + + self.assertIsInstance(qs.no_dereference().first().organization, DBRef) + + self.assertIsInstance(qs_user.organization, Organization) + self.assertIsInstance(qs.first().organization, Organization) + + def test_no_dereference_internals(self): + # Test the internals on which queryset.no_dereference relies on + class Organization(Document): + name = StringField() + + class User(Document): + organization = ReferenceField(Organization) + + User.drop_collection() + Organization.drop_collection() + + cls_organization_field = User.organization + self.assertTrue(cls_organization_field._auto_dereference, True) # default + + org = Organization(name="whatever").save() + User(organization=org).save() + + qs_no_deref = User.objects().no_dereference() + user_no_deref = qs_no_deref.first() + self.assertFalse(qs_no_deref._auto_dereference) + + # Make sure the instance field is different from the class field + instance_org_field = user_no_deref._fields['organization'] + self.assertIsNot(instance_org_field, cls_organization_field) + self.assertFalse(instance_org_field._auto_dereference) + + self.assertIsInstance(user_no_deref.organization, DBRef) + self.assertTrue(cls_organization_field._auto_dereference, True) # Make sure the class Field wasn't altered + + def test_no_dereference_no_side_effect_on_existing_instance(self): + # Relates to issue #1677 - ensures no regression of the bug + + class Organization(Document): + name = StringField() + + class User(Document): + organization = ReferenceField(Organization) + organization_gen = GenericReferenceField() + + User.drop_collection() + Organization.drop_collection() + + org = Organization(name="whatever").save() + User(organization=org, + organization_gen=org).save() + + qs = User.objects() + user = qs.first() + + qs_no_deref = User.objects().no_dereference() + user_no_deref = qs_no_deref.first() + + # ReferenceField + no_derf_org = user_no_deref.organization # was triggering the bug + self.assertIsInstance(no_derf_org, DBRef) + self.assertIsInstance(user.organization, Organization) + + # GenericReferenceField + no_derf_org_gen = user_no_deref.organization_gen + self.assertIsInstance(no_derf_org_gen, dict) + self.assertIsInstance(user.organization_gen, Organization) def test_no_dereference_embedded_doc(self): @@ -4550,7 +4870,7 @@ class QuerySetTest(unittest.TestCase): members = ListField(EmbeddedDocumentField(Member)) ceo = ReferenceField(User) member = EmbeddedDocumentField(Member) - admin = ListField(ReferenceField(User)) + admins = ListField(ReferenceField(User)) Organization.drop_collection() User.drop_collection() @@ -4560,16 +4880,22 @@ class QuerySetTest(unittest.TestCase): member = Member(name="Flash", user=user) - company = Organization(name="Mongo Inc", ceo=user, member=member) - company.admin.append(user) - company.members.append(member) + company = Organization(name="Mongo Inc", + ceo=user, + member=member, + admins=[user], + members=[member]) company.save() - result = Organization.objects().no_dereference().first() + org = 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.assertNotEqual(id(org._fields['admins']), id(Organization.admins)) + self.assertFalse(org._fields['admins']._auto_dereference) + + admin = org.admins[0] + self.assertIsInstance(admin, DBRef) + self.assertIsInstance(org.member.user, DBRef) + self.assertIsInstance(org.members[0].user, DBRef) def test_cached_queryset(self): class Person(Document): @@ -4764,6 +5090,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): @@ -4871,36 +5221,39 @@ 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' class Person(Document): name = StringField() Person.drop_collection() + Person(name="Test").save() + # Check that bool(queryset) does not uses the orderby qs = Person.objects.order_by('name') - with query_counter() as q: - if qs: + if bool(qs): pass op = q.db.system.profile.find({"ns": {"$ne": "%s.system.indexes" % q.db.name}})[0] - self.assertFalse('$orderby' in op['query'], - 'BaseQuerySet cannot use orderby in if stmt') + self.assertNotIn(ORDER_BY_KEY, op['query']) + # Check that normal query uses orderby + qs2 = Person.objects.order_by('name') with query_counter() as p: - for x in qs: + for x in qs2: pass 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(ORDER_BY_KEY, op['query']) def test_bool_with_ordering_from_meta_dict(self): @@ -4924,14 +5277,14 @@ 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') self.assertTrue(Person.objects._has_data(), 'Cursor has data and returned False') - @needs_mongodb_v26 + @requires_mongodb_gte_26 def test_queryset_aggregation_framework(self): class Person(Document): name = StringField() @@ -5102,6 +5455,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() diff --git a/tests/queryset/transform.py b/tests/queryset/transform.py index 20ab0b3f..8064f09c 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 @@ -28,12 +30,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() @@ -42,14 +48,28 @@ 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']}}}) + + 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. @@ -68,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() @@ -96,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() @@ -241,6 +259,30 @@ class TransformTest(unittest.TestCase): 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() diff --git a/tests/queryset/visitor.py b/tests/queryset/visitor.py index 6f020e88..8261faae 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) @@ -296,6 +296,18 @@ class QTest(unittest.TestCase): obj = self.Person.objects(Q(name__not=re.compile('^Gui'))).first() self.assertEqual(obj, None) + def test_q_repr(self): + self.assertEqual(repr(Q()), 'Q(**{})') + self.assertEqual(repr(Q(name='test')), "Q(**{'name': 'test'})") + + self.assertEqual( + repr(Q(name='test') & Q(age__gte=18)), + "(Q(**{'name': 'test'}) & Q(**{'age__gte': 18}))") + + self.assertEqual( + repr(Q(name='test') | Q(age__gte=18)), + "(Q(**{'name': 'test'}) | Q(**{'age__gte': 18}))") + def test_q_lists(self): """Ensure that Q objects query ListFields correctly. """ diff --git a/tests/test_connection.py b/tests/test_connection.py index cdcf1377..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: @@ -364,6 +364,12 @@ class ConnectionTest(unittest.TestCase): date_doc = DateDoc.objects.first() self.assertEqual(d, date_doc.the_date) + def test_read_preference_from_parse(self): + 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") @@ -371,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 0f6bf815..8fb7bc78 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,19 +129,17 @@ 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): 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,18 +183,124 @@ 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_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() diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index 6830a188..2f1277e6 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,360 @@ import unittest -from mongoengine.base.datastructures import StrictDict, SemiStrictDict +from mongoengine import Document +from mongoengine.base.datastructures import StrictDict, BaseList, BaseDict + + +class DocumentStub(object): + def __init__(self): + self._changed_fields = [] + + def _mark_as_changed(self, key): + self._changed_fields.append(key) + + +class TestBaseDict(unittest.TestCase): + + @staticmethod + def _get_basedict(dict_items): + """Get a BaseList bound to a fake document instance""" + fake_doc = DocumentStub() + base_list = BaseDict(dict_items, instance=None, name='my_name') + base_list._instance = fake_doc # hack to inject the mock, it does not work in the constructor + return base_list + + def test___init___(self): + class MyDoc(Document): + pass + + dict_items = {'k': 'v'} + doc = MyDoc() + base_dict = BaseDict(dict_items, instance=doc, name='my_name') + self.assertIsInstance(base_dict._instance, Document) + self.assertEqual(base_dict._name, 'my_name') + self.assertEqual(base_dict, dict_items) + + def test_setdefault_calls_mark_as_changed(self): + base_dict = self._get_basedict({}) + base_dict.setdefault('k', 'v') + self.assertEqual(base_dict._instance._changed_fields, [base_dict._name]) + + def test_popitems_calls_mark_as_changed(self): + base_dict = self._get_basedict({'k': 'v'}) + self.assertEqual(base_dict.popitem(), ('k', 'v')) + self.assertEqual(base_dict._instance._changed_fields, [base_dict._name]) + self.assertFalse(base_dict) + + def test_pop_calls_mark_as_changed(self): + base_dict = self._get_basedict({'k': 'v'}) + self.assertEqual(base_dict.pop('k'), 'v') + self.assertEqual(base_dict._instance._changed_fields, [base_dict._name]) + self.assertFalse(base_dict) + + def test_pop_calls_does_not_mark_as_changed_when_it_fails(self): + base_dict = self._get_basedict({'k': 'v'}) + with self.assertRaises(KeyError): + base_dict.pop('X') + self.assertFalse(base_dict._instance._changed_fields) + + def test_clear_calls_mark_as_changed(self): + base_dict = self._get_basedict({'k': 'v'}) + base_dict.clear() + self.assertEqual(base_dict._instance._changed_fields, ['my_name']) + self.assertEqual(base_dict, {}) + + def test___delitem___calls_mark_as_changed(self): + base_dict = self._get_basedict({'k': 'v'}) + del base_dict['k'] + self.assertEqual(base_dict._instance._changed_fields, ['my_name.k']) + self.assertEqual(base_dict, {}) + + def test___getitem____KeyError(self): + base_dict = self._get_basedict({}) + with self.assertRaises(KeyError): + base_dict['new'] + + def test___getitem____simple_value(self): + base_dict = self._get_basedict({'k': 'v'}) + base_dict['k'] = 'v' + + def test___getitem____sublist_gets_converted_to_BaseList(self): + base_dict = self._get_basedict({'k': [0, 1, 2]}) + sub_list = base_dict['k'] + self.assertEqual(sub_list, [0, 1, 2]) + self.assertIsInstance(sub_list, BaseList) + self.assertIs(sub_list._instance, base_dict._instance) + self.assertEqual(sub_list._name, 'my_name.k') + self.assertEqual(base_dict._instance._changed_fields, []) + + # Challenge mark_as_changed from sublist + sub_list[1] = None + self.assertEqual(base_dict._instance._changed_fields, ['my_name.k.1']) + + def test___getitem____subdict_gets_converted_to_BaseDict(self): + base_dict = self._get_basedict({'k': {'subk': 'subv'}}) + sub_dict = base_dict['k'] + self.assertEqual(sub_dict, {'subk': 'subv'}) + self.assertIsInstance(sub_dict, BaseDict) + self.assertIs(sub_dict._instance, base_dict._instance) + self.assertEqual(sub_dict._name, 'my_name.k') + self.assertEqual(base_dict._instance._changed_fields, []) + + # Challenge mark_as_changed from subdict + sub_dict['subk'] = None + self.assertEqual(base_dict._instance._changed_fields, ['my_name.k.subk']) + + def test_get_sublist_gets_converted_to_BaseList_just_like__getitem__(self): + base_dict = self._get_basedict({'k': [0, 1, 2]}) + sub_list = base_dict.get('k') + self.assertEqual(sub_list, [0, 1, 2]) + self.assertIsInstance(sub_list, BaseList) + + def test_get_returns_the_same_as___getitem__(self): + base_dict = self._get_basedict({'k': [0, 1, 2]}) + get_ = base_dict.get('k') + getitem_ = base_dict['k'] + self.assertEqual(get_, getitem_) + + def test_get_default(self): + base_dict = self._get_basedict({}) + sentinel = object() + self.assertEqual(base_dict.get('new'), None) + self.assertIs(base_dict.get('new', sentinel), sentinel) + + def test___setitem___calls_mark_as_changed(self): + base_dict = self._get_basedict({}) + base_dict['k'] = 'v' + self.assertEqual(base_dict._instance._changed_fields, ['my_name.k']) + self.assertEqual(base_dict, {'k': 'v'}) + + def test_update_calls_mark_as_changed(self): + base_dict = self._get_basedict({}) + base_dict.update({'k': 'v'}) + self.assertEqual(base_dict._instance._changed_fields, ['my_name']) + + def test___setattr____not_tracked_by_changes(self): + base_dict = self._get_basedict({}) + base_dict.a_new_attr = 'test' + self.assertEqual(base_dict._instance._changed_fields, []) + + def test___delattr____tracked_by_changes(self): + # This is probably a bug as __setattr__ is not tracked + # This is even bad because it could be that there is an attribute + # with the same name as a key + base_dict = self._get_basedict({}) + base_dict.a_new_attr = 'test' + del base_dict.a_new_attr + self.assertEqual(base_dict._instance._changed_fields, ['my_name.a_new_attr']) + + +class TestBaseList(unittest.TestCase): + + @staticmethod + def _get_baselist(list_items): + """Get a BaseList bound to a fake document instance""" + fake_doc = DocumentStub() + base_list = BaseList(list_items, instance=None, name='my_name') + base_list._instance = fake_doc # hack to inject the mock, it does not work in the constructor + return base_list + + def test___init___(self): + class MyDoc(Document): + pass + + list_items = [True] + doc = MyDoc() + base_list = BaseList(list_items, instance=doc, name='my_name') + self.assertIsInstance(base_list._instance, Document) + self.assertEqual(base_list._name, 'my_name') + self.assertEqual(base_list, list_items) + + def test___iter__(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) + + def test_append_calls_mark_as_changed(self): + base_list = self._get_baselist([]) + self.assertFalse(base_list._instance._changed_fields) + base_list.append(True) + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_subclass_append(self): + # Due to the way mark_as_changed_wrapper is implemented + # it is good to test subclasses + class SubBaseList(BaseList): + pass + + base_list = SubBaseList([], instance=None, name='my_name') + base_list.append(True) + + def test___getitem__using_simple_index(self): + base_list = self._get_baselist([0, 1, 2]) + self.assertEqual(base_list[0], 0) + self.assertEqual(base_list[1], 1) + self.assertEqual(base_list[-1], 2) + + def test___getitem__using_slice(self): + base_list = self._get_baselist([0, 1, 2]) + self.assertEqual(base_list[1:3], [1,2]) + self.assertEqual(base_list[0:3:2], [0, 2]) + + def test___getitem___using_slice_returns_list(self): + # Bug: using slice does not properly handles the instance + # and mark_as_changed behaviour. + base_list = self._get_baselist([0, 1, 2]) + sliced = base_list[1:3] + self.assertEqual(sliced, [1, 2]) + self.assertIsInstance(sliced, list) + self.assertEqual(base_list._instance._changed_fields, []) + + def test___getitem__sublist_returns_BaseList_bound_to_instance(self): + base_list = self._get_baselist( + [ + [1,2], + [3, 4] + ] + ) + sub_list = base_list[0] + self.assertEqual(sub_list, [1, 2]) + self.assertIsInstance(sub_list, BaseList) + self.assertIs(sub_list._instance, base_list._instance) + self.assertEqual(sub_list._name, 'my_name.0') + self.assertEqual(base_list._instance._changed_fields, []) + + # Challenge mark_as_changed from sublist + sub_list[1] = None + self.assertEqual(base_list._instance._changed_fields, ['my_name.0.1']) + + def test___getitem__subdict_returns_BaseList_bound_to_instance(self): + base_list = self._get_baselist( + [ + {'subk': 'subv'} + ] + ) + sub_dict = base_list[0] + self.assertEqual(sub_dict, {'subk': 'subv'}) + self.assertIsInstance(sub_dict, BaseDict) + self.assertIs(sub_dict._instance, base_list._instance) + self.assertEqual(sub_dict._name, 'my_name.0') + self.assertEqual(base_list._instance._changed_fields, []) + + # Challenge mark_as_changed from subdict + sub_dict['subk'] = None + self.assertEqual(base_list._instance._changed_fields, ['my_name.0.subk']) + + def test_extend_calls_mark_as_changed(self): + base_list = self._get_baselist([]) + base_list.extend([True]) + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_insert_calls_mark_as_changed(self): + base_list = self._get_baselist([]) + base_list.insert(0, True) + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_remove_calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + base_list.remove(True) + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_remove_not_mark_as_changed_when_it_fails(self): + base_list = self._get_baselist([True]) + with self.assertRaises(ValueError): + base_list.remove(False) + self.assertFalse(base_list._instance._changed_fields) + + def test_pop_calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + base_list.pop() + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_reverse_calls_mark_as_changed(self): + base_list = self._get_baselist([True, False]) + base_list.reverse() + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test___delitem___calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + del base_list[0] + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test___setitem___calls_with_full_slice_mark_as_changed(self): + base_list = self._get_baselist([]) + base_list[:] = [0, 1] # Will use __setslice__ under py2 and __setitem__ under py3 + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [0, 1]) + + def test___setitem___calls_with_partial_slice_mark_as_changed(self): + base_list = self._get_baselist([0, 1, 2]) + base_list[0:2] = [1, 0] # Will use __setslice__ under py2 and __setitem__ under py3 + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [1, 0, 2]) + + def test___setitem___calls_with_step_slice_mark_as_changed(self): + base_list = self._get_baselist([0, 1, 2]) + base_list[0:3:2] = [-1, -2] # uses __setitem__ in both py2 & 3 + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [-1, 1, -2]) + + def test___setitem___with_slice(self): + base_list = self._get_baselist([0,1,2,3,4,5]) + base_list[0:6:2] = [None, None, None] + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [None,1,None,3,None,5]) + + def test___setitem___item_0_calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + base_list[0] = False + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [False]) + + def test___setitem___item_1_calls_mark_as_changed(self): + base_list = self._get_baselist([True, True]) + base_list[1] = False + self.assertEqual(base_list._instance._changed_fields, ['my_name.1']) + self.assertEqual(base_list, [True, False]) + + def test___delslice___calls_mark_as_changed(self): + base_list = self._get_baselist([0, 1]) + del base_list[0:1] + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + self.assertEqual(base_list, [1]) + + def test___iadd___calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + base_list += [False] + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test___imul___calls_mark_as_changed(self): + base_list = self._get_baselist([True]) + self.assertEqual(base_list._instance._changed_fields, []) + base_list *= 2 + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_sort_calls_not_marked_as_changed_when_it_fails(self): + base_list = self._get_baselist([True]) + with self.assertRaises(TypeError): + base_list.sort(key=1) + + self.assertEqual(base_list._instance._changed_fields, []) + + def test_sort_calls_mark_as_changed(self): + base_list = self._get_baselist([True, False]) + base_list.sort() + self.assertEqual(base_list._instance._changed_fields, ['my_name']) + + def test_sort_calls_with_key(self): + base_list = self._get_baselist([1, 2, 11]) + base_list.sort(key=lambda i: str(i)) + self.assertEqual(base_list, [1, 11, 2]) class TestStrictDict(unittest.TestCase): @@ -76,44 +430,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() diff --git a/tests/test_dereference.py b/tests/test_dereference.py index 7f58a85b..5cf089f4 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() @@ -1029,7 +1029,6 @@ class FieldTest(unittest.TestCase): self.assertEqual(type(foo.bar), Bar) self.assertEqual(type(foo.baz), Baz) - def test_document_reload_reference_integrity(self): """ Ensure reloading a document with multiple similar id @@ -1209,10 +1208,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): """ 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') diff --git a/tests/utils.py b/tests/utils.py index 4566d864..5345f75e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,12 +7,19 @@ 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 + + +# Constant that can be used to compare the version retrieved with +# get_mongodb_version() +MONGODB_26 = (2, 6) +MONGODB_3 = (3,0) +MONGODB_32 = (3, 2) 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 @@ -27,40 +34,46 @@ class MongoDBTestCase(unittest.TestCase): 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']) + """Return the version of the connected mongoDB (first 2 digits) -def _decorated_with_ver_requirement(func, ver_tuple): + :return: tuple(int, int) + """ + version_list = get_connection().server_info()['versionArray'][:2] # e.g: (3, 2) + return tuple(version_list) + + +def _decorated_with_ver_requirement(func, version): """Return a given function decorated with the version requirement for a particular MongoDB version tuple. + + :param version: The version required (tuple(int, int)) """ def _inner(*args, **kwargs): - mongodb_ver = get_mongodb_version() - if mongodb_ver >= ver_tuple: + MONGODB_V = get_mongodb_version() + if MONGODB_V >= version: return func(*args, **kwargs) - raise SkipTest('Needs MongoDB v{}+'.format( - '.'.join([str(v) for v in ver_tuple]) - )) + raise SkipTest('Needs MongoDB v{}+'.format('.'.join(str(n) for n in version))) _inner.__name__ = func.__name__ _inner.__doc__ = func.__doc__ return _inner -def needs_mongodb_v26(func): + +def requires_mongodb_gte_26(func): """Raise a SkipTest exception if we're working with MongoDB version lower than v2.6. """ - return _decorated_with_ver_requirement(func, (2, 6)) + return _decorated_with_ver_requirement(func, MONGODB_26) -def needs_mongodb_v3(func): + +def requires_mongodb_gte_3(func): """Raise a SkipTest exception if we're working with MongoDB version lower than v3.0. """ - return _decorated_with_ver_requirement(func, (3, 0)) + return _decorated_with_ver_requirement(func, MONGODB_3) + def skip_pymongo3(f): """Raise a SkipTest exception if we're running a test against diff --git a/tox.ini b/tox.ini index 7f0d36e4..815d2acc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,12 @@ [tox] -envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30} +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 - mg30: PyMongo>=3.0 + mg35: PyMongo==3.5 + mg3x: PyMongo>=3.0,<3.7 setenv = PYTHON_EGG_CACHE = {envdir}/python-eggs