Compare commits
72 Commits
v0.10.7
...
test-repli
Author | SHA1 | Date | |
---|---|---|---|
|
2c247869f0 | ||
|
627cf90de0 | ||
|
2bedb36d7f | ||
|
e93a95d0cb | ||
|
3f31666796 | ||
|
3fe8031cf3 | ||
|
b27c7ce11b | ||
|
ed34c2ca68 | ||
|
3ca2e953fb | ||
|
d8a7328365 | ||
|
f33cd625bf | ||
|
80530bb13c | ||
|
affc12df4b | ||
|
4eedf00025 | ||
|
e5acbcc0dd | ||
|
1b6743ee53 | ||
|
b5fb82d95d | ||
|
193aa4e1f2 | ||
|
ebd34427c7 | ||
|
3d75573889 | ||
|
c6240ca415 | ||
|
2ee8984b44 | ||
|
b7ec587e5b | ||
|
47c58bce2b | ||
|
96e95ac533 | ||
|
b013a065f7 | ||
|
74b37d11cf | ||
|
c6cc013617 | ||
|
f4e1d80a87 | ||
|
91dad4060f | ||
|
e07cb82c15 | ||
|
2770cec187 | ||
|
5c3928190a | ||
|
9f4b04ea0f | ||
|
96d20756ca | ||
|
b8454c7f5b | ||
|
c84f703f92 | ||
|
57c2e867d8 | ||
|
553f496d84 | ||
|
b1d8aca46a | ||
|
8e884fd3ea | ||
|
76524b7498 | ||
|
65914fb2b2 | ||
|
a4d0da0085 | ||
|
c9d496e9a0 | ||
|
88a951ba4f | ||
|
403ceb19dc | ||
|
835d3c3d18 | ||
|
3135b456be | ||
|
0be6d3661a | ||
|
6f5f5b4711 | ||
|
c6c5f85abb | ||
|
7b860f7739 | ||
|
e28804c03a | ||
|
1b9432824b | ||
|
3b71a6b5c5 | ||
|
7ce8768c19 | ||
|
25e0f12976 | ||
|
f168682a68 | ||
|
d25058a46d | ||
|
4d0c092d9f | ||
|
15714ef855 | ||
|
eb743beaa3 | ||
|
0007535a46 | ||
|
8391af026c | ||
|
800f656dcf | ||
|
088c5f49d9 | ||
|
d8d98b6143 | ||
|
02fb3b9315 | ||
|
4f87db784e | ||
|
7e6287b925 | ||
|
999cdfd997 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,4 +14,6 @@ env/
|
|||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
tests/test_bugfix.py
|
tests/test_bugfix.py
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
venv
|
||||||
|
venv3
|
||||||
|
23
.install_mongodb_on_travis.sh
Normal file
23
.install_mongodb_on_travis.sh
Normal 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
22
.landscape.yml
Normal 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
|
72
.travis.yml
72
.travis.yml
@@ -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
|
language: python
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- '2.6'
|
- 2.7
|
||||||
- '2.7'
|
- 3.5
|
||||||
- '3.3'
|
|
||||||
- '3.4'
|
|
||||||
- '3.5'
|
|
||||||
- pypy
|
- pypy
|
||||||
- pypy3
|
- pypy3
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- PYMONGO=2.7
|
- MONGODB=2.6 PYMONGO=2.7
|
||||||
- PYMONGO=2.8
|
- MONGODB=2.6 PYMONGO=2.8
|
||||||
- PYMONGO=3.0
|
- MONGODB=2.6 PYMONGO=3.0
|
||||||
- PYMONGO=dev
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
|
# Finish the build as soon as one job fails
|
||||||
fast_finish: true
|
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:
|
before_install:
|
||||||
- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
|
- bash .install_mongodb_on_travis.sh
|
||||||
- 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
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
|
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
|
||||||
@@ -31,33 +50,52 @@ install:
|
|||||||
python-tk
|
python-tk
|
||||||
- travis_retry pip install --upgrade pip
|
- travis_retry pip install --upgrade pip
|
||||||
- travis_retry pip install coveralls
|
- 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 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 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
|
- 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
|
# Run flake8 for py27
|
||||||
before_script:
|
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:
|
script:
|
||||||
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
|
- 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:
|
notifications:
|
||||||
irc: irc.freenode.org#mongoengine
|
irc: irc.freenode.org#mongoengine
|
||||||
|
|
||||||
|
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
- /^v.*$/
|
- /^v.*$/
|
||||||
|
|
||||||
|
# Whenever a new release is created via GitHub, publish it on PyPI.
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: the_drow
|
user: the_drow
|
||||||
password:
|
password:
|
||||||
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
|
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:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
repo: MongoEngine/mongoengine
|
repo: MongoEngine/mongoengine
|
||||||
|
condition: "$PYMONGO = 3.0"
|
||||||
|
python: 2.7
|
||||||
|
1
AUTHORS
1
AUTHORS
@@ -242,3 +242,4 @@ that much better:
|
|||||||
* xiaost7 (https://github.com/xiaost7)
|
* xiaost7 (https://github.com/xiaost7)
|
||||||
* Victor Varvaryuk
|
* Victor Varvaryuk
|
||||||
* Stanislav Kaledin (https://github.com/sallyruthstruik)
|
* Stanislav Kaledin (https://github.com/sallyruthstruik)
|
||||||
|
* Dmitry Yantsen (https://github.com/mrTable)
|
||||||
|
@@ -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?state=open>`_ or `create one
|
||||||
<https://github.com/MongoEngine/mongoengine/issues>`_ for your specific
|
<https://github.com/MongoEngine/mongoengine/issues>`_ for your specific
|
||||||
issue or feature request. That way you avoid working on something
|
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>`
|
post to the `user group <http://groups.google.com/group/mongoengine-users>`
|
||||||
|
|
||||||
Supported Interpreters
|
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.
|
features not supported by all interpreters can not be used.
|
||||||
Please also ensure that your code is properly converted by
|
Please also ensure that your code is properly converted by
|
||||||
`2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support.
|
`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/>`_
|
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.
|
including 4 space indents. When possible we try to stick to 79 character line
|
||||||
However, screens got bigger and an ORM has a strong focus on readability and
|
limits. However, screens got bigger and an ORM has a strong focus on
|
||||||
if it can help, we accept 119 as maximum line length, in a similar way as
|
readability and if it can help, we accept 119 as maximum line length, in a
|
||||||
`django does <https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
|
similar way as `django does
|
||||||
|
<https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
All tests are run on `Travis <http://travis-ci.org/MongoEngine/mongoengine>`_
|
All tests are run on `Travis <http://travis-ci.org/MongoEngine/mongoengine>`_
|
||||||
and any pull requests are automatically tested by Travis. Any pull requests
|
and any pull requests are automatically tested. Any pull requests without
|
||||||
without tests will take longer to be integrated and might be refused.
|
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
|
how to fix it, it will be easier for other people to work on it and it may get
|
||||||
fixed faster.
|
fixed faster.
|
||||||
|
|
||||||
@@ -49,13 +50,18 @@ General Guidelines
|
|||||||
------------------
|
------------------
|
||||||
|
|
||||||
- Avoid backward breaking changes if at all possible.
|
- 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 inline documentation for new classes and methods.
|
||||||
- Write tests and make sure they pass (make sure you have a mongod
|
- Write tests and make sure they pass (make sure you have a mongod
|
||||||
running on the default port, then execute ``python setup.py nosetests``
|
running on the default port, then execute ``python setup.py nosetests``
|
||||||
from the cmd line to run the test suite).
|
from the cmd line to run the test suite).
|
||||||
- Ensure tests pass on every Python and PyMongo versions.
|
- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
|
||||||
You can test on these versions locally by executing ``tox``
|
You can test various Python and PyMongo versions locally by executing
|
||||||
- Add enhancements or problematic bug fixes to docs/changelog.rst
|
``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 :)
|
- Add yourself to AUTHORS :)
|
||||||
|
|
||||||
Documentation
|
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>`_.
|
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>`_
|
You might also use the github `Edit <https://github.com/blog/844-forking-with-the-edit-button>`_
|
||||||
button.
|
button.
|
||||||
|
|
||||||
|
If you want to test your documentation changes locally, you need to install
|
||||||
|
the ``sphinx`` package.
|
||||||
|
79
README.rst
79
README.rst
@@ -4,7 +4,7 @@ MongoEngine
|
|||||||
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
|
:Info: MongoEngine is an ORM-like layer on top of PyMongo.
|
||||||
:Repository: https://github.com/MongoEngine/mongoengine
|
:Repository: https://github.com/MongoEngine/mongoengine
|
||||||
:Author: Harry Marr (http://github.com/hmarr)
|
: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
|
.. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master
|
||||||
:target: https://travis-ci.org/MongoEngine/mongoengine
|
:target: https://travis-ci.org/MongoEngine/mongoengine
|
||||||
@@ -19,32 +19,42 @@ MongoEngine
|
|||||||
About
|
About
|
||||||
=====
|
=====
|
||||||
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
|
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
|
||||||
Documentation available at https://mongoengine-odm.readthedocs.io - there is currently
|
Documentation is available at https://mongoengine-odm.readthedocs.io - there
|
||||||
a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_, a `user guide
|
is currently a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_,
|
||||||
<https://mongoengine-odm.readthedocs.io/guide/index.html>`_ and an `API reference
|
a `user guide <https://mongoengine-odm.readthedocs.io/guide/index.html>`_, and
|
||||||
<https://mongoengine-odm.readthedocs.io/apireference.html>`_.
|
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
|
Installation
|
||||||
============
|
============
|
||||||
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
|
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``.
|
`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 may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||||
you can use ``easy_install -U mongoengine``. Otherwise, you can download the
|
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
|
source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and run ``python
|
||||||
setup.py install``.
|
setup.py install``.
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
============
|
============
|
||||||
- pymongo>=2.7.1
|
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
|
||||||
- sphinx (optional - for documentation generation)
|
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
|
- dateutil>=2.1.0
|
||||||
|
|
||||||
.. note
|
If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
|
||||||
MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: PyMongo 3.0.1
|
|
||||||
|
- Pillow>=2.0.0
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
========
|
========
|
||||||
@@ -52,10 +62,14 @@ Some simple examples of what MongoEngine code looks like:
|
|||||||
|
|
||||||
.. code :: python
|
.. code :: python
|
||||||
|
|
||||||
|
from mongoengine import *
|
||||||
|
connect('mydb')
|
||||||
|
|
||||||
class BlogPost(Document):
|
class BlogPost(Document):
|
||||||
title = StringField(required=True, max_length=200)
|
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))
|
tags = ListField(StringField(max_length=50))
|
||||||
|
meta = {'allow_inheritance': True}
|
||||||
|
|
||||||
class TextPost(BlogPost):
|
class TextPost(BlogPost):
|
||||||
content = StringField(required=True)
|
content = StringField(required=True)
|
||||||
@@ -83,27 +97,28 @@ Some simple examples of what MongoEngine code looks like:
|
|||||||
... print
|
... print
|
||||||
...
|
...
|
||||||
|
|
||||||
>>> len(BlogPost.objects)
|
# Count all blog posts and its subtypes
|
||||||
|
>>> BlogPost.objects.count()
|
||||||
2
|
2
|
||||||
>>> len(TextPost.objects)
|
>>> TextPost.objects.count()
|
||||||
1
|
1
|
||||||
>>> len(LinkPost.objects)
|
>>> LinkPost.objects.count()
|
||||||
1
|
1
|
||||||
|
|
||||||
# Find tagged posts
|
# Count tagged posts
|
||||||
>>> len(BlogPost.objects(tags='mongoengine'))
|
>>> BlogPost.objects(tags='mongoengine').count()
|
||||||
2
|
2
|
||||||
>>> len(BlogPost.objects(tags='mongodb'))
|
>>> BlogPost.objects(tags='mongodb').count()
|
||||||
1
|
1
|
||||||
|
|
||||||
Tests
|
Tests
|
||||||
=====
|
=====
|
||||||
To run the test suite, ensure you are running a local instance of MongoDB on
|
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,
|
To run the test suite on every supported Python and PyMongo version, you can
|
||||||
you can use ``tox``.
|
use ``tox``. You'll need to make sure you have each supported Python version
|
||||||
tox and each supported Python version should be installed in your environment:
|
installed in your environment and then:
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
@@ -112,13 +127,16 @@ tox and each supported Python version should be installed in your environment:
|
|||||||
# Run the test suites
|
# Run the test suites
|
||||||
$ tox
|
$ tox
|
||||||
|
|
||||||
If you wish to run one single or selected tests, use the nosetest convention. It will find the folder,
|
If you wish to run a subset of tests, use the nosetests convention:
|
||||||
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.
|
|
||||||
|
|
||||||
.. code-block:: shell
|
.. 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
|
Community
|
||||||
=========
|
=========
|
||||||
@@ -126,8 +144,7 @@ Community
|
|||||||
<http://groups.google.com/group/mongoengine-users>`_
|
<http://groups.google.com/group/mongoengine-users>`_
|
||||||
- `MongoEngine Developers mailing list
|
- `MongoEngine Developers mailing list
|
||||||
<http://groups.google.com/group/mongoengine-dev>`_
|
<http://groups.google.com/group/mongoengine-dev>`_
|
||||||
- `#mongoengine IRC channel <http://webchat.freenode.net/?channels=mongoengine>`_
|
|
||||||
|
|
||||||
Contributing
|
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>`_
|
||||||
|
152
benchmark.py
152
benchmark.py
@@ -1,118 +1,41 @@
|
|||||||
#!/usr/bin/env python
|
#!/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
|
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():
|
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...")
|
print("Benchmarking...")
|
||||||
|
|
||||||
setup = """
|
setup = """
|
||||||
@@ -131,7 +54,7 @@ noddy = db.noddy
|
|||||||
for i in range(10000):
|
for i in range(10000):
|
||||||
example = {'fields': {}}
|
example = {'fields': {}}
|
||||||
for j in range(20):
|
for j in range(20):
|
||||||
example['fields']["key"+str(j)] = "value "+str(j)
|
example['fields']['key' + str(j)] = 'value ' + str(j)
|
||||||
|
|
||||||
noddy.save(example)
|
noddy.save(example)
|
||||||
|
|
||||||
@@ -146,9 +69,10 @@ myNoddys = noddy.find()
|
|||||||
|
|
||||||
stmt = """
|
stmt = """
|
||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
|
from pymongo.write_concern import WriteConcern
|
||||||
connection = MongoClient()
|
connection = MongoClient()
|
||||||
|
|
||||||
db = connection.timeit_test
|
db = connection.get_database('timeit_test', write_concern=WriteConcern(w=0))
|
||||||
noddy = db.noddy
|
noddy = db.noddy
|
||||||
|
|
||||||
for i in range(10000):
|
for i in range(10000):
|
||||||
@@ -156,7 +80,7 @@ for i in range(10000):
|
|||||||
for j in range(20):
|
for j in range(20):
|
||||||
example['fields']["key"+str(j)] = "value "+str(j)
|
example['fields']["key"+str(j)] = "value "+str(j)
|
||||||
|
|
||||||
noddy.save(example, write_concern={"w": 0})
|
noddy.save(example)
|
||||||
|
|
||||||
myNoddys = noddy.find()
|
myNoddys = noddy.find()
|
||||||
[n for n in myNoddys] # iterate
|
[n for n in myNoddys] # iterate
|
||||||
@@ -171,10 +95,10 @@ myNoddys = noddy.find()
|
|||||||
from pymongo import MongoClient
|
from pymongo import MongoClient
|
||||||
connection = MongoClient()
|
connection = MongoClient()
|
||||||
connection.drop_database('timeit_test')
|
connection.drop_database('timeit_test')
|
||||||
connection.disconnect()
|
connection.close()
|
||||||
|
|
||||||
from mongoengine import Document, DictField, connect
|
from mongoengine import Document, DictField, connect
|
||||||
connect("timeit_test")
|
connect('timeit_test')
|
||||||
|
|
||||||
class Noddy(Document):
|
class Noddy(Document):
|
||||||
fields = DictField()
|
fields = DictField()
|
||||||
|
@@ -2,9 +2,38 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
Development
|
||||||
|
===========
|
||||||
|
- (Fill this out as you fix issues and develop your features).
|
||||||
|
- Fixed using sets in field choices #1481
|
||||||
|
- 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 an obscure error message when filtering by `field__in=non_iterable`. #1237
|
||||||
|
|
||||||
|
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
|
Changes in 0.10.8
|
||||||
=================
|
=================
|
||||||
- Fill this in as PRs for v0.10.8 are merged
|
- 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 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
|
Changes in 0.10.7
|
||||||
=================
|
=================
|
||||||
|
@@ -33,7 +33,7 @@ the :attr:`host` to
|
|||||||
corresponding parameters in :func:`~mongoengine.connect`: ::
|
corresponding parameters in :func:`~mongoengine.connect`: ::
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
name='test',
|
db='test',
|
||||||
username='user',
|
username='user',
|
||||||
password='12345',
|
password='12345',
|
||||||
host='mongodb://admin:qwerty@localhost/production'
|
host='mongodb://admin:qwerty@localhost/production'
|
||||||
@@ -42,13 +42,18 @@ the :attr:`host` to
|
|||||||
will establish connection to ``production`` database using
|
will establish connection to ``production`` database using
|
||||||
``admin`` username and ``qwerty`` password.
|
``admin`` username and ``qwerty`` password.
|
||||||
|
|
||||||
ReplicaSets
|
Replica Sets
|
||||||
===========
|
============
|
||||||
|
|
||||||
MongoEngine supports
|
MongoEngine supports connecting to replica sets::
|
||||||
:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them,
|
|
||||||
please use an URI style connection and provide the ``replicaSet`` name
|
from mongoengine import connect
|
||||||
in the connection kwargs.
|
|
||||||
|
# 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
|
Read preferences are supported through the connection or via individual
|
||||||
queries by passing the read_preference ::
|
queries by passing the read_preference ::
|
||||||
@@ -59,76 +64,74 @@ queries by passing the read_preference ::
|
|||||||
Multiple Databases
|
Multiple Databases
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Multiple database support was added in MongoEngine 0.6. To use multiple
|
To use multiple databases you can use :func:`~mongoengine.connect` and provide
|
||||||
databases you can use :func:`~mongoengine.connect` and provide an `alias` name
|
an `alias` name for the connection - if no `alias` is provided then "default"
|
||||||
for the connection - if no `alias` is provided then "default" is used.
|
is used.
|
||||||
|
|
||||||
In the background this uses :func:`~mongoengine.register_connection` to
|
In the background this uses :func:`~mongoengine.register_connection` to
|
||||||
store the data and you can register all aliases up front if required.
|
store the data and you can register all aliases up front if required.
|
||||||
|
|
||||||
Individual documents can also support multiple databases by providing a
|
Individual documents can also support multiple databases by providing a
|
||||||
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects
|
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef`
|
||||||
to point across databases and collections. Below is an example schema, using
|
objects to point across databases and collections. Below is an example schema,
|
||||||
3 different databases to store data::
|
using 3 different databases to store data::
|
||||||
|
|
||||||
class User(Document):
|
class User(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
meta = {"db_alias": "user-db"}
|
meta = {'db_alias': 'user-db'}
|
||||||
|
|
||||||
class Book(Document):
|
class Book(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
meta = {"db_alias": "book-db"}
|
meta = {'db_alias': 'book-db'}
|
||||||
|
|
||||||
class AuthorBooks(Document):
|
class AuthorBooks(Document):
|
||||||
author = ReferenceField(User)
|
author = ReferenceField(User)
|
||||||
book = ReferenceField(Book)
|
book = ReferenceField(Book)
|
||||||
|
|
||||||
meta = {"db_alias": "users-books-db"}
|
meta = {'db_alias': 'users-books-db'}
|
||||||
|
|
||||||
|
|
||||||
Context Managers
|
Context Managers
|
||||||
================
|
================
|
||||||
Sometimes you may want to switch the database or collection to query against
|
Sometimes you may want to switch the database or collection to query against.
|
||||||
for a class.
|
|
||||||
For example, archiving older data into a separate database for performance
|
For example, archiving older data into a separate database for performance
|
||||||
reasons or writing functions that dynamically choose collections to write
|
reasons or writing functions that dynamically choose collections to write
|
||||||
document to.
|
a document to.
|
||||||
|
|
||||||
Switch Database
|
Switch Database
|
||||||
---------------
|
---------------
|
||||||
The :class:`~mongoengine.context_managers.switch_db` context manager allows
|
The :class:`~mongoengine.context_managers.switch_db` context manager allows
|
||||||
you to change the database alias for a given class allowing quick and easy
|
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
|
from mongoengine.context_managers import switch_db
|
||||||
|
|
||||||
class User(Document):
|
class User(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
meta = {"db_alias": "user-db"}
|
meta = {'db_alias': 'user-db'}
|
||||||
|
|
||||||
with switch_db(User, 'archive-user-db') as User:
|
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
|
Switch Collection
|
||||||
-----------------
|
-----------------
|
||||||
The :class:`~mongoengine.context_managers.switch_collection` context manager
|
The :class:`~mongoengine.context_managers.switch_collection` context manager
|
||||||
allows you to change the collection for a given class allowing quick and easy
|
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
|
from mongoengine.context_managers import switch_collection
|
||||||
|
|
||||||
class Group(Document):
|
class Group(Document):
|
||||||
name = StringField()
|
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:
|
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
|
.. note:: Make sure any aliases have been registered with
|
||||||
|
@@ -150,7 +150,7 @@ arguments can be set on all fields:
|
|||||||
.. note:: If set, this field is also accessible through the `pk` field.
|
.. note:: If set, this field is also accessible through the `pk` field.
|
||||||
|
|
||||||
:attr:`choices` (Default: None)
|
: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.
|
field should be limited.
|
||||||
|
|
||||||
Can be either be a nested tuples of value (stored in mongo) and a
|
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
|
Dictionary Fields
|
||||||
-----------------
|
-----------------
|
||||||
Often, an embedded document may be used instead of a dictionary – generally
|
Often, an embedded document may be used instead of a dictionary – generally
|
||||||
embedded documents are recommended as dictionaries don’t support validation
|
embedded documents are recommended as dictionaries don’t support validation
|
||||||
or custom field types. However, sometimes you will not know the structure of what you want to
|
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::
|
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
|
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.
|
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
|
Generic reference fields
|
||||||
''''''''''''''''''''''''
|
''''''''''''''''''''''''
|
||||||
A second kind of reference field also exists,
|
A second kind of reference field also exists,
|
||||||
|
@@ -2,13 +2,13 @@
|
|||||||
Installing MongoEngine
|
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
|
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
|
`PyMongo <http://api.mongodb.org/python>`_ to use MongoEngine, but if you
|
||||||
install MongoEngine using setuptools, then the dependencies will be handled for
|
install MongoEngine using setuptools, then the dependencies will be handled for
|
||||||
you.
|
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
|
.. code-block:: console
|
||||||
|
|
||||||
|
@@ -479,6 +479,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
|
first positional argument to :attr:`Document.objects` when you filter it by
|
||||||
calling it with keyword arguments::
|
calling it with keyword arguments::
|
||||||
|
|
||||||
|
from mongoengine.queryset.visitor import Q
|
||||||
|
|
||||||
# Get published posts
|
# Get published posts
|
||||||
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))
|
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))
|
||||||
|
|
||||||
|
@@ -142,11 +142,4 @@ cleaner looking while still allowing manual execution of the callback::
|
|||||||
modified = DateTimeField()
|
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
|
.. _blinker: http://pypi.python.org/pypi/blinker
|
||||||
|
@@ -3,11 +3,10 @@ Tutorial
|
|||||||
========
|
========
|
||||||
|
|
||||||
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
This tutorial introduces **MongoEngine** by means of example --- we will walk
|
||||||
through how to create a simple **Tumblelog** application. A Tumblelog is a type
|
through how to create a simple **Tumblelog** application. A tumblelog is a
|
||||||
of blog where posts are not constrained to being conventional text-based posts.
|
blog that supports mixed media content, including text, images, links, video,
|
||||||
As well as text-based entries, users may post images, links, videos, etc. For
|
audio, etc. For simplicity's sake, we'll stick to text, image, and link
|
||||||
simplicity's sake, we'll stick to text, image and link entries in our
|
entries. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
||||||
application. 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
|
focus on the data-modelling side of the application, leaving out a user
|
||||||
interface.
|
interface.
|
||||||
|
|
||||||
@@ -16,14 +15,14 @@ Getting started
|
|||||||
|
|
||||||
Before we start, make sure that a copy of MongoDB is running in an accessible
|
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
|
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::
|
simply use pip to install it like so::
|
||||||
|
|
||||||
$ pip install mongoengine
|
$ pip install mongoengine
|
||||||
|
|
||||||
Before we can start using MongoEngine, we need to tell it how to connect to our
|
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`
|
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::
|
of the MongoDB database to use::
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@@ -39,18 +38,18 @@ Defining our documents
|
|||||||
MongoDB is *schemaless*, which means that no schema is enforced by the database
|
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.
|
--- 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
|
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
|
to the data model. However, defining schemas for our documents can help to iron
|
||||||
iron out bugs involving incorrect types or missing fields, and also allow us to
|
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
|
define utility methods on our documents in the same way that traditional
|
||||||
:abbr:`ORMs (Object-Relational Mappers)` do.
|
:abbr:`ORMs (Object-Relational Mappers)` do.
|
||||||
|
|
||||||
In our Tumblelog application we need to store several different types of
|
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
|
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
|
**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
|
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
|
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
|
posts. We'll start with **users**, as the other document models are slightly
|
||||||
more involved.
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
Object-Oriented principle of *inheritance* nicely. We can think of
|
||||||
:class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and
|
:class:`Post` as a base class, and :class:`TextPost`, :class:`ImagePost` and
|
||||||
:class:`LinkPost` as subclasses of :class:`Post`. In fact, MongoEngine supports
|
: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`::
|
by setting :attr:`allow_inheritance` to True in the :attr:`meta`::
|
||||||
|
|
||||||
class Post(Document):
|
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
|
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
|
within the post, rather than storing references to tags in a separate
|
||||||
collection. Especially as tags are generally very short (often even shorter
|
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
|
than a document's id), this denormalization won't impact the size of the
|
||||||
size of our database. So let's take a look that the code our modified
|
database very strongly. Let's take a look at the code of our modified
|
||||||
:class:`Post` class::
|
:class:`Post` class::
|
||||||
|
|
||||||
class Post(Document):
|
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
|
takes a field object as its first argument --- this means that you can have
|
||||||
lists of any type of field (including lists).
|
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`.
|
inherit from :class:`Post`.
|
||||||
|
|
||||||
Comments
|
Comments
|
||||||
@@ -149,7 +148,7 @@ Comments
|
|||||||
|
|
||||||
A comment is typically associated with *one* post. In a relational database, to
|
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
|
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
|
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
|
separately from their associated posts, other than to work around the
|
||||||
relational model. Using MongoDB we can store the comments as a list of
|
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.tags = ['mongoengine']
|
||||||
post2.save()
|
post2.save()
|
||||||
|
|
||||||
.. note:: If you change a field on a object that has already been saved, then
|
.. note:: If you change a field on an object that has already been saved and
|
||||||
call :meth:`save` again, the document will be updated.
|
then call :meth:`save` again, the document will be updated.
|
||||||
|
|
||||||
Accessing our data
|
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::
|
class. So let's see how we can get our posts' titles::
|
||||||
|
|
||||||
for post in Post.objects:
|
for post in Post.objects:
|
||||||
print post.title
|
print(post.title)
|
||||||
|
|
||||||
Retrieving type-specific information
|
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 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`::
|
to use the :attr:`objects` attribute of a subclass of :class:`Post`::
|
||||||
|
|
||||||
for post in TextPost.objects:
|
for post in TextPost.objects:
|
||||||
print post.content
|
print(post.content)
|
||||||
|
|
||||||
Using TextPost's :attr:`objects` attribute only returns documents that were
|
Using TextPost's :attr:`objects` attribute only returns documents that were
|
||||||
created using :class:`TextPost`. Actually, there is a more general rule here:
|
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::
|
practice::
|
||||||
|
|
||||||
for post in Post.objects:
|
for post in Post.objects:
|
||||||
print post.title
|
print(post.title)
|
||||||
print '=' * len(post.title)
|
print('=' * len(post.title))
|
||||||
|
|
||||||
if isinstance(post, TextPost):
|
if isinstance(post, TextPost):
|
||||||
print post.content
|
print(post.content)
|
||||||
|
|
||||||
if isinstance(post, LinkPost):
|
if isinstance(post, LinkPost):
|
||||||
print 'Link:', post.link_url
|
print('Link: {}'.format(post.link_url))
|
||||||
|
|
||||||
print
|
|
||||||
|
|
||||||
This would print the title of each post, followed by the content if it was a
|
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.
|
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::
|
are returned::
|
||||||
|
|
||||||
for post in Post.objects(tags='mongodb'):
|
for post in Post.objects(tags='mongodb'):
|
||||||
print post.title
|
print(post.title)
|
||||||
|
|
||||||
There are also methods available on :class:`~mongoengine.queryset.QuerySet`
|
There are also methods available on :class:`~mongoengine.queryset.QuerySet`
|
||||||
objects that allow different results to be returned, for example, calling
|
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::
|
used on :class:`~mongoengine.queryset.QuerySet` objects::
|
||||||
|
|
||||||
num_posts = Post.objects(tags='mongodb').count()
|
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
|
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
|
your MongoEngine journey is the `full user guide <guide/index.html>`_, where
|
||||||
can learn indepth about how to use mongoengine and mongodb.
|
you can learn in-depth about how to use MongoEngine and MongoDB.
|
||||||
|
@@ -2,6 +2,53 @@
|
|||||||
Upgrading
|
Upgrading
|
||||||
#########
|
#########
|
||||||
|
|
||||||
|
Development
|
||||||
|
***********
|
||||||
|
(Fill this out whenever you introduce breaking changes to MongoEngine)
|
||||||
|
|
||||||
|
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
|
0.9.0
|
||||||
*****
|
*****
|
||||||
|
|
||||||
|
@@ -1,25 +1,35 @@
|
|||||||
import connection
|
# Import submodules so that we can expose their __all__
|
||||||
from connection import *
|
from mongoengine import connection
|
||||||
import document
|
from mongoengine import document
|
||||||
from document import *
|
from mongoengine import errors
|
||||||
import errors
|
from mongoengine import fields
|
||||||
from errors import *
|
from mongoengine import queryset
|
||||||
import fields
|
from mongoengine import signals
|
||||||
from fields import *
|
|
||||||
import queryset
|
|
||||||
from queryset import *
|
|
||||||
import signals
|
|
||||||
from signals import *
|
|
||||||
|
|
||||||
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
|
# Import everything from each submodule so that it can be accessed via
|
||||||
list(queryset.__all__) + signals.__all__ + list(errors.__all__))
|
# 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, 11, 0)
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
if isinstance(VERSION[-1], basestring):
|
"""Return the VERSION as a string, e.g. for VERSION == (0, 10, 7),
|
||||||
return '.'.join(map(str, VERSION[:-1])) + VERSION[-1]
|
return '0.10.7'.
|
||||||
|
"""
|
||||||
return '.'.join(map(str, VERSION))
|
return '.'.join(map(str, VERSION))
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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.common import *
|
||||||
from mongoengine.base.datastructures import *
|
from mongoengine.base.datastructures import *
|
||||||
from mongoengine.base.document import *
|
from mongoengine.base.document import *
|
||||||
from mongoengine.base.fields import *
|
from mongoengine.base.fields import *
|
||||||
from mongoengine.base.metaclasses import *
|
from mongoengine.base.metaclasses import *
|
||||||
|
|
||||||
# Help with backwards compatibility
|
__all__ = (
|
||||||
from mongoengine.errors import *
|
# common
|
||||||
|
'UPDATE_OPERATORS', '_document_registry', 'get_document',
|
||||||
|
|
||||||
|
# datastructures
|
||||||
|
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
|
||||||
|
|
||||||
|
# document
|
||||||
|
'BaseDocument',
|
||||||
|
|
||||||
|
# fields
|
||||||
|
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
|
||||||
|
|
||||||
|
# metaclasses
|
||||||
|
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
|
||||||
|
)
|
||||||
|
@@ -1,13 +1,18 @@
|
|||||||
from mongoengine.errors import NotRegistered
|
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 = {}
|
_document_registry = {}
|
||||||
|
|
||||||
|
|
||||||
def get_document(name):
|
def get_document(name):
|
||||||
|
"""Get a document class by name."""
|
||||||
doc = _document_registry.get(name, None)
|
doc = _document_registry.get(name, None)
|
||||||
if not doc:
|
if not doc:
|
||||||
# Possible old style name
|
# Possible old style name
|
||||||
|
@@ -1,14 +1,16 @@
|
|||||||
import itertools
|
import itertools
|
||||||
import weakref
|
import weakref
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
from mongoengine.common import _import_class
|
from mongoengine.common import _import_class
|
||||||
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
||||||
|
|
||||||
__all__ = ("BaseDict", "BaseList", "EmbeddedDocumentList")
|
__all__ = ('BaseDict', 'BaseList', 'EmbeddedDocumentList')
|
||||||
|
|
||||||
|
|
||||||
class BaseDict(dict):
|
class BaseDict(dict):
|
||||||
"""A special dict so we can watch any changes"""
|
"""A special dict so we can watch any changes."""
|
||||||
|
|
||||||
_dereferenced = False
|
_dereferenced = False
|
||||||
_instance = None
|
_instance = None
|
||||||
@@ -93,8 +95,7 @@ class BaseDict(dict):
|
|||||||
|
|
||||||
|
|
||||||
class BaseList(list):
|
class BaseList(list):
|
||||||
"""A special list so we can watch any changes
|
"""A special list so we can watch any changes."""
|
||||||
"""
|
|
||||||
|
|
||||||
_dereferenced = False
|
_dereferenced = False
|
||||||
_instance = None
|
_instance = None
|
||||||
@@ -137,10 +138,7 @@ class BaseList(list):
|
|||||||
return super(BaseList, self).__setitem__(key, value)
|
return super(BaseList, self).__setitem__(key, value)
|
||||||
|
|
||||||
def __delitem__(self, key, *args, **kwargs):
|
def __delitem__(self, key, *args, **kwargs):
|
||||||
if isinstance(key, slice):
|
self._mark_as_changed()
|
||||||
self._mark_as_changed()
|
|
||||||
else:
|
|
||||||
self._mark_as_changed(key)
|
|
||||||
return super(BaseList, self).__delitem__(key)
|
return super(BaseList, self).__delitem__(key)
|
||||||
|
|
||||||
def __setslice__(self, *args, **kwargs):
|
def __setslice__(self, *args, **kwargs):
|
||||||
@@ -209,17 +207,22 @@ class BaseList(list):
|
|||||||
class EmbeddedDocumentList(BaseList):
|
class EmbeddedDocumentList(BaseList):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __match_all(cls, i, kwargs):
|
def __match_all(cls, embedded_doc, kwargs):
|
||||||
items = kwargs.items()
|
"""Return True if a given embedded doc matches all the filter
|
||||||
return all([
|
kwargs. If it doesn't return False.
|
||||||
getattr(i, k) == v or unicode(getattr(i, k)) == v for k, v in items
|
"""
|
||||||
])
|
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
|
@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:
|
if not kwargs:
|
||||||
return obj
|
return embedded_docs
|
||||||
return filter(lambda i: cls.__match_all(i, kwargs), obj)
|
return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)]
|
||||||
|
|
||||||
def __init__(self, list_items, instance, name):
|
def __init__(self, list_items, instance, name):
|
||||||
super(EmbeddedDocumentList, self).__init__(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)
|
values = self.__only_matches(self, kwargs)
|
||||||
if len(values) == 0:
|
if len(values) == 0:
|
||||||
raise DoesNotExist(
|
raise DoesNotExist(
|
||||||
"%s matching query does not exist." % self._name
|
'%s matching query does not exist.' % self._name
|
||||||
)
|
)
|
||||||
elif len(values) > 1:
|
elif len(values) > 1:
|
||||||
raise MultipleObjectsReturned(
|
raise MultipleObjectsReturned(
|
||||||
"%d items returned, instead of 1" % len(values)
|
'%d items returned, instead of 1' % len(values)
|
||||||
)
|
)
|
||||||
|
|
||||||
return values[0]
|
return values[0]
|
||||||
|
|
||||||
def first(self):
|
def first(self):
|
||||||
"""
|
"""Return the first embedded document in the list, or ``None``
|
||||||
Returns the first embedded document in the list, or ``None`` if empty.
|
if empty.
|
||||||
"""
|
"""
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
return self[0]
|
return self[0]
|
||||||
@@ -426,7 +429,7 @@ class StrictDict(object):
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.items() == other.items()
|
return self.items() == other.items()
|
||||||
|
|
||||||
def __neq__(self, other):
|
def __ne__(self, other):
|
||||||
return self.items() != other.items()
|
return self.items() != other.items()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -438,7 +441,7 @@ class StrictDict(object):
|
|||||||
__slots__ = allowed_keys_tuple
|
__slots__ = allowed_keys_tuple
|
||||||
|
|
||||||
def __repr__(self):
|
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
|
cls._classes[allowed_keys] = SpecificStrictDict
|
||||||
return cls._classes[allowed_keys]
|
return cls._classes[allowed_keys]
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import copy
|
import copy
|
||||||
import numbers
|
import numbers
|
||||||
import operator
|
|
||||||
from collections import Hashable
|
from collections import Hashable
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
@@ -8,30 +7,27 @@ from bson import ObjectId, json_util
|
|||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
from bson.son import SON
|
from bson.son import SON
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import six
|
||||||
|
|
||||||
from mongoengine import signals
|
from mongoengine import signals
|
||||||
from mongoengine.base.common import ALLOW_INHERITANCE, get_document
|
from mongoengine.base.common import get_document
|
||||||
from mongoengine.base.datastructures import (
|
from mongoengine.base.datastructures import (BaseDict, BaseList,
|
||||||
BaseDict,
|
EmbeddedDocumentList,
|
||||||
BaseList,
|
SemiStrictDict, StrictDict)
|
||||||
EmbeddedDocumentList,
|
|
||||||
SemiStrictDict,
|
|
||||||
StrictDict
|
|
||||||
)
|
|
||||||
from mongoengine.base.fields import ComplexBaseField
|
from mongoengine.base.fields import ComplexBaseField
|
||||||
from mongoengine.common import _import_class
|
from mongoengine.common import _import_class
|
||||||
from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError,
|
from mongoengine.errors import (FieldDoesNotExist, InvalidDocumentError,
|
||||||
LookUpError, ValidationError)
|
LookUpError, OperationError, ValidationError)
|
||||||
from mongoengine.python_support import PY3, txt_type
|
|
||||||
|
|
||||||
__all__ = ('BaseDocument', 'NON_FIELD_ERRORS')
|
__all__ = ('BaseDocument',)
|
||||||
|
|
||||||
NON_FIELD_ERRORS = '__all__'
|
NON_FIELD_ERRORS = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class BaseDocument(object):
|
class BaseDocument(object):
|
||||||
__slots__ = ('_changed_fields', '_initialised', '_created', '_data',
|
__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 = False
|
||||||
_dynamic_lock = True
|
_dynamic_lock = True
|
||||||
@@ -57,15 +53,15 @@ class BaseDocument(object):
|
|||||||
name = next(field)
|
name = next(field)
|
||||||
if name in values:
|
if name in values:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Multiple values for keyword argument '" + name + "'")
|
'Multiple values for keyword argument "%s"' % name)
|
||||||
values[name] = value
|
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
|
# 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)
|
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'])
|
self._fields.keys() + ['id', 'pk', '_cls', '_text_score'])
|
||||||
if _undefined_fields:
|
if _undefined_fields:
|
||||||
msg = (
|
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)
|
).format(_undefined_fields, self._class_name)
|
||||||
raise FieldDoesNotExist(msg)
|
raise FieldDoesNotExist(msg)
|
||||||
|
|
||||||
@@ -95,7 +91,7 @@ class BaseDocument(object):
|
|||||||
value = getattr(self, key, None)
|
value = getattr(self, key, None)
|
||||||
setattr(self, key, value)
|
setattr(self, key, value)
|
||||||
|
|
||||||
if "_cls" not in values:
|
if '_cls' not in values:
|
||||||
self._cls = self._class_name
|
self._cls = self._class_name
|
||||||
|
|
||||||
# Set passed values after initialisation
|
# Set passed values after initialisation
|
||||||
@@ -121,7 +117,7 @@ class BaseDocument(object):
|
|||||||
else:
|
else:
|
||||||
self._data[key] = value
|
self._data[key] = value
|
||||||
|
|
||||||
# Set any get_fieldname_display methods
|
# Set any get_<field>_display methods
|
||||||
self.__set_field_display()
|
self.__set_field_display()
|
||||||
|
|
||||||
if self._dynamic:
|
if self._dynamic:
|
||||||
@@ -150,7 +146,7 @@ class BaseDocument(object):
|
|||||||
if self._dynamic and not self._dynamic_lock:
|
if self._dynamic and not self._dynamic_lock:
|
||||||
|
|
||||||
if not hasattr(self, name) and not name.startswith('_'):
|
if not hasattr(self, name) and not name.startswith('_'):
|
||||||
DynamicField = _import_class("DynamicField")
|
DynamicField = _import_class('DynamicField')
|
||||||
field = DynamicField(db_field=name)
|
field = DynamicField(db_field=name)
|
||||||
field.name = name
|
field.name = name
|
||||||
self._dynamic_fields[name] = field
|
self._dynamic_fields[name] = field
|
||||||
@@ -169,11 +165,13 @@ class BaseDocument(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
self__created = True
|
self__created = True
|
||||||
|
|
||||||
if (self._is_document and not self__created and
|
if (
|
||||||
name in self._meta.get('shard_key', tuple()) and
|
self._is_document and
|
||||||
self._data.get(name) != value):
|
not self__created and
|
||||||
OperationError = _import_class('OperationError')
|
name in self._meta.get('shard_key', tuple()) and
|
||||||
msg = "Shard Keys are immutable. Tried to update %s" % name
|
self._data.get(name) != value
|
||||||
|
):
|
||||||
|
msg = 'Shard Keys are immutable. Tried to update %s' % name
|
||||||
raise OperationError(msg)
|
raise OperationError(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -197,8 +195,8 @@ class BaseDocument(object):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def __setstate__(self, data):
|
def __setstate__(self, data):
|
||||||
if isinstance(data["_data"], SON):
|
if isinstance(data['_data'], SON):
|
||||||
data["_data"] = self.__class__._from_son(data["_data"])._data
|
data['_data'] = self.__class__._from_son(data['_data'])._data
|
||||||
for k in ('_changed_fields', '_initialised', '_created', '_data',
|
for k in ('_changed_fields', '_initialised', '_created', '_data',
|
||||||
'_dynamic_fields'):
|
'_dynamic_fields'):
|
||||||
if k in data:
|
if k in data:
|
||||||
@@ -212,7 +210,7 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
dynamic_fields = data.get('_dynamic_fields') or SON()
|
dynamic_fields = data.get('_dynamic_fields') or SON()
|
||||||
for k in dynamic_fields.keys():
|
for k in dynamic_fields.keys():
|
||||||
setattr(self, k, data["_data"].get(k))
|
setattr(self, k, data['_data'].get(k))
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self._fields_ordered)
|
return iter(self._fields_ordered)
|
||||||
@@ -254,12 +252,13 @@ class BaseDocument(object):
|
|||||||
return repr_type('<%s: %s>' % (self.__class__.__name__, u))
|
return repr_type('<%s: %s>' % (self.__class__.__name__, u))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
# TODO this could be simpler?
|
||||||
if hasattr(self, '__unicode__'):
|
if hasattr(self, '__unicode__'):
|
||||||
if PY3:
|
if six.PY3:
|
||||||
return self.__unicode__()
|
return self.__unicode__()
|
||||||
else:
|
else:
|
||||||
return unicode(self).encode('utf-8')
|
return six.text_type(self).encode('utf-8')
|
||||||
return txt_type('%s object' % self.__class__.__name__)
|
return six.text_type('%s object' % self.__class__.__name__)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, self.__class__) and hasattr(other, 'id') and other.id is not None:
|
if isinstance(other, self.__class__) and hasattr(other, 'id') and other.id is not None:
|
||||||
@@ -308,7 +307,7 @@ class BaseDocument(object):
|
|||||||
fields = []
|
fields = []
|
||||||
|
|
||||||
data = SON()
|
data = SON()
|
||||||
data["_id"] = None
|
data['_id'] = None
|
||||||
data['_cls'] = self._class_name
|
data['_cls'] = self._class_name
|
||||||
|
|
||||||
# only root fields ['test1.a', 'test2'] => ['test1', 'test2']
|
# only root fields ['test1.a', 'test2'] => ['test1', 'test2']
|
||||||
@@ -351,18 +350,8 @@ class BaseDocument(object):
|
|||||||
else:
|
else:
|
||||||
data[field.name] = value
|
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
|
# Only add _cls if allow_inheritance is True
|
||||||
if (not hasattr(self, '_meta') or
|
if not self._meta.get('allow_inheritance'):
|
||||||
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
|
|
||||||
data.pop('_cls')
|
data.pop('_cls')
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -376,16 +365,16 @@ class BaseDocument(object):
|
|||||||
if clean:
|
if clean:
|
||||||
try:
|
try:
|
||||||
self.clean()
|
self.clean()
|
||||||
except ValidationError, error:
|
except ValidationError as error:
|
||||||
errors[NON_FIELD_ERRORS] = error
|
errors[NON_FIELD_ERRORS] = error
|
||||||
|
|
||||||
# Get a list of tuples of field names and their current values
|
# Get a list of tuples of field names and their current values
|
||||||
fields = [(self._fields.get(name, self._dynamic_fields.get(name)),
|
fields = [(self._fields.get(name, self._dynamic_fields.get(name)),
|
||||||
self._data.get(name)) for name in self._fields_ordered]
|
self._data.get(name)) for name in self._fields_ordered]
|
||||||
|
|
||||||
EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
|
EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
|
||||||
GenericEmbeddedDocumentField = _import_class(
|
GenericEmbeddedDocumentField = _import_class(
|
||||||
"GenericEmbeddedDocumentField")
|
'GenericEmbeddedDocumentField')
|
||||||
|
|
||||||
for field, value in fields:
|
for field, value in fields:
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -395,27 +384,29 @@ class BaseDocument(object):
|
|||||||
field._validate(value, clean=clean)
|
field._validate(value, clean=clean)
|
||||||
else:
|
else:
|
||||||
field._validate(value)
|
field._validate(value)
|
||||||
except ValidationError, error:
|
except ValidationError as error:
|
||||||
errors[field.name] = error.errors or error
|
errors[field.name] = error.errors or error
|
||||||
except (ValueError, AttributeError, AssertionError), error:
|
except (ValueError, AttributeError, AssertionError) as error:
|
||||||
errors[field.name] = error
|
errors[field.name] = error
|
||||||
elif field.required and not getattr(field, '_auto_gen', False):
|
elif field.required and not getattr(field, '_auto_gen', False):
|
||||||
errors[field.name] = ValidationError('Field is required',
|
errors[field.name] = ValidationError('Field is required',
|
||||||
field_name=field.name)
|
field_name=field.name)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
pk = "None"
|
pk = 'None'
|
||||||
if hasattr(self, 'pk'):
|
if hasattr(self, 'pk'):
|
||||||
pk = self.pk
|
pk = self.pk
|
||||||
elif self._instance and hasattr(self._instance, 'pk'):
|
elif self._instance and hasattr(self._instance, 'pk'):
|
||||||
pk = 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)
|
raise ValidationError(message, errors=errors)
|
||||||
|
|
||||||
def to_json(self, *args, **kwargs):
|
def to_json(self, *args, **kwargs):
|
||||||
"""Converts a document to JSON.
|
"""Convert this 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
|
: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)
|
use_db_field = kwargs.pop('use_db_field', True)
|
||||||
return json_util.dumps(self.to_mongo(use_db_field), *args, **kwargs)
|
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)
|
return cls._from_son(json_util.loads(json_data), created=created)
|
||||||
|
|
||||||
def __expand_dynamic_values(self, name, value):
|
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)):
|
if not isinstance(value, (dict, list, tuple)):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
# If the value is a dict with '_cls' in it, turn it into a document
|
||||||
|
is_dict = isinstance(value, dict)
|
||||||
is_list = False
|
if is_dict and '_cls' in value:
|
||||||
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:
|
|
||||||
cls = get_document(value['_cls'])
|
cls = get_document(value['_cls'])
|
||||||
return cls(**value)
|
return cls(**value)
|
||||||
|
|
||||||
data = {}
|
if is_dict:
|
||||||
for k, v in value.items():
|
value = {
|
||||||
key = name if is_list else k
|
k: self.__expand_dynamic_values(k, v)
|
||||||
data[k] = self.__expand_dynamic_values(key, v)
|
for k, v in value.items()
|
||||||
|
}
|
||||||
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]
|
|
||||||
else:
|
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
|
# Convert lists / values so we can watch for any changes on them
|
||||||
|
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||||
if (isinstance(value, (list, tuple)) and
|
if (isinstance(value, (list, tuple)) and
|
||||||
not isinstance(value, BaseList)):
|
not isinstance(value, BaseList)):
|
||||||
if issubclass(type(self), EmbeddedDocumentListField):
|
if issubclass(type(self), EmbeddedDocumentListField):
|
||||||
@@ -465,8 +449,7 @@ class BaseDocument(object):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def _mark_as_changed(self, key):
|
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:
|
if not key:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -496,10 +479,11 @@ class BaseDocument(object):
|
|||||||
remove(field)
|
remove(field)
|
||||||
|
|
||||||
def _clear_changed_fields(self):
|
def _clear_changed_fields(self):
|
||||||
"""Using get_changed_fields iterate and remove any fields that are
|
"""Using _get_changed_fields iterate and remove any fields that
|
||||||
marked as changed"""
|
are marked as changed.
|
||||||
|
"""
|
||||||
for changed in self._get_changed_fields():
|
for changed in self._get_changed_fields():
|
||||||
parts = changed.split(".")
|
parts = changed.split('.')
|
||||||
data = self
|
data = self
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
@@ -511,10 +495,13 @@ class BaseDocument(object):
|
|||||||
data = data.get(part, None)
|
data = data.get(part, None)
|
||||||
else:
|
else:
|
||||||
data = getattr(data, part, None)
|
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
|
continue
|
||||||
|
|
||||||
data._changed_fields = []
|
data._changed_fields = []
|
||||||
|
|
||||||
self._changed_fields = []
|
self._changed_fields = []
|
||||||
|
|
||||||
def _nestable_types_changed_fields(self, changed_fields, key, data, inspected):
|
def _nestable_types_changed_fields(self, changed_fields, key, data, inspected):
|
||||||
@@ -526,26 +513,27 @@ class BaseDocument(object):
|
|||||||
iterator = data.iteritems()
|
iterator = data.iteritems()
|
||||||
|
|
||||||
for index, value in iterator:
|
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
|
# don't check anything lower if this key is already marked
|
||||||
# as changed.
|
# as changed.
|
||||||
if list_key[:-1] in changed_fields:
|
if list_key[:-1] in changed_fields:
|
||||||
continue
|
continue
|
||||||
if hasattr(value, '_get_changed_fields'):
|
if hasattr(value, '_get_changed_fields'):
|
||||||
changed = value._get_changed_fields(inspected)
|
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]
|
for k in changed if k]
|
||||||
elif isinstance(value, (list, tuple, dict)):
|
elif isinstance(value, (list, tuple, dict)):
|
||||||
self._nestable_types_changed_fields(
|
self._nestable_types_changed_fields(
|
||||||
changed_fields, list_key, value, inspected)
|
changed_fields, list_key, value, inspected)
|
||||||
|
|
||||||
def _get_changed_fields(self, inspected=None):
|
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")
|
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||||
DynamicEmbeddedDocument = _import_class("DynamicEmbeddedDocument")
|
DynamicEmbeddedDocument = _import_class('DynamicEmbeddedDocument')
|
||||||
ReferenceField = _import_class("ReferenceField")
|
ReferenceField = _import_class('ReferenceField')
|
||||||
SortedListField = _import_class("SortedListField")
|
SortedListField = _import_class('SortedListField')
|
||||||
|
|
||||||
changed_fields = []
|
changed_fields = []
|
||||||
changed_fields += getattr(self, '_changed_fields', [])
|
changed_fields += getattr(self, '_changed_fields', [])
|
||||||
|
|
||||||
@@ -572,7 +560,7 @@ class BaseDocument(object):
|
|||||||
):
|
):
|
||||||
# Find all embedded fields that have been changed
|
# Find all embedded fields that have been changed
|
||||||
changed = data._get_changed_fields(inspected)
|
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
|
elif (isinstance(data, (list, tuple, dict)) and
|
||||||
db_field_name not in changed_fields):
|
db_field_name not in changed_fields):
|
||||||
if (hasattr(field, 'field') and
|
if (hasattr(field, 'field') and
|
||||||
@@ -676,21 +664,28 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_collection_name(cls):
|
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)
|
return cls._meta.get('collection', None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False):
|
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:
|
if not only_fields:
|
||||||
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 if unavailable
|
||||||
class_name = son.get('_cls', cls._class_name)
|
class_name = son.get('_cls', cls._class_name)
|
||||||
data = dict(("%s" % key, value) for key, value in son.iteritems())
|
|
||||||
|
# Convert SON to a dict, making sure each key is a string
|
||||||
|
data = {str(key): value for key, value in son.iteritems()}
|
||||||
|
|
||||||
# Return correct subclass for document type
|
# Return correct subclass for document type
|
||||||
if class_name != cls._class_name:
|
if class_name != cls._class_name:
|
||||||
@@ -712,19 +707,20 @@ class BaseDocument(object):
|
|||||||
else field.to_python(value))
|
else field.to_python(value))
|
||||||
if field_name != field.db_field:
|
if field_name != field.db_field:
|
||||||
del data[field.db_field]
|
del data[field.db_field]
|
||||||
except (AttributeError, ValueError), e:
|
except (AttributeError, ValueError) as e:
|
||||||
errors_dict[field_name] = e
|
errors_dict[field_name] = e
|
||||||
|
|
||||||
if errors_dict:
|
if errors_dict:
|
||||||
errors = "\n".join(["%s - %s" % (k, v)
|
errors = '\n'.join(['%s - %s' % (k, v)
|
||||||
for k, v in errors_dict.items()])
|
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))
|
% (cls._class_name, errors))
|
||||||
raise InvalidDocumentError(msg)
|
raise InvalidDocumentError(msg)
|
||||||
|
|
||||||
|
# In STRICT documents, remove any keys that aren't in cls._fields
|
||||||
if cls.STRICT:
|
if cls.STRICT:
|
||||||
data = dict((k, v)
|
data = {k: v for k, v in data.iteritems() if k in cls._fields}
|
||||||
for k, v in data.iteritems() if k in cls._fields)
|
|
||||||
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
|
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
|
||||||
obj._changed_fields = changed_fields
|
obj._changed_fields = changed_fields
|
||||||
if not _auto_dereference:
|
if not _auto_dereference:
|
||||||
@@ -734,37 +730,43 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_index_specs(cls, meta_indexes):
|
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()
|
geo_indices = cls._geo_indices()
|
||||||
unique_indices = cls._unique_with_indexes()
|
unique_indices = cls._unique_with_indexes()
|
||||||
index_specs = [cls._build_index_spec(spec)
|
index_specs = [cls._build_index_spec(spec) for spec in meta_indexes]
|
||||||
for spec in meta_indexes]
|
|
||||||
|
|
||||||
def merge_index_specs(index_specs, indices):
|
def merge_index_specs(index_specs, indices):
|
||||||
|
"""Helper method for merging index specs."""
|
||||||
if not indices:
|
if not indices:
|
||||||
return index_specs
|
return index_specs
|
||||||
|
|
||||||
spec_fields = [v['fields']
|
# Create a map of index fields to index spec. We're converting
|
||||||
for k, v in enumerate(index_specs)]
|
# the fields from a list to a tuple so that it's hashable.
|
||||||
# Merge unique_indexes with existing specs
|
spec_fields = {
|
||||||
for k, v in enumerate(indices):
|
tuple(index['fields']): index for index in index_specs
|
||||||
if v['fields'] in spec_fields:
|
}
|
||||||
index_specs[spec_fields.index(v['fields'])].update(v)
|
|
||||||
|
# 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:
|
else:
|
||||||
index_specs.append(v)
|
candidate.update(new_index)
|
||||||
|
|
||||||
return index_specs
|
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, geo_indices)
|
||||||
index_specs = merge_index_specs(index_specs, unique_indices)
|
index_specs = merge_index_specs(index_specs, unique_indices)
|
||||||
return index_specs
|
return index_specs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_index_spec(cls, spec):
|
def _build_index_spec(cls, spec):
|
||||||
"""Build a PyMongo index spec from a MongoEngine index spec.
|
"""Build a PyMongo index spec from a MongoEngine index spec."""
|
||||||
"""
|
if isinstance(spec, six.string_types):
|
||||||
if isinstance(spec, basestring):
|
|
||||||
spec = {'fields': [spec]}
|
spec = {'fields': [spec]}
|
||||||
elif isinstance(spec, (list, tuple)):
|
elif isinstance(spec, (list, tuple)):
|
||||||
spec = {'fields': list(spec)}
|
spec = {'fields': list(spec)}
|
||||||
@@ -775,8 +777,7 @@ class BaseDocument(object):
|
|||||||
direction = None
|
direction = None
|
||||||
|
|
||||||
# Check to see if we need to include _cls
|
# Check to see if we need to include _cls
|
||||||
allow_inheritance = cls._meta.get('allow_inheritance',
|
allow_inheritance = cls._meta.get('allow_inheritance')
|
||||||
ALLOW_INHERITANCE)
|
|
||||||
include_cls = (
|
include_cls = (
|
||||||
allow_inheritance and
|
allow_inheritance and
|
||||||
not spec.get('sparse', False) and
|
not spec.get('sparse', False) and
|
||||||
@@ -786,7 +787,7 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
# 733: don't include cls if index_cls is False unless there is an explicit cls with the index
|
# 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))
|
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')
|
spec.pop('cls')
|
||||||
for key in spec['fields']:
|
for key in spec['fields']:
|
||||||
# If inherited spec continue
|
# If inherited spec continue
|
||||||
@@ -801,19 +802,19 @@ class BaseDocument(object):
|
|||||||
# GEOHAYSTACK from )
|
# GEOHAYSTACK from )
|
||||||
# GEO2D from *
|
# GEO2D from *
|
||||||
direction = pymongo.ASCENDING
|
direction = pymongo.ASCENDING
|
||||||
if key.startswith("-"):
|
if key.startswith('-'):
|
||||||
direction = pymongo.DESCENDING
|
direction = pymongo.DESCENDING
|
||||||
elif key.startswith("$"):
|
elif key.startswith('$'):
|
||||||
direction = pymongo.TEXT
|
direction = pymongo.TEXT
|
||||||
elif key.startswith("#"):
|
elif key.startswith('#'):
|
||||||
direction = pymongo.HASHED
|
direction = pymongo.HASHED
|
||||||
elif key.startswith("("):
|
elif key.startswith('('):
|
||||||
direction = pymongo.GEOSPHERE
|
direction = pymongo.GEOSPHERE
|
||||||
elif key.startswith(")"):
|
elif key.startswith(')'):
|
||||||
direction = pymongo.GEOHAYSTACK
|
direction = pymongo.GEOHAYSTACK
|
||||||
elif key.startswith("*"):
|
elif key.startswith('*'):
|
||||||
direction = pymongo.GEO2D
|
direction = pymongo.GEO2D
|
||||||
if key.startswith(("+", "-", "*", "$", "#", "(", ")")):
|
if key.startswith(('+', '-', '*', '$', '#', '(', ')')):
|
||||||
key = key[1:]
|
key = key[1:]
|
||||||
|
|
||||||
# Use real field name, do it manually because we need field
|
# Use real field name, do it manually because we need field
|
||||||
@@ -826,7 +827,7 @@ class BaseDocument(object):
|
|||||||
parts = []
|
parts = []
|
||||||
for field in fields:
|
for field in fields:
|
||||||
try:
|
try:
|
||||||
if field != "_id":
|
if field != '_id':
|
||||||
field = field.db_field
|
field = field.db_field
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
@@ -845,49 +846,53 @@ class BaseDocument(object):
|
|||||||
return spec
|
return spec
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _unique_with_indexes(cls, namespace=""):
|
def _unique_with_indexes(cls, namespace=''):
|
||||||
"""
|
"""Find unique indexes in the document schema and return them."""
|
||||||
Find and set unique indexes
|
|
||||||
"""
|
|
||||||
unique_indexes = []
|
unique_indexes = []
|
||||||
for field_name, field in cls._fields.items():
|
for field_name, field in cls._fields.items():
|
||||||
sparse = field.sparse
|
sparse = field.sparse
|
||||||
|
|
||||||
# Generate a list of indexes needed by uniqueness constraints
|
# Generate a list of indexes needed by uniqueness constraints
|
||||||
if field.unique:
|
if field.unique:
|
||||||
unique_fields = [field.db_field]
|
unique_fields = [field.db_field]
|
||||||
|
|
||||||
# Add any unique_with fields to the back of the index spec
|
# Add any unique_with fields to the back of the index spec
|
||||||
if field.unique_with:
|
if field.unique_with:
|
||||||
if isinstance(field.unique_with, basestring):
|
if isinstance(field.unique_with, six.string_types):
|
||||||
field.unique_with = [field.unique_with]
|
field.unique_with = [field.unique_with]
|
||||||
|
|
||||||
# Convert unique_with field names to real field names
|
# Convert unique_with field names to real field names
|
||||||
unique_with = []
|
unique_with = []
|
||||||
for other_name in field.unique_with:
|
for other_name in field.unique_with:
|
||||||
parts = other_name.split('.')
|
parts = other_name.split('.')
|
||||||
|
|
||||||
# Lookup real name
|
# Lookup real name
|
||||||
parts = cls._lookup_field(parts)
|
parts = cls._lookup_field(parts)
|
||||||
name_parts = [part.db_field for part in parts]
|
name_parts = [part.db_field for part in parts]
|
||||||
unique_with.append('.'.join(name_parts))
|
unique_with.append('.'.join(name_parts))
|
||||||
|
|
||||||
# Unique field should be required
|
# Unique field should be required
|
||||||
parts[-1].required = True
|
parts[-1].required = True
|
||||||
sparse = (not sparse and
|
sparse = (not sparse and
|
||||||
parts[-1].name not in cls.__dict__)
|
parts[-1].name not in cls.__dict__)
|
||||||
|
|
||||||
unique_fields += unique_with
|
unique_fields += unique_with
|
||||||
|
|
||||||
# Add the new index to the list
|
# Add the new index to the list
|
||||||
fields = [("%s%s" % (namespace, f), pymongo.ASCENDING)
|
fields = [
|
||||||
for f in unique_fields]
|
('%s%s' % (namespace, f), pymongo.ASCENDING)
|
||||||
|
for f in unique_fields
|
||||||
|
]
|
||||||
index = {'fields': fields, 'unique': True, 'sparse': sparse}
|
index = {'fields': fields, 'unique': True, 'sparse': sparse}
|
||||||
unique_indexes.append(index)
|
unique_indexes.append(index)
|
||||||
|
|
||||||
if field.__class__.__name__ == "ListField":
|
if field.__class__.__name__ == 'ListField':
|
||||||
field = field.field
|
field = field.field
|
||||||
|
|
||||||
# Grab any embedded document field unique indexes
|
# Grab any embedded document field unique indexes
|
||||||
if (field.__class__.__name__ == "EmbeddedDocumentField" and
|
if (field.__class__.__name__ == 'EmbeddedDocumentField' and
|
||||||
field.document_type != cls):
|
field.document_type != cls):
|
||||||
field_namespace = "%s." % field_name
|
field_namespace = '%s.' % field_name
|
||||||
doc_cls = field.document_type
|
doc_cls = field.document_type
|
||||||
unique_indexes += doc_cls._unique_with_indexes(field_namespace)
|
unique_indexes += doc_cls._unique_with_indexes(field_namespace)
|
||||||
|
|
||||||
@@ -899,8 +904,9 @@ class BaseDocument(object):
|
|||||||
geo_indices = []
|
geo_indices = []
|
||||||
inspected.append(cls)
|
inspected.append(cls)
|
||||||
|
|
||||||
geo_field_type_names = ["EmbeddedDocumentField", "GeoPointField",
|
geo_field_type_names = ('EmbeddedDocumentField', 'GeoPointField',
|
||||||
"PointField", "LineStringField", "PolygonField"]
|
'PointField', 'LineStringField',
|
||||||
|
'PolygonField')
|
||||||
|
|
||||||
geo_field_types = tuple([_import_class(field)
|
geo_field_types = tuple([_import_class(field)
|
||||||
for field in geo_field_type_names])
|
for field in geo_field_type_names])
|
||||||
@@ -908,32 +914,68 @@ class BaseDocument(object):
|
|||||||
for field in cls._fields.values():
|
for field in cls._fields.values():
|
||||||
if not isinstance(field, geo_field_types):
|
if not isinstance(field, geo_field_types):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if hasattr(field, 'document_type'):
|
if hasattr(field, 'document_type'):
|
||||||
field_cls = field.document_type
|
field_cls = field.document_type
|
||||||
if field_cls in inspected:
|
if field_cls in inspected:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if hasattr(field_cls, '_geo_indices'):
|
if hasattr(field_cls, '_geo_indices'):
|
||||||
geo_indices += field_cls._geo_indices(
|
geo_indices += field_cls._geo_indices(
|
||||||
inspected, parent_field=field.db_field)
|
inspected, parent_field=field.db_field)
|
||||||
elif field._geo_index:
|
elif field._geo_index:
|
||||||
field_name = field.db_field
|
field_name = field.db_field
|
||||||
if parent_field:
|
if parent_field:
|
||||||
field_name = "%s.%s" % (parent_field, field_name)
|
field_name = '%s.%s' % (parent_field, field_name)
|
||||||
geo_indices.append({'fields':
|
geo_indices.append({
|
||||||
[(field_name, field._geo_index)]})
|
'fields': [(field_name, field._geo_index)]
|
||||||
|
})
|
||||||
|
|
||||||
return geo_indices
|
return geo_indices
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _lookup_field(cls, parts):
|
def _lookup_field(cls, parts):
|
||||||
"""Lookup a field based on its attribute and return a list containing
|
"""Given the path to a given field, return a list containing
|
||||||
the field's parents and the field.
|
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')
|
DynamicField = _import_class('DynamicField')
|
||||||
|
|
||||||
if not isinstance(parts, (list, tuple)):
|
if not isinstance(parts, (list, tuple)):
|
||||||
parts = [parts]
|
parts = [parts]
|
||||||
|
|
||||||
fields = []
|
fields = []
|
||||||
field = None
|
field = None
|
||||||
|
|
||||||
@@ -943,16 +985,17 @@ class BaseDocument(object):
|
|||||||
fields.append(field_name)
|
fields.append(field_name)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Look up first field from the document
|
||||||
if field is None:
|
if field is None:
|
||||||
# Look up first field from the document
|
|
||||||
if field_name == 'pk':
|
if field_name == 'pk':
|
||||||
# Deal with "primary key" alias
|
# Deal with "primary key" alias
|
||||||
field_name = cls._meta['id_field']
|
field_name = cls._meta['id_field']
|
||||||
|
|
||||||
if field_name in cls._fields:
|
if field_name in cls._fields:
|
||||||
field = cls._fields[field_name]
|
field = cls._fields[field_name]
|
||||||
elif cls._dynamic:
|
elif cls._dynamic:
|
||||||
field = DynamicField(db_field=field_name)
|
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
|
# 744: in case the field is defined in a subclass
|
||||||
for subcls in cls.__subclasses__():
|
for subcls in cls.__subclasses__():
|
||||||
try:
|
try:
|
||||||
@@ -965,35 +1008,55 @@ class BaseDocument(object):
|
|||||||
else:
|
else:
|
||||||
raise LookUpError('Cannot resolve field "%s"' % field_name)
|
raise LookUpError('Cannot resolve field "%s"' % field_name)
|
||||||
else:
|
else:
|
||||||
raise LookUpError('Cannot resolve field "%s"'
|
raise LookUpError('Cannot resolve field "%s"' % field_name)
|
||||||
% field_name)
|
|
||||||
else:
|
else:
|
||||||
ReferenceField = _import_class('ReferenceField')
|
ReferenceField = _import_class('ReferenceField')
|
||||||
GenericReferenceField = _import_class('GenericReferenceField')
|
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)):
|
if isinstance(field, (ReferenceField, GenericReferenceField)):
|
||||||
raise LookUpError('Cannot perform join in mongoDB: %s' %
|
raise LookUpError('Cannot perform join in mongoDB: %s' %
|
||||||
'__'.join(parts))
|
'__'.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'):
|
if hasattr(getattr(field, 'field', None), 'lookup_member'):
|
||||||
new_field = field.field.lookup_member(field_name)
|
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
|
elif cls._dynamic and (isinstance(field, DynamicField) or
|
||||||
getattr(getattr(field, 'document_type', None), '_dynamic', None)):
|
getattr(getattr(field, 'document_type', None), '_dynamic', None)):
|
||||||
new_field = DynamicField(db_field=field_name)
|
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:
|
else:
|
||||||
# Look up subfield on the previous field or raise
|
raise LookUpError(
|
||||||
try:
|
'Cannot resolve subfield or operator {} '
|
||||||
new_field = field.lookup_member(field_name)
|
'on the field {}'.format(field_name, field.name)
|
||||||
except AttributeError:
|
)
|
||||||
raise LookUpError('Cannot resolve subfield or operator {} '
|
|
||||||
'on the field {}'.format(
|
# If current field still wasn't found and the parent field
|
||||||
field_name, field.name))
|
# is a ComplexBaseField, add the name current field name and
|
||||||
|
# move on.
|
||||||
if not new_field and isinstance(field, ComplexBaseField):
|
if not new_field and isinstance(field, ComplexBaseField):
|
||||||
fields.append(field_name)
|
fields.append(field_name)
|
||||||
continue
|
continue
|
||||||
elif not new_field:
|
elif not new_field:
|
||||||
raise LookUpError('Cannot resolve field "%s"'
|
raise LookUpError('Cannot resolve field "%s"' % field_name)
|
||||||
% field_name)
|
|
||||||
field = new_field # update field to the new field type
|
field = new_field # update field to the new field type
|
||||||
|
|
||||||
fields.append(field)
|
fields.append(field)
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1005,19 +1068,18 @@ class BaseDocument(object):
|
|||||||
return '.'.join(parts)
|
return '.'.join(parts)
|
||||||
|
|
||||||
def __set_field_display(self):
|
def __set_field_display(self):
|
||||||
"""Dynamically set the display value for a field with choices"""
|
"""For each field that specifies choices, create a
|
||||||
for attr_name, field in self._fields.items():
|
get_<field>_display method.
|
||||||
if field.choices:
|
"""
|
||||||
if self._dynamic:
|
fields_with_choices = [(n, f) for n, f in self._fields.items()
|
||||||
obj = self
|
if f.choices]
|
||||||
else:
|
for attr_name, field in fields_with_choices:
|
||||||
obj = type(self)
|
setattr(self,
|
||||||
setattr(obj,
|
'get_%s_display' % attr_name,
|
||||||
'get_%s_display' % attr_name,
|
partial(self.__get_field_display, field=field))
|
||||||
partial(self.__get_field_display, field=field))
|
|
||||||
|
|
||||||
def __get_field_display(self, 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)
|
value = getattr(self, field.name)
|
||||||
if field.choices and isinstance(field.choices[0], (list, tuple)):
|
if field.choices and isinstance(field.choices[0], (list, tuple)):
|
||||||
return dict(field.choices).get(value, value)
|
return dict(field.choices).get(value, value)
|
||||||
|
@@ -4,21 +4,17 @@ import weakref
|
|||||||
|
|
||||||
from bson import DBRef, ObjectId, SON
|
from bson import DBRef, ObjectId, SON
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import six
|
||||||
|
|
||||||
from mongoengine.base.common import ALLOW_INHERITANCE
|
from mongoengine.base.common import UPDATE_OPERATORS
|
||||||
from mongoengine.base.datastructures import (
|
from mongoengine.base.datastructures import (BaseDict, BaseList,
|
||||||
BaseDict, BaseList, EmbeddedDocumentList
|
EmbeddedDocumentList)
|
||||||
)
|
|
||||||
from mongoengine.common import _import_class
|
from mongoengine.common import _import_class
|
||||||
from mongoengine.errors import ValidationError
|
from mongoengine.errors import ValidationError
|
||||||
|
|
||||||
__all__ = ("BaseField", "ComplexBaseField",
|
|
||||||
"ObjectIdField", "GeoJsonBaseField")
|
|
||||||
|
|
||||||
|
__all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
|
||||||
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
|
'GeoJsonBaseField')
|
||||||
'push_all', 'pull', 'pull_all', 'add_to_set',
|
|
||||||
'set_on_insert', 'min', 'max'])
|
|
||||||
|
|
||||||
|
|
||||||
class BaseField(object):
|
class BaseField(object):
|
||||||
@@ -27,7 +23,6 @@ class BaseField(object):
|
|||||||
|
|
||||||
.. versionchanged:: 0.5 - added verbose and help text
|
.. versionchanged:: 0.5 - added verbose and help text
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = None
|
name = None
|
||||||
_geo_index = False
|
_geo_index = False
|
||||||
_auto_gen = False # Call `generate` to generate a value
|
_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
|
:param db_field: The database field to store this field in
|
||||||
(defaults to the name of the field)
|
(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
|
:param required: If the field is required. Whether it has to have a
|
||||||
value or not. Defaults to False.
|
value or not. Defaults to False.
|
||||||
:param default: (optional) The default value for this field if no value
|
: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'
|
self.db_field = (db_field or name) if not primary_key else '_id'
|
||||||
|
|
||||||
if name:
|
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)
|
warnings.warn(msg, DeprecationWarning)
|
||||||
self.required = required or primary_key
|
self.required = required or primary_key
|
||||||
self.default = default
|
self.default = default
|
||||||
@@ -86,10 +81,21 @@ class BaseField(object):
|
|||||||
self.sparse = sparse
|
self.sparse = sparse
|
||||||
self._owner_document = None
|
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.
|
# Detect and report conflicts between metadata and base properties.
|
||||||
conflicts = set(dir(self)) & set(kwargs)
|
conflicts = set(dir(self)) & set(kwargs)
|
||||||
if conflicts:
|
if conflicts:
|
||||||
raise TypeError("%s already has attribute(s): %s" % (
|
raise TypeError('%s already has attribute(s): %s' % (
|
||||||
self.__class__.__name__, ', '.join(conflicts)))
|
self.__class__.__name__, ', '.join(conflicts)))
|
||||||
|
|
||||||
# Assign metadata to the instance
|
# Assign metadata to the instance
|
||||||
@@ -147,25 +153,21 @@ class BaseField(object):
|
|||||||
v._instance = weakref.proxy(instance)
|
v._instance = weakref.proxy(instance)
|
||||||
instance._data[self.name] = value
|
instance._data[self.name] = value
|
||||||
|
|
||||||
def error(self, message="", errors=None, field_name=None):
|
def error(self, message='', errors=None, field_name=None):
|
||||||
"""Raises a ValidationError.
|
"""Raise a ValidationError."""
|
||||||
"""
|
|
||||||
field_name = field_name if field_name else self.name
|
field_name = field_name if field_name else self.name
|
||||||
raise ValidationError(message, errors=errors, field_name=field_name)
|
raise ValidationError(message, errors=errors, field_name=field_name)
|
||||||
|
|
||||||
def to_python(self, value):
|
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
|
return value
|
||||||
|
|
||||||
def to_mongo(self, 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)
|
return self.to_python(value)
|
||||||
|
|
||||||
def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
|
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
|
f_inputs = self.to_mongo.__code__.co_varnames
|
||||||
ex_vars = {}
|
ex_vars = {}
|
||||||
if 'fields' in f_inputs:
|
if 'fields' in f_inputs:
|
||||||
@@ -177,15 +179,13 @@ class BaseField(object):
|
|||||||
return self.to_mongo(value, **ex_vars)
|
return self.to_mongo(value, **ex_vars)
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
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:
|
if op in UPDATE_OPERATORS:
|
||||||
self.validate(value)
|
self.validate(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, value, clean=True):
|
def validate(self, value, clean=True):
|
||||||
"""Perform validation on a value.
|
"""Perform validation on a value."""
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _validate_choices(self, value):
|
def _validate_choices(self, value):
|
||||||
@@ -193,18 +193,21 @@ class BaseField(object):
|
|||||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||||
|
|
||||||
choice_list = self.choices
|
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]
|
choice_list = [k for k, _ in choice_list]
|
||||||
|
|
||||||
# Choices which are other types of Documents
|
# Choices which are other types of Documents
|
||||||
if isinstance(value, (Document, EmbeddedDocument)):
|
if isinstance(value, (Document, EmbeddedDocument)):
|
||||||
if not any(isinstance(value, c) for c in choice_list):
|
if not any(isinstance(value, c) for c in choice_list):
|
||||||
self.error(
|
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
|
# Choices which are types other than Documents
|
||||||
elif value not in choice_list:
|
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):
|
def _validate(self, value, **kwargs):
|
||||||
# Check the Choices Constraint
|
# Check the Choices Constraint
|
||||||
@@ -247,8 +250,7 @@ class ComplexBaseField(BaseField):
|
|||||||
field = None
|
field = None
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
"""Descriptor to automatically dereference references.
|
"""Descriptor to automatically dereference references."""
|
||||||
"""
|
|
||||||
if instance is None:
|
if instance is None:
|
||||||
# Document class being used rather than a document object
|
# Document class being used rather than a document object
|
||||||
return self
|
return self
|
||||||
@@ -260,7 +262,7 @@ class ComplexBaseField(BaseField):
|
|||||||
(self.field is None or isinstance(self.field,
|
(self.field is None or isinstance(self.field,
|
||||||
(GenericReferenceField, ReferenceField))))
|
(GenericReferenceField, ReferenceField))))
|
||||||
|
|
||||||
_dereference = _import_class("DeReference")()
|
_dereference = _import_class('DeReference')()
|
||||||
|
|
||||||
self._auto_dereference = instance._fields[self.name]._auto_dereference
|
self._auto_dereference = instance._fields[self.name]._auto_dereference
|
||||||
if instance._initialised and dereference and instance._data.get(self.name):
|
if instance._initialised and dereference and instance._data.get(self.name):
|
||||||
@@ -295,9 +297,8 @@ class ComplexBaseField(BaseField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
"""Convert a MongoDB-compatible type to a Python type.
|
"""Convert a MongoDB-compatible type to a Python type."""
|
||||||
"""
|
if isinstance(value, six.string_types):
|
||||||
if isinstance(value, basestring):
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if hasattr(value, 'to_python'):
|
if hasattr(value, 'to_python'):
|
||||||
@@ -307,14 +308,14 @@ class ComplexBaseField(BaseField):
|
|||||||
if not hasattr(value, 'items'):
|
if not hasattr(value, 'items'):
|
||||||
try:
|
try:
|
||||||
is_list = True
|
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
|
except TypeError: # Not iterable return the value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if self.field:
|
if self.field:
|
||||||
self.field._auto_dereference = self._auto_dereference
|
self.field._auto_dereference = self._auto_dereference
|
||||||
value_dict = dict([(key, self.field.to_python(item))
|
value_dict = {key: self.field.to_python(item)
|
||||||
for key, item in value.items()])
|
for key, item in value.items()}
|
||||||
else:
|
else:
|
||||||
Document = _import_class('Document')
|
Document = _import_class('Document')
|
||||||
value_dict = {}
|
value_dict = {}
|
||||||
@@ -337,13 +338,12 @@ class ComplexBaseField(BaseField):
|
|||||||
return value_dict
|
return value_dict
|
||||||
|
|
||||||
def to_mongo(self, value, use_db_field=True, fields=None):
|
def to_mongo(self, value, use_db_field=True, fields=None):
|
||||||
"""Convert a Python type to a MongoDB-compatible type.
|
"""Convert a Python type to a MongoDB-compatible type."""
|
||||||
"""
|
Document = _import_class('Document')
|
||||||
Document = _import_class("Document")
|
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||||
EmbeddedDocument = _import_class("EmbeddedDocument")
|
GenericReferenceField = _import_class('GenericReferenceField')
|
||||||
GenericReferenceField = _import_class("GenericReferenceField")
|
|
||||||
|
|
||||||
if isinstance(value, basestring):
|
if isinstance(value, six.string_types):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if hasattr(value, 'to_mongo'):
|
if hasattr(value, 'to_mongo'):
|
||||||
@@ -360,13 +360,15 @@ class ComplexBaseField(BaseField):
|
|||||||
if not hasattr(value, 'items'):
|
if not hasattr(value, 'items'):
|
||||||
try:
|
try:
|
||||||
is_list = True
|
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
|
except TypeError: # Not iterable return the value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if self.field:
|
if self.field:
|
||||||
value_dict = dict([(key, self.field._to_mongo_safe_call(item, use_db_field, fields))
|
value_dict = {
|
||||||
for key, item in value.iteritems()])
|
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
|
||||||
|
for key, item in value.iteritems()
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
value_dict = {}
|
value_dict = {}
|
||||||
for k, v in value.iteritems():
|
for k, v in value.iteritems():
|
||||||
@@ -380,9 +382,7 @@ class ComplexBaseField(BaseField):
|
|||||||
# any _cls data so make it a generic reference allows
|
# any _cls data so make it a generic reference allows
|
||||||
# us to dereference
|
# us to dereference
|
||||||
meta = getattr(v, '_meta', {})
|
meta = getattr(v, '_meta', {})
|
||||||
allow_inheritance = (
|
allow_inheritance = meta.get('allow_inheritance')
|
||||||
meta.get('allow_inheritance', ALLOW_INHERITANCE)
|
|
||||||
is True)
|
|
||||||
if not allow_inheritance and not self.field:
|
if not allow_inheritance and not self.field:
|
||||||
value_dict[k] = GenericReferenceField().to_mongo(v)
|
value_dict[k] = GenericReferenceField().to_mongo(v)
|
||||||
else:
|
else:
|
||||||
@@ -404,8 +404,7 @@ class ComplexBaseField(BaseField):
|
|||||||
return value_dict
|
return value_dict
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
"""If field is provided ensure the value is valid.
|
"""If field is provided ensure the value is valid."""
|
||||||
"""
|
|
||||||
errors = {}
|
errors = {}
|
||||||
if self.field:
|
if self.field:
|
||||||
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
|
if hasattr(value, 'iteritems') or hasattr(value, 'items'):
|
||||||
@@ -415,9 +414,9 @@ class ComplexBaseField(BaseField):
|
|||||||
for k, v in sequence:
|
for k, v in sequence:
|
||||||
try:
|
try:
|
||||||
self.field._validate(v)
|
self.field._validate(v)
|
||||||
except ValidationError, error:
|
except ValidationError as error:
|
||||||
errors[k] = error.errors or error
|
errors[k] = error.errors or error
|
||||||
except (ValueError, AssertionError), error:
|
except (ValueError, AssertionError) as error:
|
||||||
errors[k] = error
|
errors[k] = error
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
@@ -443,8 +442,7 @@ class ComplexBaseField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class ObjectIdField(BaseField):
|
class ObjectIdField(BaseField):
|
||||||
"""A field wrapper around MongoDB's ObjectIds.
|
"""A field wrapper around MongoDB's ObjectIds."""
|
||||||
"""
|
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
try:
|
try:
|
||||||
@@ -457,10 +455,10 @@ class ObjectIdField(BaseField):
|
|||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
if not isinstance(value, ObjectId):
|
if not isinstance(value, ObjectId):
|
||||||
try:
|
try:
|
||||||
return ObjectId(unicode(value))
|
return ObjectId(six.text_type(value))
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
# e.message attribute has been deprecated since Python 2.6
|
# e.message attribute has been deprecated since Python 2.6
|
||||||
self.error(unicode(e))
|
self.error(six.text_type(e))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
def prepare_query_value(self, op, value):
|
||||||
@@ -468,7 +466,7 @@ class ObjectIdField(BaseField):
|
|||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
try:
|
try:
|
||||||
ObjectId(unicode(value))
|
ObjectId(six.text_type(value))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.error('Invalid Object ID')
|
self.error('Invalid Object ID')
|
||||||
|
|
||||||
@@ -480,21 +478,20 @@ class GeoJsonBaseField(BaseField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_geo_index = pymongo.GEOSPHERE
|
_geo_index = pymongo.GEOSPHERE
|
||||||
_type = "GeoBase"
|
_type = 'GeoBase'
|
||||||
|
|
||||||
def __init__(self, auto_index=True, *args, **kwargs):
|
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`.
|
Defaults to `True`.
|
||||||
"""
|
"""
|
||||||
self._name = "%sField" % self._type
|
self._name = '%sField' % self._type
|
||||||
if not auto_index:
|
if not auto_index:
|
||||||
self._geo_index = False
|
self._geo_index = False
|
||||||
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
|
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def validate(self, value):
|
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 isinstance(value, dict):
|
||||||
if set(value.keys()) == set(['type', 'coordinates']):
|
if set(value.keys()) == set(['type', 'coordinates']):
|
||||||
if value['type'] != self._type:
|
if value['type'] != self._type:
|
||||||
@@ -509,7 +506,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
self.error('%s can only accept lists of [x, y]' % self._name)
|
self.error('%s can only accept lists of [x, y]' % self._name)
|
||||||
return
|
return
|
||||||
|
|
||||||
validate = getattr(self, "_validate_%s" % self._type.lower())
|
validate = getattr(self, '_validate_%s' % self._type.lower())
|
||||||
error = validate(value)
|
error = validate(value)
|
||||||
if error:
|
if error:
|
||||||
self.error(error)
|
self.error(error)
|
||||||
@@ -522,7 +519,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
try:
|
try:
|
||||||
value[0][0][0]
|
value[0][0][0]
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
return "Invalid Polygon must contain at least one valid linestring"
|
return 'Invalid Polygon must contain at least one valid linestring'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for val in value:
|
for val in value:
|
||||||
@@ -533,12 +530,12 @@ class GeoJsonBaseField(BaseField):
|
|||||||
errors.append(error)
|
errors.append(error)
|
||||||
if errors:
|
if errors:
|
||||||
if top_level:
|
if top_level:
|
||||||
return "Invalid Polygon:\n%s" % ", ".join(errors)
|
return 'Invalid Polygon:\n%s' % ', '.join(errors)
|
||||||
else:
|
else:
|
||||||
return "%s" % ", ".join(errors)
|
return '%s' % ', '.join(errors)
|
||||||
|
|
||||||
def _validate_linestring(self, value, top_level=True):
|
def _validate_linestring(self, value, top_level=True):
|
||||||
"""Validates a linestring"""
|
"""Validate a linestring."""
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
return 'LineStrings must contain list of coordinate pairs'
|
return 'LineStrings must contain list of coordinate pairs'
|
||||||
|
|
||||||
@@ -546,7 +543,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
try:
|
try:
|
||||||
value[0][0]
|
value[0][0]
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
return "Invalid LineString must contain at least one valid point"
|
return 'Invalid LineString must contain at least one valid point'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for val in value:
|
for val in value:
|
||||||
@@ -555,19 +552,19 @@ class GeoJsonBaseField(BaseField):
|
|||||||
errors.append(error)
|
errors.append(error)
|
||||||
if errors:
|
if errors:
|
||||||
if top_level:
|
if top_level:
|
||||||
return "Invalid LineString:\n%s" % ", ".join(errors)
|
return 'Invalid LineString:\n%s' % ', '.join(errors)
|
||||||
else:
|
else:
|
||||||
return "%s" % ", ".join(errors)
|
return '%s' % ', '.join(errors)
|
||||||
|
|
||||||
def _validate_point(self, value):
|
def _validate_point(self, value):
|
||||||
"""Validate each set of coords"""
|
"""Validate each set of coords"""
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
return 'Points must be a list of coordinate pairs'
|
return 'Points must be a list of coordinate pairs'
|
||||||
elif not len(value) == 2:
|
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
|
elif (not isinstance(value[0], (float, int)) or
|
||||||
not isinstance(value[1], (float, int))):
|
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):
|
def _validate_multipoint(self, value):
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
@@ -577,7 +574,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
try:
|
try:
|
||||||
value[0][0]
|
value[0][0]
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
return "Invalid MultiPoint must contain at least one valid point"
|
return 'Invalid MultiPoint must contain at least one valid point'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for point in value:
|
for point in value:
|
||||||
@@ -586,7 +583,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
errors.append(error)
|
errors.append(error)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return "%s" % ", ".join(errors)
|
return '%s' % ', '.join(errors)
|
||||||
|
|
||||||
def _validate_multilinestring(self, value, top_level=True):
|
def _validate_multilinestring(self, value, top_level=True):
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
@@ -596,7 +593,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
try:
|
try:
|
||||||
value[0][0][0]
|
value[0][0][0]
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
return "Invalid MultiLineString must contain at least one valid linestring"
|
return 'Invalid MultiLineString must contain at least one valid linestring'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for linestring in value:
|
for linestring in value:
|
||||||
@@ -606,9 +603,9 @@ class GeoJsonBaseField(BaseField):
|
|||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
if top_level:
|
if top_level:
|
||||||
return "Invalid MultiLineString:\n%s" % ", ".join(errors)
|
return 'Invalid MultiLineString:\n%s' % ', '.join(errors)
|
||||||
else:
|
else:
|
||||||
return "%s" % ", ".join(errors)
|
return '%s' % ', '.join(errors)
|
||||||
|
|
||||||
def _validate_multipolygon(self, value):
|
def _validate_multipolygon(self, value):
|
||||||
if not isinstance(value, (list, tuple)):
|
if not isinstance(value, (list, tuple)):
|
||||||
@@ -618,7 +615,7 @@ class GeoJsonBaseField(BaseField):
|
|||||||
try:
|
try:
|
||||||
value[0][0][0][0]
|
value[0][0][0][0]
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
return "Invalid MultiPolygon must contain at least one valid Polygon"
|
return 'Invalid MultiPolygon must contain at least one valid Polygon'
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
for polygon in value:
|
for polygon in value:
|
||||||
@@ -627,9 +624,9 @@ class GeoJsonBaseField(BaseField):
|
|||||||
errors.append(error)
|
errors.append(error)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
return "Invalid MultiPolygon:\n%s" % ", ".join(errors)
|
return 'Invalid MultiPolygon:\n%s' % ', '.join(errors)
|
||||||
|
|
||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
return value
|
return value
|
||||||
return SON([("type", self._type), ("coordinates", value)])
|
return SON([('type', self._type), ('coordinates', value)])
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import warnings
|
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.base.fields import BaseField, ComplexBaseField, ObjectIdField
|
||||||
from mongoengine.common import _import_class
|
from mongoengine.common import _import_class
|
||||||
from mongoengine.errors import InvalidDocumentError
|
from mongoengine.errors import InvalidDocumentError
|
||||||
from mongoengine.python_support import PY3
|
|
||||||
from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
|
from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
|
||||||
MultipleObjectsReturned,
|
MultipleObjectsReturned,
|
||||||
QuerySetManager)
|
QuerySetManager)
|
||||||
@@ -45,7 +46,8 @@ class DocumentMetaclass(type):
|
|||||||
attrs['_meta'] = meta
|
attrs['_meta'] = meta
|
||||||
attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
|
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')
|
StringField = _import_class('StringField')
|
||||||
attrs['_cls'] = StringField()
|
attrs['_cls'] = StringField()
|
||||||
|
|
||||||
@@ -87,16 +89,17 @@ class DocumentMetaclass(type):
|
|||||||
# Ensure no duplicate db_fields
|
# Ensure no duplicate db_fields
|
||||||
duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
|
duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
|
||||||
if duplicate_db_fields:
|
if duplicate_db_fields:
|
||||||
msg = ("Multiple db_fields defined for: %s " %
|
msg = ('Multiple db_fields defined for: %s ' %
|
||||||
", ".join(duplicate_db_fields))
|
', '.join(duplicate_db_fields))
|
||||||
raise InvalidDocumentError(msg)
|
raise InvalidDocumentError(msg)
|
||||||
|
|
||||||
# Set _fields and db_field maps
|
# Set _fields and db_field maps
|
||||||
attrs['_fields'] = doc_fields
|
attrs['_fields'] = doc_fields
|
||||||
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
|
attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
|
||||||
for k, v in doc_fields.iteritems()])
|
for k, v in doc_fields.items()}
|
||||||
attrs['_reverse_db_field_map'] = dict(
|
attrs['_reverse_db_field_map'] = {
|
||||||
(v, k) for k, v in attrs['_db_field_map'].iteritems())
|
v: k for k, v in attrs['_db_field_map'].items()
|
||||||
|
}
|
||||||
|
|
||||||
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
|
attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
|
||||||
(v.creation_counter, v.name)
|
(v.creation_counter, v.name)
|
||||||
@@ -116,10 +119,8 @@ class DocumentMetaclass(type):
|
|||||||
if hasattr(base, '_meta'):
|
if hasattr(base, '_meta'):
|
||||||
# Warn if allow_inheritance isn't set and prevent
|
# Warn if allow_inheritance isn't set and prevent
|
||||||
# inheritance of classes where inheritance is set to False
|
# inheritance of classes where inheritance is set to False
|
||||||
allow_inheritance = base._meta.get('allow_inheritance',
|
allow_inheritance = base._meta.get('allow_inheritance')
|
||||||
ALLOW_INHERITANCE)
|
if not allow_inheritance and not base._meta.get('abstract'):
|
||||||
if (allow_inheritance is not True and
|
|
||||||
not base._meta.get('abstract')):
|
|
||||||
raise ValueError('Document %s may not be subclassed' %
|
raise ValueError('Document %s may not be subclassed' %
|
||||||
base.__name__)
|
base.__name__)
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ class DocumentMetaclass(type):
|
|||||||
# module continues to use im_func and im_self, so the code below
|
# module continues to use im_func and im_self, so the code below
|
||||||
# copies __func__ into im_func and __self__ into im_self for
|
# copies __func__ into im_func and __self__ into im_self for
|
||||||
# classmethod objects in Document derived classes.
|
# classmethod objects in Document derived classes.
|
||||||
if PY3:
|
if six.PY3:
|
||||||
for val in new_class.__dict__.values():
|
for val in new_class.__dict__.values():
|
||||||
if isinstance(val, classmethod):
|
if isinstance(val, classmethod):
|
||||||
f = val.__get__(new_class)
|
f = val.__get__(new_class)
|
||||||
@@ -179,11 +180,11 @@ class DocumentMetaclass(type):
|
|||||||
if isinstance(f, CachedReferenceField):
|
if isinstance(f, CachedReferenceField):
|
||||||
|
|
||||||
if issubclass(new_class, EmbeddedDocument):
|
if issubclass(new_class, EmbeddedDocument):
|
||||||
raise InvalidDocumentError(
|
raise InvalidDocumentError('CachedReferenceFields is not '
|
||||||
"CachedReferenceFields is not allowed in EmbeddedDocuments")
|
'allowed in EmbeddedDocuments')
|
||||||
if not f.document_type:
|
if not f.document_type:
|
||||||
raise InvalidDocumentError(
|
raise InvalidDocumentError(
|
||||||
"Document is not available to sync")
|
'Document is not available to sync')
|
||||||
|
|
||||||
if f.auto_sync:
|
if f.auto_sync:
|
||||||
f.start_listener()
|
f.start_listener()
|
||||||
@@ -195,8 +196,8 @@ class DocumentMetaclass(type):
|
|||||||
'reverse_delete_rule',
|
'reverse_delete_rule',
|
||||||
DO_NOTHING)
|
DO_NOTHING)
|
||||||
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
|
if isinstance(f, DictField) and delete_rule != DO_NOTHING:
|
||||||
msg = ("Reverse delete rules are not supported "
|
msg = ('Reverse delete rules are not supported '
|
||||||
"for %s (field: %s)" %
|
'for %s (field: %s)' %
|
||||||
(field.__class__.__name__, field.name))
|
(field.__class__.__name__, field.name))
|
||||||
raise InvalidDocumentError(msg)
|
raise InvalidDocumentError(msg)
|
||||||
|
|
||||||
@@ -204,16 +205,16 @@ class DocumentMetaclass(type):
|
|||||||
|
|
||||||
if delete_rule != DO_NOTHING:
|
if delete_rule != DO_NOTHING:
|
||||||
if issubclass(new_class, EmbeddedDocument):
|
if issubclass(new_class, EmbeddedDocument):
|
||||||
msg = ("Reverse delete rules are not supported for "
|
msg = ('Reverse delete rules are not supported for '
|
||||||
"EmbeddedDocuments (field: %s)" % field.name)
|
'EmbeddedDocuments (field: %s)' % field.name)
|
||||||
raise InvalidDocumentError(msg)
|
raise InvalidDocumentError(msg)
|
||||||
f.document_type.register_delete_rule(new_class,
|
f.document_type.register_delete_rule(new_class,
|
||||||
field.name, delete_rule)
|
field.name, delete_rule)
|
||||||
|
|
||||||
if (field.name and hasattr(Document, field.name) and
|
if (field.name and hasattr(Document, field.name) and
|
||||||
EmbeddedDocument not in new_class.mro()):
|
EmbeddedDocument not in new_class.mro()):
|
||||||
msg = ("%s is a document method and not a valid "
|
msg = ('%s is a document method and not a valid '
|
||||||
"field name" % field.name)
|
'field name' % field.name)
|
||||||
raise InvalidDocumentError(msg)
|
raise InvalidDocumentError(msg)
|
||||||
|
|
||||||
return new_class
|
return new_class
|
||||||
@@ -271,6 +272,11 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
'index_drop_dups': False,
|
'index_drop_dups': False,
|
||||||
'index_opts': None,
|
'index_opts': None,
|
||||||
'delete_rules': 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,
|
'allow_inheritance': None,
|
||||||
}
|
}
|
||||||
attrs['_is_base_cls'] = True
|
attrs['_is_base_cls'] = True
|
||||||
@@ -303,7 +309,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
# If parent wasn't an abstract class
|
# If parent wasn't an abstract class
|
||||||
if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
|
if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
|
||||||
not parent_doc_cls._meta.get('abstract', True)):
|
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)
|
warnings.warn(msg, SyntaxWarning)
|
||||||
del attrs['_meta']['collection']
|
del attrs['_meta']['collection']
|
||||||
|
|
||||||
@@ -311,7 +317,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
|
if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
|
||||||
if (parent_doc_cls and
|
if (parent_doc_cls and
|
||||||
not parent_doc_cls._meta.get('abstract', False)):
|
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)
|
raise ValueError(msg)
|
||||||
return super_new(cls, name, bases, attrs)
|
return super_new(cls, name, bases, attrs)
|
||||||
|
|
||||||
@@ -334,12 +340,16 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
|
|
||||||
meta.merge(attrs.get('_meta', {})) # Top level meta
|
meta.merge(attrs.get('_meta', {})) # Top level meta
|
||||||
|
|
||||||
# Only simple classes (direct subclasses of Document)
|
# Only simple classes (i.e. direct subclasses of Document) may set
|
||||||
# may set allow_inheritance to False
|
# 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')
|
simple_class = all([b._meta.get('abstract')
|
||||||
for b in flattened_bases if hasattr(b, '_meta')])
|
for b in flattened_bases if hasattr(b, '_meta')])
|
||||||
if (not simple_class and meta['allow_inheritance'] is False and
|
if (
|
||||||
not meta['abstract']):
|
not simple_class and
|
||||||
|
meta['allow_inheritance'] is False and
|
||||||
|
not meta['abstract']
|
||||||
|
):
|
||||||
raise ValueError('Only direct subclasses of Document may set '
|
raise ValueError('Only direct subclasses of Document may set '
|
||||||
'"allow_inheritance" to False')
|
'"allow_inheritance" to False')
|
||||||
|
|
||||||
|
@@ -34,7 +34,10 @@ def _import_class(cls_name):
|
|||||||
queryset_classes = ('OperationError',)
|
queryset_classes = ('OperationError',)
|
||||||
deref_classes = ('DeReference',)
|
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
|
from mongoengine import document as module
|
||||||
import_classes = doc_classes
|
import_classes = doc_classes
|
||||||
elif cls_name in field_classes:
|
elif cls_name in field_classes:
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
from pymongo import MongoClient, ReadPreference, uri_parser
|
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']
|
'DEFAULT_CONNECTION_NAME']
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONNECTION_NAME = 'default'
|
DEFAULT_CONNECTION_NAME = 'default'
|
||||||
|
|
||||||
if IS_PYMONGO_3:
|
if IS_PYMONGO_3:
|
||||||
READ_PREFERENCE = ReadPreference.PRIMARY
|
READ_PREFERENCE = ReadPreference.PRIMARY
|
||||||
else:
|
else:
|
||||||
@@ -13,7 +16,10 @@ else:
|
|||||||
READ_PREFERENCE = False
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -24,7 +30,9 @@ _dbs = {}
|
|||||||
|
|
||||||
def register_connection(alias, name=None, host=None, port=None,
|
def register_connection(alias, name=None, host=None, port=None,
|
||||||
read_preference=READ_PREFERENCE,
|
read_preference=READ_PREFERENCE,
|
||||||
username=None, password=None, authentication_source=None,
|
username=None, password=None,
|
||||||
|
authentication_source=None,
|
||||||
|
authentication_mechanism=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Add a connection.
|
"""Add a connection.
|
||||||
|
|
||||||
@@ -38,14 +46,17 @@ def register_connection(alias, name=None, host=None, port=None,
|
|||||||
:param username: username to authenticate with
|
:param username: username to authenticate with
|
||||||
:param password: password to authenticate with
|
:param password: password to authenticate with
|
||||||
:param authentication_source: database to authenticate against
|
:param authentication_source: database to authenticate against
|
||||||
|
:param authentication_mechanism: database authentication mechanisms.
|
||||||
|
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
|
||||||
|
MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
|
||||||
:param is_mock: explicitly use mongomock for this connection
|
:param is_mock: explicitly use mongomock for this connection
|
||||||
(can also be done by using `mongomock://` as db host prefix)
|
(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
|
.. versionchanged:: 0.10.6 - added mongomock support
|
||||||
"""
|
"""
|
||||||
global _connection_settings
|
|
||||||
|
|
||||||
conn_settings = {
|
conn_settings = {
|
||||||
'name': name or 'test',
|
'name': name or 'test',
|
||||||
'host': host or 'localhost',
|
'host': host or 'localhost',
|
||||||
@@ -53,35 +64,45 @@ def register_connection(alias, name=None, host=None, port=None,
|
|||||||
'read_preference': read_preference,
|
'read_preference': read_preference,
|
||||||
'username': username,
|
'username': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'authentication_source': authentication_source
|
'authentication_source': authentication_source,
|
||||||
|
'authentication_mechanism': authentication_mechanism
|
||||||
}
|
}
|
||||||
|
|
||||||
conn_host = conn_settings['host']
|
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]
|
conn_host = [conn_host]
|
||||||
|
|
||||||
resolved_hosts = []
|
resolved_hosts = []
|
||||||
for entity in conn_host:
|
for entity in conn_host:
|
||||||
# Handle uri style connections
|
|
||||||
|
# Handle Mongomock
|
||||||
if entity.startswith('mongomock://'):
|
if entity.startswith('mongomock://'):
|
||||||
conn_settings['is_mock'] = True
|
conn_settings['is_mock'] = True
|
||||||
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
|
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
|
||||||
resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
|
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:
|
elif '://' in entity:
|
||||||
uri_dict = uri_parser.parse_uri(entity)
|
uri_dict = uri_parser.parse_uri(entity)
|
||||||
resolved_hosts.append(entity)
|
resolved_hosts.append(entity)
|
||||||
conn_settings.update({
|
|
||||||
'name': uri_dict.get('database') or name,
|
if uri_dict.get('database'):
|
||||||
'username': uri_dict.get('username'),
|
conn_settings['name'] = uri_dict.get('database')
|
||||||
'password': uri_dict.get('password'),
|
|
||||||
'read_preference': read_preference,
|
for param in ('read_preference', 'username', 'password'):
|
||||||
})
|
if uri_dict.get(param):
|
||||||
|
conn_settings[param] = uri_dict[param]
|
||||||
|
|
||||||
uri_options = uri_dict['options']
|
uri_options = uri_dict['options']
|
||||||
if 'replicaset' in uri_options:
|
if 'replicaset' in uri_options:
|
||||||
conn_settings['replicaSet'] = True
|
conn_settings['replicaSet'] = uri_options['replicaset']
|
||||||
if 'authsource' in uri_options:
|
if 'authsource' in uri_options:
|
||||||
conn_settings['authentication_source'] = uri_options['authsource']
|
conn_settings['authentication_source'] = uri_options['authsource']
|
||||||
|
if 'authmechanism' in uri_options:
|
||||||
|
conn_settings['authentication_mechanism'] = uri_options['authmechanism']
|
||||||
else:
|
else:
|
||||||
resolved_hosts.append(entity)
|
resolved_hosts.append(entity)
|
||||||
conn_settings['host'] = resolved_hosts
|
conn_settings['host'] = resolved_hosts
|
||||||
@@ -95,9 +116,7 @@ def register_connection(alias, name=None, host=None, port=None,
|
|||||||
|
|
||||||
|
|
||||||
def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
||||||
global _connections
|
"""Close the connection with a given alias."""
|
||||||
global _dbs
|
|
||||||
|
|
||||||
if alias in _connections:
|
if alias in _connections:
|
||||||
get_connection(alias=alias).close()
|
get_connection(alias=alias).close()
|
||||||
del _connections[alias]
|
del _connections[alias]
|
||||||
@@ -106,69 +125,99 @@ def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
|||||||
|
|
||||||
|
|
||||||
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
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
|
# Connect to the database if not already connected
|
||||||
if reconnect:
|
if reconnect:
|
||||||
disconnect(alias)
|
disconnect(alias)
|
||||||
|
|
||||||
if alias not in _connections:
|
# If the requested alias already exists in the _connections list, return
|
||||||
if alias not in _connection_settings:
|
# it immediately.
|
||||||
msg = 'Connection with alias "%s" has not been defined' % alias
|
if alias in _connections:
|
||||||
if alias == DEFAULT_CONNECTION_NAME:
|
return _connections[alias]
|
||||||
msg = 'You have not defined a default connection'
|
|
||||||
raise ConnectionError(msg)
|
|
||||||
conn_settings = _connection_settings[alias].copy()
|
|
||||||
|
|
||||||
conn_settings.pop('name', None)
|
# Validate that the requested alias exists in the _connection_settings.
|
||||||
conn_settings.pop('username', None)
|
# Raise MongoEngineConnectionError if it doesn't.
|
||||||
conn_settings.pop('password', None)
|
if alias not in _connection_settings:
|
||||||
conn_settings.pop('authentication_source', None)
|
if alias == DEFAULT_CONNECTION_NAME:
|
||||||
|
msg = 'You have not defined a default connection'
|
||||||
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
|
|
||||||
else:
|
else:
|
||||||
# Use MongoClient from pymongo
|
msg = 'Connection with alias "%s" has not been defined' % alias
|
||||||
connection_class = MongoClient
|
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
|
# Discard port since it can't be used on MongoReplicaSetClient
|
||||||
conn_settings.pop('port', None)
|
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:
|
try:
|
||||||
connection = None
|
_connections[alias] = connection_class(**conn_settings)
|
||||||
# check for shared connections
|
except Exception as e:
|
||||||
connection_settings_iterator = (
|
raise MongoEngineConnectionError(
|
||||||
(db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems())
|
'Cannot connect to database %s :\n%s' % (alias, e))
|
||||||
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)
|
|
||||||
if conn_settings == connection_settings and _connections.get(db_alias, None):
|
|
||||||
connection = _connections[db_alias]
|
|
||||||
break
|
|
||||||
|
|
||||||
_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]
|
return _connections[alias]
|
||||||
|
|
||||||
|
|
||||||
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||||
global _dbs
|
|
||||||
if reconnect:
|
if reconnect:
|
||||||
disconnect(alias)
|
disconnect(alias)
|
||||||
|
|
||||||
@@ -176,11 +225,13 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
|||||||
conn = get_connection(alias)
|
conn = get_connection(alias)
|
||||||
conn_settings = _connection_settings[alias]
|
conn_settings = _connection_settings[alias]
|
||||||
db = conn[conn_settings['name']]
|
db = conn[conn_settings['name']]
|
||||||
|
auth_kwargs = {'source': conn_settings['authentication_source']}
|
||||||
|
if conn_settings['authentication_mechanism'] is not None:
|
||||||
|
auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
|
||||||
# Authenticate if necessary
|
# Authenticate if necessary
|
||||||
if conn_settings['username'] and conn_settings['password']:
|
if conn_settings['username'] and (conn_settings['password'] or
|
||||||
db.authenticate(conn_settings['username'],
|
conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
|
||||||
conn_settings['password'],
|
db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
|
||||||
source=conn_settings['authentication_source'])
|
|
||||||
_dbs[alias] = db
|
_dbs[alias] = db
|
||||||
return _dbs[alias]
|
return _dbs[alias]
|
||||||
|
|
||||||
@@ -192,12 +243,14 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
|
|||||||
running on the default port on localhost. If authentication is needed,
|
running on the default port on localhost. If authentication is needed,
|
||||||
provide username and password arguments as well.
|
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`.
|
`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.
|
.. versionchanged:: 0.6 - added multiple database support.
|
||||||
"""
|
"""
|
||||||
global _connections
|
|
||||||
if alias not in _connections:
|
if alias not in _connections:
|
||||||
register_connection(alias, db, **kwargs)
|
register_connection(alias, db, **kwargs)
|
||||||
|
|
||||||
|
@@ -2,12 +2,12 @@ from mongoengine.common import _import_class
|
|||||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("switch_db", "switch_collection", "no_dereference",
|
__all__ = ('switch_db', 'switch_collection', 'no_dereference',
|
||||||
"no_sub_classes", "query_counter")
|
'no_sub_classes', 'query_counter')
|
||||||
|
|
||||||
|
|
||||||
class switch_db(object):
|
class switch_db(object):
|
||||||
""" switch_db alias context manager.
|
"""switch_db alias context manager.
|
||||||
|
|
||||||
Example ::
|
Example ::
|
||||||
|
|
||||||
@@ -18,15 +18,14 @@ class switch_db(object):
|
|||||||
class Group(Document):
|
class Group(Document):
|
||||||
name = StringField()
|
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:
|
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):
|
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 cls: the class to change the registered db
|
||||||
:param db_alias: the name of the specific database to use
|
:param db_alias: the name of the specific database to use
|
||||||
@@ -34,37 +33,36 @@ class switch_db(object):
|
|||||||
self.cls = cls
|
self.cls = cls
|
||||||
self.collection = cls._get_collection()
|
self.collection = cls._get_collection()
|
||||||
self.db_alias = db_alias
|
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):
|
def __enter__(self):
|
||||||
""" change the db_alias and clear the cached collection """
|
"""Change the db_alias and clear the cached collection."""
|
||||||
self.cls._meta["db_alias"] = self.db_alias
|
self.cls._meta['db_alias'] = self.db_alias
|
||||||
self.cls._collection = None
|
self.cls._collection = None
|
||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
def __exit__(self, t, value, traceback):
|
def __exit__(self, t, value, traceback):
|
||||||
""" Reset the db_alias and collection """
|
"""Reset the db_alias and collection."""
|
||||||
self.cls._meta["db_alias"] = self.ori_db_alias
|
self.cls._meta['db_alias'] = self.ori_db_alias
|
||||||
self.cls._collection = self.collection
|
self.cls._collection = self.collection
|
||||||
|
|
||||||
|
|
||||||
class switch_collection(object):
|
class switch_collection(object):
|
||||||
""" switch_collection alias context manager.
|
"""switch_collection alias context manager.
|
||||||
|
|
||||||
Example ::
|
Example ::
|
||||||
|
|
||||||
class Group(Document):
|
class Group(Document):
|
||||||
name = StringField()
|
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:
|
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):
|
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 cls: the class to change the registered db
|
||||||
:param collection_name: the name of the collection to use
|
:param collection_name: the name of the collection to use
|
||||||
@@ -75,7 +73,7 @@ class switch_collection(object):
|
|||||||
self.collection_name = collection_name
|
self.collection_name = collection_name
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
""" change the _get_collection_name and clear the cached collection """
|
"""Change the _get_collection_name and clear the cached collection."""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_collection_name(cls):
|
def _get_collection_name(cls):
|
||||||
@@ -86,24 +84,23 @@ class switch_collection(object):
|
|||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
def __exit__(self, t, value, traceback):
|
def __exit__(self, t, value, traceback):
|
||||||
""" Reset the collection """
|
"""Reset the collection."""
|
||||||
self.cls._collection = self.ori_collection
|
self.cls._collection = self.ori_collection
|
||||||
self.cls._get_collection_name = self.ori_get_collection_name
|
self.cls._get_collection_name = self.ori_get_collection_name
|
||||||
|
|
||||||
|
|
||||||
class no_dereference(object):
|
class no_dereference(object):
|
||||||
""" no_dereference context manager.
|
"""no_dereference context manager.
|
||||||
|
|
||||||
Turns off all dereferencing in Documents for the duration of the context
|
Turns off all dereferencing in Documents for the duration of the context
|
||||||
manager::
|
manager::
|
||||||
|
|
||||||
with no_dereference(Group) as Group:
|
with no_dereference(Group) as Group:
|
||||||
Group.objects.find()
|
Group.objects.find()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cls):
|
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
|
:param cls: the class to turn dereferencing off on
|
||||||
"""
|
"""
|
||||||
@@ -119,103 +116,102 @@ class no_dereference(object):
|
|||||||
ComplexBaseField))]
|
ComplexBaseField))]
|
||||||
|
|
||||||
def __enter__(self):
|
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:
|
for field in self.deref_fields:
|
||||||
self.cls._fields[field]._auto_dereference = False
|
self.cls._fields[field]._auto_dereference = False
|
||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
def __exit__(self, t, value, traceback):
|
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:
|
for field in self.deref_fields:
|
||||||
self.cls._fields[field]._auto_dereference = True
|
self.cls._fields[field]._auto_dereference = True
|
||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
|
|
||||||
class no_sub_classes(object):
|
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::
|
Only returns instances of this class and no sub (inherited) classes::
|
||||||
|
|
||||||
with no_sub_classes(Group) as Group:
|
with no_sub_classes(Group) as Group:
|
||||||
Group.objects.find()
|
Group.objects.find()
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cls):
|
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
|
:param cls: the class to turn querying sub classes on
|
||||||
"""
|
"""
|
||||||
self.cls = cls
|
self.cls = cls
|
||||||
|
|
||||||
def __enter__(self):
|
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._all_subclasses = self.cls._subclasses
|
||||||
self.cls._subclasses = (self.cls,)
|
self.cls._subclasses = (self.cls,)
|
||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
def __exit__(self, t, value, traceback):
|
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
|
self.cls._subclasses = self.cls._all_subclasses
|
||||||
delattr(self.cls, '_all_subclasses')
|
delattr(self.cls, '_all_subclasses')
|
||||||
return self.cls
|
return self.cls
|
||||||
|
|
||||||
|
|
||||||
class query_counter(object):
|
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):
|
def __init__(self):
|
||||||
""" Construct the query_counter. """
|
"""Construct the query_counter."""
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
self.db = get_db()
|
self.db = get_db()
|
||||||
|
|
||||||
def __enter__(self):
|
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.set_profiling_level(0)
|
||||||
self.db.system.profile.drop()
|
self.db.system.profile.drop()
|
||||||
self.db.set_profiling_level(2)
|
self.db.set_profiling_level(2)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, t, value, traceback):
|
def __exit__(self, t, value, traceback):
|
||||||
""" Reset the profiling level. """
|
"""Reset the profiling level."""
|
||||||
self.db.set_profiling_level(0)
|
self.db.set_profiling_level(0)
|
||||||
|
|
||||||
def __eq__(self, value):
|
def __eq__(self, value):
|
||||||
""" == Compare querycounter. """
|
"""== Compare querycounter."""
|
||||||
counter = self._get_count()
|
counter = self._get_count()
|
||||||
return value == counter
|
return value == counter
|
||||||
|
|
||||||
def __ne__(self, value):
|
def __ne__(self, value):
|
||||||
""" != Compare querycounter. """
|
"""!= Compare querycounter."""
|
||||||
return not self.__eq__(value)
|
return not self.__eq__(value)
|
||||||
|
|
||||||
def __lt__(self, value):
|
def __lt__(self, value):
|
||||||
""" < Compare querycounter. """
|
"""< Compare querycounter."""
|
||||||
return self._get_count() < value
|
return self._get_count() < value
|
||||||
|
|
||||||
def __le__(self, value):
|
def __le__(self, value):
|
||||||
""" <= Compare querycounter. """
|
"""<= Compare querycounter."""
|
||||||
return self._get_count() <= value
|
return self._get_count() <= value
|
||||||
|
|
||||||
def __gt__(self, value):
|
def __gt__(self, value):
|
||||||
""" > Compare querycounter. """
|
"""> Compare querycounter."""
|
||||||
return self._get_count() > value
|
return self._get_count() > value
|
||||||
|
|
||||||
def __ge__(self, value):
|
def __ge__(self, value):
|
||||||
""" >= Compare querycounter. """
|
""">= Compare querycounter."""
|
||||||
return self._get_count() >= value
|
return self._get_count() >= value
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
""" int representation. """
|
"""int representation."""
|
||||||
return self._get_count()
|
return self._get_count()
|
||||||
|
|
||||||
def __repr__(self):
|
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()
|
return u"%s" % self._get_count()
|
||||||
|
|
||||||
def _get_count(self):
|
def _get_count(self):
|
||||||
""" Get the number of queries. """
|
"""Get the number of queries."""
|
||||||
ignore_query = {"ns": {"$ne": "%s.system.indexes" % self.db.name}}
|
ignore_query = {'ns': {'$ne': '%s.system.indexes' % self.db.name}}
|
||||||
count = self.db.system.profile.find(ignore_query).count() - self.counter
|
count = self.db.system.profile.find(ignore_query).count() - self.counter
|
||||||
self.counter += 1
|
self.counter += 1
|
||||||
return count
|
return count
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
from bson import DBRef, SON
|
from bson import DBRef, SON
|
||||||
|
import six
|
||||||
|
|
||||||
from .base import (
|
from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
|
||||||
BaseDict, BaseList, EmbeddedDocumentList,
|
TopLevelDocumentMetaclass, get_document)
|
||||||
TopLevelDocumentMetaclass, get_document
|
from mongoengine.connection import get_db
|
||||||
)
|
from mongoengine.document import Document, EmbeddedDocument
|
||||||
from .connection import get_db
|
from mongoengine.fields import DictField, ListField, MapField, ReferenceField
|
||||||
from .document import Document, EmbeddedDocument
|
from mongoengine.queryset import QuerySet
|
||||||
from .fields import DictField, ListField, MapField, ReferenceField
|
|
||||||
from .python_support import txt_type
|
|
||||||
from .queryset import QuerySet
|
|
||||||
|
|
||||||
|
|
||||||
class DeReference(object):
|
class DeReference(object):
|
||||||
@@ -25,7 +23,7 @@ class DeReference(object):
|
|||||||
:class:`~mongoengine.base.ComplexBaseField`
|
:class:`~mongoengine.base.ComplexBaseField`
|
||||||
:param get: A boolean determining if being called by __get__
|
: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
|
return items
|
||||||
|
|
||||||
# cheapest way to convert a queryset to a list
|
# cheapest way to convert a queryset to a list
|
||||||
@@ -68,11 +66,11 @@ class DeReference(object):
|
|||||||
|
|
||||||
items = _get_items(items)
|
items = _get_items(items)
|
||||||
else:
|
else:
|
||||||
items = dict([
|
items = {
|
||||||
(k, field.to_python(v))
|
k: (v if isinstance(v, (DBRef, Document))
|
||||||
if not isinstance(v, (DBRef, Document)) else (k, v)
|
else field.to_python(v))
|
||||||
for k, v in items.iteritems()]
|
for k, v in items.iteritems()
|
||||||
)
|
}
|
||||||
|
|
||||||
self.reference_map = self._find_references(items)
|
self.reference_map = self._find_references(items)
|
||||||
self.object_map = self._fetch_objects(doc_type=doc_type)
|
self.object_map = self._fetch_objects(doc_type=doc_type)
|
||||||
@@ -90,14 +88,14 @@ class DeReference(object):
|
|||||||
return reference_map
|
return reference_map
|
||||||
|
|
||||||
# Determine the iterator to use
|
# Determine the iterator to use
|
||||||
if not hasattr(items, 'items'):
|
if isinstance(items, dict):
|
||||||
iterator = enumerate(items)
|
iterator = items.values()
|
||||||
else:
|
else:
|
||||||
iterator = items.iteritems()
|
iterator = items
|
||||||
|
|
||||||
# Recursively find dbreferences
|
# Recursively find dbreferences
|
||||||
depth += 1
|
depth += 1
|
||||||
for k, item in iterator:
|
for item in iterator:
|
||||||
if isinstance(item, (Document, EmbeddedDocument)):
|
if isinstance(item, (Document, EmbeddedDocument)):
|
||||||
for field_name, field in item._fields.iteritems():
|
for field_name, field in item._fields.iteritems():
|
||||||
v = item._data.get(field_name, None)
|
v = item._data.get(field_name, None)
|
||||||
@@ -151,7 +149,7 @@ class DeReference(object):
|
|||||||
references = get_db()[collection].find({'_id': {'$in': refs}})
|
references = get_db()[collection].find({'_id': {'$in': refs}})
|
||||||
for ref in references:
|
for ref in references:
|
||||||
if '_cls' in ref:
|
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:
|
elif doc_type is None:
|
||||||
doc = get_document(
|
doc = get_document(
|
||||||
''.join(x.capitalize()
|
''.join(x.capitalize()
|
||||||
@@ -218,7 +216,7 @@ class DeReference(object):
|
|||||||
if k in self.object_map and not is_list:
|
if k in self.object_map and not is_list:
|
||||||
data[k] = self.object_map[k]
|
data[k] = self.object_map[k]
|
||||||
elif isinstance(v, (Document, EmbeddedDocument)):
|
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)
|
v = data[k]._data.get(field_name, None)
|
||||||
if isinstance(v, DBRef):
|
if isinstance(v, DBRef):
|
||||||
data[k]._data[field_name] = self.object_map.get(
|
data[k]._data[field_name] = self.object_map.get(
|
||||||
@@ -227,7 +225,7 @@ class DeReference(object):
|
|||||||
data[k]._data[field_name] = self.object_map.get(
|
data[k]._data[field_name] = self.object_map.get(
|
||||||
(v['_ref'].collection, v['_ref'].id), v)
|
(v['_ref'].collection, v['_ref'].id), v)
|
||||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
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)
|
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:
|
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||||
item_name = '%s.%s' % (name, k) if name else name
|
item_name = '%s.%s' % (name, k) if name else name
|
||||||
|
@@ -4,18 +4,12 @@ import warnings
|
|||||||
from bson.dbref import DBRef
|
from bson.dbref import DBRef
|
||||||
import pymongo
|
import pymongo
|
||||||
from pymongo.read_preferences import ReadPreference
|
from pymongo.read_preferences import ReadPreference
|
||||||
|
import six
|
||||||
|
|
||||||
from mongoengine import signals
|
from mongoengine import signals
|
||||||
from mongoengine.base import (
|
from mongoengine.base import (BaseDict, BaseDocument, BaseList,
|
||||||
ALLOW_INHERITANCE,
|
DocumentMetaclass, EmbeddedDocumentList,
|
||||||
BaseDict,
|
TopLevelDocumentMetaclass, get_document)
|
||||||
BaseDocument,
|
|
||||||
BaseList,
|
|
||||||
DocumentMetaclass,
|
|
||||||
EmbeddedDocumentList,
|
|
||||||
TopLevelDocumentMetaclass,
|
|
||||||
get_document
|
|
||||||
)
|
|
||||||
from mongoengine.common import _import_class
|
from mongoengine.common import _import_class
|
||||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||||
from mongoengine.context_managers import switch_collection, switch_db
|
from mongoengine.context_managers import switch_collection, switch_db
|
||||||
@@ -31,12 +25,10 @@ __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument',
|
|||||||
|
|
||||||
|
|
||||||
def includes_cls(fields):
|
def includes_cls(fields):
|
||||||
""" Helper function used for ensuring and comparing indexes
|
"""Helper function used for ensuring and comparing indexes."""
|
||||||
"""
|
|
||||||
|
|
||||||
first_field = None
|
first_field = None
|
||||||
if len(fields):
|
if len(fields):
|
||||||
if isinstance(fields[0], basestring):
|
if isinstance(fields[0], six.string_types):
|
||||||
first_field = fields[0]
|
first_field = fields[0]
|
||||||
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
|
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
|
||||||
first_field = fields[0][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
|
to create a specialised version of the embedded document that will be
|
||||||
stored in the same collection. To facilitate this behaviour a `_cls`
|
stored in the same collection. To facilitate this behaviour a `_cls`
|
||||||
field is added to documents (hidden though the MongoEngine interface).
|
field is added to documents (hidden though the MongoEngine interface).
|
||||||
To disable this behaviour and remove the dependence on the presence of
|
To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
|
||||||
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
:attr:`meta` dictionary.
|
||||||
dictionary.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_instance', )
|
__slots__ = ('_instance', )
|
||||||
@@ -82,6 +73,15 @@ class EmbeddedDocument(BaseDocument):
|
|||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(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):
|
def save(self, *args, **kwargs):
|
||||||
self._instance.save(*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
|
create a specialised version of the document that will be stored in the
|
||||||
same collection. To facilitate this behaviour a `_cls`
|
same collection. To facilitate this behaviour a `_cls`
|
||||||
field is added to documents (hidden though the MongoEngine interface).
|
field is added to documents (hidden though the MongoEngine interface).
|
||||||
To disable this behaviour and remove the dependence on the presence of
|
To enable this behaviourset :attr:`allow_inheritance` to ``True`` in the
|
||||||
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
:attr:`meta` dictionary.
|
||||||
dictionary.
|
|
||||||
|
|
||||||
A :class:`~mongoengine.Document` may use a **Capped Collection** by
|
A :class:`~mongoengine.Document` may use a **Capped Collection** by
|
||||||
specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
|
specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
|
||||||
@@ -149,26 +148,22 @@ class Document(BaseDocument):
|
|||||||
|
|
||||||
__slots__ = ('__objects',)
|
__slots__ = ('__objects',)
|
||||||
|
|
||||||
def pk():
|
@property
|
||||||
"""Primary key alias
|
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):
|
@pk.setter
|
||||||
if 'id_field' not in self._meta:
|
def pk(self, value):
|
||||||
return None
|
"""Set the primary key."""
|
||||||
return getattr(self, self._meta['id_field'])
|
return setattr(self, self._meta['id_field'], value)
|
||||||
|
|
||||||
def fset(self, value):
|
|
||||||
return setattr(self, self._meta['id_field'], value)
|
|
||||||
|
|
||||||
return property(fget, fset)
|
|
||||||
|
|
||||||
pk = pk()
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_db(cls):
|
def _get_db(cls):
|
||||||
"""Some Model using other db_alias"""
|
"""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
|
@classmethod
|
||||||
def _get_collection(cls):
|
def _get_collection(cls):
|
||||||
@@ -211,7 +206,20 @@ class Document(BaseDocument):
|
|||||||
cls.ensure_indexes()
|
cls.ensure_indexes()
|
||||||
return cls._collection
|
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
|
"""Perform an atomic update of the document in the database and reload
|
||||||
the document object using updated version.
|
the document object using updated version.
|
||||||
|
|
||||||
@@ -225,17 +233,19 @@ class Document(BaseDocument):
|
|||||||
database matches the query
|
database matches the query
|
||||||
:param update: Django-style update keyword arguments
|
:param update: Django-style update keyword arguments
|
||||||
"""
|
"""
|
||||||
|
if query is None:
|
||||||
|
query = {}
|
||||||
|
|
||||||
if self.pk is None:
|
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)
|
query = query.copy() if isinstance(query, dict) else query.to_query(self)
|
||||||
|
|
||||||
if id_field not in query:
|
if id_field not in query:
|
||||||
query[id_field] = self.pk
|
query[id_field] = self.pk
|
||||||
elif 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)
|
updated = self._qs(**query).modify(new=True, **update)
|
||||||
if updated is None:
|
if updated is None:
|
||||||
@@ -303,6 +313,9 @@ class Document(BaseDocument):
|
|||||||
.. versionchanged:: 0.10.7
|
.. versionchanged:: 0.10.7
|
||||||
Add signal_kwargs argument
|
Add signal_kwargs argument
|
||||||
"""
|
"""
|
||||||
|
if self._meta.get('abstract'):
|
||||||
|
raise InvalidDocumentError('Cannot save an abstract document.')
|
||||||
|
|
||||||
signal_kwargs = signal_kwargs or {}
|
signal_kwargs = signal_kwargs or {}
|
||||||
signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
|
signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
|
||||||
|
|
||||||
@@ -310,7 +323,7 @@ class Document(BaseDocument):
|
|||||||
self.validate(clean=clean)
|
self.validate(clean=clean)
|
||||||
|
|
||||||
if write_concern is None:
|
if write_concern is None:
|
||||||
write_concern = {"w": 1}
|
write_concern = {'w': 1}
|
||||||
|
|
||||||
doc = self.to_mongo()
|
doc = self.to_mongo()
|
||||||
|
|
||||||
@@ -319,105 +332,135 @@ class Document(BaseDocument):
|
|||||||
signals.pre_save_post_validation.send(self.__class__, document=self,
|
signals.pre_save_post_validation.send(self.__class__, document=self,
|
||||||
created=created, **signal_kwargs)
|
created=created, **signal_kwargs)
|
||||||
|
|
||||||
|
if self._meta.get('auto_create_index', True):
|
||||||
|
self.ensure_indexes()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
collection = self._get_collection()
|
# Save a new document or update an existing one
|
||||||
if self._meta.get('auto_create_index', True):
|
|
||||||
self.ensure_indexes()
|
|
||||||
if created:
|
if created:
|
||||||
if force_insert:
|
object_id = self._save_create(doc, force_insert, write_concern)
|
||||||
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?
|
|
||||||
else:
|
else:
|
||||||
object_id = doc['_id']
|
object_id, created = self._save_update(doc, save_condition,
|
||||||
updates, removals = self._delta()
|
write_concern)
|
||||||
# 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)
|
|
||||||
|
|
||||||
if cascade is None:
|
if cascade is None:
|
||||||
cascade = self._meta.get(
|
cascade = (self._meta.get('cascade', False) or
|
||||||
'cascade', False) or cascade_kwargs is not None
|
cascade_kwargs is not None)
|
||||||
|
|
||||||
if cascade:
|
if cascade:
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"force_insert": force_insert,
|
'force_insert': force_insert,
|
||||||
"validate": validate,
|
'validate': validate,
|
||||||
"write_concern": write_concern,
|
'write_concern': write_concern,
|
||||||
"cascade": cascade
|
'cascade': cascade
|
||||||
}
|
}
|
||||||
if cascade_kwargs: # Allow granular control over cascades
|
if cascade_kwargs: # Allow granular control over cascades
|
||||||
kwargs.update(cascade_kwargs)
|
kwargs.update(cascade_kwargs)
|
||||||
kwargs['_refs'] = _refs
|
kwargs['_refs'] = _refs
|
||||||
self.cascade_save(**kwargs)
|
self.cascade_save(**kwargs)
|
||||||
except pymongo.errors.DuplicateKeyError, err:
|
|
||||||
|
except pymongo.errors.DuplicateKeyError as err:
|
||||||
message = u'Tried to save duplicate unique keys (%s)'
|
message = u'Tried to save duplicate unique keys (%s)'
|
||||||
raise NotUniqueError(message % unicode(err))
|
raise NotUniqueError(message % six.text_type(err))
|
||||||
except pymongo.errors.OperationFailure, err:
|
except pymongo.errors.OperationFailure as err:
|
||||||
message = 'Could not save document (%s)'
|
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
|
# E11000 - duplicate key error index
|
||||||
# E11001 - duplicate key on update
|
# E11001 - duplicate key on update
|
||||||
message = u'Tried to save duplicate unique keys (%s)'
|
message = u'Tried to save duplicate unique keys (%s)'
|
||||||
raise NotUniqueError(message % unicode(err))
|
raise NotUniqueError(message % six.text_type(err))
|
||||||
raise OperationError(message % unicode(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']
|
id_field = self._meta['id_field']
|
||||||
if created or id_field not in self._meta.get('shard_key', []):
|
if created or id_field not in self._meta.get('shard_key', []):
|
||||||
self[id_field] = self._fields[id_field].to_python(object_id)
|
self[id_field] = self._fields[id_field].to_python(object_id)
|
||||||
|
|
||||||
signals.post_save.send(self.__class__, document=self,
|
signals.post_save.send(self.__class__, document=self,
|
||||||
created=created, **signal_kwargs)
|
created=created, **signal_kwargs)
|
||||||
|
|
||||||
self._clear_changed_fields()
|
self._clear_changed_fields()
|
||||||
self._created = False
|
self._created = False
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def cascade_save(self, *args, **kwargs):
|
def _save_create(self, doc, force_insert, write_concern):
|
||||||
"""Recursively saves any references /
|
"""Save a new document.
|
||||||
generic references on the document"""
|
|
||||||
_refs = kwargs.get('_refs', []) or []
|
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')
|
ReferenceField = _import_class('ReferenceField')
|
||||||
GenericReferenceField = _import_class('GenericReferenceField')
|
GenericReferenceField = _import_class('GenericReferenceField')
|
||||||
@@ -443,16 +486,17 @@ class Document(BaseDocument):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _qs(self):
|
def _qs(self):
|
||||||
"""
|
"""Return the queryset to use for updating / reloading / deletions."""
|
||||||
Returns the queryset to use for updating / reloading / deletions
|
|
||||||
"""
|
|
||||||
if not hasattr(self, '__objects'):
|
if not hasattr(self, '__objects'):
|
||||||
self.__objects = QuerySet(self, self._get_collection())
|
self.__objects = QuerySet(self, self._get_collection())
|
||||||
return self.__objects
|
return self.__objects
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _object_key(self):
|
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}
|
select_dict = {'pk': self.pk}
|
||||||
shard_key = self.__class__._meta.get('shard_key', tuple())
|
shard_key = self.__class__._meta.get('shard_key', tuple())
|
||||||
@@ -472,11 +516,11 @@ class Document(BaseDocument):
|
|||||||
Raises :class:`OperationError` if called on an object that has not yet
|
Raises :class:`OperationError` if called on an object that has not yet
|
||||||
been saved.
|
been saved.
|
||||||
"""
|
"""
|
||||||
if not self.pk:
|
if self.pk is None:
|
||||||
if kwargs.get('upsert', False):
|
if kwargs.get('upsert', False):
|
||||||
query = self.to_mongo()
|
query = self.to_mongo()
|
||||||
if "_cls" in query:
|
if '_cls' in query:
|
||||||
del query["_cls"]
|
del query['_cls']
|
||||||
return self._qs.filter(**query).update_one(**kwargs)
|
return self._qs.filter(**query).update_one(**kwargs)
|
||||||
else:
|
else:
|
||||||
raise OperationError(
|
raise OperationError(
|
||||||
@@ -513,7 +557,7 @@ class Document(BaseDocument):
|
|||||||
try:
|
try:
|
||||||
self._qs.filter(
|
self._qs.filter(
|
||||||
**self._object_key).delete(write_concern=write_concern, _from_doc_delete=True)
|
**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
|
message = u'Could not delete document (%s)' % err.message
|
||||||
raise OperationError(message)
|
raise OperationError(message)
|
||||||
signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
|
signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
|
||||||
@@ -601,11 +645,12 @@ class Document(BaseDocument):
|
|||||||
if fields and isinstance(fields[0], int):
|
if fields and isinstance(fields[0], int):
|
||||||
max_depth = fields[0]
|
max_depth = fields[0]
|
||||||
fields = fields[1:]
|
fields = fields[1:]
|
||||||
elif "max_depth" in kwargs:
|
elif 'max_depth' in kwargs:
|
||||||
max_depth = kwargs["max_depth"]
|
max_depth = kwargs['max_depth']
|
||||||
|
|
||||||
|
if self.pk is None:
|
||||||
|
raise self.DoesNotExist('Document does not exist')
|
||||||
|
|
||||||
if not self.pk:
|
|
||||||
raise self.DoesNotExist("Document does not exist")
|
|
||||||
obj = self._qs.read_preference(ReadPreference.PRIMARY).filter(
|
obj = self._qs.read_preference(ReadPreference.PRIMARY).filter(
|
||||||
**self._object_key).only(*fields).limit(
|
**self._object_key).only(*fields).limit(
|
||||||
1).select_related(max_depth=max_depth)
|
1).select_related(max_depth=max_depth)
|
||||||
@@ -613,7 +658,7 @@ class Document(BaseDocument):
|
|||||||
if obj:
|
if obj:
|
||||||
obj = obj[0]
|
obj = obj[0]
|
||||||
else:
|
else:
|
||||||
raise self.DoesNotExist("Document does not exist")
|
raise self.DoesNotExist('Document does not exist')
|
||||||
|
|
||||||
for field in obj._data:
|
for field in obj._data:
|
||||||
if not fields or field in fields:
|
if not fields or field in fields:
|
||||||
@@ -655,8 +700,8 @@ class Document(BaseDocument):
|
|||||||
def to_dbref(self):
|
def to_dbref(self):
|
||||||
"""Returns an instance of :class:`~bson.dbref.DBRef` useful in
|
"""Returns an instance of :class:`~bson.dbref.DBRef` useful in
|
||||||
`__raw__` queries."""
|
`__raw__` queries."""
|
||||||
if not self.pk:
|
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)
|
raise OperationError(msg)
|
||||||
return DBRef(self.__class__._get_collection_name(), self.pk)
|
return DBRef(self.__class__._get_collection_name(), self.pk)
|
||||||
|
|
||||||
@@ -711,7 +756,7 @@ class Document(BaseDocument):
|
|||||||
fields = index_spec.pop('fields')
|
fields = index_spec.pop('fields')
|
||||||
drop_dups = kwargs.get('drop_dups', False)
|
drop_dups = kwargs.get('drop_dups', False)
|
||||||
if IS_PYMONGO_3 and drop_dups:
|
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)
|
warnings.warn(msg, DeprecationWarning)
|
||||||
elif not IS_PYMONGO_3:
|
elif not IS_PYMONGO_3:
|
||||||
index_spec['drop_dups'] = drop_dups
|
index_spec['drop_dups'] = drop_dups
|
||||||
@@ -737,7 +782,7 @@ class Document(BaseDocument):
|
|||||||
will be removed if PyMongo3+ is used
|
will be removed if PyMongo3+ is used
|
||||||
"""
|
"""
|
||||||
if IS_PYMONGO_3 and drop_dups:
|
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)
|
warnings.warn(msg, DeprecationWarning)
|
||||||
elif not IS_PYMONGO_3:
|
elif not IS_PYMONGO_3:
|
||||||
kwargs.update({'drop_dups': drop_dups})
|
kwargs.update({'drop_dups': drop_dups})
|
||||||
@@ -757,7 +802,7 @@ class Document(BaseDocument):
|
|||||||
index_opts = cls._meta.get('index_opts') or {}
|
index_opts = cls._meta.get('index_opts') or {}
|
||||||
index_cls = cls._meta.get('index_cls', True)
|
index_cls = cls._meta.get('index_cls', True)
|
||||||
if IS_PYMONGO_3 and drop_dups:
|
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)
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
|
||||||
collection = cls._get_collection()
|
collection = cls._get_collection()
|
||||||
@@ -795,8 +840,7 @@ class Document(BaseDocument):
|
|||||||
|
|
||||||
# If _cls is being used (for polymorphism), it needs an index,
|
# If _cls is being used (for polymorphism), it needs an index,
|
||||||
# only if another index doesn't begin with _cls
|
# only if another index doesn't begin with _cls
|
||||||
if (index_cls and not cls_indexed and
|
if index_cls and not cls_indexed and cls._meta.get('allow_inheritance'):
|
||||||
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
|
|
||||||
|
|
||||||
# we shouldn't pass 'cls' to the collection.ensureIndex options
|
# we shouldn't pass 'cls' to the collection.ensureIndex options
|
||||||
# because of https://jira.mongodb.org/browse/SERVER-769
|
# 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
|
""" Lists all of the indexes that should be created for given
|
||||||
collection. It includes all the indexes from super- and sub-classes.
|
collection. It includes all the indexes from super- and sub-classes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if cls._meta.get('abstract'):
|
if cls._meta.get('abstract'):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -866,16 +909,15 @@ class Document(BaseDocument):
|
|||||||
# finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
|
# finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
|
||||||
if [(u'_id', 1)] not in indexes:
|
if [(u'_id', 1)] not in indexes:
|
||||||
indexes.append([(u'_id', 1)])
|
indexes.append([(u'_id', 1)])
|
||||||
if (cls._meta.get('index_cls', True) and
|
if cls._meta.get('index_cls', True) and cls._meta.get('allow_inheritance'):
|
||||||
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
|
|
||||||
indexes.append([(u'_cls', 1)])
|
indexes.append([(u'_cls', 1)])
|
||||||
|
|
||||||
return indexes
|
return indexes
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def compare_indexes(cls):
|
def compare_indexes(cls):
|
||||||
""" Compares the indexes defined in MongoEngine with the ones existing
|
""" Compares the indexes defined in MongoEngine with the ones
|
||||||
in the database. Returns any missing/extra indexes.
|
existing in the database. Returns any missing/extra indexes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required = cls.list_indexes()
|
required = cls.list_indexes()
|
||||||
@@ -919,8 +961,9 @@ class DynamicDocument(Document):
|
|||||||
_dynamic = True
|
_dynamic = True
|
||||||
|
|
||||||
def __delattr__(self, *args, **kwargs):
|
def __delattr__(self, *args, **kwargs):
|
||||||
"""Deletes the attribute by setting to None and allowing _delta to unset
|
"""Delete the attribute by setting to None and allowing _delta
|
||||||
it"""
|
to unset it.
|
||||||
|
"""
|
||||||
field_name = args[0]
|
field_name = args[0]
|
||||||
if field_name in self._dynamic_fields:
|
if field_name in self._dynamic_fields:
|
||||||
setattr(self, field_name, None)
|
setattr(self, field_name, None)
|
||||||
@@ -942,8 +985,9 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
|
|||||||
_dynamic = True
|
_dynamic = True
|
||||||
|
|
||||||
def __delattr__(self, *args, **kwargs):
|
def __delattr__(self, *args, **kwargs):
|
||||||
"""Deletes the attribute by setting to None and allowing _delta to unset
|
"""Delete the attribute by setting to None and allowing _delta
|
||||||
it"""
|
to unset it.
|
||||||
|
"""
|
||||||
field_name = args[0]
|
field_name = args[0]
|
||||||
if field_name in self._fields:
|
if field_name in self._fields:
|
||||||
default = self._fields[field_name].default
|
default = self._fields[field_name].default
|
||||||
@@ -985,10 +1029,10 @@ class MapReduceDocument(object):
|
|||||||
try:
|
try:
|
||||||
self.key = id_field_type(self.key)
|
self.key = id_field_type(self.key)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise Exception("Could not cast key as %s" %
|
raise Exception('Could not cast key as %s' %
|
||||||
id_field_type.__name__)
|
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)
|
self._key_object = self._document.objects.with_id(self.key)
|
||||||
return self._key_object
|
return self._key_object
|
||||||
return self._key_object
|
return self._key_object
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from mongoengine.python_support import txt_type
|
import six
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
||||||
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
||||||
@@ -51,8 +50,8 @@ class FieldDoesNotExist(Exception):
|
|||||||
or an :class:`~mongoengine.EmbeddedDocument`.
|
or an :class:`~mongoengine.EmbeddedDocument`.
|
||||||
|
|
||||||
To avoid this behavior on data loading,
|
To avoid this behavior on data loading,
|
||||||
you should the :attr:`strict` to ``False``
|
you should set the :attr:`strict` to ``False``
|
||||||
in the :attr:`meta` dictionnary.
|
in the :attr:`meta` dictionary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -71,13 +70,13 @@ class ValidationError(AssertionError):
|
|||||||
field_name = None
|
field_name = None
|
||||||
_message = None
|
_message = None
|
||||||
|
|
||||||
def __init__(self, message="", **kwargs):
|
def __init__(self, message='', **kwargs):
|
||||||
self.errors = kwargs.get('errors', {})
|
self.errors = kwargs.get('errors', {})
|
||||||
self.field_name = kwargs.get('field_name')
|
self.field_name = kwargs.get('field_name')
|
||||||
self.message = message
|
self.message = message
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return txt_type(self.message)
|
return six.text_type(self.message)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%s(%s,)' % (self.__class__.__name__, self.message)
|
return '%s(%s,)' % (self.__class__.__name__, self.message)
|
||||||
@@ -111,17 +110,20 @@ class ValidationError(AssertionError):
|
|||||||
errors_dict = {}
|
errors_dict = {}
|
||||||
if not source:
|
if not source:
|
||||||
return errors_dict
|
return errors_dict
|
||||||
|
|
||||||
if isinstance(source, dict):
|
if isinstance(source, dict):
|
||||||
for field_name, error in source.iteritems():
|
for field_name, error in source.iteritems():
|
||||||
errors_dict[field_name] = build_dict(error)
|
errors_dict[field_name] = build_dict(error)
|
||||||
elif isinstance(source, ValidationError) and source.errors:
|
elif isinstance(source, ValidationError) and source.errors:
|
||||||
return build_dict(source.errors)
|
return build_dict(source.errors)
|
||||||
else:
|
else:
|
||||||
return unicode(source)
|
return six.text_type(source)
|
||||||
|
|
||||||
return errors_dict
|
return errors_dict
|
||||||
|
|
||||||
if not self.errors:
|
if not self.errors:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return build_dict(self.errors)
|
return build_dict(self.errors)
|
||||||
|
|
||||||
def _format_errors(self):
|
def _format_errors(self):
|
||||||
@@ -134,10 +136,10 @@ class ValidationError(AssertionError):
|
|||||||
value = ' '.join(
|
value = ' '.join(
|
||||||
[generate_key(v, k) for k, v in value.iteritems()])
|
[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
|
return results
|
||||||
|
|
||||||
error_dict = defaultdict(list)
|
error_dict = defaultdict(list)
|
||||||
for k, v in self.to_dict().iteritems():
|
for k, v in self.to_dict().iteritems():
|
||||||
error_dict[generate_key(v)].append(k)
|
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()])
|
||||||
|
@@ -3,7 +3,6 @@ import decimal
|
|||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import urllib2
|
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
@@ -25,13 +24,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
Int64 = long
|
Int64 = long
|
||||||
|
|
||||||
from .base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField,
|
from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField,
|
||||||
ObjectIdField, get_document)
|
GeoJsonBaseField, ObjectIdField, get_document)
|
||||||
from .connection import DEFAULT_CONNECTION_NAME, get_db
|
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||||
from .document import Document, EmbeddedDocument
|
from mongoengine.document import Document, EmbeddedDocument
|
||||||
from .errors import DoesNotExist, ValidationError
|
from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError
|
||||||
from .python_support import PY3, StringIO, bin_type, str_types, txt_type
|
from mongoengine.python_support import StringIO
|
||||||
from .queryset import DO_NOTHING, QuerySet
|
from mongoengine.queryset import DO_NOTHING, QuerySet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
@@ -39,7 +38,7 @@ except ImportError:
|
|||||||
Image = None
|
Image = None
|
||||||
ImageOps = None
|
ImageOps = None
|
||||||
|
|
||||||
__all__ = [
|
__all__ = (
|
||||||
'StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
|
'StringField', 'URLField', 'EmailField', 'IntField', 'LongField',
|
||||||
'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
|
'FloatField', 'DecimalField', 'BooleanField', 'DateTimeField',
|
||||||
'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
|
'ComplexDateTimeField', 'EmbeddedDocumentField', 'ObjectIdField',
|
||||||
@@ -50,14 +49,14 @@ __all__ = [
|
|||||||
'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
|
'FileField', 'ImageGridFsProxy', 'ImproperlyConfigured', 'ImageField',
|
||||||
'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
|
'GeoPointField', 'PointField', 'LineStringField', 'PolygonField',
|
||||||
'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField',
|
'SequenceField', 'UUIDField', 'MultiPointField', 'MultiLineStringField',
|
||||||
'MultiPolygonField', 'GeoJsonBaseField']
|
'MultiPolygonField', 'GeoJsonBaseField'
|
||||||
|
)
|
||||||
|
|
||||||
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
RECURSIVE_REFERENCE_CONSTANT = 'self'
|
||||||
|
|
||||||
|
|
||||||
class StringField(BaseField):
|
class StringField(BaseField):
|
||||||
"""A unicode string field.
|
"""A unicode string field."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
|
def __init__(self, regex=None, max_length=None, min_length=None, **kwargs):
|
||||||
self.regex = re.compile(regex) if regex else None
|
self.regex = re.compile(regex) if regex else None
|
||||||
@@ -66,7 +65,7 @@ class StringField(BaseField):
|
|||||||
super(StringField, self).__init__(**kwargs)
|
super(StringField, self).__init__(**kwargs)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if isinstance(value, unicode):
|
if isinstance(value, six.text_type):
|
||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
value = value.decode('utf-8')
|
value = value.decode('utf-8')
|
||||||
@@ -75,7 +74,7 @@ class StringField(BaseField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if not isinstance(value, basestring):
|
if not isinstance(value, six.string_types):
|
||||||
self.error('StringField only accepts string values')
|
self.error('StringField only accepts string values')
|
||||||
|
|
||||||
if self.max_length is not None and len(value) > self.max_length:
|
if self.max_length is not None and len(value) > self.max_length:
|
||||||
@@ -91,7 +90,7 @@ class StringField(BaseField):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
def prepare_query_value(self, op, value):
|
||||||
if not isinstance(op, basestring):
|
if not isinstance(op, six.string_types):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
|
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
|
||||||
@@ -140,25 +139,14 @@ class URLField(StringField):
|
|||||||
# Check first if the scheme is valid
|
# Check first if the scheme is valid
|
||||||
scheme = value.split('://')[0].lower()
|
scheme = value.split('://')[0].lower()
|
||||||
if scheme not in self.schemes:
|
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
|
return
|
||||||
|
|
||||||
# Then check full URL
|
# Then check full URL
|
||||||
if not self.url_regex.match(value):
|
if not self.url_regex.match(value):
|
||||||
self.error('Invalid URL: {}'.format(value))
|
self.error(u'Invalid URL: {}'.format(value))
|
||||||
return
|
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):
|
class EmailField(StringField):
|
||||||
"""A field that validates input as an email address.
|
"""A field that validates input as an email address.
|
||||||
@@ -182,8 +170,7 @@ class EmailField(StringField):
|
|||||||
|
|
||||||
|
|
||||||
class IntField(BaseField):
|
class IntField(BaseField):
|
||||||
"""An 32-bit integer field.
|
"""32-bit integer field."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, min_value=None, max_value=None, **kwargs):
|
def __init__(self, min_value=None, max_value=None, **kwargs):
|
||||||
self.min_value, self.max_value = min_value, max_value
|
self.min_value, self.max_value = min_value, max_value
|
||||||
@@ -216,8 +203,7 @@ class IntField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class LongField(BaseField):
|
class LongField(BaseField):
|
||||||
"""An 64-bit integer field.
|
"""64-bit integer field."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, min_value=None, max_value=None, **kwargs):
|
def __init__(self, min_value=None, max_value=None, **kwargs):
|
||||||
self.min_value, self.max_value = min_value, max_value
|
self.min_value, self.max_value = min_value, max_value
|
||||||
@@ -253,8 +239,7 @@ class LongField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class FloatField(BaseField):
|
class FloatField(BaseField):
|
||||||
"""An floating point number field.
|
"""Floating point number field."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, min_value=None, max_value=None, **kwargs):
|
def __init__(self, min_value=None, max_value=None, **kwargs):
|
||||||
self.min_value, self.max_value = min_value, max_value
|
self.min_value, self.max_value = min_value, max_value
|
||||||
@@ -291,7 +276,7 @@ class FloatField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class DecimalField(BaseField):
|
class DecimalField(BaseField):
|
||||||
"""A fixed-point decimal number field.
|
"""Fixed-point decimal number field.
|
||||||
|
|
||||||
.. versionchanged:: 0.8
|
.. versionchanged:: 0.8
|
||||||
.. versionadded:: 0.3
|
.. versionadded:: 0.3
|
||||||
@@ -332,25 +317,25 @@ class DecimalField(BaseField):
|
|||||||
|
|
||||||
# Convert to string for python 2.6 before casting to Decimal
|
# Convert to string for python 2.6 before casting to Decimal
|
||||||
try:
|
try:
|
||||||
value = decimal.Decimal("%s" % value)
|
value = decimal.Decimal('%s' % value)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
return value
|
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):
|
def to_mongo(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
if self.force_string:
|
if self.force_string:
|
||||||
return unicode(value)
|
return six.text_type(self.to_python(value))
|
||||||
return float(self.to_python(value))
|
return float(self.to_python(value))
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if not isinstance(value, decimal.Decimal):
|
if not isinstance(value, decimal.Decimal):
|
||||||
if not isinstance(value, basestring):
|
if not isinstance(value, six.string_types):
|
||||||
value = unicode(value)
|
value = six.text_type(value)
|
||||||
try:
|
try:
|
||||||
value = decimal.Decimal(value)
|
value = decimal.Decimal(value)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
self.error('Could not convert value to decimal: %s' % exc)
|
self.error('Could not convert value to decimal: %s' % exc)
|
||||||
|
|
||||||
if self.min_value is not None and value < self.min_value:
|
if self.min_value is not None and value < self.min_value:
|
||||||
@@ -364,7 +349,7 @@ class DecimalField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class BooleanField(BaseField):
|
class BooleanField(BaseField):
|
||||||
"""A boolean field type.
|
"""Boolean field type.
|
||||||
|
|
||||||
.. versionadded:: 0.1.2
|
.. versionadded:: 0.1.2
|
||||||
"""
|
"""
|
||||||
@@ -382,7 +367,7 @@ class BooleanField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class DateTimeField(BaseField):
|
class DateTimeField(BaseField):
|
||||||
"""A datetime field.
|
"""Datetime field.
|
||||||
|
|
||||||
Uses the python-dateutil library if available alternatively use time.strptime
|
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
|
to parse the dates. Note: python-dateutil's parser is fully featured and when
|
||||||
@@ -410,7 +395,7 @@ class DateTimeField(BaseField):
|
|||||||
if callable(value):
|
if callable(value):
|
||||||
return value()
|
return value()
|
||||||
|
|
||||||
if not isinstance(value, basestring):
|
if not isinstance(value, six.string_types):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Attempt to parse a datetime:
|
# Attempt to parse a datetime:
|
||||||
@@ -537,16 +522,19 @@ class EmbeddedDocumentField(BaseField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, document_type, **kwargs):
|
def __init__(self, document_type, **kwargs):
|
||||||
if not isinstance(document_type, basestring):
|
if (
|
||||||
if not issubclass(document_type, EmbeddedDocument):
|
not isinstance(document_type, six.string_types) and
|
||||||
self.error('Invalid embedded document class provided to an '
|
not issubclass(document_type, EmbeddedDocument)
|
||||||
'EmbeddedDocumentField')
|
):
|
||||||
|
self.error('Invalid embedded document class provided to an '
|
||||||
|
'EmbeddedDocumentField')
|
||||||
|
|
||||||
self.document_type_obj = document_type
|
self.document_type_obj = document_type
|
||||||
super(EmbeddedDocumentField, self).__init__(**kwargs)
|
super(EmbeddedDocumentField, self).__init__(**kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def document_type(self):
|
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:
|
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
|
||||||
self.document_type_obj = self.owner_document
|
self.document_type_obj = self.owner_document
|
||||||
else:
|
else:
|
||||||
@@ -577,8 +565,12 @@ class EmbeddedDocumentField(BaseField):
|
|||||||
return self.document_type._fields.get(member_name)
|
return self.document_type._fields.get(member_name)
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
def prepare_query_value(self, op, value):
|
||||||
if not isinstance(value, self.document_type):
|
if value is not None and not isinstance(value, self.document_type):
|
||||||
value = self.document_type._from_son(value)
|
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)
|
super(EmbeddedDocumentField, self).prepare_query_value(op, value)
|
||||||
return self.to_mongo(value)
|
return self.to_mongo(value)
|
||||||
|
|
||||||
@@ -631,7 +623,7 @@ class DynamicField(BaseField):
|
|||||||
"""Convert a Python type to a MongoDB compatible type.
|
"""Convert a Python type to a MongoDB compatible type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(value, basestring):
|
if isinstance(value, six.string_types):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if hasattr(value, 'to_mongo'):
|
if hasattr(value, 'to_mongo'):
|
||||||
@@ -639,7 +631,7 @@ class DynamicField(BaseField):
|
|||||||
val = value.to_mongo(use_db_field, fields)
|
val = value.to_mongo(use_db_field, fields)
|
||||||
# If we its a document thats not inherited add _cls
|
# If we its a document thats not inherited add _cls
|
||||||
if isinstance(value, Document):
|
if isinstance(value, Document):
|
||||||
val = {"_ref": value.to_dbref(), "_cls": cls.__name__}
|
val = {'_ref': value.to_dbref(), '_cls': cls.__name__}
|
||||||
if isinstance(value, EmbeddedDocument):
|
if isinstance(value, EmbeddedDocument):
|
||||||
val['_cls'] = cls.__name__
|
val['_cls'] = cls.__name__
|
||||||
return val
|
return val
|
||||||
@@ -650,7 +642,7 @@ class DynamicField(BaseField):
|
|||||||
is_list = False
|
is_list = False
|
||||||
if not hasattr(value, 'items'):
|
if not hasattr(value, 'items'):
|
||||||
is_list = True
|
is_list = True
|
||||||
value = dict([(k, v) for k, v in enumerate(value)])
|
value = {k: v for k, v in enumerate(value)}
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for k, v in value.iteritems():
|
for k, v in value.iteritems():
|
||||||
@@ -674,12 +666,12 @@ class DynamicField(BaseField):
|
|||||||
return member_name
|
return member_name
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
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 StringField().prepare_query_value(op, value)
|
||||||
return super(DynamicField, self).prepare_query_value(op, self.to_mongo(value))
|
return super(DynamicField, self).prepare_query_value(op, self.to_mongo(value))
|
||||||
|
|
||||||
def validate(self, value, clean=True):
|
def validate(self, value, clean=True):
|
||||||
if hasattr(value, "validate"):
|
if hasattr(value, 'validate'):
|
||||||
value.validate(clean=clean)
|
value.validate(clean=clean)
|
||||||
|
|
||||||
|
|
||||||
@@ -699,21 +691,27 @@ class ListField(ComplexBaseField):
|
|||||||
super(ListField, self).__init__(**kwargs)
|
super(ListField, self).__init__(**kwargs)
|
||||||
|
|
||||||
def validate(self, value):
|
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
|
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')
|
self.error('Only lists and tuples may be used in a list field')
|
||||||
super(ListField, self).validate(value)
|
super(ListField, self).validate(value)
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
def prepare_query_value(self, op, value):
|
||||||
if self.field:
|
if self.field:
|
||||||
if op in ('set', 'unset', None) and (
|
|
||||||
not isinstance(value, basestring) and
|
# If the value is iterable and it's not a string nor a
|
||||||
not isinstance(value, BaseDocument) and
|
# BaseDocument, call prepare_query_value for each of its items.
|
||||||
hasattr(value, '__iter__')):
|
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, v) for v in value]
|
||||||
|
|
||||||
return self.field.prepare_query_value(op, value)
|
return self.field.prepare_query_value(op, value)
|
||||||
|
|
||||||
return super(ListField, self).prepare_query_value(op, value)
|
return super(ListField, self).prepare_query_value(op, value)
|
||||||
|
|
||||||
|
|
||||||
@@ -726,7 +724,6 @@ class EmbeddedDocumentListField(ListField):
|
|||||||
:class:`~mongoengine.EmbeddedDocument`.
|
:class:`~mongoengine.EmbeddedDocument`.
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, document_type, **kwargs):
|
def __init__(self, document_type, **kwargs):
|
||||||
@@ -775,17 +772,17 @@ class SortedListField(ListField):
|
|||||||
|
|
||||||
|
|
||||||
def key_not_string(d):
|
def key_not_string(d):
|
||||||
""" Helper function to recursively determine if any key in a dictionary is
|
"""Helper function to recursively determine if any key in a
|
||||||
not a string.
|
dictionary is not a string.
|
||||||
"""
|
"""
|
||||||
for k, v in d.items():
|
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
|
return True
|
||||||
|
|
||||||
|
|
||||||
def key_has_dot_or_dollar(d):
|
def key_has_dot_or_dollar(d):
|
||||||
""" Helper function to recursively determine if any key in a dictionary
|
"""Helper function to recursively determine if any key in a
|
||||||
contains a dot or a dollar sign.
|
dictionary contains a dot or a dollar sign.
|
||||||
"""
|
"""
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)):
|
if ('.' in k or '$' in k) or (isinstance(v, dict) and key_has_dot_or_dollar(v)):
|
||||||
@@ -813,14 +810,13 @@ class DictField(ComplexBaseField):
|
|||||||
super(DictField, self).__init__(*args, **kwargs)
|
super(DictField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def validate(self, value):
|
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):
|
if not isinstance(value, dict):
|
||||||
self.error('Only dictionaries may be used in a DictField')
|
self.error('Only dictionaries may be used in a DictField')
|
||||||
|
|
||||||
if key_not_string(value):
|
if key_not_string(value):
|
||||||
msg = ("Invalid dictionary key - documents must "
|
msg = ('Invalid dictionary key - documents must '
|
||||||
"have only string keys")
|
'have only string keys')
|
||||||
self.error(msg)
|
self.error(msg)
|
||||||
if key_has_dot_or_dollar(value):
|
if key_has_dot_or_dollar(value):
|
||||||
self.error('Invalid dictionary key name - keys may not contain "."'
|
self.error('Invalid dictionary key name - keys may not contain "."'
|
||||||
@@ -835,14 +831,15 @@ class DictField(ComplexBaseField):
|
|||||||
'istartswith', 'endswith', 'iendswith',
|
'istartswith', 'endswith', 'iendswith',
|
||||||
'exact', 'iexact']
|
'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)
|
return StringField().prepare_query_value(op, value)
|
||||||
|
|
||||||
if hasattr(self.field, 'field'):
|
if hasattr(self.field, 'field'):
|
||||||
if op in ('set', 'unset') and isinstance(value, dict):
|
if op in ('set', 'unset') and isinstance(value, dict):
|
||||||
return dict(
|
return {
|
||||||
(k, self.field.prepare_query_value(op, v))
|
k: self.field.prepare_query_value(op, v)
|
||||||
for k, v in value.items())
|
for k, v in value.items()
|
||||||
|
}
|
||||||
return self.field.prepare_query_value(op, value)
|
return self.field.prepare_query_value(op, value)
|
||||||
|
|
||||||
return super(DictField, self).prepare_query_value(op, value)
|
return super(DictField, self).prepare_query_value(op, value)
|
||||||
@@ -891,10 +888,6 @@ class ReferenceField(BaseField):
|
|||||||
|
|
||||||
Foo.register_delete_rule(Bar, 'foo', NULLIFY)
|
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`
|
.. versionchanged:: 0.5 added `reverse_delete_rule`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -911,10 +904,12 @@ class ReferenceField(BaseField):
|
|||||||
A reference to an abstract document type is always stored as a
|
A reference to an abstract document type is always stored as a
|
||||||
:class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
|
:class:`~pymongo.dbref.DBRef`, regardless of the value of `dbref`.
|
||||||
"""
|
"""
|
||||||
if not isinstance(document_type, basestring):
|
if (
|
||||||
if not issubclass(document_type, (Document, basestring)):
|
not isinstance(document_type, six.string_types) and
|
||||||
self.error('Argument to ReferenceField constructor must be a '
|
not issubclass(document_type, Document)
|
||||||
'document class or a string')
|
):
|
||||||
|
self.error('Argument to ReferenceField constructor must be a '
|
||||||
|
'document class or a string')
|
||||||
|
|
||||||
self.dbref = dbref
|
self.dbref = dbref
|
||||||
self.document_type_obj = document_type
|
self.document_type_obj = document_type
|
||||||
@@ -923,7 +918,7 @@ class ReferenceField(BaseField):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def document_type(self):
|
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:
|
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
|
||||||
self.document_type_obj = self.owner_document
|
self.document_type_obj = self.owner_document
|
||||||
else:
|
else:
|
||||||
@@ -931,8 +926,7 @@ class ReferenceField(BaseField):
|
|||||||
return self.document_type_obj
|
return self.document_type_obj
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
"""Descriptor to allow lazy dereferencing.
|
"""Descriptor to allow lazy dereferencing."""
|
||||||
"""
|
|
||||||
if instance is None:
|
if instance is None:
|
||||||
# Document class being used rather than a document object
|
# Document class being used rather than a document object
|
||||||
return self
|
return self
|
||||||
@@ -989,8 +983,7 @@ class ReferenceField(BaseField):
|
|||||||
return id_
|
return id_
|
||||||
|
|
||||||
def to_python(self, value):
|
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
|
if (not self.dbref and
|
||||||
not isinstance(value, (DBRef, Document, EmbeddedDocument))):
|
not isinstance(value, (DBRef, Document, EmbeddedDocument))):
|
||||||
collection = self.document_type._get_collection_name()
|
collection = self.document_type._get_collection_name()
|
||||||
@@ -1006,7 +999,7 @@ class ReferenceField(BaseField):
|
|||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
|
||||||
if not isinstance(value, (self.document_type, DBRef)):
|
if not isinstance(value, (self.document_type, DBRef)):
|
||||||
self.error("A ReferenceField only accepts DBRef or documents")
|
self.error('A ReferenceField only accepts DBRef or documents')
|
||||||
|
|
||||||
if isinstance(value, Document) and value.id is None:
|
if isinstance(value, Document) and value.id is None:
|
||||||
self.error('You can only reference documents once they have been '
|
self.error('You can only reference documents once they have been '
|
||||||
@@ -1030,14 +1023,19 @@ class CachedReferenceField(BaseField):
|
|||||||
.. versionadded:: 0.9
|
.. 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.
|
"""Initialises the Cached Reference Field.
|
||||||
|
|
||||||
:param fields: A list of fields to be cached in document
|
:param fields: A list of fields to be cached in document
|
||||||
:param auto_sync: if True documents are auto updated.
|
:param auto_sync: if True documents are auto updated.
|
||||||
"""
|
"""
|
||||||
if not isinstance(document_type, basestring) and \
|
if fields is None:
|
||||||
not issubclass(document_type, (Document, basestring)):
|
fields = []
|
||||||
|
|
||||||
|
if (
|
||||||
|
not isinstance(document_type, six.string_types) and
|
||||||
|
not issubclass(document_type, Document)
|
||||||
|
):
|
||||||
self.error('Argument to CachedReferenceField constructor must be a'
|
self.error('Argument to CachedReferenceField constructor must be a'
|
||||||
' document class or a string')
|
' document class or a string')
|
||||||
|
|
||||||
@@ -1053,18 +1051,20 @@ class CachedReferenceField(BaseField):
|
|||||||
sender=self.document_type)
|
sender=self.document_type)
|
||||||
|
|
||||||
def on_document_pre_save(self, sender, document, created, **kwargs):
|
def on_document_pre_save(self, sender, document, created, **kwargs):
|
||||||
if not created:
|
if created:
|
||||||
update_kwargs = dict(
|
return None
|
||||||
('set__%s__%s' % (self.name, k), v)
|
|
||||||
for k, v in document._delta()[0].items()
|
|
||||||
if k in self.fields)
|
|
||||||
|
|
||||||
if update_kwargs:
|
update_kwargs = {
|
||||||
filter_kwargs = {}
|
'set__%s__%s' % (self.name, key): val
|
||||||
filter_kwargs[self.name] = document
|
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(
|
self.owner_document.objects(
|
||||||
**filter_kwargs).update(**update_kwargs)
|
**filter_kwargs).update(**update_kwargs)
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -1077,7 +1077,7 @@ class CachedReferenceField(BaseField):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def document_type(self):
|
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:
|
if self.document_type_obj == RECURSIVE_REFERENCE_CONSTANT:
|
||||||
self.document_type_obj = self.owner_document
|
self.document_type_obj = self.owner_document
|
||||||
else:
|
else:
|
||||||
@@ -1117,7 +1117,7 @@ class CachedReferenceField(BaseField):
|
|||||||
# TODO: should raise here or will fail next statement
|
# TODO: should raise here or will fail next statement
|
||||||
|
|
||||||
value = SON((
|
value = SON((
|
||||||
("_id", id_field.to_mongo(id_)),
|
('_id', id_field.to_mongo(id_)),
|
||||||
))
|
))
|
||||||
|
|
||||||
if fields:
|
if fields:
|
||||||
@@ -1143,7 +1143,7 @@ class CachedReferenceField(BaseField):
|
|||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
|
||||||
if not isinstance(value, self.document_type):
|
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:
|
if isinstance(value, Document) and value.id is None:
|
||||||
self.error('You can only reference documents once they have been '
|
self.error('You can only reference documents once they have been '
|
||||||
@@ -1191,13 +1191,13 @@ class GenericReferenceField(BaseField):
|
|||||||
# Keep the choices as a list of allowed Document class names
|
# Keep the choices as a list of allowed Document class names
|
||||||
if choices:
|
if choices:
|
||||||
for choice in choices:
|
for choice in choices:
|
||||||
if isinstance(choice, basestring):
|
if isinstance(choice, six.string_types):
|
||||||
self.choices.append(choice)
|
self.choices.append(choice)
|
||||||
elif isinstance(choice, type) and issubclass(choice, Document):
|
elif isinstance(choice, type) and issubclass(choice, Document):
|
||||||
self.choices.append(choice._class_name)
|
self.choices.append(choice._class_name)
|
||||||
else:
|
else:
|
||||||
self.error('Invalid choices provided: must be a list of'
|
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):
|
def _validate_choices(self, value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -1249,7 +1249,7 @@ class GenericReferenceField(BaseField):
|
|||||||
if document is None:
|
if document is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(document, (dict, SON)):
|
if isinstance(document, (dict, SON, ObjectId, DBRef)):
|
||||||
return document
|
return document
|
||||||
|
|
||||||
id_field_name = document.__class__._meta['id_field']
|
id_field_name = document.__class__._meta['id_field']
|
||||||
@@ -1280,8 +1280,7 @@ class GenericReferenceField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class BinaryField(BaseField):
|
class BinaryField(BaseField):
|
||||||
"""A binary data field.
|
"""A binary data field."""
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, max_bytes=None, **kwargs):
|
def __init__(self, max_bytes=None, **kwargs):
|
||||||
self.max_bytes = max_bytes
|
self.max_bytes = max_bytes
|
||||||
@@ -1289,18 +1288,18 @@ class BinaryField(BaseField):
|
|||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
"""Handle bytearrays in python 3.1"""
|
"""Handle bytearrays in python 3.1"""
|
||||||
if PY3 and isinstance(value, bytearray):
|
if six.PY3 and isinstance(value, bytearray):
|
||||||
value = bin_type(value)
|
value = six.binary_type(value)
|
||||||
return super(BinaryField, self).__set__(instance, value)
|
return super(BinaryField, self).__set__(instance, value)
|
||||||
|
|
||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
return Binary(value)
|
return Binary(value)
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if not isinstance(value, (bin_type, txt_type, Binary)):
|
if not isinstance(value, (six.binary_type, six.text_type, Binary)):
|
||||||
self.error("BinaryField only accepts instances of "
|
self.error('BinaryField only accepts instances of '
|
||||||
"(%s, %s, Binary)" % (
|
'(%s, %s, Binary)' % (
|
||||||
bin_type.__name__, txt_type.__name__))
|
six.binary_type.__name__, six.text_type.__name__))
|
||||||
|
|
||||||
if self.max_bytes is not None and len(value) > self.max_bytes:
|
if self.max_bytes is not None and len(value) > self.max_bytes:
|
||||||
self.error('Binary value is too long')
|
self.error('Binary value is too long')
|
||||||
@@ -1384,11 +1383,13 @@ class GridFSProxy(object):
|
|||||||
get_db(self.db_alias), self.collection_name)
|
get_db(self.db_alias), self.collection_name)
|
||||||
return self._fs
|
return self._fs
|
||||||
|
|
||||||
def get(self, id=None):
|
def get(self, grid_id=None):
|
||||||
if id:
|
if grid_id:
|
||||||
self.grid_id = id
|
self.grid_id = grid_id
|
||||||
|
|
||||||
if self.grid_id is None:
|
if self.grid_id is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.gridout is None:
|
if self.gridout is None:
|
||||||
self.gridout = self.fs.get(self.grid_id)
|
self.gridout = self.fs.get(self.grid_id)
|
||||||
@@ -1432,7 +1433,7 @@ class GridFSProxy(object):
|
|||||||
try:
|
try:
|
||||||
return gridout.read(size)
|
return gridout.read(size)
|
||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ''
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
# Delete file from GridFS, FileField still remains
|
# Delete file from GridFS, FileField still remains
|
||||||
@@ -1464,9 +1465,8 @@ class FileField(BaseField):
|
|||||||
"""
|
"""
|
||||||
proxy_class = GridFSProxy
|
proxy_class = GridFSProxy
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs',
|
||||||
db_alias=DEFAULT_CONNECTION_NAME,
|
**kwargs):
|
||||||
collection_name="fs", **kwargs):
|
|
||||||
super(FileField, self).__init__(**kwargs)
|
super(FileField, self).__init__(**kwargs)
|
||||||
self.collection_name = collection_name
|
self.collection_name = collection_name
|
||||||
self.db_alias = db_alias
|
self.db_alias = db_alias
|
||||||
@@ -1488,8 +1488,10 @@ class FileField(BaseField):
|
|||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
key = self.name
|
key = self.name
|
||||||
if ((hasattr(value, 'read') and not
|
if (
|
||||||
isinstance(value, GridFSProxy)) or isinstance(value, str_types)):
|
(hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or
|
||||||
|
isinstance(value, (six.binary_type, six.string_types))
|
||||||
|
):
|
||||||
# using "FileField() = file/string" notation
|
# using "FileField() = file/string" notation
|
||||||
grid_file = instance._data.get(self.name)
|
grid_file = instance._data.get(self.name)
|
||||||
# If a file already exists, delete it
|
# If a file already exists, delete it
|
||||||
@@ -1558,7 +1560,7 @@ class ImageGridFsProxy(GridFSProxy):
|
|||||||
try:
|
try:
|
||||||
img = Image.open(file_obj)
|
img = Image.open(file_obj)
|
||||||
img_format = img.format
|
img_format = img.format
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise ValidationError('Invalid image: %s' % e)
|
raise ValidationError('Invalid image: %s' % e)
|
||||||
|
|
||||||
# Progressive JPEG
|
# Progressive JPEG
|
||||||
@@ -1667,10 +1669,10 @@ class ImageGridFsProxy(GridFSProxy):
|
|||||||
return self.fs.get(out.thumbnail_id)
|
return self.fs.get(out.thumbnail_id)
|
||||||
|
|
||||||
def write(self, *args, **kwargs):
|
def write(self, *args, **kwargs):
|
||||||
raise RuntimeError("Please use \"put\" method instead")
|
raise RuntimeError('Please use "put" method instead')
|
||||||
|
|
||||||
def writelines(self, *args, **kwargs):
|
def writelines(self, *args, **kwargs):
|
||||||
raise RuntimeError("Please use \"put\" method instead")
|
raise RuntimeError('Please use "put" method instead')
|
||||||
|
|
||||||
|
|
||||||
class ImproperlyConfigured(Exception):
|
class ImproperlyConfigured(Exception):
|
||||||
@@ -1695,14 +1697,17 @@ class ImageField(FileField):
|
|||||||
def __init__(self, size=None, thumbnail_size=None,
|
def __init__(self, size=None, thumbnail_size=None,
|
||||||
collection_name='images', **kwargs):
|
collection_name='images', **kwargs):
|
||||||
if not Image:
|
if not Image:
|
||||||
raise ImproperlyConfigured("PIL library was not found")
|
raise ImproperlyConfigured('PIL library was not found')
|
||||||
|
|
||||||
params_size = ('width', 'height', 'force')
|
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():
|
for att_name, att in extra_args.items():
|
||||||
value = None
|
value = None
|
||||||
if isinstance(att, (tuple, list)):
|
if isinstance(att, (tuple, list)):
|
||||||
if PY3:
|
if six.PY3:
|
||||||
value = dict(itertools.zip_longest(params_size, att,
|
value = dict(itertools.zip_longest(params_size, att,
|
||||||
fillvalue=None))
|
fillvalue=None))
|
||||||
else:
|
else:
|
||||||
@@ -1763,10 +1768,10 @@ class SequenceField(BaseField):
|
|||||||
Generate and Increment the counter
|
Generate and Increment the counter
|
||||||
"""
|
"""
|
||||||
sequence_name = self.get_sequence_name()
|
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]
|
collection = get_db(alias=self.db_alias)[self.collection_name]
|
||||||
counter = collection.find_and_modify(query={"_id": sequence_id},
|
counter = collection.find_and_modify(query={'_id': sequence_id},
|
||||||
update={"$inc": {"next": 1}},
|
update={'$inc': {'next': 1}},
|
||||||
new=True,
|
new=True,
|
||||||
upsert=True)
|
upsert=True)
|
||||||
return self.value_decorator(counter['next'])
|
return self.value_decorator(counter['next'])
|
||||||
@@ -1789,9 +1794,9 @@ class SequenceField(BaseField):
|
|||||||
as it is only fixed on set.
|
as it is only fixed on set.
|
||||||
"""
|
"""
|
||||||
sequence_name = self.get_sequence_name()
|
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]
|
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:
|
if data:
|
||||||
return self.value_decorator(data['next'] + 1)
|
return self.value_decorator(data['next'] + 1)
|
||||||
@@ -1861,8 +1866,8 @@ class UUIDField(BaseField):
|
|||||||
if not self._binary:
|
if not self._binary:
|
||||||
original_value = value
|
original_value = value
|
||||||
try:
|
try:
|
||||||
if not isinstance(value, basestring):
|
if not isinstance(value, six.string_types):
|
||||||
value = unicode(value)
|
value = six.text_type(value)
|
||||||
return uuid.UUID(value)
|
return uuid.UUID(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
return original_value
|
return original_value
|
||||||
@@ -1870,8 +1875,8 @@ class UUIDField(BaseField):
|
|||||||
|
|
||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
if not self._binary:
|
if not self._binary:
|
||||||
return unicode(value)
|
return six.text_type(value)
|
||||||
elif isinstance(value, basestring):
|
elif isinstance(value, six.string_types):
|
||||||
return uuid.UUID(value)
|
return uuid.UUID(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -1882,11 +1887,11 @@ class UUIDField(BaseField):
|
|||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if not isinstance(value, uuid.UUID):
|
if not isinstance(value, uuid.UUID):
|
||||||
if not isinstance(value, basestring):
|
if not isinstance(value, six.string_types):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
try:
|
try:
|
||||||
uuid.UUID(value)
|
uuid.UUID(value)
|
||||||
except Exception, exc:
|
except Exception as exc:
|
||||||
self.error('Could not convert to UUID: %s' % exc)
|
self.error('Could not convert to UUID: %s' % exc)
|
||||||
|
|
||||||
|
|
||||||
@@ -1904,19 +1909,18 @@ class GeoPointField(BaseField):
|
|||||||
_geo_index = pymongo.GEO2D
|
_geo_index = pymongo.GEO2D
|
||||||
|
|
||||||
def validate(self, value):
|
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)):
|
if not isinstance(value, (list, tuple)):
|
||||||
self.error('GeoPointField can only accept tuples or lists '
|
self.error('GeoPointField can only accept tuples or lists '
|
||||||
'of (x, y)')
|
'of (x, y)')
|
||||||
|
|
||||||
if not len(value) == 2:
|
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))
|
repr(value))
|
||||||
elif (not isinstance(value[0], (float, int)) or
|
elif (not isinstance(value[0], (float, int)) or
|
||||||
not isinstance(value[1], (float, int))):
|
not isinstance(value[1], (float, int))):
|
||||||
self.error(
|
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):
|
class PointField(GeoJsonBaseField):
|
||||||
@@ -1926,8 +1930,8 @@ class PointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "Point" ,
|
{'type' : 'Point' ,
|
||||||
"coordinates" : [x, y]}
|
'coordinates' : [x, y]}
|
||||||
|
|
||||||
You can either pass a dict with the full information or a list
|
You can either pass a dict with the full information or a list
|
||||||
to set the value.
|
to set the value.
|
||||||
@@ -1936,7 +1940,7 @@ class PointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = "Point"
|
_type = 'Point'
|
||||||
|
|
||||||
|
|
||||||
class LineStringField(GeoJsonBaseField):
|
class LineStringField(GeoJsonBaseField):
|
||||||
@@ -1946,8 +1950,8 @@ class LineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "LineString" ,
|
{'type' : 'LineString' ,
|
||||||
"coordinates" : [[x1, y1], [x1, y1] ... [xn, yn]]}
|
'coordinates' : [[x1, y1], [x1, y1] ... [xn, yn]]}
|
||||||
|
|
||||||
You can either pass a dict with the full information or a list of points.
|
You can either pass a dict with the full information or a list of points.
|
||||||
|
|
||||||
@@ -1955,7 +1959,7 @@ class LineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = "LineString"
|
_type = 'LineString'
|
||||||
|
|
||||||
|
|
||||||
class PolygonField(GeoJsonBaseField):
|
class PolygonField(GeoJsonBaseField):
|
||||||
@@ -1965,9 +1969,9 @@ class PolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "Polygon" ,
|
{'type' : 'Polygon' ,
|
||||||
"coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]],
|
'coordinates' : [[[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
|
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
|
of LineStrings. The first LineString being the outside and the rest being
|
||||||
@@ -1977,7 +1981,7 @@ class PolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = "Polygon"
|
_type = 'Polygon'
|
||||||
|
|
||||||
|
|
||||||
class MultiPointField(GeoJsonBaseField):
|
class MultiPointField(GeoJsonBaseField):
|
||||||
@@ -1987,8 +1991,8 @@ class MultiPointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "MultiPoint" ,
|
{'type' : 'MultiPoint' ,
|
||||||
"coordinates" : [[x1, y1], [x2, y2]]}
|
'coordinates' : [[x1, y1], [x2, y2]]}
|
||||||
|
|
||||||
You can either pass a dict with the full information or a list
|
You can either pass a dict with the full information or a list
|
||||||
to set the value.
|
to set the value.
|
||||||
@@ -1997,7 +2001,7 @@ class MultiPointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = "MultiPoint"
|
_type = 'MultiPoint'
|
||||||
|
|
||||||
|
|
||||||
class MultiLineStringField(GeoJsonBaseField):
|
class MultiLineStringField(GeoJsonBaseField):
|
||||||
@@ -2007,9 +2011,9 @@ class MultiLineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "MultiLineString" ,
|
{'type' : 'MultiLineString' ,
|
||||||
"coordinates" : [[[x1, y1], [x1, y1] ... [xn, yn]],
|
'coordinates' : [[[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 of points.
|
You can either pass a dict with the full information or a list of points.
|
||||||
|
|
||||||
@@ -2017,7 +2021,7 @@ class MultiLineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = "MultiLineString"
|
_type = 'MultiLineString'
|
||||||
|
|
||||||
|
|
||||||
class MultiPolygonField(GeoJsonBaseField):
|
class MultiPolygonField(GeoJsonBaseField):
|
||||||
@@ -2027,14 +2031,14 @@ class MultiPolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. code-block:: js
|
.. code-block:: js
|
||||||
|
|
||||||
{ "type" : "MultiPolygon" ,
|
{'type' : 'MultiPolygon' ,
|
||||||
"coordinates" : [[
|
'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]]
|
||||||
], [
|
], [
|
||||||
[[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
|
You can either pass a dict with the full information or a list
|
||||||
@@ -2044,4 +2048,4 @@ class MultiPolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = "MultiPolygon"
|
_type = 'MultiPolygon'
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
"""Helper functions and types to aid with Python 2.5 - 3 support."""
|
"""
|
||||||
|
Helper functions, constants, and types to aid with Python v2.7 - v3.x and
|
||||||
import sys
|
PyMongo v2.7 - v3.x support.
|
||||||
|
"""
|
||||||
import pymongo
|
import pymongo
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
if pymongo.version_tuple[0] < 3:
|
if pymongo.version_tuple[0] < 3:
|
||||||
@@ -9,29 +11,15 @@ if pymongo.version_tuple[0] < 3:
|
|||||||
else:
|
else:
|
||||||
IS_PYMONGO_3 = True
|
IS_PYMONGO_3 = True
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
if PY3:
|
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
|
||||||
import codecs
|
StringIO = six.BytesIO
|
||||||
from io import BytesIO as StringIO
|
|
||||||
|
|
||||||
# return s converted to binary. b('test') should be equivalent to b'test'
|
# Additionally for Py2, try to use the faster cStringIO, if available
|
||||||
def b(s):
|
if not six.PY3:
|
||||||
return codecs.latin_1_encode(s)[0]
|
|
||||||
|
|
||||||
bin_type = bytes
|
|
||||||
txt_type = str
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
from cStringIO import StringIO
|
import cStringIO
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from StringIO import StringIO
|
pass
|
||||||
|
else:
|
||||||
# Conversion to binary only necessary in Python 3
|
StringIO = cStringIO.StringIO
|
||||||
def b(s):
|
|
||||||
return s
|
|
||||||
|
|
||||||
bin_type = str
|
|
||||||
txt_type = unicode
|
|
||||||
|
|
||||||
str_types = (bin_type, txt_type)
|
|
||||||
|
@@ -1,11 +1,17 @@
|
|||||||
from mongoengine.errors import (DoesNotExist, InvalidQueryError,
|
from mongoengine.errors import *
|
||||||
MultipleObjectsReturned, NotUniqueError,
|
|
||||||
OperationError)
|
|
||||||
from mongoengine.queryset.field_list import *
|
from mongoengine.queryset.field_list import *
|
||||||
from mongoengine.queryset.manager import *
|
from mongoengine.queryset.manager import *
|
||||||
from mongoengine.queryset.queryset import *
|
from mongoengine.queryset.queryset import *
|
||||||
from mongoengine.queryset.transform import *
|
from mongoengine.queryset.transform import *
|
||||||
from mongoengine.queryset.visitor import *
|
from mongoengine.queryset.visitor import *
|
||||||
|
|
||||||
__all__ = (field_list.__all__ + manager.__all__ + queryset.__all__ +
|
# Expose just the public subset of all imported objects and constants.
|
||||||
transform.__all__ + visitor.__all__)
|
__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
@@ -67,7 +67,7 @@ class QueryFieldList(object):
|
|||||||
return bool(self.fields)
|
return bool(self.fields)
|
||||||
|
|
||||||
def as_dict(self):
|
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:
|
if self.slice:
|
||||||
field_list.update(self.slice)
|
field_list.update(self.slice)
|
||||||
if self._id is not None:
|
if self._id is not None:
|
||||||
|
@@ -27,9 +27,10 @@ class QuerySet(BaseQuerySet):
|
|||||||
in batches of ``ITER_CHUNK_SIZE``.
|
in batches of ``ITER_CHUNK_SIZE``.
|
||||||
|
|
||||||
If ``self._has_more`` the cursor hasn't been exhausted so cache then
|
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
|
self._iter = True
|
||||||
|
|
||||||
if self._has_more:
|
if self._has_more:
|
||||||
return self._iter_results()
|
return self._iter_results()
|
||||||
|
|
||||||
@@ -42,40 +43,56 @@ class QuerySet(BaseQuerySet):
|
|||||||
"""
|
"""
|
||||||
if self._len is not None:
|
if self._len is not None:
|
||||||
return self._len
|
return self._len
|
||||||
|
|
||||||
|
# Populate the result cache with *all* of the docs in the cursor
|
||||||
if self._has_more:
|
if self._has_more:
|
||||||
# populate the cache
|
|
||||||
list(self._iter_results())
|
list(self._iter_results())
|
||||||
|
|
||||||
|
# Cache the length of the complete result cache and return it
|
||||||
self._len = len(self._result_cache)
|
self._len = len(self._result_cache)
|
||||||
return self._len
|
return self._len
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
"""Provides the string representation of the QuerySet
|
"""Provide a string representation of the QuerySet"""
|
||||||
"""
|
|
||||||
if self._iter:
|
if self._iter:
|
||||||
return '.. queryset mid-iteration ..'
|
return '.. queryset mid-iteration ..'
|
||||||
|
|
||||||
self._populate_cache()
|
self._populate_cache()
|
||||||
data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
|
data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
|
||||||
if len(data) > REPR_OUTPUT_SIZE:
|
if len(data) > REPR_OUTPUT_SIZE:
|
||||||
data[-1] = "...(remaining elements truncated)..."
|
data[-1] = '...(remaining elements truncated)...'
|
||||||
return repr(data)
|
return repr(data)
|
||||||
|
|
||||||
def _iter_results(self):
|
def _iter_results(self):
|
||||||
"""A generator for iterating over the result cache.
|
"""A generator for iterating over the result cache.
|
||||||
|
|
||||||
Also populates the cache if there are more possible results to yield.
|
Also populates the cache if there are more possible results to
|
||||||
Raises StopIteration when there are no more results"""
|
yield. Raises StopIteration when there are no more results.
|
||||||
|
"""
|
||||||
if self._result_cache is None:
|
if self._result_cache is None:
|
||||||
self._result_cache = []
|
self._result_cache = []
|
||||||
|
|
||||||
pos = 0
|
pos = 0
|
||||||
while True:
|
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]
|
yield self._result_cache[pos]
|
||||||
pos += 1
|
pos += 1
|
||||||
|
|
||||||
|
# Raise StopIteration if we already established there were no more
|
||||||
|
# docs in the db cursor.
|
||||||
if not self._has_more:
|
if not self._has_more:
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
|
|
||||||
|
# Otherwise, populate more of the cache and repeat.
|
||||||
if len(self._result_cache) <= pos:
|
if len(self._result_cache) <= pos:
|
||||||
self._populate_cache()
|
self._populate_cache()
|
||||||
|
|
||||||
@@ -86,12 +103,22 @@ class QuerySet(BaseQuerySet):
|
|||||||
"""
|
"""
|
||||||
if self._result_cache is None:
|
if self._result_cache is None:
|
||||||
self._result_cache = []
|
self._result_cache = []
|
||||||
if self._has_more:
|
|
||||||
try:
|
# Skip populating the cache if we already established there are no
|
||||||
for i in xrange(ITER_CHUNK_SIZE):
|
# more docs to pull from the database.
|
||||||
self._result_cache.append(self.next())
|
if not self._has_more:
|
||||||
except StopIteration:
|
return
|
||||||
self._has_more = False
|
|
||||||
|
# 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):
|
def count(self, with_limit_and_skip=False):
|
||||||
"""Count the selected elements in the query.
|
"""Count the selected elements in the query.
|
||||||
@@ -109,13 +136,15 @@ class QuerySet(BaseQuerySet):
|
|||||||
return self._len
|
return self._len
|
||||||
|
|
||||||
def no_cache(self):
|
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
|
.. versionadded:: 0.8.3 Convert to non caching queryset
|
||||||
"""
|
"""
|
||||||
if self._result_cache is not None:
|
if self._result_cache is not None:
|
||||||
raise OperationError("QuerySet already cached")
|
raise OperationError('QuerySet already cached')
|
||||||
return self.clone_into(QuerySetNoCache(self._document, self._collection))
|
|
||||||
|
return self._clone_into(QuerySetNoCache(self._document,
|
||||||
|
self._collection))
|
||||||
|
|
||||||
|
|
||||||
class QuerySetNoCache(BaseQuerySet):
|
class QuerySetNoCache(BaseQuerySet):
|
||||||
@@ -126,7 +155,7 @@ class QuerySetNoCache(BaseQuerySet):
|
|||||||
|
|
||||||
.. versionadded:: 0.8.3 Convert to caching queryset
|
.. 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):
|
def __repr__(self):
|
||||||
"""Provides the string representation of the QuerySet
|
"""Provides the string representation of the QuerySet
|
||||||
@@ -137,13 +166,14 @@ class QuerySetNoCache(BaseQuerySet):
|
|||||||
return '.. queryset mid-iteration ..'
|
return '.. queryset mid-iteration ..'
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for i in xrange(REPR_OUTPUT_SIZE + 1):
|
for _ in xrange(REPR_OUTPUT_SIZE + 1):
|
||||||
try:
|
try:
|
||||||
data.append(self.next())
|
data.append(self.next())
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
break
|
break
|
||||||
|
|
||||||
if len(data) > REPR_OUTPUT_SIZE:
|
if len(data) > REPR_OUTPUT_SIZE:
|
||||||
data[-1] = "...(remaining elements truncated)..."
|
data[-1] = '...(remaining elements truncated)...'
|
||||||
|
|
||||||
self.rewind()
|
self.rewind()
|
||||||
return repr(data)
|
return repr(data)
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from bson import SON
|
from bson import ObjectId, SON
|
||||||
|
from bson.dbref import DBRef
|
||||||
import pymongo
|
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.common import _import_class
|
||||||
from mongoengine.connection import get_connection
|
from mongoengine.connection import get_connection
|
||||||
from mongoengine.errors import InvalidQueryError
|
from mongoengine.errors import InvalidQueryError
|
||||||
@@ -26,13 +28,13 @@ MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
|
|||||||
STRING_OPERATORS + CUSTOM_OPERATORS)
|
STRING_OPERATORS + CUSTOM_OPERATORS)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO make this less complex
|
||||||
def query(_doc_cls=None, **kwargs):
|
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 = {}
|
mongo_query = {}
|
||||||
merge_query = defaultdict(list)
|
merge_query = defaultdict(list)
|
||||||
for key, value in sorted(kwargs.items()):
|
for key, value in sorted(kwargs.items()):
|
||||||
if key == "__raw__":
|
if key == '__raw__':
|
||||||
mongo_query.update(value)
|
mongo_query.update(value)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
op = parts.pop()
|
op = parts.pop()
|
||||||
|
|
||||||
# Allow to escape operator-like field name by __
|
# Allow to escape operator-like field name by __
|
||||||
if len(parts) > 1 and parts[-1] == "":
|
if len(parts) > 1 and parts[-1] == '':
|
||||||
parts.pop()
|
parts.pop()
|
||||||
|
|
||||||
negate = False
|
negate = False
|
||||||
@@ -57,16 +59,17 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
# Switch field names to proper names [set in Field(name='foo')]
|
# Switch field names to proper names [set in Field(name='foo')]
|
||||||
try:
|
try:
|
||||||
fields = _doc_cls._lookup_field(parts)
|
fields = _doc_cls._lookup_field(parts)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise InvalidQueryError(e)
|
raise InvalidQueryError(e)
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
CachedReferenceField = _import_class('CachedReferenceField')
|
CachedReferenceField = _import_class('CachedReferenceField')
|
||||||
|
GenericReferenceField = _import_class('GenericReferenceField')
|
||||||
|
|
||||||
cleaned_fields = []
|
cleaned_fields = []
|
||||||
for field in fields:
|
for field in fields:
|
||||||
append_field = True
|
append_field = True
|
||||||
if isinstance(field, basestring):
|
if isinstance(field, six.string_types):
|
||||||
parts.append(field)
|
parts.append(field)
|
||||||
append_field = False
|
append_field = False
|
||||||
# is last and CachedReferenceField
|
# 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 = [None, 'ne', 'gt', 'gte', 'lt', 'lte', 'not']
|
||||||
singular_ops += STRING_OPERATORS
|
singular_ops += STRING_OPERATORS
|
||||||
if op in singular_ops:
|
if op in singular_ops:
|
||||||
if isinstance(field, basestring):
|
if isinstance(field, six.string_types):
|
||||||
if (op in STRING_OPERATORS and
|
if (op in STRING_OPERATORS and
|
||||||
isinstance(value, basestring)):
|
isinstance(value, six.string_types)):
|
||||||
StringField = _import_class('StringField')
|
StringField = _import_class('StringField')
|
||||||
value = StringField.prepare_query_value(op, value)
|
value = StringField.prepare_query_value(op, value)
|
||||||
else:
|
else:
|
||||||
@@ -98,8 +101,31 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
value = value['_id']
|
value = value['_id']
|
||||||
|
|
||||||
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
|
elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
|
||||||
# 'in', 'nin' and 'all' require a list of values
|
# Raise an error if the in/nin/all/near param is not iterable. We need a
|
||||||
value = [field.prepare_query_value(op, v) for v in value]
|
# 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 and op not in COMPARISON_OPERATORS:
|
||||||
if op:
|
if op:
|
||||||
@@ -116,10 +142,10 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
value = query(field.field.document_type, **value)
|
value = query(field.field.document_type, **value)
|
||||||
else:
|
else:
|
||||||
value = field.prepare_query_value(op, value)
|
value = field.prepare_query_value(op, value)
|
||||||
value = {"$elemMatch": value}
|
value = {'$elemMatch': value}
|
||||||
elif op in CUSTOM_OPERATORS:
|
elif op in CUSTOM_OPERATORS:
|
||||||
NotImplementedError("Custom method '%s' has not "
|
NotImplementedError('Custom method "%s" has not '
|
||||||
"been implemented" % op)
|
'been implemented' % op)
|
||||||
elif op not in STRING_OPERATORS:
|
elif op not in STRING_OPERATORS:
|
||||||
value = {'$' + op: value}
|
value = {'$' + op: value}
|
||||||
|
|
||||||
@@ -128,11 +154,13 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
|
|
||||||
for i, part in indices:
|
for i, part in indices:
|
||||||
parts.insert(i, part)
|
parts.insert(i, part)
|
||||||
|
|
||||||
key = '.'.join(parts)
|
key = '.'.join(parts)
|
||||||
|
|
||||||
if op is None or key not in mongo_query:
|
if op is None or key not in mongo_query:
|
||||||
mongo_query[key] = value
|
mongo_query[key] = value
|
||||||
elif key in mongo_query:
|
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)
|
mongo_query[key].update(value)
|
||||||
# $max/minDistance needs to come last - convert to SON
|
# $max/minDistance needs to come last - convert to SON
|
||||||
value_dict = mongo_query[key]
|
value_dict = mongo_query[key]
|
||||||
@@ -182,15 +210,16 @@ def query(_doc_cls=None, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def update(_doc_cls=None, **update):
|
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 = {}
|
mongo_update = {}
|
||||||
for key, value in update.items():
|
for key, value in update.items():
|
||||||
if key == "__raw__":
|
if key == '__raw__':
|
||||||
mongo_update.update(value)
|
mongo_update.update(value)
|
||||||
continue
|
continue
|
||||||
parts = key.split('__')
|
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:
|
if len(parts) < 3 and parts[0] not in UPDATE_OPERATORS:
|
||||||
parts.insert(0, 'set')
|
parts.insert(0, 'set')
|
||||||
# Check for an operator and transform to mongo-style if there is
|
# 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
|
# Support decrement by flipping a positive value's sign
|
||||||
# and using 'inc'
|
# and using 'inc'
|
||||||
op = 'inc'
|
op = 'inc'
|
||||||
if value > 0:
|
value = -value
|
||||||
value = -value
|
|
||||||
elif op == 'add_to_set':
|
elif op == 'add_to_set':
|
||||||
op = 'addToSet'
|
op = 'addToSet'
|
||||||
elif op == 'set_on_insert':
|
elif op == 'set_on_insert':
|
||||||
op = "setOnInsert"
|
op = 'setOnInsert'
|
||||||
|
|
||||||
match = None
|
match = None
|
||||||
if parts[-1] in COMPARISON_OPERATORS:
|
if parts[-1] in COMPARISON_OPERATORS:
|
||||||
match = parts.pop()
|
match = parts.pop()
|
||||||
|
|
||||||
# Allow to escape operator-like field name by __
|
# Allow to escape operator-like field name by __
|
||||||
if len(parts) > 1 and parts[-1] == "":
|
if len(parts) > 1 and parts[-1] == '':
|
||||||
parts.pop()
|
parts.pop()
|
||||||
|
|
||||||
if _doc_cls:
|
if _doc_cls:
|
||||||
# Switch field names to proper names [set in Field(name='foo')]
|
# Switch field names to proper names [set in Field(name='foo')]
|
||||||
try:
|
try:
|
||||||
fields = _doc_cls._lookup_field(parts)
|
fields = _doc_cls._lookup_field(parts)
|
||||||
except Exception, e:
|
except Exception as e:
|
||||||
raise InvalidQueryError(e)
|
raise InvalidQueryError(e)
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
@@ -231,7 +259,7 @@ def update(_doc_cls=None, **update):
|
|||||||
appended_sub_field = False
|
appended_sub_field = False
|
||||||
for field in fields:
|
for field in fields:
|
||||||
append_field = True
|
append_field = True
|
||||||
if isinstance(field, basestring):
|
if isinstance(field, six.string_types):
|
||||||
# Convert the S operator to $
|
# Convert the S operator to $
|
||||||
if field == 'S':
|
if field == 'S':
|
||||||
field = '$'
|
field = '$'
|
||||||
@@ -252,7 +280,7 @@ def update(_doc_cls=None, **update):
|
|||||||
else:
|
else:
|
||||||
field = cleaned_fields[-1]
|
field = cleaned_fields[-1]
|
||||||
|
|
||||||
GeoJsonBaseField = _import_class("GeoJsonBaseField")
|
GeoJsonBaseField = _import_class('GeoJsonBaseField')
|
||||||
if isinstance(field, GeoJsonBaseField):
|
if isinstance(field, GeoJsonBaseField):
|
||||||
value = field.to_mongo(value)
|
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]
|
value = [field.prepare_query_value(op, v) for v in value]
|
||||||
elif field.required or value is not None:
|
elif field.required or value is not None:
|
||||||
value = field.prepare_query_value(op, value)
|
value = field.prepare_query_value(op, value)
|
||||||
elif op == "unset":
|
elif op == 'unset':
|
||||||
value = 1
|
value = 1
|
||||||
|
|
||||||
if match:
|
if match:
|
||||||
@@ -276,16 +304,16 @@ def update(_doc_cls=None, **update):
|
|||||||
key = '.'.join(parts)
|
key = '.'.join(parts)
|
||||||
|
|
||||||
if not op:
|
if not op:
|
||||||
raise InvalidQueryError("Updates must supply an operation "
|
raise InvalidQueryError('Updates must supply an operation '
|
||||||
"eg: set__FIELD=value")
|
'eg: set__FIELD=value')
|
||||||
|
|
||||||
if 'pull' in op and '.' in key:
|
if 'pull' in op and '.' in key:
|
||||||
# Dot operators don't work on pull operations
|
# Dot operators don't work on pull operations
|
||||||
# unless they point to a list field
|
# unless they point to a list field
|
||||||
# Otherwise it uses nested dict syntax
|
# Otherwise it uses nested dict syntax
|
||||||
if op == 'pullAll':
|
if op == 'pullAll':
|
||||||
raise InvalidQueryError("pullAll operations only support "
|
raise InvalidQueryError('pullAll operations only support '
|
||||||
"a single field depth")
|
'a single field depth')
|
||||||
|
|
||||||
# Look for the last list field and use dot notation until there
|
# Look for the last list field and use dot notation until there
|
||||||
field_classes = [c.__class__ for c in cleaned_fields]
|
field_classes = [c.__class__ for c in cleaned_fields]
|
||||||
@@ -296,7 +324,7 @@ def update(_doc_cls=None, **update):
|
|||||||
# Then process as normal
|
# Then process as normal
|
||||||
last_listField = len(
|
last_listField = len(
|
||||||
cleaned_fields) - field_classes.index(ListField)
|
cleaned_fields) - field_classes.index(ListField)
|
||||||
key = ".".join(parts[:last_listField])
|
key = '.'.join(parts[:last_listField])
|
||||||
parts = parts[last_listField:]
|
parts = parts[last_listField:]
|
||||||
parts.insert(0, key)
|
parts.insert(0, key)
|
||||||
|
|
||||||
@@ -304,7 +332,7 @@ def update(_doc_cls=None, **update):
|
|||||||
for key in parts:
|
for key in parts:
|
||||||
value = {key: value}
|
value = {key: value}
|
||||||
elif op == 'addToSet' and isinstance(value, list):
|
elif op == 'addToSet' and isinstance(value, list):
|
||||||
value = {key: {"$each": value}}
|
value = {key: {'$each': value}}
|
||||||
else:
|
else:
|
||||||
value = {key: value}
|
value = {key: value}
|
||||||
key = '$' + op
|
key = '$' + op
|
||||||
@@ -318,78 +346,82 @@ def update(_doc_cls=None, **update):
|
|||||||
|
|
||||||
|
|
||||||
def _geo_operator(field, op, value):
|
def _geo_operator(field, op, value):
|
||||||
"""Helper to return the query for a given geo query"""
|
"""Helper to return the query for a given geo query."""
|
||||||
if op == "max_distance":
|
if op == 'max_distance':
|
||||||
value = {'$maxDistance': value}
|
value = {'$maxDistance': value}
|
||||||
elif op == "min_distance":
|
elif op == 'min_distance':
|
||||||
value = {'$minDistance': value}
|
value = {'$minDistance': value}
|
||||||
elif field._geo_index == pymongo.GEO2D:
|
elif field._geo_index == pymongo.GEO2D:
|
||||||
if op == "within_distance":
|
if op == 'within_distance':
|
||||||
value = {'$within': {'$center': value}}
|
value = {'$within': {'$center': value}}
|
||||||
elif op == "within_spherical_distance":
|
elif op == 'within_spherical_distance':
|
||||||
value = {'$within': {'$centerSphere': value}}
|
value = {'$within': {'$centerSphere': value}}
|
||||||
elif op == "within_polygon":
|
elif op == 'within_polygon':
|
||||||
value = {'$within': {'$polygon': value}}
|
value = {'$within': {'$polygon': value}}
|
||||||
elif op == "near":
|
elif op == 'near':
|
||||||
value = {'$near': value}
|
value = {'$near': value}
|
||||||
elif op == "near_sphere":
|
elif op == 'near_sphere':
|
||||||
value = {'$nearSphere': value}
|
value = {'$nearSphere': value}
|
||||||
elif op == 'within_box':
|
elif op == 'within_box':
|
||||||
value = {'$within': {'$box': value}}
|
value = {'$within': {'$box': value}}
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Geo method '%s' has not "
|
raise NotImplementedError('Geo method "%s" has not been '
|
||||||
"been implemented for a GeoPointField" % op)
|
'implemented for a GeoPointField' % op)
|
||||||
else:
|
else:
|
||||||
if op == "geo_within":
|
if op == 'geo_within':
|
||||||
value = {"$geoWithin": _infer_geometry(value)}
|
value = {'$geoWithin': _infer_geometry(value)}
|
||||||
elif op == "geo_within_box":
|
elif op == 'geo_within_box':
|
||||||
value = {"$geoWithin": {"$box": value}}
|
value = {'$geoWithin': {'$box': value}}
|
||||||
elif op == "geo_within_polygon":
|
elif op == 'geo_within_polygon':
|
||||||
value = {"$geoWithin": {"$polygon": value}}
|
value = {'$geoWithin': {'$polygon': value}}
|
||||||
elif op == "geo_within_center":
|
elif op == 'geo_within_center':
|
||||||
value = {"$geoWithin": {"$center": value}}
|
value = {'$geoWithin': {'$center': value}}
|
||||||
elif op == "geo_within_sphere":
|
elif op == 'geo_within_sphere':
|
||||||
value = {"$geoWithin": {"$centerSphere": value}}
|
value = {'$geoWithin': {'$centerSphere': value}}
|
||||||
elif op == "geo_intersects":
|
elif op == 'geo_intersects':
|
||||||
value = {"$geoIntersects": _infer_geometry(value)}
|
value = {'$geoIntersects': _infer_geometry(value)}
|
||||||
elif op == "near":
|
elif op == 'near':
|
||||||
value = {'$near': _infer_geometry(value)}
|
value = {'$near': _infer_geometry(value)}
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Geo method '%s' has not "
|
raise NotImplementedError(
|
||||||
"been implemented for a %s " % (op, field._name))
|
'Geo method "%s" has not been implemented for a %s '
|
||||||
|
% (op, field._name)
|
||||||
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _infer_geometry(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 isinstance(value, dict):
|
||||||
if "$geometry" in value:
|
if '$geometry' in value:
|
||||||
return value
|
return value
|
||||||
elif 'coordinates' in value and 'type' in value:
|
elif 'coordinates' in value and 'type' in value:
|
||||||
return {"$geometry": value}
|
return {'$geometry': value}
|
||||||
raise InvalidQueryError("Invalid $geometry dictionary should have "
|
raise InvalidQueryError('Invalid $geometry dictionary should have '
|
||||||
"type and coordinates keys")
|
'type and coordinates keys')
|
||||||
elif isinstance(value, (list, set)):
|
elif isinstance(value, (list, set)):
|
||||||
# TODO: shouldn't we test value[0][0][0][0] to see if it is MultiPolygon?
|
# 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?
|
# TODO: should both TypeError and IndexError be alike interpreted?
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value[0][0][0]
|
value[0][0][0]
|
||||||
return {"$geometry": {"type": "Polygon", "coordinates": value}}
|
return {'$geometry': {'type': 'Polygon', 'coordinates': value}}
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value[0][0]
|
value[0][0]
|
||||||
return {"$geometry": {"type": "LineString", "coordinates": value}}
|
return {'$geometry': {'type': 'LineString', 'coordinates': value}}
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value[0]
|
value[0]
|
||||||
return {"$geometry": {"type": "Point", "coordinates": value}}
|
return {'$geometry': {'type': 'Point', 'coordinates': value}}
|
||||||
except (TypeError, IndexError):
|
except (TypeError, IndexError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
raise InvalidQueryError("Invalid $geometry data. Can be either a dictionary "
|
raise InvalidQueryError('Invalid $geometry data. Can be either a '
|
||||||
"or (nested) lists of coordinate(s)")
|
'dictionary or (nested) lists of coordinate(s)')
|
||||||
|
@@ -69,9 +69,9 @@ class QueryCompilerVisitor(QNodeVisitor):
|
|||||||
self.document = document
|
self.document = document
|
||||||
|
|
||||||
def visit_combination(self, combination):
|
def visit_combination(self, combination):
|
||||||
operator = "$and"
|
operator = '$and'
|
||||||
if combination.operation == combination.OR:
|
if combination.operation == combination.OR:
|
||||||
operator = "$or"
|
operator = '$or'
|
||||||
return {operator: combination.children}
|
return {operator: combination.children}
|
||||||
|
|
||||||
def visit_query(self, query):
|
def visit_query(self, query):
|
||||||
@@ -79,8 +79,7 @@ class QueryCompilerVisitor(QNodeVisitor):
|
|||||||
|
|
||||||
|
|
||||||
class QNode(object):
|
class QNode(object):
|
||||||
"""Base class for nodes in query trees.
|
"""Base class for nodes in query trees."""
|
||||||
"""
|
|
||||||
|
|
||||||
AND = 0
|
AND = 0
|
||||||
OR = 1
|
OR = 1
|
||||||
@@ -94,7 +93,8 @@ class QNode(object):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _combine(self, other, operation):
|
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):
|
if getattr(other, 'empty', True):
|
||||||
return self
|
return self
|
||||||
@@ -116,8 +116,8 @@ class QNode(object):
|
|||||||
|
|
||||||
|
|
||||||
class QCombination(QNode):
|
class QCombination(QNode):
|
||||||
"""Represents the combination of several conditions by a given logical
|
"""Represents the combination of several conditions by a given
|
||||||
operator.
|
logical operator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, operation, children):
|
def __init__(self, operation, children):
|
||||||
|
@@ -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
|
signals_available = False
|
||||||
try:
|
try:
|
||||||
@@ -34,6 +32,7 @@ except ImportError:
|
|||||||
temporarily_connected_to = _fail
|
temporarily_connected_to = _fail
|
||||||
del _fail
|
del _fail
|
||||||
|
|
||||||
|
|
||||||
# the namespace for code signals. If you are not mongoengine code, do
|
# the namespace for code signals. If you are not mongoengine code, do
|
||||||
# not put signals in here. Create your own namespace instead.
|
# not put signals in here. Create your own namespace instead.
|
||||||
_signals = Namespace()
|
_signals = Namespace()
|
||||||
|
14
setup.cfg
14
setup.cfg
@@ -1,13 +1,11 @@
|
|||||||
[nosetests]
|
[nosetests]
|
||||||
verbosity = 2
|
verbosity=2
|
||||||
detailed-errors = 1
|
detailed-errors=1
|
||||||
cover-erase = 1
|
tests=tests
|
||||||
cover-branches = 1
|
cover-package=mongoengine
|
||||||
cover-package = mongoengine
|
|
||||||
tests = tests
|
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore=E501,F401,F403,F405,I201
|
ignore=E501,F401,F403,F405,I201
|
||||||
exclude=build,dist,docs,venv,.tox,.eggs,tests
|
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
|
||||||
max-complexity=42
|
max-complexity=47
|
||||||
application-import-names=mongoengine,tests
|
application-import-names=mongoengine,tests
|
||||||
|
25
setup.py
25
setup.py
@@ -21,8 +21,9 @@ except Exception:
|
|||||||
|
|
||||||
|
|
||||||
def get_version(version_tuple):
|
def get_version(version_tuple):
|
||||||
if not isinstance(version_tuple[-1], int):
|
"""Return the version tuple as a string, e.g. for (0, 10, 7),
|
||||||
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
|
return '0.10.7'.
|
||||||
|
"""
|
||||||
return '.'.join(map(str, version_tuple))
|
return '.'.join(map(str, version_tuple))
|
||||||
|
|
||||||
|
|
||||||
@@ -41,31 +42,29 @@ CLASSIFIERS = [
|
|||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
"Programming Language :: Python :: 2.6",
|
|
||||||
"Programming Language :: Python :: 2.7",
|
"Programming Language :: Python :: 2.7",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.2",
|
|
||||||
"Programming Language :: Python :: 3.3",
|
"Programming Language :: Python :: 3.3",
|
||||||
"Programming Language :: Python :: 3.4",
|
"Programming Language :: Python :: 3.4",
|
||||||
|
"Programming Language :: Python :: 3.5",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
'Topic :: Database',
|
'Topic :: Database',
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
'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:
|
if sys.version_info[0] == 3:
|
||||||
extra_opts['use_2to3'] = True
|
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['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:
|
else:
|
||||||
# coverage 4 does not support Python 3.2 anymore
|
extra_opts['tests_require'] += ['python-dateutil']
|
||||||
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')
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='mongoengine',
|
name='mongoengine',
|
||||||
|
@@ -2,4 +2,3 @@ from all_warnings import AllWarnings
|
|||||||
from document import *
|
from document import *
|
||||||
from queryset import *
|
from queryset import *
|
||||||
from fields import *
|
from fields import *
|
||||||
from migration import *
|
|
||||||
|
@@ -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
|
only get triggered on first hit. This way we can ensure its imported into the
|
||||||
top level and called first by the test suite.
|
top level and called first by the test suite.
|
||||||
"""
|
"""
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from class_methods import *
|
from class_methods import *
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bson import SON
|
from bson import SON
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import get_db
|
from mongoengine.connection import get_db
|
||||||
@@ -143,11 +141,9 @@ class DynamicTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_three_level_complex_data_lookups(self):
|
def test_three_level_complex_data_lookups(self):
|
||||||
"""Ensure you can query three level document dynamic fields"""
|
"""Ensure you can query three level document dynamic fields"""
|
||||||
p = self.Person()
|
p = self.Person.objects.create(
|
||||||
p.misc = {'hello': {'hello2': 'world'}}
|
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')
|
|
||||||
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
|
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
|
||||||
|
|
||||||
def test_complex_embedded_document_validation(self):
|
def test_complex_embedded_document_validation(self):
|
||||||
|
@@ -2,26 +2,22 @@
|
|||||||
import unittest
|
import unittest
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import pymongo
|
|
||||||
from random import randint
|
|
||||||
|
|
||||||
from nose.plugins.skip import SkipTest
|
from nose.plugins.skip import SkipTest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import pymongo
|
||||||
|
|
||||||
from mongoengine import *
|
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", )
|
__all__ = ("IndexesTest", )
|
||||||
|
|
||||||
|
|
||||||
class IndexesTest(unittest.TestCase):
|
class IndexesTest(unittest.TestCase):
|
||||||
_MAX_RAND = 10 ** 10
|
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.db_name = 'mongoenginetest_IndexesTest_' + str(randint(0, self._MAX_RAND))
|
self.connection = connect(db='mongoenginetest')
|
||||||
self.connection = connect(db=self.db_name)
|
|
||||||
self.db = get_db()
|
self.db = get_db()
|
||||||
|
|
||||||
class Person(Document):
|
class Person(Document):
|
||||||
@@ -498,8 +494,7 @@ class IndexesTest(unittest.TestCase):
|
|||||||
obj = Test(a=1)
|
obj = Test(a=1)
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
connection = get_connection()
|
IS_MONGODB_3 = get_mongodb_version()[0] >= 3
|
||||||
IS_MONGODB_3 = connection.server_info()['versionArray'][0] >= 3
|
|
||||||
|
|
||||||
# Need to be explicit about covered indexes as mongoDB doesn't know if
|
# Need to be explicit about covered indexes as mongoDB doesn't know if
|
||||||
# the documents returned might have more keys in that here.
|
# the documents returned might have more keys in that here.
|
||||||
@@ -560,8 +555,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
for i in xrange(0, 10):
|
for i in range(0, 10):
|
||||||
tags = [("tag %i" % n) for n in xrange(0, i % 2)]
|
tags = [("tag %i" % n) for n in range(0, i % 2)]
|
||||||
BlogPost(tags=tags).save()
|
BlogPost(tags=tags).save()
|
||||||
|
|
||||||
self.assertEqual(BlogPost.objects.count(), 10)
|
self.assertEqual(BlogPost.objects.count(), 10)
|
||||||
@@ -737,14 +732,6 @@ class IndexesTest(unittest.TestCase):
|
|||||||
|
|
||||||
Log.drop_collection()
|
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
|
# Indexes are lazy so use list() to perform query
|
||||||
list(Log.objects)
|
list(Log.objects)
|
||||||
info = Log.objects._collection.index_information()
|
info = Log.objects._collection.index_information()
|
||||||
@@ -878,8 +865,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
|
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
|
||||||
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
|
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
|
||||||
|
|
||||||
|
@needs_mongodb_v26
|
||||||
def test_text_indexes(self):
|
def test_text_indexes(self):
|
||||||
|
|
||||||
class Book(Document):
|
class Book(Document):
|
||||||
title = DictField()
|
title = DictField()
|
||||||
meta = {
|
meta = {
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@@ -253,19 +251,17 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(classes, [Human])
|
self.assertEqual(classes, [Human])
|
||||||
|
|
||||||
def test_allow_inheritance(self):
|
def test_allow_inheritance(self):
|
||||||
"""Ensure that inheritance may be disabled on simple classes and that
|
"""Ensure that inheritance is disabled by default on simple
|
||||||
_cls and _subclasses will not be used.
|
classes and that _cls will not be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Animal(Document):
|
class Animal(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
def create_dog_class():
|
# can't inherit because Animal didn't explicitly allow inheritance
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
class Dog(Animal):
|
class Dog(Animal):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.assertRaises(ValueError, create_dog_class)
|
|
||||||
|
|
||||||
# Check that _cls etc aren't present on simple documents
|
# Check that _cls etc aren't present on simple documents
|
||||||
dog = Animal(name='dog').save()
|
dog = Animal(name='dog').save()
|
||||||
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
|
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
|
||||||
@@ -275,17 +271,15 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
self.assertFalse('_cls' in obj)
|
self.assertFalse('_cls' in obj)
|
||||||
|
|
||||||
def test_cant_turn_off_inheritance_on_subclass(self):
|
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):
|
class Animal(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
meta = {'allow_inheritance': True}
|
meta = {'allow_inheritance': True}
|
||||||
|
|
||||||
def create_mammal_class():
|
with self.assertRaises(ValueError):
|
||||||
class Mammal(Animal):
|
class Mammal(Animal):
|
||||||
meta = {'allow_inheritance': False}
|
meta = {'allow_inheritance': False}
|
||||||
self.assertRaises(ValueError, create_mammal_class)
|
|
||||||
|
|
||||||
def test_allow_inheritance_abstract_document(self):
|
def test_allow_inheritance_abstract_document(self):
|
||||||
"""Ensure that abstract documents can set inheritance rules and that
|
"""Ensure that abstract documents can set inheritance rules and that
|
||||||
@@ -298,10 +292,9 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
class Animal(FinalDocument):
|
class Animal(FinalDocument):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
def create_mammal_class():
|
with self.assertRaises(ValueError):
|
||||||
class Mammal(Animal):
|
class Mammal(Animal):
|
||||||
pass
|
pass
|
||||||
self.assertRaises(ValueError, create_mammal_class)
|
|
||||||
|
|
||||||
# Check that _cls isn't present in simple documents
|
# Check that _cls isn't present in simple documents
|
||||||
doc = Animal(name='dog')
|
doc = Animal(name='dog')
|
||||||
@@ -360,29 +353,26 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(berlin.pk, berlin.auto_id_0)
|
self.assertEqual(berlin.pk, berlin.auto_id_0)
|
||||||
|
|
||||||
def test_abstract_document_creation_does_not_fail(self):
|
def test_abstract_document_creation_does_not_fail(self):
|
||||||
|
|
||||||
class City(Document):
|
class City(Document):
|
||||||
continent = StringField()
|
continent = StringField()
|
||||||
meta = {'abstract': True,
|
meta = {'abstract': True,
|
||||||
'allow_inheritance': False}
|
'allow_inheritance': False}
|
||||||
|
|
||||||
bkk = City(continent='asia')
|
bkk = City(continent='asia')
|
||||||
self.assertEqual(None, bkk.pk)
|
self.assertEqual(None, bkk.pk)
|
||||||
# TODO: expected error? Shouldn't we create a new error type?
|
# 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):
|
def test_allow_inheritance_embedded_document(self):
|
||||||
"""Ensure embedded documents respect inheritance
|
"""Ensure embedded documents respect inheritance."""
|
||||||
"""
|
|
||||||
|
|
||||||
class Comment(EmbeddedDocument):
|
class Comment(EmbeddedDocument):
|
||||||
content = StringField()
|
content = StringField()
|
||||||
|
|
||||||
def create_special_comment():
|
with self.assertRaises(ValueError):
|
||||||
class SpecialComment(Comment):
|
class SpecialComment(Comment):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.assertRaises(ValueError, create_special_comment)
|
|
||||||
|
|
||||||
doc = Comment(content='test')
|
doc = Comment(content='test')
|
||||||
self.assertFalse('_cls' in doc.to_mongo())
|
self.assertFalse('_cls' in doc.to_mongo())
|
||||||
|
|
||||||
@@ -454,11 +444,11 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(Guppy._get_collection_name(), 'fish')
|
self.assertEqual(Guppy._get_collection_name(), 'fish')
|
||||||
self.assertEqual(Human._get_collection_name(), 'human')
|
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):
|
class EvilHuman(Human):
|
||||||
evil = BooleanField(default=True)
|
evil = BooleanField(default=True)
|
||||||
meta = {'abstract': True}
|
meta = {'abstract': True}
|
||||||
self.assertRaises(ValueError, create_bad_abstract)
|
|
||||||
|
|
||||||
def test_abstract_embedded_documents(self):
|
def test_abstract_embedded_documents(self):
|
||||||
# 789: EmbeddedDocument shouldn't inherit abstract
|
# 789: EmbeddedDocument shouldn't inherit abstract
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import bson
|
import bson
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
@@ -16,12 +13,12 @@ from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
|
|||||||
PickleDynamicEmbedded, PickleDynamicTest)
|
PickleDynamicEmbedded, PickleDynamicTest)
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
from mongoengine.base import get_document, _document_registry
|
||||||
|
from mongoengine.connection import get_db
|
||||||
from mongoengine.errors import (NotRegistered, InvalidDocumentError,
|
from mongoengine.errors import (NotRegistered, InvalidDocumentError,
|
||||||
InvalidQueryError, NotUniqueError,
|
InvalidQueryError, NotUniqueError,
|
||||||
FieldDoesNotExist, SaveConditionError)
|
FieldDoesNotExist, SaveConditionError)
|
||||||
from mongoengine.queryset import NULLIFY, Q
|
from mongoengine.queryset import NULLIFY, Q
|
||||||
from mongoengine.connection import get_db
|
|
||||||
from mongoengine.base import get_document
|
|
||||||
from mongoengine.context_managers import switch_db, query_counter
|
from mongoengine.context_managers import switch_db, query_counter
|
||||||
from mongoengine import signals
|
from mongoengine import signals
|
||||||
|
|
||||||
@@ -102,21 +99,18 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(options['size'], 4096)
|
self.assertEqual(options['size'], 4096)
|
||||||
|
|
||||||
# Check that the document cannot be redefined with different options
|
# Check that the document cannot be redefined with different options
|
||||||
def recreate_log_document():
|
class Log(Document):
|
||||||
class Log(Document):
|
date = DateTimeField(default=datetime.now)
|
||||||
date = DateTimeField(default=datetime.now)
|
meta = {
|
||||||
meta = {
|
'max_documents': 11,
|
||||||
'max_documents': 11,
|
}
|
||||||
}
|
|
||||||
# Create the collection by accessing Document.objects
|
|
||||||
Log.objects
|
|
||||||
self.assertRaises(InvalidCollectionError, recreate_log_document)
|
|
||||||
|
|
||||||
Log.drop_collection()
|
# Accessing Document.objects creates the collection
|
||||||
|
with self.assertRaises(InvalidCollectionError):
|
||||||
|
Log.objects
|
||||||
|
|
||||||
def test_capped_collection_default(self):
|
def test_capped_collection_default(self):
|
||||||
"""Ensure that capped collections defaults work properly.
|
"""Ensure that capped collections defaults work properly."""
|
||||||
"""
|
|
||||||
class Log(Document):
|
class Log(Document):
|
||||||
date = DateTimeField(default=datetime.now)
|
date = DateTimeField(default=datetime.now)
|
||||||
meta = {
|
meta = {
|
||||||
@@ -134,16 +128,14 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(options['size'], 10 * 2**20)
|
self.assertEqual(options['size'], 10 * 2**20)
|
||||||
|
|
||||||
# Check that the document with default value can be recreated
|
# Check that the document with default value can be recreated
|
||||||
def recreate_log_document():
|
class Log(Document):
|
||||||
class Log(Document):
|
date = DateTimeField(default=datetime.now)
|
||||||
date = DateTimeField(default=datetime.now)
|
meta = {
|
||||||
meta = {
|
'max_documents': 10,
|
||||||
'max_documents': 10,
|
}
|
||||||
}
|
|
||||||
# Create the collection by accessing Document.objects
|
# Create the collection by accessing Document.objects
|
||||||
Log.objects
|
Log.objects
|
||||||
recreate_log_document()
|
|
||||||
Log.drop_collection()
|
|
||||||
|
|
||||||
def test_capped_collection_no_max_size_problems(self):
|
def test_capped_collection_no_max_size_problems(self):
|
||||||
"""Ensure that capped collections with odd max_size work properly.
|
"""Ensure that capped collections with odd max_size work properly.
|
||||||
@@ -166,16 +158,14 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertTrue(options['size'] >= 10000)
|
self.assertTrue(options['size'] >= 10000)
|
||||||
|
|
||||||
# Check that the document with odd max_size value can be recreated
|
# Check that the document with odd max_size value can be recreated
|
||||||
def recreate_log_document():
|
class Log(Document):
|
||||||
class Log(Document):
|
date = DateTimeField(default=datetime.now)
|
||||||
date = DateTimeField(default=datetime.now)
|
meta = {
|
||||||
meta = {
|
'max_size': 10000,
|
||||||
'max_size': 10000,
|
}
|
||||||
}
|
|
||||||
# Create the collection by accessing Document.objects
|
# Create the collection by accessing Document.objects
|
||||||
Log.objects
|
Log.objects
|
||||||
recreate_log_document()
|
|
||||||
Log.drop_collection()
|
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
"""Ensure that unicode representation works
|
"""Ensure that unicode representation works
|
||||||
@@ -286,7 +276,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
list_stats = []
|
list_stats = []
|
||||||
|
|
||||||
for i in xrange(10):
|
for i in range(10):
|
||||||
s = Stats()
|
s = Stats()
|
||||||
s.save()
|
s.save()
|
||||||
list_stats.append(s)
|
list_stats.append(s)
|
||||||
@@ -356,14 +346,14 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(User._fields['username'].db_field, '_id')
|
self.assertEqual(User._fields['username'].db_field, '_id')
|
||||||
self.assertEqual(User._meta['id_field'], 'username')
|
self.assertEqual(User._meta['id_field'], 'username')
|
||||||
|
|
||||||
def create_invalid_user():
|
# test no primary key field
|
||||||
User(name='test').save() # no primary key field
|
self.assertRaises(ValidationError, User(name='test').save)
|
||||||
self.assertRaises(ValidationError, create_invalid_user)
|
|
||||||
|
|
||||||
def define_invalid_user():
|
# define a subclass with a different primary key field than the
|
||||||
|
# parent
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
class EmailUser(User):
|
class EmailUser(User):
|
||||||
email = StringField(primary_key=True)
|
email = StringField(primary_key=True)
|
||||||
self.assertRaises(ValueError, define_invalid_user)
|
|
||||||
|
|
||||||
class EmailUser(User):
|
class EmailUser(User):
|
||||||
email = StringField()
|
email = StringField()
|
||||||
@@ -411,12 +401,10 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Mimic Place and NicePlace definitions being in a different file
|
# Mimic Place and NicePlace definitions being in a different file
|
||||||
# and the NicePlace model not being imported in at query time.
|
# and the NicePlace model not being imported in at query time.
|
||||||
from mongoengine.base import _document_registry
|
|
||||||
del(_document_registry['Place.NicePlace'])
|
del(_document_registry['Place.NicePlace'])
|
||||||
|
|
||||||
def query_without_importing_nice_place():
|
with self.assertRaises(NotRegistered):
|
||||||
print Place.objects.all()
|
list(Place.objects.all())
|
||||||
self.assertRaises(NotRegistered, query_without_importing_nice_place)
|
|
||||||
|
|
||||||
def test_document_registry_regressions(self):
|
def test_document_registry_regressions(self):
|
||||||
|
|
||||||
@@ -447,6 +435,15 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
person.to_dbref()
|
person.to_dbref()
|
||||||
|
|
||||||
|
def test_save_abstract_document(self):
|
||||||
|
"""Saving an abstract document should fail."""
|
||||||
|
class Doc(Document):
|
||||||
|
name = StringField()
|
||||||
|
meta = {'abstract': True}
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidDocumentError):
|
||||||
|
Doc(name='aaa').save()
|
||||||
|
|
||||||
def test_reload(self):
|
def test_reload(self):
|
||||||
"""Ensure that attributes may be reloaded.
|
"""Ensure that attributes may be reloaded.
|
||||||
"""
|
"""
|
||||||
@@ -745,7 +742,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
t.save()
|
t.save()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
expect_msg = "Draft entries may not have a publication date."
|
expect_msg = "Draft entries may not have a publication date."
|
||||||
self.assertTrue(expect_msg in e.message)
|
self.assertTrue(expect_msg in e.message)
|
||||||
self.assertEqual(e.to_dict(), {'__all__': expect_msg})
|
self.assertEqual(e.to_dict(), {'__all__': expect_msg})
|
||||||
@@ -784,7 +781,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25, z=15))
|
t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25, z=15))
|
||||||
try:
|
try:
|
||||||
t.save()
|
t.save()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
expect_msg = "Value of z != x + y"
|
expect_msg = "Value of z != x + y"
|
||||||
self.assertTrue(expect_msg in e.message)
|
self.assertTrue(expect_msg in e.message)
|
||||||
self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}})
|
self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}})
|
||||||
@@ -798,8 +795,10 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_modify_empty(self):
|
def test_modify_empty(self):
|
||||||
doc = self.Person(name="bob", age=10).save()
|
doc = self.Person(name="bob", age=10).save()
|
||||||
self.assertRaises(
|
|
||||||
InvalidDocumentError, lambda: self.Person().modify(set__age=10))
|
with self.assertRaises(InvalidDocumentError):
|
||||||
|
self.Person().modify(set__age=10)
|
||||||
|
|
||||||
self.assertDbEqual([dict(doc.to_mongo())])
|
self.assertDbEqual([dict(doc.to_mongo())])
|
||||||
|
|
||||||
def test_modify_invalid_query(self):
|
def test_modify_invalid_query(self):
|
||||||
@@ -807,9 +806,8 @@ class InstanceTest(unittest.TestCase):
|
|||||||
doc2 = self.Person(name="jim", age=20).save()
|
doc2 = self.Person(name="jim", age=20).save()
|
||||||
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
|
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
|
||||||
|
|
||||||
self.assertRaises(
|
with self.assertRaises(InvalidQueryError):
|
||||||
InvalidQueryError,
|
doc1.modify({'id': doc2.id}, set__value=20)
|
||||||
lambda: doc1.modify(dict(id=doc2.id), set__value=20))
|
|
||||||
|
|
||||||
self.assertDbEqual(docs)
|
self.assertDbEqual(docs)
|
||||||
|
|
||||||
@@ -818,7 +816,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
doc2 = self.Person(name="jim", age=20).save()
|
doc2 = self.Person(name="jim", age=20).save()
|
||||||
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
|
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
|
||||||
|
|
||||||
assert not doc1.modify(dict(name=doc2.name), set__age=100)
|
assert not doc1.modify({'name': doc2.name}, set__age=100)
|
||||||
|
|
||||||
self.assertDbEqual(docs)
|
self.assertDbEqual(docs)
|
||||||
|
|
||||||
@@ -827,7 +825,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
doc2 = self.Person(id=ObjectId(), name="jim", age=20)
|
doc2 = self.Person(id=ObjectId(), name="jim", age=20)
|
||||||
docs = [dict(doc1.to_mongo())]
|
docs = [dict(doc1.to_mongo())]
|
||||||
|
|
||||||
assert not doc2.modify(dict(name=doc2.name), set__age=100)
|
assert not doc2.modify({'name': doc2.name}, set__age=100)
|
||||||
|
|
||||||
self.assertDbEqual(docs)
|
self.assertDbEqual(docs)
|
||||||
|
|
||||||
@@ -1234,6 +1232,19 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(person.name, None)
|
self.assertEqual(person.name, None)
|
||||||
self.assertEqual(person.age, None)
|
self.assertEqual(person.age, None)
|
||||||
|
|
||||||
|
def test_update_rename_operator(self):
|
||||||
|
"""Test the $rename operator."""
|
||||||
|
coll = self.Person._get_collection()
|
||||||
|
doc = self.Person(name='John').save()
|
||||||
|
raw_doc = coll.find_one({'_id': doc.pk})
|
||||||
|
self.assertEqual(set(raw_doc.keys()), set(['_id', '_cls', 'name']))
|
||||||
|
|
||||||
|
doc.update(rename__name='first_name')
|
||||||
|
raw_doc = coll.find_one({'_id': doc.pk})
|
||||||
|
self.assertEqual(set(raw_doc.keys()),
|
||||||
|
set(['_id', '_cls', 'first_name']))
|
||||||
|
self.assertEqual(raw_doc['first_name'], 'John')
|
||||||
|
|
||||||
def test_inserts_if_you_set_the_pk(self):
|
def test_inserts_if_you_set_the_pk(self):
|
||||||
p1 = self.Person(name='p1', id=bson.ObjectId()).save()
|
p1 = self.Person(name='p1', id=bson.ObjectId()).save()
|
||||||
p2 = self.Person(name='p2')
|
p2 = self.Person(name='p2')
|
||||||
@@ -1293,12 +1304,11 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_document_update(self):
|
def test_document_update(self):
|
||||||
|
|
||||||
def update_not_saved_raises():
|
# try updating a non-saved document
|
||||||
|
with self.assertRaises(OperationError):
|
||||||
person = self.Person(name='dcrosta')
|
person = self.Person(name='dcrosta')
|
||||||
person.update(set__name='Dan Crosta')
|
person.update(set__name='Dan Crosta')
|
||||||
|
|
||||||
self.assertRaises(OperationError, update_not_saved_raises)
|
|
||||||
|
|
||||||
author = self.Person(name='dcrosta')
|
author = self.Person(name='dcrosta')
|
||||||
author.save()
|
author.save()
|
||||||
|
|
||||||
@@ -1308,19 +1318,17 @@ class InstanceTest(unittest.TestCase):
|
|||||||
p1 = self.Person.objects.first()
|
p1 = self.Person.objects.first()
|
||||||
self.assertEqual(p1.name, author.name)
|
self.assertEqual(p1.name, author.name)
|
||||||
|
|
||||||
def update_no_value_raises():
|
# try sending an empty update
|
||||||
|
with self.assertRaises(OperationError):
|
||||||
person = self.Person.objects.first()
|
person = self.Person.objects.first()
|
||||||
person.update()
|
person.update()
|
||||||
|
|
||||||
self.assertRaises(OperationError, update_no_value_raises)
|
# update that doesn't explicitly specify an operator should default
|
||||||
|
# to 'set__'
|
||||||
def update_no_op_should_default_to_set():
|
person = self.Person.objects.first()
|
||||||
person = self.Person.objects.first()
|
person.update(name="Dan")
|
||||||
person.update(name="Dan")
|
person.reload()
|
||||||
person.reload()
|
self.assertEqual("Dan", person.name)
|
||||||
return person.name
|
|
||||||
|
|
||||||
self.assertEqual("Dan", update_no_op_should_default_to_set())
|
|
||||||
|
|
||||||
def test_update_unique_field(self):
|
def test_update_unique_field(self):
|
||||||
class Doc(Document):
|
class Doc(Document):
|
||||||
@@ -1329,8 +1337,8 @@ class InstanceTest(unittest.TestCase):
|
|||||||
doc1 = Doc(name="first").save()
|
doc1 = Doc(name="first").save()
|
||||||
doc2 = Doc(name="second").save()
|
doc2 = Doc(name="second").save()
|
||||||
|
|
||||||
self.assertRaises(NotUniqueError, lambda:
|
with self.assertRaises(NotUniqueError):
|
||||||
doc2.update(set__name=doc1.name))
|
doc2.update(set__name=doc1.name)
|
||||||
|
|
||||||
def test_embedded_update(self):
|
def test_embedded_update(self):
|
||||||
"""
|
"""
|
||||||
@@ -1848,15 +1856,13 @@ class InstanceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_duplicate_db_fields_raise_invalid_document_error(self):
|
def test_duplicate_db_fields_raise_invalid_document_error(self):
|
||||||
"""Ensure a InvalidDocumentError is thrown if duplicate fields
|
"""Ensure a InvalidDocumentError is thrown if duplicate fields
|
||||||
declare the same db_field"""
|
declare the same db_field.
|
||||||
|
"""
|
||||||
def throw_invalid_document_error():
|
with self.assertRaises(InvalidDocumentError):
|
||||||
class Foo(Document):
|
class Foo(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
name2 = StringField(db_field='name')
|
name2 = StringField(db_field='name')
|
||||||
|
|
||||||
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
|
|
||||||
|
|
||||||
def test_invalid_son(self):
|
def test_invalid_son(self):
|
||||||
"""Raise an error if loading invalid data"""
|
"""Raise an error if loading invalid data"""
|
||||||
class Occurrence(EmbeddedDocument):
|
class Occurrence(EmbeddedDocument):
|
||||||
@@ -1868,11 +1874,17 @@ class InstanceTest(unittest.TestCase):
|
|||||||
forms = ListField(StringField(), default=list)
|
forms = ListField(StringField(), default=list)
|
||||||
occurs = ListField(EmbeddedDocumentField(Occurrence), default=list)
|
occurs = ListField(EmbeddedDocumentField(Occurrence), default=list)
|
||||||
|
|
||||||
def raise_invalid_document():
|
with self.assertRaises(InvalidDocumentError):
|
||||||
Word._from_son({'stem': [1, 2, 3], 'forms': 1, 'count': 'one',
|
Word._from_son({
|
||||||
'occurs': {"hello": None}})
|
'stem': [1, 2, 3],
|
||||||
|
'forms': 1,
|
||||||
|
'count': 'one',
|
||||||
|
'occurs': {"hello": None}
|
||||||
|
})
|
||||||
|
|
||||||
self.assertRaises(InvalidDocumentError, raise_invalid_document)
|
# Tests for issue #1438: https://github.com/MongoEngine/mongoengine/issues/1438
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Word._from_son('this is not a valid SON dict')
|
||||||
|
|
||||||
def test_reverse_delete_rule_cascade_and_nullify(self):
|
def test_reverse_delete_rule_cascade_and_nullify(self):
|
||||||
"""Ensure that a referenced document is also deleted upon deletion.
|
"""Ensure that a referenced document is also deleted upon deletion.
|
||||||
@@ -2103,8 +2115,7 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(Bar.objects.get().foo, None)
|
self.assertEqual(Bar.objects.get().foo, None)
|
||||||
|
|
||||||
def test_invalid_reverse_delete_rule_raise_errors(self):
|
def test_invalid_reverse_delete_rule_raise_errors(self):
|
||||||
|
with self.assertRaises(InvalidDocumentError):
|
||||||
def throw_invalid_document_error():
|
|
||||||
class Blog(Document):
|
class Blog(Document):
|
||||||
content = StringField()
|
content = StringField()
|
||||||
authors = MapField(ReferenceField(
|
authors = MapField(ReferenceField(
|
||||||
@@ -2114,21 +2125,15 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.Person,
|
self.Person,
|
||||||
reverse_delete_rule=NULLIFY))
|
reverse_delete_rule=NULLIFY))
|
||||||
|
|
||||||
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
|
with self.assertRaises(InvalidDocumentError):
|
||||||
|
|
||||||
def throw_invalid_document_error_embedded():
|
|
||||||
class Parents(EmbeddedDocument):
|
class Parents(EmbeddedDocument):
|
||||||
father = ReferenceField('Person', reverse_delete_rule=DENY)
|
father = ReferenceField('Person', reverse_delete_rule=DENY)
|
||||||
mother = ReferenceField('Person', reverse_delete_rule=DENY)
|
mother = ReferenceField('Person', reverse_delete_rule=DENY)
|
||||||
|
|
||||||
self.assertRaises(
|
|
||||||
InvalidDocumentError, throw_invalid_document_error_embedded)
|
|
||||||
|
|
||||||
def test_reverse_delete_rule_cascade_recurs(self):
|
def test_reverse_delete_rule_cascade_recurs(self):
|
||||||
"""Ensure that a chain of documents is also deleted upon cascaded
|
"""Ensure that a chain of documents is also deleted upon cascaded
|
||||||
deletion.
|
deletion.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class BlogPost(Document):
|
class BlogPost(Document):
|
||||||
content = StringField()
|
content = StringField()
|
||||||
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
|
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
|
||||||
@@ -2344,15 +2349,14 @@ class InstanceTest(unittest.TestCase):
|
|||||||
pickle_doc.save()
|
pickle_doc.save()
|
||||||
pickle_doc.delete()
|
pickle_doc.delete()
|
||||||
|
|
||||||
def test_throw_invalid_document_error(self):
|
def test_override_method_with_field(self):
|
||||||
|
"""Test creating a field with a field name that would override
|
||||||
# test handles people trying to upsert
|
the "validate" method.
|
||||||
def throw_invalid_document_error():
|
"""
|
||||||
|
with self.assertRaises(InvalidDocumentError):
|
||||||
class Blog(Document):
|
class Blog(Document):
|
||||||
validate = DictField()
|
validate = DictField()
|
||||||
|
|
||||||
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
|
|
||||||
|
|
||||||
def test_mutating_documents(self):
|
def test_mutating_documents(self):
|
||||||
|
|
||||||
class B(EmbeddedDocument):
|
class B(EmbeddedDocument):
|
||||||
@@ -2815,11 +2819,10 @@ class InstanceTest(unittest.TestCase):
|
|||||||
log.log = "Saving"
|
log.log = "Saving"
|
||||||
log.save()
|
log.save()
|
||||||
|
|
||||||
def change_shard_key():
|
# try to change the shard key
|
||||||
|
with self.assertRaises(OperationError):
|
||||||
log.machine = "127.0.0.1"
|
log.machine = "127.0.0.1"
|
||||||
|
|
||||||
self.assertRaises(OperationError, change_shard_key)
|
|
||||||
|
|
||||||
def test_shard_key_in_embedded_document(self):
|
def test_shard_key_in_embedded_document(self):
|
||||||
class Foo(EmbeddedDocument):
|
class Foo(EmbeddedDocument):
|
||||||
foo = StringField()
|
foo = StringField()
|
||||||
@@ -2840,12 +2843,11 @@ class InstanceTest(unittest.TestCase):
|
|||||||
bar_doc.bar = 'baz'
|
bar_doc.bar = 'baz'
|
||||||
bar_doc.save()
|
bar_doc.save()
|
||||||
|
|
||||||
def change_shard_key():
|
# try to change the shard key
|
||||||
|
with self.assertRaises(OperationError):
|
||||||
bar_doc.foo.foo = 'something'
|
bar_doc.foo.foo = 'something'
|
||||||
bar_doc.save()
|
bar_doc.save()
|
||||||
|
|
||||||
self.assertRaises(OperationError, change_shard_key)
|
|
||||||
|
|
||||||
def test_shard_key_primary(self):
|
def test_shard_key_primary(self):
|
||||||
class LogEntry(Document):
|
class LogEntry(Document):
|
||||||
machine = StringField(primary_key=True)
|
machine = StringField(primary_key=True)
|
||||||
@@ -2866,11 +2868,10 @@ class InstanceTest(unittest.TestCase):
|
|||||||
log.log = "Saving"
|
log.log = "Saving"
|
||||||
log.save()
|
log.save()
|
||||||
|
|
||||||
def change_shard_key():
|
# try to change the shard key
|
||||||
|
with self.assertRaises(OperationError):
|
||||||
log.machine = "127.0.0.1"
|
log.machine = "127.0.0.1"
|
||||||
|
|
||||||
self.assertRaises(OperationError, change_shard_key)
|
|
||||||
|
|
||||||
def test_kwargs_simple(self):
|
def test_kwargs_simple(self):
|
||||||
|
|
||||||
class Embedded(EmbeddedDocument):
|
class Embedded(EmbeddedDocument):
|
||||||
@@ -2955,11 +2956,9 @@ class InstanceTest(unittest.TestCase):
|
|||||||
def test_bad_mixed_creation(self):
|
def test_bad_mixed_creation(self):
|
||||||
"""Ensure that document gives correct error when duplicating arguments
|
"""Ensure that document gives correct error when duplicating arguments
|
||||||
"""
|
"""
|
||||||
def construct_bad_instance():
|
with self.assertRaises(TypeError):
|
||||||
return self.Person("Test User", 42, name="Bad User")
|
return self.Person("Test User", 42, name="Bad User")
|
||||||
|
|
||||||
self.assertRaises(TypeError, construct_bad_instance)
|
|
||||||
|
|
||||||
def test_data_contains_id_field(self):
|
def test_data_contains_id_field(self):
|
||||||
"""Ensure that asking for _data returns 'id'
|
"""Ensure that asking for _data returns 'id'
|
||||||
"""
|
"""
|
||||||
@@ -3118,17 +3117,17 @@ class InstanceTest(unittest.TestCase):
|
|||||||
p4 = Person.objects()[0]
|
p4 = Person.objects()[0]
|
||||||
p4.save()
|
p4.save()
|
||||||
self.assertEquals(p4.height, 189)
|
self.assertEquals(p4.height, 189)
|
||||||
|
|
||||||
# However the default will not be fixed in DB
|
# However the default will not be fixed in DB
|
||||||
self.assertEquals(Person.objects(height=189).count(), 0)
|
self.assertEquals(Person.objects(height=189).count(), 0)
|
||||||
|
|
||||||
# alter DB for the new default
|
# alter DB for the new default
|
||||||
coll = Person._get_collection()
|
coll = Person._get_collection()
|
||||||
for person in Person.objects.as_pymongo():
|
for person in Person.objects.as_pymongo():
|
||||||
if 'height' not in person:
|
if 'height' not in person:
|
||||||
person['height'] = 189
|
person['height'] = 189
|
||||||
coll.save(person)
|
coll.save(person)
|
||||||
|
|
||||||
self.assertEquals(Person.objects(height=189).count(), 1)
|
self.assertEquals(Person.objects(height=189).count(), 1)
|
||||||
|
|
||||||
def test_from_son(self):
|
def test_from_son(self):
|
||||||
@@ -3202,5 +3201,20 @@ class InstanceTest(unittest.TestCase):
|
|||||||
self.assertEqual(b._instance, a)
|
self.assertEqual(b._instance, a)
|
||||||
self.assertEqual(idx, 2)
|
self.assertEqual(idx, 2)
|
||||||
|
|
||||||
|
def test_falsey_pk(self):
|
||||||
|
"""Ensure that we can create and update a document with Falsey PK.
|
||||||
|
"""
|
||||||
|
class Person(Document):
|
||||||
|
age = IntField(primary_key=True)
|
||||||
|
height = FloatField()
|
||||||
|
|
||||||
|
person = Person()
|
||||||
|
person.age = 0
|
||||||
|
person.height = 1.89
|
||||||
|
person.save()
|
||||||
|
|
||||||
|
person.update(set__height=2.0)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -60,7 +57,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
User().validate()
|
User().validate()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
self.assertTrue("User:None" in e.message)
|
self.assertTrue("User:None" in e.message)
|
||||||
self.assertEqual(e.to_dict(), {
|
self.assertEqual(e.to_dict(), {
|
||||||
'username': 'Field is required',
|
'username': 'Field is required',
|
||||||
@@ -70,7 +67,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
user.name = None
|
user.name = None
|
||||||
try:
|
try:
|
||||||
user.save()
|
user.save()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
self.assertTrue("User:RossC0" in e.message)
|
self.assertTrue("User:RossC0" in e.message)
|
||||||
self.assertEqual(e.to_dict(), {
|
self.assertEqual(e.to_dict(), {
|
||||||
'name': 'Field is required'})
|
'name': 'Field is required'})
|
||||||
@@ -118,7 +115,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Doc(id="bad").validate()
|
Doc(id="bad").validate()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
self.assertTrue("SubDoc:None" in e.message)
|
self.assertTrue("SubDoc:None" in e.message)
|
||||||
self.assertEqual(e.to_dict(), {
|
self.assertEqual(e.to_dict(), {
|
||||||
"e": {'val': 'OK could not be converted to int'}})
|
"e": {'val': 'OK could not be converted to int'}})
|
||||||
@@ -136,7 +133,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
doc.e.val = "OK"
|
doc.e.val = "OK"
|
||||||
try:
|
try:
|
||||||
doc.save()
|
doc.save()
|
||||||
except ValidationError, e:
|
except ValidationError as e:
|
||||||
self.assertTrue("Doc:test" in e.message)
|
self.assertTrue("Doc:test" in e.message)
|
||||||
self.assertEqual(e.to_dict(), {
|
self.assertEqual(e.to_dict(), {
|
||||||
"e": {'val': 'OK could not be converted to int'}})
|
"e": {'val': 'OK could not be converted to int'}})
|
||||||
@@ -156,14 +153,14 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
s = SubDoc()
|
s = SubDoc()
|
||||||
|
|
||||||
self.assertRaises(ValidationError, lambda: s.validate())
|
self.assertRaises(ValidationError, s.validate)
|
||||||
|
|
||||||
d1.e = s
|
d1.e = s
|
||||||
d2.e = s
|
d2.e = s
|
||||||
|
|
||||||
del d1
|
del d1
|
||||||
|
|
||||||
self.assertRaises(ValidationError, lambda: d2.validate())
|
self.assertRaises(ValidationError, d2.validate)
|
||||||
|
|
||||||
def test_parent_reference_in_child_document(self):
|
def test_parent_reference_in_child_document(self):
|
||||||
"""
|
"""
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,16 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import gridfs
|
import gridfs
|
||||||
|
import six
|
||||||
|
|
||||||
from nose.plugins.skip import SkipTest
|
from nose.plugins.skip import SkipTest
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import get_db
|
from mongoengine.connection import get_db
|
||||||
from mongoengine.python_support import b, StringIO
|
from mongoengine.python_support import StringIO
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -20,15 +18,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAS_PIL = False
|
HAS_PIL = False
|
||||||
|
|
||||||
|
from tests.utils import MongoDBTestCase
|
||||||
|
|
||||||
TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__), 'mongoengine.png')
|
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')
|
TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), 'mongodb_leaf.png')
|
||||||
|
|
||||||
|
|
||||||
class FileTest(unittest.TestCase):
|
class FileTest(MongoDBTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
connect(db='mongoenginetest')
|
|
||||||
self.db = get_db()
|
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.db.drop_collection('fs.files')
|
self.db.drop_collection('fs.files')
|
||||||
@@ -49,7 +45,7 @@ class FileTest(unittest.TestCase):
|
|||||||
|
|
||||||
PutFile.drop_collection()
|
PutFile.drop_collection()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
putfile = PutFile()
|
putfile = PutFile()
|
||||||
@@ -88,8 +84,8 @@ class FileTest(unittest.TestCase):
|
|||||||
|
|
||||||
StreamFile.drop_collection()
|
StreamFile.drop_collection()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
more_text = b('Foo Bar')
|
more_text = six.b('Foo Bar')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
streamfile = StreamFile()
|
streamfile = StreamFile()
|
||||||
@@ -123,8 +119,8 @@ class FileTest(unittest.TestCase):
|
|||||||
|
|
||||||
StreamFile.drop_collection()
|
StreamFile.drop_collection()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
more_text = b('Foo Bar')
|
more_text = six.b('Foo Bar')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
streamfile = StreamFile()
|
streamfile = StreamFile()
|
||||||
@@ -155,8 +151,8 @@ class FileTest(unittest.TestCase):
|
|||||||
class SetFile(Document):
|
class SetFile(Document):
|
||||||
the_file = FileField()
|
the_file = FileField()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
more_text = b('Foo Bar')
|
more_text = six.b('Foo Bar')
|
||||||
|
|
||||||
SetFile.drop_collection()
|
SetFile.drop_collection()
|
||||||
|
|
||||||
@@ -185,7 +181,7 @@ class FileTest(unittest.TestCase):
|
|||||||
GridDocument.drop_collection()
|
GridDocument.drop_collection()
|
||||||
|
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
f.write(b("Hello World!"))
|
f.write(six.b("Hello World!"))
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
# Test without default
|
# 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)
|
self.assertEqual(doc_b.the_file.grid_id, doc_c.the_file.grid_id)
|
||||||
|
|
||||||
# Test with default
|
# Test with default
|
||||||
doc_d = GridDocument(the_file=b(''))
|
doc_d = GridDocument(the_file=six.b(''))
|
||||||
doc_d.save()
|
doc_d.save()
|
||||||
|
|
||||||
doc_e = GridDocument.objects.with_id(doc_d.id)
|
doc_e = GridDocument.objects.with_id(doc_d.id)
|
||||||
@@ -228,7 +224,7 @@ class FileTest(unittest.TestCase):
|
|||||||
# First instance
|
# First instance
|
||||||
test_file = TestFile()
|
test_file = TestFile()
|
||||||
test_file.name = "Hello, World!"
|
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()
|
test_file.save()
|
||||||
|
|
||||||
# Second instance
|
# Second instance
|
||||||
@@ -282,7 +278,7 @@ class FileTest(unittest.TestCase):
|
|||||||
|
|
||||||
test_file = TestFile()
|
test_file = TestFile()
|
||||||
self.assertFalse(bool(test_file.the_file))
|
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()
|
test_file.save()
|
||||||
self.assertTrue(bool(test_file.the_file))
|
self.assertTrue(bool(test_file.the_file))
|
||||||
|
|
||||||
@@ -297,66 +293,66 @@ class FileTest(unittest.TestCase):
|
|||||||
test_file = TestFile()
|
test_file = TestFile()
|
||||||
self.assertFalse(test_file.the_file in [{"test": 1}])
|
self.assertFalse(test_file.the_file in [{"test": 1}])
|
||||||
|
|
||||||
def test_file_disk_space(self):
|
def test_file_disk_space(self):
|
||||||
""" Test disk space usage when we delete/replace a file """
|
""" Test disk space usage when we delete/replace a file """
|
||||||
class TestFile(Document):
|
class TestFile(Document):
|
||||||
the_file = FileField()
|
the_file = FileField()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
testfile = TestFile()
|
testfile = TestFile()
|
||||||
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||||
testfile.save()
|
testfile.save()
|
||||||
|
|
||||||
# Now check fs.files and fs.chunks
|
# Now check fs.files and fs.chunks
|
||||||
db = TestFile._get_db()
|
db = TestFile._get_db()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 1)
|
self.assertEquals(len(list(files)), 1)
|
||||||
self.assertEquals(len(list(chunks)), 1)
|
self.assertEquals(len(list(chunks)), 1)
|
||||||
|
|
||||||
# Deleting the docoument should delete the files
|
# Deleting the docoument should delete the files
|
||||||
testfile.delete()
|
testfile.delete()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 0)
|
self.assertEquals(len(list(files)), 0)
|
||||||
self.assertEquals(len(list(chunks)), 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 = TestFile()
|
||||||
testfile.save()
|
testfile.save()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 0)
|
self.assertEquals(len(list(files)), 0)
|
||||||
self.assertEquals(len(list(chunks)), 0)
|
self.assertEquals(len(list(chunks)), 0)
|
||||||
|
|
||||||
testfile.delete()
|
testfile.delete()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 0)
|
self.assertEquals(len(list(files)), 0)
|
||||||
self.assertEquals(len(list(chunks)), 0)
|
self.assertEquals(len(list(chunks)), 0)
|
||||||
|
|
||||||
# Test case where we overwrite the file
|
# Test case where we overwrite the file
|
||||||
testfile = TestFile()
|
testfile = TestFile()
|
||||||
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||||
testfile.save()
|
testfile.save()
|
||||||
|
|
||||||
text = b('Bonjour, World!')
|
text = six.b('Bonjour, World!')
|
||||||
testfile.the_file.replace(text, content_type=content_type, filename="hello")
|
testfile.the_file.replace(text, content_type=content_type, filename="hello")
|
||||||
testfile.save()
|
testfile.save()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 1)
|
self.assertEquals(len(list(files)), 1)
|
||||||
self.assertEquals(len(list(chunks)), 1)
|
self.assertEquals(len(list(chunks)), 1)
|
||||||
|
|
||||||
testfile.delete()
|
testfile.delete()
|
||||||
|
|
||||||
files = db.fs.files.find()
|
files = db.fs.files.find()
|
||||||
chunks = db.fs.chunks.find()
|
chunks = db.fs.chunks.find()
|
||||||
self.assertEquals(len(list(files)), 0)
|
self.assertEquals(len(list(files)), 0)
|
||||||
@@ -372,14 +368,14 @@ class FileTest(unittest.TestCase):
|
|||||||
TestImage.drop_collection()
|
TestImage.drop_collection()
|
||||||
|
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
f.write(b("Hello World!"))
|
f.write(six.b("Hello World!"))
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
t = TestImage()
|
t = TestImage()
|
||||||
try:
|
try:
|
||||||
t.image.put(f)
|
t.image.put(f)
|
||||||
self.fail("Should have raised an invalidation error")
|
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)
|
self.assertEqual("%s" % e, "Invalid image: cannot identify image file %s" % f)
|
||||||
|
|
||||||
t = TestImage()
|
t = TestImage()
|
||||||
@@ -496,7 +492,7 @@ class FileTest(unittest.TestCase):
|
|||||||
# First instance
|
# First instance
|
||||||
test_file = TestFile()
|
test_file = TestFile()
|
||||||
test_file.name = "Hello, World!"
|
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")
|
name="hello.txt")
|
||||||
test_file.save()
|
test_file.save()
|
||||||
|
|
||||||
@@ -504,16 +500,15 @@ class FileTest(unittest.TestCase):
|
|||||||
self.assertEqual(data.get('name'), 'hello.txt')
|
self.assertEqual(data.get('name'), 'hello.txt')
|
||||||
|
|
||||||
test_file = TestFile.objects.first()
|
test_file = TestFile.objects.first()
|
||||||
self.assertEqual(test_file.the_file.read(),
|
self.assertEqual(test_file.the_file.read(), six.b('Hello, World!'))
|
||||||
b('Hello, World!'))
|
|
||||||
|
|
||||||
test_file = TestFile.objects.first()
|
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.save()
|
||||||
|
|
||||||
test_file = TestFile.objects.first()
|
test_file = TestFile.objects.first()
|
||||||
self.assertEqual(test_file.the_file.read(),
|
self.assertEqual(test_file.the_file.read(),
|
||||||
b('HELLO, WORLD!'))
|
six.b('HELLO, WORLD!'))
|
||||||
|
|
||||||
def test_copyable(self):
|
def test_copyable(self):
|
||||||
class PutFile(Document):
|
class PutFile(Document):
|
||||||
@@ -521,7 +516,7 @@ class FileTest(unittest.TestCase):
|
|||||||
|
|
||||||
PutFile.drop_collection()
|
PutFile.drop_collection()
|
||||||
|
|
||||||
text = b('Hello, World!')
|
text = six.b('Hello, World!')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
putfile = PutFile()
|
putfile = PutFile()
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
@@ -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()
|
|
@@ -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()
|
|
@@ -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']]))
|
|
@@ -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'])
|
|
@@ -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()
|
|
@@ -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']]))
|
|
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@@ -95,7 +92,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
exclude = ['d', 'e']
|
exclude = ['d', 'e']
|
||||||
only = ['b', 'c']
|
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(),
|
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||||
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
|
{'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
|
||||||
qs = qs.only(*only)
|
qs = qs.only(*only)
|
||||||
@@ -103,14 +100,14 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
qs = qs.exclude(*exclude)
|
qs = qs.exclude(*exclude)
|
||||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
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)
|
qs = qs.exclude(*exclude)
|
||||||
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
||||||
qs = qs.only(*only)
|
qs = qs.only(*only)
|
||||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||||
|
|
||||||
qs = MyDoc.objects.exclude(*exclude)
|
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})
|
self.assertEqual(qs._loaded_fields.as_dict(), {'a': 1, 'b': 1, 'c': 1})
|
||||||
qs = qs.only(*only)
|
qs = qs.only(*only)
|
||||||
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
self.assertEqual(qs._loaded_fields.as_dict(), {'b': 1, 'c': 1})
|
||||||
@@ -129,7 +126,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
exclude = ['d', 'e']
|
exclude = ['d', 'e']
|
||||||
only = ['b', 'c']
|
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.exclude(*exclude)
|
||||||
qs = qs.only(*only)
|
qs = qs.only(*only)
|
||||||
qs = qs.fields(slice__b=5)
|
qs = qs.fields(slice__b=5)
|
||||||
@@ -144,6 +141,16 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
self.assertEqual(qs._loaded_fields.as_dict(),
|
self.assertEqual(qs._loaded_fields.as_dict(),
|
||||||
{'b': {'$slice': 5}})
|
{'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):
|
def test_only(self):
|
||||||
"""Ensure that QuerySet.only only returns the requested fields.
|
"""Ensure that QuerySet.only only returns the requested fields.
|
||||||
"""
|
"""
|
||||||
|
@@ -1,109 +1,139 @@
|
|||||||
import sys
|
import datetime
|
||||||
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from pymongo.errors import OperationFailure
|
|
||||||
from mongoengine import *
|
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",)
|
__all__ = ("GeoQueriesTest",)
|
||||||
|
|
||||||
|
|
||||||
class GeoQueriesTest(unittest.TestCase):
|
class GeoQueriesTest(MongoDBTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def _create_event_data(self, point_field_class=GeoPointField):
|
||||||
connect(db='mongoenginetest')
|
"""Create some sample data re-used in many of the tests below."""
|
||||||
|
|
||||||
def test_geospatial_operators(self):
|
|
||||||
"""Ensure that geospatial queries are working.
|
|
||||||
"""
|
|
||||||
class Event(Document):
|
class Event(Document):
|
||||||
title = StringField()
|
title = StringField()
|
||||||
date = DateTimeField()
|
date = DateTimeField()
|
||||||
location = GeoPointField()
|
location = point_field_class()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
self.Event = Event
|
||||||
|
|
||||||
Event.drop_collection()
|
Event.drop_collection()
|
||||||
|
|
||||||
event1 = Event(title="Coltrane Motion @ Double Door",
|
event1 = Event.objects.create(
|
||||||
date=datetime.now() - timedelta(days=1),
|
title="Coltrane Motion @ Double Door",
|
||||||
location=[-87.677137, 41.909889]).save()
|
date=datetime.datetime.now() - datetime.timedelta(days=1),
|
||||||
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
|
location=[-87.677137, 41.909889])
|
||||||
date=datetime.now() - timedelta(days=10),
|
event2 = Event.objects.create(
|
||||||
location=[-122.4194155, 37.7749295]).save()
|
title="Coltrane Motion @ Bottom of the Hill",
|
||||||
event3 = Event(title="Coltrane Motion @ Empty Bottle",
|
date=datetime.datetime.now() - datetime.timedelta(days=10),
|
||||||
date=datetime.now(),
|
location=[-122.4194155, 37.7749295])
|
||||||
location=[-87.686638, 41.900474]).save()
|
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.
|
# find all events "near" pitchfork office, chicago.
|
||||||
# note that "near" will show the san francisco event, too,
|
# note that "near" will show the san francisco event, too,
|
||||||
# although it sorts to last.
|
# 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(events.count(), 3)
|
||||||
self.assertEqual(list(events), [event1, event3, event2])
|
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
|
# find events within 5 degrees of pitchfork office, chicago
|
||||||
point_and_distance = [[-87.67892, 41.9120459], 5]
|
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)
|
self.assertEqual(events.count(), 2)
|
||||||
events = list(events)
|
events = list(events)
|
||||||
self.assertTrue(event2 not in events)
|
self.assertTrue(event2 not in events)
|
||||||
self.assertTrue(event1 in events)
|
self.assertTrue(event1 in events)
|
||||||
self.assertTrue(event3 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
|
# find events within 10 degrees of san francisco
|
||||||
point_and_distance = [[-122.415579, 37.7566023], 10]
|
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.count(), 1)
|
||||||
self.assertEqual(events[0], event2)
|
self.assertEqual(events[0], event2)
|
||||||
|
|
||||||
# find events within 1 degree of greenpoint, broolyn, nyc, ny
|
# find events within 1 degree of greenpoint, broolyn, nyc, ny
|
||||||
point_and_distance = [[-73.9509714, 40.7237134], 1]
|
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)
|
self.assertEqual(events.count(), 0)
|
||||||
|
|
||||||
# ensure ordering is respected by "within_distance"
|
# ensure ordering is respected by "within_distance"
|
||||||
point_and_distance = [[-87.67892, 41.9120459], 10]
|
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")
|
events = events.order_by("-date")
|
||||||
self.assertEqual(events.count(), 2)
|
self.assertEqual(events.count(), 2)
|
||||||
self.assertEqual(events[0], event3)
|
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
|
# check that within_box works
|
||||||
box = [(-125.0, 35.0), (-100.0, 40.0)]
|
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.count(), 1)
|
||||||
self.assertEqual(events[0].id, event2.id)
|
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 = [
|
polygon = [
|
||||||
(-87.694445, 41.912114),
|
(-87.694445, 41.912114),
|
||||||
(-87.69084, 41.919395),
|
(-87.69084, 41.919395),
|
||||||
@@ -111,7 +141,7 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
(-87.654276, 41.911731),
|
(-87.654276, 41.911731),
|
||||||
(-87.656164, 41.898061),
|
(-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.count(), 1)
|
||||||
self.assertEqual(events[0].id, event1.id)
|
self.assertEqual(events[0].id, event1.id)
|
||||||
|
|
||||||
@@ -120,13 +150,151 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
(-1.225891, 52.792797),
|
(-1.225891, 52.792797),
|
||||||
(-4.40094, 53.389881)
|
(-4.40094, 53.389881)
|
||||||
]
|
]
|
||||||
events = Event.objects(location__within_polygon=polygon2)
|
events = self.Event.objects(location__within_polygon=polygon2)
|
||||||
self.assertEqual(events.count(), 0)
|
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):
|
class Venue(EmbeddedDocument):
|
||||||
location = GeoPointField()
|
location = point_field_class()
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
class Event(Document):
|
class Event(Document):
|
||||||
@@ -152,16 +320,18 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
self.assertEqual(events.count(), 3)
|
self.assertEqual(events.count(), 3)
|
||||||
self.assertEqual(list(events), [event1, event3, event2])
|
self.assertEqual(list(events), [event1, event3, event2])
|
||||||
|
|
||||||
def test_spherical_geospatial_operators(self):
|
def test_geo_spatial_embedded(self):
|
||||||
"""Ensure that spherical geospatial queries are working
|
"""Make sure GeoPointField works properly in an embedded document."""
|
||||||
"""
|
self._test_embedded(point_field_class=GeoPointField)
|
||||||
# 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_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):
|
class Point(Document):
|
||||||
location = GeoPointField()
|
location = GeoPointField()
|
||||||
|
|
||||||
@@ -181,7 +351,10 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Same behavior for _within_spherical_distance
|
# Same behavior for _within_spherical_distance
|
||||||
points = Point.objects(
|
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)
|
self.assertEqual(points.count(), 2)
|
||||||
|
|
||||||
@@ -198,14 +371,9 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
# Test query works with min_distance, being farer from one point
|
# Test query works with min_distance, being farer from one point
|
||||||
points = Point.objects(location__near_sphere=[-122, 37.8],
|
points = Point.objects(location__near_sphere=[-122, 37.8],
|
||||||
location__min_distance=60 / earth_radius)
|
location__min_distance=60 / earth_radius)
|
||||||
# The following real test passes on MongoDB 3 but minDistance seems
|
self.assertEqual(points.count(), 1)
|
||||||
# buggy on older MongoDB versions
|
far_point = points.first()
|
||||||
if get_connection().server_info()['versionArray'][0] > 2:
|
self.assertNotEqual(close_point, far_point)
|
||||||
self.assertEqual(points.count(), 1)
|
|
||||||
far_point = points.first()
|
|
||||||
self.assertNotEqual(close_point, far_point)
|
|
||||||
else:
|
|
||||||
self.assertTrue(points.count() >= 1)
|
|
||||||
|
|
||||||
# Finds both points, but orders the north point first because it's
|
# Finds both points, but orders the north point first because it's
|
||||||
# closer to the reference point to the north.
|
# 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
|
# Finds only one point because only the first point is within 60km of
|
||||||
# the reference point to the south.
|
# the reference point to the south.
|
||||||
points = Point.objects(
|
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.count(), 1)
|
||||||
self.assertEqual(points[0].id, south_point.id)
|
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):
|
def test_linestring(self):
|
||||||
|
|
||||||
class Road(Document):
|
class Road(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
line = LineStringField()
|
line = LineStringField()
|
||||||
@@ -414,7 +456,6 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
self.assertEqual(1, roads)
|
self.assertEqual(1, roads)
|
||||||
|
|
||||||
def test_polygon(self):
|
def test_polygon(self):
|
||||||
|
|
||||||
class Road(Document):
|
class Road(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
poly = PolygonField()
|
poly = PolygonField()
|
||||||
@@ -511,5 +552,6 @@ class GeoQueriesTest(unittest.TestCase):
|
|||||||
loc = Location.objects.as_pymongo()[0]
|
loc = Location.objects.as_pymongo()[0]
|
||||||
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
|
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import connect, Document, IntField
|
from mongoengine import connect, Document, IntField
|
||||||
@@ -99,4 +96,4 @@ class FindAndModifyTest(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -238,7 +238,8 @@ class TransformTest(unittest.TestCase):
|
|||||||
box = [(35.0, -125.0), (40.0, -100.0)]
|
box = [(35.0, -125.0), (40.0, -100.0)]
|
||||||
# I *meant* to execute location__within_box=box
|
# I *meant* to execute location__within_box=box
|
||||||
events = Event.objects(location__within=box)
|
events = Event.objects(location__within=box)
|
||||||
self.assertRaises(InvalidQueryError, lambda: events.count())
|
with self.assertRaises(InvalidQueryError):
|
||||||
|
events.count()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@@ -185,7 +185,7 @@ class QTest(unittest.TestCase):
|
|||||||
x = IntField()
|
x = IntField()
|
||||||
|
|
||||||
TestDoc.drop_collection()
|
TestDoc.drop_collection()
|
||||||
for i in xrange(1, 101):
|
for i in range(1, 101):
|
||||||
t = TestDoc(x=i)
|
t = TestDoc(x=i)
|
||||||
t.save()
|
t.save()
|
||||||
|
|
||||||
@@ -268,14 +268,13 @@ class QTest(unittest.TestCase):
|
|||||||
self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3)
|
self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3)
|
||||||
|
|
||||||
# Test invalid query objs
|
# Test invalid query objs
|
||||||
def wrong_query_objs():
|
with self.assertRaises(InvalidQueryError):
|
||||||
self.Person.objects('user1')
|
self.Person.objects('user1')
|
||||||
|
|
||||||
def wrong_query_objs_filter():
|
# filter should fail, too
|
||||||
self.Person.objects('user1')
|
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):
|
def test_q_regex(self):
|
||||||
"""Ensure that Q objects can be queried using regexes.
|
"""Ensure that Q objects can be queried using regexes.
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
import sys
|
|
||||||
import datetime
|
import datetime
|
||||||
from pymongo.errors import OperationFailure
|
from pymongo.errors import OperationFailure
|
||||||
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import unittest2 as unittest
|
import unittest2 as unittest
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -19,7 +16,8 @@ from mongoengine import (
|
|||||||
)
|
)
|
||||||
from mongoengine.python_support import IS_PYMONGO_3
|
from mongoengine.python_support import IS_PYMONGO_3
|
||||||
import mongoengine.connection
|
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):
|
def get_tz_awareness(connection):
|
||||||
@@ -37,8 +35,7 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
mongoengine.connection._dbs = {}
|
mongoengine.connection._dbs = {}
|
||||||
|
|
||||||
def test_connect(self):
|
def test_connect(self):
|
||||||
"""Ensure that the connect() method works properly.
|
"""Ensure that the connect() method works properly."""
|
||||||
"""
|
|
||||||
connect('mongoenginetest')
|
connect('mongoenginetest')
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
@@ -148,8 +145,7 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
self.assertEqual(expected_connection, actual_connection)
|
self.assertEqual(expected_connection, actual_connection)
|
||||||
|
|
||||||
def test_connect_uri(self):
|
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 = connect(db='mongoenginetest', alias='admin')
|
||||||
c.admin.system.users.remove({})
|
c.admin.system.users.remove({})
|
||||||
c.mongoenginetest.system.users.remove({})
|
c.mongoenginetest.system.users.remove({})
|
||||||
@@ -159,7 +155,10 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
c.mongoenginetest.add_user("username", "password")
|
c.mongoenginetest.add_user("username", "password")
|
||||||
|
|
||||||
if not IS_PYMONGO_3:
|
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')
|
connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest')
|
||||||
|
|
||||||
@@ -174,19 +173,9 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
c.mongoenginetest.system.users.remove({})
|
c.mongoenginetest.system.users.remove({})
|
||||||
|
|
||||||
def test_connect_uri_without_db(self):
|
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/')
|
connect("mongoenginetest", host='mongodb://localhost/')
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
@@ -196,13 +185,35 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||||
self.assertEqual(db.name, 'mongoenginetest')
|
self.assertEqual(db.name, 'mongoenginetest')
|
||||||
|
|
||||||
c.admin.system.users.remove({})
|
def test_connect_uri_default_db(self):
|
||||||
c.mongoenginetest.system.users.remove({})
|
"""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):
|
def test_connect_uri_with_authsource(self):
|
||||||
"""Ensure that the connect() method works well with
|
"""Ensure that the connect() method works well with `authSource`
|
||||||
the option `authSource` in URI.
|
option in the URI.
|
||||||
This feature was introduced in MongoDB 2.4 and removed in 2.6
|
|
||||||
"""
|
"""
|
||||||
# Create users
|
# Create users
|
||||||
c = connect('mongoenginetest')
|
c = connect('mongoenginetest')
|
||||||
@@ -211,36 +222,38 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Authentication fails without "authSource"
|
# Authentication fails without "authSource"
|
||||||
if IS_PYMONGO_3:
|
if IS_PYMONGO_3:
|
||||||
test_conn = connect('mongoenginetest', alias='test1',
|
test_conn = connect(
|
||||||
host='mongodb://username2:password@localhost/mongoenginetest')
|
'mongoenginetest', alias='test1',
|
||||||
|
host='mongodb://username2:password@localhost/mongoenginetest'
|
||||||
|
)
|
||||||
self.assertRaises(OperationFailure, test_conn.server_info)
|
self.assertRaises(OperationFailure, test_conn.server_info)
|
||||||
else:
|
else:
|
||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
ConnectionError, connect, 'mongoenginetest', alias='test1',
|
MongoEngineConnectionError,
|
||||||
|
connect, 'mongoenginetest', alias='test1',
|
||||||
host='mongodb://username2:password@localhost/mongoenginetest'
|
host='mongodb://username2:password@localhost/mongoenginetest'
|
||||||
)
|
)
|
||||||
self.assertRaises(ConnectionError, get_db, 'test1')
|
self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
|
||||||
|
|
||||||
# Authentication succeeds with "authSource"
|
# Authentication succeeds with "authSource"
|
||||||
connect(
|
authd_conn = connect(
|
||||||
'mongoenginetest', alias='test2',
|
'mongoenginetest', alias='test2',
|
||||||
host=('mongodb://username2:password@localhost/'
|
host=('mongodb://username2:password@localhost/'
|
||||||
'mongoenginetest?authSource=admin')
|
'mongoenginetest?authSource=admin')
|
||||||
)
|
)
|
||||||
# This will fail starting from MongoDB 2.6+
|
|
||||||
db = get_db('test2')
|
db = get_db('test2')
|
||||||
self.assertTrue(isinstance(db, pymongo.database.Database))
|
self.assertTrue(isinstance(db, pymongo.database.Database))
|
||||||
self.assertEqual(db.name, 'mongoenginetest')
|
self.assertEqual(db.name, 'mongoenginetest')
|
||||||
|
|
||||||
# Clear all users
|
# Clear all users
|
||||||
c.admin.system.users.remove({})
|
authd_conn.admin.system.users.remove({})
|
||||||
|
|
||||||
def test_register_connection(self):
|
def test_register_connection(self):
|
||||||
"""Ensure that connections with different aliases may be registered.
|
"""Ensure that connections with different aliases may be registered.
|
||||||
"""
|
"""
|
||||||
register_connection('testdb', 'mongoenginetest2')
|
register_connection('testdb', 'mongoenginetest2')
|
||||||
|
|
||||||
self.assertRaises(ConnectionError, get_connection)
|
self.assertRaises(MongoEngineConnectionError, get_connection)
|
||||||
conn = get_connection('testdb')
|
conn = get_connection('testdb')
|
||||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||||
|
|
||||||
@@ -257,8 +270,7 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
|
||||||
|
|
||||||
def test_connection_kwargs(self):
|
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)
|
connect('mongoenginetest', alias='t1', tz_aware=True)
|
||||||
conn = get_connection('t1')
|
conn = get_connection('t1')
|
||||||
|
|
||||||
@@ -268,6 +280,77 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
conn = get_connection('t2')
|
conn = get_connection('t2')
|
||||||
self.assertFalse(get_tz_awareness(conn))
|
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):
|
def test_datetime(self):
|
||||||
connect('mongoenginetest', tz_aware=True)
|
connect('mongoenginetest', tz_aware=True)
|
||||||
d = datetime.datetime(2010, 5, 5, tzinfo=utc)
|
d = datetime.datetime(2010, 5, 5, tzinfo=utc)
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@@ -79,7 +77,7 @@ class ContextManagersTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
@@ -117,7 +115,7 @@ class ContextManagersTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
@@ -195,7 +193,7 @@ class ContextManagersTest(unittest.TestCase):
|
|||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
self.assertEqual(0, q)
|
self.assertEqual(0, q)
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
db.test.find({}).count()
|
db.test.find({}).count()
|
||||||
|
|
||||||
self.assertEqual(50, q)
|
self.assertEqual(50, q)
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from mongoengine.base.datastructures import StrictDict, SemiStrictDict
|
|
||||||
|
from mongoengine.base.datastructures import StrictDict, SemiStrictDict
|
||||||
|
|
||||||
|
|
||||||
class TestStrictDict(unittest.TestCase):
|
class TestStrictDict(unittest.TestCase):
|
||||||
@@ -13,9 +14,18 @@ class TestStrictDict(unittest.TestCase):
|
|||||||
d = self.dtype(a=1, b=1, c=1)
|
d = self.dtype(a=1, b=1, c=1)
|
||||||
self.assertEqual((d.a, d.b, d.c), (1, 1, 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):
|
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):
|
def test_eq(self):
|
||||||
d = self.dtype(a=1, b=1, c=1)
|
d = self.dtype(a=1, b=1, c=1)
|
||||||
dd = 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)
|
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)
|
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)
|
i = self.strict_dict_class(("a", "c", "b"))(a=1, b=1, c=2)
|
||||||
|
|
||||||
self.assertEqual(d, dd)
|
self.assertEqual(d, dd)
|
||||||
self.assertNotEqual(d, e)
|
self.assertNotEqual(d, e)
|
||||||
self.assertNotEqual(d, f)
|
self.assertNotEqual(d, f)
|
||||||
@@ -37,20 +47,18 @@ class TestStrictDict(unittest.TestCase):
|
|||||||
d = self.dtype()
|
d = self.dtype()
|
||||||
d.a = 1
|
d.a = 1
|
||||||
self.assertEqual(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):
|
def test_setattr_raises_on_nonexisting_attr(self):
|
||||||
d = self.dtype()
|
d = self.dtype()
|
||||||
|
with self.assertRaises(AttributeError):
|
||||||
def _f():
|
|
||||||
d.x = 1
|
d.x = 1
|
||||||
self.assertRaises(AttributeError, _f)
|
|
||||||
|
|
||||||
def test_setattr_getattr_special(self):
|
def test_setattr_getattr_special(self):
|
||||||
d = self.strict_dict_class(["items"])
|
d = self.strict_dict_class(["items"])
|
||||||
d.items = 1
|
d.items = 1
|
||||||
self.assertEqual(d.items, 1)
|
self.assertEqual(d.items, 1)
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
d = self.dtype(a=1)
|
d = self.dtype(a=1)
|
||||||
self.assertEqual(d.get('a'), 1)
|
self.assertEqual(d.get('a'), 1)
|
||||||
@@ -88,7 +96,7 @@ class TestSemiSrictDict(TestStrictDict):
|
|||||||
def test_init_succeeds_with_nonexisting_attrs(self):
|
def test_init_succeeds_with_nonexisting_attrs(self):
|
||||||
d = self.dtype(a=1, b=1, c=1, x=2)
|
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))
|
self.assertEqual((d.a, d.b, d.c, d.x), (1, 1, 1, 2))
|
||||||
|
|
||||||
def test_iter_with_nonexisting_attrs(self):
|
def test_iter_with_nonexisting_attrs(self):
|
||||||
d = self.dtype(a=1, b=1, c=1, x=2)
|
d = self.dtype(a=1, b=1, c=1, x=2)
|
||||||
self.assertEqual(list(d), ['a', 'b', 'c', 'x'])
|
self.assertEqual(list(d), ['a', 'b', 'c', 'x'])
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from bson import DBRef, ObjectId
|
from bson import DBRef, ObjectId
|
||||||
@@ -32,7 +30,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -90,7 +88,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -162,7 +160,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 26):
|
for i in range(1, 26):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -440,7 +438,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -531,7 +529,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -614,15 +612,15 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
members.append(user)
|
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.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()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -687,7 +685,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -699,9 +697,9 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
members += [a, b, c]
|
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.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()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -783,16 +781,16 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
members += [a]
|
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.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()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -866,7 +864,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -878,9 +876,9 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
members += [a, b, c]
|
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.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()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -1103,7 +1101,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
Group(name="Test", members=User.objects).save()
|
Group(name="Test", members=User.objects).save()
|
||||||
@@ -1132,7 +1130,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
Group(name="Test", members=User.objects).save()
|
Group(name="Test", members=User.objects).save()
|
||||||
@@ -1169,7 +1167,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in xrange(1, 51):
|
for i in range(1, 51):
|
||||||
a = UserA(name='User A %s' % i).save()
|
a = UserA(name='User A %s' % i).save()
|
||||||
b = UserB(name='User B %s' % i).save()
|
b = UserB(name='User B %s' % i).save()
|
||||||
c = UserC(name='User C %s' % i).save()
|
c = UserC(name='User C %s' % i).save()
|
||||||
|
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pymongo import ReadPreference
|
from pymongo import ReadPreference
|
||||||
@@ -18,7 +15,7 @@ else:
|
|||||||
|
|
||||||
import mongoengine
|
import mongoengine
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import ConnectionError
|
from mongoengine.connection import MongoEngineConnectionError
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTest(unittest.TestCase):
|
class ConnectionTest(unittest.TestCase):
|
||||||
@@ -41,7 +38,7 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
conn = connect(db='mongoenginetest',
|
conn = connect(db='mongoenginetest',
|
||||||
host="mongodb://localhost/mongoenginetest?replicaSet=rs",
|
host="mongodb://localhost/mongoenginetest?replicaSet=rs",
|
||||||
read_preference=READ_PREF)
|
read_preference=READ_PREF)
|
||||||
except ConnectionError, e:
|
except MongoEngineConnectionError as e:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(conn, CONN_CLASS):
|
if not isinstance(conn, CONN_CLASS):
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
sys.path[0:0] = [""]
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
78
tests/utils.py
Normal file
78
tests/utils.py
Normal 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
13
tox.ini
@@ -1,5 +1,5 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = {py26,py27,py33,py34,py35,pypy,pypy3}-{mg27,mg28},flake8
|
envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
@@ -7,16 +7,7 @@ commands =
|
|||||||
deps =
|
deps =
|
||||||
nose
|
nose
|
||||||
mg27: PyMongo<2.8
|
mg27: PyMongo<2.8
|
||||||
mg28: PyMongo>=2.8,<3.0
|
mg28: PyMongo>=2.8,<2.9
|
||||||
mg30: PyMongo>=3.0
|
mg30: PyMongo>=3.0
|
||||||
mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master
|
|
||||||
setenv =
|
setenv =
|
||||||
PYTHON_EGG_CACHE = {envdir}/python-eggs
|
PYTHON_EGG_CACHE = {envdir}/python-eggs
|
||||||
passenv = windir
|
|
||||||
|
|
||||||
[testenv:flake8]
|
|
||||||
deps =
|
|
||||||
flake8
|
|
||||||
flake8-import-order
|
|
||||||
commands =
|
|
||||||
flake8
|
|
||||||
|
Reference in New Issue
Block a user