summaryrefslogtreecommitdiff
diff options
authorHåvard Flaget Aasen <haavard_aasen@yahoo.no>2020-12-24 15:38:08 -0500
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2020-12-25 04:35:20 +0000
commit6c5213d32dd643b56691b7877ea7ef279a70ac77 (patch)
tree8df6326cb9c627dde8e1a70a3929aa1084b93dfc
parent70abf305fbb19f5880b12cb630f9d5715d33b43d (diff)
Imported using git-ubuntu import.
Notes
Notes: [ Ondřej Nový ] * d/control: Update Maintainer field with new Debian Python Team contact address. * d/control: Update Vcs-* fields with new Debian Python Team Salsa layout. [ Håvard Flaget Aasen ] * New upstream version 0.21.0 * d/watch: - Update to version 4 - Use the new substitution string from uscan. * d/control: - Update Standards-Version to 4.5.1 - Drop version restriction on suggested packages. * d/rules: Change target to "execute_after" * autopkgtest: Mark tests as superficial.
-rw-r--r--AUTHORS2
-rw-r--r--PKG-INFO5
-rw-r--r--README.rst3
-rw-r--r--debian/changelog21
-rw-r--r--debian/control14
-rwxr-xr-xdebian/rules3
-rw-r--r--debian/tests/control2
-rw-r--r--debian/watch4
-rw-r--r--docs/Makefile2
-rw-r--r--docs/changelog.rst22
-rw-r--r--docs/conf.py1
-rw-r--r--docs/guide/connecting.rst2
-rw-r--r--docs/guide/defining-documents.rst14
-rw-r--r--docs/guide/document-instances.rst29
-rw-r--r--docs/guide/index.rst2
-rw-r--r--docs/guide/migration.rst267
-rw-r--r--docs/guide/querying.rst2
-rw-r--r--docs/guide/validation.rst123
-rw-r--r--docs/requirements.txt3
-rw-r--r--mongoengine.egg-info/PKG-INFO5
-rw-r--r--mongoengine.egg-info/SOURCES.txt3
-rw-r--r--mongoengine/__init__.py2
-rw-r--r--mongoengine/base/datastructures.py4
-rw-r--r--mongoengine/base/document.py106
-rw-r--r--mongoengine/document.py39
-rw-r--r--mongoengine/fields.py75
-rw-r--r--mongoengine/pymongo_support.py41
-rw-r--r--mongoengine/queryset/base.py93
-rw-r--r--mongoengine/queryset/queryset.py1
-rw-r--r--mongoengine/queryset/visitor.py16
30 files changed, 736 insertions, 170 deletions
diff --git a/AUTHORS b/AUTHORS
index 02e4395..1cf7d78 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -257,3 +257,5 @@ that much better:
* Matthew Simpson (https://github.com/mcsimps2)
* Leonardo Domingues (https://github.com/leodmgs)
* Agustin Barto (https://github.com/abarto)
+ * Stankiewicz Mateusz (https://github.com/mas15)
+ * Felix Schultheiß (https://github.com/felix-smashdocs)
diff --git a/PKG-INFO b/PKG-INFO
index c90c251..3d5ce07 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: mongoengine
-Version: 0.20.0
+Version: 0.21.0
Summary: MongoEngine is a Python Object-Document Mapper for working with MongoDB.
Home-page: http://mongoengine.org/
Author: Harry Marr
@@ -27,6 +27,9 @@ Description: ===========
:target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
diff --git a/README.rst b/README.rst
index aca8edc..7b15c4e 100644
--- a/README.rst
+++ b/README.rst
@@ -16,6 +16,9 @@ MongoEngine
:target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
diff --git a/debian/changelog b/debian/changelog
index e37262f..fe86e0c 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,24 @@
+python-mongoengine (0.21.0-1) unstable; urgency=medium
+
+ [ Ondřej Nový ]
+ * d/control: Update Maintainer field with new Debian Python Team
+ contact address.
+ * d/control: Update Vcs-* fields with new Debian Python Team Salsa
+ layout.
+
+ [ Håvard Flaget Aasen ]
+ * New upstream version 0.21.0
+ * d/watch:
+ - Update to version 4
+ - Use the new substitution string from uscan.
+ * d/control:
+ - Update Standards-Version to 4.5.1
+ - Drop version restriction on suggested packages.
+ * d/rules: Change target to "execute_after"
+ * autopkgtest: Mark tests as superficial.
+
+ -- Håvard Flaget Aasen <haavard_aasen@yahoo.no> Thu, 24 Dec 2020 15:38:08 -0500
+
python-mongoengine (0.20.0-1) unstable; urgency=medium
* New upstream version 0.20.0
diff --git a/debian/control b/debian/control
index 0df1d6a..9653246 100644
--- a/debian/control
+++ b/debian/control
@@ -1,7 +1,7 @@
Source: python-mongoengine
Section: python
Priority: optional
-Maintainer: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
+Maintainer: Debian Python Team <team+python@tracker.debian.org>
Uploaders: Håvard Flaget Aasen <haavard_aasen@yahoo.no>
Build-Depends:
debhelper-compat (= 13),
@@ -15,11 +15,11 @@ Build-Depends:
python3-setuptools,
python3-sphinx,
python3-sphinx-rtd-theme,
-Standards-Version: 4.5.0
+Standards-Version: 4.5.1
Rules-Requires-Root: no
Homepage: http://mongoengine.org/
-Vcs-Git: https://salsa.debian.org/python-team/modules/python-mongoengine.git
-Vcs-Browser: https://salsa.debian.org/python-team/modules/python-mongoengine
+Vcs-Git: https://salsa.debian.org/python-team/packages/python-mongoengine.git
+Vcs-Browser: https://salsa.debian.org/python-team/packages/python-mongoengine
Package: python3-mongoengine
Architecture: all
@@ -32,9 +32,9 @@ Provides:
${python3:Provides},
Suggests:
python-mongoengine-doc,
- python3-blinker (>= 1.3),
- python3-dateutil (>= 2.1.0),
- python3-pil (>= 2.0.0),
+ python3-blinker,
+ python3-dateutil,
+ python3-pil,
Description: Python 3 Document-Object Mapper for working with MongoDB
MongoEngine is a Document-Object Mapper (think ORM, but for document
databases) for working with MongoDB from Python. It uses a simple declarative
diff --git a/debian/rules b/debian/rules
index 4053172..37868b3 100755
--- a/debian/rules
+++ b/debian/rules
@@ -7,6 +7,5 @@ export PYBUILD_DISABLE=test
%:
dh $@ --with sphinxdoc,python3 --buildsystem=pybuild
-override_dh_auto_build:
+execute_after_dh_auto_build:
PYTHONPATH=$(CURDIR) $(MAKE) -C docs html
- dh_auto_build
diff --git a/debian/tests/control b/debian/tests/control
index 9a173b7..22c3918 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,5 +1,7 @@
Test-Command: cd "$AUTOPKGTEST_TMP"; python3 -c "import mongoengine; print(mongoengine)"
Depends: python3-mongoengine
+Restrictions: superficial
Tests: mongo-test.py
Depends: python3-mongoengine
+Restrictions: superficial
diff --git a/debian/watch b/debian/watch
index 013a95c..233d25a 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,3 +1,3 @@
-version=3
+version=4
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
-https://pypi.debian.net/mongoengine/mongoengine-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+https://pypi.debian.net/mongoengine/mongoengine-@ANY_VERSION@@ARCHIVE_EXT@
diff --git a/docs/Makefile b/docs/Makefile
index 76eb910..cdc77c3 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -33,7 +33,7 @@ clean:
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+ @echo "Build finished. Check $(BUILDDIR)/html/index.html"
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2dd5bcd..247bd8e 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -7,6 +7,23 @@ Development
===========
- (Fill this out as you fix issues and develop your features).
+Changes in 0.21.0
+=================
+- Bug fix in DynamicDocument which is not parsing known fields in constructor like Document do #2412
+- When using pymongo >= 3.7, make use of Collection.count_documents instead of Collection.count
+ and Cursor.count that got deprecated in pymongo >= 3.7.
+ This should have a negative impact on performance of count see Issue #2219
+- Fix a bug that made the queryset drop the read_preference after clone().
+- Remove Py3.5 from CI as it reached EOL and add Python 3.9
+- Fix some issues related with db_field/field conflict in constructor #2414
+- BREAKING CHANGE: Fix the behavior of Doc.objects.limit(0) which should return all documents (similar to mongodb) #2311
+- Bug fix in ListField when updating the first item, it was saving the whole list, instead of
+ just replacing the first item (as usually done when updating 1 item of the list) #2392
+- Add EnumField: ``mongoengine.fields.EnumField``
+- Refactoring - Remove useless code related to Document.__only_fields and Queryset.only_fields
+- Fix query transformation regarding special operators #2365
+- Bug Fix: Document.save() fails when shard_key is not _id #2154
+
Changes in 0.20.0
=================
- ATTENTION: Drop support for Python2
@@ -28,7 +45,7 @@ Changes in 0.20.0
Changes in 0.19.1
=================
-- Requires Pillow < 7.0.0 as it dropped Python2 support
+- Tests require 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
@@ -456,9 +473,6 @@ Changes in 0.8.3
- Document.select_related() now respects ``db_alias`` (#377)
- Reload uses shard_key if applicable (#384)
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
-
- **Potential breaking change:** http://docs.mongoengine.org/en/latest/upgrade.html#to-0-8-3
-
- Fixed pickling dynamic documents ``_dynamic_fields`` (#387)
- Fixed ListField setslice and delslice dirty tracking (#390)
- Added Django 1.5 PY3 support (#392)
diff --git a/docs/conf.py b/docs/conf.py
index 48c8e85..fdb5b61 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
#
# MongoEngine documentation build configuration file, created by
# sphinx-quickstart on Sun Nov 22 18:14:13 2009.
diff --git a/docs/guide/connecting.rst b/docs/guide/connecting.rst
index ac2146a..03d6ed5 100644
--- a/docs/guide/connecting.rst
+++ b/docs/guide/connecting.rst
@@ -31,6 +31,8 @@ the :attr:`host` to
connect('project1', host='mongodb://localhost/database_name')
+.. note:: URI containing SRV records (e.g mongodb+srv://server.example.com/) can be used as well as the :attr:`host`
+
.. note:: Database, username and password from URI string overrides
corresponding parameters in :func:`~mongoengine.connect`: ::
diff --git a/docs/guide/defining-documents.rst b/docs/guide/defining-documents.rst
index 6dc35c3..7fc20ba 100644
--- a/docs/guide/defining-documents.rst
+++ b/docs/guide/defining-documents.rst
@@ -76,6 +76,7 @@ are as follows:
* :class:`~mongoengine.fields.EmailField`
* :class:`~mongoengine.fields.EmbeddedDocumentField`
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
+* :class:`~mongoengine.fields.EnumField`
* :class:`~mongoengine.fields.FileField`
* :class:`~mongoengine.fields.FloatField`
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
@@ -426,19 +427,6 @@ either a single field name, or a list or tuple of field names::
first_name = StringField()
last_name = StringField(unique_with='first_name')
-Skipping Document validation on save
-------------------------------------
-You can also skip the whole document validation process by setting
-``validate=False`` when calling the :meth:`~mongoengine.document.Document.save`
-method::
-
- class Recipient(Document):
- name = StringField()
- email = EmailField()
-
- recipient = Recipient(name='admin', email='root@localhost')
- recipient.save() # will raise a ValidationError while
- recipient.save(validate=False) # won't
Document collections
====================
diff --git a/docs/guide/document-instances.rst b/docs/guide/document-instances.rst
index 64f17c0..5dacc0d 100644
--- a/docs/guide/document-instances.rst
+++ b/docs/guide/document-instances.rst
@@ -41,35 +41,6 @@ already exist, then any changes will be updated atomically. For example::
.. seealso::
:ref:`guide-atomic-updates`
-Pre save data validation and cleaning
--------------------------------------
-MongoEngine allows you to create custom cleaning rules for your documents when
-calling :meth:`~mongoengine.Document.save`. By providing a custom
-:meth:`~mongoengine.Document.clean` method you can do any pre validation / data
-cleaning.
-
-This might be useful if you want to ensure a default value based on other
-document values for example::
-
- class Essay(Document):
- status = StringField(choices=('Published', 'Draft'), required=True)
- pub_date = DateTimeField()
-
- def clean(self):
- """Ensures that only published essays have a `pub_date` and
- 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)
- # Set the pub_date for published items if not set.
- if self.status == 'Published' and self.pub_date is None:
- self.pub_date = datetime.now()
-
-.. note::
- Cleaning is only called if validation is turned on and when calling
- :meth:`~mongoengine.Document.save`.
-
Cascading Saves
---------------
If your document contains :class:`~mongoengine.fields.ReferenceField` or
diff --git a/docs/guide/index.rst b/docs/guide/index.rst
index a0364ec..018b253 100644
--- a/docs/guide/index.rst
+++ b/docs/guide/index.rst
@@ -10,8 +10,10 @@ User Guide
defining-documents
document-instances
querying
+ validation
gridfs
signals
text-indexes
+ migration
logging-monitoring
mongomock
diff --git a/docs/guide/migration.rst b/docs/guide/migration.rst
new file mode 100644
index 0000000..2b8982e
--- /dev/null
+++ b/docs/guide/migration.rst
@@ -0,0 +1,267 @@
+===================
+Documents migration
+===================
+
+The structure of your documents and their associated mongoengine schemas are likely
+to change over the lifetime of an application. This section provides guidance and
+recommendations on how to deal with migrations.
+
+Due to the very flexible nature of mongodb, migrations of models aren't trivial and
+for people that know about `alembic` for `sqlalchemy`, there is unfortunately no equivalent
+library that will manage the migration in an automatic fashion for mongoengine.
+
+Example 1: Addition of a field
+==============================
+
+Let's start by taking a simple example of a model change and review the different option you
+have to deal with the migration.
+
+Let's assume we start with the following schema and save an instance:
+
+.. code-block:: python
+
+ class User(Document):
+ name = StringField()
+
+ User(name="John Doe").save()
+
+ # print the objects as they exist in mongodb
+ print(User.objects().as_pymongo()) # [{u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John Doe'}]
+
+On the next version of your application, let's now assume that a new field `enabled` gets added to the
+existing ``User`` model with a `default=True`. Thus you simply update the ``User`` class to the following:
+
+.. code-block:: python
+
+ class User(Document):
+ name = StringField(required=True)
+ enabled = BooleaField(default=True)
+
+Without applying any migration, we now reload an object from the database into the ``User`` class
+and checks its `enabled` attribute:
+
+.. code-block:: python
+
+ assert User.objects.count() == 1
+ user = User.objects().first()
+ assert user.enabled is True
+ assert User.objects(enabled=True).count() == 0 # uh?
+ assert User.objects(enabled=False).count() == 0 # uh?
+
+ # this is consistent with what we have in the database
+ # in fact, 'enabled' does not exist
+ print(User.objects().as_pymongo().first()) # {u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John'}
+ assert User.objects(enabled=None).count() == 1
+
+As you can see, even if the document wasn't updated, mongoengine applies the default value seamlessly when it
+loads the pymongo dict into a ``User`` instance. At first sight it looks like you don't need to migrate the
+existing documents when adding new fields but this actually leads to inconsistencies when it comes to querying.
+
+In fact, when querying, mongoengine isn't trying to account for the default value of the new field and so
+if you don't actually migrate the existing documents, you are taking a risk that querying/updating
+will be missing relevant record.
+
+When adding fields/modifying default values, you can use any of the following to do the migration
+as a standalone script:
+
+.. code-block:: python
+
+ # Use mongoengine to set a default value for a given field
+ User.objects().update(enabled=True)
+ # or use pymongo
+ user_coll = User._get_collection()
+ user_coll.update_many({}, {'$set': {'enabled': True}})
+
+
+Example 2: Inheritance change
+=============================
+
+Let's consider the following example:
+
+.. code-block:: python
+
+ class Human(Document):
+ name = StringField()
+ meta = {"allow_inheritance": True}
+
+ class Jedi(Human):
+ dark_side = BooleanField()
+ light_saber_color = StringField()
+
+ Jedi(name="Darth Vader", dark_side=True, light_saber_color="red").save()
+ Jedi(name="Obi Wan Kenobi", dark_side=False, light_saber_color="blue").save()
+
+ assert Human.objects.count() == 2
+ assert Jedi.objects.count() == 2
+
+ # Let's check how these documents got stored in mongodb
+ print(Jedi.objects.as_pymongo())
+ # [
+ # {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'},
+ # {'_id': ObjectId('5fac4ac4f61d7fb06046e0fa'), '_cls': 'Human.Jedi', 'name': 'Obi Wan Kenobi', 'dark_side': False, 'light_saber_color': 'blue'}
+ # ]
+
+As you can observe, when you use inheritance, MongoEngine stores a field named '_cls' behind the scene to keep
+track of the Document class.
+
+Let's now take the scenario that you want to refactor the inheritance schema and:
+- Have the Jedi's with dark_side=True/False become GoodJedi's/DarkSith
+- get rid of the 'dark_side' field
+
+move to the following schemas:
+
+.. code-block:: python
+
+ # unchanged
+ class Human(Document):
+ name = StringField()
+ meta = {"allow_inheritance": True}
+
+ # attribute 'dark_side' removed
+ class GoodJedi(Human):
+ light_saber_color = StringField()
+
+ # new class
+ class BadSith(Human):
+ light_saber_color = StringField()
+
+MongoEngine doesn't know about the change or how to map them with the existing data
+so if you don't apply any migration, you will observe a strange behavior, as if the collection was suddenly
+empty.
+
+.. code-block:: python
+
+ # As a reminder, the documents that we inserted
+ # have the _cls field = 'Human.Jedi'
+
+ # Following has no match
+ # because the query that is used behind the scene is
+ # filtering on {'_cls': 'Human.GoodJedi'}
+ assert GoodJedi.objects().count() == 0
+
+ # Following has also no match
+ # because it is filtering on {'_cls': {'$in': ('Human', 'Human.GoodJedi', 'Human.BadSith')}}
+ # which has no match
+ assert Human.objects.count() == 0
+ assert Human.objects.first() is None
+
+ # If we bypass MongoEngine and make use of underlying driver (PyMongo)
+ # we can see that the documents are there
+ humans_coll = Human._get_collection()
+ assert humans_coll.count_documents({}) == 2
+ # print first document
+ print(humans_coll.find_one())
+ # {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'}
+
+As you can see, first obvious problem is that we need to modify '_cls' values based on existing values of
+'dark_side' documents.
+
+.. code-block:: python
+
+ humans_coll = Human._get_collection()
+ old_class = 'Human.Jedi'
+ good_jedi_class = 'Human.GoodJedi'
+ bad_sith_class = 'Human.BadSith'
+ humans_coll.update_many({'_cls': old_class, 'dark_side': False}, {'$set': {'_cls': good_jedi_class}})
+ humans_coll.update_many({'_cls': old_class, 'dark_side': True}, {'$set': {'_cls': bad_sith_class}})
+
+Let's now check if querying improved in MongoEngine:
+
+.. code-block:: python
+
+ assert GoodJedi.objects().count() == 1 # Hoorah!
+ assert BadSith.objects().count() == 1 # Hoorah!
+ assert Human.objects.count() == 2 # Hoorah!
+
+ # let's now check that documents load correctly
+ jedi = GoodJedi.objects().first()
+ # raises FieldDoesNotExist: The fields "{'dark_side'}" do not exist on the document "Human.GoodJedi"
+
+In fact we only took care of renaming the _cls values but we havn't removed the 'dark_side' fields
+which does not exist anymore on the GoodJedi's and BadSith's models.
+Let's remove the field from the collections:
+
+.. code-block:: python
+
+ humans_coll = Human._get_collection()
+ humans_coll.update_many({}, {'$unset': {'dark_side': 1}})
+
+.. note:: We did this migration in 2 different steps for the sake of example but it could have been combined
+ with the migration of the _cls fields: ::
+
+ humans_coll.update_many(
+ {'_cls': old_class, 'dark_side': False},
+ {
+ '$set': {'_cls': good_jedi_class},
+ '$unset': {'dark_side': 1}
+ }
+ )
+
+
+And verify that the documents now load correctly:
+
+.. code-block:: python
+
+ jedi = GoodJedi.objects().first()
+ assert jedi.name == "Obi Wan Kenobi"
+
+ sith = BadSith.objects().first()
+ assert sith.name == "Darth Vader"
+
+
+An other way of dealing with this migration is to iterate over
+the documents and update/replace them one by one. This is way slower but
+it is often useful for complex migrations of Document models.
+
+.. code-block:: python
+
+ for doc in humans_coll.find():
+ if doc['_cls'] == 'Human.Jedi':
+ doc['_cls'] = 'Human.BadSith' if doc['dark_side'] else 'Human.GoodJedi'
+ doc.pop('dark_side')
+ humans_coll.replace_one({'_id': doc['_id']}, doc)
+
+.. warning:: Be aware of this `flaw <https://groups.google.com/g/mongodb-user/c/AFC1ia7MHzk>`_ if you modify documents while iterating
+
+Recommendations
+===============
+
+- Write migration scripts whenever you do changes to the model schemas
+- Using :class:`~mongoengine.DynamicDocument` or ``meta = {"strict": False}`` may help to avoid some migrations or to have the 2 versions of your application to co-exist.
+- Write post-processing checks to verify that migrations script worked. See below
+
+Post-processing checks
+======================
+
+The following recipe can be used to sanity check a Document collection after you applied migration.
+It does not make any assumption on what was migrated, it will fetch 1000 objects randomly and
+run some quick checks on the documents to make sure the document looks OK. As it is, it will fail
+on the first occurrence of an error but this is something that can be adapted based on your needs.
+
+.. code-block:: python
+
+ def get_random_oids(collection, sample_size):
+ pipeline = [{"$project": {'_id': 1}}, {"$sample": {"size": sample_size}}]
+ return [s['_id'] for s in collection.aggregate(pipeline)]
+
+ def get_random_documents(DocCls, sample_size):
+ doc_collection = DocCls._get_collection()
+ random_oids = get_random_oids(doc_collection, sample_size)
+ return DocCls.objects(id__in=random_oids)
+
+ def check_documents(DocCls, sample_size):
+ for doc in get_random_documents(DocCls, sample_size):
+ # general validation (types and values)
+ doc.validate()
+
+ # load all subfields,
+ # this may trigger additional queries if you have ReferenceFields
+ # so it may be slow
+ for field in doc._fields:
+ try:
+ getattr(doc, field)
+ except Exception:
+ LOG.warning(f"Could not load field {field} in Document {doc.id}")
+ raise
+
+ check_documents(Human, sample_size=1000)
diff --git a/docs/guide/querying.rst b/docs/guide/querying.rst
index 07de037..7307b00 100644
--- a/docs/guide/querying.rst
+++ b/docs/guide/querying.rst
@@ -609,7 +609,7 @@ to push values with index::
.. note::
Currently only top level lists are handled, future versions of mongodb /
pymongo plan to support nested positional operators. See `The $ positional
- operator <http://www.mongodb.org/display/DOCS/Updating#Updating-The%24positionaloperator>`_.
+ operator <https://docs.mongodb.com/manual/tutorial/update-documents/#Updating-The%24positionaloperator>`_.
Server-side javascript execution
================================
diff --git a/docs/guide/validation.rst b/docs/guide/validation.rst
new file mode 100644
index 0000000..971f6eb
--- /dev/null
+++ b/docs/guide/validation.rst
@@ -0,0 +1,123 @@
+====================
+Document Validation
+====================
+
+By design, MongoEngine strictly validates the documents right before they are inserted in MongoDB
+and makes sure they are consistent with the fields defined in your models.
+
+MongoEngine makes the assumption that the documents that exists in the DB are compliant with the schema.
+This means that Mongoengine will not validate a document when an object is loaded from the DB into an instance
+of your model but this operation may fail under some circumstances (e.g. if there is a field in
+the document fetched from the database that is not defined in your model).
+
+
+Built-in validation
+===================
+
+Mongoengine provides different fields that encapsulate the corresponding validation
+out of the box. Validation runs when calling `.validate()` or `.save()`
+
+.. code-block:: python
+
+ from mongoengine import Document, EmailField
+
+ class User(Document):
+ email = EmailField()
+ age = IntField(min_value=0, max_value=99)
+
+ user = User(email='invalid@', age=24)
+ user.validate() # raises ValidationError (Invalid email address: ['email'])
+ user.save() # raises ValidationError (Invalid email address: ['email'])
+
+ user2 = User(email='john.doe@garbage.com', age=1000)
+ user2.save() # raises ValidationError (Integer value is too large: ['age'])
+
+Custom validation
+=================
+
+The following feature can be used to customize the validation:
+
+* Field `validation` parameter
+
+.. code-block:: python
+
+ def not_john_doe(name):
+ if name == 'John Doe':
+ raise ValidationError("John Doe is not a valid name")
+
+ class Person(Document):
+ full_name = StringField(validation=not_john_doe)
+
+ Person(full_name='Billy Doe').save()
+ Person(full_name='John Doe').save() # raises ValidationError (John Doe is not a valid name)
+
+
+* Document `clean` method
+
+This method is called as part of :meth:`~mongoengine.document.Document.save` and should be used to provide
+custom model validation and/or to modify some of the field values prior to validation.
+For instance, you could use it to automatically provide a value for a field, or to do validation
+that requires access to more than a single field.
+
+.. code-block:: python
+
+ class Essay(Document):
+ status = StringField(choices=('Published', 'Draft'), required=True)
+ pub_date = DateTimeField()
+
+ def clean(self):
+ # Validate that only published essays have a `pub_date`
+ if self.status == 'Draft' and self.pub_date is not None:
+ raise ValidationError('Draft entries should not have a publication date.')
+ # Set the pub_date for published items if not set.
+ if self.status == 'Published' and self.pub_date is None:
+ self.pub_date = datetime.now()
+
+.. note::
+ Cleaning is only called if validation is turned on and when calling
+ :meth:`~mongoengine.Document.save`.
+
+* Adding custom Field classes
+
+We recommend as much as possible to use fields provided by MongoEngine. However, it is also possible
+to subclass a Field and encapsulate some validation by overriding the `validate` method
+
+.. code-block:: python
+
+ class AgeField(IntField):
+
+ def validate(self, value):
+ super(AgeField, self).validate(value) # let IntField.validate run first
+ if value == 60:
+ self.error('60 is not allowed')
+
+ class Person(Document):
+ age = AgeField(min_value=0, max_value=99)
+
+ Person(age=20).save() # passes
+ Person(age=1000).save() # raises ValidationError (Integer value is too large: ['age'])
+ Person(age=60).save() # raises ValidationError (Person:None) (60 is not allowed: ['age'])
+
+
+.. note::
+
+ When overriding `validate`, use `self.error("your-custom-error")` instead of raising ValidationError explicitly,
+ it will provide a better context with the error message
+
+Skipping validation
+====================
+
+Although discouraged as it allows to violate fields constraints, if for some reason you need to disable
+the validation and cleaning of a document when you call :meth:`~mongoengine.document.Document.save`, you can use `.save(validate=False)`.
+
+.. code-block:: python
+
+ class Person(Document):
+ age = IntField(max_value=100)
+
+ Person(age=1000).save() # raises ValidationError (Integer value is too large)
+
+ Person(age=1000).save(validate=False)
+ person = Person.objects.first()
+ assert person.age == 1000
+
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..abd1262
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,3 @@
+pymongo>=3.11
+Sphinx==3.2.1
+sphinx-rtd-theme==0.5.0
diff --git a/mongoengine.egg-info/PKG-INFO b/mongoengine.egg-info/PKG-INFO
index c90c251..3d5ce07 100644
--- a/mongoengine.egg-info/PKG-INFO
+++ b/mongoengine.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.2
Name: mongoengine
-Version: 0.20.0
+Version: 0.21.0
Summary: MongoEngine is a Python Object-Document Mapper for working with MongoDB.
Home-page: http://mongoengine.org/
Author: Harry Marr
@@ -27,6 +27,9 @@ Description: ===========
:target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
diff --git a/mongoengine.egg-info/SOURCES.txt b/mongoengine.egg-info/SOURCES.txt
index 35c381a..0e2d85a 100644
--- a/mongoengine.egg-info/SOURCES.txt
+++ b/mongoengine.egg-info/SOURCES.txt
@@ -11,6 +11,7 @@ docs/conf.py
docs/django.rst
docs/faq.rst
docs/index.rst
+docs/requirements.txt
docs/tutorial.rst
docs/upgrade.rst
docs/code/tumblelog.py
@@ -21,10 +22,12 @@ docs/guide/gridfs.rst
docs/guide/index.rst
docs/guide/installing.rst
docs/guide/logging-monitoring.rst
+docs/guide/migration.rst
docs/guide/mongomock.rst
docs/guide/querying.rst
docs/guide/signals.rst
docs/guide/text-indexes.rst
+docs/guide/validation.rst
mongoengine/__init__.py
mongoengine/common.py
mongoengine/connection.py
diff --git a/mongoengine/__init__.py b/mongoengine/__init__.py
index dbd88a6..d0a0f1f 100644
--- a/mongoengine/__init__.py
+++ b/mongoengine/__init__.py
@@ -28,7 +28,7 @@ __all__ = (
)
-VERSION = (0, 20, 0)
+VERSION = (0, 21, 0)
def get_version():
diff --git a/mongoengine/base/datastructures.py b/mongoengine/base/datastructures.py
index d08d493..2a8fde6 100644
--- a/mongoengine/base/datastructures.py
+++ b/mongoengine/base/datastructures.py
@@ -179,7 +179,7 @@ class BaseList(list):
def _mark_as_changed(self, key=None):
if hasattr(self._instance, "_mark_as_changed"):
- if key:
+ if key is not None:
self._instance._mark_as_changed(
"{}.{}".format(self._name, key % len(self))
)
@@ -215,7 +215,7 @@ class EmbeddedDocumentList(BaseList):
Filters the list by only including embedded documents with the
given keyword arguments.
- This method only supports simple comparison (e.g: .filter(name='John Doe'))
+ 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
diff --git a/mongoengine/base/document.py b/mongoengine/base/document.py
index e697fe4..019d62d 100644
--- a/mongoengine/base/document.py
+++ b/mongoengine/base/document.py
@@ -64,8 +64,6 @@ class BaseDocument:
It may contain additional reserved keywords, e.g. "__auto_convert".
:param __auto_convert: If True, supplied values will be converted
to Python-type values via each field's `to_python` method.
- :param __only_fields: A set of fields that have been loaded for
- this document. Empty if all fields have been loaded.
:param _created: Indicates whether this is a brand new document
or whether it's already been persisted before. Defaults to true.
"""
@@ -80,8 +78,6 @@ class BaseDocument:
__auto_convert = values.pop("__auto_convert", True)
- __only_fields = set(values.pop("__only_fields", values))
-
_created = values.pop("_created", True)
signals.pre_init.send(self.__class__, document=self, values=values)
@@ -105,37 +101,32 @@ class BaseDocument:
self._dynamic_fields = SON()
- # Assign default values to the instance.
- # We set default values only for fields loaded from DB. See
- # https://github.com/mongoengine/mongoengine/issues/399 for more info.
- for key, field in self._fields.items():
- if self._db_field_map.get(key, key) in __only_fields:
+ # Assign default values for fields
+ # not set in the constructor
+ for field_name in self._fields:
+ if field_name in values:
continue
- value = getattr(self, key, None)
- setattr(self, key, value)
+ value = getattr(self, field_name, None)
+ setattr(self, field_name, value)
if "_cls" not in values:
self._cls = self._class_name
- # Set passed values after initialisation
- if self._dynamic:
- dynamic_data = {}
- for key, value in values.items():
- if key in self._fields or key == "_id":
- setattr(self, key, value)
- else:
+ # Set actual values
+ dynamic_data = {}
+ FileField = _import_class("FileField")
+ for key, value in values.items():
+ field = self._fields.get(key)
+ if field or key in ("id", "pk", "_cls"):
+ if __auto_convert and value is not None:
+ if field and not isinstance(field, FileField):
+ value = field.to_python(value)
+ setattr(self, key, value)
+ else:
+ if self._dynamic:
dynamic_data[key] = value
- else:
- FileField = _import_class("FileField")
- for key, value in values.items():
- 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:
- field = self._fields.get(key)
- if field and not isinstance(field, FileField):
- value = field.to_python(value)
- setattr(self, key, value)
else:
+ # For strict Document
self._data[key] = value
# Set any get_<field>_display methods
@@ -314,7 +305,8 @@ class BaseDocument:
def clean(self):
"""
- Hook for doing document level data cleaning before validation is run.
+ Hook for doing document level data cleaning (usually validation or assignment)
+ before validation is run.
Any ValidationError raised by this method will not be associated with
a particular field; it will have a special-case association with the
@@ -537,6 +529,9 @@ class BaseDocument:
"""Using _get_changed_fields iterate and remove any fields that
are marked as changed.
"""
+ ReferenceField = _import_class("ReferenceField")
+ GenericReferenceField = _import_class("GenericReferenceField")
+
for changed in self._get_changed_fields():
parts = changed.split(".")
data = self
@@ -549,7 +544,8 @@ class BaseDocument:
elif isinstance(data, dict):
data = data.get(part, None)
else:
- data = getattr(data, part, None)
+ field_name = data._reverse_db_field_map.get(part, part)
+ data = getattr(data, field_name, None)
if not isinstance(data, LazyReference) and hasattr(
data, "_changed_fields"
@@ -558,10 +554,40 @@ class BaseDocument:
continue
data._changed_fields = []
+ elif isinstance(data, (list, tuple, dict)):
+ if hasattr(data, "field") and isinstance(
+ data.field, (ReferenceField, GenericReferenceField)
+ ):
+ continue
+ BaseDocument._nestable_types_clear_changed_fields(data)
self._changed_fields = []
- def _nestable_types_changed_fields(self, changed_fields, base_key, data):
+ @staticmethod
+ def _nestable_types_clear_changed_fields(data):
+ """Inspect nested data for changed fields
+
+ :param data: data to inspect for changes
+ """
+ Document = _import_class("Document")
+
+ # Loop list / dict fields as they contain documents
+ # Determine the iterator to use
+ if not hasattr(data, "items"):
+ iterator = enumerate(data)
+ else:
+ iterator = data.items()
+
+ for index_or_key, value in iterator:
+ if hasattr(value, "_get_changed_fields") and not isinstance(
+ value, Document
+ ): # don't follow references
+ value._clear_changed_fields()
+ elif isinstance(value, (list, tuple, dict)):
+ BaseDocument._nestable_types_clear_changed_fields(value)
+
+ @staticmethod
+ def _nestable_types_changed_fields(changed_fields, base_key, data):
"""Inspect nested data for changed fields
:param changed_fields: Previously collected changed fields
@@ -586,7 +612,9 @@ class BaseDocument:
changed = value._get_changed_fields()
changed_fields += ["{}{}".format(item_key, k) for k in changed if k]
elif isinstance(value, (list, tuple, dict)):
- self._nestable_types_changed_fields(changed_fields, item_key, value)
+ BaseDocument._nestable_types_changed_fields(
+ changed_fields, item_key, value
+ )
def _get_changed_fields(self):
"""Return a list of all fields that have explicitly been changed.
@@ -721,11 +749,9 @@ class BaseDocument:
return cls._meta.get("collection", None)
@classmethod
- def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False):
- """Create an instance of a Document (subclass) from a PyMongo SON."""
- if not only_fields:
- only_fields = []
-
+ def _from_son(cls, son, _auto_dereference=True, created=False):
+ """Create an instance of a Document (subclass) from a PyMongo SON (dict)
+ """
if son and not isinstance(son, dict):
raise ValueError(
"The source SON object needs to be of type 'dict' but a '%s' was found"
@@ -738,6 +764,8 @@ class BaseDocument:
# Convert SON to a data dict, making sure each key is a string and
# corresponds to the right db field.
+ # This is needed as _from_son is currently called both from BaseDocument.__init__
+ # and from EmbeddedDocumentField.to_python
data = {}
for key, value in son.items():
key = str(key)
@@ -780,9 +808,7 @@ class BaseDocument:
if cls.STRICT:
data = {k: v for k, v in data.items() if k in cls._fields}
- obj = cls(
- __auto_convert=False, _created=created, __only_fields=only_fields, **data
- )
+ obj = cls(__auto_convert=False, _created=created, **data)
obj._changed_fields = []
if not _auto_dereference:
obj._fields = fields
diff --git a/mongoengine/document.py b/mongoengine/document.py
index db64054..801c8df 100644
--- a/mongoengine/document.py
+++ b/mongoengine/document.py
@@ -464,9 +464,9 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
# 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
- )
+ select_dict = {"_id": doc["_id"]}
+ select_dict = self._integrate_shard_key(doc, select_dict)
+ raw_object = wc_collection.find_one_and_replace(select_dict, doc)
if raw_object:
return doc["_id"]
@@ -489,6 +489,23 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
return update_doc
+ def _integrate_shard_key(self, doc, select_dict):
+ """Integrates the collection's shard key to the `select_dict`, which will be used for the query.
+ The value from the shard key is taken from the `doc` and finally the select_dict is returned.
+ """
+
+ # Need to add shard key to query, or you get an error
+ shard_key = self._meta.get("shard_key", tuple())
+ for k in shard_key:
+ path = self._lookup_field(k.split("."))
+ actual_key = [p.db_field for p in path]
+ val = doc
+ for ak in actual_key:
+ val = val[ak]
+ select_dict[".".join(actual_key)] = val
+
+ return select_dict
+
def _save_update(self, doc, save_condition, write_concern):
"""Update an existing document.
@@ -504,15 +521,7 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
select_dict["_id"] = object_id
- # Need to add shard key to query, or you get an error
- shard_key = self._meta.get("shard_key", tuple())
- for k in shard_key:
- path = self._lookup_field(k.split("."))
- actual_key = [p.db_field for p in path]
- val = doc
- for ak in actual_key:
- val = val[ak]
- select_dict[".".join(actual_key)] = val
+ select_dict = self._integrate_shard_key(doc, select_dict)
update_doc = self._get_update_doc()
if update_doc:
@@ -639,7 +648,7 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
write_concern=write_concern, _from_doc_delete=True
)
except pymongo.errors.OperationFailure as err:
- message = "Could not delete document (%s)" % err.message
+ message = "Could not delete document (%s)" % err.args
raise OperationError(message)
signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
@@ -919,7 +928,7 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
@classmethod
def list_indexes(cls):
- """ Lists all of the indexes that should be created for given
+ """Lists all of the indexes that should be created for given
collection. It includes all the indexes from super- and sub-classes.
"""
if cls._meta.get("abstract"):
@@ -984,7 +993,7 @@ class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
@classmethod
def compare_indexes(cls):
- """ Compares the indexes defined in MongoEngine with the ones
+ """Compares the indexes defined in MongoEngine with the ones
existing in the database. Returns any missing/extra indexes.
"""
diff --git a/mongoengine/fields.py b/mongoengine/fields.py
index b05e726..8915d80 100644
--- a/mongoengine/fields.py
+++ b/mongoengine/fields.py
@@ -87,6 +87,7 @@ __all__ = (
"PolygonField",
"SequenceField",
"UUIDField",
+ "EnumField",
"MultiPointField",
"MultiLineStringField",
"MultiPolygonField",
@@ -433,7 +434,7 @@ class DecimalField(BaseField):
:param max_value: Validation rule for the maximum acceptable value.
:param force_string: Store the value as a string (instead of a float).
Be aware that this affects query sorting and operation like lte, gte (as string comparison is applied)
- and some query operator won't work (e.g: inc, dec)
+ and some query operator won't work (e.g. inc, dec)
:param precision: Number of decimal places to store.
:param rounding: The rounding rule from the python decimal library:
@@ -773,6 +774,9 @@ class EmbeddedDocumentField(BaseField):
def prepare_query_value(self, op, value):
if value is not None and not isinstance(value, self.document_type):
+ # Short circuit for special operators, returning them as is
+ if isinstance(value, dict) and all(k.startswith("$") for k in value.keys()):
+ return value
try:
value = self.document_type._from_son(value)
except ValueError:
@@ -844,8 +848,7 @@ class DynamicField(BaseField):
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def to_mongo(self, value, use_db_field=True, fields=None):
- """Convert a Python type to a MongoDB compatible type.
- """
+ """Convert a Python type to a MongoDB compatible type."""
if isinstance(value, str):
return value
@@ -1619,6 +1622,70 @@ class BinaryField(BaseField):
return super().prepare_query_value(op, self.to_mongo(value))
+class EnumField(BaseField):
+ """Enumeration Field. Values are stored underneath as is,
+ so it will only work with simple types (str, int, etc) that
+ are bson encodable
+ Example usage:
+ .. code-block:: python
+
+ class Status(Enum):
+ NEW = 'new'
+ DONE = 'done'
+
+ class ModelWithEnum(Document):
+ status = EnumField(Status, default=Status.NEW)
+
+ ModelWithEnum(status='done')
+ ModelWithEnum(status=Status.DONE)
+
+ Enum fields can be searched using enum or its value:
+ .. code-block:: python
+
+ ModelWithEnum.objects(status='new').count()
+ ModelWithEnum.objects(status=Status.NEW).count()
+
+ Note that choices cannot be set explicitly, they are derived
+ from the provided enum class.
+ """
+
+ def __init__(self, enum, **kwargs):
+ self._enum_cls = enum
+ if "choices" in kwargs:
+ raise ValueError(
+ "'choices' can't be set on EnumField, "
+ "it is implicitly set as the enum class"
+ )
+ kwargs["choices"] = list(self._enum_cls)
+ super().__init__(**kwargs)
+
+ def __set__(self, instance, value):
+ is_legal_value = value is None or isinstance(value, self._enum_cls)
+ if not is_legal_value:
+ try:
+ value = self._enum_cls(value)
+ except Exception:
+ pass
+ return super().__set__(instance, value)
+
+ def to_mongo(self, value):
+ if isinstance(value, self._enum_cls):
+ return value.value
+ return value
+
+ def validate(self, value):
+ if value and not isinstance(value, self._enum_cls):
+ try:
+ self._enum_cls(value)
+ except Exception as e:
+ self.error(str(e))
+
+ def prepare_query_value(self, op, value):
+ if value is None:
+ return value
+ return super().prepare_query_value(op, self.to_mongo(value))
+
+
class GridFSError(Exception):
pass
@@ -2042,7 +2109,7 @@ class ImageField(FileField):
class SequenceField(BaseField):
"""Provides a sequential counter see:
- http://www.mongodb.org/display/DOCS/Object+IDs#ObjectIDs-SequenceNumbers
+ https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectIDs-SequenceNumbers
.. note::
diff --git a/mongoengine/pymongo_support.py b/mongoengine/pymongo_support.py
index 80c0661..9cf9e2a 100644
--- a/mongoengine/pymongo_support.py
+++ b/mongoengine/pymongo_support.py
@@ -2,6 +2,7 @@
Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support.
"""
import pymongo
+from pymongo.errors import OperationFailure
_PYMONGO_37 = (3, 7)
@@ -10,13 +11,41 @@ PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])
IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37
-def count_documents(collection, filter):
- """Pymongo>3.7 deprecates count in favour of count_documents"""
+def count_documents(
+ collection, filter, skip=None, limit=None, hint=None, collation=None
+):
+ """Pymongo>3.7 deprecates count in favour of count_documents
+ """
+ if limit == 0:
+ return 0 # Pymongo raises an OperationFailure if called with limit=0
+
+ kwargs = {}
+ if skip is not None:
+ kwargs["skip"] = skip
+ if limit is not None:
+ kwargs["limit"] = limit
+ if hint not in (-1, None):
+ kwargs["hint"] = hint
+ if collation is not None:
+ kwargs["collation"] = collation
+
+ # count_documents appeared in pymongo 3.7
if IS_PYMONGO_GTE_37:
- return collection.count_documents(filter)
- else:
- count = collection.find(filter).count()
- return count
+ try:
+ return collection.count_documents(filter=filter, **kwargs)
+ except OperationFailure:
+ # OperationFailure - accounts for some operators that used to work
+ # with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
+ # fallback to deprecated Cursor.count
+ # Keeping this should be reevaluated the day pymongo removes .count entirely
+ pass
+
+ cursor = collection.find(filter)
+ for option, option_value in kwargs.items():
+ cursor_method = getattr(cursor, option)
+ cursor = cursor_method(option_value)
+ with_limit_and_skip = "skip" in kwargs or "limit" in kwargs
+ return cursor.count(with_limit_and_skip=with_limit_and_skip)
def list_collection_names(db, include_system_collections=False):
diff --git a/mongoengine/queryset/base.py b/mongoengine/queryset/base.py
index 39c44b2..1021bf5 100644
--- a/mongoengine/queryset/base.py
+++ b/mongoengine/queryset/base.py
@@ -29,6 +29,7 @@ from mongoengine.errors import (
NotUniqueError,
OperationError,
)
+from mongoengine.pymongo_support import count_documents
from mongoengine.queryset import transform
from mongoengine.queryset.field_list import QueryFieldList
from mongoengine.queryset.visitor import Q, QNode
@@ -83,13 +84,20 @@ class BaseQuerySet:
self._cursor_obj = None
self._limit = None
self._skip = None
+
self._hint = -1 # Using -1 as None is a valid value for hint
self._collation = None
self._batch_size = None
- self.only_fields = []
self._max_time_ms = None
self._comment = None
+ # Hack - As people expect cursor[5:5] to return
+ # an empty result set. It's hard to do that right, though, because the
+ # server uses limit(0) to mean 'no limit'. So we set _empty
+ # in that case and check for it when iterating. We also unset
+ # it anytime we change _limit. Inspired by how it is done in pymongo.Cursor
+ self._empty = False
+
def __call__(self, q_obj=None, **query):
"""Filter the selected documents by calling the
:class:`~mongoengine.queryset.QuerySet` with a query.
@@ -162,6 +170,7 @@ class BaseQuerySet:
[<User: User object>, <User: User object>]
"""
queryset = self.clone()
+ queryset._empty = False
# Handle a slice
if isinstance(key, slice):
@@ -169,6 +178,8 @@ class BaseQuerySet:
queryset._skip, queryset._limit = key.start, key.stop
if key.start and key.stop:
queryset._limit = key.stop - key.start
+ if queryset._limit == 0:
+ queryset._empty = True
# Allow further QuerySet modifications to be performed
return queryset
@@ -178,9 +189,7 @@ class BaseQuerySet:
if queryset._scalar:
return queryset._get_scalar(
queryset._document._from_son(
- queryset._cursor[key],
- _auto_dereference=self._auto_dereference,
- only_fields=self.only_fields,
+ queryset._cursor[key], _auto_dereference=self._auto_dereference,
)
)
@@ -188,9 +197,7 @@ class BaseQuerySet:
return queryset._cursor[key]
return queryset._document._from_son(
- queryset._cursor[key],
- _auto_dereference=self._auto_dereference,
- only_fields=self.only_fields,
+ queryset._cursor[key], _auto_dereference=self._auto_dereference,
)
raise TypeError("Provide a slice or an integer index")
@@ -394,9 +401,36 @@ class BaseQuerySet:
:meth:`skip` that has been applied to this cursor into account when
getting the count
"""
- if self._limit == 0 and with_limit_and_skip is False or self._none:
+ # mimic the fact that setting .limit(0) in pymongo sets no limit
+ # https://docs.mongodb.com/manual/reference/method/cursor.limit/#zero-value
+ if (
+ self._limit == 0
+ and with_limit_and_skip is False
+ or self._none
+ or self._empty
+ ):
return 0
- count = self._cursor.count(with_limit_and_skip=with_limit_and_skip)
+
+ kwargs = (
+ {"limit": self._limit, "skip": self._skip} if with_limit_and_skip else {}
+ )
+
+ if self._limit == 0:
+ # mimic the fact that historically .limit(0) sets no limit
+ kwargs.pop("limit", None)
+
+ if self._hint not in (-1, None):
+ kwargs["hint"] = self._hint
+
+ if self._collation:
+ kwargs["collation"] = self._collation
+
+ count = count_documents(
+ collection=self._cursor.collection,
+ filter=self._cursor._Cursor__spec,
+ **kwargs
+ )
+
self._cursor_obj = None
return count
@@ -680,12 +714,10 @@ class BaseQuerySet:
if full_response:
if result["value"] is not None:
- result["value"] = self._document._from_son(
- result["value"], only_fields=self.only_fields
- )
+ result["value"] = self._document._from_son(result["value"])
else:
if result is not None:
- result = self._document._from_son(result, only_fields=self.only_fields)
+ result = self._document._from_son(result)
return result
@@ -718,24 +750,22 @@ class BaseQuerySet:
docs = self._collection.find({"_id": {"$in": object_ids}}, **self._cursor_args)
if self._scalar:
for doc in docs:
- doc_map[doc["_id"]] = self._get_scalar(
- self._document._from_son(doc, only_fields=self.only_fields)
- )
+ doc_map[doc["_id"]] = self._get_scalar(self._document._from_son(doc))
elif self._as_pymongo:
for doc in docs:
doc_map[doc["_id"]] = doc
else:
for doc in docs:
doc_map[doc["_id"]] = self._document._from_son(
- doc,
- only_fields=self.only_fields,
- _auto_dereference=self._auto_dereference,
+ doc, _auto_dereference=self._auto_dereference,
)
return doc_map
def none(self):
- """Helper that just returns a list"""
+ """Returns a queryset that never returns any objects and no query will be executed when accessing the results
+ inspired by django none() https://docs.djangoproject.com/en/dev/ref/models/querysets/#none
+ """
queryset = self.clone()
queryset._none = True
return queryset
@@ -789,16 +819,17 @@ class BaseQuerySet:
"_snapshot",
"_timeout",
"_read_preference",
+ "_read_concern",
"_iter",
"_scalar",
"_as_pymongo",
"_limit",
"_skip",
+ "_empty",
"_hint",
"_collation",
"_auto_dereference",
"_search_text",
- "only_fields",
"_max_time_ms",
"_comment",
"_batch_size",
@@ -834,6 +865,7 @@ class BaseQuerySet:
"""
queryset = self.clone()
queryset._limit = n
+ queryset._empty = False # cancels the effect of empty
# If a cursor object has already been created, apply the limit to it.
if queryset._cursor_obj:
@@ -1001,7 +1033,6 @@ class BaseQuerySet:
.. versionchanged:: 0.5 - Added subfield support
"""
fields = {f: QueryFieldList.ONLY for f in fields}
- self.only_fields = list(fields.keys())
return self.fields(True, **fields)
def exclude(self, *fields):
@@ -1266,10 +1297,7 @@ class BaseQuerySet:
def from_json(self, json_data):
"""Converts json data to unsaved objects"""
son_data = json_util.loads(json_data)
- return [
- self._document._from_son(data, only_fields=self.only_fields)
- for data in son_data
- ]
+ return [self._document._from_son(data) for data in son_data]
def aggregate(self, pipeline, *suppl_pipeline, **kwargs):
"""Perform a aggregate function based in your queryset params
@@ -1311,10 +1339,11 @@ class BaseQuerySet:
final_pipeline = initial_pipeline + user_pipeline
collection = self._collection
- if self._read_preference is not None:
+ if self._read_preference is not None or self._read_concern is not None:
collection = self._collection.with_options(
- read_preference=self._read_preference
+ read_preference=self._read_preference, read_concern=self._read_concern
)
+
return collection.aggregate(final_pipeline, cursor={}, **kwargs)
# JS functionality
@@ -1584,7 +1613,7 @@ class BaseQuerySet:
def __next__(self):
"""Wrap the result in a :class:`~mongoengine.Document` object.
"""
- if self._limit == 0 or self._none:
+ if self._none or self._empty:
raise StopIteration
raw_doc = next(self._cursor)
@@ -1593,9 +1622,7 @@ class BaseQuerySet:
return raw_doc
doc = self._document._from_son(
- raw_doc,
- _auto_dereference=self._auto_dereference,
- only_fields=self.only_fields,
+ raw_doc, _auto_dereference=self._auto_dereference,
)
if self._scalar:
@@ -1603,8 +1630,6 @@ class BaseQuerySet:
return doc
- next = __next__ # For Python2 support
-
def rewind(self):
"""Rewind the cursor to its unevaluated state.
diff --git a/mongoengine/queryset/queryset.py b/mongoengine/queryset/queryset.py
index 8b5872f..e2db8f0 100644
--- a/mongoengine/queryset/queryset.py
+++ b/mongoengine/queryset/queryset.py
@@ -144,6 +144,7 @@ class QuerySet(BaseQuerySet):
return super().count(with_limit_and_skip)
if self._len is None:
+ # cache the length
self._len = super().count(with_limit_and_skip)
return self._len
diff --git a/mongoengine/queryset/visitor.py b/mongoengine/queryset/visitor.py
index 0eacc2e..a2448f2 100644
--- a/mongoengine/queryset/visitor.py
+++ b/mongoengine/queryset/visitor.py
@@ -7,6 +7,11 @@ from mongoengine.queryset import transform
__all__ = ("Q", "QNode")
+def warn_empty_is_deprecated():
+ msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
+
+
class QNodeVisitor:
"""Base visitor class for visiting Q-object nodes in a query tree.
"""
@@ -98,19 +103,18 @@ class QNode:
object.
"""
# If the other Q() is empty, ignore it and just use `self`.
- if getattr(other, "empty", True):
+ if not bool(other):
return self
# Or if this Q is empty, ignore it and just use `other`.
- if self.empty:
+ if not bool(self):
return other
return QCombination(operation, [self, other])
@property
def empty(self):
- msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
- warnings.warn(msg, DeprecationWarning)
+ warn_empty_is_deprecated()
return False
def __or__(self, other):
@@ -152,8 +156,7 @@ class QCombination(QNode):
@property
def empty(self):
- msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
- warnings.warn(msg, DeprecationWarning)
+ warn_empty_is_deprecated()
return not bool(self.children)
def __eq__(self, other):
@@ -186,4 +189,5 @@ class Q(QNode):
@property
def empty(self):
+ warn_empty_is_deprecated()
return not bool(self.query)