Merge branch 'master' of github.com:MongoEngine/mongoengine into py2py3_improve_compat
This commit is contained in:
		
							
								
								
									
										20
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -3,7 +3,7 @@ | |||||||
| # with a very large number of jobs, hence we only test a subset of all the | # with a very large number of jobs, hence we only test a subset of all the | ||||||
| # combinations: | # combinations: | ||||||
| # * MongoDB v3.4 & the latest PyMongo v3.x is currently the "main" setup, | # * MongoDB v3.4 & the latest PyMongo v3.x is currently the "main" setup, | ||||||
| #   tested against Python v2.7, v3.5, v3.6, and PyPy. | #   tested against Python v2.7, v3.5, v3.6, v3.7, v3.8, PyPy and PyPy3. | ||||||
| # * Besides that, we test the lowest actively supported Python/MongoDB/PyMongo | # * Besides that, we test the lowest actively supported Python/MongoDB/PyMongo | ||||||
| #   combination: MongoDB v3.4, PyMongo v3.4, Python v2.7. | #   combination: MongoDB v3.4, PyMongo v3.4, Python v2.7. | ||||||
| # * MongoDB v3.6 is tested against Python v3.6, and PyMongo v3.6, v3.7, v3.8. | # * MongoDB v3.6 is tested against Python v3.6, and PyMongo v3.6, v3.7, v3.8. | ||||||
| @@ -23,6 +23,7 @@ python: | |||||||
| - 3.5 | - 3.5 | ||||||
| - 3.6 | - 3.6 | ||||||
| - 3.7 | - 3.7 | ||||||
|  | - 3.8 | ||||||
| - pypy | - pypy | ||||||
| - pypy3 | - pypy3 | ||||||
|  |  | ||||||
| @@ -34,14 +35,16 @@ env: | |||||||
|   global: |   global: | ||||||
|     - MONGODB_3_4=3.4.17 |     - MONGODB_3_4=3.4.17 | ||||||
|     - MONGODB_3_6=3.6.12 |     - MONGODB_3_6=3.6.12 | ||||||
|     - PYMONGO_3_9=3.9 |     - MONGODB_4_0=4.0.13 | ||||||
|     - PYMONGO_3_6=3.6 |  | ||||||
|     - PYMONGO_3_4=3.4 |     - PYMONGO_3_4=3.4 | ||||||
|  |     - PYMONGO_3_6=3.6 | ||||||
|  |     - PYMONGO_3_9=3.9 | ||||||
|  |     - PYMONGO_3_10=3.10 | ||||||
|   matrix: |   matrix: | ||||||
|     - MONGODB=${MONGODB_3_4} PYMONGO=${PYMONGO_3_9} |     - MONGODB=${MONGODB_3_4} PYMONGO=${PYMONGO_3_10} | ||||||
|  |  | ||||||
| matrix: | matrix: | ||||||
|  |  | ||||||
|   # Finish the build as soon as one job fails |   # Finish the build as soon as one job fails | ||||||
|   fast_finish: true |   fast_finish: true | ||||||
|  |  | ||||||
| @@ -52,7 +55,10 @@ matrix: | |||||||
|     env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_6} |     env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_6} | ||||||
|   - python: 3.7 |   - python: 3.7 | ||||||
|     env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_9} |     env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_9} | ||||||
|  |   - python: 3.7 | ||||||
|  |     env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_10} | ||||||
|  |   - python: 3.8 | ||||||
|  |     env: MONGODB=${MONGODB_4_0} PYMONGO=${PYMONGO_3_10} | ||||||
|  |  | ||||||
| install: | install: | ||||||
|   # Install Mongo |   # Install Mongo | ||||||
| @@ -109,5 +115,5 @@ deploy: | |||||||
|   on: |   on: | ||||||
|     tags: true |     tags: true | ||||||
|     repo: MongoEngine/mongoengine |     repo: MongoEngine/mongoengine | ||||||
|     condition: ($PYMONGO = ${PYMONGO_3_6}) && ($MONGODB = ${MONGODB_3_4}) |     condition: ($PYMONGO = ${PYMONGO_3_10}) && ($MONGODB = ${MONGODB_3_4}) | ||||||
|     python: 2.7 |     python: 2.7 | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								AUTHORS
									
									
									
									
									
								
							| @@ -253,3 +253,6 @@ that much better: | |||||||
