Compare commits

...

88 Commits

Author SHA1 Message Date
Stefan Wojcik
888a6da4a5 update the changelog and bump the version to v0.12.0 2017-04-07 10:18:39 -04:00
Omer Katz
972ac73dd9 Merge pull request #1497 from userlocalhost/feature/order_guarantee
added a feature to save object data in order
2017-04-07 10:49:39 +03:00
Hiroyasu OHYAMA
d8b238d5f1 Refactored the implementation of DynamicField extension for storing data in order 2017-04-06 00:42:11 +00:00
Omer Katz
63206c3da2 Merge pull request #1520 from ZoetropeLabs/fix/allow-reference-fields-take-object-ids
Allow ReferenceFields to take ObjectIds
2017-04-02 13:57:58 +03:00
Richard Fortescue-Webb
5713de8966 Use the objectid in the test 2017-03-29 11:34:57 +01:00
Richard Fortescue-Webb
58f293fef3 Allow ReferenceFields to take ObjectIds 2017-03-29 10:34:50 +01:00
Hiroyasu OHYAMA
ffbb2c9689 This is Additional tests for the container_class parameter of DynamicField
This tests DynamicField dereference with ordering guarantee.
2017-03-08 14:46:04 +00:00
Hiroyasu OHYAMA
9cd3dcdebf Added a test for the change of the condition in DeReference processing
This checks DBRef conversion using DynamicField with the ordering
guarantee.
2017-03-08 14:45:43 +00:00
Hiroyasu OHYAMA
f2fe58c3c5 Added a condition to store data to ObjectDict when the items type is it
Previous dereference implementation re-contains data as `dict` except
for the predicted type.
But the OrderedDict is not predicted, so the its data would be converted
`dict` implicitly.
As the result, the order of stored data get wrong. And this patch
prevents it.
2017-03-08 14:35:50 +00:00
Stefan Wojcik
b78010aa94 remove test_last_field_name_like_operator (it's a dupe of the same test in tests/queryset/transform.py) 2017-03-05 21:24:46 -05:00
Stefan Wójcik
49035543b9 cleanup BaseQuerySet.__getitem__ (#1502) 2017-03-05 21:17:53 -05:00
Stefan Wójcik
f9ccf635ca Respect db fields in multiple layers of embedded docs (#1501) 2017-03-05 18:20:09 -05:00
Stefan Wojcik
e8ea294964 test negative indexes (closes #1119) 2017-03-05 18:12:01 -05:00
Stefan Wojcik
19ef2be88b fix #937 2017-03-05 00:05:33 -05:00
Stefan Wojcik
30e8b8186f clean up document instance tests 2017-03-02 00:25:56 -05:00
Stefan Wójcik
741643af5f clean up field unit tests (#1498) 2017-03-02 00:05:10 -05:00
Hiroyasu OHYAMA
6aaf9ba470 removed a checking of dict order because this order is not cared (some implementation might be in ordered, but other one is not) 2017-03-01 09:32:28 +00:00
Hiroyasu OHYAMA
5957dc72eb To achive storing object data in order with minimum implementation, I
changed followings.

- added optional parameter `container_class` which enables to choose
  intermediate class at encoding Python data, instead of additional
  field class.
- removed OrderedDocument class because the equivalent feature could
  be implemented by the outside of Mongoengine.
2017-03-01 09:20:57 +00:00
Hiroyasu OHYAMA
e32a9777d7 added test for OrderedDynamicField and OrderedDocument 2017-02-28 03:35:53 +00:00
Hiroyasu OHYAMA
84a8f1eb2b added OrderedDocument class to decode BSON data to OrderedDict for retrieving data in order 2017-02-28 03:35:39 +00:00
Hiroyasu OHYAMA
6810953014 added OrderedDynamicField class to store data in the defined order because of #203 2017-02-28 03:34:42 +00:00
Ephraim Berkovitch
398964945a Document.objects.create should raise NotUniqueError upon saving duplicate primary key (#1485) 2017-02-27 09:42:44 -05:00
Stefan Wójcik
5f43c032f2 revamp the "connecting" user guide and test more ways of connecting to a replica set (#1490) 2017-02-26 21:29:06 -05:00
Stefan Wojcik
627cf90de0 tutorial tweaks: better copy + use py3-friendly syntax 2017-02-26 20:30:37 -05:00
Omer Katz
2bedb36d7f Test against multiple MongoDB versions in Travis (#1074) 2017-02-26 14:52:43 -05:00
Stefan Wójcik
e93a95d0cb Test and document controlling the size of the connection pool (#1489) 2017-02-25 14:09:10 -05:00
Stefan Wójcik
3f31666796 Fix the exception message when validating unicode URLs (#1486) 2017-02-24 16:18:34 -05:00
Stefan Wojcik
3fe8031cf3 fix EmbeddedDocumentListFieldTestCase 2017-02-22 12:44:05 -05:00
bagerard
b27c7ce11b allow to use sets in field choices (#1482) 2017-02-15 08:51:47 -05:00
Stefan Wojcik
ed34c2ca68 update the changelog and upgrade docs 2017-02-09 12:13:56 -08:00
Stefan Wójcik
3ca2e953fb Fix limit/skip/hint/batch_size chaining (#1476) 2017-02-09 12:02:46 -08:00
martin sereinig
d8a7328365 Fix docs regarding reverse_delete_rule and delete signals (#1473) 2017-02-06 14:11:42 -07:00
Stefan Wojcik
f33cd625bf nicer readme 2017-01-17 02:47:45 -05:00
Stefan Wojcik
80530bb13c nicer readme 2017-01-17 02:46:37 -05:00
Stefan Wójcik
affc12df4b Update README.rst 2017-01-17 02:43:29 -05:00
Stefan Wojcik
4eedf00025 nicer readme note about dependencies 2017-01-17 02:42:23 -05:00
Eli Boyarski
e5acbcc0dd Improved a docstring for FieldDoesNotExist (#1466) 2017-01-09 11:24:27 -05:00
Stefan Wojcik
1b6743ee53 add a changelog entry about broken references raising DoesNotExist 2017-01-08 14:50:16 -05:00
Eli Boyarski
b5fb82d95d Typo fix (#1463) 2017-01-08 12:57:36 -05:00
lanf0n
193aa4e1f2 [#1459] fix typo __neq__ to __ne__ (#1461) 2017-01-05 22:37:09 -05:00
Stefan Wójcik
ebd34427c7 Cleaner Document.save (#1458) 2016-12-30 05:43:56 -05:00
Stefan Wójcik
3d75573889 Validate db_field (#1448) 2016-12-29 12:39:05 -05:00
Stefan Wójcik
c6240ca415 Test connection's write concern (#1456) 2016-12-29 12:37:38 -05:00
Stefan Wójcik
2ee8984b44 add a $rename operator (#1454) 2016-12-28 23:25:38 -05:00
Stefan Wojcik
b7ec587e5b better docstring for BaseDocument.to_json 2016-12-28 22:15:46 -05:00
Stefan Wojcik
47c58bce2b fix "connect" example in the docs 2016-12-28 21:08:18 -05:00
Stefan Wojcik
96e95ac533 minor readme tweaks 2016-12-28 17:18:55 -05:00
Stefan Wojcik
b013a065f7 remove readme mention of the irc channel 2016-12-28 11:50:28 -05:00
Stefan Wojcik
74b37d11cf only validate db_field if it's a string type 2016-12-28 11:46:18 -05:00
Stefan Wójcik
c6cc013617 fix BaseQuerySet.fields when mixing exclusion/inclusion with complex values like $slice (#1452) 2016-12-28 11:40:57 -05:00
Stefan Wójcik
f4e1d80a87 support a negative dec operator (#1450) 2016-12-28 02:04:49 -05:00
Stefan Wójcik
91dad4060f raise an error when trying to save an abstract document (#1449) 2016-12-28 00:51:47 -05:00
Stefan Wojcik
e07cb82c15 validate db_field 2016-12-27 17:38:26 -05:00
Stefan Wojcik
2770cec187 better docstring for BaseQuerySet.fields 2016-12-27 10:20:13 -05:00
Stefan Wojcik
5c3928190a fix line width 2016-12-22 13:20:05 -05:00
Manuel Jeckelmann
9f4b04ea0f Fix querying an embedded document field by an invalid value (#1440) 2016-12-22 13:19:18 -05:00
Stefan Wojcik
96d20756ca remove redundant whitespace 2016-12-22 13:13:19 -05:00
John Dupuy
b8454c7f5b Fixed ListField deletion bug (#1435) 2016-12-22 13:11:44 -05:00
George Karakostas
c84f703f92 Update documentation to include a Q import (#1441) 2016-12-22 13:06:55 -05:00
Manuel Jeckelmann
57c2e867d8 Remove py26 from contributing docs (#1439)
Python 2.6 is not supported anymore with version 0.11.0
2016-12-19 17:54:43 -05:00
Stefan Wojcik
553f496d84 fix tests 2016-12-13 00:42:03 -05:00
Stefan Wojcik
b1d8aca46a update the changelog 2016-12-12 23:33:49 -05:00
Stefan Wojcik
8e884fd3ea make the __in=non_iterable_or_doc tests more concise 2016-12-12 23:30:38 -05:00
Malthe Jørgensen
76524b7498 Raise TypeError when __in-operator used with a Document (#1237) 2016-12-12 23:27:25 -05:00
Stefan Wojcik
65914fb2b2 fix the way MongoDB URI w/ ?replicaset is passed 2016-12-12 23:24:19 -05:00
Stefan Wojcik
a4d0da0085 update the changelog 2016-12-12 23:08:57 -05:00
Stefan Wójcik
c9d496e9a0 Fix connecting to MongoReplicaSetClient (#1436) 2016-12-12 23:08:11 -05:00
Stefan Wojcik
88a951ba4f version bump 2016-12-12 19:03:21 -05:00
Stefan Wójcik
403ceb19dc set @wojcikstefan as the maintainer (closes #1342) (#1434) 2016-12-12 10:44:03 -05:00
Stefan Wójcik
835d3c3d18 Improve the health of this package (#1428) 2016-12-11 18:49:21 -05:00
Gilbert Gilb's
3135b456be Upgrade notice for 0.10.7 (#1433) 2016-12-11 15:38:06 -05:00
Omer Katz
0be6d3661a Merge branch 'master' of git://github.com/mrTable/mongoengine 2016-12-11 10:52:08 +02:00
Stefan Wojcik
6f5f5b4711 version bump (forgot to do it with v0.10.8 release, so have to go for v0.10.9) 2016-12-10 23:36:06 -05:00
Stefan Wojcik
c6c5f85abb update the changelog with everything we've added in v0.10.8 2016-12-10 23:30:16 -05:00
Stefan Wójcik
7b860f7739 Deprecate Python v2.6 (#1430) 2016-12-10 13:29:31 -05:00
Stefan Wojcik
e28804c03a add venv & venv3 to .gitignore 2016-12-10 13:11:37 -05:00
Stefan Wójcik
1b9432824b Add ability to filter the generic reference field by ObjectId and DBRef (#1425) 2016-12-09 12:56:06 -05:00
Дмитрий Янцен
3b71a6b5c5 Improved tests to avoid regression Issue #1103 2016-12-06 11:33:44 +05:00
Дмитрий Янцен
7ce8768c19 Added rounding for unicode return value of to_mongo in DecimalField. Closes #1103 2016-12-06 10:42:10 +05:00
rmendocna
25e0f12976 fix delete cascade for models without a literal id field: replace with pk (#1247) 2016-12-05 22:54:21 -05:00
Stefan Wójcik
f168682a68 Dont let the MongoDB URI override connection settings it doesnt explicitly specify (#1421) 2016-12-05 22:31:00 -05:00
Stefan Wójcik
d25058a46d Implement BaseQuerySet.batch_size (#1426) 2016-12-05 22:13:22 -05:00
Stefan Wójcik
4d0c092d9f Fix iteration within iteration (#1427) 2016-12-05 09:38:24 -05:00
Stefan Wójcik
15714ef855 Fix __repr__ method of the StrictDict (#1424) 2016-12-04 16:10:59 -05:00
Stefan Wójcik
eb743beaa3 fix doc.get_<field>_display + unit test inspired by #1279 (#1419) 2016-12-04 00:34:24 -05:00
Stefan Wójcik
0007535a46 Add support for cursor.comment (#1420) 2016-12-04 00:33:42 -05:00
Stefan Wójcik
8391af026c Fix filtering by embedded_doc=None (#1422) 2016-12-04 00:32:53 -05:00
Stefan Wójcik
800f656dcf remove unnecessary randomness in indexes tests (#1423) 2016-12-04 00:31:54 -05:00
74 changed files with 5764 additions and 4891 deletions

5
.gitignore vendored
View File

@@ -14,4 +14,7 @@ env/
.project
.pydevproject
tests/test_bugfix.py
htmlcov/
htmlcov/
venv
venv3
scratchpad

View File

@@ -0,0 +1,23 @@
#!/bin/bash
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
if [ "$MONGODB" = "2.4" ]; then
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-10gen=2.4.14
sudo service mongodb start
elif [ "$MONGODB" = "2.6" ]; then
echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-org-server=2.6.12
# service should be started automatically
elif [ "$MONGODB" = "3.0" ]; then
echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
sudo apt-get update
sudo apt-get install mongodb-org-server=3.0.14
# service should be started automatically
else
echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0."
exit 1
fi;

22
.landscape.yml Normal file
View File

@@ -0,0 +1,22 @@
pylint:
disable:
# We use this a lot (e.g. via document._meta)
- protected-access
options:
additional-builtins:
# add xrange and long as valid built-ins. In Python 3, xrange is
# translated into range and long is translated into int via 2to3 (see
# "use_2to3" in setup.py). This should be removed when we drop Python
# 2 support (which probably won't happen any time soon).
- xrange
- long
pyflakes:
disable:
# undefined variables are already covered by pylint (and exclude
# xrange & long)
- F821
ignore-paths:
- benchmark.py

View File

@@ -1,29 +1,48 @@
# For full coverage, we'd have to test all supported Python, MongoDB, and
# PyMongo combinations. However, that would result in an overly long build
# with a very large number of jobs, hence we only test a subset of all the
# combinations:
# * MongoDB v2.4 & v3.0 are only tested against Python v2.7 & v3.5.
# * MongoDB v2.4 is tested against PyMongo v2.7 & v3.x.
# * MongoDB v3.0 is tested against PyMongo v3.x.
# * MongoDB v2.6 is currently the "main" version tested against Python v2.7,
# v3.5, PyPy & PyPy3, and PyMongo v2.7, v2.8 & v3.x.
#
# Reminder: Update README.rst if you change MongoDB versions we test.
language: python
python:
- '2.6'
- '2.7'
- '3.3'
- '3.4'
- '3.5'
- 2.7
- 3.5
- pypy
- pypy3
env:
- PYMONGO=2.7
- PYMONGO=2.8
- PYMONGO=3.0
- PYMONGO=dev
- MONGODB=2.6 PYMONGO=2.7
- MONGODB=2.6 PYMONGO=2.8
- MONGODB=2.6 PYMONGO=3.0
matrix:
# Finish the build as soon as one job fails
fast_finish: true
include:
- python: 2.7
env: MONGODB=2.4 PYMONGO=2.7
- python: 2.7
env: MONGODB=2.4 PYMONGO=3.0
- python: 2.7
env: MONGODB=3.0 PYMONGO=3.0
- python: 3.5
env: MONGODB=2.4 PYMONGO=2.7
- python: 3.5
env: MONGODB=2.4 PYMONGO=3.0
- python: 3.5
env: MONGODB=3.0 PYMONGO=3.0
before_install:
- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' |
sudo tee /etc/apt/sources.list.d/mongodb.list
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install mongodb-org-server
- bash .install_mongodb_on_travis.sh
install:
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
@@ -31,33 +50,52 @@ install:
python-tk
- travis_retry pip install --upgrade pip
- travis_retry pip install coveralls
- travis_retry pip install flake8
- travis_retry pip install flake8 flake8-import-order
- travis_retry pip install tox>=1.9
- travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
# Cache dependencies installed via pip
cache: pip
# Run flake8 for py27
before_script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e flake8; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi
script:
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
after_script: coveralls --verbose
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
# 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible
# code in a separate dir and runs tests on that.
after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
notifications:
irc: irc.freenode.org#mongoengine
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
branches:
only:
- master
- /^v.*$/
# Whenever a new release is created via GitHub, publish it on PyPI.
deploy:
provider: pypi
user: the_drow
password:
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
# create a source distribution and a pure python wheel for faster installs
distributions: "sdist bdist_wheel"
# only deploy on tagged commits (aka GitHub releases) and only for the
# parent repo's builds running Python 2.7 along with dev PyMongo (we run
# Travis against many different Python and PyMongo versions and we don't
# want the deploy to occur multiple times).
on:
tags: true
repo: MongoEngine/mongoengine
condition: "$PYMONGO = 3.0"
python: 2.7

View File

@@ -242,3 +242,4 @@ that much better:
* xiaost7 (https://github.com/xiaost7)
* Victor Varvaryuk
* Stanislav Kaledin (https://github.com/sallyruthstruik)
* Dmitry Yantsen (https://github.com/mrTable)

View File

@@ -14,13 +14,13 @@ Before starting to write code, look for existing `tickets
<https://github.com/MongoEngine/mongoengine/issues?state=open>`_ or `create one
<https://github.com/MongoEngine/mongoengine/issues>`_ for your specific
issue or feature request. That way you avoid working on something
that might not be of interest or that has already been addressed. If in doubt
that might not be of interest or that has already been addressed. If in doubt
post to the `user group <http://groups.google.com/group/mongoengine-users>`
Supported Interpreters
----------------------
MongoEngine supports CPython 2.6 and newer. Language
MongoEngine supports CPython 2.7 and newer. Language
features not supported by all interpreters can not be used.
Please also ensure that your code is properly converted by
`2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support.
@@ -29,19 +29,20 @@ Style Guide
-----------
MongoEngine aims to follow `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_
including 4 space indents. When possible we try to stick to 79 character line limits.
However, screens got bigger and an ORM has a strong focus on readability and
if it can help, we accept 119 as maximum line length, in a similar way as
`django does <https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
including 4 space indents. When possible we try to stick to 79 character line
limits. However, screens got bigger and an ORM has a strong focus on
readability and if it can help, we accept 119 as maximum line length, in a
similar way as `django does
<https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
Testing
-------
All tests are run on `Travis <http://travis-ci.org/MongoEngine/mongoengine>`_
and any pull requests are automatically tested by Travis. Any pull requests
without tests will take longer to be integrated and might be refused.
and any pull requests are automatically tested. Any pull requests without
tests will take longer to be integrated and might be refused.
You may also submit a simple failing test as a PullRequest if you don't know
You may also submit a simple failing test as a pull request if you don't know
how to fix it, it will be easier for other people to work on it and it may get
fixed faster.
@@ -49,13 +50,18 @@ General Guidelines
------------------
- Avoid backward breaking changes if at all possible.
- If you *have* to introduce a breaking change, make it very clear in your
pull request's description. Also, describe how users of this package
should adapt to the breaking change in docs/upgrade.rst.
- Write inline documentation for new classes and methods.
- Write tests and make sure they pass (make sure you have a mongod
running on the default port, then execute ``python setup.py nosetests``
from the cmd line to run the test suite).
- Ensure tests pass on every Python and PyMongo versions.
You can test on these versions locally by executing ``tox``
- Add enhancements or problematic bug fixes to docs/changelog.rst
- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
You can test various Python and PyMongo versions locally by executing
``tox``. For different MongoDB versions, you can rely on our automated
Travis tests.
- Add enhancements or problematic bug fixes to docs/changelog.rst.
- Add yourself to AUTHORS :)
Documentation
@@ -69,3 +75,6 @@ just make your changes to the inline documentation of the appropriate
branch and submit a `pull request <https://help.github.com/articles/using-pull-requests>`_.
You might also use the github `Edit <https://github.com/blog/844-forking-with-the-edit-button>`_
button.
If you want to test your documentation changes locally, you need to install
the ``sphinx`` package.

View File

@@ -4,7 +4,7 @@ MongoEngine
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
:Repository: https://github.com/MongoEngine/mongoengine
:Author: Harry Marr (http://github.com/hmarr)
:Maintainer: Ross Lawley (http://github.com/rozza)
:Maintainer: Stefan Wójcik (http://github.com/wojcikstefan)
.. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master
:target: https://travis-ci.org/MongoEngine/mongoengine
@@ -19,32 +19,42 @@ MongoEngine
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
Documentation available at https://mongoengine-odm.readthedocs.io - there is currently
a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_, a `user guide
<https://mongoengine-odm.readthedocs.io/guide/index.html>`_ and an `API reference
<https://mongoengine-odm.readthedocs.io/apireference.html>`_.
Documentation is available at https://mongoengine-odm.readthedocs.io - there
is currently a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_,
a `user guide <https://mongoengine-odm.readthedocs.io/guide/index.html>`_, and
an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
Supported MongoDB Versions
==========================
MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future
versions should be supported as well, but aren't actively tested at the moment.
Make sure to open an issue or submit a pull request if you experience any
problems with MongoDB v3.2+.
Installation
============
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
`pip <https://pip.pypa.io/>`_. You can then use ``pip install -U mongoengine``.
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ and thus
you can use ``easy_install -U mongoengine``. Otherwise, you can download the
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the
source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and run ``python
setup.py install``.
Dependencies
============
- pymongo>=2.7.1
- sphinx (optional - for documentation generation)
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
At the very least, you'll need these two packages to use MongoEngine:
- pymongo>=2.7.1
- six>=1.10.0
If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
Optional Dependencies
---------------------
- **Image Fields**: Pillow>=2.0.0
- dateutil>=2.1.0
.. note
MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: PyMongo 3.0.1
If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
- Pillow>=2.0.0
Examples
========
@@ -57,7 +67,7 @@ Some simple examples of what MongoEngine code looks like:
class BlogPost(Document):
title = StringField(required=True, max_length=200)
posted = DateTimeField(default=datetime.datetime.now)
posted = DateTimeField(default=datetime.datetime.utcnow)
tags = ListField(StringField(max_length=50))
meta = {'allow_inheritance': True}
@@ -87,27 +97,28 @@ Some simple examples of what MongoEngine code looks like:
... print
...
>>> len(BlogPost.objects)
# Count all blog posts and its subtypes
>>> BlogPost.objects.count()
2
>>> len(TextPost.objects)
>>> TextPost.objects.count()
1
>>> len(LinkPost.objects)
>>> LinkPost.objects.count()
1
# Find tagged posts
>>> len(BlogPost.objects(tags='mongoengine'))
# Count tagged posts
>>> BlogPost.objects(tags='mongoengine').count()
2
>>> len(BlogPost.objects(tags='mongodb'))
>>> BlogPost.objects(tags='mongodb').count()
1
Tests
=====
To run the test suite, ensure you are running a local instance of MongoDB on
the standard port and have ``nose`` installed. Then, run: ``python setup.py nosetests``.
the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``.
To run the test suite on every supported Python version and every supported PyMongo version,
you can use ``tox``.
tox and each supported Python version should be installed in your environment:
To run the test suite on every supported Python and PyMongo version, you can
use ``tox``. You'll need to make sure you have each supported Python version
installed in your environment and then:
.. code-block:: shell
@@ -116,13 +127,16 @@ tox and each supported Python version should be installed in your environment:
# Run the test suites
$ tox
If you wish to run one single or selected tests, use the nosetest convention. It will find the folder,
eventually the file, go to the TestClass specified after the colon and eventually right to the single test.
Also use the -s argument if you want to print out whatever or access pdb while testing.
If you wish to run a subset of tests, use the nosetests convention:
.. code-block:: shell
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest.test_cls_field -s
# Run all the tests in a particular test file
$ python setup.py nosetests --tests tests/fields/fields.py
# Run only particular test class in that file
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest
# Use the -s option if you want to print some debug statements or use pdb
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s
Community
=========
@@ -130,8 +144,7 @@ Community
<http://groups.google.com/group/mongoengine-users>`_
- `MongoEngine Developers mailing list
<http://groups.google.com/group/mongoengine-dev>`_
- `#mongoengine IRC channel <http://webchat.freenode.net/?channels=mongoengine>`_
Contributing
============
We welcome contributions! see the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_
We welcome contributions! See the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_

View File

@@ -1,118 +1,41 @@
#!/usr/bin/env python
"""
Simple benchmark comparing PyMongo and MongoEngine.
Sample run on a mid 2015 MacBook Pro (commit b282511):
Benchmarking...
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
2.58979988098
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo write_concern={"w": 0}
1.26657605171
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
8.4351580143
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries without continual assign - MongoEngine
7.20191693306
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade = True
6.31104588509
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True
6.07083487511
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False
5.97704291344
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False
5.9111430645
"""
import timeit
def cprofile_main():
from pymongo import Connection
connection = Connection()
connection.drop_database('timeit_test')
connection.disconnect()
from mongoengine import Document, DictField, connect
connect("timeit_test")
class Noddy(Document):
fields = DictField()
for i in range(1):
noddy = Noddy()
for j in range(20):
noddy.fields["key" + str(j)] = "value " + str(j)
noddy.save()
def main():
"""
0.4 Performance Figures ...
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
3.86744189262
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
6.23374891281
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
5.33027005196
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
pass - No Cascade
0.5.X
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
3.89597702026
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
21.7735359669
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
19.8670389652
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
pass - No Cascade
0.6.X
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
3.81559205055
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
10.0446798801
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
9.51354718208
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
9.02567505836
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force=True
8.44933390617
0.7.X
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
3.78801012039
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
9.73050498962
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False
8.33456707001
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, safe=False, validate=False, cascade=False
8.37778115273
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force=True
8.36906409264
0.8.X
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo
3.69964408875
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - Pymongo write_concern={"w": 0}
3.5526599884
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine
7.00959801674
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries without continual assign - MongoEngine
5.60943293571
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine - write_concern={"w": 0}, cascade=True
6.715102911
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False, cascade=True
5.50644683838
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, write_concern={"w": 0}, validate=False
4.69851183891
----------------------------------------------------------------------------------------------------
Creating 10000 dictionaries - MongoEngine, force_insert=True, write_concern={"w": 0}, validate=False
4.68946313858
----------------------------------------------------------------------------------------------------
"""
print("Benchmarking...")
setup = """
@@ -131,7 +54,7 @@ noddy = db.noddy
for i in range(10000):
example = {'fields': {}}
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
example['fields']['key' + str(j)] = 'value ' + str(j)
noddy.save(example)
@@ -146,9 +69,10 @@ myNoddys = noddy.find()
stmt = """
from pymongo import MongoClient
from pymongo.write_concern import WriteConcern
connection = MongoClient()
db = connection.timeit_test
db = connection.get_database('timeit_test', write_concern=WriteConcern(w=0))
noddy = db.noddy
for i in range(10000):
@@ -156,7 +80,7 @@ for i in range(10000):
for j in range(20):
example['fields']["key"+str(j)] = "value "+str(j)
noddy.save(example, write_concern={"w": 0})
noddy.save(example)
myNoddys = noddy.find()
[n for n in myNoddys] # iterate
@@ -171,10 +95,10 @@ myNoddys = noddy.find()
from pymongo import MongoClient
connection = MongoClient()
connection.drop_database('timeit_test')
connection.disconnect()
connection.close()
from mongoengine import Document, DictField, connect
connect("timeit_test")
connect('timeit_test')
class Noddy(Document):
fields = DictField()

View File

@@ -2,11 +2,48 @@
Changelog
=========
Development
===========
- (Fill this out as you fix issues and develop your features).
Changes in 0.12.0
=================
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
- Fixed connecting to a replica set with PyMongo 2.x #1436
- Fixed using sets in field choices #1481
- Fixed deleting items from a `ListField` #1318
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
- Fixed behavior of a `dec` update operator #1450
- Added a `rename` update operator #1454
- Added validation for the `db_field` parameter #1448
- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
- Fixed the error message displayed when validating unicode URLs #1486
- Raise an error when trying to save an abstract document #1449
Changes in 0.11.0
=================
- BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
- BREAKING CHANGE: Dropped Python 2.6 support. #1428
- BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428
- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
- Fixed absent rounding for DecimalField when `force_string` is set. #1103
Changes in 0.10.8
=================
- Added support for QuerySet.batch_size (#1426)
- Fixed query set iteration within iteration #1427
- Fixed an issue where specifying a MongoDB URI host would override more information than it should #1421
- Added ability to filter the generic reference field by ObjectId and DBRef #1425
- Fixed delete cascade for models with a custom primary key field #1247
- Added ability to specify an authentication mechanism (e.g. X.509) #1333
- Added support for falsey primary keys (e.g. doc.pk = 0) #1354
- Fixed BaseQuerySet#sum/average for fields w/ explicit db_field #1417
- Fixed QuerySet#sum/average for fields w/ explicit db_field #1417
- Fixed filtering by embedded_doc=None #1422
- Added support for cursor.comment #1420
- Fixed doc.get_<field>_display #1419
- Fixed __repr__ method of the StrictDict #1424
- Added a deprecation warning for Python 2.6
Changes in 0.10.7
=================

View File

@@ -33,7 +33,7 @@ the :attr:`host` to
corresponding parameters in :func:`~mongoengine.connect`: ::
connect(
name='test',
db='test',
username='user',
password='12345',
host='mongodb://admin:qwerty@localhost/production'
@@ -42,13 +42,18 @@ the :attr:`host` to
will establish connection to ``production`` database using
``admin`` username and ``qwerty`` password.
ReplicaSets
===========
Replica Sets
============
MongoEngine supports
:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them,
please use an URI style connection and provide the ``replicaSet`` name
in the connection kwargs.
MongoEngine supports connecting to replica sets::
from mongoengine import connect
# Regular connect
connect('dbname', replicaset='rs-name')
# MongoDB URI-style connect
connect(host='mongodb://localhost/dbname?replicaSet=rs-name')
Read preferences are supported through the connection or via individual
queries by passing the read_preference ::
@@ -59,76 +64,74 @@ queries by passing the read_preference ::
Multiple Databases
==================
Multiple database support was added in MongoEngine 0.6. To use multiple
databases you can use :func:`~mongoengine.connect` and provide an `alias` name
for the connection - if no `alias` is provided then "default" is used.
To use multiple databases you can use :func:`~mongoengine.connect` and provide
an `alias` name for the connection - if no `alias` is provided then "default"
is used.
In the background this uses :func:`~mongoengine.register_connection` to
store the data and you can register all aliases up front if required.
Individual documents can also support multiple databases by providing a
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects
to point across databases and collections. Below is an example schema, using
3 different databases to store data::
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef`
objects to point across databases and collections. Below is an example schema,
using 3 different databases to store data::
class User(Document):
name = StringField()
meta = {"db_alias": "user-db"}
meta = {'db_alias': 'user-db'}
class Book(Document):
name = StringField()
meta = {"db_alias": "book-db"}
meta = {'db_alias': 'book-db'}
class AuthorBooks(Document):
author = ReferenceField(User)
book = ReferenceField(Book)
meta = {"db_alias": "users-books-db"}
meta = {'db_alias': 'users-books-db'}
Context Managers
================
Sometimes you may want to switch the database or collection to query against
for a class.
Sometimes you may want to switch the database or collection to query against.
For example, archiving older data into a separate database for performance
reasons or writing functions that dynamically choose collections to write
document to.
a document to.
Switch Database
---------------
The :class:`~mongoengine.context_managers.switch_db` context manager allows
you to change the database alias for a given class allowing quick and easy
access the same User document across databases::
access to the same User document across databases::
from mongoengine.context_managers import switch_db
class User(Document):
name = StringField()
meta = {"db_alias": "user-db"}
meta = {'db_alias': 'user-db'}
with switch_db(User, 'archive-user-db') as User:
User(name="Ross").save() # Saves the 'archive-user-db'
User(name='Ross').save() # Saves the 'archive-user-db'
Switch Collection
-----------------
The :class:`~mongoengine.context_managers.switch_collection` context manager
allows you to change the collection for a given class allowing quick and easy
access the same Group document across collection::
access to the same Group document across collection::
from mongoengine.context_managers import switch_collection
class Group(Document):
name = StringField()
Group(name="test").save() # Saves in the default db
Group(name='test').save() # Saves in the default db
with switch_collection(Group, 'group2000') as Group:
Group(name="hello Group 2000 collection!").save() # Saves in group2000 collection
Group(name='hello Group 2000 collection!').save() # Saves in group2000 collection
.. note:: Make sure any aliases have been registered with

View File

@@ -150,7 +150,7 @@ arguments can be set on all fields:
.. note:: If set, this field is also accessible through the `pk` field.
:attr:`choices` (Default: None)
An iterable (e.g. a list or tuple) of choices to which the value of this
An iterable (e.g. list, tuple or set) of choices to which the value of this
field should be limited.
Can be either be a nested tuples of value (stored in mongo) and a
@@ -214,8 +214,8 @@ document class as the first argument::
Dictionary Fields
-----------------
Often, an embedded document may be used instead of a dictionary generally
embedded documents are recommended as dictionaries dont support validation
Often, an embedded document may be used instead of a dictionary generally
embedded documents are recommended as dictionaries dont support validation
or custom field types. However, sometimes you will not know the structure of what you want to
store; in this situation a :class:`~mongoengine.fields.DictField` is appropriate::
@@ -361,11 +361,6 @@ Its value can take any of the following constants:
In Django, be sure to put all apps that have such delete rule declarations in
their :file:`models.py` in the :const:`INSTALLED_APPS` tuple.
.. warning::
Signals are not triggered when doing cascading updates / deletes - if this
is required you must manually handle the update / delete.
Generic reference fields
''''''''''''''''''''''''
A second kind of reference field also exists,

View File

@@ -2,13 +2,13 @@
Installing MongoEngine
======================
To use MongoEngine, you will need to download `MongoDB <http://mongodb.org/>`_
To use MongoEngine, you will need to download `MongoDB <http://mongodb.com/>`_
and ensure it is running in an accessible location. You will also need
`PyMongo <http://api.mongodb.org/python>`_ to use MongoEngine, but if you
install MongoEngine using setuptools, then the dependencies will be handled for
you.
MongoEngine is available on PyPI, so to use it you can use :program:`pip`:
MongoEngine is available on PyPI, so you can use :program:`pip`:
.. code-block:: console

View File

@@ -340,14 +340,19 @@ Javascript code that is executed on the database server.
Counting results
----------------
Just as with limiting and skipping results, there is a method on
:class:`~mongoengine.queryset.QuerySet` objects --
:meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic
way of achieving this::
Just as with limiting and skipping results, there is a method on a
:class:`~mongoengine.queryset.QuerySet` object --
:meth:`~mongoengine.queryset.QuerySet.count`::
num_users = len(User.objects)
num_users = User.objects.count()
Even if len() is the Pythonic way of counting results, keep in mind that if you concerned about performance, :meth:`~mongoengine.queryset.QuerySet.count` is the way to go since it only execute a server side count query, while len() retrieves the results, places them in cache, and finally counts them. If we compare the performance of the two operations, len() is much slower than :meth:`~mongoengine.queryset.QuerySet.count`.
You could technically use ``len(User.objects)`` to get the same result, but it
would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`.
When you execute a server-side count query, you let MongoDB do the heavy
lifting and you receive a single integer over the wire. Meanwhile, len()
retrieves all the results, places them in a local cache, and finally counts
them. If we compare the performance of the two operations, len() is much slower
than :meth:`~mongoengine.queryset.QuerySet.count`.
Further aggregation
-------------------
@@ -479,6 +484,8 @@ operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the
first positional argument to :attr:`Document.objects` when you filter it by
calling it with keyword arguments::
from mongoengine.queryset.visitor import Q
# Get published posts
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))

View File

@@ -142,11 +142,4 @@ cleaner looking while still allowing manual execution of the callback::
modified = DateTimeField()
ReferenceFields and Signals
---------------------------
Currently `reverse_delete_rule` does not trigger signals on the other part of
the relationship. If this is required you must manually handle the
reverse deletion.
.. _blinker: http://pypi.python.org/pypi/blinker

View File

@@ -3,11 +3,10 @@ Tutorial
========
This tutorial introduces **MongoEngine** by means of example --- we will walk
through how to create a simple **Tumblelog** application. A Tumblelog is a type
of blog where posts are not constrained to being conventional text-based posts.
As well as text-based entries, users may post images, links, videos, etc. For
simplicity's sake, we'll stick to text, image and link entries in our
application. As the purpose of this tutorial is to introduce MongoEngine, we'll
through how to create a simple **Tumblelog** application. A tumblelog is a
blog that supports mixed media content, including text, images, links, video,
audio, etc. For simplicity's sake, we'll stick to text, image, and link
entries. As the purpose of this tutorial is to introduce MongoEngine, we'll
focus on the data-modelling side of the application, leaving out a user
interface.
@@ -16,14 +15,14 @@ Getting started
Before we start, make sure that a copy of MongoDB is running in an accessible
location --- running it locally will be easier, but if that is not an option
then it may be run on a remote server. If you haven't installed mongoengine,
then it may be run on a remote server. If you haven't installed MongoEngine,
simply use pip to install it like so::
$ pip install mongoengine
Before we can start using MongoEngine, we need to tell it how to connect to our
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`
function. If running locally the only argument we need to provide is the name
function. If running locally, the only argument we need to provide is the name
of the MongoDB database to use::
from mongoengine import *
@@ -39,18 +38,18 @@ Defining our documents
MongoDB is *schemaless*, which means that no schema is enforced by the database
--- we may add and remove fields however we want and MongoDB won't complain.
This makes life a lot easier in many regards, especially when there is a change
to the data model. However, defining schemata for our documents can help to
iron out bugs involving incorrect types or missing fields, and also allow us to
to the data model. However, defining schemas for our documents can help to iron
out bugs involving incorrect types or missing fields, and also allow us to
define utility methods on our documents in the same way that traditional
:abbr:`ORMs (Object-Relational Mappers)` do.
In our Tumblelog application we need to store several different types of
information. We will need to have a collection of **users**, so that we may
information. We will need to have a collection of **users**, so that we may
link posts to an individual. We also need to store our different types of
**posts** (eg: text, image and link) in the database. To aid navigation of our
Tumblelog, posts may have **tags** associated with them, so that the list of
posts shown to the user may be limited to posts that have been assigned a
specific tag. Finally, it would be nice if **comments** could be added to
specific tag. Finally, it would be nice if **comments** could be added to
posts. We'll start with **users**, as the other document models are slightly
more involved.
@@ -78,7 +77,7 @@ Now we'll think about how to store the rest of the information. If we were
using a relational database, we would most likely have a table of **posts**, a
table of **comments** and a table of **tags**. To associate the comments with
individual posts, we would put a column in the comments table that contained a
foreign key to the posts table. We'd also need a link table to provide the
foreign key to the posts table. We'd also need a link table to provide the
many-to-many relationship between posts and tags. Then we'd need to address the
problem of storing the specialised post-types (text, image and link). There are
several ways we can achieve this, but each of them have their problems --- none
@@ -96,7 +95,7 @@ using* the new fields we need to support video posts. This fits with the
Object-Oriented principle of *inheritance* nicely. We can think of
:class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and
:class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports
this kind of modelling out of the box --- all you need do is turn on inheritance
this kind of modeling out of the box --- all you need do is turn on inheritance
by setting :attr:`allow_inheritance` to True in the :attr:`meta`::
class Post(Document):
@@ -128,8 +127,8 @@ link table, we can just store a list of tags in each post. So, for both
efficiency and simplicity's sake, we'll store the tags as strings directly
within the post, rather than storing references to tags in a separate
collection. Especially as tags are generally very short (often even shorter
than a document's id), this denormalisation won't impact very strongly on the
size of our database. So let's take a look that the code our modified
than a document's id), this denormalization won't impact the size of the
database very strongly. Let's take a look at the code of our modified
:class:`Post` class::
class Post(Document):
@@ -141,7 +140,7 @@ The :class:`~mongoengine.fields.ListField` object that is used to define a Post'
takes a field object as its first argument --- this means that you can have
lists of any type of field (including lists).
.. note:: We don't need to modify the specialised post types as they all
.. note:: We don't need to modify the specialized post types as they all
inherit from :class:`Post`.
Comments
@@ -149,7 +148,7 @@ Comments
A comment is typically associated with *one* post. In a relational database, to
display a post with its comments, we would have to retrieve the post from the
database, then query the database again for the comments associated with the
database and then query the database again for the comments associated with the
post. This works, but there is no real reason to be storing the comments
separately from their associated posts, other than to work around the
relational model. Using MongoDB we can store the comments as a list of
@@ -219,8 +218,8 @@ Now that we've got our user in the database, let's add a couple of posts::
post2.tags = ['mongoengine']
post2.save()
.. note:: If you change a field on a object that has already been saved, then
call :meth:`save` again, the document will be updated.
.. note:: If you change a field on an object that has already been saved and
then call :meth:`save` again, the document will be updated.
Accessing our data
==================
@@ -232,17 +231,17 @@ used to access the documents in the database collection associated with that
class. So let's see how we can get our posts' titles::
for post in Post.objects:
print post.title
print(post.title)
Retrieving type-specific information
------------------------------------
This will print the titles of our posts, one on each line. But What if we want
This will print the titles of our posts, one on each line. But what if we want
to access the type-specific data (link_url, content, etc.)? One way is simply
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
for post in TextPost.objects:
print post.content
print(post.content)
Using TextPost's :attr:`objects` attribute only returns documents that were
created using :class:`TextPost`. Actually, there is a more general rule here:
@@ -259,16 +258,14 @@ instances of :class:`Post` --- they were instances of the subclass of
practice::
for post in Post.objects:
print post.title
print '=' * len(post.title)
print(post.title)
print('=' * len(post.title))
if isinstance(post, TextPost):
print post.content
print(post.content)
if isinstance(post, LinkPost):
print 'Link:', post.link_url
print
print('Link: {}'.format(post.link_url))
This would print the title of each post, followed by the content if it was a
text post, and "Link: <url>" if it was a link post.
@@ -283,7 +280,7 @@ your query. Let's adjust our query so that only posts with the tag "mongodb"
are returned::
for post in Post.objects(tags='mongodb'):
print post.title
print(post.title)
There are also methods available on :class:`~mongoengine.queryset.QuerySet`
objects that allow different results to be returned, for example, calling
@@ -292,11 +289,11 @@ the first matched by the query you provide. Aggregation functions may also be
used on :class:`~mongoengine.queryset.QuerySet` objects::
num_posts = Post.objects(tags='mongodb').count()
print 'Found %d posts with tag "mongodb"' % num_posts
print('Found {} posts with tag "mongodb"'.format(num_posts))
Learning more about mongoengine
Learning more about MongoEngine
-------------------------------
If you got this far you've made a great start, so well done! The next step on
your mongoengine journey is the `full user guide <guide/index.html>`_, where you
can learn indepth about how to use mongoengine and mongodb.
If you got this far you've made a great start, so well done! The next step on
your MongoEngine journey is the `full user guide <guide/index.html>`_, where
you can learn in-depth about how to use MongoEngine and MongoDB.

View File

@@ -2,6 +2,56 @@
Upgrading
#########
Development
***********
(Fill this out whenever you introduce breaking changes to MongoEngine)
0.12.0
******
This release includes various fixes for the `BaseQuerySet` methods and how they
are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size
to an already-existing queryset wouldn't modify the underlying PyMongo cursor.
This has been fixed now, so you'll need to make sure that your code didn't rely
on the broken implementation.
Additionally, a public `BaseQuerySet.clone_into` has been renamed to a private
`_clone_into`. If you directly used that method in your code, you'll need to
rename its occurrences.
0.11.0
******
This release includes a major rehaul of MongoEngine's code quality and
introduces a few breaking changes. It also touches many different parts of
the package and although all the changes have been tested and scrutinized,
you're encouraged to thorougly test the upgrade.
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
If you import or catch this exception, you'll need to rename it in your code.
Second breaking change drops Python v2.6 support. If you run MongoEngine on
that Python version, you'll need to upgrade it first.
Third breaking change drops an old backward compatibility measure where
`from mongoengine.base import ErrorClass` would work on top of
`from mongoengine.errors import ErrorClass` (where `ErrorClass` is e.g.
`ValidationError`). If you import any exceptions from `mongoengine.base`,
change it to `mongoengine.errors`.
0.10.8
******
This version fixed an issue where specifying a MongoDB URI host would override
more information than it should. These changes are minor, but they still
subtly modify the connection logic and thus you're encouraged to test your
MongoDB connection before shipping v0.10.8 in production.
0.10.7
******
`QuerySet.aggregate_sum` and `QuerySet.aggregate_average` are dropped. Use
`QuerySet.sum` and `QuerySet.average` instead which use the aggreation framework
by default from now on.
0.9.0
*****

View File

@@ -1,25 +1,35 @@
import connection
from connection import *
import document
from document import *
import errors
from errors import *
import fields
from fields import *
import queryset
from queryset import *
import signals
from signals import *
# Import submodules so that we can expose their __all__
from mongoengine import connection
from mongoengine import document
from mongoengine import errors
from mongoengine import fields
from mongoengine import queryset
from mongoengine import signals
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
list(queryset.__all__) + signals.__all__ + list(errors.__all__))
# Import everything from each submodule so that it can be accessed via
# mongoengine, e.g. instead of `from mongoengine.connection import connect`,
# users can simply use `from mongoengine import connect`, or even
# `from mongoengine import *` and then `connect('testdb')`.
from mongoengine.connection import *
from mongoengine.document import *
from mongoengine.errors import *
from mongoengine.fields import *
from mongoengine.queryset import *
from mongoengine.signals import *
VERSION = (0, 10, 7)
__all__ = (list(document.__all__) + list(fields.__all__) +
list(connection.__all__) + list(queryset.__all__) +
list(signals.__all__) + list(errors.__all__))
VERSION = (0, 12, 0)
def get_version():
if isinstance(VERSION[-1], basestring):
return '.'.join(map(str, VERSION[:-1])) + VERSION[-1]
"""Return the VERSION as a string, e.g. for VERSION == (0, 10, 7),
return '0.10.7'.
"""
return '.'.join(map(str, VERSION))

View File

@@ -1,8 +1,28 @@
# Base module is split into several files for convenience. Files inside of
# this module should import from a specific submodule (e.g.
# `from mongoengine.base.document import BaseDocument`), but all of the
# other modules should import directly from the top-level module (e.g.
# `from mongoengine.base import BaseDocument`). This approach is cleaner and
# also helps with cyclical import errors.
from mongoengine.base.common import *
from mongoengine.base.datastructures import *
from mongoengine.base.document import *
from mongoengine.base.fields import *
from mongoengine.base.metaclasses import *
# Help with backwards compatibility
from mongoengine.errors import *
__all__ = (
# common
'UPDATE_OPERATORS', '_document_registry', 'get_document',
# datastructures
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
# document
'BaseDocument',
# fields
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
# metaclasses
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
)

View File

@@ -1,13 +1,18 @@
from mongoengine.errors import NotRegistered
__all__ = ('ALLOW_INHERITANCE', 'get_document', '_document_registry')
__all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
'push_all', 'pull', 'pull_all', 'add_to_set',
'set_on_insert', 'min', 'max', 'rename'])
ALLOW_INHERITANCE = False
_document_registry = {}
def get_document(name):
"""Get a document class by name."""
doc = _document_registry.get(name, None)
if not doc:
# Possible old style name

View File

@@ -1,14 +1,16 @@
import itertools
import weakref
import six
from mongoengine.common import _import_class
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
__all__ = ("BaseDict", "BaseList", "EmbeddedDocumentList")
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList')
class BaseDict(dict):
"""A special dict so we can watch any changes"""
"""A special dict so we can watch any changes."""
_dereferenced = False
_instance = None
@@ -93,8 +95,7 @@ class BaseDict(dict):
class BaseList(list):
"""A special list so we can watch any changes
"""
"""A special list so we can watch any changes."""
_dereferenced = False
_instance = None
@@ -137,10 +138,7 @@ class BaseList(list):
return super(BaseList, self).__setitem__(key, value)
def __delitem__(self, key, *args, **kwargs):
if isinstance(key, slice):
self._mark_as_changed()
else:
self._mark_as_changed(key)
self._mark_as_changed()
return super(BaseList, self).__delitem__(key)
def __setslice__(self, *args, **kwargs):
@@ -209,17 +207,22 @@ class BaseList(list):
class EmbeddedDocumentList(BaseList):
@classmethod
def __match_all(cls, i, kwargs):
items = kwargs.items()
return all([
getattr(i, k) == v or unicode(getattr(i, k)) == v for k, v in items
])
def __match_all(cls, embedded_doc, kwargs):
"""Return True if a given embedded doc matches all the filter
kwargs. If it doesn't return False.
"""
for key, expected_value in kwargs.items():
doc_val = getattr(embedded_doc, key)
if doc_val != expected_value and six.text_type(doc_val) != expected_value:
return False
return True
@classmethod
def __only_matches(cls, obj, kwargs):
def __only_matches(cls, embedded_docs, kwargs):
"""Return embedded docs that match the filter kwargs."""
if not kwargs:
return obj
return filter(lambda i: cls.__match_all(i, kwargs), obj)
return embedded_docs
return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)]
def __init__(self, list_items, instance, name):
super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
@@ -285,18 +288,18 @@ class EmbeddedDocumentList(BaseList):
values = self.__only_matches(self, kwargs)
if len(values) == 0:
raise DoesNotExist(
"%s matching query does not exist." % self._name
'%s matching query does not exist.' % self._name
)
elif len(values) > 1:
raise MultipleObjectsReturned(
"%d items returned, instead of 1" % len(values)
'%d items returned, instead of 1' % len(values)
)
return values[0]
def first(self):
"""
Returns the first embedded document in the list, or ``None`` if empty.
"""Return the first embedded document in the list, or ``None``
if empty.
"""
if len(self) > 0:
return self[0]
@@ -426,7 +429,7 @@ class StrictDict(object):
def __eq__(self, other):
return self.items() == other.items()
def __neq__(self, other):
def __ne__(self, other):
return self.items() != other.items()
@classmethod
@@ -438,7 +441,7 @@ class StrictDict(object):
__slots__ = allowed_keys_tuple
def __repr__(self):
return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k) for k in self.iterkeys())
return '{%s}' % ', '.join('"{0!s}": {1!r}'.format(k, v) for k, v in self.items())
cls._classes[allowed_keys] = SpecificStrictDict
return cls._classes[allowed_keys]

View File

@@ -1,6 +1,5 @@
import copy
import numbers
import operator
from collections import Hashable
from functools import partial
@@ -8,30 +7,27 @@ from bson import ObjectId, json_util
from bson.dbref import DBRef
from bson.son import SON
import pymongo
import six
from mongoengine import signals
from mongoengine.base.common import ALLOW_INHERITANCE, get_document
from mongoengine.base.datastructures import (
BaseDict,
BaseList,
EmbeddedDocumentList,
SemiStrictDict,
StrictDict
)
from mongoengine.base.common import get_document
from mongoengine.base.datastructures import (BaseDict, BaseList,
EmbeddedDocumentList,
SemiStrictDict, StrictDict)
from mongoengine.base.fields import ComplexBaseField
from mongoengine.common import _import_class
from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError,
LookUpError, ValidationError)
from mongoengine.python_support import PY3, txt_type
LookUpError, OperationError, ValidationError)
__all__ = ('BaseDocument', 'NON_FIELD_ERRORS')
__all__ = ('BaseDocument',)
NON_FIELD_ERRORS = '__all__'
class BaseDocument(object):
__slots__ = ('_changed_fields', '_initialised', '_created', '_data',
'_dynamic_fields', '_auto_id_field', '_db_field_map', '__weakref__')
'_dynamic_fields', '_auto_id_field', '_db_field_map',
'__weakref__')
_dynamic = False
_dynamic_lock = True
@@ -57,15 +53,15 @@ class BaseDocument(object):
name = next(field)
if name in values:
raise TypeError(
"Multiple values for keyword argument '" + name + "'")
'Multiple values for keyword argument "%s"' % name)
values[name] = value
__auto_convert = values.pop("__auto_convert", True)
__auto_convert = values.pop('__auto_convert', True)
# 399: set default values only to fields loaded from DB
__only_fields = set(values.pop("__only_fields", values))
__only_fields = set(values.pop('__only_fields', values))
_created = values.pop("_created", True)
_created = values.pop('_created', True)
signals.pre_init.send(self.__class__, document=self, values=values)
@@ -76,7 +72,7 @@ class BaseDocument(object):
self._fields.keys() + ['id', 'pk', '_cls', '_text_score'])
if _undefined_fields:
msg = (
"The fields '{0}' do not exist on the document '{1}'"
'The fields "{0}" do not exist on the document "{1}"'
).format(_undefined_fields, self._class_name)
raise FieldDoesNotExist(msg)
@@ -95,7 +91,7 @@ class BaseDocument(object):
value = getattr(self, key, None)
setattr(self, key, value)
if "_cls" not in values:
if '_cls' not in values:
self._cls = self._class_name
# Set passed values after initialisation
@@ -121,7 +117,7 @@ class BaseDocument(object):
else:
self._data[key] = value
# Set any get_fieldname_display methods
# Set any get_<field>_display methods
self.__set_field_display()
if self._dynamic:
@@ -150,7 +146,7 @@ class BaseDocument(object):
if self._dynamic and not self._dynamic_lock:
if not hasattr(self, name) and not name.startswith('_'):
DynamicField = _import_class("DynamicField")
DynamicField = _import_class('DynamicField')
field = DynamicField(db_field=name)
field.name = name
self._dynamic_fields[name] = field
@@ -169,11 +165,13 @@ class BaseDocument(object):
except AttributeError:
self__created = True
if (self._is_document and not self__created and
name in self._meta.get('shard_key', tuple()) and
self._data.get(name) != value):
OperationError = _import_class('OperationError')
msg = "Shard Keys are immutable. Tried to update %s" % name
if (
self._is_document and
not self__created and
name in self._meta.get('shard_key', tuple()) and
self._data.get(name) != value
):
msg = 'Shard Keys are immutable. Tried to update %s' % name
raise OperationError(msg)
try:
@@ -197,8 +195,8 @@ class BaseDocument(object):
return data
def __setstate__(self, data):
if isinstance(data["_data"], SON):
data["_data"] = self.__class__._from_son(data["_data"])._data
if isinstance(data['_data'], SON):
data['_data'] = self.__class__._from_son(data['_data'])._data
for k in ('_changed_fields', '_initialised', '_created', '_data',
'_dynamic_fields'):
if k in data:
@@ -212,7 +210,7 @@ class BaseDocument(object):
dynamic_fields = data.get('_dynamic_fields') or SON()
for k in dynamic_fields.keys():
setattr(self, k, data["_data"].get(k))
setattr(self, k, data['_data'].get(k))
def __iter__(self):
return iter(self._fields_ordered)
@@ -254,12 +252,13 @@ class BaseDocument(object):
return repr_type('<%s: %s>' % (self.__class__.__name__, u))
def __str__(self):
# TODO this could be simpler?
if hasattr(self, '__unicode__'):
if PY3:
if six.PY3:
return self.__unicode__()
else:
return unicode(self).encode('utf-8')
return txt_type('%s object' % self.__class__.__name__)
return six.text_type(self).encode('utf-8')
return six.text_type('%s object' % self.__class__.__name__)
def __eq__(self, other):
if isinstance(other, self.__class__) and hasattr(other, 'id') and other.id is not None:
@@ -308,7 +307,7 @@ class BaseDocument(object):
fields = []
data = SON()
data["_id"] = None
data['_id'] = None
data['_cls'] = self._class_name
# only root fields ['test1.a', 'test2'] => ['test1', 'test2']
@@ -351,18 +350,8 @@ class BaseDocument(object):
else:
data[field.name] = value
# If "_id" has not been set, then try and set it
Document = _import_class("Document")
if isinstance(self, Document):
if data["_id"] is None:
data["_id"] = self._data.get("id", None)
if data['_id'] is None:
data.pop('_id')
# Only add _cls if allow_inheritance is True
if (not hasattr(self, '_meta') or
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
if not self._meta.get('allow_inheritance'):
data.pop('_cls')
return data
@@ -376,16 +365,16 @@ class BaseDocument(object):
if clean:
try:
self.clean()
except ValidationError, error:
except ValidationError as error:
errors[NON_FIELD_ERRORS] = error
# Get a list of tuples of field names and their current values
fields = [(self._fields.get(name, self._dynamic_fields.get(name)),
self._data.get(name)) for name in self._fields_ordered]
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
GenericEmbeddedDocumentField = _import_class(
"GenericEmbeddedDocumentField")
'GenericEmbeddedDocumentField')
for field, value in fields:
if value is not None:
@@ -395,27 +384,29 @@ class BaseDocument(object):
field._validate(value, clean=clean)
else:
field._validate(value)
except ValidationError, error:
except ValidationError as error:
errors[field.name] = error.errors or error
except (ValueError, AttributeError, AssertionError), error:
except (ValueError, AttributeError, AssertionError) as error:
errors[field.name] = error
elif field.required and not getattr(field, '_auto_gen', False):
errors[field.name] = ValidationError('Field is required',
field_name=field.name)
if errors:
pk = "None"
pk = 'None'
if hasattr(self, 'pk'):
pk = self.pk
elif self._instance and hasattr(self._instance, 'pk'):
pk = self._instance.pk
message = "ValidationError (%s:%s) " % (self._class_name, pk)
message = 'ValidationError (%s:%s) ' % (self._class_name, pk)
raise ValidationError(message, errors=errors)
def to_json(self, *args, **kwargs):
"""Converts a document to JSON.
:param use_db_field: Set to True by default but enables the output of the json structure with the field names
and not the mongodb store db_names in case of set to False
"""Convert this document to JSON.
:param use_db_field: Serialize field names as they appear in
MongoDB (as opposed to attribute names on this document).
Defaults to True.
"""
use_db_field = kwargs.pop('use_db_field', True)
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
@@ -426,33 +417,26 @@ class BaseDocument(object):
return cls._from_son(json_util.loads(json_data), created=created)
def __expand_dynamic_values(self, name, value):
"""expand any dynamic values to their correct types / values"""
"""Expand any dynamic values to their correct types / values."""
if not isinstance(value, (dict, list, tuple)):
return value
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
is_list = False
if not hasattr(value, 'items'):
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
if not is_list and '_cls' in value:
# If the value is a dict with '_cls' in it, turn it into a document
is_dict = isinstance(value, dict)
if is_dict and '_cls' in value:
cls = get_document(value['_cls'])
return cls(**value)
data = {}
for k, v in value.items():
key = name if is_list else k
data[k] = self.__expand_dynamic_values(key, v)
if is_list: # Convert back to a list
data_items = sorted(data.items(), key=operator.itemgetter(0))
value = [v for k, v in data_items]
if is_dict:
value = {
k: self.__expand_dynamic_values(k, v)
for k, v in value.items()
}
else:
value = data
value = [self.__expand_dynamic_values(name, v) for v in value]
# Convert lists / values so we can watch for any changes on them
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
if (isinstance(value, (list, tuple)) and
not isinstance(value, BaseList)):
if issubclass(type(self), EmbeddedDocumentListField):
@@ -465,8 +449,7 @@ class BaseDocument(object):
return value
def _mark_as_changed(self, key):
"""Marks a key as explicitly changed by the user
"""
"""Mark a key as explicitly changed by the user."""
if not key:
return
@@ -496,10 +479,11 @@ class BaseDocument(object):
remove(field)
def _clear_changed_fields(self):
"""Using get_changed_fields iterate and remove any fields that are
marked as changed"""
"""Using _get_changed_fields iterate and remove any fields that
are marked as changed.
"""
for changed in self._get_changed_fields():
parts = changed.split(".")
parts = changed.split('.')
data = self
for part in parts:
if isinstance(data, list):
@@ -511,10 +495,13 @@ class BaseDocument(object):
data = data.get(part, None)
else:
data = getattr(data, part, None)
if hasattr(data, "_changed_fields"):
if hasattr(data, "_is_document") and data._is_document:
if hasattr(data, '_changed_fields'):
if getattr(data, '_is_document', False):
continue
data._changed_fields = []
self._changed_fields = []
def _nestable_types_changed_fields(self, changed_fields, key, data, inspected):
@@ -526,26 +513,27 @@ class BaseDocument(object):
iterator = data.iteritems()
for index, value in iterator:
list_key = "%s%s." % (key, index)
list_key = '%s%s.' % (key, index)
# don't check anything lower if this key is already marked
# as changed.
if list_key[:-1] in changed_fields:
continue
if hasattr(value, '_get_changed_fields'):
changed = value._get_changed_fields(inspected)
changed_fields += ["%s%s" % (list_key, k)
changed_fields += ['%s%s' % (list_key, k)
for k in changed if k]
elif isinstance(value, (list, tuple, dict)):
self._nestable_types_changed_fields(
changed_fields, list_key, value, inspected)
def _get_changed_fields(self, inspected=None):
"""Returns a list of all fields that have explicitly been changed.
"""Return a list of all fields that have explicitly been changed.
"""
EmbeddedDocument = _import_class("EmbeddedDocument")
DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument")
ReferenceField = _import_class("ReferenceField")
SortedListField = _import_class("SortedListField")
EmbeddedDocument = _import_class('EmbeddedDocument')
DynamicEmbeddedDocument = _import_class('DynamicEmbeddedDocument')
ReferenceField = _import_class('ReferenceField')
SortedListField = _import_class('SortedListField')
changed_fields = []
changed_fields += getattr(self, '_changed_fields', [])
@@ -572,7 +560,7 @@ class BaseDocument(object):
):
# Find all embedded fields that have been changed
changed = data._get_changed_fields(inspected)
changed_fields += ["%s%s" % (key, k) for k in changed if k]
changed_fields += ['%s%s' % (key, k) for k in changed if k]
elif (isinstance(data, (list, tuple, dict)) and
db_field_name not in changed_fields):
if (hasattr(field, 'field') and
@@ -676,21 +664,33 @@ class BaseDocument(object):
@classmethod
def _get_collection_name(cls):
"""Returns the collection name for this class. None for abstract class
"""Return the collection name for this class. None for abstract
class.
"""
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.
"""Create an instance of a Document (subclass) from a PyMongo
SON.
"""
if not only_fields:
only_fields = []
# get the class name from the document, falling back to the given
if son and not isinstance(son, dict):
raise ValueError("The source SON object needs to be of type 'dict'")
# Get the class name from the document, falling back to the given
# class if unavailable
class_name = son.get('_cls', cls._class_name)
data = dict(("%s" % key, value) for key, value in son.iteritems())
# Convert SON to a data dict, making sure each key is a string and
# corresponds to the right db field.
data = {}
for key, value in son.iteritems():
key = str(key)
key = cls._db_field_map.get(key, key)
data[key] = value
# Return correct subclass for document type
if class_name != cls._class_name:
@@ -712,19 +712,20 @@ class BaseDocument(object):
else field.to_python(value))
if field_name != field.db_field:
del data[field.db_field]
except (AttributeError, ValueError), e:
except (AttributeError, ValueError) as e:
errors_dict[field_name] = e
if errors_dict:
errors = "\n".join(["%s - %s" % (k, v)
errors = '\n'.join(['%s - %s' % (k, v)
for k, v in errors_dict.items()])
msg = ("Invalid data to create a `%s` instance.\n%s"
msg = ('Invalid data to create a `%s` instance.\n%s'
% (cls._class_name, errors))
raise InvalidDocumentError(msg)
# In STRICT documents, remove any keys that aren't in cls._fields
if cls.STRICT:
data = dict((k, v)
for k, v in data.iteritems() if k in cls._fields)
data = {k: v for k, v in data.iteritems() if k in cls._fields}
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
obj._changed_fields = changed_fields
if not _auto_dereference:
@@ -734,37 +735,43 @@ class BaseDocument(object):
@classmethod
def _build_index_specs(cls, meta_indexes):
"""Generate and merge the full index specs
"""
"""Generate and merge the full index specs."""
geo_indices = cls._geo_indices()
unique_indices = cls._unique_with_indexes()
index_specs = [cls._build_index_spec(spec)
for spec in meta_indexes]
index_specs = [cls._build_index_spec(spec) for spec in meta_indexes]
def merge_index_specs(index_specs, indices):
"""Helper method for merging index specs."""
if not indices:
return index_specs
spec_fields = [v['fields']
for k, v in enumerate(index_specs)]
# Merge unique_indexes with existing specs
for k, v in enumerate(indices):
if v['fields'] in spec_fields:
index_specs[spec_fields.index(v['fields'])].update(v)
# Create a map of index fields to index spec. We're converting
# the fields from a list to a tuple so that it's hashable.
spec_fields = {
tuple(index['fields']): index for index in index_specs
}
# For each new index, if there's an existing index with the same
# fields list, update the existing spec with all data from the
# new spec.
for new_index in indices:
candidate = spec_fields.get(tuple(new_index['fields']))
if candidate is None:
index_specs.append(new_index)
else:
index_specs.append(v)
candidate.update(new_index)
return index_specs
# Merge geo indexes and unique_with indexes into the meta index specs.
index_specs = merge_index_specs(index_specs, geo_indices)
index_specs = merge_index_specs(index_specs, unique_indices)
return index_specs
@classmethod
def _build_index_spec(cls, spec):
"""Build a PyMongo index spec from a MongoEngine index spec.
"""
if isinstance(spec, basestring):
"""Build a PyMongo index spec from a MongoEngine index spec."""
if isinstance(spec, six.string_types):
spec = {'fields': [spec]}
elif isinstance(spec, (list, tuple)):
spec = {'fields': list(spec)}
@@ -775,8 +782,7 @@ class BaseDocument(object):
direction = None
# Check to see if we need to include _cls
allow_inheritance = cls._meta.get('allow_inheritance',
ALLOW_INHERITANCE)
allow_inheritance = cls._meta.get('allow_inheritance')
include_cls = (
allow_inheritance and
not spec.get('sparse', False) and
@@ -786,7 +792,7 @@ class BaseDocument(object):
# 733: don't include cls if index_cls is False unless there is an explicit cls with the index
include_cls = include_cls and (spec.get('cls', False) or cls._meta.get('index_cls', True))
if "cls" in spec:
if 'cls' in spec:
spec.pop('cls')
for key in spec['fields']:
# If inherited spec continue
@@ -801,19 +807,19 @@ class BaseDocument(object):
# GEOHAYSTACK from )
# GEO2D from *
direction = pymongo.ASCENDING
if key.startswith("-"):
if key.startswith('-'):
direction = pymongo.DESCENDING
elif key.startswith("$"):
elif key.startswith('$'):
direction = pymongo.TEXT
elif key.startswith("#"):
elif key.startswith('#'):
direction = pymongo.HASHED
elif key.startswith("("):
elif key.startswith('('):
direction = pymongo.GEOSPHERE
elif key.startswith(")"):
elif key.startswith(')'):
direction = pymongo.GEOHAYSTACK
elif key.startswith("*"):
elif key.startswith('*'):
direction = pymongo.GEO2D
if key.startswith(("+", "-", "*", "$", "#", "(", ")")):
if key.startswith(('+', '-', '*', '$', '#', '(', ')')):
key = key[1:]
# Use real field name, do it manually because we need field
@@ -826,7 +832,7 @@ class BaseDocument(object):
parts = []
for field in fields:
try:
if field != "_id":
if field != '_id':
field = field.db_field
except AttributeError:
pass
@@ -845,49 +851,53 @@ class BaseDocument(object):
return spec
@classmethod
def _unique_with_indexes(cls, namespace=""):
"""
Find and set unique indexes
"""
def _unique_with_indexes(cls, namespace=''):
"""Find unique indexes in the document schema and return them."""
unique_indexes = []
for field_name, field in cls._fields.items():
sparse = field.sparse
# Generate a list of indexes needed by uniqueness constraints
if field.unique:
unique_fields = [field.db_field]
# Add any unique_with fields to the back of the index spec
if field.unique_with:
if isinstance(field.unique_with, basestring):
if isinstance(field.unique_with, six.string_types):
field.unique_with = [field.unique_with]
# Convert unique_with field names to real field names
unique_with = []
for other_name in field.unique_with:
parts = other_name.split('.')
# Lookup real name
parts = cls._lookup_field(parts)
name_parts = [part.db_field for part in parts]
unique_with.append('.'.join(name_parts))
# Unique field should be required
parts[-1].required = True
sparse = (not sparse and
parts[-1].name not in cls.__dict__)
unique_fields += unique_with
# Add the new index to the list
fields = [("%s%s" % (namespace, f), pymongo.ASCENDING)
for f in unique_fields]
fields = [
('%s%s' % (namespace, f), pymongo.ASCENDING)
for f in unique_fields
]
index = {'fields': fields, 'unique': True, 'sparse': sparse}
unique_indexes.append(index)
if field.__class__.__name__ == "ListField":
if field.__class__.__name__ == 'ListField':
field = field.field
# Grab any embedded document field unique indexes
if (field.__class__.__name__ == "EmbeddedDocumentField" and
if (field.__class__.__name__ == 'EmbeddedDocumentField' and
field.document_type != cls):
field_namespace = "%s." % field_name
field_namespace = '%s.' % field_name
doc_cls = field.document_type
unique_indexes += doc_cls._unique_with_indexes(field_namespace)
@@ -899,8 +909,9 @@ class BaseDocument(object):
geo_indices = []
inspected.append(cls)
geo_field_type_names = ["EmbeddedDocumentField", "GeoPointField",
"PointField", "LineStringField", "PolygonField"]
geo_field_type_names = ('EmbeddedDocumentField', 'GeoPointField',
'PointField', 'LineStringField',
'PolygonField')
geo_field_types = tuple([_import_class(field)
for field in geo_field_type_names])
@@ -908,32 +919,68 @@ class BaseDocument(object):
for field in cls._fields.values():
if not isinstance(field, geo_field_types):
continue
if hasattr(field, 'document_type'):
field_cls = field.document_type
if field_cls in inspected:
continue
if hasattr(field_cls, '_geo_indices'):
geo_indices += field_cls._geo_indices(
inspected, parent_field=field.db_field)
elif field._geo_index:
field_name = field.db_field
if parent_field:
field_name = "%s.%s" % (parent_field, field_name)
geo_indices.append({'fields':
[(field_name, field._geo_index)]})
field_name = '%s.%s' % (parent_field, field_name)
geo_indices.append({
'fields': [(field_name, field._geo_index)]
})
return geo_indices
@classmethod
def _lookup_field(cls, parts):
"""Lookup a field based on its attribute and return a list containing
the field's parents and the field.
"""
"""Given the path to a given field, return a list containing
the Field object associated with that field and all of its parent
Field objects.
ListField = _import_class("ListField")
Args:
parts (str, list, or tuple) - path to the field. Should be a
string for simple fields existing on this document or a list
of strings for a field that exists deeper in embedded documents.
Returns:
A list of Field instances for fields that were found or
strings for sub-fields that weren't.
Example:
>>> user._lookup_field('name')
[<mongoengine.fields.StringField at 0x1119bff50>]
>>> user._lookup_field('roles')
[<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>]
>>> user._lookup_field(['roles', 'role'])
[<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>,
<mongoengine.fields.StringField at 0x1119ec050>]
>>> user._lookup_field('doesnt_exist')
raises LookUpError
>>> user._lookup_field(['roles', 'doesnt_exist'])
[<mongoengine.fields.EmbeddedDocumentListField at 0x1119ec250>,
'doesnt_exist']
"""
# TODO this method is WAY too complicated. Simplify it.
# TODO don't think returning a string for embedded non-existent fields is desired
ListField = _import_class('ListField')
DynamicField = _import_class('DynamicField')
if not isinstance(parts, (list, tuple)):
parts = [parts]
fields = []
field = None
@@ -943,16 +990,17 @@ class BaseDocument(object):
fields.append(field_name)
continue
# Look up first field from the document
if field is None:
# Look up first field from the document
if field_name == 'pk':
# Deal with "primary key" alias
field_name = cls._meta['id_field']
if field_name in cls._fields:
field = cls._fields[field_name]
elif cls._dynamic:
field = DynamicField(db_field=field_name)
elif cls._meta.get("allow_inheritance", False) or cls._meta.get("abstract", False):
elif cls._meta.get('allow_inheritance') or cls._meta.get('abstract', False):
# 744: in case the field is defined in a subclass
for subcls in cls.__subclasses__():
try:
@@ -965,35 +1013,55 @@ class BaseDocument(object):
else:
raise LookUpError('Cannot resolve field "%s"' % field_name)
else:
raise LookUpError('Cannot resolve field "%s"'
% field_name)
raise LookUpError('Cannot resolve field "%s"' % field_name)
else:
ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
# If previous field was a reference, throw an error (we
# cannot look up fields that are on references).
if isinstance(field, (ReferenceField, GenericReferenceField)):
raise LookUpError('Cannot perform join in mongoDB: %s' %
'__'.join(parts))
# If the parent field has a "field" attribute which has a
# lookup_member method, call it to find the field
# corresponding to this iteration.
if hasattr(getattr(field, 'field', None), 'lookup_member'):
new_field = field.field.lookup_member(field_name)
# If the parent field is a DynamicField or if it's part of
# a DynamicDocument, mark current field as a DynamicField
# with db_name equal to the field name.
elif cls._dynamic and (isinstance(field, DynamicField) or
getattr(getattr(field, 'document_type', None), '_dynamic', None)):
new_field = DynamicField(db_field=field_name)
# Else, try to use the parent field's lookup_member method
# to find the subfield.
elif hasattr(field, 'lookup_member'):
new_field = field.lookup_member(field_name)
# Raise a LookUpError if all the other conditions failed.
else:
# Look up subfield on the previous field or raise
try:
new_field = field.lookup_member(field_name)
except AttributeError:
raise LookUpError('Cannot resolve subfield or operator {} '
'on the field {}'.format(
field_name, field.name))
raise LookUpError(
'Cannot resolve subfield or operator {} '
'on the field {}'.format(field_name, field.name)
)
# If current field still wasn't found and the parent field
# is a ComplexBaseField, add the name current field name and
# move on.
if not new_field and isinstance(field, ComplexBaseField):
fields.append(field_name)
continue
elif not new_field:
raise LookUpError('Cannot resolve field "%s"'
% field_name)
raise LookUpError('Cannot resolve field "%s"' % field_name)
field = new_field # update field to the new field type
fields.append(field)
return fields
@classmethod
@@ -1005,19 +1073,18 @@ class BaseDocument(object):
return '.'.join(parts)
def __set_field_display(self):
"""Dynamically set the display value for a field with choices"""
for attr_name, field in self._fields.items():
if field.choices:
if self._dynamic:
obj = self
else:
obj = type(self)
setattr(obj,
'get_%s_display' % attr_name,
partial(self.__get_field_display, field=field))
"""For each field that specifies choices, create a
get_<field>_display method.
"""
fields_with_choices = [(n, f) for n, f in self._fields.items()
if f.choices]
for attr_name, field in fields_with_choices:
setattr(self,
'get_%s_display' % attr_name,
partial(self.__get_field_display, field=field))
def __get_field_display(self, field):
"""Returns the display value for a choice field"""
"""Return the display value for a choice field"""
value = getattr(self, field.name)
if field.choices and isinstance(field.choices[0], (list, tuple)):
return dict(field.choices).get(value, value)

View File

@@ -4,21 +4,17 @@ import weakref
from bson import DBRef, ObjectId, SON
import pymongo
import six
from mongoengine.base.common import ALLOW_INHERITANCE
from mongoengine.base.datastructures import (
BaseDict, BaseList, EmbeddedDocumentList
)
from mongoengine.base.common import UPDATE_OPERATORS
from mongoengine.base.datastructures import (BaseDict, BaseList,
EmbeddedDocumentList)
from mongoengine.common import _import_class
from mongoengine.errors import ValidationError
__all__ = ("BaseField", "ComplexBaseField",
"ObjectIdField", "GeoJsonBaseField")
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
'push_all', 'pull', 'pull_all', 'add_to_set',
'set_on_insert', 'min', 'max'])
__all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
'GeoJsonBaseField')
class BaseField(object):
@@ -27,7 +23,6 @@ class BaseField(object):
.. versionchanged:: 0.5 - added verbose and help text
"""
name = None
_geo_index = False
_auto_gen = False # Call `generate` to generate a value
@@ -46,7 +41,7 @@ class BaseField(object):
"""
:param db_field: The database field to store this field in
(defaults to the name of the field)
:param name: Depreciated - use db_field
:param name: Deprecated - use db_field
:param required: If the field is required. Whether it has to have a
value or not. Defaults to False.
:param default: (optional) The default value for this field if no value
@@ -73,7 +68,7 @@ class BaseField(object):
self.db_field = (db_field or name) if not primary_key else '_id'
if name:
msg = "Fields' 'name' attribute deprecated in favour of 'db_field'"
msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
warnings.warn(msg, DeprecationWarning)
self.required = required or primary_key
self.default = default
@@ -86,10 +81,21 @@ class BaseField(object):
self.sparse = sparse
self._owner_document = None
# Validate the db_field
if isinstance(self.db_field, six.string_types) and (
'.' in self.db_field or
'\0' in self.db_field or
self.db_field.startswith('$')
):
raise ValueError(
'field names cannot contain dots (".") or null characters '
'("\\0"), and they must not start with a dollar sign ("$").'
)
# Detect and report conflicts between metadata and base properties.
conflicts = set(dir(self)) & set(kwargs)
if conflicts:
raise TypeError("%s already has attribute(s): %s" % (
raise TypeError('%s already has attribute(s): %s' % (
self.__class__.__name__, ', '.join(conflicts)))
# Assign metadata to the instance
@@ -147,25 +153,21 @@ class BaseField(object):
v._instance = weakref.proxy(instance)
instance._data[self.name] = value
def error(self, message="", errors=None, field_name=None):
"""Raises a ValidationError.
"""
def error(self, message='', errors=None, field_name=None):
"""Raise a ValidationError."""
field_name = field_name if field_name else self.name
raise ValidationError(message, errors=errors, field_name=field_name)
def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type.
"""
"""Convert a MongoDB-compatible type to a Python type."""
return value
def to_mongo(self, value):
"""Convert a Python type to a MongoDB-compatible type.
"""
"""Convert a Python type to a MongoDB-compatible type."""
return self.to_python(value)
def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
"""A helper method to call to_mongo with proper inputs
"""
"""Helper method to call to_mongo with proper inputs."""
f_inputs = self.to_mongo.__code__.co_varnames
ex_vars = {}
if 'fields' in f_inputs:
@@ -177,15 +179,13 @@ class BaseField(object):
return self.to_mongo(value, **ex_vars)
def prepare_query_value(self, op, value):
"""Prepare a value that is being used in a query for PyMongo.
"""
"""Prepare a value that is being used in a query for PyMongo."""
if op in UPDATE_OPERATORS:
self.validate(value)
return value
def validate(self, value, clean=True):
"""Perform validation on a value.
"""
"""Perform validation on a value."""
pass
def _validate_choices(self, value):
@@ -193,18 +193,21 @@ class BaseField(object):
EmbeddedDocument = _import_class('EmbeddedDocument')
choice_list = self.choices
if isinstance(choice_list[0], (list, tuple)):
if isinstance(next(iter(choice_list)), (list, tuple)):
# next(iter) is useful for sets
choice_list = [k for k, _ in choice_list]
# Choices which are other types of Documents
if isinstance(value, (Document, EmbeddedDocument)):
if not any(isinstance(value, c) for c in choice_list):
self.error(
'Value must be instance of %s' % unicode(choice_list)
'Value must be an instance of %s' % (
six.text_type(choice_list)
)
)
# Choices which are types other than Documents
elif value not in choice_list:
self.error('Value must be one of %s' % unicode(choice_list))
self.error('Value must be one of %s' % six.text_type(choice_list))
def _validate(self, value, **kwargs):
# Check the Choices Constraint
@@ -247,8 +250,7 @@ class ComplexBaseField(BaseField):
field = None
def __get__(self, instance, owner):
"""Descriptor to automatically dereference references.
"""
"""Descriptor to automatically dereference references."""
if instance is None:
# Document class being used rather than a document object
return self
@@ -260,7 +262,7 @@ class ComplexBaseField(BaseField):
(self.field is None or isinstance(self.field,
(GenericReferenceField, ReferenceField))))
_dereference = _import_class("DeReference")()
_dereference = _import_class('DeReference')()
self._auto_dereference = instance._fields[self.name]._auto_dereference
if instance._initialised and dereference and instance._data.get(self.name):
@@ -295,9 +297,8 @@ class ComplexBaseField(BaseField):
return value
def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type.
"""
if isinstance(value, basestring):
"""Convert a MongoDB-compatible type to a Python type."""
if isinstance(value, six.string_types):
return value
if hasattr(value, 'to_python'):
@@ -307,14 +308,14 @@ class ComplexBaseField(BaseField):
if not hasattr(value, 'items'):
try:
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
value = {k: v for k, v in enumerate(value)}
except TypeError: # Not iterable return the value
return value
if self.field:
self.field._auto_dereference = self._auto_dereference
value_dict = dict([(key, self.field.to_python(item))
for key, item in value.items()])
value_dict = {key: self.field.to_python(item)
for key, item in value.items()}
else:
Document = _import_class('Document')
value_dict = {}
@@ -337,13 +338,12 @@ class ComplexBaseField(BaseField):
return value_dict
def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB-compatible type.
"""
Document = _import_class("Document")
EmbeddedDocument = _import_class("EmbeddedDocument")
GenericReferenceField = _import_class("GenericReferenceField")
"""Convert a Python type to a MongoDB-compatible type."""
Document = _import_class('Document')
EmbeddedDocument = _import_class('EmbeddedDocument')
GenericReferenceField = _import_class('GenericReferenceField')
if isinstance(value, basestring):
if isinstance(value, six.string_types):
return value
if hasattr(value, 'to_mongo'):
@@ -360,13 +360,15 @@ class ComplexBaseField(BaseField):
if not hasattr(value, 'items'):
try:
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
value = {k: v for k, v in enumerate(value)}
except TypeError: # Not iterable return the value
return value
if self.field:
value_dict = dict([(key, self.field._to_mongo_safe_call(item, use_db_field, fields))
for key, item in value.iteritems()])
value_dict = {
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
for key, item in value.iteritems()
}
else:
value_dict = {}
for k, v in value.iteritems():
@@ -380,9 +382,7 @@ class ComplexBaseField(BaseField):
# any _cls data so make it a generic reference allows
# us to dereference
meta = getattr(v, '_meta', {})
allow_inheritance = (
meta.get('allow_inheritance', ALLOW_INHERITANCE)
is True)
allow_inheritance = meta.get('allow_inheritance')
if not allow_inheritance and not self.field:
value_dict[k] = GenericReferenceField().to_mongo(v)
else:
@@ -404,8 +404,7 @@ class ComplexBaseField(BaseField):
return value_dict
def validate(self, value):
"""If field is provided ensure the value is valid.
"""
"""If field is provided ensure the value is valid."""
errors = {}
if self.field:
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
@@ -415,9 +414,9 @@ class ComplexBaseField(BaseField):
for k, v in sequence:
try:
self.field._validate(v)
except ValidationError, error:
except ValidationError as error:
errors[k] = error.errors or error
except (ValueError, AssertionError), error:
except (ValueError, AssertionError) as error:
errors[k] = error
if errors:
@@ -443,8 +442,7 @@ class ComplexBaseField(BaseField):
class ObjectIdField(BaseField):
"""A field wrapper around MongoDB's ObjectIds.
"""
"""A field wrapper around MongoDB's ObjectIds."""
def to_python(self, value):
try:
@@ -457,10 +455,10 @@ class ObjectIdField(BaseField):
def to_mongo(self, value):
if not isinstance(value, ObjectId):
try:
return ObjectId(unicode(value))
except Exception, e:
return ObjectId(six.text_type(value))
except Exception as e:
# e.message attribute has been deprecated since Python 2.6
self.error(unicode(e))
self.error(six.text_type(e))
return value
def prepare_query_value(self, op, value):
@@ -468,7 +466,7 @@ class ObjectIdField(BaseField):
def validate(self, value):
try:
ObjectId(unicode(value))
ObjectId(six.text_type(value))
except Exception:
self.error('Invalid Object ID')
@@ -480,21 +478,20 @@ class GeoJsonBaseField(BaseField):
"""
_geo_index = pymongo.GEOSPHERE
_type = "GeoBase"
_type = 'GeoBase'
def __init__(self, auto_index=True, *args, **kwargs):
"""
:param bool auto_index: Automatically create a "2dsphere" index.\
:param bool auto_index: Automatically create a '2dsphere' index.\
Defaults to `True`.
"""
self._name = "%sField" % self._type
self._name = '%sField' % self._type
if not auto_index:
self._geo_index = False
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
def validate(self, value):
"""Validate the GeoJson object based on its type
"""
"""Validate the GeoJson object based on its type."""
if isinstance(value, dict):
if set(value.keys()) == set(['type', 'coordinates']):
if value['type'] != self._type:
@@ -509,7 +506,7 @@ class GeoJsonBaseField(BaseField):
self.error('%s can only accept lists of [x, y]' % self._name)
return
validate = getattr(self, "_validate_%s" % self._type.lower())
validate = getattr(self, '_validate_%s' % self._type.lower())
error = validate(value)
if error:
self.error(error)
@@ -522,7 +519,7 @@ class GeoJsonBaseField(BaseField):
try:
value[0][0][0]
except (TypeError, IndexError):
return "Invalid Polygon must contain at least one valid linestring"
return 'Invalid Polygon must contain at least one valid linestring'
errors = []
for val in value:
@@ -533,12 +530,12 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
if top_level:
return "Invalid Polygon:\n%s" % ", ".join(errors)
return 'Invalid Polygon:\n%s' % ', '.join(errors)
else:
return "%s" % ", ".join(errors)
return '%s' % ', '.join(errors)
def _validate_linestring(self, value, top_level=True):
"""Validates a linestring"""
"""Validate a linestring."""
if not isinstance(value, (list, tuple)):
return 'LineStrings must contain list of coordinate pairs'
@@ -546,7 +543,7 @@ class GeoJsonBaseField(BaseField):
try:
value[0][0]
except (TypeError, IndexError):
return "Invalid LineString must contain at least one valid point"
return 'Invalid LineString must contain at least one valid point'
errors = []
for val in value:
@@ -555,19 +552,19 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
if top_level:
return "Invalid LineString:\n%s" % ", ".join(errors)
return 'Invalid LineString:\n%s' % ', '.join(errors)
else:
return "%s" % ", ".join(errors)
return '%s' % ', '.join(errors)
def _validate_point(self, value):
"""Validate each set of coords"""
if not isinstance(value, (list, tuple)):
return 'Points must be a list of coordinate pairs'
elif not len(value) == 2:
return "Value (%s) must be a two-dimensional point" % repr(value)
return 'Value (%s) must be a two-dimensional point' % repr(value)
elif (not isinstance(value[0], (float, int)) or
not isinstance(value[1], (float, int))):
return "Both values (%s) in point must be float or int" % repr(value)
return 'Both values (%s) in point must be float or int' % repr(value)
def _validate_multipoint(self, value):
if not isinstance(value, (list, tuple)):
@@ -577,7 +574,7 @@ class GeoJsonBaseField(BaseField):
try:
value[0][0]
except (TypeError, IndexError):
return "Invalid MultiPoint must contain at least one valid point"
return 'Invalid MultiPoint must contain at least one valid point'
errors = []
for point in value:
@@ -586,7 +583,7 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
return "%s" % ", ".join(errors)
return '%s' % ', '.join(errors)
def _validate_multilinestring(self, value, top_level=True):
if not isinstance(value, (list, tuple)):
@@ -596,7 +593,7 @@ class GeoJsonBaseField(BaseField):
try:
value[0][0][0]
except (TypeError, IndexError):
return "Invalid MultiLineString must contain at least one valid linestring"
return 'Invalid MultiLineString must contain at least one valid linestring'
errors = []
for linestring in value:
@@ -606,9 +603,9 @@ class GeoJsonBaseField(BaseField):
if errors:
if top_level:
return "Invalid MultiLineString:\n%s" % ", ".join(errors)
return 'Invalid MultiLineString:\n%s' % ', '.join(errors)
else:
return "%s" % ", ".join(errors)
return '%s' % ', '.join(errors)
def _validate_multipolygon(self, value):
if not isinstance(value, (list, tuple)):
@@ -618,7 +615,7 @@ class GeoJsonBaseField(BaseField):
try:
value[0][0][0][0]
except (TypeError, IndexError):
return "Invalid MultiPolygon must contain at least one valid Polygon"
return 'Invalid MultiPolygon must contain at least one valid Polygon'
errors = []
for polygon in value:
@@ -627,9 +624,9 @@ class GeoJsonBaseField(BaseField):
errors.append(error)
if errors:
return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
return 'Invalid MultiPolygon:\n%s' % ', '.join(errors)
def to_mongo(self, value):
if isinstance(value, dict):
return value
return SON([("type", self._type), ("coordinates", value)])
return SON([('type', self._type), ('coordinates', value)])

View File

@@ -1,10 +1,11 @@
import warnings
from mongoengine.base.common import ALLOW_INHERITANCE, _document_registry
import six
from mongoengine.base.common import _document_registry
from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
from mongoengine.common import _import_class
from mongoengine.errors import InvalidDocumentError
from mongoengine.python_support import PY3
from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
MultipleObjectsReturned,
QuerySetManager)
@@ -45,7 +46,8 @@ class DocumentMetaclass(type):
attrs['_meta'] = meta
attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
if attrs['_meta'].get('allow_inheritance', ALLOW_INHERITANCE):
# If allow_inheritance is True, add a "_cls" string field to the attrs
if attrs['_meta'].get('allow_inheritance'):
StringField = _import_class('StringField')
attrs['_cls'] = StringField()
@@ -87,16 +89,17 @@ class DocumentMetaclass(type):
# Ensure no duplicate db_fields
duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
if duplicate_db_fields:
msg = ("Multiple db_fields defined for: %s " %
", ".join(duplicate_db_fields))
msg = ('Multiple db_fields defined for: %s ' %
', '.join(duplicate_db_fields))
raise InvalidDocumentError(msg)
# Set _fields and db_field maps
attrs['_fields'] = doc_fields
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
for k, v in doc_fields.iteritems()])
attrs['_reverse_db_field_map'] = dict(
(v, k) for k, v in attrs['_db_field_map'].iteritems())
attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
for k, v in doc_fields.items()}
attrs['_reverse_db_field_map'] = {
v: k for k, v in attrs['_db_field_map'].items()
}
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
(v.creation_counter, v.name)
@@ -116,10 +119,8 @@ class DocumentMetaclass(type):
if hasattr(base, '_meta'):
# Warn if allow_inheritance isn't set and prevent
# inheritance of classes where inheritance is set to False
allow_inheritance = base._meta.get('allow_inheritance',
ALLOW_INHERITANCE)
if (allow_inheritance is not True and
not base._meta.get('abstract')):
allow_inheritance = base._meta.get('allow_inheritance')
if not allow_inheritance and not base._meta.get('abstract'):
raise ValueError('Document %s may not be subclassed' %
base.__name__)
@@ -161,7 +162,7 @@ class DocumentMetaclass(type):
# module continues to use im_func and im_self, so the code below
# copies __func__ into im_func and __self__ into im_self for
# classmethod objects in Document derived classes.
if PY3:
if six.PY3:
for val in new_class.__dict__.values():
if isinstance(val, classmethod):
f = val.__get__(new_class)
@@ -179,11 +180,11 @@ class DocumentMetaclass(type):
if isinstance(f, CachedReferenceField):
if issubclass(new_class, EmbeddedDocument):
raise InvalidDocumentError(
"CachedReferenceFields is not allowed in EmbeddedDocuments")
raise InvalidDocumentError('CachedReferenceFields is not '
'allowed in EmbeddedDocuments')
if not f.document_type:
raise InvalidDocumentError(
"Document is not available to sync")
'Document is not available to sync')
if f.auto_sync:
f.start_listener()
@@ -195,8 +196,8 @@ class DocumentMetaclass(type):
'reverse_delete_rule',
DO_NOTHING)
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
msg = ("Reverse delete rules are not supported "
"for %s (field: %s)" %
msg = ('Reverse delete rules are not supported '
'for %s (field: %s)' %
(field.__class__.__name__, field.name))
raise InvalidDocumentError(msg)
@@ -204,16 +205,16 @@ class DocumentMetaclass(type):
if delete_rule != DO_NOTHING:
if issubclass(new_class, EmbeddedDocument):
msg = ("Reverse delete rules are not supported for "
"EmbeddedDocuments (field: %s)" % field.name)
msg = ('Reverse delete rules are not supported for '
'EmbeddedDocuments (field: %s)' % field.name)
raise InvalidDocumentError(msg)
f.document_type.register_delete_rule(new_class,
field.name, delete_rule)
if (field.name and hasattr(Document, field.name) and
EmbeddedDocument not in new_class.mro()):
msg = ("%s is a document method and not a valid "
"field name" % field.name)
msg = ('%s is a document method and not a valid '
'field name' % field.name)
raise InvalidDocumentError(msg)
return new_class
@@ -271,6 +272,11 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
'index_drop_dups': False,
'index_opts': None,
'delete_rules': None,
# allow_inheritance can be True, False, and None. True means
# "allow inheritance", False means "don't allow inheritance",
# None means "do whatever your parent does, or don't allow
# inheritance if you're a top-level class".
'allow_inheritance': None,
}
attrs['_is_base_cls'] = True
@@ -303,7 +309,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
# If parent wasn't an abstract class
if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
not parent_doc_cls._meta.get('abstract', True)):
msg = "Trying to set a collection on a subclass (%s)" % name
msg = 'Trying to set a collection on a subclass (%s)' % name
warnings.warn(msg, SyntaxWarning)
del attrs['_meta']['collection']
@@ -311,7 +317,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
if (parent_doc_cls and
not parent_doc_cls._meta.get('abstract', False)):
msg = "Abstract document cannot have non-abstract base"
msg = 'Abstract document cannot have non-abstract base'
raise ValueError(msg)
return super_new(cls, name, bases, attrs)
@@ -334,12 +340,16 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
meta.merge(attrs.get('_meta', {})) # Top level meta
# Only simple classes (direct subclasses of Document)
# may set allow_inheritance to False
# Only simple classes (i.e. direct subclasses of Document) may set
# allow_inheritance to False. If the base Document allows inheritance,
# none of its subclasses can override allow_inheritance to False.
simple_class = all([b._meta.get('abstract')
for b in flattened_bases if hasattr(b, '_meta')])
if (not simple_class and meta['allow_inheritance'] is False and
not meta['abstract']):
if (
not simple_class and
meta['allow_inheritance'] is False and
not meta['abstract']
):
raise ValueError('Only direct subclasses of Document may set '
'"allow_inheritance" to False')

View File

@@ -34,7 +34,10 @@ def _import_class(cls_name):
queryset_classes = ('OperationError',)
deref_classes = ('DeReference',)
if cls_name in doc_classes:
if cls_name == 'BaseDocument':
from mongoengine.base import document as module
import_classes = ['BaseDocument']
elif cls_name in doc_classes:
from mongoengine import document as module
import_classes = doc_classes
elif cls_name in field_classes:

View File

@@ -1,7 +1,9 @@
from pymongo import MongoClient, ReadPreference, uri_parser
from mongoengine.python_support import (IS_PYMONGO_3, str_types)
import six
__all__ = ['ConnectionError', 'connect', 'register_connection',
from mongoengine.python_support import IS_PYMONGO_3
__all__ = ['MongoEngineConnectionError', 'connect', 'register_connection',
'DEFAULT_CONNECTION_NAME']
@@ -14,7 +16,10 @@ else:
READ_PREFERENCE = False
class ConnectionError(Exception):
class MongoEngineConnectionError(Exception):
"""Error raised when the database connection can't be established or
when a connection with a requested alias can't be retrieved.
"""
pass
@@ -25,7 +30,8 @@ _dbs = {}
def register_connection(alias, name=None, host=None, port=None,
read_preference=READ_PREFERENCE,
username=None, password=None, authentication_source=None,
username=None, password=None,
authentication_source=None,
authentication_mechanism=None,
**kwargs):
"""Add a connection.
@@ -45,12 +51,12 @@ def register_connection(alias, name=None, host=None, port=None,
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
:param is_mock: explicitly use mongomock for this connection
(can also be done by using `mongomock://` as db host prefix)
:param kwargs: allow ad-hoc parameters to be passed into the pymongo driver
:param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list.
.. versionchanged:: 0.10.6 - added mongomock support
"""
global _connection_settings
conn_settings = {
'name': name or 'test',
'host': host or 'localhost',
@@ -62,31 +68,37 @@ def register_connection(alias, name=None, host=None, port=None,
'authentication_mechanism': authentication_mechanism
}
# Handle uri style connections
conn_host = conn_settings['host']
# host can be a list or a string, so if string, force to a list
if isinstance(conn_host, str_types):
# Host can be a list or a string, so if string, force to a list.
if isinstance(conn_host, six.string_types):
conn_host = [conn_host]
resolved_hosts = []
for entity in conn_host:
# Handle uri style connections
# Handle Mongomock
if entity.startswith('mongomock://'):
conn_settings['is_mock'] = True
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
# Handle URI style connections, only updating connection params which
# were explicitly specified in the URI.
elif '://' in entity:
uri_dict = uri_parser.parse_uri(entity)
resolved_hosts.append(entity)
conn_settings.update({
'name': uri_dict.get('database') or name,
'username': uri_dict.get('username'),
'password': uri_dict.get('password'),
'read_preference': read_preference,
})
if uri_dict.get('database'):
conn_settings['name'] = uri_dict.get('database')
for param in ('read_preference', 'username', 'password'):
if uri_dict.get(param):
conn_settings[param] = uri_dict[param]
uri_options = uri_dict['options']
if 'replicaset' in uri_options:
conn_settings['replicaSet'] = True
conn_settings['replicaSet'] = uri_options['replicaset']
if 'authsource' in uri_options:
conn_settings['authentication_source'] = uri_options['authsource']
if 'authmechanism' in uri_options:
@@ -104,9 +116,7 @@ def register_connection(alias, name=None, host=None, port=None,
def disconnect(alias=DEFAULT_CONNECTION_NAME):
global _connections
global _dbs
"""Close the connection with a given alias."""
if alias in _connections:
get_connection(alias=alias).close()
del _connections[alias]
@@ -115,71 +125,99 @@ def disconnect(alias=DEFAULT_CONNECTION_NAME):
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
global _connections
"""Return a connection with a given alias."""
# Connect to the database if not already connected
if reconnect:
disconnect(alias)
if alias not in _connections:
if alias not in _connection_settings:
msg = 'Connection with alias "%s" has not been defined' % alias
if alias == DEFAULT_CONNECTION_NAME:
msg = 'You have not defined a default connection'
raise ConnectionError(msg)
conn_settings = _connection_settings[alias].copy()
# If the requested alias already exists in the _connections list, return
# it immediately.
if alias in _connections:
return _connections[alias]
conn_settings.pop('name', None)
conn_settings.pop('username', None)
conn_settings.pop('password', None)
conn_settings.pop('authentication_source', None)
conn_settings.pop('authentication_mechanism', None)
is_mock = conn_settings.pop('is_mock', None)
if is_mock:
# Use MongoClient from mongomock
try:
import mongomock
except ImportError:
raise RuntimeError('You need mongomock installed '
'to mock MongoEngine.')
connection_class = mongomock.MongoClient
# Validate that the requested alias exists in the _connection_settings.
# Raise MongoEngineConnectionError if it doesn't.
if alias not in _connection_settings:
if alias == DEFAULT_CONNECTION_NAME:
msg = 'You have not defined a default connection'
else:
# Use MongoClient from pymongo
connection_class = MongoClient
msg = 'Connection with alias "%s" has not been defined' % alias
raise MongoEngineConnectionError(msg)
def _clean_settings(settings_dict):
irrelevant_fields = set([
'name', 'username', 'password', 'authentication_source',
'authentication_mechanism'
])
return {
k: v for k, v in settings_dict.items()
if k not in irrelevant_fields
}
# Retrieve a copy of the connection settings associated with the requested
# alias and remove the database name and authentication info (we don't
# care about them at this point).
conn_settings = _clean_settings(_connection_settings[alias].copy())
# Determine if we should use PyMongo's or mongomock's MongoClient.
is_mock = conn_settings.pop('is_mock', False)
if is_mock:
try:
import mongomock
except ImportError:
raise RuntimeError('You need mongomock installed to mock '
'MongoEngine.')
connection_class = mongomock.MongoClient
else:
connection_class = MongoClient
# For replica set connections with PyMongo 2.x, use
# MongoReplicaSetClient.
# TODO remove this once we stop supporting PyMongo 2.x.
if 'replicaSet' in conn_settings and not IS_PYMONGO_3:
connection_class = MongoReplicaSetClient
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
# hosts_or_uri has to be a string, so if 'host' was provided
# as a list, join its parts and separate them by ','
if isinstance(conn_settings['hosts_or_uri'], list):
conn_settings['hosts_or_uri'] = ','.join(
conn_settings['hosts_or_uri'])
if 'replicaSet' in conn_settings:
# Discard port since it can't be used on MongoReplicaSetClient
conn_settings.pop('port', None)
# Discard replicaSet if not base string
if not isinstance(conn_settings['replicaSet'], basestring):
conn_settings.pop('replicaSet', None)
if not IS_PYMONGO_3:
connection_class = MongoReplicaSetClient
conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
# Iterate over all of the connection settings and if a connection with
# the same parameters is already established, use it instead of creating
# a new one.
existing_connection = None
connection_settings_iterator = (
(db_alias, settings.copy())
for db_alias, settings in _connection_settings.items()
)
for db_alias, connection_settings in connection_settings_iterator:
connection_settings = _clean_settings(connection_settings)
if conn_settings == connection_settings and _connections.get(db_alias):
existing_connection = _connections[db_alias]
break
# If an existing connection was found, assign it to the new alias
if existing_connection:
_connections[alias] = existing_connection
else:
# Otherwise, create the new connection for this alias. Raise
# MongoEngineConnectionError if it can't be established.
try:
connection = None
# check for shared connections
connection_settings_iterator = (
(db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems())
for db_alias, connection_settings in connection_settings_iterator:
connection_settings.pop('name', None)
connection_settings.pop('username', None)
connection_settings.pop('password', None)
connection_settings.pop('authentication_source', None)
connection_settings.pop('authentication_mechanism', None)
if conn_settings == connection_settings and _connections.get(db_alias, None):
connection = _connections[db_alias]
break
_connections[alias] = connection_class(**conn_settings)
except Exception as e:
raise MongoEngineConnectionError(
'Cannot connect to database %s :\n%s' % (alias, e))
_connections[alias] = connection if connection else connection_class(**conn_settings)
except Exception, e:
raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e))
return _connections[alias]
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
global _dbs
if reconnect:
disconnect(alias)
@@ -205,12 +243,14 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
running on the default port on localhost. If authentication is needed,
provide username and password arguments as well.
Multiple databases are supported by using aliases. Provide a separate
Multiple databases are supported by using aliases. Provide a separate
`alias` to connect to a different instance of :program:`mongod`.
See the docstring for `register_connection` for more details about all
supported kwargs.
.. versionchanged:: 0.6 - added multiple database support.
"""
global _connections
if alias not in _connections:
register_connection(alias, db, **kwargs)

View File

@@ -2,12 +2,12 @@ from mongoengine.common import _import_class
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
__all__ = ("switch_db", "switch_collection", "no_dereference",
"no_sub_classes", "query_counter")
__all__ = ('switch_db', 'switch_collection', 'no_dereference',
'no_sub_classes', 'query_counter')
class switch_db(object):
""" switch_db alias context manager.
"""switch_db alias context manager.
Example ::
@@ -18,15 +18,14 @@ class switch_db(object):
class Group(Document):
name = StringField()
Group(name="test").save() # Saves in the default db
Group(name='test').save() # Saves in the default db
with switch_db(Group, 'testdb-1') as Group:
Group(name="hello testdb!").save() # Saves in testdb-1
Group(name='hello testdb!').save() # Saves in testdb-1
"""
def __init__(self, cls, db_alias):
""" Construct the switch_db context manager
"""Construct the switch_db context manager
:param cls: the class to change the registered db
:param db_alias: the name of the specific database to use
@@ -34,37 +33,36 @@ class switch_db(object):
self.cls = cls
self.collection = cls._get_collection()
self.db_alias = db_alias
self.ori_db_alias = cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME)
self.ori_db_alias = cls._meta.get('db_alias', DEFAULT_CONNECTION_NAME)
def __enter__(self):
""" change the db_alias and clear the cached collection """
self.cls._meta["db_alias"] = self.db_alias
"""Change the db_alias and clear the cached collection."""
self.cls._meta['db_alias'] = self.db_alias
self.cls._collection = None
return self.cls
def __exit__(self, t, value, traceback):
""" Reset the db_alias and collection """
self.cls._meta["db_alias"] = self.ori_db_alias
"""Reset the db_alias and collection."""
self.cls._meta['db_alias'] = self.ori_db_alias
self.cls._collection = self.collection
class switch_collection(object):
""" switch_collection alias context manager.
"""switch_collection alias context manager.
Example ::
class Group(Document):
name = StringField()
Group(name="test").save() # Saves in the default db
Group(name='test').save() # Saves in the default db
with switch_collection(Group, 'group1') as Group:
Group(name="hello testdb!").save() # Saves in group1 collection
Group(name='hello testdb!').save() # Saves in group1 collection
"""
def __init__(self, cls, collection_name):
""" Construct the switch_collection context manager
"""Construct the switch_collection context manager.
:param cls: the class to change the registered db
:param collection_name: the name of the collection to use
@@ -75,7 +73,7 @@ class switch_collection(object):
self.collection_name = collection_name
def __enter__(self):
""" change the _get_collection_name and clear the cached collection """
"""Change the _get_collection_name and clear the cached collection."""
@classmethod
def _get_collection_name(cls):
@@ -86,24 +84,23 @@ class switch_collection(object):
return self.cls
def __exit__(self, t, value, traceback):
""" Reset the collection """
"""Reset the collection."""
self.cls._collection = self.ori_collection
self.cls._get_collection_name = self.ori_get_collection_name
class no_dereference(object):
""" no_dereference context manager.
"""no_dereference context manager.
Turns off all dereferencing in Documents for the duration of the context
manager::
with no_dereference(Group) as Group:
Group.objects.find()
"""
def __init__(self, cls):
""" Construct the no_dereference context manager.
"""Construct the no_dereference context manager.
:param cls: the class to turn dereferencing off on
"""
@@ -119,103 +116,102 @@ class no_dereference(object):
ComplexBaseField))]
def __enter__(self):
""" change the objects default and _auto_dereference values"""
"""Change the objects default and _auto_dereference values."""
for field in self.deref_fields:
self.cls._fields[field]._auto_dereference = False
return self.cls
def __exit__(self, t, value, traceback):
""" Reset the default and _auto_dereference values"""
"""Reset the default and _auto_dereference values."""
for field in self.deref_fields:
self.cls._fields[field]._auto_dereference = True
return self.cls
class no_sub_classes(object):
""" no_sub_classes context manager.
"""no_sub_classes context manager.
Only returns instances of this class and no sub (inherited) classes::
with no_sub_classes(Group) as Group:
Group.objects.find()
"""
def __init__(self, cls):
""" Construct the no_sub_classes context manager.
"""Construct the no_sub_classes context manager.
:param cls: the class to turn querying sub classes on
"""
self.cls = cls
def __enter__(self):
""" change the objects default and _auto_dereference values"""
"""Change the objects default and _auto_dereference values."""
self.cls._all_subclasses = self.cls._subclasses
self.cls._subclasses = (self.cls,)
return self.cls
def __exit__(self, t, value, traceback):
""" Reset the default and _auto_dereference values"""
"""Reset the default and _auto_dereference values."""
self.cls._subclasses = self.cls._all_subclasses
delattr(self.cls, '_all_subclasses')
return self.cls
class query_counter(object):
""" Query_counter context manager to get the number of queries. """
"""Query_counter context manager to get the number of queries."""
def __init__(self):
""" Construct the query_counter. """
"""Construct the query_counter."""
self.counter = 0
self.db = get_db()
def __enter__(self):
""" On every with block we need to drop the profile collection. """
"""On every with block we need to drop the profile collection."""
self.db.set_profiling_level(0)
self.db.system.profile.drop()
self.db.set_profiling_level(2)
return self
def __exit__(self, t, value, traceback):
""" Reset the profiling level. """
"""Reset the profiling level."""
self.db.set_profiling_level(0)
def __eq__(self, value):
""" == Compare querycounter. """
"""== Compare querycounter."""
counter = self._get_count()
return value == counter
def __ne__(self, value):
""" != Compare querycounter. """
"""!= Compare querycounter."""
return not self.__eq__(value)
def __lt__(self, value):
""" < Compare querycounter. """
"""< Compare querycounter."""
return self._get_count() < value
def __le__(self, value):
""" <= Compare querycounter. """
"""<= Compare querycounter."""
return self._get_count() <= value
def __gt__(self, value):
""" > Compare querycounter. """
"""> Compare querycounter."""
return self._get_count() > value
def __ge__(self, value):
""" >= Compare querycounter. """
""">= Compare querycounter."""
return self._get_count() >= value
def __int__(self):
""" int representation. """
"""int representation."""
return self._get_count()
def __repr__(self):
""" repr query_counter as the number of queries. """
"""repr query_counter as the number of queries."""
return u"%s" % self._get_count()
def _get_count(self):
""" Get the number of queries. """
ignore_query = {"ns": {"$ne": "%s.system.indexes" % self.db.name}}
"""Get the number of queries."""
ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}}
count = self.db.system.profile.find(ignore_query).count() - self.counter
self.counter += 1
return count

View File

@@ -1,14 +1,13 @@
from collections import OrderedDict
from bson import DBRef, SON
import six
from .base import (
BaseDict, BaseList, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document
)
from .connection import get_db
from .document import Document, EmbeddedDocument
from .fields import DictField, ListField, MapField, ReferenceField
from .python_support import txt_type
from .queryset import QuerySet
from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document)
from mongoengine.connection import get_db
from mongoengine.document import Document, EmbeddedDocument
from mongoengine.fields import DictField, ListField, MapField, ReferenceField
from mongoengine.queryset import QuerySet
class DeReference(object):
@@ -25,7 +24,7 @@ class DeReference(object):
:class:`~mongoengine.base.ComplexBaseField`
:param get: A boolean determining if being called by __get__
"""
if items is None or isinstance(items, basestring):
if items is None or isinstance(items, six.string_types):
return items
# cheapest way to convert a queryset to a list
@@ -68,11 +67,11 @@ class DeReference(object):
items = _get_items(items)
else:
items = dict([
(k, field.to_python(v))
if not isinstance(v, (DBRef, Document)) else (k, v)
for k, v in items.iteritems()]
)
items = {
k: (v if isinstance(v, (DBRef, Document))
else field.to_python(v))
for k, v in items.iteritems()
}
self.reference_map = self._find_references(items)
self.object_map = self._fetch_objects(doc_type=doc_type)
@@ -90,14 +89,14 @@ class DeReference(object):
return reference_map
# Determine the iterator to use
if not hasattr(items, 'items'):
iterator = enumerate(items)
if isinstance(items, dict):
iterator = items.values()
else:
iterator = items.iteritems()
iterator = items
# Recursively find dbreferences
depth += 1
for k, item in iterator:
for item in iterator:
if isinstance(item, (Document, EmbeddedDocument)):
for field_name, field in item._fields.iteritems():
v = item._data.get(field_name, None)
@@ -151,7 +150,7 @@ class DeReference(object):
references = get_db()[collection].find({'_id': {'$in': refs}})
for ref in references:
if '_cls' in ref:
doc = get_document(ref["_cls"])._from_son(ref)
doc = get_document(ref['_cls'])._from_son(ref)
elif doc_type is None:
doc = get_document(
''.join(x.capitalize()
@@ -203,6 +202,10 @@ class DeReference(object):
as_tuple = isinstance(items, tuple)
iterator = enumerate(items)
data = []
elif isinstance(items, OrderedDict):
is_list = False
iterator = items.iteritems()
data = OrderedDict()
else:
is_list = False
iterator = items.iteritems()
@@ -218,7 +221,7 @@ class DeReference(object):
if k in self.object_map and not is_list:
data[k] = self.object_map[k]
elif isinstance(v, (Document, EmbeddedDocument)):
for field_name, field in v._fields.iteritems():
for field_name in v._fields:
v = data[k]._data.get(field_name, None)
if isinstance(v, DBRef):
data[k]._data[field_name] = self.object_map.get(
@@ -227,7 +230,7 @@ class DeReference(object):
data[k]._data[field_name] = self.object_map.get(
(v['_ref'].collection, v['_ref'].id), v)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
item_name = txt_type("{0}.{1}.{2}").format(name, k, field_name)
item_name = six.text_type('{0}.{1}.{2}').format(name, k, field_name)
data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
item_name = '%s.%s' % (name, k) if name else name

View File

@@ -4,18 +4,12 @@ import warnings
from bson.dbref import DBRef
import pymongo
from pymongo.read_preferences import ReadPreference
import six
from mongoengine import signals
from mongoengine.base import (
ALLOW_INHERITANCE,
BaseDict,
BaseDocument,
BaseList,
DocumentMetaclass,
EmbeddedDocumentList,
TopLevelDocumentMetaclass,
get_document
)
from mongoengine.base import (BaseDict, BaseDocument, BaseList,
DocumentMetaclass, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document)
from mongoengine.common import _import_class
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
from mongoengine.context_managers import switch_collection, switch_db
@@ -31,12 +25,10 @@ __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument',
def includes_cls(fields):
""" Helper function used for ensuring and comparing indexes
"""
"""Helper function used for ensuring and comparing indexes."""
first_field = None
if len(fields):
if isinstance(fields[0], basestring):
if isinstance(fields[0], six.string_types):
first_field = fields[0]
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
first_field = fields[0][0]
@@ -57,9 +49,8 @@ class EmbeddedDocument(BaseDocument):
to create a specialised version of the embedded document that will be
stored in the same collection. To facilitate this behaviour a `_cls`
field is added to documents (hidden though the MongoEngine interface).
To disable this behaviour and remove the dependence on the presence of
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
dictionary.
To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
:attr:`meta` dictionary.
"""
__slots__ = ('_instance', )
@@ -82,6 +73,15 @@ class EmbeddedDocument(BaseDocument):
def __ne__(self, other):
return not self.__eq__(other)
def to_mongo(self, *args, **kwargs):
data = super(EmbeddedDocument, self).to_mongo(*args, **kwargs)
# remove _id from the SON if it's in it and it's None
if '_id' in data and data['_id'] is None:
del data['_id']
return data
def save(self, *args, **kwargs):
self._instance.save(*args, **kwargs)
@@ -106,9 +106,8 @@ class Document(BaseDocument):
create a specialised version of the document that will be stored in the
same collection. To facilitate this behaviour a `_cls`
field is added to documents (hidden though the MongoEngine interface).
To disable this behaviour and remove the dependence on the presence of
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
dictionary.
To enable this behaviourset :attr:`allow_inheritance` to ``True`` in the
:attr:`meta` dictionary.
A :class:`~mongoengine.Document` may use a **Capped Collection** by
specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
@@ -149,26 +148,22 @@ class Document(BaseDocument):
__slots__ = ('__objects',)
def pk():
"""Primary key alias
"""
@property
def pk(self):
"""Get the primary key."""
if 'id_field' not in self._meta:
return None
return getattr(self, self._meta['id_field'])
def fget(self):
if 'id_field' not in self._meta:
return None
return getattr(self, self._meta['id_field'])
def fset(self, value):
return setattr(self, self._meta['id_field'], value)
return property(fget, fset)
pk = pk()
@pk.setter
def pk(self, value):
"""Set the primary key."""
return setattr(self, self._meta['id_field'], value)
@classmethod
def _get_db(cls):
"""Some Model using other db_alias"""
return get_db(cls._meta.get("db_alias", DEFAULT_CONNECTION_NAME))
return get_db(cls._meta.get('db_alias', DEFAULT_CONNECTION_NAME))
@classmethod
def _get_collection(cls):
@@ -211,7 +206,20 @@ class Document(BaseDocument):
cls.ensure_indexes()
return cls._collection
def modify(self, query={}, **update):
def to_mongo(self, *args, **kwargs):
data = super(Document, self).to_mongo(*args, **kwargs)
# If '_id' is None, try and set it from self._data. If that
# doesn't exist either, remote '_id' from the SON completely.
if data['_id'] is None:
if self._data.get('id') is None:
del data['_id']
else:
data['_id'] = self._data['id']
return data
def modify(self, query=None, **update):
"""Perform an atomic update of the document in the database and reload
the document object using updated version.
@@ -225,17 +233,19 @@ class Document(BaseDocument):
database matches the query
:param update: Django-style update keyword arguments
"""
if query is None:
query = {}
if self.pk is None:
raise InvalidDocumentError("The document does not have a primary key.")
raise InvalidDocumentError('The document does not have a primary key.')
id_field = self._meta["id_field"]
id_field = self._meta['id_field']
query = query.copy() if isinstance(query, dict) else query.to_query(self)
if id_field not in query:
query[id_field] = self.pk
elif query[id_field] != self.pk:
raise InvalidQueryError("Invalid document modify query: it must modify only this document.")
raise InvalidQueryError('Invalid document modify query: it must modify only this document.')
updated = self._qs(**query).modify(new=True, **update)
if updated is None:
@@ -303,6 +313,9 @@ class Document(BaseDocument):
.. versionchanged:: 0.10.7
Add signal_kwargs argument
"""
if self._meta.get('abstract'):
raise InvalidDocumentError('Cannot save an abstract document.')
signal_kwargs = signal_kwargs or {}
signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
@@ -310,7 +323,7 @@ class Document(BaseDocument):
self.validate(clean=clean)
if write_concern is None:
write_concern = {"w": 1}
write_concern = {'w': 1}
doc = self.to_mongo()
@@ -319,105 +332,135 @@ class Document(BaseDocument):
signals.pre_save_post_validation.send(self.__class__, document=self,
created=created, **signal_kwargs)
if self._meta.get('auto_create_index', True):
self.ensure_indexes()
try:
collection = self._get_collection()
if self._meta.get('auto_create_index', True):
self.ensure_indexes()
# Save a new document or update an existing one
if created:
if force_insert:
object_id = collection.insert(doc, **write_concern)
else:
object_id = collection.save(doc, **write_concern)
# In PyMongo 3.0, the save() call calls internally the _update() call
# but they forget to return the _id value passed back, therefore getting it back here
# Correct behaviour in 2.X and in 3.0.1+ versions
if not object_id and pymongo.version_tuple == (3, 0):
pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk)
object_id = (
self._qs.filter(pk=pk_as_mongo_obj).first() and
self._qs.filter(pk=pk_as_mongo_obj).first().pk
) # TODO doesn't this make 2 queries?
object_id = self._save_create(doc, force_insert, write_concern)
else:
object_id = doc['_id']
updates, removals = self._delta()
# Need to add shard key to query, or you get an error
if save_condition is not None:
select_dict = transform.query(self.__class__,
**save_condition)
else:
select_dict = {}
select_dict['_id'] = object_id
shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key:
path = self._lookup_field(k.split('.'))
actual_key = [p.db_field for p in path]
val = doc
for ak in actual_key:
val = val[ak]
select_dict['.'.join(actual_key)] = val
def is_new_object(last_error):
if last_error is not None:
updated = last_error.get("updatedExisting")
if updated is not None:
return not updated
return created
update_query = {}
if updates:
update_query["$set"] = updates
if removals:
update_query["$unset"] = removals
if updates or removals:
upsert = save_condition is None
last_error = collection.update(select_dict, update_query,
upsert=upsert, **write_concern)
if not upsert and last_error["n"] == 0:
raise SaveConditionError('Race condition preventing'
' document update detected')
created = is_new_object(last_error)
object_id, created = self._save_update(doc, save_condition,
write_concern)
if cascade is None:
cascade = self._meta.get(
'cascade', False) or cascade_kwargs is not None
cascade = (self._meta.get('cascade', False) or
cascade_kwargs is not None)
if cascade:
kwargs = {
"force_insert": force_insert,
"validate": validate,
"write_concern": write_concern,
"cascade": cascade
'force_insert': force_insert,
'validate': validate,
'write_concern': write_concern,
'cascade': cascade
}
if cascade_kwargs: # Allow granular control over cascades
kwargs.update(cascade_kwargs)
kwargs['_refs'] = _refs
self.cascade_save(**kwargs)
except pymongo.errors.DuplicateKeyError, err:
except pymongo.errors.DuplicateKeyError as err:
message = u'Tried to save duplicate unique keys (%s)'
raise NotUniqueError(message % unicode(err))
except pymongo.errors.OperationFailure, err:
raise NotUniqueError(message % six.text_type(err))
except pymongo.errors.OperationFailure as err:
message = 'Could not save document (%s)'
if re.match('^E1100[01] duplicate key', unicode(err)):
if re.match('^E1100[01] duplicate key', six.text_type(err)):
# E11000 - duplicate key error index
# E11001 - duplicate key on update
message = u'Tried to save duplicate unique keys (%s)'
raise NotUniqueError(message % unicode(err))
raise OperationError(message % unicode(err))
raise NotUniqueError(message % six.text_type(err))
raise OperationError(message % six.text_type(err))
# Make sure we store the PK on this document now that it's saved
id_field = self._meta['id_field']
if created or id_field not in self._meta.get('shard_key', []):
self[id_field] = self._fields[id_field].to_python(object_id)
signals.post_save.send(self.__class__, document=self,
created=created, **signal_kwargs)
self._clear_changed_fields()
self._created = False
return self
def cascade_save(self, *args, **kwargs):
"""Recursively saves any references /
generic references on the document"""
_refs = kwargs.get('_refs', []) or []
def _save_create(self, doc, force_insert, write_concern):
"""Save a new document.
Helper method, should only be used inside save().
"""
collection = self._get_collection()
if force_insert:
return collection.insert(doc, **write_concern)
object_id = collection.save(doc, **write_concern)
# In PyMongo 3.0, the save() call calls internally the _update() call
# but they forget to return the _id value passed back, therefore getting it back here
# Correct behaviour in 2.X and in 3.0.1+ versions
if not object_id and pymongo.version_tuple == (3, 0):
pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk)
object_id = (
self._qs.filter(pk=pk_as_mongo_obj).first() and
self._qs.filter(pk=pk_as_mongo_obj).first().pk
) # TODO doesn't this make 2 queries?
return object_id
def _save_update(self, doc, save_condition, write_concern):
"""Update an existing document.
Helper method, should only be used inside save().
"""
collection = self._get_collection()
object_id = doc['_id']
created = False
select_dict = {}
if save_condition is not None:
select_dict = transform.query(self.__class__, **save_condition)
select_dict['_id'] = object_id
# Need to add shard key to query, or you get an error
shard_key = self._meta.get('shard_key', tuple())
for k in shard_key:
path = self._lookup_field(k.split('.'))
actual_key = [p.db_field for p in path]
val = doc
for ak in actual_key:
val = val[ak]
select_dict['.'.join(actual_key)] = val
updates, removals = self._delta()
update_query = {}
if updates:
update_query['$set'] = updates
if removals:
update_query['$unset'] = removals
if updates or removals:
upsert = save_condition is None
last_error = collection.update(select_dict, update_query,
upsert=upsert, **write_concern)
if not upsert and last_error['n'] == 0:
raise SaveConditionError('Race condition preventing'
' document update detected')
if last_error is not None:
updated_existing = last_error.get('updatedExisting')
if updated_existing is False:
created = True
# !!! This is bad, means we accidentally created a new,
# potentially corrupted document. See
# https://github.com/MongoEngine/mongoengine/issues/564
return object_id, created
def cascade_save(self, **kwargs):
"""Recursively save any references and generic references on the
document.
"""
_refs = kwargs.get('_refs') or []
ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
@@ -443,16 +486,17 @@ class Document(BaseDocument):
@property
def _qs(self):
"""
Returns the queryset to use for updating / reloading / deletions
"""
"""Return the queryset to use for updating / reloading / deletions."""
if not hasattr(self, '__objects'):
self.__objects = QuerySet(self, self._get_collection())
return self.__objects
@property
def _object_key(self):
"""Dict to identify object in collection
"""Get the query dict that can be used to fetch this object from
the database. Most of the time it's a simple PK lookup, but in
case of a sharded collection with a compound shard key, it can
contain a more complex query.
"""
select_dict = {'pk': self.pk}
shard_key = self.__class__._meta.get('shard_key', tuple())
@@ -475,8 +519,8 @@ class Document(BaseDocument):
if self.pk is None:
if kwargs.get('upsert', False):
query = self.to_mongo()
if "_cls" in query:
del query["_cls"]
if '_cls' in query:
del query['_cls']
return self._qs.filter(**query).update_one(**kwargs)
else:
raise OperationError(
@@ -513,7 +557,7 @@ class Document(BaseDocument):
try:
self._qs.filter(
**self._object_key).delete(write_concern=write_concern, _from_doc_delete=True)
except pymongo.errors.OperationFailure, err:
except pymongo.errors.OperationFailure as err:
message = u'Could not delete document (%s)' % err.message
raise OperationError(message)
signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
@@ -601,11 +645,12 @@ class Document(BaseDocument):
if fields and isinstance(fields[0], int):
max_depth = fields[0]
fields = fields[1:]
elif "max_depth" in kwargs:
max_depth = kwargs["max_depth"]
elif 'max_depth' in kwargs:
max_depth = kwargs['max_depth']
if self.pk is None:
raise self.DoesNotExist("Document does not exist")
raise self.DoesNotExist('Document does not exist')
obj = self._qs.read_preference(ReadPreference.PRIMARY).filter(
**self._object_key).only(*fields).limit(
1).select_related(max_depth=max_depth)
@@ -613,7 +658,7 @@ class Document(BaseDocument):
if obj:
obj = obj[0]
else:
raise self.DoesNotExist("Document does not exist")
raise self.DoesNotExist('Document does not exist')
for field in obj._data:
if not fields or field in fields:
@@ -656,7 +701,7 @@ class Document(BaseDocument):
"""Returns an instance of :class:`~bson.dbref.DBRef` useful in
`__raw__` queries."""
if self.pk is None:
msg = "Only saved documents can have a valid dbref"
msg = 'Only saved documents can have a valid dbref'
raise OperationError(msg)
return DBRef(self.__class__._get_collection_name(), self.pk)
@@ -711,7 +756,7 @@ class Document(BaseDocument):
fields = index_spec.pop('fields')
drop_dups = kwargs.get('drop_dups', False)
if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning)
elif not IS_PYMONGO_3:
index_spec['drop_dups'] = drop_dups
@@ -737,7 +782,7 @@ class Document(BaseDocument):
will be removed if PyMongo3+ is used
"""
if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning)
elif not IS_PYMONGO_3:
kwargs.update({'drop_dups': drop_dups})
@@ -757,7 +802,7 @@ class Document(BaseDocument):
index_opts = cls._meta.get('index_opts') or {}
index_cls = cls._meta.get('index_cls', True)
if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning)
collection = cls._get_collection()
@@ -795,8 +840,7 @@ class Document(BaseDocument):
# If _cls is being used (for polymorphism), it needs an index,
# only if another index doesn't begin with _cls
if (index_cls and not cls_indexed and
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
if index_cls and not cls_indexed and cls._meta.get('allow_inheritance'):
# we shouldn't pass 'cls' to the collection.ensureIndex options
# because of https://jira.mongodb.org/browse/SERVER-769
@@ -815,7 +859,6 @@ class Document(BaseDocument):
""" Lists all of the indexes that should be created for given
collection. It includes all the indexes from super- and sub-classes.
"""
if cls._meta.get('abstract'):
return []
@@ -866,16 +909,15 @@ class Document(BaseDocument):
# finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
if [(u'_id', 1)] not in indexes:
indexes.append([(u'_id', 1)])
if (cls._meta.get('index_cls', True) and
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
if cls._meta.get('index_cls', True) and cls._meta.get('allow_inheritance'):
indexes.append([(u'_cls', 1)])
return indexes
@classmethod
def compare_indexes(cls):
""" Compares the indexes defined in MongoEngine with the ones existing
in the database. Returns any missing/extra indexes.
""" Compares the indexes defined in MongoEngine with the ones
existing in the database. Returns any missing/extra indexes.
"""
required = cls.list_indexes()
@@ -919,8 +961,9 @@ class DynamicDocument(Document):
_dynamic = True
def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset
it"""
"""Delete the attribute by setting to None and allowing _delta
to unset it.
"""
field_name = args[0]
if field_name in self._dynamic_fields:
setattr(self, field_name, None)
@@ -942,8 +985,9 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
_dynamic = True
def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset
it"""
"""Delete the attribute by setting to None and allowing _delta
to unset it.
"""
field_name = args[0]
if field_name in self._fields:
default = self._fields[field_name].default
@@ -985,10 +1029,10 @@ class MapReduceDocument(object):
try:
self.key = id_field_type(self.key)
except Exception:
raise Exception("Could not cast key as %s" %
raise Exception('Could not cast key as %s' %
id_field_type.__name__)
if not hasattr(self, "_key_object"):
if not hasattr(self, '_key_object'):
self._key_object = self._document.objects.with_id(self.key)
return self._key_object
return self._key_object

View File

@@ -1,7 +1,6 @@
from collections import defaultdict
from mongoengine.python_support import txt_type
import six
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
@@ -51,8 +50,8 @@ class FieldDoesNotExist(Exception):
or an :class:`~mongoengine.EmbeddedDocument`.
To avoid this behavior on data loading,
you should the :attr:`strict` to ``False``
in the :attr:`meta` dictionnary.
you should set the :attr:`strict` to ``False``
in the :attr:`meta` dictionary.
"""
@@ -71,13 +70,13 @@ class ValidationError(AssertionError):
field_name = None
_message = None
def __init__(self, message="", **kwargs):
def __init__(self, message='', **kwargs):
self.errors = kwargs.get('errors', {})
self.field_name = kwargs.get('field_name')
self.message = message
def __str__(self):
return txt_type(self.message)
return six.text_type(self.message)
def __repr__(self):
return '%s(%s,)' % (self.__class__.__name__, self.message)
@@ -111,17 +110,20 @@ class ValidationError(AssertionError):
errors_dict = {}
if not source:
return errors_dict
if isinstance(source, dict):
for field_name, error in source.iteritems():
errors_dict[field_name] = build_dict(error)
elif isinstance(source, ValidationError) and source.errors:
return build_dict(source.errors)
else:
return unicode(source)
return six.text_type(source)
return errors_dict
if not self.errors:
return {}
return build_dict(self.errors)
def _format_errors(self):
@@ -134,10 +136,10 @@ class ValidationError(AssertionError):
value = ' '.join(
[generate_key(v, k) for k, v in value.iteritems()])
results = "%s.%s" % (prefix, value) if prefix else value
results = '%s.%s' % (prefix, value) if prefix else value
return results
error_dict = defaultdict(list)
for k, v in self.to_dict().iteritems():
error_dict[generate_key(v)].append(k)
return ' '.join(["%s: %s" % (k, v) for k, v in error_dict.iteritems()])
return ' '.join(['%s: %s' % (k, v) for k, v in error_dict.iteritems()])

View File

@@ -3,9 +3,9 @@ import decimal
import itertools
import re
import time
import urllib2
import uuid
import warnings
from collections import Mapping
from operator import itemgetter
from bson import Binary, DBRef, ObjectId, SON
@@ -25,13 +25,13 @@ try:
except ImportError:
Int64 = long
from .base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField,
ObjectIdField, get_document)
from .connection import DEFAULT_CONNECTION_NAME, get_db
from .document import Document, EmbeddedDocument
from .errors import DoesNotExist, ValidationError
from .python_support import PY3, StringIO, bin_type, str_types, txt_type
from .queryset import DO_NOTHING, QuerySet
from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField,
GeoJsonBaseField, ObjectIdField, get_document)
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
from mongoengine.document import Document, EmbeddedDocument
from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError
from mongoengine.python_support import StringIO
from mongoengine.queryset import DO_NOTHING, QuerySet
try:
from PIL import Image, ImageOps
@@ -39,7 +39,7 @@ except ImportError:
Image = None
ImageOps = None
__all__ = [
__all__ = (
'StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
@@ -50,14 +50,14 @@ __all__ = [
'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField',
'MultiPolygonField', 'GeoJsonBaseField']
'MultiPolygonField', 'GeoJsonBaseField'
)
RECURSIVE_REFERENCE_CONSTANT = 'self'
class StringField(BaseField):
"""A unicode string field.
"""
"""A unicode string field."""
def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
self.regex = re.compile(regex) if regex else None
@@ -66,7 +66,7 @@ class StringField(BaseField):
super(StringField, self).__init__(**kwargs)
def to_python(self, value):
if isinstance(value, unicode):
if isinstance(value, six.text_type):
return value
try:
value = value.decode('utf-8')
@@ -75,7 +75,7 @@ class StringField(BaseField):
return value
def validate(self, value):
if not isinstance(value, basestring):
if not isinstance(value, six.string_types):
self.error('StringField only accepts string values')
if self.max_length is not None and len(value) > self.max_length:
@@ -91,7 +91,7 @@ class StringField(BaseField):
return None
def prepare_query_value(self, op, value):
if not isinstance(op, basestring):
if not isinstance(op, six.string_types):
return value
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
@@ -140,25 +140,14 @@ class URLField(StringField):
# Check first if the scheme is valid
scheme = value.split('://')[0].lower()
if scheme not in self.schemes:
self.error('Invalid scheme {} in URL: {}'.format(scheme, value))
self.error(u'Invalid scheme {} in URL: {}'.format(scheme, value))
return
# Then check full URL
if not self.url_regex.match(value):
self.error('Invalid URL: {}'.format(value))
self.error(u'Invalid URL: {}'.format(value))
return
if self.verify_exists:
warnings.warn(
"The URLField verify_exists argument has intractable security "
"and performance issues. Accordingly, it has been deprecated.",
DeprecationWarning)
try:
request = urllib2.Request(value)
urllib2.urlopen(request)
except Exception, e:
self.error('This URL appears to be a broken link: %s' % e)
class EmailField(StringField):
"""A field that validates input as an email address.
@@ -182,8 +171,7 @@ class EmailField(StringField):
class IntField(BaseField):
"""An 32-bit integer field.
"""
"""32-bit integer field."""
def __init__(self, min_value=None, max_value=None, **kwargs):
self.min_value, self.max_value = min_value, max_value
@@ -216,8 +204,7 @@ class IntField(BaseField):
class LongField(BaseField):
"""An 64-bit integer field.
"""
"""64-bit integer field."""
def __init__(self, min_value=None, max_value=None, **kwargs):
self.min_value, self.max_value = min_value, max_value
@@ -253,8 +240,7 @@ class LongField(BaseField):
class FloatField(BaseField):
"""An floating point number field.
"""
"""Floating point number field."""
def __init__(self, min_value=None, max_value=None, **kwargs):
self.min_value, self.max_value = min_value, max_value
@@ -291,7 +277,7 @@ class FloatField(BaseField):
class DecimalField(BaseField):
"""A fixed-point decimal number field.
"""Fixed-point decimal number field.
.. versionchanged:: 0.8
.. versionadded:: 0.3
@@ -332,25 +318,25 @@ class DecimalField(BaseField):
# Convert to string for python 2.6 before casting to Decimal
try:
value = decimal.Decimal("%s" % value)
value = decimal.Decimal('%s' % value)
except decimal.InvalidOperation:
return value
return value.quantize(decimal.Decimal(".%s" % ("0" * self.precision)), rounding=self.rounding)
return value.quantize(decimal.Decimal('.%s' % ('0' * self.precision)), rounding=self.rounding)
def to_mongo(self, value):
if value is None:
return value
if self.force_string:
return unicode(value)
return six.text_type(self.to_python(value))
return float(self.to_python(value))
def validate(self, value):
if not isinstance(value, decimal.Decimal):
if not isinstance(value, basestring):
value = unicode(value)
if not isinstance(value, six.string_types):
value = six.text_type(value)
try:
value = decimal.Decimal(value)
except Exception, exc:
except Exception as exc:
self.error('Could not convert value to decimal: %s' % exc)
if self.min_value is not None and value < self.min_value:
@@ -364,7 +350,7 @@ class DecimalField(BaseField):
class BooleanField(BaseField):
"""A boolean field type.
"""Boolean field type.
.. versionadded:: 0.1.2
"""
@@ -382,7 +368,7 @@ class BooleanField(BaseField):
class DateTimeField(BaseField):
"""A datetime field.
"""Datetime field.
Uses the python-dateutil library if available alternatively use time.strptime
to parse the dates. Note: python-dateutil's parser is fully featured and when
@@ -410,7 +396,7 @@ class DateTimeField(BaseField):
if callable(value):
return value()
if not isinstance(value, basestring):
if not isinstance(value, six.string_types):
return None
# Attempt to parse a datetime:
@@ -537,16 +523,19 @@ class EmbeddedDocumentField(BaseField):
"""
def __init__(self, document_type, **kwargs):
if not isinstance(document_type, basestring):
if not issubclass(document_type, EmbeddedDocument):
self.error('Invalid embedded document class provided to an '
'EmbeddedDocumentField')
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, EmbeddedDocument)
):
self.error('Invalid embedded document class provided to an '
'EmbeddedDocumentField')
self.document_type_obj = document_type
super(EmbeddedDocumentField, self).__init__(**kwargs)
@property
def document_type(self):
if isinstance(self.document_type_obj, basestring):
if isinstance(self.document_type_obj, six.string_types):
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
self.document_type_obj = self.owner_document
else:
@@ -577,8 +566,12 @@ class EmbeddedDocumentField(BaseField):
return self.document_type._fields.get(member_name)
def prepare_query_value(self, op, value):
if not isinstance(value, self.document_type):
value = self.document_type._from_son(value)
if value is not None and not isinstance(value, self.document_type):
try:
value = self.document_type._from_son(value)
except ValueError:
raise InvalidQueryError("Querying the embedded document '%s' failed, due to an invalid query value" %
(self.document_type._class_name,))
super(EmbeddedDocumentField, self).prepare_query_value(op, value)
return self.to_mongo(value)
@@ -627,11 +620,19 @@ class DynamicField(BaseField):
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
def __init__(self, container_class=dict, *args, **kwargs):
self._container_cls = container_class
if not issubclass(self._container_cls, Mapping):
self.error('The class that is specified in `container_class` parameter '
'must be a subclass of `dict`.')
super(DynamicField, self).__init__(*args, **kwargs)
def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB compatible type.
"""
if isinstance(value, basestring):
if isinstance(value, six.string_types):
return value
if hasattr(value, 'to_mongo'):
@@ -639,7 +640,7 @@ class DynamicField(BaseField):
val = value.to_mongo(use_db_field, fields)
# If we its a document thats not inherited add _cls
if isinstance(value, Document):
val = {"_ref": value.to_dbref(), "_cls": cls.__name__}
val = {'_ref': value.to_dbref(), '_cls': cls.__name__}
if isinstance(value, EmbeddedDocument):
val['_cls'] = cls.__name__
return val
@@ -650,9 +651,9 @@ class DynamicField(BaseField):
is_list = False
if not hasattr(value, 'items'):
is_list = True
value = dict([(k, v) for k, v in enumerate(value)])
value = {k: v for k, v in enumerate(value)}
data = {}
data = self._container_cls()
for k, v in value.iteritems():
data[k] = self.to_mongo(v, use_db_field, fields)
@@ -674,12 +675,12 @@ class DynamicField(BaseField):
return member_name
def prepare_query_value(self, op, value):
if isinstance(value, basestring):
if isinstance(value, six.string_types):
return StringField().prepare_query_value(op, value)
return super(DynamicField, self).prepare_query_value(op, self.to_mongo(value))
def validate(self, value, clean=True):
if hasattr(value, "validate"):
if hasattr(value, 'validate'):
value.validate(clean=clean)
@@ -699,21 +700,27 @@ class ListField(ComplexBaseField):
super(ListField, self).__init__(**kwargs)
def validate(self, value):
"""Make sure that a list of valid fields is being used.
"""
"""Make sure that a list of valid fields is being used."""
if (not isinstance(value, (list, tuple, QuerySet)) or
isinstance(value, basestring)):
isinstance(value, six.string_types)):
self.error('Only lists and tuples may be used in a list field')
super(ListField, self).validate(value)
def prepare_query_value(self, op, value):
if self.field:
if op in ('set', 'unset', None) and (
not isinstance(value, basestring) and
not isinstance(value, BaseDocument) and
hasattr(value, '__iter__')):
# If the value is iterable and it's not a string nor a
# BaseDocument, call prepare_query_value for each of its items.
if (
op in ('set', 'unset', None) and
hasattr(value, '__iter__') and
not isinstance(value, six.string_types) and
not isinstance(value, BaseDocument)
):
return [self.field.prepare_query_value(op, v) for v in value]
return self.field.prepare_query_value(op, value)
return super(ListField, self).prepare_query_value(op, value)
@@ -726,7 +733,6 @@ class EmbeddedDocumentListField(ListField):
:class:`~mongoengine.EmbeddedDocument`.
.. versionadded:: 0.9
"""
def __init__(self, document_type, **kwargs):
@@ -775,17 +781,17 @@ class SortedListField(ListField):
def key_not_string(d):
""" Helper function to recursively determine if any key in a dictionary is
not a string.
"""Helper function to recursively determine if any key in a
dictionary is not a string.
"""
for k, v in d.items():
if not isinstance(k, basestring) or (isinstance(v, dict) and key_not_string(v)):
if not isinstance(k, six.string_types) or (isinstance(v, dict) and key_not_string(v)):
return True
def key_has_dot_or_dollar(d):
""" Helper function to recursively determine if any key in a dictionary
contains a dot or a dollar sign.
"""Helper function to recursively determine if any key in a
dictionary contains a dot or a dollar sign.
"""
for k, v in d.items():
if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)):
@@ -813,14 +819,13 @@ class DictField(ComplexBaseField):
super(DictField, self).__init__(*args, **kwargs)
def validate(self, value):
"""Make sure that a list of valid fields is being used.
"""
"""Make sure that a list of valid fields is being used."""
if not isinstance(value, dict):
self.error('Only dictionaries may be used in a DictField')
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)
if key_has_dot_or_dollar(value):
self.error('Invalid dictionary key name - keys may not contain "."'
@@ -835,14 +840,15 @@ class DictField(ComplexBaseField):
'istartswith', 'endswith', 'iendswith',
'exact', 'iexact']
if op in match_operators and isinstance(value, basestring):
if op in match_operators and isinstance(value, six.string_types):
return StringField().prepare_query_value(op, value)
if hasattr(self.field, 'field'):
if op in ('set', 'unset') and isinstance(value, dict):
return dict(
(k, self.field.prepare_query_value(op, v))
for k, v in value.items())
return {
k: self.field.prepare_query_value(op, v)
for k, v in value.items()
}
return self.field.prepare_query_value(op, value)
return super(DictField, self).prepare_query_value(op, value)
@@ -891,10 +897,6 @@ class ReferenceField(BaseField):
Foo.register_delete_rule(Bar, 'foo', NULLIFY)
.. note ::
`reverse_delete_rule` does not trigger pre / post delete signals to be
triggered.
.. versionchanged:: 0.5 added `reverse_delete_rule`
"""
@@ -911,10 +913,12 @@ class ReferenceField(BaseField):
A reference to an abstract document type is always stored as a
:class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
"""
if not isinstance(document_type, basestring):
if not issubclass(document_type, (Document, basestring)):
self.error('Argument to ReferenceField constructor must be a '
'document class or a string')
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, Document)
):
self.error('Argument to ReferenceField constructor must be a '
'document class or a string')
self.dbref = dbref
self.document_type_obj = document_type
@@ -923,7 +927,7 @@ class ReferenceField(BaseField):
@property
def document_type(self):
if isinstance(self.document_type_obj, basestring):
if isinstance(self.document_type_obj, six.string_types):
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
self.document_type_obj = self.owner_document
else:
@@ -931,8 +935,7 @@ class ReferenceField(BaseField):
return self.document_type_obj
def __get__(self, instance, owner):
"""Descriptor to allow lazy dereferencing.
"""
"""Descriptor to allow lazy dereferencing."""
if instance is None:
# Document class being used rather than a document object
return self
@@ -989,8 +992,7 @@ class ReferenceField(BaseField):
return id_
def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type.
"""
"""Convert a MongoDB-compatible type to a Python type."""
if (not self.dbref and
not isinstance(value, (DBRef, Document, EmbeddedDocument))):
collection = self.document_type._get_collection_name()
@@ -1005,8 +1007,8 @@ class ReferenceField(BaseField):
def validate(self, value):
if not isinstance(value, (self.document_type, DBRef)):
self.error("A ReferenceField only accepts DBRef or documents")
if not isinstance(value, (self.document_type, DBRef, ObjectId)):
self.error('A ReferenceField only accepts DBRef, ObjectId or documents')
if isinstance(value, Document) and value.id is None:
self.error('You can only reference documents once they have been '
@@ -1030,14 +1032,19 @@ class CachedReferenceField(BaseField):
.. versionadded:: 0.9
"""
def __init__(self, document_type, fields=[], auto_sync=True, **kwargs):
def __init__(self, document_type, fields=None, auto_sync=True, **kwargs):
"""Initialises the Cached Reference Field.
:param fields: A list of fields to be cached in document
:param auto_sync: if True documents are auto updated.
"""
if not isinstance(document_type, basestring) and \
not issubclass(document_type, (Document, basestring)):
if fields is None:
fields = []
if (
not isinstance(document_type, six.string_types) and
not issubclass(document_type, Document)
):
self.error('Argument to CachedReferenceField constructor must be a'
' document class or a string')
@@ -1053,18 +1060,20 @@ class CachedReferenceField(BaseField):
sender=self.document_type)
def on_document_pre_save(self, sender, document, created, **kwargs):
if not created:
update_kwargs = dict(
('set__%s__%s' % (self.name, k), v)
for k, v in document._delta()[0].items()
if k in self.fields)
if created:
return None
if update_kwargs:
filter_kwargs = {}
filter_kwargs[self.name] = document
update_kwargs = {
'set__%s__%s' % (self.name, key): val
for key, val in document._delta()[0].items()
if key in self.fields
}
if update_kwargs:
filter_kwargs = {}
filter_kwargs[self.name] = document
self.owner_document.objects(
**filter_kwargs).update(**update_kwargs)
self.owner_document.objects(
**filter_kwargs).update(**update_kwargs)
def to_python(self, value):
if isinstance(value, dict):
@@ -1077,7 +1086,7 @@ class CachedReferenceField(BaseField):
@property
def document_type(self):
if isinstance(self.document_type_obj, basestring):
if isinstance(self.document_type_obj, six.string_types):
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
self.document_type_obj = self.owner_document
else:
@@ -1117,7 +1126,7 @@ class CachedReferenceField(BaseField):
# TODO: should raise here or will fail next statement
value = SON((
("_id", id_field.to_mongo(id_)),
('_id', id_field.to_mongo(id_)),
))
if fields:
@@ -1143,7 +1152,7 @@ class CachedReferenceField(BaseField):
def validate(self, value):
if not isinstance(value, self.document_type):
self.error("A CachedReferenceField only accepts documents")
self.error('A CachedReferenceField only accepts documents')
if isinstance(value, Document) and value.id is None:
self.error('You can only reference documents once they have been '
@@ -1191,13 +1200,13 @@ class GenericReferenceField(BaseField):
# Keep the choices as a list of allowed Document class names
if choices:
for choice in choices:
if isinstance(choice, basestring):
if isinstance(choice, six.string_types):
self.choices.append(choice)
elif isinstance(choice, type) and issubclass(choice, Document):
self.choices.append(choice._class_name)
else:
self.error('Invalid choices provided: must be a list of'
'Document subclasses and/or basestrings')
'Document subclasses and/or six.string_typess')
def _validate_choices(self, value):
if isinstance(value, dict):
@@ -1249,7 +1258,7 @@ class GenericReferenceField(BaseField):
if document is None:
return None
if isinstance(document, (dict, SON)):
if isinstance(document, (dict, SON, ObjectId, DBRef)):
return document
id_field_name = document.__class__._meta['id_field']
@@ -1280,8 +1289,7 @@ class GenericReferenceField(BaseField):
class BinaryField(BaseField):
"""A binary data field.
"""
"""A binary data field."""
def __init__(self, max_bytes=None, **kwargs):
self.max_bytes = max_bytes
@@ -1289,18 +1297,18 @@ class BinaryField(BaseField):
def __set__(self, instance, value):
"""Handle bytearrays in python 3.1"""
if PY3 and isinstance(value, bytearray):
value = bin_type(value)
if six.PY3 and isinstance(value, bytearray):
value = six.binary_type(value)
return super(BinaryField, self).__set__(instance, value)
def to_mongo(self, value):
return Binary(value)
def validate(self, value):
if not isinstance(value, (bin_type, txt_type, Binary)):
self.error("BinaryField only accepts instances of "
"(%s, %s, Binary)" % (
bin_type.__name__, txt_type.__name__))
if not isinstance(value, (six.binary_type, six.text_type, Binary)):
self.error('BinaryField only accepts instances of '
'(%s, %s, Binary)' % (
six.binary_type.__name__, six.text_type.__name__))
if self.max_bytes is not None and len(value) > self.max_bytes:
self.error('Binary value is too long')
@@ -1384,11 +1392,13 @@ class GridFSProxy(object):
get_db(self.db_alias), self.collection_name)
return self._fs
def get(self, id=None):
if id:
self.grid_id = id
def get(self, grid_id=None):
if grid_id:
self.grid_id = grid_id
if self.grid_id is None:
return None
try:
if self.gridout is None:
self.gridout = self.fs.get(self.grid_id)
@@ -1432,7 +1442,7 @@ class GridFSProxy(object):
try:
return gridout.read(size)
except Exception:
return ""
return ''
def delete(self):
# Delete file from GridFS, FileField still remains
@@ -1464,9 +1474,8 @@ class FileField(BaseField):
"""
proxy_class = GridFSProxy
def __init__(self,
db_alias=DEFAULT_CONNECTION_NAME,
collection_name="fs", **kwargs):
def __init__(self, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs',
**kwargs):
super(FileField, self).__init__(**kwargs)
self.collection_name = collection_name
self.db_alias = db_alias
@@ -1488,8 +1497,10 @@ class FileField(BaseField):
def __set__(self, instance, value):
key = self.name
if ((hasattr(value, 'read') and not
isinstance(value, GridFSProxy)) or isinstance(value, str_types)):
if (
(hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or
isinstance(value, (six.binary_type, six.string_types))
):
# using "FileField() = file/string" notation
grid_file = instance._data.get(self.name)
# If a file already exists, delete it
@@ -1558,7 +1569,7 @@ class ImageGridFsProxy(GridFSProxy):
try:
img = Image.open(file_obj)
img_format = img.format
except Exception, e:
except Exception as e:
raise ValidationError('Invalid image: %s' % e)
# Progressive JPEG
@@ -1667,10 +1678,10 @@ class ImageGridFsProxy(GridFSProxy):
return self.fs.get(out.thumbnail_id)
def write(self, *args, **kwargs):
raise RuntimeError("Please use \"put\" method instead")
raise RuntimeError('Please use "put" method instead')
def writelines(self, *args, **kwargs):
raise RuntimeError("Please use \"put\" method instead")
raise RuntimeError('Please use "put" method instead')
class ImproperlyConfigured(Exception):
@@ -1695,14 +1706,17 @@ class ImageField(FileField):
def __init__(self, size=None, thumbnail_size=None,
collection_name='images', **kwargs):
if not Image:
raise ImproperlyConfigured("PIL library was not found")
raise ImproperlyConfigured('PIL library was not found')
params_size = ('width', 'height', 'force')
extra_args = dict(size=size, thumbnail_size=thumbnail_size)
extra_args = {
'size': size,
'thumbnail_size': thumbnail_size
}
for att_name, att in extra_args.items():
value = None
if isinstance(att, (tuple, list)):
if PY3:
if six.PY3:
value = dict(itertools.zip_longest(params_size, att,
fillvalue=None))
else:
@@ -1763,10 +1777,10 @@ class SequenceField(BaseField):
Generate and Increment the counter
"""
sequence_name = self.get_sequence_name()
sequence_id = "%s.%s" % (sequence_name, self.name)
sequence_id = '%s.%s' % (sequence_name, self.name)
collection = get_db(alias=self.db_alias)[self.collection_name]
counter = collection.find_and_modify(query={"_id": sequence_id},
update={"$inc": {"next": 1}},
counter = collection.find_and_modify(query={'_id': sequence_id},
update={'$inc': {'next': 1}},
new=True,
upsert=True)
return self.value_decorator(counter['next'])
@@ -1789,9 +1803,9 @@ class SequenceField(BaseField):
as it is only fixed on set.
"""
sequence_name = self.get_sequence_name()
sequence_id = "%s.%s" % (sequence_name, self.name)
sequence_id = '%s.%s' % (sequence_name, self.name)
collection = get_db(alias=self.db_alias)[self.collection_name]
data = collection.find_one({"_id": sequence_id})
data = collection.find_one({'_id': sequence_id})
if data:
return self.value_decorator(data['next'] + 1)
@@ -1861,8 +1875,8 @@ class UUIDField(BaseField):
if not self._binary:
original_value = value
try:
if not isinstance(value, basestring):
value = unicode(value)
if not isinstance(value, six.string_types):
value = six.text_type(value)
return uuid.UUID(value)
except Exception:
return original_value
@@ -1870,8 +1884,8 @@ class UUIDField(BaseField):
def to_mongo(self, value):
if not self._binary:
return unicode(value)
elif isinstance(value, basestring):
return six.text_type(value)
elif isinstance(value, six.string_types):
return uuid.UUID(value)
return value
@@ -1882,11 +1896,11 @@ class UUIDField(BaseField):
def validate(self, value):
if not isinstance(value, uuid.UUID):
if not isinstance(value, basestring):
if not isinstance(value, six.string_types):
value = str(value)
try:
uuid.UUID(value)
except Exception, exc:
except Exception as exc:
self.error('Could not convert to UUID: %s' % exc)
@@ -1904,19 +1918,18 @@ class GeoPointField(BaseField):
_geo_index = pymongo.GEO2D
def validate(self, value):
"""Make sure that a geo-value is of type (x, y)
"""
"""Make sure that a geo-value is of type (x, y)"""
if not isinstance(value, (list, tuple)):
self.error('GeoPointField can only accept tuples or lists '
'of (x, y)')
if not len(value) == 2:
self.error("Value (%s) must be a two-dimensional point" %
self.error('Value (%s) must be a two-dimensional point' %
repr(value))
elif (not isinstance(value[0], (float, int)) or
not isinstance(value[1], (float, int))):
self.error(
"Both values (%s) in point must be float or int" % repr(value))
'Both values (%s) in point must be float or int' % repr(value))
class PointField(GeoJsonBaseField):
@@ -1926,8 +1939,8 @@ class PointField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "Point" ,
"coordinates" : [x, y]}
{'type' : 'Point' ,
'coordinates' : [x, y]}
You can either pass a dict with the full information or a list
to set the value.
@@ -1936,7 +1949,7 @@ class PointField(GeoJsonBaseField):
.. versionadded:: 0.8
"""
_type = "Point"
_type = 'Point'
class LineStringField(GeoJsonBaseField):
@@ -1946,8 +1959,8 @@ class LineStringField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "LineString" ,
"coordinates" : [[x1, y1], [x1, y1] ... [xn, yn]]}
{'type' : 'LineString' ,
'coordinates' : [[x1, y1], [x1, y1] ... [xn, yn]]}
You can either pass a dict with the full information or a list of points.
@@ -1955,7 +1968,7 @@ class LineStringField(GeoJsonBaseField):
.. versionadded:: 0.8
"""
_type = "LineString"
_type = 'LineString'
class PolygonField(GeoJsonBaseField):
@@ -1965,9 +1978,9 @@ class PolygonField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "Polygon" ,
"coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]}
{'type' : 'Polygon' ,
'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]}
You can either pass a dict with the full information or a list
of LineStrings. The first LineString being the outside and the rest being
@@ -1977,7 +1990,7 @@ class PolygonField(GeoJsonBaseField):
.. versionadded:: 0.8
"""
_type = "Polygon"
_type = 'Polygon'
class MultiPointField(GeoJsonBaseField):
@@ -1987,8 +2000,8 @@ class MultiPointField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "MultiPoint" ,
"coordinates" : [[x1, y1], [x2, y2]]}
{'type' : 'MultiPoint' ,
'coordinates' : [[x1, y1], [x2, y2]]}
You can either pass a dict with the full information or a list
to set the value.
@@ -1997,7 +2010,7 @@ class MultiPointField(GeoJsonBaseField):
.. versionadded:: 0.9
"""
_type = "MultiPoint"
_type = 'MultiPoint'
class MultiLineStringField(GeoJsonBaseField):
@@ -2007,9 +2020,9 @@ class MultiLineStringField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "MultiLineString" ,
"coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]]}
{'type' : 'MultiLineString' ,
'coordinates' : [[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]]}
You can either pass a dict with the full information or a list of points.
@@ -2017,7 +2030,7 @@ class MultiLineStringField(GeoJsonBaseField):
.. versionadded:: 0.9
"""
_type = "MultiLineString"
_type = 'MultiLineString'
class MultiPolygonField(GeoJsonBaseField):
@@ -2027,14 +2040,14 @@ class MultiPolygonField(GeoJsonBaseField):
.. code-block:: js
{ "type" : "MultiPolygon" ,
"coordinates" : [[
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
], [
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
]
{'type' : 'MultiPolygon' ,
'coordinates' : [[
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
], [
[[x1, y1], [x1, y1] ... [xn, yn]],
[[x1, y1], [x1, y1] ... [xn, yn]]
]
}
You can either pass a dict with the full information or a list
@@ -2044,4 +2057,4 @@ class MultiPolygonField(GeoJsonBaseField):
.. versionadded:: 0.9
"""
_type = "MultiPolygon"
_type = 'MultiPolygon'

View File

@@ -1,7 +1,9 @@
"""Helper functions and types to aid with Python 2.5 - 3 support."""
import sys
"""
Helper functions, constants, and types to aid with Python v2.7 - v3.x and
PyMongo v2.7 - v3.x support.
"""
import pymongo
import six
if pymongo.version_tuple[0] < 3:
@@ -9,29 +11,15 @@ if pymongo.version_tuple[0] < 3:
else:
IS_PYMONGO_3 = True
PY3 = sys.version_info[0] == 3
if PY3:
import codecs
from io import BytesIO as StringIO
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
StringIO = six.BytesIO
# return s converted to binary. b('test') should be equivalent to b'test'
def b(s):
return codecs.latin_1_encode(s)[0]
bin_type = bytes
txt_type = str
else:
# Additionally for Py2, try to use the faster cStringIO, if available
if not six.PY3:
try:
from cStringIO import StringIO
import cStringIO
except ImportError:
from StringIO import StringIO
# Conversion to binary only necessary in Python 3
def b(s):
return s
bin_type = str
txt_type = unicode
str_types = (bin_type, txt_type)
pass
else:
StringIO = cStringIO.StringIO

View File

@@ -1,11 +1,17 @@
from mongoengine.errors import (DoesNotExist, InvalidQueryError,
MultipleObjectsReturned, NotUniqueError,
OperationError)
from mongoengine.errors import *
from mongoengine.queryset.field_list import *
from mongoengine.queryset.manager import *
from mongoengine.queryset.queryset import *
from mongoengine.queryset.transform import *
from mongoengine.queryset.visitor import *
__all__ = (field_list.__all__ + manager.__all__ + queryset.__all__ +
transform.__all__ + visitor.__all__)
# Expose just the public subset of all imported objects and constants.
__all__ = (
'QuerySet', 'QuerySetNoCache', 'Q', 'queryset_manager', 'QuerySetManager',
'QueryFieldList', 'DO_NOTHING', 'NULLIFY', 'CASCADE', 'DENY', 'PULL',
# Errors that might be related to a queryset, mostly here for backward
# compatibility
'DoesNotExist', 'InvalidQueryError', 'MultipleObjectsReturned',
'NotUniqueError', 'OperationError',
)

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,7 @@ class QueryFieldList(object):
return bool(self.fields)
def as_dict(self):
field_list = dict((field, self.value) for field in self.fields)
field_list = {field: self.value for field in self.fields}
if self.slice:
field_list.update(self.slice)
if self._id is not None:

View File

@@ -27,9 +27,10 @@ class QuerySet(BaseQuerySet):
in batches of ``ITER_CHUNK_SIZE``.
If ``self._has_more`` the cursor hasn't been exhausted so cache then
batch. Otherwise iterate the result_cache.
batch. Otherwise iterate the result_cache.
"""
self._iter = True
if self._has_more:
return self._iter_results()
@@ -42,40 +43,56 @@ class QuerySet(BaseQuerySet):
"""
if self._len is not None:
return self._len
# Populate the result cache with *all* of the docs in the cursor
if self._has_more:
# populate the cache
list(self._iter_results())
# Cache the length of the complete result cache and return it
self._len = len(self._result_cache)
return self._len
def __repr__(self):
"""Provides the string representation of the QuerySet
"""
"""Provide a string representation of the QuerySet"""
if self._iter:
return '.. queryset mid-iteration ..'
self._populate_cache()
data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
if len(data) > REPR_OUTPUT_SIZE:
data[-1] = "...(remaining elements truncated)..."
data[-1] = '...(remaining elements truncated)...'
return repr(data)
def _iter_results(self):
"""A generator for iterating over the result cache.
Also populates the cache if there are more possible results to yield.
Raises StopIteration when there are no more results"""
Also populates the cache if there are more possible results to
yield. Raises StopIteration when there are no more results.
"""
if self._result_cache is None:
self._result_cache = []
pos = 0
while True:
upper = len(self._result_cache)
while pos < upper:
# For all positions lower than the length of the current result
# cache, serve the docs straight from the cache w/o hitting the
# database.
# XXX it's VERY important to compute the len within the `while`
# condition because the result cache might expand mid-iteration
# (e.g. if we call len(qs) inside a loop that iterates over the
# queryset). Fortunately len(list) is O(1) in Python, so this
# doesn't cause performance issues.
while pos < len(self._result_cache):
yield self._result_cache[pos]
pos += 1
# Raise StopIteration if we already established there were no more
# docs in the db cursor.
if not self._has_more:
raise StopIteration
# Otherwise, populate more of the cache and repeat.
if len(self._result_cache) <= pos:
self._populate_cache()
@@ -86,12 +103,22 @@ class QuerySet(BaseQuerySet):
"""
if self._result_cache is None:
self._result_cache = []
if self._has_more:
try:
for i in xrange(ITER_CHUNK_SIZE):
self._result_cache.append(self.next())
except StopIteration:
self._has_more = False
# Skip populating the cache if we already established there are no
# more docs to pull from the database.
if not self._has_more:
return
# Pull in ITER_CHUNK_SIZE docs from the database and store them in
# the result cache.
try:
for _ in xrange(ITER_CHUNK_SIZE):
self._result_cache.append(self.next())
except StopIteration:
# Getting this exception means there are no more docs in the
# db cursor. Set _has_more to False so that we can use that
# information in other places.
self._has_more = False
def count(self, with_limit_and_skip=False):
"""Count the selected elements in the query.
@@ -109,13 +136,15 @@ class QuerySet(BaseQuerySet):
return self._len
def no_cache(self):
"""Convert to a non_caching queryset
"""Convert to a non-caching queryset
.. versionadded:: 0.8.3 Convert to non caching queryset
"""
if self._result_cache is not None:
raise OperationError("QuerySet already cached")
return self.clone_into(QuerySetNoCache(self._document, self._collection))
raise OperationError('QuerySet already cached')
return self._clone_into(QuerySetNoCache(self._document,
self._collection))
class QuerySetNoCache(BaseQuerySet):
@@ -126,7 +155,7 @@ class QuerySetNoCache(BaseQuerySet):
.. versionadded:: 0.8.3 Convert to caching queryset
"""
return self.clone_into(QuerySet(self._document, self._collection))
return self._clone_into(QuerySet(self._document, self._collection))
def __repr__(self):
"""Provides the string representation of the QuerySet
@@ -137,13 +166,14 @@ class QuerySetNoCache(BaseQuerySet):
return '.. queryset mid-iteration ..'
data = []
for i in xrange(REPR_OUTPUT_SIZE + 1):
for _ in xrange(REPR_OUTPUT_SIZE + 1):
try:
data.append(self.next())
except StopIteration:
break
if len(data) > REPR_OUTPUT_SIZE:
data[-1] = "...(remaining elements truncated)..."
data[-1] = '...(remaining elements truncated)...'
self.rewind()
return repr(data)

View File

@@ -1,9 +1,11 @@
from collections import defaultdict
from bson import SON
from bson import ObjectId, SON
from bson.dbref import DBRef
import pymongo
import six
from mongoengine.base.fields import UPDATE_OPERATORS
from mongoengine.base import UPDATE_OPERATORS
from mongoengine.common import _import_class
from mongoengine.connection import get_connection
from mongoengine.errors import InvalidQueryError
@@ -26,13 +28,13 @@ MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
STRING_OPERATORS + CUSTOM_OPERATORS)
# TODO make this less complex
def query(_doc_cls=None, **kwargs):
"""Transform a query from Django-style format to Mongo format.
"""
"""Transform a query from Django-style format to Mongo format."""
mongo_query = {}
merge_query = defaultdict(list)
for key, value in sorted(kwargs.items()):
if key == "__raw__":
if key == '__raw__':
mongo_query.update(value)
continue
@@ -45,7 +47,7 @@ def query(_doc_cls=None, **kwargs):
op = parts.pop()
# Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == "":
if len(parts) > 1 and parts[-1] == '':
parts.pop()
negate = False
@@ -57,16 +59,17 @@ def query(_doc_cls=None, **kwargs):
# Switch field names to proper names [set in Field(name='foo')]
try:
fields = _doc_cls._lookup_field(parts)
except Exception, e:
except Exception as e:
raise InvalidQueryError(e)
parts = []
CachedReferenceField = _import_class('CachedReferenceField')
GenericReferenceField = _import_class('GenericReferenceField')
cleaned_fields = []
for field in fields:
append_field = True
if isinstance(field, basestring):
if isinstance(field, six.string_types):
parts.append(field)
append_field = False
# is last and CachedReferenceField
@@ -84,9 +87,9 @@ def query(_doc_cls=None, **kwargs):
singular_ops = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
singular_ops += STRING_OPERATORS
if op in singular_ops:
if isinstance(field, basestring):
if isinstance(field, six.string_types):
if (op in STRING_OPERATORS and
isinstance(value, basestring)):
isinstance(value, six.string_types)):
StringField = _import_class('StringField')
value = StringField.prepare_query_value(op, value)
else:
@@ -98,8 +101,31 @@ def query(_doc_cls=None, **kwargs):
value = value['_id']
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
# 'in', 'nin' and 'all' require a list of values
value = [field.prepare_query_value(op, v) for v in value]
# Raise an error if the in/nin/all/near param is not iterable. We need a
# special check for BaseDocument, because - although it's iterable - using
# it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class('BaseDocument')
if isinstance(value, BaseDocument):
raise TypeError("When using the `in`, `nin`, `all`, or "
"`near`-operators you can\'t use a "
"`Document`, you must wrap your object "
"in a list (object -> [object]).")
elif not hasattr(value, '__iter__'):
raise TypeError("The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an "
"iterable (e.g. a list).")
else:
value = [field.prepare_query_value(op, v) for v in value]
# If we're querying a GenericReferenceField, we need to alter the
# key depending on the value:
# * If the value is a DBRef, the key should be "field_name._ref".
# * If the value is an ObjectId, the key should be "field_name._ref.$id".
if isinstance(field, GenericReferenceField):
if isinstance(value, DBRef):
parts[-1] += '._ref'
elif isinstance(value, ObjectId):
parts[-1] += '._ref.$id'
# if op and op not in COMPARISON_OPERATORS:
if op:
@@ -116,10 +142,10 @@ def query(_doc_cls=None, **kwargs):
value = query(field.field.document_type, **value)
else:
value = field.prepare_query_value(op, value)
value = {"$elemMatch": value}
value = {'$elemMatch': value}
elif op in CUSTOM_OPERATORS:
NotImplementedError("Custom method '%s' has not "
"been implemented" % op)
NotImplementedError('Custom method "%s" has not '
'been implemented' % op)
elif op not in STRING_OPERATORS:
value = {'$' + op: value}
@@ -128,11 +154,13 @@ def query(_doc_cls=None, **kwargs):
for i, part in indices:
parts.insert(i, part)
key = '.'.join(parts)
if op is None or key not in mongo_query:
mongo_query[key] = value
elif key in mongo_query:
if key in mongo_query and isinstance(mongo_query[key], dict):
if isinstance(mongo_query[key], dict):
mongo_query[key].update(value)
# $max/minDistance needs to come last - convert to SON
value_dict = mongo_query[key]
@@ -182,15 +210,16 @@ def query(_doc_cls=None, **kwargs):
def update(_doc_cls=None, **update):
"""Transform an update spec from Django-style format to Mongo format.
"""Transform an update spec from Django-style format to Mongo
format.
"""
mongo_update = {}
for key, value in update.items():
if key == "__raw__":
if key == '__raw__':
mongo_update.update(value)
continue
parts = key.split('__')
# if there is no operator, default to "set"
# if there is no operator, default to 'set'
if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS:
parts.insert(0, 'set')
# Check for an operator and transform to mongo-style if there is
@@ -204,26 +233,25 @@ def update(_doc_cls=None, **update):
# Support decrement by flipping a positive value's sign
# and using 'inc'
op = 'inc'
if value > 0:
value = -value
value = -value
elif op == 'add_to_set':
op = 'addToSet'
elif op == 'set_on_insert':
op = "setOnInsert"
op = 'setOnInsert'
match = None
if parts[-1] in COMPARISON_OPERATORS:
match = parts.pop()
# Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == "":
if len(parts) > 1 and parts[-1] == '':
parts.pop()
if _doc_cls:
# Switch field names to proper names [set in Field(name='foo')]
try:
fields = _doc_cls._lookup_field(parts)
except Exception, e:
except Exception as e:
raise InvalidQueryError(e)
parts = []
@@ -231,7 +259,7 @@ def update(_doc_cls=None, **update):
appended_sub_field = False
for field in fields:
append_field = True
if isinstance(field, basestring):
if isinstance(field, six.string_types):
# Convert the S operator to $
if field == 'S':
field = '$'
@@ -252,7 +280,7 @@ def update(_doc_cls=None, **update):
else:
field = cleaned_fields[-1]
GeoJsonBaseField = _import_class("GeoJsonBaseField")
GeoJsonBaseField = _import_class('GeoJsonBaseField')
if isinstance(field, GeoJsonBaseField):
value = field.to_mongo(value)
@@ -266,7 +294,7 @@ def update(_doc_cls=None, **update):
value = [field.prepare_query_value(op, v) for v in value]
elif field.required or value is not None:
value = field.prepare_query_value(op, value)
elif op == "unset":
elif op == 'unset':
value = 1
if match:
@@ -276,16 +304,16 @@ def update(_doc_cls=None, **update):
key = '.'.join(parts)
if not op:
raise InvalidQueryError("Updates must supply an operation "
"eg: set__FIELD=value")
raise InvalidQueryError('Updates must supply an operation '
'eg: set__FIELD=value')
if 'pull' in op and '.' in key:
# Dot operators don't work on pull operations
# unless they point to a list field
# Otherwise it uses nested dict syntax
if op == 'pullAll':
raise InvalidQueryError("pullAll operations only support "
"a single field depth")
raise InvalidQueryError('pullAll operations only support '
'a single field depth')
# Look for the last list field and use dot notation until there
field_classes = [c.__class__ for c in cleaned_fields]
@@ -296,7 +324,7 @@ def update(_doc_cls=None, **update):
# Then process as normal
last_listField = len(
cleaned_fields) - field_classes.index(ListField)
key = ".".join(parts[:last_listField])
key = '.'.join(parts[:last_listField])
parts = parts[last_listField:]
parts.insert(0, key)
@@ -304,7 +332,7 @@ def update(_doc_cls=None, **update):
for key in parts:
value = {key: value}
elif op == 'addToSet' and isinstance(value, list):
value = {key: {"$each": value}}
value = {key: {'$each': value}}
else:
value = {key: value}
key = '$' + op
@@ -318,78 +346,82 @@ def update(_doc_cls=None, **update):
def _geo_operator(field, op, value):
"""Helper to return the query for a given geo query"""
if op == "max_distance":
"""Helper to return the query for a given geo query."""
if op == 'max_distance':
value = {'$maxDistance': value}
elif op == "min_distance":
elif op == 'min_distance':
value = {'$minDistance': value}
elif field._geo_index == pymongo.GEO2D:
if op == "within_distance":
if op == 'within_distance':
value = {'$within': {'$center': value}}
elif op == "within_spherical_distance":
elif op == 'within_spherical_distance':
value = {'$within': {'$centerSphere': value}}
elif op == "within_polygon":
elif op == 'within_polygon':
value = {'$within': {'$polygon': value}}
elif op == "near":
elif op == 'near':
value = {'$near': value}
elif op == "near_sphere":
elif op == 'near_sphere':
value = {'$nearSphere': value}
elif op == 'within_box':
value = {'$within': {'$box': value}}
else:
raise NotImplementedError("Geo method '%s' has not "
"been implemented for a GeoPointField" % op)
raise NotImplementedError('Geo method "%s" has not been '
'implemented for a GeoPointField' % op)
else:
if op == "geo_within":
value = {"$geoWithin": _infer_geometry(value)}
elif op == "geo_within_box":
value = {"$geoWithin": {"$box": value}}
elif op == "geo_within_polygon":
value = {"$geoWithin": {"$polygon": value}}
elif op == "geo_within_center":
value = {"$geoWithin": {"$center": value}}
elif op == "geo_within_sphere":
value = {"$geoWithin": {"$centerSphere": value}}
elif op == "geo_intersects":
value = {"$geoIntersects": _infer_geometry(value)}
elif op == "near":
if op == 'geo_within':
value = {'$geoWithin': _infer_geometry(value)}
elif op == 'geo_within_box':
value = {'$geoWithin': {'$box': value}}
elif op == 'geo_within_polygon':
value = {'$geoWithin': {'$polygon': value}}
elif op == 'geo_within_center':
value = {'$geoWithin': {'$center': value}}
elif op == 'geo_within_sphere':
value = {'$geoWithin': {'$centerSphere': value}}
elif op == 'geo_intersects':
value = {'$geoIntersects': _infer_geometry(value)}
elif op == 'near':
value = {'$near': _infer_geometry(value)}
else:
raise NotImplementedError("Geo method '%s' has not "
"been implemented for a %s " % (op, field._name))
raise NotImplementedError(
'Geo method "%s" has not been implemented for a %s '
% (op, field._name)
)
return value
def _infer_geometry(value):
"""Helper method that tries to infer the $geometry shape for a given value"""
"""Helper method that tries to infer the $geometry shape for a
given value.
"""
if isinstance(value, dict):
if "$geometry" in value:
if '$geometry' in value:
return value
elif 'coordinates' in value and 'type' in value:
return {"$geometry": value}
raise InvalidQueryError("Invalid $geometry dictionary should have "
"type and coordinates keys")
return {'$geometry': value}
raise InvalidQueryError('Invalid $geometry dictionary should have '
'type and coordinates keys')
elif isinstance(value, (list, set)):
# TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon?
# TODO: should both TypeError and IndexError be alike interpreted?
try:
value[0][0][0]
return {"$geometry": {"type": "Polygon", "coordinates": value}}
return {'$geometry': {'type': 'Polygon', 'coordinates': value}}
except (TypeError, IndexError):
pass
try:
value[0][0]
return {"$geometry": {"type": "LineString", "coordinates": value}}
return {'$geometry': {'type': 'LineString', 'coordinates': value}}
except (TypeError, IndexError):
pass
try:
value[0]
return {"$geometry": {"type": "Point", "coordinates": value}}
return {'$geometry': {'type': 'Point', 'coordinates': value}}
except (TypeError, IndexError):
pass
raise InvalidQueryError("Invalid $geometry data. Can be either a dictionary "
"or (nested) lists of coordinate(s)")
raise InvalidQueryError('Invalid $geometry data. Can be either a '
'dictionary or (nested) lists of coordinate(s)')

View File

@@ -69,9 +69,9 @@ class QueryCompilerVisitor(QNodeVisitor):
self.document = document
def visit_combination(self, combination):
operator = "$and"
operator = '$and'
if combination.operation == combination.OR:
operator = "$or"
operator = '$or'
return {operator: combination.children}
def visit_query(self, query):
@@ -79,8 +79,7 @@ class QueryCompilerVisitor(QNodeVisitor):
class QNode(object):
"""Base class for nodes in query trees.
"""
"""Base class for nodes in query trees."""
AND = 0
OR = 1
@@ -94,7 +93,8 @@ class QNode(object):
raise NotImplementedError
def _combine(self, other, operation):
"""Combine this node with another node into a QCombination object.
"""Combine this node with another node into a QCombination
object.
"""
if getattr(other, 'empty', True):
return self
@@ -116,8 +116,8 @@ class QNode(object):
class QCombination(QNode):
"""Represents the combination of several conditions by a given logical
operator.
"""Represents the combination of several conditions by a given
logical operator.
"""
def __init__(self, operation, children):

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
__all__ = ['pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
'post_save', 'pre_delete', 'post_delete']
__all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
'post_save', 'pre_delete', 'post_delete')
signals_available = False
try:
@@ -34,6 +32,7 @@ except ImportError:
temporarily_connected_to = _fail
del _fail
# the namespace for code signals. If you are not mongoengine code, do
# not put signals in here. Create your own namespace instead.
_signals = Namespace()

View File

@@ -1,13 +1,11 @@
[nosetests]
verbosity = 2
detailed-errors = 1
cover-erase = 1
cover-branches = 1
cover-package = mongoengine
tests = tests
verbosity=2
detailed-errors=1
tests=tests
cover-package=mongoengine
[flake8]
ignore=E501,F401,F403,F405,I201
exclude=build,dist,docs,venv,.tox,.eggs,tests
max-complexity=42
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
max-complexity=47
application-import-names=mongoengine,tests

View File

@@ -21,8 +21,9 @@ except Exception:
def get_version(version_tuple):
if not isinstance(version_tuple[-1], int):
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
"""Return the version tuple as a string, e.g. for (0, 10, 7),
return '0.10.7'.
"""
return '.'.join(map(str, version_tuple))
@@ -41,31 +42,29 @@ CLASSIFIERS = [
'Operating System :: OS Independent',
'Programming Language :: Python',
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
'Topic :: Database',
'Topic :: Software Development :: Libraries :: Python Modules',
]
extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])}
extra_opts = {
'packages': find_packages(exclude=['tests', 'tests.*']),
'tests_require': ['nose', 'coverage==4.2', 'blinker', 'Pillow>=2.0.0']
}
if sys.version_info[0] == 3:
extra_opts['use_2to3'] = True
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0']
if "test" in sys.argv or "nosetests" in sys.argv:
if 'test' in sys.argv or 'nosetests' in sys.argv:
extra_opts['packages'] = find_packages()
extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]}
extra_opts['package_data'] = {
'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
else:
# coverage 4 does not support Python 3.2 anymore
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0', 'python-dateutil']
if sys.version_info[0] == 2 and sys.version_info[1] == 6:
extra_opts['tests_require'].append('unittest2')
extra_opts['tests_require'] += ['python-dateutil']
setup(
name='mongoengine',

View File

@@ -2,4 +2,3 @@ from all_warnings import AllWarnings
from document import *
from queryset import *
from fields import *
from migration import *

View File

@@ -3,8 +3,6 @@ This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the
top level and called first by the test suite.
"""
import sys
sys.path[0:0] = [""]
import unittest
import warnings

View File

@@ -1,5 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
from class_methods import *

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import *

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from bson import SON

View File

@@ -1,6 +1,4 @@
import unittest
import sys
sys.path[0:0] = [""]
from mongoengine import *
from mongoengine.connection import get_db
@@ -143,11 +141,9 @@ class DynamicTest(unittest.TestCase):
def test_three_level_complex_data_lookups(self):
"""Ensure you can query three level document dynamic fields"""
p = self.Person()
p.misc = {'hello': {'hello2': 'world'}}
p.save()
# from pprint import pprint as pp; import pdb; pdb.set_trace();
print self.Person.objects(misc__hello__hello2='world')
p = self.Person.objects.create(
misc={'hello': {'hello2': 'world'}}
)
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
def test_complex_embedded_document_validation(self):

View File

@@ -2,26 +2,22 @@
import unittest
import sys
sys.path[0:0] = [""]
import pymongo
from random import randint
from nose.plugins.skip import SkipTest
from datetime import datetime
import pymongo
from mongoengine import *
from mongoengine.connection import get_db, get_connection
from mongoengine.connection import get_db
from tests.utils import get_mongodb_version, needs_mongodb_v26
__all__ = ("IndexesTest", )
class IndexesTest(unittest.TestCase):
_MAX_RAND = 10 ** 10
def setUp(self):
self.db_name = 'mongoenginetest_IndexesTest_' + str(randint(0, self._MAX_RAND))
self.connection = connect(db=self.db_name)
self.connection = connect(db='mongoenginetest')
self.db = get_db()
class Person(Document):
@@ -416,7 +412,6 @@ class IndexesTest(unittest.TestCase):
User.ensure_indexes()
info = User.objects._collection.index_information()
self.assertEqual(sorted(info.keys()), ['_cls_1_user_guid_1', '_id_'])
User.drop_collection()
def test_embedded_document_index(self):
"""Tests settings an index on an embedded document
@@ -438,7 +433,6 @@ class IndexesTest(unittest.TestCase):
info = BlogPost.objects._collection.index_information()
self.assertEqual(sorted(info.keys()), ['_id_', 'date.yr_-1'])
BlogPost.drop_collection()
def test_list_embedded_document_index(self):
"""Ensure list embedded documents can be indexed
@@ -465,7 +459,6 @@ class IndexesTest(unittest.TestCase):
post1 = BlogPost(title="Embedded Indexes tests in place",
tags=[Tag(name="about"), Tag(name="time")])
post1.save()
BlogPost.drop_collection()
def test_recursive_embedded_objects_dont_break_indexes(self):
@@ -498,8 +491,7 @@ class IndexesTest(unittest.TestCase):
obj = Test(a=1)
obj.save()
connection = get_connection()
IS_MONGODB_3 = connection.server_info()['versionArray'][0] >= 3
IS_MONGODB_3 = get_mongodb_version()[0] >= 3
# Need to be explicit about covered indexes as mongoDB doesn't know if
# the documents returned might have more keys in that here.
@@ -560,8 +552,8 @@ class IndexesTest(unittest.TestCase):
BlogPost.drop_collection()
for i in xrange(0, 10):
tags = [("tag %i" % n) for n in xrange(0, i % 2)]
for i in range(0, 10):
tags = [("tag %i" % n) for n in range(0, i % 2)]
BlogPost(tags=tags).save()
self.assertEqual(BlogPost.objects.count(), 10)
@@ -627,8 +619,6 @@ class IndexesTest(unittest.TestCase):
post3 = BlogPost(title='test3', date=Date(year=2010), slug='test')
self.assertRaises(OperationError, post3.save)
BlogPost.drop_collection()
def test_unique_embedded_document(self):
"""Ensure that uniqueness constraints are applied to fields on embedded documents.
"""
@@ -656,8 +646,6 @@ class IndexesTest(unittest.TestCase):
sub=SubDocument(year=2010, slug='test'))
self.assertRaises(NotUniqueError, post3.save)
BlogPost.drop_collection()
def test_unique_embedded_document_in_list(self):
"""
Ensure that the uniqueness constraints are applied to fields in
@@ -688,8 +676,6 @@ class IndexesTest(unittest.TestCase):
self.assertRaises(NotUniqueError, post2.save)
BlogPost.drop_collection()
def test_unique_with_embedded_document_and_embedded_unique(self):
"""Ensure that uniqueness constraints are applied to fields on
embedded documents. And work with unique_with as well.
@@ -723,8 +709,6 @@ class IndexesTest(unittest.TestCase):
sub=SubDocument(year=2009, slug='test-1'))
self.assertRaises(NotUniqueError, post3.save)
BlogPost.drop_collection()
def test_ttl_indexes(self):
class Log(Document):
@@ -737,14 +721,6 @@ class IndexesTest(unittest.TestCase):
Log.drop_collection()
if pymongo.version_tuple[0] < 2 and pymongo.version_tuple[1] < 3:
raise SkipTest('pymongo needs to be 2.3 or higher for this test')
connection = get_connection()
version_array = connection.server_info()['versionArray']
if version_array[0] < 2 and version_array[1] < 2:
raise SkipTest('MongoDB needs to be 2.2 or higher for this test')
# Indexes are lazy so use list() to perform query
list(Log.objects)
info = Log.objects._collection.index_information()
@@ -772,13 +748,11 @@ class IndexesTest(unittest.TestCase):
raise AssertionError("We saved a dupe!")
except NotUniqueError:
pass
Customer.drop_collection()
def test_unique_and_primary(self):
"""If you set a field as primary, then unexpected behaviour can occur.
You won't create a duplicate but you will update an existing document.
"""
class User(Document):
name = StringField(primary_key=True, unique=True)
password = StringField()
@@ -794,8 +768,23 @@ class IndexesTest(unittest.TestCase):
self.assertEqual(User.objects.count(), 1)
self.assertEqual(User.objects.get().password, 'secret2')
def test_unique_and_primary_create(self):
"""Create a new record with a duplicate primary key
throws an exception
"""
class User(Document):
name = StringField(primary_key=True)
password = StringField()
User.drop_collection()
User.objects.create(name='huangz', password='secret')
with self.assertRaises(NotUniqueError):
User.objects.create(name='huangz', password='secret2')
self.assertEqual(User.objects.count(), 1)
self.assertEqual(User.objects.get().password, 'secret')
def test_index_with_pk(self):
"""Ensure you can use `pk` as part of a query"""
@@ -878,8 +867,8 @@ class IndexesTest(unittest.TestCase):
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
@needs_mongodb_v26
def test_text_indexes(self):
class Book(Document):
title = DictField()
meta = {

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
import warnings
@@ -253,19 +251,17 @@ class InheritanceTest(unittest.TestCase):
self.assertEqual(classes, [Human])
def test_allow_inheritance(self):
"""Ensure that inheritance may be disabled on simple classes and that
_cls and _subclasses will not be used.
"""Ensure that inheritance is disabled by default on simple
classes and that _cls will not be used.
"""
class Animal(Document):
name = StringField()
def create_dog_class():
# can't inherit because Animal didn't explicitly allow inheritance
with self.assertRaises(ValueError):
class Dog(Animal):
pass
self.assertRaises(ValueError, create_dog_class)
# Check that _cls etc aren't present on simple documents
dog = Animal(name='dog').save()
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
@@ -275,17 +271,15 @@ class InheritanceTest(unittest.TestCase):
self.assertFalse('_cls' in obj)
def test_cant_turn_off_inheritance_on_subclass(self):
"""Ensure if inheritance is on in a subclass you cant turn it off
"""Ensure if inheritance is on in a subclass you cant turn it off.
"""
class Animal(Document):
name = StringField()
meta = {'allow_inheritance': True}
def create_mammal_class():
with self.assertRaises(ValueError):
class Mammal(Animal):
meta = {'allow_inheritance': False}
self.assertRaises(ValueError, create_mammal_class)
def test_allow_inheritance_abstract_document(self):
"""Ensure that abstract documents can set inheritance rules and that
@@ -298,10 +292,9 @@ class InheritanceTest(unittest.TestCase):
class Animal(FinalDocument):
name = StringField()
def create_mammal_class():
with self.assertRaises(ValueError):
class Mammal(Animal):
pass
self.assertRaises(ValueError, create_mammal_class)
# Check that _cls isn't present in simple documents
doc = Animal(name='dog')
@@ -360,29 +353,26 @@ class InheritanceTest(unittest.TestCase):
self.assertEqual(berlin.pk, berlin.auto_id_0)
def test_abstract_document_creation_does_not_fail(self):
class City(Document):
continent = StringField()
meta = {'abstract': True,
'allow_inheritance': False}
bkk = City(continent='asia')
self.assertEqual(None, bkk.pk)
# TODO: expected error? Shouldn't we create a new error type?
self.assertRaises(KeyError, lambda: setattr(bkk, 'pk', 1))
with self.assertRaises(KeyError):
setattr(bkk, 'pk', 1)
def test_allow_inheritance_embedded_document(self):
"""Ensure embedded documents respect inheritance
"""
"""Ensure embedded documents respect inheritance."""
class Comment(EmbeddedDocument):
content = StringField()
def create_special_comment():
with self.assertRaises(ValueError):
class SpecialComment(Comment):
pass
self.assertRaises(ValueError, create_special_comment)
doc = Comment(content='test')
self.assertFalse('_cls' in doc.to_mongo())
@@ -454,11 +444,11 @@ class InheritanceTest(unittest.TestCase):
self.assertEqual(Guppy._get_collection_name(), 'fish')
self.assertEqual(Human._get_collection_name(), 'human')
def create_bad_abstract():
# ensure that a subclass of a non-abstract class can't be abstract
with self.assertRaises(ValueError):
class EvilHuman(Human):
evil = BooleanField(default=True)
meta = {'abstract': True}
self.assertRaises(ValueError, create_bad_abstract)
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
import uuid

View File

@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from datetime import datetime
@@ -60,7 +57,7 @@ class ValidatorErrorTest(unittest.TestCase):
try:
User().validate()
except ValidationError, e:
except ValidationError as e:
self.assertTrue("User:None" in e.message)
self.assertEqual(e.to_dict(), {
'username': 'Field is required',
@@ -70,7 +67,7 @@ class ValidatorErrorTest(unittest.TestCase):
user.name = None
try:
user.save()
except ValidationError, e:
except ValidationError as e:
self.assertTrue("User:RossC0" in e.message)
self.assertEqual(e.to_dict(), {
'name': 'Field is required'})
@@ -118,7 +115,7 @@ class ValidatorErrorTest(unittest.TestCase):
try:
Doc(id="bad").validate()
except ValidationError, e:
except ValidationError as e:
self.assertTrue("SubDoc:None" in e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
@@ -136,7 +133,7 @@ class ValidatorErrorTest(unittest.TestCase):
doc.e.val = "OK"
try:
doc.save()
except ValidationError, e:
except ValidationError as e:
self.assertTrue("Doc:test" in e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
@@ -156,14 +153,14 @@ class ValidatorErrorTest(unittest.TestCase):
s = SubDoc()
self.assertRaises(ValidationError, lambda: s.validate())
self.assertRaises(ValidationError, s.validate)
d1.e = s
d2.e = s
del d1
self.assertRaises(ValidationError, lambda: d2.validate())
self.assertRaises(ValidationError, d2.validate)
def test_parent_reference_in_child_document(self):
"""

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import copy
import os
import unittest
import tempfile
import gridfs
import six
from nose.plugins.skip import SkipTest
from mongoengine import *
from mongoengine.connection import get_db
from mongoengine.python_support import b, StringIO
from mongoengine.python_support import StringIO
try:
from PIL import Image
@@ -20,15 +18,13 @@ try:
except ImportError:
HAS_PIL = False
from tests.utils import MongoDBTestCase
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), 'mongodb_leaf.png')
class FileTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
class FileTest(MongoDBTestCase):
def tearDown(self):
self.db.drop_collection('fs.files')
@@ -49,7 +45,7 @@ class FileTest(unittest.TestCase):
PutFile.drop_collection()
text = b('Hello, World!')
text = six.b('Hello, World!')
content_type = 'text/plain'
putfile = PutFile()
@@ -88,8 +84,8 @@ class FileTest(unittest.TestCase):
StreamFile.drop_collection()
text = b('Hello, World!')
more_text = b('Foo Bar')
text = six.b('Hello, World!')
more_text = six.b('Foo Bar')
content_type = 'text/plain'
streamfile = StreamFile()
@@ -123,8 +119,8 @@ class FileTest(unittest.TestCase):
StreamFile.drop_collection()
text = b('Hello, World!')
more_text = b('Foo Bar')
text = six.b('Hello, World!')
more_text = six.b('Foo Bar')
content_type = 'text/plain'
streamfile = StreamFile()
@@ -155,8 +151,8 @@ class FileTest(unittest.TestCase):
class SetFile(Document):
the_file = FileField()
text = b('Hello, World!')
more_text = b('Foo Bar')
text = six.b('Hello, World!')
more_text = six.b('Foo Bar')
SetFile.drop_collection()
@@ -185,7 +181,7 @@ class FileTest(unittest.TestCase):
GridDocument.drop_collection()
with tempfile.TemporaryFile() as f:
f.write(b("Hello World!"))
f.write(six.b("Hello World!"))
f.flush()
# Test without default
@@ -202,7 +198,7 @@ class FileTest(unittest.TestCase):
self.assertEqual(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
# Test with default
doc_d = GridDocument(the_file=b(''))
doc_d = GridDocument(the_file=six.b(''))
doc_d.save()
doc_e = GridDocument.objects.with_id(doc_d.id)
@@ -228,7 +224,7 @@ class FileTest(unittest.TestCase):
# First instance
test_file = TestFile()
test_file.name = "Hello, World!"
test_file.the_file.put(b('Hello, World!'))
test_file.the_file.put(six.b('Hello, World!'))
test_file.save()
# Second instance
@@ -282,7 +278,7 @@ class FileTest(unittest.TestCase):
test_file = TestFile()
self.assertFalse(bool(test_file.the_file))
test_file.the_file.put(b('Hello, World!'), content_type='text/plain')
test_file.the_file.put(six.b('Hello, World!'), content_type='text/plain')
test_file.save()
self.assertTrue(bool(test_file.the_file))
@@ -297,66 +293,66 @@ class FileTest(unittest.TestCase):
test_file = TestFile()
self.assertFalse(test_file.the_file in [{"test": 1}])
def test_file_disk_space(self):
""" Test disk space usage when we delete/replace a file """
def test_file_disk_space(self):
""" Test disk space usage when we delete/replace a file """
class TestFile(Document):
the_file = FileField()
text = b('Hello, World!')
text = six.b('Hello, World!')
content_type = 'text/plain'
testfile = TestFile()
testfile.the_file.put(text, content_type=content_type, filename="hello")
testfile.save()
# Now check fs.files and fs.chunks
# Now check fs.files and fs.chunks
db = TestFile._get_db()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 1)
self.assertEquals(len(list(chunks)), 1)
# Deleting the docoument should delete the files
# Deleting the docoument should delete the files
testfile.delete()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 0)
self.assertEquals(len(list(chunks)), 0)
# Test case where we don't store a file in the first place
# Test case where we don't store a file in the first place
testfile = TestFile()
testfile.save()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 0)
self.assertEquals(len(list(chunks)), 0)
testfile.delete()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 0)
self.assertEquals(len(list(chunks)), 0)
# Test case where we overwrite the file
# Test case where we overwrite the file
testfile = TestFile()
testfile.the_file.put(text, content_type=content_type, filename="hello")
testfile.save()
text = b('Bonjour, World!')
text = six.b('Bonjour, World!')
testfile.the_file.replace(text, content_type=content_type, filename="hello")
testfile.save()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 1)
self.assertEquals(len(list(chunks)), 1)
testfile.delete()
files = db.fs.files.find()
chunks = db.fs.chunks.find()
self.assertEquals(len(list(files)), 0)
@@ -372,14 +368,14 @@ class FileTest(unittest.TestCase):
TestImage.drop_collection()
with tempfile.TemporaryFile() as f:
f.write(b("Hello World!"))
f.write(six.b("Hello World!"))
f.flush()
t = TestImage()
try:
t.image.put(f)
self.fail("Should have raised an invalidation error")
except ValidationError, e:
except ValidationError as e:
self.assertEqual("%s" % e, "Invalid image: cannot identify image file %s" % f)
t = TestImage()
@@ -496,7 +492,7 @@ class FileTest(unittest.TestCase):
# First instance
test_file = TestFile()
test_file.name = "Hello, World!"
test_file.the_file.put(b('Hello, World!'),
test_file.the_file.put(six.b('Hello, World!'),
name="hello.txt")
test_file.save()
@@ -504,16 +500,15 @@ class FileTest(unittest.TestCase):
self.assertEqual(data.get('name'), 'hello.txt')
test_file = TestFile.objects.first()
self.assertEqual(test_file.the_file.read(),
b('Hello, World!'))
self.assertEqual(test_file.the_file.read(), six.b('Hello, World!'))
test_file = TestFile.objects.first()
test_file.the_file = b('HELLO, WORLD!')
test_file.the_file = six.b('HELLO, WORLD!')
test_file.save()
test_file = TestFile.objects.first()
self.assertEqual(test_file.the_file.read(),
b('HELLO, WORLD!'))
six.b('HELLO, WORLD!'))
def test_copyable(self):
class PutFile(Document):
@@ -521,7 +516,7 @@ class FileTest(unittest.TestCase):
PutFile.drop_collection()
text = b('Hello, World!')
text = six.b('Hello, World!')
content_type = 'text/plain'
putfile = PutFile()

View File

@@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import *

View File

@@ -1,11 +0,0 @@
import unittest
from convert_to_new_inheritance_model import *
from decimalfield_as_float import *
from referencefield_dbref_to_object_id import *
from turn_off_inheritance import *
from uuidfield_to_binary import *
if __name__ == '__main__':
unittest.main()

View File

@@ -1,51 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from mongoengine import Document, connect
from mongoengine.connection import get_db
from mongoengine.fields import StringField
__all__ = ('ConvertToNewInheritanceModel', )
class ConvertToNewInheritanceModel(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def tearDown(self):
for collection in self.db.collection_names():
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_how_to_convert_to_the_new_inheritance_model(self):
"""Demonstrates migrating from 0.7 to 0.8
"""
# 1. Declaration of the class
class Animal(Document):
name = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['name']
}
# 2. Remove _types
collection = Animal._get_collection()
collection.update({}, {"$unset": {"_types": 1}}, multi=True)
# 3. Confirm extra data is removed
count = collection.find({'_types': {"$exists": True}}).count()
self.assertEqual(0, count)
# 4. Remove indexes
info = collection.index_information()
indexes_to_drop = [key for key, value in info.iteritems()
if '_types' in dict(value['key'])]
for index in indexes_to_drop:
collection.drop_index(index)
# 5. Recreate indexes
Animal.ensure_indexes()

View File

@@ -1,50 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
import decimal
from decimal import Decimal
from mongoengine import Document, connect
from mongoengine.connection import get_db
from mongoengine.fields import StringField, DecimalField, ListField
__all__ = ('ConvertDecimalField', )
class ConvertDecimalField(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def test_how_to_convert_decimal_fields(self):
"""Demonstrates migrating from 0.7 to 0.8
"""
# 1. Old definition - using dbrefs
class Person(Document):
name = StringField()
money = DecimalField(force_string=True)
monies = ListField(DecimalField(force_string=True))
Person.drop_collection()
Person(name="Wilson Jr", money=Decimal("2.50"),
monies=[Decimal("2.10"), Decimal("5.00")]).save()
# 2. Start the migration by changing the schema
# Change DecimalField - add precision and rounding settings
class Person(Document):
name = StringField()
money = DecimalField(precision=2, rounding=decimal.ROUND_HALF_UP)
monies = ListField(DecimalField(precision=2,
rounding=decimal.ROUND_HALF_UP))
# 3. Loop all the objects and mark parent as changed
for p in Person.objects:
p._mark_as_changed('money')
p._mark_as_changed('monies')
p.save()
# 4. Confirmation of the fix!
wilson = Person.objects(name="Wilson Jr").as_pymongo()[0]
self.assertTrue(isinstance(wilson['money'], float))
self.assertTrue(all([isinstance(m, float) for m in wilson['monies']]))

View File

@@ -1,52 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from mongoengine import Document, connect
from mongoengine.connection import get_db
from mongoengine.fields import StringField, ReferenceField, ListField
__all__ = ('ConvertToObjectIdsModel', )
class ConvertToObjectIdsModel(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def test_how_to_convert_to_object_id_reference_fields(self):
"""Demonstrates migrating from 0.7 to 0.8
"""
# 1. Old definition - using dbrefs
class Person(Document):
name = StringField()
parent = ReferenceField('self', dbref=True)
friends = ListField(ReferenceField('self', dbref=True))
Person.drop_collection()
p1 = Person(name="Wilson", parent=None).save()
f1 = Person(name="John", parent=None).save()
f2 = Person(name="Paul", parent=None).save()
f3 = Person(name="George", parent=None).save()
f4 = Person(name="Ringo", parent=None).save()
Person(name="Wilson Jr", parent=p1, friends=[f1, f2, f3, f4]).save()
# 2. Start the migration by changing the schema
# Change ReferenceField as now dbref defaults to False
class Person(Document):
name = StringField()
parent = ReferenceField('self')
friends = ListField(ReferenceField('self'))
# 3. Loop all the objects and mark parent as changed
for p in Person.objects:
p._mark_as_changed('parent')
p._mark_as_changed('friends')
p.save()
# 4. Confirmation of the fix!
wilson = Person.objects(name="Wilson Jr").as_pymongo()[0]
self.assertEqual(p1.id, wilson['parent'])
self.assertEqual([f1.id, f2.id, f3.id, f4.id], wilson['friends'])

View File

@@ -1,62 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
from mongoengine import Document, connect
from mongoengine.connection import get_db
from mongoengine.fields import StringField
__all__ = ('TurnOffInheritanceTest', )
class TurnOffInheritanceTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def tearDown(self):
for collection in self.db.collection_names():
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_how_to_turn_off_inheritance(self):
"""Demonstrates migrating from allow_inheritance = True to False.
"""
# 1. Old declaration of the class
class Animal(Document):
name = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['name']
}
# 2. Turn off inheritance
class Animal(Document):
name = StringField()
meta = {
'allow_inheritance': False,
'indexes': ['name']
}
# 3. Remove _types and _cls
collection = Animal._get_collection()
collection.update({}, {"$unset": {"_types": 1, "_cls": 1}}, multi=True)
# 3. Confirm extra data is removed
count = collection.find({"$or": [{'_types': {"$exists": True}},
{'_cls': {"$exists": True}}]}).count()
assert count == 0
# 4. Remove indexes
info = collection.index_information()
indexes_to_drop = [key for key, value in info.iteritems()
if '_types' in dict(value['key'])
or '_cls' in dict(value['key'])]
for index in indexes_to_drop:
collection.drop_index(index)
# 5. Recreate indexes
Animal.ensure_indexes()

View File

@@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
import unittest
import uuid
from mongoengine import Document, connect
from mongoengine.connection import get_db
from mongoengine.fields import StringField, UUIDField, ListField
__all__ = ('ConvertToBinaryUUID', )
class ConvertToBinaryUUID(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def test_how_to_convert_to_binary_uuid_fields(self):
"""Demonstrates migrating from 0.7 to 0.8
"""
# 1. Old definition - using dbrefs
class Person(Document):
name = StringField()
uuid = UUIDField(binary=False)
uuids = ListField(UUIDField(binary=False))
Person.drop_collection()
Person(name="Wilson Jr", uuid=uuid.uuid4(),
uuids=[uuid.uuid4(), uuid.uuid4()]).save()
# 2. Start the migration by changing the schema
# Change UUIDFIeld as now binary defaults to True
class Person(Document):
name = StringField()
uuid = UUIDField()
uuids = ListField(UUIDField())
# 3. Loop all the objects and mark parent as changed
for p in Person.objects:
p._mark_as_changed('uuid')
p._mark_as_changed('uuids')
p.save()
# 4. Confirmation of the fix!
wilson = Person.objects(name="Wilson Jr").as_pymongo()[0]
self.assertTrue(isinstance(wilson['uuid'], uuid.UUID))
self.assertTrue(all([isinstance(u, uuid.UUID) for u in wilson['uuids']]))

View File

@@ -1,6 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import *
@@ -95,7 +92,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
exclude = ['d', 'e']
only = ['b', 'c']
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
qs = MyDoc.objects.fields(**{i: 1 for i in include})
self.assertEqual(qs._loaded_fields.as_dict(),
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
qs = qs.only(*only)
@@ -103,14 +100,14 @@ class OnlyExcludeAllTest(unittest.TestCase):
qs = qs.exclude(*exclude)
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
qs = MyDoc.objects.fields(**{i: 1 for i in include})
qs = qs.exclude(*exclude)
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
qs = qs.only(*only)
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
qs = MyDoc.objects.exclude(*exclude)
qs = qs.fields(**dict(((i, 1) for i in include)))
qs = qs.fields(**{i: 1 for i in include})
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
qs = qs.only(*only)
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
@@ -129,7 +126,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
exclude = ['d', 'e']
only = ['b', 'c']
qs = MyDoc.objects.fields(**dict(((i, 1) for i in include)))
qs = MyDoc.objects.fields(**{i: 1 for i in include})
qs = qs.exclude(*exclude)
qs = qs.only(*only)
qs = qs.fields(slice__b=5)
@@ -144,6 +141,16 @@ class OnlyExcludeAllTest(unittest.TestCase):
self.assertEqual(qs._loaded_fields.as_dict(),
{'b': {'$slice': 5}})
def test_mix_slice_with_other_fields(self):
class MyDoc(Document):
a = ListField()
b = ListField()
c = ListField()
qs = MyDoc.objects.fields(a=1, b=0, slice__c=2)
self.assertEqual(qs._loaded_fields.as_dict(),
{'c': {'$slice': 2}, 'a': 1})
def test_only(self):
"""Ensure that QuerySet.only only returns the requested fields.
"""

View File

@@ -1,109 +1,139 @@
import sys
sys.path[0:0] = [""]
import datetime
import unittest
from datetime import datetime, timedelta
from pymongo.errors import OperationFailure
from mongoengine import *
from mongoengine.connection import get_connection
from nose.plugins.skip import SkipTest
from tests.utils import MongoDBTestCase, needs_mongodb_v3
__all__ = ("GeoQueriesTest",)
class GeoQueriesTest(unittest.TestCase):
class GeoQueriesTest(MongoDBTestCase):
def setUp(self):
connect(db='mongoenginetest')
def test_geospatial_operators(self):
"""Ensure that geospatial queries are working.
"""
def _create_event_data(self, point_field_class=GeoPointField):
"""Create some sample data re-used in many of the tests below."""
class Event(Document):
title = StringField()
date = DateTimeField()
location = GeoPointField()
location = point_field_class()
def __unicode__(self):
return self.title
self.Event = Event
Event.drop_collection()
event1 = Event(title="Coltrane Motion @ Double Door",
date=datetime.now() - timedelta(days=1),
location=[-87.677137, 41.909889]).save()
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
date=datetime.now() - timedelta(days=10),
location=[-122.4194155, 37.7749295]).save()
event3 = Event(title="Coltrane Motion @ Empty Bottle",
date=datetime.now(),
location=[-87.686638, 41.900474]).save()
event1 = Event.objects.create(
title="Coltrane Motion @ Double Door",
date=datetime.datetime.now() - datetime.timedelta(days=1),
location=[-87.677137, 41.909889])
event2 = Event.objects.create(
title="Coltrane Motion @ Bottom of the Hill",
date=datetime.datetime.now() - datetime.timedelta(days=10),
location=[-122.4194155, 37.7749295])
event3 = Event.objects.create(
title="Coltrane Motion @ Empty Bottle",
date=datetime.datetime.now(),
location=[-87.686638, 41.900474])
return event1, event2, event3
def test_near(self):
"""Make sure the "near" operator works."""
event1, event2, event3 = self._create_event_data()
# find all events "near" pitchfork office, chicago.
# note that "near" will show the san francisco event, too,
# although it sorts to last.
events = Event.objects(location__near=[-87.67892, 41.9120459])
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
# ensure ordering is respected by "near"
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
events = events.order_by("-date")
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event3, event1, event2])
def test_near_and_max_distance(self):
"""Ensure the "max_distance" operator works alongside the "near"
operator.
"""
event1, event2, event3 = self._create_event_data()
# find events within 10 degrees of san francisco
point = [-122.415579, 37.7566023]
events = self.Event.objects(location__near=point,
location__max_distance=10)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# $minDistance was added in MongoDB v2.6, but continued being buggy
# until v3.0; skip for older versions
@needs_mongodb_v3
def test_near_and_min_distance(self):
"""Ensure the "min_distance" operator works alongside the "near"
operator.
"""
event1, event2, event3 = self._create_event_data()
# find events at least 10 degrees away of san francisco
point = [-122.415579, 37.7566023]
events = self.Event.objects(location__near=point,
location__min_distance=10)
self.assertEqual(events.count(), 2)
def test_within_distance(self):
"""Make sure the "within_distance" operator works."""
event1, event2, event3 = self._create_event_data()
# find events within 5 degrees of pitchfork office, chicago
point_and_distance = [[-87.67892, 41.9120459], 5]
events = Event.objects(location__within_distance=point_and_distance)
events = self.Event.objects(
location__within_distance=point_and_distance)
self.assertEqual(events.count(), 2)
events = list(events)
self.assertTrue(event2 not in events)
self.assertTrue(event1 in events)
self.assertTrue(event3 in events)
# ensure ordering is respected by "near"
events = Event.objects(location__near=[-87.67892, 41.9120459])
events = events.order_by("-date")
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event3, event1, event2])
# find events within 10 degrees of san francisco
point = [-122.415579, 37.7566023]
events = Event.objects(location__near=point, location__max_distance=10)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# find events at least 10 degrees away of san francisco
point = [-122.415579, 37.7566023]
events = Event.objects(location__near=point, location__min_distance=10)
# The following real test passes on MongoDB 3 but minDistance seems
# buggy on older MongoDB versions
if get_connection().server_info()['versionArray'][0] > 2:
self.assertEqual(events.count(), 2)
else:
self.assertTrue(events.count() >= 2)
# find events within 10 degrees of san francisco
point_and_distance = [[-122.415579, 37.7566023], 10]
events = Event.objects(location__within_distance=point_and_distance)
events = self.Event.objects(
location__within_distance=point_and_distance)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# find events within 1 degree of greenpoint, broolyn, nyc, ny
point_and_distance = [[-73.9509714, 40.7237134], 1]
events = Event.objects(location__within_distance=point_and_distance)
events = self.Event.objects(
location__within_distance=point_and_distance)
self.assertEqual(events.count(), 0)
# ensure ordering is respected by "within_distance"
point_and_distance = [[-87.67892, 41.9120459], 10]
events = Event.objects(location__within_distance=point_and_distance)
events = self.Event.objects(
location__within_distance=point_and_distance)
events = events.order_by("-date")
self.assertEqual(events.count(), 2)
self.assertEqual(events[0], event3)
def test_within_box(self):
"""Ensure the "within_box" operator works."""
event1, event2, event3 = self._create_event_data()
# check that within_box works
box = [(-125.0, 35.0), (-100.0, 40.0)]
events = Event.objects(location__within_box=box)
events = self.Event.objects(location__within_box=box)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event2.id)
def test_within_polygon(self):
"""Ensure the "within_polygon" operator works."""
event1, event2, event3 = self._create_event_data()
polygon = [
(-87.694445, 41.912114),
(-87.69084, 41.919395),
@@ -111,7 +141,7 @@ class GeoQueriesTest(unittest.TestCase):
(-87.654276, 41.911731),
(-87.656164, 41.898061),
]
events = Event.objects(location__within_polygon=polygon)
events = self.Event.objects(location__within_polygon=polygon)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event1.id)
@@ -120,13 +150,151 @@ class GeoQueriesTest(unittest.TestCase):
(-1.225891, 52.792797),
(-4.40094, 53.389881)
]
events = Event.objects(location__within_polygon=polygon2)
events = self.Event.objects(location__within_polygon=polygon2)
self.assertEqual(events.count(), 0)
def test_geo_spatial_embedded(self):
def test_2dsphere_near(self):
"""Make sure the "near" operator works with a PointField, which
corresponds to a 2dsphere index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
# find all events "near" pitchfork office, chicago.
# note that "near" will show the san francisco event, too,
# although it sorts to last.
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
# ensure ordering is respected by "near"
events = self.Event.objects(location__near=[-87.67892, 41.9120459])
events = events.order_by("-date")
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event3, event1, event2])
def test_2dsphere_near_and_max_distance(self):
"""Ensure the "max_distance" operator works alongside the "near"
operator with a 2dsphere index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
# find events within 10km of san francisco
point = [-122.415579, 37.7566023]
events = self.Event.objects(location__near=point,
location__max_distance=10000)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# find events within 1km of greenpoint, broolyn, nyc, ny
events = self.Event.objects(location__near=[-73.9509714, 40.7237134],
location__max_distance=1000)
self.assertEqual(events.count(), 0)
# ensure ordering is respected by "near"
events = self.Event.objects(
location__near=[-87.67892, 41.9120459],
location__max_distance=10000
).order_by("-date")
self.assertEqual(events.count(), 2)
self.assertEqual(events[0], event3)
def test_2dsphere_geo_within_box(self):
"""Ensure the "geo_within_box" operator works with a 2dsphere
index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
# check that within_box works
box = [(-125.0, 35.0), (-100.0, 40.0)]
events = self.Event.objects(location__geo_within_box=box)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event2.id)
def test_2dsphere_geo_within_polygon(self):
"""Ensure the "geo_within_polygon" operator works with a
2dsphere index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
polygon = [
(-87.694445, 41.912114),
(-87.69084, 41.919395),
(-87.681742, 41.927186),
(-87.654276, 41.911731),
(-87.656164, 41.898061),
]
events = self.Event.objects(location__geo_within_polygon=polygon)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event1.id)
polygon2 = [
(-1.742249, 54.033586),
(-1.225891, 52.792797),
(-4.40094, 53.389881)
]
events = self.Event.objects(location__geo_within_polygon=polygon2)
self.assertEqual(events.count(), 0)
# $minDistance was added in MongoDB v2.6, but continued being buggy
# until v3.0; skip for older versions
@needs_mongodb_v3
def test_2dsphere_near_and_min_max_distance(self):
"""Ensure "min_distace" and "max_distance" operators work well
together with the "near" operator in a 2dsphere index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
# ensure min_distance and max_distance combine well
events = self.Event.objects(
location__near=[-87.67892, 41.9120459],
location__min_distance=1000,
location__max_distance=10000
).order_by("-date")
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event3)
# ensure ordering is respected by "near" with "min_distance"
events = self.Event.objects(
location__near=[-87.67892, 41.9120459],
location__min_distance=10000
).order_by("-date")
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
def test_2dsphere_geo_within_center(self):
"""Make sure the "geo_within_center" operator works with a
2dsphere index.
"""
event1, event2, event3 = self._create_event_data(
point_field_class=PointField
)
# find events within 5 degrees of pitchfork office, chicago
point_and_distance = [[-87.67892, 41.9120459], 2]
events = self.Event.objects(
location__geo_within_center=point_and_distance)
self.assertEqual(events.count(), 2)
events = list(events)
self.assertTrue(event2 not in events)
self.assertTrue(event1 in events)
self.assertTrue(event3 in events)
def _test_embedded(self, point_field_class):
"""Helper test method ensuring given point field class works
well in an embedded document.
"""
class Venue(EmbeddedDocument):
location = GeoPointField()
location = point_field_class()
name = StringField()
class Event(Document):
@@ -152,16 +320,18 @@ class GeoQueriesTest(unittest.TestCase):
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
def test_spherical_geospatial_operators(self):
"""Ensure that spherical geospatial queries are working
"""
# Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
connection = get_connection()
info = connection.test.command('buildInfo')
mongodb_version = tuple([int(i) for i in info['version'].split('.')])
if mongodb_version < (2, 6, 4):
raise SkipTest("Need MongoDB version 2.6.4+")
def test_geo_spatial_embedded(self):
"""Make sure GeoPointField works properly in an embedded document."""
self._test_embedded(point_field_class=GeoPointField)
def test_2dsphere_point_embedded(self):
"""Make sure PointField works properly in an embedded document."""
self._test_embedded(point_field_class=PointField)
# Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
@needs_mongodb_v3
def test_spherical_geospatial_operators(self):
"""Ensure that spherical geospatial queries are working."""
class Point(Document):
location = GeoPointField()
@@ -181,7 +351,10 @@ class GeoQueriesTest(unittest.TestCase):
# Same behavior for _within_spherical_distance
points = Point.objects(
location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius]
location__within_spherical_distance=[
[-122, 37.5],
60 / earth_radius
]
)
self.assertEqual(points.count(), 2)
@@ -198,14 +371,9 @@ class GeoQueriesTest(unittest.TestCase):
# Test query works with min_distance, being farer from one point
points = Point.objects(location__near_sphere=[-122, 37.8],
location__min_distance=60 / earth_radius)
# The following real test passes on MongoDB 3 but minDistance seems
# buggy on older MongoDB versions
if get_connection().server_info()['versionArray'][0] > 2:
self.assertEqual(points.count(), 1)
far_point = points.first()
self.assertNotEqual(close_point, far_point)
else:
self.assertTrue(points.count() >= 1)
self.assertEqual(points.count(), 1)
far_point = points.first()
self.assertNotEqual(close_point, far_point)
# Finds both points, but orders the north point first because it's
# closer to the reference point to the north.
@@ -224,141 +392,15 @@ class GeoQueriesTest(unittest.TestCase):
# Finds only one point because only the first point is within 60km of
# the reference point to the south.
points = Point.objects(
location__within_spherical_distance=[[-122, 36.5], 60/earth_radius])
location__within_spherical_distance=[
[-122, 36.5],
60 / earth_radius
]
)
self.assertEqual(points.count(), 1)
self.assertEqual(points[0].id, south_point.id)
def test_2dsphere_point(self):
class Event(Document):
title = StringField()
date = DateTimeField()
location = PointField()
def __unicode__(self):
return self.title
Event.drop_collection()
event1 = Event(title="Coltrane Motion @ Double Door",
date=datetime.now() - timedelta(days=1),
location=[-87.677137, 41.909889])
event1.save()
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
date=datetime.now() - timedelta(days=10),
location=[-122.4194155, 37.7749295]).save()
event3 = Event(title="Coltrane Motion @ Empty Bottle",
date=datetime.now(),
location=[-87.686638, 41.900474]).save()
# find all events "near" pitchfork office, chicago.
# note that "near" will show the san francisco event, too,
# although it sorts to last.
events = Event.objects(location__near=[-87.67892, 41.9120459])
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
# find events within 5 degrees of pitchfork office, chicago
point_and_distance = [[-87.67892, 41.9120459], 2]
events = Event.objects(location__geo_within_center=point_and_distance)
self.assertEqual(events.count(), 2)
events = list(events)
self.assertTrue(event2 not in events)
self.assertTrue(event1 in events)
self.assertTrue(event3 in events)
# ensure ordering is respected by "near"
events = Event.objects(location__near=[-87.67892, 41.9120459])
events = events.order_by("-date")
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event3, event1, event2])
# find events within 10km of san francisco
point = [-122.415579, 37.7566023]
events = Event.objects(location__near=point, location__max_distance=10000)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# find events within 1km of greenpoint, broolyn, nyc, ny
events = Event.objects(location__near=[-73.9509714, 40.7237134], location__max_distance=1000)
self.assertEqual(events.count(), 0)
# ensure ordering is respected by "near"
events = Event.objects(location__near=[-87.67892, 41.9120459],
location__max_distance=10000).order_by("-date")
self.assertEqual(events.count(), 2)
self.assertEqual(events[0], event3)
# ensure min_distance and max_distance combine well
events = Event.objects(location__near=[-87.67892, 41.9120459],
location__min_distance=1000,
location__max_distance=10000).order_by("-date")
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event3)
# ensure ordering is respected by "near"
events = Event.objects(location__near=[-87.67892, 41.9120459],
# location__min_distance=10000
location__min_distance=10000).order_by("-date")
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# check that within_box works
box = [(-125.0, 35.0), (-100.0, 40.0)]
events = Event.objects(location__geo_within_box=box)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event2.id)
polygon = [
(-87.694445, 41.912114),
(-87.69084, 41.919395),
(-87.681742, 41.927186),
(-87.654276, 41.911731),
(-87.656164, 41.898061),
]
events = Event.objects(location__geo_within_polygon=polygon)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event1.id)
polygon2 = [
(-1.742249, 54.033586),
(-1.225891, 52.792797),
(-4.40094, 53.389881)
]
events = Event.objects(location__geo_within_polygon=polygon2)
self.assertEqual(events.count(), 0)
def test_2dsphere_point_embedded(self):
class Venue(EmbeddedDocument):
location = GeoPointField()
name = StringField()
class Event(Document):
title = StringField()
venue = EmbeddedDocumentField(Venue)
Event.drop_collection()
venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889])
venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295])
event1 = Event(title="Coltrane Motion @ Double Door",
venue=venue1).save()
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
venue=venue2).save()
event3 = Event(title="Coltrane Motion @ Empty Bottle",
venue=venue1).save()
# find all events "near" pitchfork office, chicago.
# note that "near" will show the san francisco event, too,
# although it sorts to last.
events = Event.objects(venue__location__near=[-87.67892, 41.9120459])
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
def test_linestring(self):
class Road(Document):
name = StringField()
line = LineStringField()
@@ -414,7 +456,6 @@ class GeoQueriesTest(unittest.TestCase):
self.assertEqual(1, roads)
def test_polygon(self):
class Road(Document):
name = StringField()
poly = PolygonField()
@@ -511,5 +552,6 @@ class GeoQueriesTest(unittest.TestCase):
loc = Location.objects.as_pymongo()[0]
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
if __name__ == '__main__':
unittest.main()

View File

@@ -1,6 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import connect, Document, IntField
@@ -99,4 +96,4 @@ class FindAndModifyTest(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
unittest.main()

View File

@@ -9,57 +9,32 @@ from nose.plugins.skip import SkipTest
import pymongo
from pymongo.errors import ConfigurationError
from pymongo.read_preferences import ReadPreference
import six
from mongoengine import *
from mongoengine.connection import get_connection, get_db
from mongoengine.context_managers import query_counter, switch_db
from mongoengine.errors import InvalidQueryError
from mongoengine.python_support import IS_PYMONGO_3, PY3
from mongoengine.python_support import IS_PYMONGO_3
from mongoengine.queryset import (DoesNotExist, MultipleObjectsReturned,
QuerySet, QuerySetManager, queryset_manager)
from tests.utils import needs_mongodb_v26, skip_pymongo3
__all__ = ("QuerySetTest",)
class db_ops_tracker(query_counter):
def get_ops(self):
ignore_query = {"ns": {"$ne": "%s.system.indexes" % self.db.name}}
ignore_query = {
'ns': {'$ne': '%s.system.indexes' % self.db.name},
'command.count': {'$ne': 'system.profile'}
}
return list(self.db.system.profile.find(ignore_query))
def skip_older_mongodb(f):
def _inner(*args, **kwargs):
connection = get_connection()
info = connection.test.command('buildInfo')
mongodb_version = tuple([int(i) for i in info['version'].split('.')])
if mongodb_version < (2, 6):
raise SkipTest("Need MongoDB version 2.6+")
return f(*args, **kwargs)
_inner.__name__ = f.__name__
_inner.__doc__ = f.__doc__
return _inner
def skip_pymongo3(f):
def _inner(*args, **kwargs):
if IS_PYMONGO_3:
raise SkipTest("Useless with PyMongo 3+")
return f(*args, **kwargs)
_inner.__name__ = f.__name__
_inner.__doc__ = f.__doc__
return _inner
class QuerySetTest(unittest.TestCase):
def setUp(self):
@@ -94,67 +69,120 @@ class QuerySetTest(unittest.TestCase):
author = ReferenceField(self.Person)
author2 = GenericReferenceField()
def test_reference():
# test addressing a field from a reference
with self.assertRaises(InvalidQueryError):
list(BlogPost.objects(author__name="test"))
self.assertRaises(InvalidQueryError, test_reference)
def test_generic_reference():
# should fail for a generic reference as well
with self.assertRaises(InvalidQueryError):
list(BlogPost.objects(author2__name="test"))
def test_find(self):
"""Ensure that a query returns a valid set of results.
"""
self.Person(name="User A", age=20).save()
self.Person(name="User B", age=30).save()
"""Ensure that a query returns a valid set of results."""
user_a = self.Person.objects.create(name='User A', age=20)
user_b = self.Person.objects.create(name='User B', age=30)
# Find all people in the collection
people = self.Person.objects
self.assertEqual(people.count(), 2)
results = list(people)
self.assertTrue(isinstance(results[0], self.Person))
self.assertTrue(isinstance(results[0].id, (ObjectId, str, unicode)))
self.assertEqual(results[0].name, "User A")
self.assertEqual(results[0], user_a)
self.assertEqual(results[0].name, 'User A')
self.assertEqual(results[0].age, 20)
self.assertEqual(results[1].name, "User B")
self.assertEqual(results[1], user_b)
self.assertEqual(results[1].name, 'User B')
self.assertEqual(results[1].age, 30)
# Use a query to filter the people found to just person1
# Filter people by age
people = self.Person.objects(age=20)
self.assertEqual(people.count(), 1)
person = people.next()
self.assertEqual(person, user_a)
self.assertEqual(person.name, "User A")
self.assertEqual(person.age, 20)
# Test limit
def test_limit(self):
"""Ensure that QuerySet.limit works as expected."""
user_a = self.Person.objects.create(name='User A', age=20)
user_b = self.Person.objects.create(name='User B', age=30)
# Test limit on a new queryset
people = list(self.Person.objects.limit(1))
self.assertEqual(len(people), 1)
self.assertEqual(people[0].name, 'User A')
self.assertEqual(people[0], user_a)
# Test skip
# Test limit on an existing queryset
people = self.Person.objects
self.assertEqual(len(people), 2)
people2 = people.limit(1)
self.assertEqual(len(people), 2)
self.assertEqual(len(people2), 1)
self.assertEqual(people2[0], user_a)
# Test chaining of only after limit
person = self.Person.objects().limit(1).only('name').first()
self.assertEqual(person, user_a)
self.assertEqual(person.name, 'User A')
self.assertEqual(person.age, None)
def test_skip(self):
"""Ensure that QuerySet.skip works as expected."""
user_a = self.Person.objects.create(name='User A', age=20)
user_b = self.Person.objects.create(name='User B', age=30)
# Test skip on a new queryset
people = list(self.Person.objects.skip(1))
self.assertEqual(len(people), 1)
self.assertEqual(people[0].name, 'User B')
self.assertEqual(people[0], user_b)
person3 = self.Person(name="User C", age=40)
person3.save()
# Test skip on an existing queryset
people = self.Person.objects
self.assertEqual(len(people), 2)
people2 = people.skip(1)
self.assertEqual(len(people), 2)
self.assertEqual(len(people2), 1)
self.assertEqual(people2[0], user_b)
# Test chaining of only after skip
person = self.Person.objects().skip(1).only('name').first()
self.assertEqual(person, user_b)
self.assertEqual(person.name, 'User B')
self.assertEqual(person.age, None)
def test_slice(self):
"""Ensure slicing a queryset works as expected."""
user_a = self.Person.objects.create(name='User A', age=20)
user_b = self.Person.objects.create(name='User B', age=30)
user_c = self.Person.objects.create(name="User C", age=40)
# Test slice limit
people = list(self.Person.objects[:2])
self.assertEqual(len(people), 2)
self.assertEqual(people[0].name, 'User A')
self.assertEqual(people[1].name, 'User B')
self.assertEqual(people[0], user_a)
self.assertEqual(people[1], user_b)
# Test slice skip
people = list(self.Person.objects[1:])
self.assertEqual(len(people), 2)
self.assertEqual(people[0].name, 'User B')
self.assertEqual(people[1].name, 'User C')
self.assertEqual(people[0], user_b)
self.assertEqual(people[1], user_c)
# Test slice limit and skip
people = list(self.Person.objects[1:2])
self.assertEqual(len(people), 1)
self.assertEqual(people[0].name, 'User B')
self.assertEqual(people[0], user_b)
# Test slice limit and skip on an existing queryset
people = self.Person.objects
self.assertEqual(len(people), 3)
people2 = people[1:2]
self.assertEqual(len(people2), 1)
self.assertEqual(people2[0], user_b)
# Test slice limit and skip cursor reset
qs = self.Person.objects[1:2]
@@ -165,6 +193,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(len(people), 1)
self.assertEqual(people[0].name, 'User B')
# Test empty slice
people = list(self.Person.objects[1:1])
self.assertEqual(len(people), 0)
@@ -174,7 +203,7 @@ class QuerySetTest(unittest.TestCase):
# Test larger slice __repr__
self.Person.objects.delete()
for i in xrange(55):
for i in range(55):
self.Person(name='A%s' % i, age=i).save()
self.assertEqual(self.Person.objects.count(), 55)
@@ -184,12 +213,6 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual("[<Person: Person object>, <Person: Person object>]",
"%s" % self.Person.objects[51:53])
# Test only after limit
self.assertEqual(self.Person.objects().limit(2).only('name')[0].age, None)
# Test only after skip
self.assertEqual(self.Person.objects().skip(2).only('name')[0].age, None)
def test_find_one(self):
"""Ensure that a query using find_one returns a valid result.
"""
@@ -218,14 +241,15 @@ class QuerySetTest(unittest.TestCase):
person = self.Person.objects[1]
self.assertEqual(person.name, "User B")
self.assertRaises(IndexError, self.Person.objects.__getitem__, 2)
with self.assertRaises(IndexError):
self.Person.objects[2]
# Find a document using just the object id
person = self.Person.objects.with_id(person1.id)
self.assertEqual(person.name, "User A")
self.assertRaises(
InvalidQueryError, self.Person.objects(name="User A").with_id, person1.id)
with self.assertRaises(InvalidQueryError):
self.Person.objects(name="User A").with_id(person1.id)
def test_find_only_one(self):
"""Ensure that a query using ``get`` returns at most one result.
@@ -337,9 +361,37 @@ class QuerySetTest(unittest.TestCase):
query = query.filter(boolfield=True)
self.assertEqual(query.count(), 1)
def test_batch_size(self):
"""Ensure that batch_size works."""
class A(Document):
s = StringField()
A.drop_collection()
for i in range(100):
A.objects.create(s=str(i))
# test iterating over the result set
cnt = 0
for a in A.objects.batch_size(10):
cnt += 1
self.assertEqual(cnt, 100)
# test chaining
qs = A.objects.all()
qs = qs.limit(10).batch_size(20).skip(91)
cnt = 0
for a in qs:
cnt += 1
self.assertEqual(cnt, 9)
# test invalid batch size
qs = A.objects.batch_size(-1)
with self.assertRaises(ValueError):
list(qs)
def test_update_write_concern(self):
"""Test that passing write_concern works"""
self.Person.drop_collection()
write_concern = {"fsync": True}
@@ -365,18 +417,14 @@ class QuerySetTest(unittest.TestCase):
"""Test to ensure that update is passed a value to update to"""
self.Person.drop_collection()
author = self.Person(name='Test User')
author.save()
author = self.Person.objects.create(name='Test User')
def update_raises():
with self.assertRaises(OperationError):
self.Person.objects(pk=author.pk).update({})
def update_one_raises():
with self.assertRaises(OperationError):
self.Person.objects(pk=author.pk).update_one({})
self.assertRaises(OperationError, update_raises)
self.assertRaises(OperationError, update_one_raises)
def test_update_array_position(self):
"""Ensure that updating by array position works.
@@ -404,8 +452,8 @@ class QuerySetTest(unittest.TestCase):
Blog.objects.create(posts=[post2, post1])
# Update all of the first comments of second posts of all blogs
Blog.objects().update(set__posts__1__comments__0__name="testc")
testc_blogs = Blog.objects(posts__1__comments__0__name="testc")
Blog.objects().update(set__posts__1__comments__0__name='testc')
testc_blogs = Blog.objects(posts__1__comments__0__name='testc')
self.assertEqual(testc_blogs.count(), 2)
Blog.drop_collection()
@@ -414,14 +462,13 @@ class QuerySetTest(unittest.TestCase):
# Update only the first blog returned by the query
Blog.objects().update_one(
set__posts__1__comments__1__name="testc")
testc_blogs = Blog.objects(posts__1__comments__1__name="testc")
set__posts__1__comments__1__name='testc')
testc_blogs = Blog.objects(posts__1__comments__1__name='testc')
self.assertEqual(testc_blogs.count(), 1)
# Check that using this indexing syntax on a non-list fails
def non_list_indexing():
Blog.objects().update(set__posts__1__comments__0__name__1="asdf")
self.assertRaises(InvalidQueryError, non_list_indexing)
with self.assertRaises(InvalidQueryError):
Blog.objects().update(set__posts__1__comments__0__name__1='asdf')
Blog.drop_collection()
@@ -489,15 +536,12 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(simple.x, [1, 2, None, 4, 3, 2, 3, 4])
# Nested updates arent supported yet..
def update_nested():
with self.assertRaises(OperationError):
Simple.drop_collection()
Simple(x=[{'test': [1, 2, 3, 4]}]).save()
Simple.objects(x__test=2).update(set__x__S__test__S=3)
self.assertEqual(simple.x, [1, 2, 3, 4])
self.assertRaises(OperationError, update_nested)
Simple.drop_collection()
def test_update_using_positional_operator_embedded_document(self):
"""Ensure that the embedded documents can be updated using the positional
operator."""
@@ -527,16 +571,23 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(post.comments[0].by, 'joe')
self.assertEqual(post.comments[0].votes.score, 4)
@needs_mongodb_v26
def test_update_min_max(self):
class Scores(Document):
high_score = IntField()
low_score = IntField()
scores = Scores(high_score=800, low_score=200)
scores.save()
scores = Scores.objects.create(high_score=800, low_score=200)
Scores.objects(id=scores.id).update(min__low_score=150)
self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150)
Scores.objects(id=scores.id).update(min__low_score=250)
self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150)
Scores.objects(id=scores.id).update(max__high_score=1000)
self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000)
Scores.objects(id=scores.id).update(max__high_score=500)
self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000)
def test_updates_can_have_match_operators(self):
@@ -590,11 +641,11 @@ class QuerySetTest(unittest.TestCase):
members = DictField()
club = Club()
club.members['John'] = dict(gender="M", age=13)
club.members['John'] = {'gender': 'M', 'age': 13}
club.save()
Club.objects().update(
set__members={"John": dict(gender="F", age=14)})
set__members={"John": {'gender': 'F', 'age': 14}})
club = Club.objects().first()
self.assertEqual(club.members['John']['gender'], "F")
@@ -775,7 +826,7 @@ class QuerySetTest(unittest.TestCase):
post2 = Post(comments=[comment2, comment2])
blogs = []
for i in xrange(1, 100):
for i in range(1, 100):
blogs.append(Blog(title="post %s" % i, posts=[post1, post2]))
Blog.objects.insert(blogs, load_bulk=False)
@@ -812,30 +863,31 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(Blog.objects.count(), 2)
# test handles people trying to upsert
def throw_operation_error():
# test inserting an existing document (shouldn't be allowed)
with self.assertRaises(OperationError):
blog = Blog.objects.first()
Blog.objects.insert(blog)
# test inserting a query set
with self.assertRaises(OperationError):
blogs = Blog.objects
Blog.objects.insert(blogs)
self.assertRaises(OperationError, throw_operation_error)
# Test can insert new doc
# insert a new doc
new_post = Blog(title="code123", id=ObjectId())
Blog.objects.insert(new_post)
# test handles other classes being inserted
def throw_operation_error_wrong_doc():
class Author(Document):
pass
class Author(Document):
pass
# try inserting a different document class
with self.assertRaises(OperationError):
Blog.objects.insert(Author())
self.assertRaises(OperationError, throw_operation_error_wrong_doc)
def throw_operation_error_not_a_document():
# try inserting a non-document
with self.assertRaises(OperationError):
Blog.objects.insert("HELLO WORLD")
self.assertRaises(OperationError, throw_operation_error_not_a_document)
Blog.drop_collection()
blog1 = Blog(title="code", posts=[post1, post2])
@@ -855,14 +907,13 @@ class QuerySetTest(unittest.TestCase):
blog3 = Blog(title="baz", posts=[post1, post2])
Blog.objects.insert([blog1, blog2])
def throw_operation_error_not_unique():
with self.assertRaises(NotUniqueError):
Blog.objects.insert([blog2, blog3])
self.assertRaises(NotUniqueError, throw_operation_error_not_unique)
self.assertEqual(Blog.objects.count(), 2)
Blog.objects.insert([blog2, blog3], write_concern={"w": 0,
'continue_on_error': True})
Blog.objects.insert([blog2, blog3],
write_concern={"w": 0, 'continue_on_error': True})
self.assertEqual(Blog.objects.count(), 3)
def test_get_changed_fields_query_count(self):
@@ -940,7 +991,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(person.name, "User A")
self.assertEqual(person.age, 20)
@skip_older_mongodb
@needs_mongodb_v26
@skip_pymongo3
def test_cursor_args(self):
"""Ensures the cursor args can be set as expected
@@ -995,7 +1046,7 @@ class QuerySetTest(unittest.TestCase):
Doc.drop_collection()
for i in xrange(1000):
for i in range(1000):
Doc(number=i).save()
docs = Doc.objects.order_by('number')
@@ -1149,7 +1200,7 @@ class QuerySetTest(unittest.TestCase):
qs = list(qs)
expected = list(expected)
self.assertEqual(len(qs), len(expected))
for i in xrange(len(qs)):
for i in range(len(qs)):
self.assertEqual(qs[i], expected[i])
def test_ordering(self):
@@ -1189,7 +1240,8 @@ class QuerySetTest(unittest.TestCase):
self.assertSequence(qs, expected)
def test_clear_ordering(self):
""" Ensure that the default ordering can be cleared by calling order_by().
"""Ensure that the default ordering can be cleared by calling
order_by() w/o any arguments.
"""
class BlogPost(Document):
title = StringField()
@@ -1201,16 +1253,35 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection()
# default ordering should be used by default
with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').first()
self.assertEqual(len(q.get_ops()), 1)
self.assertEqual(
q.get_ops()[0]['query']['$orderby'], {u'published_date': -1})
q.get_ops()[0]['query']['$orderby'],
{'published_date': -1}
)
# calling order_by() should clear the default ordering
with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').order_by().first()
self.assertEqual(len(q.get_ops()), 1)
print q.get_ops()[0]['query']
self.assertFalse('$orderby' in q.get_ops()[0]['query'])
# calling an explicit order_by should use a specified sort
with db_ops_tracker() as q:
BlogPost.objects.filter(title='whatever').order_by('published_date').first()
self.assertEqual(len(q.get_ops()), 1)
self.assertEqual(
q.get_ops()[0]['query']['$orderby'],
{'published_date': 1}
)
# calling order_by() after an explicit sort should clear it
with db_ops_tracker() as q:
qs = BlogPost.objects.filter(title='whatever').order_by('published_date')
qs.order_by().first()
self.assertEqual(len(q.get_ops()), 1)
self.assertFalse('$orderby' in q.get_ops()[0]['query'])
def test_no_ordering_for_get(self):
@@ -1239,7 +1310,8 @@ class QuerySetTest(unittest.TestCase):
self.assertFalse('$orderby' in q.get_ops()[0]['query'])
def test_find_embedded(self):
"""Ensure that an embedded document is properly returned from a query.
"""Ensure that an embedded document is properly returned from
different manners of querying.
"""
class User(EmbeddedDocument):
name = StringField()
@@ -1250,16 +1322,45 @@ class QuerySetTest(unittest.TestCase):
BlogPost.drop_collection()
post = BlogPost(content='Had a good coffee today...')
post.author = User(name='Test User')
post.save()
user = User(name='Test User')
BlogPost.objects.create(
author=user,
content='Had a good coffee today...'
)
result = BlogPost.objects.first()
self.assertTrue(isinstance(result.author, User))
self.assertEqual(result.author.name, 'Test User')
result = BlogPost.objects.get(author__name=user.name)
self.assertTrue(isinstance(result.author, User))
self.assertEqual(result.author.name, 'Test User')
result = BlogPost.objects.get(author={'name': user.name})
self.assertTrue(isinstance(result.author, User))
self.assertEqual(result.author.name, 'Test User')
# Fails, since the string is not a type that is able to represent the
# author's document structure (should be dict)
with self.assertRaises(InvalidQueryError):
BlogPost.objects.get(author=user.name)
def test_find_empty_embedded(self):
"""Ensure that you can save and find an empty embedded document."""
class User(EmbeddedDocument):
name = StringField()
class BlogPost(Document):
content = StringField()
author = EmbeddedDocumentField(User)
BlogPost.drop_collection()
BlogPost.objects.create(content='Anonymous post...')
result = BlogPost.objects.get(author=None)
self.assertEqual(result.author, None)
def test_find_dict_item(self):
"""Ensure that DictField items may be found.
"""
@@ -1667,7 +1768,7 @@ class QuerySetTest(unittest.TestCase):
Log.drop_collection()
for i in xrange(10):
for i in range(10):
Log().save()
Log.objects()[3:5].delete()
@@ -1770,6 +1871,11 @@ class QuerySetTest(unittest.TestCase):
post.reload()
self.assertEqual(post.hits, 10)
# Negative dec operator is equal to a positive inc operator
BlogPost.objects.update_one(dec__hits=-1)
post.reload()
self.assertEqual(post.hits, 11)
BlogPost.objects.update(push__tags='mongo')
post.reload()
self.assertTrue('mongo' in post.tags)
@@ -1867,12 +1973,10 @@ class QuerySetTest(unittest.TestCase):
Site.objects(id=s.id).update_one(pull__collaborators__user='Esteban')
self.assertEqual(Site.objects.first().collaborators, [])
def pull_all():
with self.assertRaises(InvalidQueryError):
Site.objects(id=s.id).update_one(
pull_all__collaborators__user=['Ross'])
self.assertRaises(InvalidQueryError, pull_all)
def test_pull_from_nested_embedded(self):
class User(EmbeddedDocument):
@@ -1903,12 +2007,10 @@ class QuerySetTest(unittest.TestCase):
pull__collaborators__unhelpful={'name': 'Frank'})
self.assertEqual(Site.objects.first().collaborators['unhelpful'], [])
def pull_all():
with self.assertRaises(InvalidQueryError):
Site.objects(id=s.id).update_one(
pull_all__collaborators__helpful__name=['Ross'])
self.assertRaises(InvalidQueryError, pull_all)
def test_pull_from_nested_mapfield(self):
class Collaborator(EmbeddedDocument):
@@ -1937,12 +2039,10 @@ class QuerySetTest(unittest.TestCase):
pull__collaborators__unhelpful={'user': 'Frank'})
self.assertEqual(Site.objects.first().collaborators['unhelpful'], [])
def pull_all():
with self.assertRaises(InvalidQueryError):
Site.objects(id=s.id).update_one(
pull_all__collaborators__helpful__user=['Ross'])
self.assertRaises(InvalidQueryError, pull_all)
def test_update_one_pop_generic_reference(self):
class BlogTag(Document):
@@ -2199,6 +2299,21 @@ class QuerySetTest(unittest.TestCase):
a.author.name for a in Author.objects.order_by('-author__age')]
self.assertEqual(names, ['User A', 'User B', 'User C'])
def test_comment(self):
"""Make sure adding a comment to the query works."""
class User(Document):
age = IntField()
with db_ops_tracker() as q:
adult = (User.objects.filter(age__gte=18)
.comment('looking for an adult')
.first())
ops = q.get_ops()
self.assertEqual(len(ops), 1)
op = ops[0]
self.assertEqual(op['query']['$query'], {'age': {'$gte': 18}})
self.assertEqual(op['query']['$comment'], 'looking for an adult')
def test_map_reduce(self):
"""Ensure map/reduce is both mapping and reducing.
"""
@@ -2552,7 +2667,7 @@ class QuerySetTest(unittest.TestCase):
BlogPost(hits=2, tags=['music', 'actors']).save()
def test_assertions(f):
f = dict((key, int(val)) for key, val in f.items())
f = {key: int(val) for key, val in f.items()}
self.assertEqual(
set(['music', 'film', 'actors', 'watch']), set(f.keys()))
self.assertEqual(f['music'], 3)
@@ -2567,7 +2682,7 @@ class QuerySetTest(unittest.TestCase):
# Ensure query is taken into account
def test_assertions(f):
f = dict((key, int(val)) for key, val in f.items())
f = {key: int(val) for key, val in f.items()}
self.assertEqual(set(['music', 'actors', 'watch']), set(f.keys()))
self.assertEqual(f['music'], 2)
self.assertEqual(f['actors'], 1)
@@ -2631,7 +2746,7 @@ class QuerySetTest(unittest.TestCase):
doc.save()
def test_assertions(f):
f = dict((key, int(val)) for key, val in f.items())
f = {key: int(val) for key, val in f.items()}
self.assertEqual(
set(['62-3331-1656', '62-3332-1656']), set(f.keys()))
self.assertEqual(f['62-3331-1656'], 2)
@@ -2645,7 +2760,7 @@ class QuerySetTest(unittest.TestCase):
# Ensure query is taken into account
def test_assertions(f):
f = dict((key, int(val)) for key, val in f.items())
f = {key: int(val) for key, val in f.items()}
self.assertEqual(set(['62-3331-1656']), set(f.keys()))
self.assertEqual(f['62-3331-1656'], 2)
@@ -2752,10 +2867,10 @@ class QuerySetTest(unittest.TestCase):
Test.drop_collection()
for i in xrange(50):
for i in range(50):
Test(val=1).save()
for i in xrange(20):
for i in range(20):
Test(val=2).save()
freqs = Test.objects.item_frequencies(
@@ -2993,7 +3108,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(Foo.objects.distinct("bar"), [bar])
@skip_older_mongodb
@needs_mongodb_v26
def test_text_indexes(self):
class News(Document):
title = StringField()
@@ -3080,7 +3195,7 @@ class QuerySetTest(unittest.TestCase):
'brasil').order_by('$text_score').first()
self.assertEqual(item.get_text_score(), max_text_score)
@skip_older_mongodb
@needs_mongodb_v26
def test_distinct_handles_references_to_alias(self):
register_connection('testdb', 'mongoenginetest2')
@@ -3545,7 +3660,7 @@ class QuerySetTest(unittest.TestCase):
Post.drop_collection()
for i in xrange(10):
for i in range(10):
Post(title="Post %s" % i).save()
self.assertEqual(5, Post.objects.limit(5).skip(5).count(with_limit_and_skip=True))
@@ -3560,7 +3675,7 @@ class QuerySetTest(unittest.TestCase):
pass
MyDoc.drop_collection()
for i in xrange(0, 10):
for i in range(0, 10):
MyDoc().save()
self.assertEqual(MyDoc.objects.count(), 10)
@@ -3616,7 +3731,7 @@ class QuerySetTest(unittest.TestCase):
Number.drop_collection()
for i in xrange(1, 101):
for i in range(1, 101):
t = Number(n=i)
t.save()
@@ -3763,11 +3878,9 @@ class QuerySetTest(unittest.TestCase):
self.assertTrue(a in results)
self.assertTrue(c in results)
def invalid_where():
with self.assertRaises(TypeError):
list(IntPair.objects.where(fielda__gte=3))
self.assertRaises(TypeError, invalid_where)
def test_scalar(self):
class Organization(Document):
@@ -4023,7 +4136,7 @@ class QuerySetTest(unittest.TestCase):
# Test larger slice __repr__
self.Person.objects.delete()
for i in xrange(55):
for i in range(55):
self.Person(name='A%s' % i, age=i).save()
self.assertEqual(self.Person.objects.scalar('name').count(), 55)
@@ -4031,7 +4144,7 @@ class QuerySetTest(unittest.TestCase):
"A0", "%s" % self.Person.objects.order_by('name').scalar('name').first())
self.assertEqual(
"A0", "%s" % self.Person.objects.scalar('name').order_by('name')[0])
if PY3:
if six.PY3:
self.assertEqual("['A1', 'A2']", "%s" % self.Person.objects.order_by(
'age').scalar('name')[1:3])
self.assertEqual("['A51', 'A52']", "%s" % self.Person.objects.order_by(
@@ -4049,7 +4162,7 @@ class QuerySetTest(unittest.TestCase):
pks = self.Person.objects.order_by('age').scalar('pk')[1:3]
names = self.Person.objects.scalar('name').in_bulk(list(pks)).values()
if PY3:
if six.PY3:
expected = "['A1', 'A2']"
else:
expected = "[u'A1', u'A2']"
@@ -4405,7 +4518,7 @@ class QuerySetTest(unittest.TestCase):
name = StringField()
Person.drop_collection()
for i in xrange(100):
for i in range(100):
Person(name="No: %s" % i).save()
with query_counter() as q:
@@ -4436,7 +4549,7 @@ class QuerySetTest(unittest.TestCase):
name = StringField()
Person.drop_collection()
for i in xrange(100):
for i in range(100):
Person(name="No: %s" % i).save()
with query_counter() as q:
@@ -4480,7 +4593,7 @@ class QuerySetTest(unittest.TestCase):
fields = DictField()
Noddy.drop_collection()
for i in xrange(100):
for i in range(100):
noddy = Noddy()
for j in range(20):
noddy.fields["key" + str(j)] = "value " + str(j)
@@ -4492,7 +4605,9 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(counter, 100)
self.assertEqual(len(list(docs)), 100)
self.assertRaises(TypeError, lambda: len(docs))
with self.assertRaises(TypeError):
len(docs)
with query_counter() as q:
self.assertEqual(q, 0)
@@ -4681,7 +4796,7 @@ class QuerySetTest(unittest.TestCase):
name = StringField()
Person.drop_collection()
for i in xrange(100):
for i in range(100):
Person(name="No: %s" % i).save()
with query_counter() as q:
@@ -4755,6 +4870,7 @@ class QuerySetTest(unittest.TestCase):
self.assertTrue(Person.objects._has_data(),
'Cursor has data and returned False')
@needs_mongodb_v26
def test_queryset_aggregation_framework(self):
class Person(Document):
name = StringField()
@@ -4789,26 +4905,22 @@ class QuerySetTest(unittest.TestCase):
{'_id': p1.pk, 'name': "ISABELLA LUANNA"}
])
data = Person.objects(
age__gte=17, age__lte=40).order_by('-age').aggregate(
{'$group': {
'_id': None,
'total': {'$sum': 1},
'avg': {'$avg': '$age'}
}
}
)
data = Person.objects(age__gte=17, age__lte=40).order_by('-age').aggregate({
'$group': {
'_id': None,
'total': {'$sum': 1},
'avg': {'$avg': '$age'}
}
})
self.assertEqual(list(data), [
{'_id': None, 'avg': 29, 'total': 2}
])
def test_delete_count(self):
[self.Person(name="User {0}".format(i), age=i * 10).save() for i in xrange(1, 4)]
[self.Person(name="User {0}".format(i), age=i * 10).save() for i in range(1, 4)]
self.assertEqual(self.Person.objects().delete(), 3) # test ordinary QuerySey delete count
[self.Person(name="User {0}".format(i), age=i * 10).save() for i in xrange(1, 4)]
[self.Person(name="User {0}".format(i), age=i * 10).save() for i in range(1, 4)]
self.assertEqual(self.Person.objects().skip(1).delete(), 2) # test Document delete with existing documents
@@ -4817,12 +4929,14 @@ class QuerySetTest(unittest.TestCase):
def test_max_time_ms(self):
# 778: max_time_ms can get only int or None as input
self.assertRaises(TypeError, self.Person.objects(name="name").max_time_ms, "not a number")
self.assertRaises(TypeError,
self.Person.objects(name="name").max_time_ms,
'not a number')
def test_subclass_field_query(self):
class Animal(Document):
is_mamal = BooleanField()
meta = dict(allow_inheritance=True)
meta = {'allow_inheritance': True}
class Cat(Animal):
whiskers_length = FloatField()
@@ -4838,27 +4952,94 @@ class QuerySetTest(unittest.TestCase):
self.assertEquals(Animal.objects(folded_ears=True).count(), 1)
self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1)
def test_loop_via_invalid_id_does_not_crash(self):
def test_loop_over_invalid_id_does_not_crash(self):
class Person(Document):
name = StringField()
Person.objects.delete()
Person._get_collection().update({"name": "a"}, {"$set": {"_id": ""}}, upsert=True)
Person.drop_collection()
Person._get_collection().insert({'name': 'a', 'id': ''})
for p in Person.objects():
self.assertEqual(p.name, 'a')
def test_last_field_name_like_operator(self):
class EmbeddedItem(EmbeddedDocument):
type = StringField()
def test_len_during_iteration(self):
"""Tests that calling len on a queyset during iteration doesn't
stop paging.
"""
class Data(Document):
pass
class Doc(Document):
item = EmbeddedDocumentField(EmbeddedItem)
for i in range(300):
Data().save()
Doc.drop_collection()
records = Data.objects.limit(250)
doc = Doc(item=EmbeddedItem(type="axe"))
doc.save()
# This should pull all 250 docs from mongo and populate the result
# cache
len(records)
self.assertEqual(1, Doc.objects(item__type__="axe").count())
# Assert that iterating over documents in the qs touches every
# document even if we call len(qs) midway through the iteration.
for i, r in enumerate(records):
if i == 58:
len(records)
self.assertEqual(i, 249)
# Assert the same behavior is true even if we didn't pre-populate the
# result cache.
records = Data.objects.limit(250)
for i, r in enumerate(records):
if i == 58:
len(records)
self.assertEqual(i, 249)
def test_iteration_within_iteration(self):
"""You should be able to reliably iterate over all the documents
in a given queryset even if there are multiple iterations of it
happening at the same time.
"""
class Data(Document):
pass
for i in range(300):
Data().save()
qs = Data.objects.limit(250)
for i, doc in enumerate(qs):
for j, doc2 in enumerate(qs):
pass
self.assertEqual(i, 249)
self.assertEqual(j, 249)
def test_in_operator_on_non_iterable(self):
"""Ensure that using the `__in` operator on a non-iterable raises an
error.
"""
class User(Document):
name = StringField()
class BlogPost(Document):
content = StringField()
authors = ListField(ReferenceField(User))
User.drop_collection()
BlogPost.drop_collection()
author = User.objects.create(name='Test User')
post = BlogPost.objects.create(content='Had a good coffee today...',
authors=[author])
# Make sure using `__in` with a list works
blog_posts = BlogPost.objects(authors__in=[author])
self.assertEqual(list(blog_posts), [post])
# Using `__in` with a non-iterable should raise a TypeError
self.assertRaises(TypeError, BlogPost.objects(authors__in=author.pk).count)
# Using `__in` with a `Document` (which is seemingly iterable but not
# in a way we'd expect) should raise a TypeError, too
self.assertRaises(TypeError, BlogPost.objects(authors__in=author).count)
if __name__ == '__main__':

View File

@@ -238,7 +238,8 @@ class TransformTest(unittest.TestCase):
box = [(35.0, -125.0), (40.0, -100.0)]
# I *meant* to execute location__within_box=box
events = Event.objects(location__within=box)
self.assertRaises(InvalidQueryError, lambda: events.count())
with self.assertRaises(InvalidQueryError):
events.count()
if __name__ == '__main__':

View File

@@ -185,7 +185,7 @@ class QTest(unittest.TestCase):
x = IntField()
TestDoc.drop_collection()
for i in xrange(1, 101):
for i in range(1, 101):
t = TestDoc(x=i)
t.save()
@@ -268,14 +268,13 @@ class QTest(unittest.TestCase):
self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3)
# Test invalid query objs
def wrong_query_objs():
with self.assertRaises(InvalidQueryError):
self.Person.objects('user1')
def wrong_query_objs_filter():
self.Person.objects('user1')
# filter should fail, too
with self.assertRaises(InvalidQueryError):
self.Person.objects.filter('user1')
self.assertRaises(InvalidQueryError, wrong_query_objs)
self.assertRaises(InvalidQueryError, wrong_query_objs_filter)
def test_q_regex(self):
"""Ensure that Q objects can be queried using regexes.

View File

@@ -1,9 +1,6 @@
import sys
import datetime
from pymongo.errors import OperationFailure
sys.path[0:0] = [""]
try:
import unittest2 as unittest
except ImportError:
@@ -19,7 +16,8 @@ from mongoengine import (
)
from mongoengine.python_support import IS_PYMONGO_3
import mongoengine.connection
from mongoengine.connection import get_db, get_connection, ConnectionError
from mongoengine.connection import (MongoEngineConnectionError, get_db,
get_connection)
def get_tz_awareness(connection):
@@ -37,8 +35,7 @@ class ConnectionTest(unittest.TestCase):
mongoengine.connection._dbs = {}
def test_connect(self):
"""Ensure that the connect() method works properly.
"""
"""Ensure that the connect() method works properly."""
connect('mongoenginetest')
conn = get_connection()
@@ -148,8 +145,7 @@ class ConnectionTest(unittest.TestCase):
self.assertEqual(expected_connection, actual_connection)
def test_connect_uri(self):
"""Ensure that the connect() method works properly with uri's
"""
"""Ensure that the connect() method works properly with URIs."""
c = connect(db='mongoenginetest', alias='admin')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
@@ -159,7 +155,10 @@ class ConnectionTest(unittest.TestCase):
c.mongoenginetest.add_user("username", "password")
if not IS_PYMONGO_3:
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
self.assertRaises(
MongoEngineConnectionError, connect, 'testdb_uri_bad',
host='mongodb://test:password@localhost'
)
connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest')
@@ -174,19 +173,9 @@ class ConnectionTest(unittest.TestCase):
c.mongoenginetest.system.users.remove({})
def test_connect_uri_without_db(self):
"""Ensure connect() method works properly with uri's without database_name
"""Ensure connect() method works properly if the URI doesn't
include a database name.
"""
c = connect(db='mongoenginetest', alias='admin')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
c.admin.add_user("admin", "password")
c.admin.authenticate("admin", "password")
c.mongoenginetest.add_user("username", "password")
if not IS_PYMONGO_3:
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
connect("mongoenginetest", host='mongodb://localhost/')
conn = get_connection()
@@ -196,13 +185,35 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
def test_connect_uri_default_db(self):
"""Ensure connect() defaults to the right database name if
the URI and the database_name don't explicitly specify it.
"""
connect(host='mongodb://localhost/')
conn = get_connection()
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'test')
def test_uri_without_credentials_doesnt_override_conn_settings(self):
"""Ensure connect() uses the username & password params if the URI
doesn't explicitly specify them.
"""
c = connect(host='mongodb://localhost/mongoenginetest',
username='user',
password='pass')
# OperationFailure means that mongoengine attempted authentication
# w/ the provided username/password and failed - that's the desired
# behavior. If the MongoDB URI would override the credentials
self.assertRaises(OperationFailure, get_db)
def test_connect_uri_with_authsource(self):
"""Ensure that the connect() method works well with
the option `authSource` in URI.
This feature was introduced in MongoDB 2.4 and removed in 2.6
"""Ensure that the connect() method works well with `authSource`
option in the URI.
"""
# Create users
c = connect('mongoenginetest')
@@ -211,36 +222,38 @@ class ConnectionTest(unittest.TestCase):
# Authentication fails without "authSource"
if IS_PYMONGO_3:
test_conn = connect('mongoenginetest', alias='test1',
host='mongodb://username2:password@localhost/mongoenginetest')
test_conn = connect(
'mongoenginetest', alias='test1',
host='mongodb://username2:password@localhost/mongoenginetest'
)
self.assertRaises(OperationFailure, test_conn.server_info)
else:
self.assertRaises(
ConnectionError, connect, 'mongoenginetest', alias='test1',
MongoEngineConnectionError,
connect, 'mongoenginetest', alias='test1',
host='mongodb://username2:password@localhost/mongoenginetest'
)
self.assertRaises(ConnectionError, get_db, 'test1')
self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
# Authentication succeeds with "authSource"
connect(
authd_conn = connect(
'mongoenginetest', alias='test2',
host=('mongodb://username2:password@localhost/'
'mongoenginetest?authSource=admin')
)
# This will fail starting from MongoDB 2.6+
db = get_db('test2')
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest')
# Clear all users
c.admin.system.users.remove({})
authd_conn.admin.system.users.remove({})
def test_register_connection(self):
"""Ensure that connections with different aliases may be registered.
"""
register_connection('testdb', 'mongoenginetest2')
self.assertRaises(ConnectionError, get_connection)
self.assertRaises(MongoEngineConnectionError, get_connection)
conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
@@ -257,8 +270,7 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
def test_connection_kwargs(self):
"""Ensure that connection kwargs get passed to pymongo.
"""
"""Ensure that connection kwargs get passed to pymongo."""
connect('mongoenginetest', alias='t1', tz_aware=True)
conn = get_connection('t1')
@@ -268,6 +280,77 @@ class ConnectionTest(unittest.TestCase):
conn = get_connection('t2')
self.assertFalse(get_tz_awareness(conn))
def test_connection_pool_via_kwarg(self):
"""Ensure we can specify a max connection pool size using
a connection kwarg.
"""
# Use "max_pool_size" or "maxpoolsize" depending on PyMongo version
# (former was changed to the latter as described in
# https://jira.mongodb.org/browse/PYTHON-854).
# TODO remove once PyMongo < 3.0 support is dropped
if pymongo.version_tuple[0] >= 3:
pool_size_kwargs = {'maxpoolsize': 100}
else:
pool_size_kwargs = {'max_pool_size': 100}
conn = connect('mongoenginetest', alias='max_pool_size_via_kwarg', **pool_size_kwargs)
self.assertEqual(conn.max_pool_size, 100)
def test_connection_pool_via_uri(self):
"""Ensure we can specify a max connection pool size using
an option in a connection URI.
"""
if pymongo.version_tuple[0] == 2 and pymongo.version_tuple[1] < 9:
raise SkipTest('maxpoolsize as a URI option is only supported in PyMongo v2.9+')
conn = connect(host='mongodb://localhost/test?maxpoolsize=100', alias='max_pool_size_via_uri')
self.assertEqual(conn.max_pool_size, 100)
def test_write_concern(self):
"""Ensure write concern can be specified in connect() via
a kwarg or as part of the connection URI.
"""
conn1 = connect(alias='conn1', host='mongodb://localhost/testing?w=1&j=true')
conn2 = connect('testing', alias='conn2', w=1, j=True)
if IS_PYMONGO_3:
self.assertEqual(conn1.write_concern.document, {'w': 1, 'j': True})
self.assertEqual(conn2.write_concern.document, {'w': 1, 'j': True})
else:
self.assertEqual(dict(conn1.write_concern), {'w': 1, 'j': True})
self.assertEqual(dict(conn2.write_concern), {'w': 1, 'j': True})
def test_connect_with_replicaset_via_uri(self):
"""Ensure connect() works when specifying a replicaSet via the
MongoDB URI.
"""
if IS_PYMONGO_3:
c = connect(host='mongodb://localhost/test?replicaSet=local-rs')
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'test')
else:
# PyMongo < v3.x raises an exception:
# "localhost:27017 is not a member of replica set local-rs"
with self.assertRaises(MongoEngineConnectionError):
c = connect(host='mongodb://localhost/test?replicaSet=local-rs')
def test_connect_with_replicaset_via_kwargs(self):
"""Ensure connect() works when specifying a replicaSet via the
connection kwargs
"""
if IS_PYMONGO_3:
c = connect(replicaset='local-rs')
self.assertEqual(c._MongoClient__options.replica_set_name,
'local-rs')
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'test')
else:
# PyMongo < v3.x raises an exception:
# "localhost:27017 is not a member of replica set local-rs"
with self.assertRaises(MongoEngineConnectionError):
c = connect(replicaset='local-rs')
def test_datetime(self):
connect('mongoenginetest', tz_aware=True)
d = datetime.datetime(2010, 5, 5, tzinfo=utc)

View File

@@ -1,5 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import *
@@ -79,7 +77,7 @@ class ContextManagersTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
User(name='user %s' % i).save()
user = User.objects.first()
@@ -117,7 +115,7 @@ class ContextManagersTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
User(name='user %s' % i).save()
user = User.objects.first()
@@ -195,7 +193,7 @@ class ContextManagersTest(unittest.TestCase):
with query_counter() as q:
self.assertEqual(0, q)
for i in xrange(1, 51):
for i in range(1, 51):
db.test.find({}).count()
self.assertEqual(50, q)

View File

@@ -1,5 +1,6 @@
import unittest
from mongoengine.base.datastructures import StrictDict, SemiStrictDict
from mongoengine.base.datastructures import StrictDict, SemiStrictDict
class TestStrictDict(unittest.TestCase):
@@ -13,9 +14,18 @@ class TestStrictDict(unittest.TestCase):
d = self.dtype(a=1, b=1, c=1)
self.assertEqual((d.a, d.b, d.c), (1, 1, 1))
def test_repr(self):
d = self.dtype(a=1, b=2, c=3)
self.assertEqual(repr(d), '{"a": 1, "b": 2, "c": 3}')
# make sure quotes are escaped properly
d = self.dtype(a='"', b="'", c="")
self.assertEqual(repr(d), '{"a": \'"\', "b": "\'", "c": \'\'}')
def test_init_fails_on_nonexisting_attrs(self):
self.assertRaises(AttributeError, lambda: self.dtype(a=1, b=2, d=3))
with self.assertRaises(AttributeError):
self.dtype(a=1, b=2, d=3)
def test_eq(self):
d = self.dtype(a=1, b=1, c=1)
dd = self.dtype(a=1, b=1, c=1)
@@ -24,7 +34,7 @@ class TestStrictDict(unittest.TestCase):
g = self.strict_dict_class(("a", "b", "c", "d"))(a=1, b=1, c=1, d=1)
h = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=1)
i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2)
self.assertEqual(d, dd)
self.assertNotEqual(d, e)
self.assertNotEqual(d, f)
@@ -37,20 +47,18 @@ class TestStrictDict(unittest.TestCase):
d = self.dtype()
d.a = 1
self.assertEqual(d.a, 1)
self.assertRaises(AttributeError, lambda: d.b)
self.assertRaises(AttributeError, getattr, d, 'b')
def test_setattr_raises_on_nonexisting_attr(self):
d = self.dtype()
def _f():
with self.assertRaises(AttributeError):
d.x = 1
self.assertRaises(AttributeError, _f)
def test_setattr_getattr_special(self):
d = self.strict_dict_class(["items"])
d.items = 1
self.assertEqual(d.items, 1)
def test_get(self):
d = self.dtype(a=1)
self.assertEqual(d.get('a'), 1)
@@ -88,7 +96,7 @@ class TestSemiSrictDict(TestStrictDict):
def test_init_succeeds_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
self.assertEqual((d.a, d.b, d.c, d.x), (1, 1, 1, 2))
def test_iter_with_nonexisting_attrs(self):
d = self.dtype(a=1, b=1, c=1, x=2)
self.assertEqual(list(d), ['a', 'b', 'c', 'x'])

View File

@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from bson import DBRef, ObjectId
from collections import OrderedDict
from mongoengine import *
from mongoengine.connection import get_db
from mongoengine.context_managers import query_counter
from mongoengine.python_support import IS_PYMONGO_3
from mongoengine.base import TopLevelDocumentMetaclass
if IS_PYMONGO_3:
from bson import CodecOptions
class FieldTest(unittest.TestCase):
@@ -32,7 +35,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
user = User(name='user %s' % i)
user.save()
@@ -90,7 +93,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
user = User(name='user %s' % i)
user.save()
@@ -162,7 +165,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 26):
for i in range(1, 26):
user = User(name='user %s' % i)
user.save()
@@ -440,7 +443,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i)
a.save()
@@ -531,7 +534,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i)
a.save()
@@ -614,15 +617,15 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
user = User(name='user %s' % i)
user.save()
members.append(user)
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
with query_counter() as q:
@@ -687,7 +690,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i)
a.save()
@@ -699,9 +702,9 @@ class FieldTest(unittest.TestCase):
members += [a, b, c]
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
with query_counter() as q:
@@ -783,16 +786,16 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i)
a.save()
members += [a]
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
with query_counter() as q:
@@ -866,7 +869,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i)
a.save()
@@ -878,9 +881,9 @@ class FieldTest(unittest.TestCase):
members += [a, b, c]
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
group = Group(members=dict([(str(u.id), u) for u in members]))
group = Group(members={str(u.id): u for u in members})
group.save()
with query_counter() as q:
@@ -1103,7 +1106,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
User(name='user %s' % i).save()
Group(name="Test", members=User.objects).save()
@@ -1132,7 +1135,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection()
Group.drop_collection()
for i in xrange(1, 51):
for i in range(1, 51):
User(name='user %s' % i).save()
Group(name="Test", members=User.objects).save()
@@ -1169,7 +1172,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection()
members = []
for i in xrange(1, 51):
for i in range(1, 51):
a = UserA(name='User A %s' % i).save()
b = UserB(name='User B %s' % i).save()
c = UserC(name='User C %s' % i).save()
@@ -1289,5 +1292,70 @@ class FieldTest(unittest.TestCase):
self.assertEqual(q, 2)
def test_dynamic_field_dereference(self):
class Merchandise(Document):
name = StringField()
price = IntField()
class Store(Document):
merchandises = DynamicField()
Merchandise.drop_collection()
Store.drop_collection()
merchandises = {
'#1': Merchandise(name='foo', price=100).save(),
'#2': Merchandise(name='bar', price=120).save(),
'#3': Merchandise(name='baz', price=110).save(),
}
Store(merchandises=merchandises).save()
store = Store.objects().first()
for obj in store.merchandises.values():
self.assertFalse(isinstance(obj, Merchandise))
store.select_related()
for obj in store.merchandises.values():
self.assertTrue(isinstance(obj, Merchandise))
def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self):
# This is because 'codec_options' is supported on pymongo3 or later
if IS_PYMONGO_3:
class OrderedDocument(Document):
my_metaclass = TopLevelDocumentMetaclass
__metaclass__ = TopLevelDocumentMetaclass
@classmethod
def _get_collection(cls):
collection = super(OrderedDocument, cls)._get_collection()
opts = CodecOptions(document_class=OrderedDict)
return collection.with_options(codec_options=opts)
class Merchandise(Document):
name = StringField()
price = IntField()
class Store(OrderedDocument):
merchandises = DynamicField(container_class=OrderedDict)
Merchandise.drop_collection()
Store.drop_collection()
merchandises = OrderedDict()
merchandises['#1'] = Merchandise(name='foo', price=100).save()
merchandises['#2'] = Merchandise(name='bar', price=120).save()
merchandises['#3'] = Merchandise(name='baz', price=110).save()
Store(merchandises=merchandises).save()
store = Store.objects().first()
store.select_related()
# confirms that the load data order is same with the one at storing
self.assertTrue(type(store.merchandises), OrderedDict)
self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3')
if __name__ == '__main__':
unittest.main()

View File

@@ -1,6 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest
from pymongo import ReadPreference
@@ -18,7 +15,7 @@ else:
import mongoengine
from mongoengine import *
from mongoengine.connection import ConnectionError
from mongoengine.connection import MongoEngineConnectionError
class ConnectionTest(unittest.TestCase):
@@ -41,7 +38,7 @@ class ConnectionTest(unittest.TestCase):
conn = connect(db='mongoenginetest',
host="mongodb://localhost/mongoenginetest?replicaSet=rs",
read_preference=READ_PREF)
except ConnectionError, e:
except MongoEngineConnectionError as e:
return
if not isinstance(conn, CONN_CLASS):

View File

@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest
from mongoengine import *

78
tests/utils.py Normal file
View File

@@ -0,0 +1,78 @@
import unittest
from nose.plugins.skip import SkipTest
from mongoengine import connect
from mongoengine.connection import get_db, get_connection
from mongoengine.python_support import IS_PYMONGO_3
MONGO_TEST_DB = 'mongoenginetest'
class MongoDBTestCase(unittest.TestCase):
"""Base class for tests that need a mongodb connection
db is being dropped automatically
"""
@classmethod
def setUpClass(cls):
cls._connection = connect(db=MONGO_TEST_DB)
cls._connection.drop_database(MONGO_TEST_DB)
cls.db = get_db()
@classmethod
def tearDownClass(cls):
cls._connection.drop_database(MONGO_TEST_DB)
def get_mongodb_version():
"""Return the version tuple of the MongoDB server that the default
connection is connected to.
"""
return tuple(get_connection().server_info()['versionArray'])
def _decorated_with_ver_requirement(func, ver_tuple):
"""Return a given function decorated with the version requirement
for a particular MongoDB version tuple.
"""
def _inner(*args, **kwargs):
mongodb_ver = get_mongodb_version()
if mongodb_ver >= ver_tuple:
return func(*args, **kwargs)
raise SkipTest('Needs MongoDB v{}+'.format(
'.'.join([str(v) for v in ver_tuple])
))
_inner.__name__ = func.__name__
_inner.__doc__ = func.__doc__
return _inner
def needs_mongodb_v26(func):
"""Raise a SkipTest exception if we're working with MongoDB version
lower than v2.6.
"""
return _decorated_with_ver_requirement(func, (2, 6))
def needs_mongodb_v3(func):
"""Raise a SkipTest exception if we're working with MongoDB version
lower than v3.0.
"""
return _decorated_with_ver_requirement(func, (3, 0))
def skip_pymongo3(f):
"""Raise a SkipTest exception if we're running a test against
PyMongo v3.x.
"""
def _inner(*args, **kwargs):
if IS_PYMONGO_3:
raise SkipTest("Useless with PyMongo 3+")
return f(*args, **kwargs)
_inner.__name__ = f.__name__
_inner.__doc__ = f.__doc__
return _inner

13
tox.ini
View File

@@ -1,5 +1,5 @@
[tox]
envlist = {py26,py27,py33,py34,py35,pypy,pypy3}-{mg27,mg28},flake8
envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30}
[testenv]
commands =
@@ -7,16 +7,7 @@ commands =
deps =
nose
mg27: PyMongo<2.8
mg28: PyMongo>=2.8,<3.0
mg28: PyMongo>=2.8,<2.9
mg30: PyMongo>=3.0
mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master
setenv =
PYTHON_EGG_CACHE = {envdir}/python-eggs
passenv = windir
[testenv:flake8]
deps =
flake8
flake8-import-order
commands =
flake8