|  * Gaurav Dadhania (https://github.com/GVRV) |  * Gaurav Dadhania (https://github.com/GVRV) | ||||||
|  * Yurii Andrieiev (https://github.com/yandrieiev) |  * Yurii Andrieiev (https://github.com/yandrieiev) | ||||||
|  * Filip Kucharczyk (https://github.com/Pacu2) |  * Filip Kucharczyk (https://github.com/Pacu2) | ||||||
|  |  * Eric Timmons (https://github.com/daewok) | ||||||
|  |  * Matthew Simpson (https://github.com/mcsimps2) | ||||||
|  |  * Leonardo Domingues (https://github.com/leodmgs) | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.rst
									
									
									
									
									
								
							| @@ -26,10 +26,10 @@ an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_. | |||||||
|  |  | ||||||
| Supported MongoDB Versions | Supported MongoDB Versions | ||||||
| ========================== | ========================== | ||||||
| MongoEngine is currently tested against MongoDB v3.4 and v3.6. Future versions | MongoEngine is currently tested against MongoDB v3.4, v3.6 and v4.0. Future versions | ||||||
| should be supported as well, but aren't actively tested at the moment. Make | 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 | sure to open an issue or submit a pull request if you experience any problems | ||||||
| with MongoDB version > 3.6. | with MongoDB version > 4.0. | ||||||
|  |  | ||||||
| Installation | Installation | ||||||
| ============ | ============ | ||||||
| @@ -91,12 +91,11 @@ Some simple examples of what MongoEngine code looks like: | |||||||
|  |  | ||||||
|     # Iterate over all posts using the BlogPost superclass |     # Iterate over all posts using the BlogPost superclass | ||||||
|     >>> for post in BlogPost.objects: |     >>> for post in BlogPost.objects: | ||||||
|     ...     print '===', post.title, '===' |     ...     print('===', post.title, '===') | ||||||
|     ...     if isinstance(post, TextPost): |     ...     if isinstance(post, TextPost): | ||||||
|     ...         print post.content |     ...         print(post.content) | ||||||
|     ...     elif isinstance(post, LinkPost): |     ...     elif isinstance(post, LinkPost): | ||||||
|     ...         print 'Link:', post.url |     ...         print('Link:', post.url) | ||||||
|     ...     print |  | ||||||
|     ... |     ... | ||||||
|  |  | ||||||
|     # Count all blog posts and its subtypes |     # Count all blog posts and its subtypes | ||||||
|   | |||||||
| @@ -6,9 +6,18 @@ Changelog | |||||||
| Development | Development | ||||||
| =========== | =========== | ||||||
| - (Fill this out as you fix issues and develop your features). | - (Fill this out as you fix issues and develop your features). | ||||||
| - Codebase is now compatible with both Python2 and Python3 (no more relying on 2to3 during installation) #2087 | - ATTENTION: Drop support for Python2 | ||||||
| - Documentation improvements: | - Add Mongo 4.0 to Travis | ||||||
|     - Documented how `pymongo.monitoring` can be used to log all queries issued by MongoEngine to the driver. | - Fixed a bug causing inaccurate query results, while combining ``__raw__`` and regular filters for the same field #2264 | ||||||
|  |  | ||||||
|  | Changes in 0.19.1 | ||||||
|  | ================= | ||||||
|  | - Requires Pillow < 7.0.0 as it dropped Python2 support | ||||||
|  | - DEPRECATION: The interface of ``QuerySet.aggregate`` method was changed, it no longer takes an unpacked list of | ||||||
|  |     pipeline steps (*pipeline) but simply takes the pipeline list just like ``pymongo.Collection.aggregate`` does. #2079 | ||||||
|  |  | ||||||
|  | Changes in 0.19.0 | ||||||
|  | ================= | ||||||
| - BREAKING CHANGE: ``class_check`` and ``read_preference`` keyword arguments are no longer available when filtering a ``QuerySet``. #2112 | - BREAKING CHANGE: ``class_check`` and ``read_preference`` keyword arguments are no longer available when filtering a ``QuerySet``. #2112 | ||||||
|     - Instead of ``Doc.objects(foo=bar, read_preference=...)`` use ``Doc.objects(foo=bar).read_preference(...)``. |     - Instead of ``Doc.objects(foo=bar, read_preference=...)`` use ``Doc.objects(foo=bar).read_preference(...)``. | ||||||
|     - Instead of ``Doc.objects(foo=bar, class_check=False)`` use ``Doc.objects(foo=bar).clear_cls_query(...)``. |     - Instead of ``Doc.objects(foo=bar, class_check=False)`` use ``Doc.objects(foo=bar).clear_cls_query(...)``. | ||||||
| @@ -18,17 +27,23 @@ Development | |||||||
|     - If you catch/use ``MongoEngineConnectionError`` in your code, you'll have to rename it. |     - If you catch/use ``MongoEngineConnectionError`` in your code, you'll have to rename it. | ||||||
| - BREAKING CHANGE: Positional arguments when instantiating a document are no longer supported. #2103 | - BREAKING CHANGE: Positional arguments when instantiating a document are no longer supported. #2103 | ||||||
|     - From now on keyword arguments (e.g. ``Doc(field_name=value)``) are required. |     - From now on keyword arguments (e.g. ``Doc(field_name=value)``) are required. | ||||||
|  | - BREAKING CHANGE: A ``LazyReferenceField`` is now stored in the ``_data`` field of its parent as a ``DBRef``, ``Document``, or ``EmbeddedDocument`` (``ObjectId`` is no longer allowed). #2182 | ||||||
| - DEPRECATION: ``Q.empty`` & ``QNode.empty`` are marked as deprecated and will be removed in a next version of MongoEngine. #2210 | - DEPRECATION: ``Q.empty`` & ``QNode.empty`` are marked as deprecated and will be removed in a next version of MongoEngine. #2210 | ||||||
|     - Added ability to check if Q or QNode are empty by parsing them to bool. |     - Added ability to check if Q or QNode are empty by parsing them to bool. | ||||||
|     - Instead of ``Q(name="John").empty`` use ``not Q(name="John")``. |     - Instead of ``Q(name="John").empty`` use ``not Q(name="John")``. | ||||||
| - Improve error message related to InvalidDocumentError #2180 |  | ||||||
| - Fix updating/modifying/deleting/reloading a document that's sharded by a field with ``db_field`` specified. #2125 | - Fix updating/modifying/deleting/reloading a document that's sharded by a field with ``db_field`` specified. #2125 | ||||||
|  | - Only set no_cursor_timeout when requested (fixes an incompatibility with MongoDB 4.2) #2148 | ||||||
| - ``ListField`` now accepts an optional ``max_length`` parameter. #2110 | - ``ListField`` now accepts an optional ``max_length`` parameter. #2110 | ||||||
| - Switch from nosetest to pytest as test runner #2114 | - Improve error message related to InvalidDocumentError #2180 | ||||||
| - The codebase is now formatted using ``black``. #2109 | - Added BulkWriteError to replace NotUniqueError which was misleading in bulk write insert #2152 | ||||||
| - In bulk write insert, the detailed error message would raise in exception. |  | ||||||
| - Added ability to compare Q and Q operations #2204 | - Added ability to compare Q and Q operations #2204 | ||||||
| - Added ability to use a db alias on query_counter #2194 | - Added ability to use a db alias on query_counter #2194 | ||||||
|  | - Added ability to specify collations for querysets with ``Doc.objects.collation`` #2024 | ||||||
|  | - Fix updates of a list field by negative index #2094 | ||||||
|  | - Switch from nosetest to pytest as test runner #2114 | ||||||
|  | - The codebase is now formatted using ``black``. #2109 | ||||||
|  | - Documentation improvements: | ||||||
|  |     - Documented how `pymongo.monitoring` can be used to log all queries issued by MongoEngine to the driver. | ||||||
|  |  | ||||||
| Changes in 0.18.2 | Changes in 0.18.2 | ||||||
| ================= | ================= | ||||||
|   | |||||||
| @@ -352,7 +352,7 @@ Its value can take any of the following constants: | |||||||
|   Deletion is denied if there still exist references to the object being |   Deletion is denied if there still exist references to the object being | ||||||
|   deleted. |   deleted. | ||||||
| :const:`mongoengine.NULLIFY` | :const:`mongoengine.NULLIFY` | ||||||
|   Any object's fields still referring to the object being deleted are removed |   Any object's fields still referring to the object being deleted are set to None | ||||||
|   (using MongoDB's "unset" operation), effectively nullifying the relationship. |   (using MongoDB's "unset" operation), effectively nullifying the relationship. | ||||||
| :const:`mongoengine.CASCADE` | :const:`mongoengine.CASCADE` | ||||||
|   Any object containing fields that are referring to the object being deleted |   Any object containing fields that are referring to the object being deleted | ||||||
| @@ -744,7 +744,7 @@ Document inheritance | |||||||
|  |  | ||||||
| To create a specialised type of a :class:`~mongoengine.Document` you have | To create a specialised type of a :class:`~mongoengine.Document` you have | ||||||
| defined, you may subclass it and add any extra fields or methods you may need. | defined, you may subclass it and add any extra fields or methods you may need. | ||||||
| As this is new class is not a direct subclass of | As this new class is not a direct subclass of | ||||||
| :class:`~mongoengine.Document`, it will not be stored in its own collection; it | :class:`~mongoengine.Document`, it will not be stored in its own collection; it | ||||||
| will use the same collection as its superclass uses. This allows for more | will use the same collection as its superclass uses. This allows for more | ||||||
| convenient and efficient retrieval of related documents -- all you need do is | convenient and efficient retrieval of related documents -- all you need do is | ||||||
| @@ -767,6 +767,27 @@ document.:: | |||||||
|           Setting :attr:`allow_inheritance` to True should also be used in |           Setting :attr:`allow_inheritance` to True should also be used in | ||||||
|           :class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it |           :class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it | ||||||
|  |  | ||||||
|  | When it comes to querying using :attr:`.objects()`, querying `Page.objects()` will query | ||||||
|  | both `Page` and `DatedPage` whereas querying `DatedPage` will only query the `DatedPage` documents. | ||||||
|  | Behind the scenes, MongoEngine deals with inheritance by adding a :attr:`_cls` attribute that contains | ||||||
|  | the class name in every documents. When a document is loaded, MongoEngine checks | ||||||
|  | it's :attr:`_cls` attribute and use that class to construct the instance.:: | ||||||
|  |  | ||||||
|  |     Page(title='a funky title').save() | ||||||
|  |     DatedPage(title='another title', date=datetime.utcnow()).save() | ||||||
|  |  | ||||||
|  |     print(Page.objects().count())         # 2 | ||||||
|  |     print(DatedPage.objects().count())    # 1 | ||||||
|  |  | ||||||
|  |     # print documents in their native form | ||||||
|  |     # we remove 'id' to avoid polluting the output with unnecessary detail | ||||||
|  |     qs = Page.objects.exclude('id').as_pymongo() | ||||||
|  |     print(list(qs)) | ||||||
|  |     # [ | ||||||
|  |     #   {'_cls': u 'Page', 'title': 'a funky title'}, | ||||||
|  |     #   {'_cls': u 'Page.DatedPage', 'title': u 'another title', 'date': datetime.datetime(2019, 12, 13, 20, 16, 59, 993000)} | ||||||
|  |     # ] | ||||||
|  |  | ||||||
| Working with existing data | Working with existing data | ||||||
| -------------------------- | -------------------------- | ||||||
| As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and | As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and | ||||||
|   | |||||||
| @@ -10,8 +10,9 @@ Writing | |||||||
| GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field | GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field | ||||||
| object. This field acts as a file-like object and provides a couple of | object. This field acts as a file-like object and provides a couple of | ||||||
| different ways of inserting and retrieving data. Arbitrary metadata such as | different ways of inserting and retrieving data. Arbitrary metadata such as | ||||||
| content type can also be stored alongside the files. In the following example, | content type can also be stored alongside the files. The object returned when accessing a | ||||||
| a document is created to store details about animals, including a photo:: | FileField is a proxy to `Pymongo's GridFS <https://api.mongodb.com/python/current/examples/gridfs.html#gridfs-example>`_ | ||||||
|  | In the following example, a document is created to store details about animals, including a photo:: | ||||||
|  |  | ||||||
|     class Animal(Document): |     class Animal(Document): | ||||||
|         genus = StringField() |         genus = StringField() | ||||||
| @@ -20,8 +21,8 @@ a document is created to store details about animals, including a photo:: | |||||||
|  |  | ||||||
|     marmot = Animal(genus='Marmota', family='Sciuridae') |     marmot = Animal(genus='Marmota', family='Sciuridae') | ||||||
|  |  | ||||||
|     marmot_photo = open('marmot.jpg', 'rb') |     with open('marmot.jpg', 'rb') as fd: | ||||||
|     marmot.photo.put(marmot_photo, content_type = 'image/jpeg') |         marmot.photo.put(fd, content_type = 'image/jpeg') | ||||||
|     marmot.save() |     marmot.save() | ||||||
|  |  | ||||||
| Retrieval | Retrieval | ||||||
| @@ -34,6 +35,20 @@ field. The file can also be retrieved just as easily:: | |||||||
|     photo = marmot.photo.read() |     photo = marmot.photo.read() | ||||||
|     content_type = marmot.photo.content_type |     content_type = marmot.photo.content_type | ||||||
|  |  | ||||||
|  | .. note:: If you need to read() the content of a file multiple times, you'll need to "rewind" | ||||||
|  |     the file-like object using `seek`:: | ||||||
|  |  | ||||||
|  |         marmot = Animal.objects(genus='Marmota').first() | ||||||
|  |         content1 = marmot.photo.read() | ||||||
|  |         assert content1 != "" | ||||||
|  |  | ||||||
|  |         content2 = marmot.photo.read()    # will be empty | ||||||
|  |         assert content2 == "" | ||||||
|  |  | ||||||
|  |         marmot.photo.seek(0)              # rewind the file by setting the current position of the cursor in the file to 0 | ||||||
|  |         content3 = marmot.photo.read() | ||||||
|  |         assert content3 == content1 | ||||||
|  |  | ||||||
| Streaming | Streaming | ||||||
| --------- | --------- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,10 +2,10 @@ | |||||||
| Use mongomock for testing | Use mongomock for testing | ||||||
| ============================== | ============================== | ||||||
|  |  | ||||||
| `mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just  | `mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just | ||||||
| what the name implies, mocking a mongo database. | what the name implies, mocking a mongo database. | ||||||
|  |  | ||||||
| To use with mongoengine, simply specify mongomock when connecting with  | To use with mongoengine, simply specify mongomock when connecting with | ||||||
| mongoengine: | mongoengine: | ||||||
|  |  | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
| @@ -21,7 +21,7 @@ or with an alias: | |||||||
|     conn = get_connection('testdb') |     conn = get_connection('testdb') | ||||||
|  |  | ||||||
| Example of test file: | Example of test file: | ||||||
| -------- | --------------------- | ||||||
| .. code-block:: python | .. code-block:: python | ||||||
|  |  | ||||||
|     import unittest |     import unittest | ||||||
| @@ -45,4 +45,4 @@ Example of test file: | |||||||
|             pers.save() |             pers.save() | ||||||
|  |  | ||||||
|             fresh_pers = Person.objects().first() |             fresh_pers = Person.objects().first() | ||||||
|             self.assertEqual(fresh_pers.name, 'John') |             assert fresh_pers.name ==  'John' | ||||||
|   | |||||||
| @@ -222,6 +222,18 @@ keyword argument:: | |||||||
|  |  | ||||||
| .. versionadded:: 0.4 | .. versionadded:: 0.4 | ||||||
|  |  | ||||||
|  | Sorting/Ordering results | ||||||
|  | ======================== | ||||||
|  | It is possible to order the results by 1 or more keys using :meth:`~mongoengine.queryset.QuerySet.order_by`. | ||||||
|  | The order may be specified by prepending each of the keys by "+" or "-". Ascending order is assumed if there's no prefix.:: | ||||||
|  |  | ||||||
|  |     # Order by ascending date | ||||||
|  |     blogs = BlogPost.objects().order_by('date')    # equivalent to .order_by('+date') | ||||||
|  |  | ||||||
|  |     # Order by ascending date first, then descending title | ||||||
|  |     blogs = BlogPost.objects().order_by('+date', '-title') | ||||||
|  |  | ||||||
|  |  | ||||||
| Limiting and skipping results | Limiting and skipping results | ||||||
| ============================= | ============================= | ||||||
| Just as with traditional ORMs, you may limit the number of results returned or | Just as with traditional ORMs, you may limit the number of results returned or | ||||||
| @@ -388,7 +400,7 @@ would be generating "tag-clouds":: | |||||||
|  |  | ||||||
| MongoDB aggregation API | MongoDB aggregation API | ||||||
| ----------------------- | ----------------------- | ||||||
| If you need to run aggregation pipelines, MongoEngine provides an entry point `Pymongo's aggregation framework <https://api.mongodb.com/python/current/examples/aggregation.html#aggregation-framework>`_ | If you need to run aggregation pipelines, MongoEngine provides an entry point to `Pymongo's aggregation framework <https://api.mongodb.com/python/current/examples/aggregation.html#aggregation-framework>`_ | ||||||
| through :meth:`~mongoengine.queryset.QuerySet.aggregate`. Check out Pymongo's documentation for the syntax and pipeline. | through :meth:`~mongoengine.queryset.QuerySet.aggregate`. Check out Pymongo's documentation for the syntax and pipeline. | ||||||
| An example of its use would be:: | An example of its use would be:: | ||||||
|  |  | ||||||
| @@ -402,7 +414,7 @@ An example of its use would be:: | |||||||
|             {"$sort" : {"name" : -1}}, |             {"$sort" : {"name" : -1}}, | ||||||
|             {"$project": {"_id": 0, "name": {"$toUpper": "$name"}}} |             {"$project": {"_id": 0, "name": {"$toUpper": "$name"}}} | ||||||
|             ] |             ] | ||||||
|         data = Person.objects().aggregate(*pipeline) |         data = Person.objects().aggregate(pipeline) | ||||||
|         assert data == [{'name': 'BOB'}, {'name': 'JOHN'}] |         assert data == [{'name': 'BOB'}, {'name': 'JOHN'}] | ||||||
|  |  | ||||||
| Query efficiency and performance | Query efficiency and performance | ||||||
| @@ -585,7 +597,8 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`:: | |||||||
|     ['database', 'mongodb'] |     ['database', 'mongodb'] | ||||||
|  |  | ||||||
| From MongoDB version 2.6, push operator supports $position value which allows | From MongoDB version 2.6, push operator supports $position value which allows | ||||||
| to push values with index. | to push values with index:: | ||||||
|  |  | ||||||
|     >>> post = BlogPost(title="Test", tags=["mongo"]) |     >>> post = BlogPost(title="Test", tags=["mongo"]) | ||||||
|     >>> post.save() |     >>> post.save() | ||||||
|     >>> post.update(push__tags__0=["database", "code"]) |     >>> post.update(push__tags__0=["database", "code"]) | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ rename its occurrences. | |||||||
| This release includes a major rehaul of MongoEngine's code quality and | This release includes a major rehaul of MongoEngine's code quality and | ||||||
| introduces a few breaking changes. It also touches many different parts of | introduces a few breaking changes. It also touches many different parts of | ||||||
| the package and although all the changes have been tested and scrutinized, | the package and although all the changes have been tested and scrutinized, | ||||||
| you're encouraged to thorougly test the upgrade. | you're encouraged to thoroughly test the upgrade. | ||||||
|  |  | ||||||
| First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`. | First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`. | ||||||
| If you import or catch this exception, you'll need to rename it in your code. | If you import or catch this exception, you'll need to rename it in your code. | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ __all__ = ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| VERSION = (0, 18, 2) | VERSION = (0, 19, 1) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_version(): | def get_version(): | ||||||
|   | |||||||
| @@ -121,6 +121,9 @@ class BaseList(list): | |||||||
|         super(BaseList, self).__init__(list_items) |         super(BaseList, self).__init__(list_items) | ||||||
|  |  | ||||||
|     def __getitem__(self, key): |     def __getitem__(self, key): | ||||||
|  |         # change index to positive value because MongoDB does not support negative one | ||||||
|  |         if isinstance(key, int) and key < 0: | ||||||
|  |             key = len(self) + key | ||||||
|         value = super(BaseList, self).__getitem__(key) |         value = super(BaseList, self).__getitem__(key) | ||||||
|  |  | ||||||
|         if isinstance(key, slice): |         if isinstance(key, slice): | ||||||
|   | |||||||
| @@ -330,7 +330,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)): | |||||||
|     ): |     ): | ||||||
|         """Save the :class:`~mongoengine.Document` to the database. If the |         """Save the :class:`~mongoengine.Document` to the database. If the | ||||||
|         document already exists, it will be updated, otherwise it will be |         document already exists, it will be updated, otherwise it will be | ||||||
|         created. |         created. Returns the saved object instance. | ||||||
|  |  | ||||||
|         :param force_insert: only try to create a new document, don't allow |         :param force_insert: only try to create a new document, don't allow | ||||||
|             updates of existing documents. |             updates of existing documents. | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ from mongoengine.common import _import_class | |||||||
| from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db | from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db | ||||||
| from mongoengine.document import Document, EmbeddedDocument | from mongoengine.document import Document, EmbeddedDocument | ||||||
| from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError | from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError | ||||||
|  | from mongoengine.mongodb_support import MONGODB_36, get_mongodb_version | ||||||
| from mongoengine.python_support import StringIO | from mongoengine.python_support import StringIO | ||||||
| from mongoengine.queryset import DO_NOTHING | from mongoengine.queryset import DO_NOTHING | ||||||
| from mongoengine.queryset.base import BaseQuerySet | from mongoengine.queryset.base import BaseQuerySet | ||||||
| @@ -1043,6 +1044,15 @@ def key_has_dot_or_dollar(d): | |||||||
|             return True |             return True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def key_starts_with_dollar(d): | ||||||
|  |     """Helper function to recursively determine if any key in a | ||||||
|  |     dictionary starts with a dollar | ||||||
|  |     """ | ||||||
|  |     for k, v in d.items(): | ||||||
|  |         if (k.startswith("$")) or (isinstance(v, dict) and key_starts_with_dollar(v)): | ||||||
|  |             return True | ||||||
|  |  | ||||||
|  |  | ||||||
| class DictField(ComplexBaseField): | class DictField(ComplexBaseField): | ||||||
|     """A dictionary field that wraps a standard Python dictionary. This is |     """A dictionary field that wraps a standard Python dictionary. This is | ||||||
|     similar to an embedded document, but the structure is not defined. |     similar to an embedded document, but the structure is not defined. | ||||||
| @@ -1069,11 +1079,18 @@ class DictField(ComplexBaseField): | |||||||
|         if key_not_string(value): |         if key_not_string(value): | ||||||
|             msg = "Invalid dictionary key - documents must have only string keys" |             msg = "Invalid dictionary key - documents must have only string keys" | ||||||
|             self.error(msg) |             self.error(msg) | ||||||
|         if key_has_dot_or_dollar(value): |  | ||||||
|  |         curr_mongo_ver = get_mongodb_version() | ||||||
|  |  | ||||||
|  |         if curr_mongo_ver < MONGODB_36 and key_has_dot_or_dollar(value): | ||||||
|             self.error( |             self.error( | ||||||
|                 'Invalid dictionary key name - keys may not contain "."' |                 'Invalid dictionary key name - keys may not contain "."' | ||||||
|                 ' or startswith "$" characters' |                 ' or startswith "$" characters' | ||||||
|             ) |             ) | ||||||
|  |         elif curr_mongo_ver >= MONGODB_36 and key_starts_with_dollar(value): | ||||||
|  |             self.error( | ||||||
|  |                 'Invalid dictionary key name - keys may not startswith "$" characters' | ||||||
|  |             ) | ||||||
|         super(DictField, self).validate(value) |         super(DictField, self).validate(value) | ||||||
|  |  | ||||||
|     def lookup_member(self, member_name): |     def lookup_member(self, member_name): | ||||||
| @@ -2494,6 +2511,13 @@ class LazyReferenceField(BaseField): | |||||||
|         else: |         else: | ||||||
|             return pk |             return pk | ||||||
|  |  | ||||||
|  |     def to_python(self, value): | ||||||
|  |         """Convert a MongoDB-compatible type to a Python type.""" | ||||||
|  |         if not isinstance(value, (DBRef, Document, EmbeddedDocument)): | ||||||
|  |             collection = self.document_type._get_collection_name() | ||||||
|  |             value = DBRef(collection, self.document_type.id.to_python(value)) | ||||||
|  |         return value | ||||||
|  |  | ||||||
|     def validate(self, value): |     def validate(self, value): | ||||||
|         if isinstance(value, LazyReference): |         if isinstance(value, LazyReference): | ||||||
|             if value.collection != self.document_type._get_collection_name(): |             if value.collection != self.document_type._get_collection_name(): | ||||||
|   | |||||||
| @@ -302,7 +302,7 @@ class BaseQuerySet(object): | |||||||
|                 ``insert(..., {w: 2, fsync: True})`` will wait until at least |                 ``insert(..., {w: 2, fsync: True})`` will wait until at least | ||||||
|                 two servers have recorded the write and will force an fsync on |                 two servers have recorded the write and will force an fsync on | ||||||
|                 each server being written to. |                 each server being written to. | ||||||
|         :parm signal_kwargs: (optional) kwargs dictionary to be passed to |         :param signal_kwargs: (optional) kwargs dictionary to be passed to | ||||||
|             the signal calls. |             the signal calls. | ||||||
|  |  | ||||||
|         By default returns document instances, set ``load_bulk`` to False to |         By default returns document instances, set ``load_bulk`` to False to | ||||||
| @@ -1255,16 +1255,27 @@ class BaseQuerySet(object): | |||||||
|             for data in son_data |             for data in son_data | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|     def aggregate(self, *pipeline, **kwargs): |     def aggregate(self, pipeline, *suppl_pipeline, **kwargs): | ||||||
|         """ |         """Perform a aggregate function based in your queryset params | ||||||
|         Perform a aggregate function based in your queryset params |  | ||||||
|         :param pipeline: list of aggregation commands,\ |         :param pipeline: list of aggregation commands,\ | ||||||
|             see: http://docs.mongodb.org/manual/core/aggregation-pipeline/ |             see: http://docs.mongodb.org/manual/core/aggregation-pipeline/ | ||||||
|  |         :param suppl_pipeline: unpacked list of pipeline (added to support deprecation of the old interface) | ||||||
|  |             parameter will be removed shortly | ||||||
|  |         :param kwargs: (optional) kwargs dictionary to be passed to pymongo's aggregate call | ||||||
|  |             See https://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.aggregate | ||||||
|         .. versionadded:: 0.9 |         .. versionadded:: 0.9 | ||||||
|         """ |         """ | ||||||
|         initial_pipeline = [] |         using_deprecated_interface = isinstance(pipeline, dict) or bool(suppl_pipeline) | ||||||
|  |         user_pipeline = [pipeline] if isinstance(pipeline, dict) else list(pipeline) | ||||||
|  |  | ||||||
|  |         if using_deprecated_interface: | ||||||
|  |             msg = "Calling .aggregate() with un unpacked list (*pipeline) is deprecated, it will soon change and will expect a list (similar to pymongo.Collection.aggregate interface), see documentation" | ||||||
|  |             warnings.warn(msg, DeprecationWarning) | ||||||
|  |  | ||||||
|  |         user_pipeline += suppl_pipeline | ||||||
|  |  | ||||||
|  |         initial_pipeline = [] | ||||||
|         if self._query: |         if self._query: | ||||||
|             initial_pipeline.append({"$match": self._query}) |             initial_pipeline.append({"$match": self._query}) | ||||||
|  |  | ||||||
| @@ -1281,14 +1292,14 @@ class BaseQuerySet(object): | |||||||
|         if self._skip is not None: |         if self._skip is not None: | ||||||
|             initial_pipeline.append({"$skip": self._skip}) |             initial_pipeline.append({"$skip": self._skip}) | ||||||
|  |  | ||||||
|         pipeline = initial_pipeline + list(pipeline) |         final_pipeline = initial_pipeline + user_pipeline | ||||||
|  |  | ||||||
|  |         collection = self._collection | ||||||
|         if self._read_preference is not None: |         if self._read_preference is not None: | ||||||
|             return self._collection.with_options( |             collection = self._collection.with_options( | ||||||
|                 read_preference=self._read_preference |                 read_preference=self._read_preference | ||||||
|             ).aggregate(pipeline, cursor={}, **kwargs) |             ) | ||||||
|  |         return collection.aggregate(final_pipeline, cursor={}, **kwargs) | ||||||
|         return self._collection.aggregate(pipeline, cursor={}, **kwargs) |  | ||||||
|  |  | ||||||
|     # JS functionality |     # JS functionality | ||||||
|     def map_reduce( |     def map_reduce( | ||||||
|   | |||||||
| @@ -169,9 +169,9 @@ def query(_doc_cls=None, **kwargs): | |||||||
|  |  | ||||||
|         key = ".".join(parts) |         key = ".".join(parts) | ||||||
|  |  | ||||||
|         if op is None or key not in mongo_query: |         if key not in mongo_query: | ||||||
|             mongo_query[key] = value |             mongo_query[key] = value | ||||||
|         elif key in mongo_query: |         else: | ||||||
|             if isinstance(mongo_query[key], dict) and isinstance(value, dict): |             if isinstance(mongo_query[key], dict) and isinstance(value, dict): | ||||||
|                 mongo_query[key].update(value) |                 mongo_query[key].update(value) | ||||||
|                 # $max/minDistance needs to come last - convert to SON |                 # $max/minDistance needs to come last - convert to SON | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								setup.py
									
									
									
									
									
								
							| @@ -108,6 +108,10 @@ CLASSIFIERS = [ | |||||||
|     "Topic :: Software Development :: Libraries :: Python Modules", |     "Topic :: Software Development :: Libraries :: Python Modules", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | PYTHON_VERSION = sys.version_info[0] | ||||||
|  | PY3 = PYTHON_VERSION == 3 | ||||||
|  | PY2 = PYTHON_VERSION == 2 | ||||||
|  |  | ||||||
| extra_opts = { | extra_opts = { | ||||||
|     "packages": find_packages(exclude=["tests", "tests.*"]), |     "packages": find_packages(exclude=["tests", "tests.*"]), | ||||||
|     "tests_require": [ |     "tests_require": [ | ||||||
| @@ -115,11 +119,12 @@ extra_opts = { | |||||||
|         "pytest-cov", |         "pytest-cov", | ||||||
|         "coverage<5.0",  # recent coverage switched to sqlite format for the .coverage file which isn't handled properly by coveralls |         "coverage<5.0",  # recent coverage switched to sqlite format for the .coverage file which isn't handled properly by coveralls | ||||||
|         "blinker", |         "blinker", | ||||||
|         "Pillow>=2.0.0", |         "Pillow>=2.0.0, <7.0.0",  # 7.0.0 dropped Python2 support | ||||||
|  |         "zipp<2.0.0",  # (dependency of pytest) dropped python2 support | ||||||
|     ], |     ], | ||||||
| } | } | ||||||
|  |  | ||||||
| if sys.version_info[0] == 3: | if PY3: | ||||||
|     if "test" in sys.argv: |     if "test" in sys.argv: | ||||||
|         extra_opts["packages"] = find_packages() |         extra_opts["packages"] = find_packages() | ||||||
|         extra_opts["package_data"] = { |         extra_opts["package_data"] = { | ||||||
| @@ -143,7 +148,7 @@ setup( | |||||||
|     long_description=LONG_DESCRIPTION, |     long_description=LONG_DESCRIPTION, | ||||||
|     platforms=["any"], |     platforms=["any"], | ||||||
|     classifiers=CLASSIFIERS, |     classifiers=CLASSIFIERS, | ||||||
|     install_requires=["pymongo>=3.4", "six", "future"], |     install_requires=["pymongo>=3.4, <4.0", "six>=1.10.0", "future"], | ||||||
|     cmdclass={"test": PyTest}, |     cmdclass={"test": PyTest}, | ||||||
|     **extra_opts |     **extra_opts | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ from tests.utils import MongoDBTestCase, get_as_pymongo | |||||||
| TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), "../fields/mongoengine.png") | TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), "../fields/mongoengine.png") | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestInstance(MongoDBTestCase): | class TestDocumentInstance(MongoDBTestCase): | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         class Job(EmbeddedDocument): |         class Job(EmbeddedDocument): | ||||||
|             name = StringField() |             name = StringField() | ||||||
| @@ -3319,6 +3319,39 @@ class TestInstance(MongoDBTestCase): | |||||||
|         f1.ref  # Dereferences lazily |         f1.ref  # Dereferences lazily | ||||||
|         assert f1 == f2 |         assert f1 == f2 | ||||||
|  |  | ||||||
|  |     def test_embedded_document_equality_with_lazy_ref(self): | ||||||
|  |         class Job(EmbeddedDocument): | ||||||
|  |             boss = LazyReferenceField("Person") | ||||||
|  |             boss_dbref = LazyReferenceField("Person", dbref=True) | ||||||
|  |  | ||||||
|  |         class Person(Document): | ||||||
|  |             job = EmbeddedDocumentField(Job) | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         boss = Person() | ||||||
|  |         worker = Person(job=Job(boss=boss, boss_dbref=boss)) | ||||||
|  |         boss.save() | ||||||
|  |         worker.save() | ||||||
|  |  | ||||||
|  |         worker1 = Person.objects.get(id=worker.id) | ||||||
|  |  | ||||||
|  |         # worker1.job should be equal to the job used originally to create the | ||||||
|  |         # document. | ||||||
|  |         assert worker1.job == worker.job | ||||||
|  |  | ||||||
|  |         # worker1.job should be equal to a newly created Job EmbeddedDocument | ||||||
|  |         # using either the Boss object or his ID. | ||||||
|  |         assert worker1.job == Job(boss=boss, boss_dbref=boss) | ||||||
|  |         assert worker1.job == Job(boss=boss.id, boss_dbref=boss.id) | ||||||
|  |  | ||||||
|  |         # The above equalities should also hold after worker1.job.boss has been | ||||||
|  |         # fetch()ed. | ||||||
|  |         worker1.job.boss.fetch() | ||||||
|  |         assert worker1.job == worker.job | ||||||
|  |         assert worker1.job == Job(boss=boss, boss_dbref=boss) | ||||||
|  |         assert worker1.job == Job(boss=boss.id, boss_dbref=boss.id) | ||||||
|  |  | ||||||
|     def test_dbref_equality(self): |     def test_dbref_equality(self): | ||||||
|         class Test2(Document): |         class Test2(Document): | ||||||
|             name = StringField() |             name = StringField() | ||||||
| @@ -3584,6 +3617,51 @@ class TestInstance(MongoDBTestCase): | |||||||
|             assert b._instance == a |             assert b._instance == a | ||||||
|         assert idx == 2 |         assert idx == 2 | ||||||
|  |  | ||||||
|  |     def test_updating_listfield_manipulate_list(self): | ||||||
|  |         class Company(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             employees = ListField(field=DictField()) | ||||||
|  |  | ||||||
|  |         Company.drop_collection() | ||||||
|  |  | ||||||
|  |         comp = Company(name="BigBank", employees=[{"name": "John"}]) | ||||||
|  |         comp.save() | ||||||
|  |         comp.employees.append({"name": "Bill"}) | ||||||
|  |         comp.save() | ||||||
|  |  | ||||||
|  |         stored_comp = get_as_pymongo(comp) | ||||||
|  |         self.assertEqual( | ||||||
|  |             stored_comp, | ||||||
|  |             { | ||||||
|  |                 "_id": comp.id, | ||||||
|  |                 "employees": [{"name": "John"}, {"name": "Bill"}], | ||||||
|  |                 "name": "BigBank", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         comp = comp.reload() | ||||||
|  |         comp.employees[0]["color"] = "red" | ||||||
|  |         comp.employees[-1]["color"] = "blue" | ||||||
|  |         comp.employees[-1].update({"size": "xl"}) | ||||||
|  |         comp.save() | ||||||
|  |  | ||||||
|  |         assert len(comp.employees) == 2 | ||||||
|  |         assert comp.employees[0] == {"name": "John", "color": "red"} | ||||||
|  |         assert comp.employees[1] == {"name": "Bill", "size": "xl", "color": "blue"} | ||||||
|  |  | ||||||
|  |         stored_comp = get_as_pymongo(comp) | ||||||
|  |         self.assertEqual( | ||||||
|  |             stored_comp, | ||||||
|  |             { | ||||||
|  |                 "_id": comp.id, | ||||||
|  |                 "employees": [ | ||||||
|  |                     {"name": "John", "color": "red"}, | ||||||
|  |                     {"size": "xl", "color": "blue", "name": "Bill"}, | ||||||
|  |                 ], | ||||||
|  |                 "name": "BigBank", | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_falsey_pk(self): |     def test_falsey_pk(self): | ||||||
|         """Ensure that we can create and update a document with Falsey PK.""" |         """Ensure that we can create and update a document with Falsey PK.""" | ||||||
|  |  | ||||||
| @@ -3660,13 +3738,13 @@ class TestInstance(MongoDBTestCase): | |||||||
|         value = u"I_should_be_a_dict" |         value = u"I_should_be_a_dict" | ||||||
|         coll.insert_one({"light_saber": value}) |         coll.insert_one({"light_saber": value}) | ||||||
|  |  | ||||||
|         with self.assertRaises(InvalidDocumentError) as cm: |         with pytest.raises(InvalidDocumentError) as exc_info: | ||||||
|             list(Jedi.objects) |             list(Jedi.objects) | ||||||
|  |  | ||||||
|         self.assertEqual( |         assert str( | ||||||
|             str(cm.exception), |             exc_info.value | ||||||
|             "Invalid data to create a `Jedi` instance.\nField 'light_saber' - The source SON object needs to be of type 'dict' but a '%s' was found" |         ) == "Invalid data to create a `Jedi` instance.\nField 'light_saber' - The source SON object needs to be of type 'dict' but a '%s' was found" % type( | ||||||
|             % type(value), |             value | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import pytest | |||||||
|  |  | ||||||
| from mongoengine import * | from mongoengine import * | ||||||
| from mongoengine.base import BaseDict | from mongoengine.base import BaseDict | ||||||
|  | from mongoengine.mongodb_support import MONGODB_36, get_mongodb_version | ||||||
|  |  | ||||||
| from tests.utils import MongoDBTestCase, get_as_pymongo | from tests.utils import MongoDBTestCase, get_as_pymongo | ||||||
|  |  | ||||||
| @@ -43,11 +44,7 @@ class TestDictField(MongoDBTestCase): | |||||||
|         with pytest.raises(ValidationError): |         with pytest.raises(ValidationError): | ||||||
|             post.validate() |             post.validate() | ||||||
|  |  | ||||||
|         post.info = {"the.title": "test"} |         post.info = {"$title.test": "test"} | ||||||
|         with pytest.raises(ValidationError): |  | ||||||
|             post.validate() |  | ||||||
|  |  | ||||||
|         post.info = {"nested": {"the.title": "test"}} |  | ||||||
|         with pytest.raises(ValidationError): |         with pytest.raises(ValidationError): | ||||||
|             post.validate() |             post.validate() | ||||||
|  |  | ||||||
| @@ -55,6 +52,20 @@ class TestDictField(MongoDBTestCase): | |||||||
|         with pytest.raises(ValidationError): |         with pytest.raises(ValidationError): | ||||||
|             post.validate() |             post.validate() | ||||||
|  |  | ||||||
|  |         post.info = {"nested": {"the.title": "test"}} | ||||||
|  |         if get_mongodb_version() < MONGODB_36: | ||||||
|  |             with pytest.raises(ValidationError): | ||||||
|  |                 post.validate() | ||||||
|  |         else: | ||||||
|  |             post.validate() | ||||||
|  |  | ||||||
|  |         post.info = {"dollar_and_dot": {"te$st.test": "test"}} | ||||||
|  |         if get_mongodb_version() < MONGODB_36: | ||||||
|  |             with pytest.raises(ValidationError): | ||||||
|  |                 post.validate() | ||||||
|  |         else: | ||||||
|  |             post.validate() | ||||||
|  |  | ||||||
|         post.info = {"title": "test"} |         post.info = {"title": "test"} | ||||||
|         post.save() |         post.save() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -151,7 +151,7 @@ class TestFileField(MongoDBTestCase): | |||||||
|         result = StreamFile.objects.first() |         result = StreamFile.objects.first() | ||||||
|         assert streamfile == result |         assert streamfile == result | ||||||
|         assert result.the_file.read() == text + more_text |         assert result.the_file.read() == text + more_text | ||||||
|         # self.assertEqual(result.the_file.content_type, content_type) |         # assert result.the_file.content_type == content_type | ||||||
|         result.the_file.seek(0) |         result.the_file.seek(0) | ||||||
|         assert result.the_file.tell() == 0 |         assert result.the_file.tell() == 0 | ||||||
|         assert result.the_file.read(len(text)) == text |         assert result.the_file.read(len(text)) == text | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ import six | |||||||
| from six import iteritems | from six import iteritems | ||||||
|  |  | ||||||
| from mongoengine import * | from mongoengine import * | ||||||
| from mongoengine.connection import get_connection, get_db | from mongoengine.connection import get_db | ||||||
| from mongoengine.context_managers import query_counter, switch_db | from mongoengine.context_managers import query_counter, switch_db | ||||||
| from mongoengine.errors import InvalidQueryError | from mongoengine.errors import InvalidQueryError | ||||||
| from mongoengine.mongodb_support import MONGODB_36, get_mongodb_version | from mongoengine.mongodb_support import MONGODB_36, get_mongodb_version | ||||||
| @@ -4658,21 +4658,6 @@ class TestQueryset(unittest.TestCase): | |||||||
|         ) |         ) | ||||||
|         assert_read_pref(bars, ReadPreference.SECONDARY_PREFERRED) |         assert_read_pref(bars, ReadPreference.SECONDARY_PREFERRED) | ||||||
|  |  | ||||||
|     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() |  | ||||||
|         assert ( |  | ||||||
|             bars._CommandCursor__collection.read_preference |  | ||||||
|             == ReadPreference.SECONDARY_PREFERRED |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|     def test_json_simple(self): |     def test_json_simple(self): | ||||||
|         class Embedded(EmbeddedDocument): |         class Embedded(EmbeddedDocument): | ||||||
|             string = StringField() |             string = StringField() | ||||||
| @@ -5399,225 +5384,6 @@ class TestQueryset(unittest.TestCase): | |||||||
|             assert Person.objects.first().name == "A" |             assert Person.objects.first().name == "A" | ||||||
|             assert Person.objects._has_data(), "Cursor has data and returned False" |             assert Person.objects._has_data(), "Cursor has data and returned False" | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_framework(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = Person.objects(age__lte=22).aggregate( |  | ||||||
|             {"$project": {"name": {"$toUpper": "$name"}}} |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [ |  | ||||||
|             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, |  | ||||||
|             {"_id": p2.pk, "name": "WILSON JUNIOR"}, |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects(age__lte=22) |  | ||||||
|             .order_by("-name") |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [ |  | ||||||
|             {"_id": p2.pk, "name": "WILSON JUNIOR"}, |  | ||||||
|             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects(age__gte=17, age__lte=40) |  | ||||||
|             .order_by("-age") |  | ||||||
|             .aggregate( |  | ||||||
|                 {"$group": {"_id": None, "total": {"$sum": 1}, "avg": {"$avg": "$age"}}} |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         assert list(data) == [{"_id": None, "avg": 29, "total": 2}] |  | ||||||
|  |  | ||||||
|         data = Person.objects().aggregate({"$match": {"name": "Isabella Luanna"}}) |  | ||||||
|         assert list(data) == [{u"_id": p1.pk, u"age": 16, u"name": u"Isabella Luanna"}] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_skip(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = Person.objects.skip(1).aggregate( |  | ||||||
|             {"$project": {"name": {"$toUpper": "$name"}}} |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [ |  | ||||||
|             {"_id": p2.pk, "name": "WILSON JUNIOR"}, |  | ||||||
|             {"_id": p3.pk, "name": "SANDRA MARA"}, |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_limit(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = Person.objects.limit(1).aggregate( |  | ||||||
|             {"$project": {"name": {"$toUpper": "$name"}}} |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p1.pk, "name": "ISABELLA LUANNA"}] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_sort(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = Person.objects.order_by("name").aggregate( |  | ||||||
|             {"$project": {"name": {"$toUpper": "$name"}}} |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [ |  | ||||||
|             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, |  | ||||||
|             {"_id": p3.pk, "name": "SANDRA MARA"}, |  | ||||||
|             {"_id": p2.pk, "name": "WILSON JUNIOR"}, |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_skip_with_limit(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = list( |  | ||||||
|             Person.objects.skip(1) |  | ||||||
|             .limit(1) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p2.pk, "name": "WILSON JUNIOR"}] |  | ||||||
|  |  | ||||||
|         # Make sure limit/skip chaining order has no impact |  | ||||||
|         data2 = ( |  | ||||||
|             Person.objects.limit(1) |  | ||||||
|             .skip(1) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert data == list(data2) |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_sort_with_limit(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects.order_by("name") |  | ||||||
|             .limit(2) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [ |  | ||||||
|             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, |  | ||||||
|             {"_id": p3.pk, "name": "SANDRA MARA"}, |  | ||||||
|         ] |  | ||||||
|  |  | ||||||
|         # Verify adding limit/skip steps works as expected |  | ||||||
|         data = ( |  | ||||||
|             Person.objects.order_by("name") |  | ||||||
|             .limit(2) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}, {"$limit": 1}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p1.pk, "name": "ISABELLA LUANNA"}] |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects.order_by("name") |  | ||||||
|             .limit(2) |  | ||||||
|             .aggregate( |  | ||||||
|                 {"$project": {"name": {"$toUpper": "$name"}}}, |  | ||||||
|                 {"$skip": 1}, |  | ||||||
|                 {"$limit": 1}, |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p3.pk, "name": "SANDRA MARA"}] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_sort_with_skip(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects.order_by("name") |  | ||||||
|             .skip(2) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p2.pk, "name": "WILSON JUNIOR"}] |  | ||||||
|  |  | ||||||
|     def test_queryset_aggregation_with_sort_with_skip_with_limit(self): |  | ||||||
|         class Person(Document): |  | ||||||
|             name = StringField() |  | ||||||
|             age = IntField() |  | ||||||
|  |  | ||||||
|         Person.drop_collection() |  | ||||||
|  |  | ||||||
|         p1 = Person(name="Isabella Luanna", age=16) |  | ||||||
|         p2 = Person(name="Wilson Junior", age=21) |  | ||||||
|         p3 = Person(name="Sandra Mara", age=37) |  | ||||||
|         Person.objects.insert([p1, p2, p3]) |  | ||||||
|  |  | ||||||
|         data = ( |  | ||||||
|             Person.objects.order_by("name") |  | ||||||
|             .skip(1) |  | ||||||
|             .limit(1) |  | ||||||
|             .aggregate({"$project": {"name": {"$toUpper": "$name"}}}) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         assert list(data) == [{"_id": p3.pk, "name": "SANDRA MARA"}] |  | ||||||
|  |  | ||||||
|     def test_delete_count(self): |     def test_delete_count(self): | ||||||
|         [self.Person(name="User {0}".format(i), age=i * 10).save() for i in range(1, 4)] |         [self.Person(name="User {0}".format(i), age=i * 10).save() for i in range(1, 4)] | ||||||
|         assert ( |         assert ( | ||||||
|   | |||||||
							
								
								
									
										255
									
								
								tests/queryset/test_queryset_aggregation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								tests/queryset/test_queryset_aggregation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | |||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  |  | ||||||
|  | import unittest | ||||||
|  | import warnings | ||||||
|  |  | ||||||
|  | from pymongo.read_preferences import ReadPreference | ||||||
|  |  | ||||||
|  | from mongoengine import * | ||||||
|  | from tests.utils import MongoDBTestCase | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestQuerysetAggregate(MongoDBTestCase): | ||||||
|  |     def test_read_preference_aggregation_framework(self): | ||||||
|  |         class Bar(Document): | ||||||
|  |             txt = StringField() | ||||||
|  |  | ||||||
|  |             meta = {"indexes": ["txt"]} | ||||||
|  |  | ||||||
|  |         # Aggregates with read_preference | ||||||
|  |         pipeline = [] | ||||||
|  |         bars = Bar.objects.read_preference( | ||||||
|  |             ReadPreference.SECONDARY_PREFERRED | ||||||
|  |         ).aggregate(pipeline) | ||||||
|  |         assert ( | ||||||
|  |             bars._CommandCursor__collection.read_preference | ||||||
|  |             == ReadPreference.SECONDARY_PREFERRED | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_framework(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects(age__lte=22).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, | ||||||
|  |             {"_id": p2.pk, "name": "WILSON JUNIOR"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects(age__lte=22).order_by("-name").aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p2.pk, "name": "WILSON JUNIOR"}, | ||||||
|  |             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         pipeline = [ | ||||||
|  |             {"$group": {"_id": None, "total": {"$sum": 1}, "avg": {"$avg": "$age"}}} | ||||||
|  |         ] | ||||||
|  |         data = ( | ||||||
|  |             Person.objects(age__gte=17, age__lte=40) | ||||||
|  |             .order_by("-age") | ||||||
|  |             .aggregate(pipeline) | ||||||
|  |         ) | ||||||
|  |         assert list(data) == [{"_id": None, "avg": 29, "total": 2}] | ||||||
|  |  | ||||||
|  |         pipeline = [{"$match": {"name": "Isabella Luanna"}}] | ||||||
|  |         data = Person.objects().aggregate(pipeline) | ||||||
|  |         assert list(data) == [{u"_id": p1.pk, u"age": 16, u"name": u"Isabella Luanna"}] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_skip(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.skip(1).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p2.pk, "name": "WILSON JUNIOR"}, | ||||||
|  |             {"_id": p3.pk, "name": "SANDRA MARA"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_limit(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.limit(1).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p1.pk, "name": "ISABELLA LUANNA"}] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_sort(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.order_by("name").aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, | ||||||
|  |             {"_id": p3.pk, "name": "SANDRA MARA"}, | ||||||
|  |             {"_id": p2.pk, "name": "WILSON JUNIOR"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_skip_with_limit(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = list(Person.objects.skip(1).limit(1).aggregate(pipeline)) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p2.pk, "name": "WILSON JUNIOR"}] | ||||||
|  |  | ||||||
|  |         # Make sure limit/skip chaining order has no impact | ||||||
|  |         data2 = Person.objects.limit(1).skip(1).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert data == list(data2) | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_sort_with_limit(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.order_by("name").limit(2).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, | ||||||
|  |             {"_id": p3.pk, "name": "SANDRA MARA"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         # Verify adding limit/skip steps works as expected | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}, {"$limit": 1}] | ||||||
|  |         data = Person.objects.order_by("name").limit(2).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p1.pk, "name": "ISABELLA LUANNA"}] | ||||||
|  |  | ||||||
|  |         pipeline = [ | ||||||
|  |             {"$project": {"name": {"$toUpper": "$name"}}}, | ||||||
|  |             {"$skip": 1}, | ||||||
|  |             {"$limit": 1}, | ||||||
|  |         ] | ||||||
|  |         data = Person.objects.order_by("name").limit(2).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p3.pk, "name": "SANDRA MARA"}] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_sort_with_skip(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.order_by("name").skip(2).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p2.pk, "name": "WILSON JUNIOR"}] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_with_sort_with_skip_with_limit(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |             age = IntField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna", age=16) | ||||||
|  |         p2 = Person(name="Wilson Junior", age=21) | ||||||
|  |         p3 = Person(name="Sandra Mara", age=37) | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |         data = Person.objects.order_by("name").skip(1).limit(1).aggregate(pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p3.pk, "name": "SANDRA MARA"}] | ||||||
|  |  | ||||||
|  |     def test_queryset_aggregation_deprecated_interface(self): | ||||||
|  |         class Person(Document): | ||||||
|  |             name = StringField() | ||||||
|  |  | ||||||
|  |         Person.drop_collection() | ||||||
|  |  | ||||||
|  |         p1 = Person(name="Isabella Luanna") | ||||||
|  |         p2 = Person(name="Wilson Junior") | ||||||
|  |         p3 = Person(name="Sandra Mara") | ||||||
|  |         Person.objects.insert([p1, p2, p3]) | ||||||
|  |  | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}] | ||||||
|  |  | ||||||
|  |         # Make sure a warning is emitted | ||||||
|  |         with warnings.catch_warnings(): | ||||||
|  |             warnings.simplefilter("error", DeprecationWarning) | ||||||
|  |             with self.assertRaises(DeprecationWarning): | ||||||
|  |                 Person.objects.order_by("name").limit(2).aggregate(*pipeline) | ||||||
|  |  | ||||||
|  |         # Make sure old interface works as expected with a 1-step pipeline | ||||||
|  |         data = Person.objects.order_by("name").limit(2).aggregate(*pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [ | ||||||
|  |             {"_id": p1.pk, "name": "ISABELLA LUANNA"}, | ||||||
|  |             {"_id": p3.pk, "name": "SANDRA MARA"}, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         # Make sure old interface works as expected with a 2-steps pipeline | ||||||
|  |         pipeline = [{"$project": {"name": {"$toUpper": "$name"}}}, {"$limit": 1}] | ||||||
|  |         data = Person.objects.order_by("name").limit(2).aggregate(*pipeline) | ||||||
|  |  | ||||||
|  |         assert list(data) == [{"_id": p1.pk, "name": "ISABELLA LUANNA"}] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     unittest.main() | ||||||
| @@ -24,6 +24,12 @@ class TestTransform(unittest.TestCase): | |||||||
|         } |         } | ||||||
|         assert transform.query(friend__age__gte=30) == {"friend.age": {"$gte": 30}} |         assert transform.query(friend__age__gte=30) == {"friend.age": {"$gte": 30}} | ||||||
|         assert transform.query(name__exists=True) == {"name": {"$exists": True}} |         assert transform.query(name__exists=True) == {"name": {"$exists": True}} | ||||||
|  |         assert transform.query(name=["Mark"], __raw__={"name": {"$in": "Tom"}}) == { | ||||||
|  |             "$and": [{"name": ["Mark"]}, {"name": {"$in": "Tom"}}] | ||||||
|  |         } | ||||||
|  |         assert transform.query(name__in=["Tom"], __raw__={"name": "Mark"}) == { | ||||||
|  |             "$and": [{"name": {"$in": ["Tom"]}}, {"name": "Mark"}] | ||||||
|  |         } | ||||||
|  |  | ||||||
|     def test_transform_update(self): |     def test_transform_update(self): | ||||||
|         class LisDoc(Document): |         class LisDoc(Document): | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = {py27,py35,pypy,pypy3}-{mg34,mg36} | envlist = {py27,py35,pypy,pypy3}-{mg34,mg36,mg39,mg310} | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
| commands = | commands = | ||||||
| @@ -7,6 +7,7 @@ commands = | |||||||
| deps = | deps = | ||||||
|     mg34: pymongo>=3.4,<3.5 |     mg34: pymongo>=3.4,<3.5 | ||||||
|     mg36: pymongo>=3.6,<3.7 |     mg36: pymongo>=3.6,<3.7 | ||||||
|     mg39: pymongo>=3.9,<4.0 |     mg39: pymongo>=3.9,<3.10 | ||||||
|  |     mg310: pymongo>=3.10,<3.11 | ||||||
| setenv = | setenv = | ||||||
|     PYTHON_EGG_CACHE = {envdir}/python-eggs |     PYTHON_EGG_CACHE = {envdir}/python-eggs | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user