Compare commits
15 Commits
get-collec
...
improve-he
Author | SHA1 | Date | |
---|---|---|---|
|
8a84398cf6 | ||
|
5de80f0327 | ||
|
ea91383558 | ||
|
081b0e1edd | ||
|
5f2cd8ac75 | ||
|
f2b8dcf292 | ||
|
e2097061e9 | ||
|
c72465cbb4 | ||
|
5d093e1011 | ||
|
13ddef2033 | ||
|
0229ebff4b | ||
|
fd8cfbf610 | ||
|
43adba115b | ||
|
3985bd3e53 | ||
|
c35dab9541 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -14,7 +14,4 @@ env/
|
|||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
tests/test_bugfix.py
|
tests/test_bugfix.py
|
||||||
htmlcov/
|
htmlcov/
|
||||||
venv
|
|
||||||
venv3
|
|
||||||
scratchpad
|
|
@@ -1,23 +0,0 @@
|
|||||||
#!/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;
|
|
@@ -1,22 +0,0 @@
|
|||||||
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,48 +1,29 @@
|
|||||||
# 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.7
|
- '2.6'
|
||||||
- 3.5
|
- '2.7'
|
||||||
|
- '3.3'
|
||||||
|
- '3.4'
|
||||||
|
- '3.5'
|
||||||
- pypy
|
- pypy
|
||||||
- pypy3
|
- pypy3
|
||||||
|
|
||||||
env:
|
env:
|
||||||
- MONGODB=2.6 PYMONGO=2.7
|
- PYMONGO=2.7
|
||||||
- MONGODB=2.6 PYMONGO=2.8
|
- PYMONGO=2.8
|
||||||
- MONGODB=2.6 PYMONGO=3.0
|
- 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:
|
||||||
- bash .install_mongodb_on_travis.sh
|
- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
|
||||||
|
- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' |
|
||||||
|
sudo tee /etc/apt/sources.list.d/mongodb.list
|
||||||
|
- travis_retry sudo apt-get update
|
||||||
|
- travis_retry sudo apt-get install mongodb-org-server
|
||||||
|
|
||||||
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
|
||||||
@@ -50,52 +31,33 @@ 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 flake8-import-order
|
- travis_retry pip install flake8
|
||||||
- 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 flake8 .; else echo "flake8 only runs on py27"; fi
|
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e flake8; 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
|
||||||
|
|
||||||
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
|
after_script: coveralls --verbose
|
||||||
# 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,4 +242,3 @@ 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.7 and newer. Language
|
MongoEngine supports CPython 2.6 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,20 +29,19 @@ 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
|
including 4 space indents. When possible we try to stick to 79 character line limits.
|
||||||
limits. However, screens got bigger and an ORM has a strong focus on
|
However, screens got bigger and an ORM has a strong focus on readability and
|
||||||
readability and if it can help, we accept 119 as maximum line length, in a
|
if it can help, we accept 119 as maximum line length, in a similar way as
|
||||||
similar way as `django does
|
`django does <https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
|
||||||
<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. Any pull requests without
|
and any pull requests are automatically tested by Travis. Any pull requests
|
||||||
tests will take longer to be integrated and might be refused.
|
without tests will take longer to be integrated and might be refused.
|
||||||
|
|
||||||
You may also submit a simple failing test as a pull request if you don't know
|
You may also submit a simple failing test as a PullRequest 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.
|
||||||
|
|
||||||
@@ -50,18 +49,13 @@ 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 all supported Python, PyMongo, and MongoDB versions.
|
- Ensure tests pass on every Python and PyMongo versions.
|
||||||
You can test various Python and PyMongo versions locally by executing
|
You can test on these versions locally by executing ``tox``
|
||||||
``tox``. For different MongoDB versions, you can rely on our automated
|
- Add enhancements or problematic bug fixes to docs/changelog.rst
|
||||||
Travis tests.
|
|
||||||
- Add enhancements or problematic bug fixes to docs/changelog.rst.
|
|
||||||
- Add yourself to AUTHORS :)
|
- Add yourself to AUTHORS :)
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
@@ -75,6 +69,3 @@ 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.
|
|
||||||
|
91
README.rst
91
README.rst
@@ -4,57 +4,47 @@ 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: Stefan Wójcik (http://github.com/wojcikstefan)
|
:Maintainer: Ross Lawley (http://github.com/rozza)
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master
|
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master
|
||||||
:target: https://travis-ci.org/MongoEngine/mongoengine
|
:target: http://travis-ci.org/MongoEngine/mongoengine
|
||||||
|
|
||||||
.. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master
|
.. image:: https://coveralls.io/repos/MongoEngine/mongoengine/badge.png?branch=master
|
||||||
:target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master
|
:target: https://coveralls.io/r/MongoEngine/mongoengine?branch=master
|
||||||
|
|
||||||
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.svg?style=flat
|
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.png
|
||||||
:target: https://landscape.io/github/MongoEngine/mongoengine/master
|
:target: https://landscape.io/github/MongoEngine/mongoengine/master
|
||||||
:alt: Code Health
|
:alt: Code Health
|
||||||
|
|
||||||
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 is available at https://mongoengine-odm.readthedocs.io - there
|
Documentation available at https://mongoengine-odm.readthedocs.io - there is currently
|
||||||
is currently a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_,
|
a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_, a `user guide
|
||||||
a `user guide <https://mongoengine-odm.readthedocs.io/guide/index.html>`_, and
|
<https://mongoengine-odm.readthedocs.io/guide/index.html>`_ and an `API reference
|
||||||
an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
|
<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>`_
|
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ and thus
|
||||||
and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the
|
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
|
||||||
============
|
============
|
||||||
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
|
|
||||||
At the very least, you'll need these two packages to use MongoEngine:
|
|
||||||
|
|
||||||
- pymongo>=2.7.1
|
- pymongo>=2.7.1
|
||||||
- six>=1.10.0
|
- sphinx (optional - for documentation generation)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
|
.. note
|
||||||
|
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
|
||||||
========
|
========
|
||||||
@@ -62,14 +52,10 @@ 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.utcnow)
|
posted = DateTimeField(default=datetime.datetime.now)
|
||||||
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)
|
||||||
@@ -97,28 +83,27 @@ Some simple examples of what MongoEngine code looks like:
|
|||||||
... print
|
... print
|
||||||
...
|
...
|
||||||
|
|
||||||
# Count all blog posts and its subtypes
|
>>> len(BlogPost.objects)
|
||||||
>>> BlogPost.objects.count()
|
|
||||||
2
|
2
|
||||||
>>> TextPost.objects.count()
|
>>> len(TextPost.objects)
|
||||||
1
|
1
|
||||||
>>> LinkPost.objects.count()
|
>>> len(LinkPost.objects)
|
||||||
1
|
1
|
||||||
|
|
||||||
# Count tagged posts
|
# Find tagged posts
|
||||||
>>> BlogPost.objects(tags='mongoengine').count()
|
>>> len(BlogPost.objects(tags='mongoengine'))
|
||||||
2
|
2
|
||||||
>>> BlogPost.objects(tags='mongodb').count()
|
>>> len(BlogPost.objects(tags='mongodb'))
|
||||||
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 and PyMongo version, you can
|
To run the test suite on every supported Python version and every supported PyMongo version,
|
||||||
use ``tox``. You'll need to make sure you have each supported Python version
|
you can use ``tox``.
|
||||||
installed in your environment and then:
|
tox and each supported Python version should be installed in your environment:
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
@@ -127,16 +112,13 @@ installed in your environment and then:
|
|||||||
# Run the test suites
|
# Run the test suites
|
||||||
$ tox
|
$ tox
|
||||||
|
|
||||||
If you wish to run a subset of tests, use the nosetests convention:
|
If you wish to run one single or selected tests, use the nosetest convention. It will find the folder,
|
||||||
|
eventually the file, go to the TestClass specified after the colon and eventually right to the single test.
|
||||||
|
Also use the -s argument if you want to print out whatever or access pdb while testing.
|
||||||
|
|
||||||
.. code-block:: shell
|
.. code-block:: shell
|
||||||
|
|
||||||
# Run all the tests in a particular test file
|
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest.test_cls_field -s
|
||||||
$ 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
|
||||||
=========
|
=========
|
||||||
@@ -144,7 +126,8 @@ 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,41 +1,118 @@
|
|||||||
#!/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 = """
|
||||||
@@ -54,7 +131,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)
|
||||||
|
|
||||||
@@ -69,10 +146,9 @@ 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.get_database('timeit_test', write_concern=WriteConcern(w=0))
|
db = connection.timeit_test
|
||||||
noddy = db.noddy
|
noddy = db.noddy
|
||||||
|
|
||||||
for i in range(10000):
|
for i in range(10000):
|
||||||
@@ -80,7 +156,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)
|
noddy.save(example, write_concern={"w": 0})
|
||||||
|
|
||||||
myNoddys = noddy.find()
|
myNoddys = noddy.find()
|
||||||
[n for n in myNoddys] # iterate
|
[n for n in myNoddys] # iterate
|
||||||
@@ -95,10 +171,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.close()
|
connection.disconnect()
|
||||||
|
|
||||||
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,60 +2,16 @@
|
|||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
Development
|
|
||||||
===========
|
|
||||||
- (Fill this out as you fix issues and develop your features).
|
|
||||||
|
|
||||||
Changes in 0.13.0
|
|
||||||
=================
|
|
||||||
- POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see
|
|
||||||
docs/upgrade.rst for details.
|
|
||||||
|
|
||||||
Changes in 0.12.0
|
|
||||||
=================
|
|
||||||
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
|
|
||||||
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
|
|
||||||
- Fixed the way `Document.objects.create` works with duplicate IDs #1485
|
|
||||||
- Fixed connecting to a replica set with PyMongo 2.x #1436
|
|
||||||
- Fixed using sets in field choices #1481
|
|
||||||
- Fixed deleting items from a `ListField` #1318
|
|
||||||
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
|
|
||||||
- Fixed behavior of a `dec` update operator #1450
|
|
||||||
- Added a `rename` update operator #1454
|
|
||||||
- Added validation for the `db_field` parameter #1448
|
|
||||||
- Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
|
|
||||||
- Fixed the error message displayed when validating unicode URLs #1486
|
|
||||||
- Raise an error when trying to save an abstract document #1449
|
|
||||||
|
|
||||||
Changes in 0.11.0
|
|
||||||
=================
|
|
||||||
- BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
|
|
||||||
- BREAKING CHANGE: Dropped Python 2.6 support. #1428
|
|
||||||
- BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428
|
|
||||||
- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
|
|
||||||
- Fixed absent rounding for DecimalField when `force_string` is set. #1103
|
|
||||||
|
|
||||||
Changes in 0.10.8
|
Changes in 0.10.8
|
||||||
=================
|
=================
|
||||||
- Added support for QuerySet.batch_size (#1426)
|
- Fill this in as PRs for v0.10.8 are merged
|
||||||
- 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
|
||||||
=================
|
=================
|
||||||
- Dropped Python 3.2 support #1390
|
- Dropped Python 3.2 support #1390
|
||||||
- Fixed the bug where dynamic doc has index inside a dict field #1278
|
- Fixed the bug where dynamic doc has index inside a dict field #1278
|
||||||
- Fixed: ListField minus index assignment does not work #1128
|
- Fixed: ListField minus index assignment does not work #1128
|
||||||
|
- Fixed not being able to specify `use_db_field=False` on `ListField(EmbeddedDocumentField)` instances
|
||||||
- Fixed cascade delete mixing among collections #1224
|
- Fixed cascade delete mixing among collections #1224
|
||||||
- Add `signal_kwargs` argument to `Document.save`, `Document.delete` and `BaseQuerySet.insert` to be passed to signals calls #1206
|
- Add `signal_kwargs` argument to `Document.save`, `Document.delete` and `BaseQuerySet.insert` to be passed to signals calls #1206
|
||||||
- Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
|
- Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
|
||||||
@@ -71,8 +27,7 @@ Changes in 0.10.7
|
|||||||
- Added support for pickling QuerySet instances. #1397
|
- Added support for pickling QuerySet instances. #1397
|
||||||
- Fixed connecting to a list of hosts #1389
|
- Fixed connecting to a list of hosts #1389
|
||||||
- Fixed a bug where accessing broken references wouldn't raise a DoesNotExist error #1334
|
- Fixed a bug where accessing broken references wouldn't raise a DoesNotExist error #1334
|
||||||
- Fixed not being able to specify use_db_field=False on ListField(EmbeddedDocumentField) instances #1218
|
- Improvements to the dictionary fields docs # 1383
|
||||||
- Improvements to the dictionary fields docs #1383
|
|
||||||
|
|
||||||
Changes in 0.10.6
|
Changes in 0.10.6
|
||||||
=================
|
=================
|
||||||
|
@@ -33,7 +33,7 @@ the :attr:`host` to
|
|||||||
corresponding parameters in :func:`~mongoengine.connect`: ::
|
corresponding parameters in :func:`~mongoengine.connect`: ::
|
||||||
|
|
||||||
connect(
|
connect(
|
||||||
db='test',
|
name='test',
|
||||||
username='user',
|
username='user',
|
||||||
password='12345',
|
password='12345',
|
||||||
host='mongodb://admin:qwerty@localhost/production'
|
host='mongodb://admin:qwerty@localhost/production'
|
||||||
@@ -42,18 +42,13 @@ 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.
|
||||||
|
|
||||||
Replica Sets
|
ReplicaSets
|
||||||
============
|
===========
|
||||||
|
|
||||||
MongoEngine supports connecting to replica sets::
|
MongoEngine supports
|
||||||
|
:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`. To use them,
|
||||||
from mongoengine import connect
|
please use an URI style connection and provide the ``replicaSet`` name
|
||||||
|
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 ::
|
||||||
@@ -64,74 +59,76 @@ queries by passing the read_preference ::
|
|||||||
Multiple Databases
|
Multiple Databases
|
||||||
==================
|
==================
|
||||||
|
|
||||||
To use multiple databases you can use :func:`~mongoengine.connect` and provide
|
Multiple database support was added in MongoEngine 0.6. To use multiple
|
||||||
an `alias` name for the connection - if no `alias` is provided then "default"
|
databases you can use :func:`~mongoengine.connect` and provide an `alias` name
|
||||||
is used.
|
for the connection - if no `alias` is provided then "default" 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`
|
`db_alias` in their meta data. This allows :class:`~pymongo.dbref.DBRef` objects
|
||||||
objects to point across databases and collections. Below is an example schema,
|
to point across databases and collections. Below is an example schema, using
|
||||||
using 3 different databases to store data::
|
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
|
||||||
a document to.
|
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 to the same User document across databases::
|
access 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 to the same Group document across collection::
|
access 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. list, tuple or set) of choices to which the value of this
|
An iterable (e.g. a list or tuple) 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,6 +361,11 @@ 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.com/>`_
|
To use MongoEngine, you will need to download `MongoDB <http://mongodb.org/>`_
|
||||||
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 you can use :program:`pip`:
|
MongoEngine is available on PyPI, so to use it you can use :program:`pip`:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
|
@@ -340,19 +340,14 @@ Javascript code that is executed on the database server.
|
|||||||
|
|
||||||
Counting results
|
Counting results
|
||||||
----------------
|
----------------
|
||||||
Just as with limiting and skipping results, there is a method on a
|
Just as with limiting and skipping results, there is a method on
|
||||||
:class:`~mongoengine.queryset.QuerySet` object --
|
:class:`~mongoengine.queryset.QuerySet` objects --
|
||||||
:meth:`~mongoengine.queryset.QuerySet.count`::
|
:meth:`~mongoengine.queryset.QuerySet.count`, but there is also a more Pythonic
|
||||||
|
way of achieving this::
|
||||||
|
|
||||||
num_users = User.objects.count()
|
num_users = len(User.objects)
|
||||||
|
|
||||||
You could technically use ``len(User.objects)`` to get the same result, but it
|
Even if len() is the Pythonic way of counting results, keep in mind that if you concerned about performance, :meth:`~mongoengine.queryset.QuerySet.count` is the way to go since it only execute a server side count query, while len() retrieves the results, places them in cache, and finally counts them. If we compare the performance of the two operations, len() is much slower than :meth:`~mongoengine.queryset.QuerySet.count`.
|
||||||
would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`.
|
|
||||||
When you execute a server-side count query, you let MongoDB do the heavy
|
|
||||||
lifting and you receive a single integer over the wire. Meanwhile, len()
|
|
||||||
retrieves all the results, places them in a local cache, and finally counts
|
|
||||||
them. If we compare the performance of the two operations, len() is much slower
|
|
||||||
than :meth:`~mongoengine.queryset.QuerySet.count`.
|
|
||||||
|
|
||||||
Further aggregation
|
Further aggregation
|
||||||
-------------------
|
-------------------
|
||||||
@@ -484,8 +479,6 @@ 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,4 +142,11 @@ 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,10 +3,11 @@ 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
|
through how to create a simple **Tumblelog** application. A Tumblelog is a type
|
||||||
blog that supports mixed media content, including text, images, links, video,
|
of blog where posts are not constrained to being conventional text-based posts.
|
||||||
audio, etc. For simplicity's sake, we'll stick to text, image, and link
|
As well as text-based entries, users may post images, links, videos, etc. For
|
||||||
entries. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
simplicity's sake, we'll stick to text, image and link entries in our
|
||||||
|
application. As the purpose of this tutorial is to introduce MongoEngine, we'll
|
||||||
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.
|
||||||
|
|
||||||
@@ -15,14 +16,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 *
|
||||||
@@ -38,18 +39,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 schemas for our documents can help to iron
|
to the data model. However, defining schemata for our documents can help to
|
||||||
out bugs involving incorrect types or missing fields, and also allow us to
|
iron out bugs involving incorrect types or missing fields, and also allow us to
|
||||||
define utility methods on our documents in the same way that traditional
|
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.
|
||||||
|
|
||||||
@@ -77,7 +78,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
|
||||||
@@ -95,7 +96,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 modeling out of the box --- all you need do is turn on inheritance
|
this kind of modelling 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):
|
||||||
@@ -127,8 +128,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 denormalization won't impact the size of the
|
than a document's id), this denormalisation won't impact very strongly on the
|
||||||
database very strongly. Let's take a look at the code of our modified
|
size of our database. So let's take a look that the code our modified
|
||||||
:class:`Post` class::
|
:class:`Post` class::
|
||||||
|
|
||||||
class Post(Document):
|
class Post(Document):
|
||||||
@@ -140,7 +141,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 specialized post types as they all
|
.. note:: We don't need to modify the specialised post types as they all
|
||||||
inherit from :class:`Post`.
|
inherit from :class:`Post`.
|
||||||
|
|
||||||
Comments
|
Comments
|
||||||
@@ -148,7 +149,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 and then query the database again for the comments associated with the
|
database, 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
|
||||||
@@ -206,10 +207,7 @@ object::
|
|||||||
ross.last_name = 'Lawley'
|
ross.last_name = 'Lawley'
|
||||||
ross.save()
|
ross.save()
|
||||||
|
|
||||||
Assign another user to a variable called ``john``, just like we did above with
|
Now that we've got our user in the database, let's add a couple of posts::
|
||||||
``ross``.
|
|
||||||
|
|
||||||
Now that we've got our users in the database, let's add a couple of posts::
|
|
||||||
|
|
||||||
post1 = TextPost(title='Fun with MongoEngine', author=john)
|
post1 = TextPost(title='Fun with MongoEngine', author=john)
|
||||||
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
|
post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
|
||||||
@@ -221,8 +219,8 @@ Now that we've got our users 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 an object that has already been saved and
|
.. note:: If you change a field on a object that has already been saved, then
|
||||||
then call :meth:`save` again, the document will be updated.
|
call :meth:`save` again, the document will be updated.
|
||||||
|
|
||||||
Accessing our data
|
Accessing our data
|
||||||
==================
|
==================
|
||||||
@@ -234,17 +232,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:
|
||||||
@@ -261,14 +259,16 @@ 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: {}'.format(post.link_url))
|
print 'Link:', 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 +283,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 +292,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 {} posts with tag "mongodb"'.format(num_posts))
|
print 'Found %d posts with tag "mongodb"' % 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
|
your mongoengine journey is the `full user guide <guide/index.html>`_, where you
|
||||||
you can learn in-depth about how to use MongoEngine and MongoDB.
|
can learn indepth about how to use mongoengine and mongodb.
|
||||||
|
@@ -2,67 +2,6 @@
|
|||||||
Upgrading
|
Upgrading
|
||||||
#########
|
#########
|
||||||
|
|
||||||
Development
|
|
||||||
***********
|
|
||||||
(Fill this out whenever you introduce breaking changes to MongoEngine)
|
|
||||||
|
|
||||||
0.13.0
|
|
||||||
******
|
|
||||||
This release adds Unicode support to the `EmailField` and changes its
|
|
||||||
structure significantly. Previously, email addresses containing Unicode
|
|
||||||
characters didn't work at all. Starting with v0.13.0, domains with Unicode
|
|
||||||
characters are supported out of the box, meaning some emails that previously
|
|
||||||
didn't pass validation now do. Make sure the rest of your application can
|
|
||||||
accept such email addresses. Additionally, if you subclassed the `EmailField`
|
|
||||||
in your application and overrode `EmailField.EMAIL_REGEX`, you will have to
|
|
||||||
adjust your code to override `EmailField.USER_REGEX`, `EmailField.DOMAIN_REGEX`,
|
|
||||||
and potentially `EmailField.UTF8_USER_REGEX`.
|
|
||||||
|
|
||||||
0.12.0
|
|
||||||
******
|
|
||||||
This release includes various fixes for the `BaseQuerySet` methods and how they
|
|
||||||
are chained together. Since version 0.10.1 applying limit/skip/hint/batch_size
|
|
||||||
to an already-existing queryset wouldn't modify the underlying PyMongo cursor.
|
|
||||||
This has been fixed now, so you'll need to make sure that your code didn't rely
|
|
||||||
on the broken implementation.
|
|
||||||
|
|
||||||
Additionally, a public `BaseQuerySet.clone_into` has been renamed to a private
|
|
||||||
`_clone_into`. If you directly used that method in your code, you'll need to
|
|
||||||
rename its occurrences.
|
|
||||||
|
|
||||||
0.11.0
|
|
||||||
******
|
|
||||||
This release includes a major rehaul of MongoEngine's code quality and
|
|
||||||
introduces a few breaking changes. It also touches many different parts of
|
|
||||||
the package and although all the changes have been tested and scrutinized,
|
|
||||||
you're encouraged to thorougly test the upgrade.
|
|
||||||
|
|
||||||
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
|
|
||||||
If you import or catch this exception, you'll need to rename it in your code.
|
|
||||||
|
|
||||||
Second breaking change drops Python v2.6 support. If you run MongoEngine on
|
|
||||||
that Python version, you'll need to upgrade it first.
|
|
||||||
|
|
||||||
Third breaking change drops an old backward compatibility measure where
|
|
||||||
`from mongoengine.base import ErrorClass` would work on top of
|
|
||||||
`from mongoengine.errors import ErrorClass` (where `ErrorClass` is e.g.
|
|
||||||
`ValidationError`). If you import any exceptions from `mongoengine.base`,
|
|
||||||
change it to `mongoengine.errors`.
|
|
||||||
|
|
||||||
0.10.8
|
|
||||||
******
|
|
||||||
This version fixed an issue where specifying a MongoDB URI host would override
|
|
||||||
more information than it should. These changes are minor, but they still
|
|
||||||
subtly modify the connection logic and thus you're encouraged to test your
|
|
||||||
MongoDB connection before shipping v0.10.8 in production.
|
|
||||||
|
|
||||||
0.10.7
|
|
||||||
******
|
|
||||||
|
|
||||||
`QuerySet.aggregate_sum` and `QuerySet.aggregate_average` are dropped. Use
|
|
||||||
`QuerySet.sum` and `QuerySet.average` instead which use the aggreation framework
|
|
||||||
by default from now on.
|
|
||||||
|
|
||||||
0.9.0
|
0.9.0
|
||||||
*****
|
*****
|
||||||
|
|
||||||
|
@@ -1,35 +1,25 @@
|
|||||||
# Import submodules so that we can expose their __all__
|
import connection
|
||||||
from mongoengine import connection
|
from connection import *
|
||||||
from mongoengine import document
|
import document
|
||||||
from mongoengine import errors
|
from document import *
|
||||||
from mongoengine import fields
|
import errors
|
||||||
from mongoengine import queryset
|
from errors import *
|
||||||
from mongoengine import signals
|
import fields
|
||||||
|
from fields import *
|
||||||
|
import queryset
|
||||||
|
from queryset import *
|
||||||
|
import signals
|
||||||
|
from signals import *
|
||||||
|
|
||||||
# Import everything from each submodule so that it can be accessed via
|
__all__ = (list(document.__all__) + fields.__all__ + connection.__all__ +
|
||||||
# mongoengine, e.g. instead of `from mongoengine.connection import connect`,
|
list(queryset.__all__) + signals.__all__ + list(errors.__all__))
|
||||||
# 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, 13, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def get_version():
|
def get_version():
|
||||||
"""Return the VERSION as a string, e.g. for VERSION == (0, 10, 7),
|
if isinstance(VERSION[-1], basestring):
|
||||||
return '0.10.7'.
|
return '.'.join(map(str, VERSION[:-1])) + VERSION[-1]
|
||||||
"""
|
|
||||||
return '.'.join(map(str, VERSION))
|
return '.'.join(map(str, VERSION))
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,28 +1,8 @@
|
|||||||
# 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 *
|
||||||
|
|
||||||
__all__ = (
|
# Help with backwards compatibility
|
||||||
# common
|
from mongoengine.errors import *
|
||||||
'UPDATE_OPERATORS', '_document_registry', 'get_document',
|
|
||||||
|
|
||||||
# datastructures
|
|
||||||
'BaseDict', 'BaseList', 'EmbeddedDocumentList',
|
|
||||||
|
|
||||||
# document
|
|
||||||
'BaseDocument',
|
|
||||||
|
|
||||||
# fields
|
|
||||||
'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
|
|
||||||
|
|
||||||
# metaclasses
|
|
||||||
'DocumentMetaclass', 'TopLevelDocumentMetaclass'
|
|
||||||
)
|
|
||||||
|
@@ -1,18 +1,13 @@
|
|||||||
from mongoengine.errors import NotRegistered
|
from mongoengine.errors import NotRegistered
|
||||||
|
|
||||||
__all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
|
__all__ = ('ALLOW_INHERITANCE', '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,16 +1,14 @@
|
|||||||
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
|
||||||
@@ -95,7 +93,8 @@ 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
|
||||||
@@ -138,7 +137,10 @@ 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):
|
||||||
self._mark_as_changed()
|
if isinstance(key, slice):
|
||||||
|
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):
|
||||||
@@ -207,22 +209,17 @@ class BaseList(list):
|
|||||||
class EmbeddedDocumentList(BaseList):
|
class EmbeddedDocumentList(BaseList):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __match_all(cls, embedded_doc, kwargs):
|
def __match_all(cls, i, kwargs):
|
||||||
"""Return True if a given embedded doc matches all the filter
|
items = kwargs.items()
|
||||||
kwargs. If it doesn't return False.
|
return all([
|
||||||
"""
|
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, embedded_docs, kwargs):
|
def __only_matches(cls, obj, kwargs):
|
||||||
"""Return embedded docs that match the filter kwargs."""
|
|
||||||
if not kwargs:
|
if not kwargs:
|
||||||
return embedded_docs
|
return obj
|
||||||
return [doc for doc in embedded_docs if cls.__match_all(doc, kwargs)]
|
return filter(lambda i: cls.__match_all(i, kwargs), obj)
|
||||||
|
|
||||||
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)
|
||||||
@@ -288,18 +285,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``
|
"""
|
||||||
if empty.
|
Returns the first embedded document in the list, or ``None`` if empty.
|
||||||
"""
|
"""
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
return self[0]
|
return self[0]
|
||||||
@@ -429,7 +426,7 @@ class StrictDict(object):
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.items() == other.items()
|
return self.items() == other.items()
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __neq__(self, other):
|
||||||
return self.items() != other.items()
|
return self.items() != other.items()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -441,7 +438,7 @@ class StrictDict(object):
|
|||||||
__slots__ = allowed_keys_tuple
|
__slots__ = allowed_keys_tuple
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '{%s}' % ', '.join('"{0!s}": {1!r}'.format(k, v) for k, v in self.items())
|
return "{%s}" % ', '.join('"{0!s}": {0!r}'.format(k) for k in self.iterkeys())
|
||||||
|
|
||||||
cls._classes[allowed_keys] = SpecificStrictDict
|
cls._classes[allowed_keys] = SpecificStrictDict
|
||||||
return cls._classes[allowed_keys]
|
return cls._classes[allowed_keys]
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -7,27 +8,30 @@ 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 get_document
|
from mongoengine.base.common import ALLOW_INHERITANCE, get_document
|
||||||
from mongoengine.base.datastructures import (BaseDict, BaseList,
|
from mongoengine.base.datastructures import (
|
||||||
EmbeddedDocumentList,
|
BaseDict,
|
||||||
SemiStrictDict, StrictDict)
|
BaseList,
|
||||||
|
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, OperationError, ValidationError)
|
LookUpError, ValidationError)
|
||||||
|
from mongoengine.python_support import PY3, txt_type
|
||||||
|
|
||||||
__all__ = ('BaseDocument',)
|
__all__ = ('BaseDocument', 'NON_FIELD_ERRORS')
|
||||||
|
|
||||||
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',
|
'_dynamic_fields', '_auto_id_field', '_db_field_map', '__weakref__')
|
||||||
'__weakref__')
|
|
||||||
|
|
||||||
_dynamic = False
|
_dynamic = False
|
||||||
_dynamic_lock = True
|
_dynamic_lock = True
|
||||||
@@ -53,15 +57,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 "%s"' % name)
|
"Multiple values for keyword argument '" + 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)
|
||||||
|
|
||||||
@@ -72,7 +76,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)
|
||||||
|
|
||||||
@@ -91,7 +95,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
|
||||||
@@ -117,7 +121,7 @@ class BaseDocument(object):
|
|||||||
else:
|
else:
|
||||||
self._data[key] = value
|
self._data[key] = value
|
||||||
|
|
||||||
# Set any get_<field>_display methods
|
# Set any get_fieldname_display methods
|
||||||
self.__set_field_display()
|
self.__set_field_display()
|
||||||
|
|
||||||
if self._dynamic:
|
if self._dynamic:
|
||||||
@@ -146,7 +150,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
|
||||||
@@ -165,13 +169,11 @@ class BaseDocument(object):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
self__created = True
|
self__created = True
|
||||||
|
|
||||||
if (
|
if (self._is_document and not self__created and
|
||||||
self._is_document and
|
name in self._meta.get('shard_key', tuple()) and
|
||||||
not self__created and
|
self._data.get(name) != value):
|
||||||
name in self._meta.get('shard_key', tuple()) and
|
OperationError = _import_class('OperationError')
|
||||||
self._data.get(name) != value
|
msg = "Shard Keys are immutable. Tried to update %s" % name
|
||||||
):
|
|
||||||
msg = 'Shard Keys are immutable. Tried to update %s' % name
|
|
||||||
raise OperationError(msg)
|
raise OperationError(msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -195,8 +197,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:
|
||||||
@@ -210,7 +212,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)
|
||||||
@@ -252,13 +254,12 @@ 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 six.PY3:
|
if PY3:
|
||||||
return self.__unicode__()
|
return self.__unicode__()
|
||||||
else:
|
else:
|
||||||
return six.text_type(self).encode('utf-8')
|
return unicode(self).encode('utf-8')
|
||||||
return six.text_type('%s object' % self.__class__.__name__)
|
return txt_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:
|
||||||
@@ -307,7 +308,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']
|
||||||
@@ -350,8 +351,18 @@ 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 self._meta.get('allow_inheritance'):
|
if (not hasattr(self, '_meta') or
|
||||||
|
not self._meta.get('allow_inheritance', ALLOW_INHERITANCE)):
|
||||||
data.pop('_cls')
|
data.pop('_cls')
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -365,16 +376,16 @@ class BaseDocument(object):
|
|||||||
if clean:
|
if clean:
|
||||||
try:
|
try:
|
||||||
self.clean()
|
self.clean()
|
||||||
except ValidationError as error:
|
except ValidationError, 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:
|
||||||
@@ -384,29 +395,27 @@ class BaseDocument(object):
|
|||||||
field._validate(value, clean=clean)
|
field._validate(value, clean=clean)
|
||||||
else:
|
else:
|
||||||
field._validate(value)
|
field._validate(value)
|
||||||
except ValidationError as error:
|
except ValidationError, error:
|
||||||
errors[field.name] = error.errors or error
|
errors[field.name] = error.errors or error
|
||||||
except (ValueError, AttributeError, AssertionError) as error:
|
except (ValueError, AttributeError, AssertionError), 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):
|
||||||
"""Convert this document to JSON.
|
"""Converts a document to JSON.
|
||||||
|
:param use_db_field: Set to True by default but enables the output of the json structure with the field names
|
||||||
:param use_db_field: Serialize field names as they appear in
|
and not the mongodb store db_names in case of set to False
|
||||||
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)
|
||||||
@@ -417,26 +426,33 @@ 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
|
||||||
|
|
||||||
# If the value is a dict with '_cls' in it, turn it into a document
|
EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
|
||||||
is_dict = isinstance(value, dict)
|
|
||||||
if is_dict and '_cls' in value:
|
is_list = False
|
||||||
|
if not hasattr(value, 'items'):
|
||||||
|
is_list = True
|
||||||
|
value = dict([(k, v) for k, v in enumerate(value)])
|
||||||
|
|
||||||
|
if not is_list and '_cls' in value:
|
||||||
cls = get_document(value['_cls'])
|
cls = get_document(value['_cls'])
|
||||||
return cls(**value)
|
return cls(**value)
|
||||||
|
|
||||||
if is_dict:
|
data = {}
|
||||||
value = {
|
for k, v in value.items():
|
||||||
k: self.__expand_dynamic_values(k, v)
|
key = name if is_list else k
|
||||||
for k, v in value.items()
|
data[k] = self.__expand_dynamic_values(key, v)
|
||||||
}
|
|
||||||
|
if is_list: # Convert back to a list
|
||||||
|
data_items = sorted(data.items(), key=operator.itemgetter(0))
|
||||||
|
value = [v for k, v in data_items]
|
||||||
else:
|
else:
|
||||||
value = [self.__expand_dynamic_values(name, v) for v in value]
|
value = data
|
||||||
|
|
||||||
# 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):
|
||||||
@@ -449,7 +465,8 @@ class BaseDocument(object):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def _mark_as_changed(self, key):
|
def _mark_as_changed(self, key):
|
||||||
"""Mark a key as explicitly changed by the user."""
|
"""Marks a key as explicitly changed by the user
|
||||||
|
"""
|
||||||
if not key:
|
if not key:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -479,11 +496,10 @@ 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
|
"""Using get_changed_fields iterate and remove any fields that are
|
||||||
are marked as changed.
|
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):
|
||||||
@@ -495,13 +511,10 @@ 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, '_changed_fields'):
|
if hasattr(data, "_is_document") and data._is_document:
|
||||||
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):
|
||||||
@@ -513,27 +526,26 @@ 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):
|
||||||
"""Return a list of all fields that have explicitly been changed.
|
"""Returns 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', [])
|
||||||
|
|
||||||
@@ -560,7 +572,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
|
||||||
@@ -664,33 +676,21 @@ class BaseDocument(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_collection_name(cls):
|
def _get_collection_name(cls):
|
||||||
"""Return the collection name for this class. None for abstract
|
"""Returns the collection name for this class. None for abstract class
|
||||||
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
|
"""Create an instance of a Document (subclass) from a PyMongo SON.
|
||||||
SON.
|
|
||||||
"""
|
"""
|
||||||
if not only_fields:
|
if not only_fields:
|
||||||
only_fields = []
|
only_fields = []
|
||||||
|
|
||||||
if son and not isinstance(son, dict):
|
# get the class name from the document, falling back to the given
|
||||||
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 data dict, making sure each key is a string and
|
|
||||||
# corresponds to the right db field.
|
|
||||||
data = {}
|
|
||||||
for key, value in son.iteritems():
|
|
||||||
key = str(key)
|
|
||||||
key = cls._db_field_map.get(key, key)
|
|
||||||
data[key] = value
|
|
||||||
|
|
||||||
# Return correct subclass for document type
|
# Return correct subclass for document type
|
||||||
if class_name != cls._class_name:
|
if class_name != cls._class_name:
|
||||||
@@ -712,20 +712,19 @@ 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) as e:
|
except (AttributeError, ValueError), 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 = {k: v for k, v in data.iteritems() if k in cls._fields}
|
data = dict((k, v)
|
||||||
|
for k, v in data.iteritems() if k in cls._fields)
|
||||||
obj = cls(__auto_convert=False, _created=created, __only_fields=only_fields, **data)
|
obj = 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:
|
||||||
@@ -735,43 +734,37 @@ 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) for spec in meta_indexes]
|
index_specs = [cls._build_index_spec(spec)
|
||||||
|
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
|
||||||
|
|
||||||
# Create a map of index fields to index spec. We're converting
|
spec_fields = [v['fields']
|
||||||
# the fields from a list to a tuple so that it's hashable.
|
for k, v in enumerate(index_specs)]
|
||||||
spec_fields = {
|
# Merge unique_indexes with existing specs
|
||||||
tuple(index['fields']): index for index in index_specs
|
for k, v in enumerate(indices):
|
||||||
}
|
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:
|
||||||
candidate.update(new_index)
|
index_specs.append(v)
|
||||||
|
|
||||||
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)}
|
||||||
@@ -782,7 +775,8 @@ 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
|
||||||
@@ -792,7 +786,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
|
||||||
@@ -807,19 +801,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
|
||||||
@@ -832,7 +826,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
|
||||||
@@ -851,53 +845,49 @@ 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, six.string_types):
|
if isinstance(field.unique_with, basestring):
|
||||||
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 = [
|
fields = [("%s%s" % (namespace, f), pymongo.ASCENDING)
|
||||||
('%s%s' % (namespace, f), pymongo.ASCENDING)
|
for f in unique_fields]
|
||||||
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)
|
||||||
|
|
||||||
@@ -909,9 +899,8 @@ 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',
|
"PointField", "LineStringField", "PolygonField"]
|
||||||
'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])
|
||||||
@@ -919,68 +908,32 @@ 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({
|
geo_indices.append({'fields':
|
||||||
'fields': [(field_name, field._geo_index)]
|
[(field_name, field._geo_index)]})
|
||||||
})
|
|
||||||
|
|
||||||
return geo_indices
|
return geo_indices
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _lookup_field(cls, parts):
|
def _lookup_field(cls, parts):
|
||||||
"""Given the path to a given field, return a list containing
|
"""Lookup a field based on its attribute and return a list containing
|
||||||
the Field object associated with that field and all of its parent
|
the field's parents and the field.
|
||||||
Field objects.
|
|
||||||
|
|
||||||
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')
|
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
|
||||||
|
|
||||||
@@ -990,17 +943,16 @@ 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') or cls._meta.get('abstract', False):
|
elif cls._meta.get("allow_inheritance", False) 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:
|
||||||
@@ -1013,55 +965,35 @@ 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"' % field_name)
|
raise LookUpError('Cannot resolve field "%s"'
|
||||||
|
% 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:
|
||||||
raise LookUpError(
|
# Look up subfield on the previous field or raise
|
||||||
'Cannot resolve subfield or operator {} '
|
try:
|
||||||
'on the field {}'.format(field_name, field.name)
|
new_field = field.lookup_member(field_name)
|
||||||
)
|
except AttributeError:
|
||||||
|
raise LookUpError('Cannot resolve subfield or operator {} '
|
||||||
# If current field still wasn't found and the parent field
|
'on the field {}'.format(
|
||||||
# is a ComplexBaseField, add the name current field name and
|
field_name, field.name))
|
||||||
# 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"' % field_name)
|
raise LookUpError('Cannot resolve field "%s"'
|
||||||
|
% 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
|
||||||
@@ -1073,18 +1005,19 @@ class BaseDocument(object):
|
|||||||
return '.'.join(parts)
|
return '.'.join(parts)
|
||||||
|
|
||||||
def __set_field_display(self):
|
def __set_field_display(self):
|
||||||
"""For each field that specifies choices, create a
|
"""Dynamically set the display value for a field with choices"""
|
||||||
get_<field>_display method.
|
for attr_name, field in self._fields.items():
|
||||||
"""
|
if field.choices:
|
||||||
fields_with_choices = [(n, f) for n, f in self._fields.items()
|
if self._dynamic:
|
||||||
if f.choices]
|
obj = self
|
||||||
for attr_name, field in fields_with_choices:
|
else:
|
||||||
setattr(self,
|
obj = type(self)
|
||||||
'get_%s_display' % attr_name,
|
setattr(obj,
|
||||||
partial(self.__get_field_display, field=field))
|
'get_%s_display' % attr_name,
|
||||||
|
partial(self.__get_field_display, field=field))
|
||||||
|
|
||||||
def __get_field_display(self, field):
|
def __get_field_display(self, field):
|
||||||
"""Return the display value for a choice field"""
|
"""Returns 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,17 +4,21 @@ 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 UPDATE_OPERATORS
|
from mongoengine.base.common import ALLOW_INHERITANCE
|
||||||
from mongoengine.base.datastructures import (BaseDict, BaseList,
|
from mongoengine.base.datastructures import (
|
||||||
EmbeddedDocumentList)
|
BaseDict, BaseList, 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',
|
|
||||||
'GeoJsonBaseField')
|
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push',
|
||||||
|
'push_all', 'pull', 'pull_all', 'add_to_set',
|
||||||
|
'set_on_insert', 'min', 'max'])
|
||||||
|
|
||||||
|
|
||||||
class BaseField(object):
|
class BaseField(object):
|
||||||
@@ -23,6 +27,7 @@ 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
|
||||||
@@ -41,7 +46,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: Deprecated - use db_field
|
:param name: Depreciated - 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
|
||||||
@@ -68,7 +73,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 = 'Field\'s "name" attribute deprecated in favour of "db_field"'
|
msg = "Fields' '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
|
||||||
@@ -81,21 +86,10 @@ 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
|
||||||
@@ -153,21 +147,25 @@ 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):
|
||||||
"""Raise a ValidationError."""
|
"""Raises 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):
|
||||||
"""Helper method to call to_mongo with proper inputs."""
|
"""A 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:
|
||||||
@@ -179,13 +177,15 @@ 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,21 +193,18 @@ class BaseField(object):
|
|||||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
EmbeddedDocument = _import_class('EmbeddedDocument')
|
||||||
|
|
||||||
choice_list = self.choices
|
choice_list = self.choices
|
||||||
if isinstance(next(iter(choice_list)), (list, tuple)):
|
if isinstance(choice_list[0], (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 an instance of %s' % (
|
'Value must be instance of %s' % unicode(choice_list)
|
||||||
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' % six.text_type(choice_list))
|
self.error('Value must be one of %s' % unicode(choice_list))
|
||||||
|
|
||||||
def _validate(self, value, **kwargs):
|
def _validate(self, value, **kwargs):
|
||||||
# Check the Choices Constraint
|
# Check the Choices Constraint
|
||||||
@@ -250,7 +247,8 @@ 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
|
||||||
@@ -262,7 +260,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):
|
||||||
@@ -297,8 +295,9 @@ 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'):
|
||||||
@@ -308,14 +307,14 @@ class ComplexBaseField(BaseField):
|
|||||||
if not hasattr(value, 'items'):
|
if not hasattr(value, 'items'):
|
||||||
try:
|
try:
|
||||||
is_list = True
|
is_list = True
|
||||||
value = {k: v for k, v in enumerate(value)}
|
value = dict([(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 = {key: self.field.to_python(item)
|
value_dict = 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 = {}
|
||||||
@@ -338,12 +337,13 @@ 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')
|
"""
|
||||||
EmbeddedDocument = _import_class('EmbeddedDocument')
|
Document = _import_class("Document")
|
||||||
GenericReferenceField = _import_class('GenericReferenceField')
|
EmbeddedDocument = _import_class("EmbeddedDocument")
|
||||||
|
GenericReferenceField = _import_class("GenericReferenceField")
|
||||||
|
|
||||||
if isinstance(value, six.string_types):
|
if isinstance(value, basestring):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if hasattr(value, 'to_mongo'):
|
if hasattr(value, 'to_mongo'):
|
||||||
@@ -360,15 +360,13 @@ class ComplexBaseField(BaseField):
|
|||||||
if not hasattr(value, 'items'):
|
if not hasattr(value, 'items'):
|
||||||
try:
|
try:
|
||||||
is_list = True
|
is_list = True
|
||||||
value = {k: v for k, v in enumerate(value)}
|
value = dict([(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 = {
|
value_dict = dict([(key, self.field._to_mongo_safe_call(item, use_db_field, fields))
|
||||||
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
|
for key, item in value.iteritems()])
|
||||||
for key, item in value.iteritems()
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
value_dict = {}
|
value_dict = {}
|
||||||
for k, v in value.iteritems():
|
for k, v in value.iteritems():
|
||||||
@@ -382,7 +380,9 @@ 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 = meta.get('allow_inheritance')
|
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,7 +404,8 @@ 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'):
|
||||||
@@ -414,9 +415,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 as error:
|
except ValidationError, error:
|
||||||
errors[k] = error.errors or error
|
errors[k] = error.errors or error
|
||||||
except (ValueError, AssertionError) as error:
|
except (ValueError, AssertionError), error:
|
||||||
errors[k] = error
|
errors[k] = error
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
@@ -442,7 +443,8 @@ 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:
|
||||||
@@ -455,10 +457,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(six.text_type(value))
|
return ObjectId(unicode(value))
|
||||||
except Exception as e:
|
except Exception, e:
|
||||||
# e.message attribute has been deprecated since Python 2.6
|
# e.message attribute has been deprecated since Python 2.6
|
||||||
self.error(six.text_type(e))
|
self.error(unicode(e))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def prepare_query_value(self, op, value):
|
def prepare_query_value(self, op, value):
|
||||||
@@ -466,7 +468,7 @@ class ObjectIdField(BaseField):
|
|||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
try:
|
try:
|
||||||
ObjectId(six.text_type(value))
|
ObjectId(unicode(value))
|
||||||
except Exception:
|
except Exception:
|
||||||
self.error('Invalid Object ID')
|
self.error('Invalid Object ID')
|
||||||
|
|
||||||
@@ -478,20 +480,21 @@ 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:
|
||||||
@@ -506,7 +509,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)
|
||||||
@@ -519,7 +522,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:
|
||||||
@@ -530,12 +533,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):
|
||||||
"""Validate a linestring."""
|
"""Validates 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'
|
||||||
|
|
||||||
@@ -543,7 +546,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:
|
||||||
@@ -552,19 +555,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)):
|
||||||
@@ -574,7 +577,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:
|
||||||
@@ -583,7 +586,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)):
|
||||||
@@ -593,7 +596,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:
|
||||||
@@ -603,9 +606,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)):
|
||||||
@@ -615,7 +618,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:
|
||||||
@@ -624,9 +627,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,11 +1,10 @@
|
|||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import six
|
from mongoengine.base.common import ALLOW_INHERITANCE, _document_registry
|
||||||
|
|
||||||
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)
|
||||||
@@ -46,8 +45,7 @@ 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 allow_inheritance is True, add a "_cls" string field to the attrs
|
if attrs['_meta'].get('allow_inheritance', ALLOW_INHERITANCE):
|
||||||
if attrs['_meta'].get('allow_inheritance'):
|
|
||||||
StringField = _import_class('StringField')
|
StringField = _import_class('StringField')
|
||||||
attrs['_cls'] = StringField()
|
attrs['_cls'] = StringField()
|
||||||
|
|
||||||
@@ -89,17 +87,16 @@ 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'] = {k: getattr(v, 'db_field', k)
|
attrs['_db_field_map'] = dict([(k, getattr(v, 'db_field', k))
|
||||||
for k, v in doc_fields.items()}
|
for k, v in doc_fields.iteritems()])
|
||||||
attrs['_reverse_db_field_map'] = {
|
attrs['_reverse_db_field_map'] = dict(
|
||||||
v: k for k, v in attrs['_db_field_map'].items()
|
(v, k) for k, v in attrs['_db_field_map'].iteritems())
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -119,8 +116,10 @@ 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',
|
||||||
if not allow_inheritance and not base._meta.get('abstract'):
|
ALLOW_INHERITANCE)
|
||||||
|
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__)
|
||||||
|
|
||||||
@@ -162,7 +161,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 six.PY3:
|
if 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)
|
||||||
@@ -180,11 +179,11 @@ class DocumentMetaclass(type):
|
|||||||
if isinstance(f, CachedReferenceField):
|
if isinstance(f, CachedReferenceField):
|
||||||
|
|
||||||
if issubclass(new_class, EmbeddedDocument):
|
if issubclass(new_class, EmbeddedDocument):
|
||||||
raise InvalidDocumentError('CachedReferenceFields is not '
|
raise InvalidDocumentError(
|
||||||
'allowed in EmbeddedDocuments')
|
"CachedReferenceFields is not 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()
|
||||||
@@ -196,8 +195,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)
|
||||||
|
|
||||||
@@ -205,16 +204,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
|
||||||
@@ -272,11 +271,6 @@ 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
|
||||||
@@ -309,7 +303,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']
|
||||||
|
|
||||||
@@ -317,7 +311,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)
|
||||||
|
|
||||||
@@ -340,16 +334,12 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
|||||||
|
|
||||||
meta.merge(attrs.get('_meta', {})) # Top level meta
|
meta.merge(attrs.get('_meta', {})) # Top level meta
|
||||||
|
|
||||||
# Only simple classes (i.e. direct subclasses of Document) may set
|
# Only simple classes (direct subclasses of Document)
|
||||||
# allow_inheritance to False. If the base Document allows inheritance,
|
# may set allow_inheritance to False
|
||||||
# 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 (
|
if (not simple_class and meta['allow_inheritance'] is False and
|
||||||
not simple_class and
|
not meta['abstract']):
|
||||||
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,10 +34,7 @@ def _import_class(cls_name):
|
|||||||
queryset_classes = ('OperationError',)
|
queryset_classes = ('OperationError',)
|
||||||
deref_classes = ('DeReference',)
|
deref_classes = ('DeReference',)
|
||||||
|
|
||||||
if cls_name == 'BaseDocument':
|
if cls_name in doc_classes:
|
||||||
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,14 +1,11 @@
|
|||||||
from pymongo import MongoClient, ReadPreference, uri_parser
|
from pymongo import MongoClient, ReadPreference, uri_parser
|
||||||
import six
|
from mongoengine.python_support import (IS_PYMONGO_3, str_types)
|
||||||
|
|
||||||
from mongoengine.python_support import IS_PYMONGO_3
|
__all__ = ['ConnectionError', 'connect', 'register_connection',
|
||||||
|
|
||||||
__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:
|
||||||
@@ -16,10 +13,7 @@ else:
|
|||||||
READ_PREFERENCE = False
|
READ_PREFERENCE = False
|
||||||
|
|
||||||
|
|
||||||
class MongoEngineConnectionError(Exception):
|
class ConnectionError(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
|
||||||
|
|
||||||
|
|
||||||
@@ -30,9 +24,7 @@ _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,
|
username=None, password=None, authentication_source=None,
|
||||||
authentication_source=None,
|
|
||||||
authentication_mechanism=None,
|
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Add a connection.
|
"""Add a connection.
|
||||||
|
|
||||||
@@ -46,17 +38,14 @@ 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: ad-hoc parameters to be passed into the pymongo driver,
|
:param kwargs: allow 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',
|
||||||
@@ -64,45 +53,35 @@ 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
|
||||||
# Host can be a list or a string, so if string, force to a list.
|
if isinstance(conn_host, str_types):
|
||||||
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({
|
||||||
if uri_dict.get('database'):
|
'name': uri_dict.get('database') or name,
|
||||||
conn_settings['name'] = uri_dict.get('database')
|
'username': uri_dict.get('username'),
|
||||||
|
'password': uri_dict.get('password'),
|
||||||
for param in ('read_preference', 'username', 'password'):
|
'read_preference': read_preference,
|
||||||
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'] = uri_options['replicaset']
|
conn_settings['replicaSet'] = True
|
||||||
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
|
||||||
@@ -116,7 +95,9 @@ def register_connection(alias, name=None, host=None, port=None,
|
|||||||
|
|
||||||
|
|
||||||
def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
||||||
"""Close the connection with a given alias."""
|
global _connections
|
||||||
|
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]
|
||||||
@@ -125,99 +106,69 @@ def disconnect(alias=DEFAULT_CONNECTION_NAME):
|
|||||||
|
|
||||||
|
|
||||||
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
|
||||||
"""Return a connection with a given alias."""
|
global _connections
|
||||||
|
|
||||||
# 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 the requested alias already exists in the _connections list, return
|
if alias not in _connections:
|
||||||
# it immediately.
|
if alias not in _connection_settings:
|
||||||
if alias in _connections:
|
|
||||||
return _connections[alias]
|
|
||||||
|
|
||||||
# Validate that the requested alias exists in the _connection_settings.
|
|
||||||
# Raise MongoEngineConnectionError if it doesn't.
|
|
||||||
if alias not in _connection_settings:
|
|
||||||
if alias == DEFAULT_CONNECTION_NAME:
|
|
||||||
msg = 'You have not defined a default connection'
|
|
||||||
else:
|
|
||||||
msg = 'Connection with alias "%s" has not been defined' % alias
|
msg = 'Connection with alias "%s" has not been defined' % alias
|
||||||
raise MongoEngineConnectionError(msg)
|
if alias == DEFAULT_CONNECTION_NAME:
|
||||||
|
msg = 'You have not defined a default connection'
|
||||||
|
raise ConnectionError(msg)
|
||||||
|
conn_settings = _connection_settings[alias].copy()
|
||||||
|
|
||||||
def _clean_settings(settings_dict):
|
conn_settings.pop('name', None)
|
||||||
irrelevant_fields = set([
|
conn_settings.pop('username', None)
|
||||||
'name', 'username', 'password', 'authentication_source',
|
conn_settings.pop('password', None)
|
||||||
'authentication_mechanism'
|
conn_settings.pop('authentication_source', None)
|
||||||
])
|
|
||||||
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
|
is_mock = conn_settings.pop('is_mock', None)
|
||||||
# alias and remove the database name and authentication info (we don't
|
if is_mock:
|
||||||
# care about them at this point).
|
# Use MongoClient from mongomock
|
||||||
conn_settings = _clean_settings(_connection_settings[alias].copy())
|
try:
|
||||||
|
import mongomock
|
||||||
# Determine if we should use PyMongo's or mongomock's MongoClient.
|
except ImportError:
|
||||||
is_mock = conn_settings.pop('is_mock', False)
|
raise RuntimeError('You need mongomock installed '
|
||||||
if is_mock:
|
'to mock MongoEngine.')
|
||||||
try:
|
connection_class = mongomock.MongoClient
|
||||||
import mongomock
|
else:
|
||||||
except ImportError:
|
# Use MongoClient from pymongo
|
||||||
raise RuntimeError('You need mongomock installed to mock '
|
connection_class = MongoClient
|
||||||
'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:
|
||||||
_connections[alias] = connection_class(**conn_settings)
|
connection = None
|
||||||
except Exception as e:
|
# check for shared connections
|
||||||
raise MongoEngineConnectionError(
|
connection_settings_iterator = (
|
||||||
'Cannot connect to database %s :\n%s' % (alias, e))
|
(db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems())
|
||||||
|
for db_alias, connection_settings in connection_settings_iterator:
|
||||||
|
connection_settings.pop('name', None)
|
||||||
|
connection_settings.pop('username', None)
|
||||||
|
connection_settings.pop('password', None)
|
||||||
|
connection_settings.pop('authentication_source', None)
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -225,13 +176,11 @@ 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'] or
|
if conn_settings['username'] and conn_settings['password']:
|
||||||
conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
|
db.authenticate(conn_settings['username'],
|
||||||
db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
|
conn_settings['password'],
|
||||||
|
source=conn_settings['authentication_source'])
|
||||||
_dbs[alias] = db
|
_dbs[alias] = db
|
||||||
return _dbs[alias]
|
return _dbs[alias]
|
||||||
|
|
||||||
@@ -243,14 +192,12 @@ 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,14 +18,15 @@ 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
|
||||||
@@ -33,36 +34,37 @@ 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
|
||||||
@@ -73,7 +75,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):
|
||||||
@@ -84,23 +86,24 @@ 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
|
||||||
"""
|
"""
|
||||||
@@ -116,102 +119,103 @@ 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,13 +1,14 @@
|
|||||||
from collections import OrderedDict
|
|
||||||
from bson import DBRef, SON
|
from bson import DBRef, SON
|
||||||
import six
|
|
||||||
|
|
||||||
from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
|
from .base import (
|
||||||
TopLevelDocumentMetaclass, get_document)
|
BaseDict, BaseList, EmbeddedDocumentList,
|
||||||
from mongoengine.connection import get_db
|
TopLevelDocumentMetaclass, get_document
|
||||||
from mongoengine.document import Document, EmbeddedDocument
|
)
|
||||||
from mongoengine.fields import DictField, ListField, MapField, ReferenceField
|
from .connection import get_db
|
||||||
from mongoengine.queryset import QuerySet
|
from .document import Document, EmbeddedDocument
|
||||||
|
from .fields import DictField, ListField, MapField, ReferenceField
|
||||||
|
from .python_support import txt_type
|
||||||
|
from .queryset import QuerySet
|
||||||
|
|
||||||
|
|
||||||
class DeReference(object):
|
class DeReference(object):
|
||||||
@@ -24,7 +25,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, six.string_types):
|
if items is None or isinstance(items, basestring):
|
||||||
return items
|
return items
|
||||||
|
|
||||||
# cheapest way to convert a queryset to a list
|
# cheapest way to convert a queryset to a list
|
||||||
@@ -67,11 +68,11 @@ class DeReference(object):
|
|||||||
|
|
||||||
items = _get_items(items)
|
items = _get_items(items)
|
||||||
else:
|
else:
|
||||||
items = {
|
items = dict([
|
||||||
k: (v if isinstance(v, (DBRef, Document))
|
(k, field.to_python(v))
|
||||||
else field.to_python(v))
|
if not isinstance(v, (DBRef, Document)) else (k, 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)
|
||||||
@@ -89,14 +90,14 @@ class DeReference(object):
|
|||||||
return reference_map
|
return reference_map
|
||||||
|
|
||||||
# Determine the iterator to use
|
# Determine the iterator to use
|
||||||
if isinstance(items, dict):
|
if not hasattr(items, 'items'):
|
||||||
iterator = items.values()
|
iterator = enumerate(items)
|
||||||
else:
|
else:
|
||||||
iterator = items
|
iterator = items.iteritems()
|
||||||
|
|
||||||
# Recursively find dbreferences
|
# Recursively find dbreferences
|
||||||
depth += 1
|
depth += 1
|
||||||
for item in iterator:
|
for k, 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)
|
||||||
@@ -150,7 +151,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()
|
||||||
@@ -202,10 +203,6 @@ class DeReference(object):
|
|||||||
as_tuple = isinstance(items, tuple)
|
as_tuple = isinstance(items, tuple)
|
||||||
iterator = enumerate(items)
|
iterator = enumerate(items)
|
||||||
data = []
|
data = []
|
||||||
elif isinstance(items, OrderedDict):
|
|
||||||
is_list = False
|
|
||||||
iterator = items.iteritems()
|
|
||||||
data = OrderedDict()
|
|
||||||
else:
|
else:
|
||||||
is_list = False
|
is_list = False
|
||||||
iterator = items.iteritems()
|
iterator = items.iteritems()
|
||||||
@@ -221,7 +218,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 in v._fields:
|
for field_name, field in v._fields.iteritems():
|
||||||
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(
|
||||||
@@ -230,7 +227,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 = six.text_type('{0}.{1}.{2}').format(name, k, field_name)
|
item_name = txt_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,12 +4,18 @@ 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 (BaseDict, BaseDocument, BaseList,
|
from mongoengine.base import (
|
||||||
DocumentMetaclass, EmbeddedDocumentList,
|
ALLOW_INHERITANCE,
|
||||||
TopLevelDocumentMetaclass, get_document)
|
BaseDict,
|
||||||
|
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
|
||||||
@@ -25,10 +31,12 @@ __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], six.string_types):
|
if isinstance(fields[0], basestring):
|
||||||
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]
|
||||||
@@ -49,8 +57,9 @@ 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 enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
|
To disable this behaviour and remove the dependence on the presence of
|
||||||
:attr:`meta` dictionary.
|
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
||||||
|
dictionary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ('_instance', )
|
__slots__ = ('_instance', )
|
||||||
@@ -73,15 +82,6 @@ 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,8 +106,9 @@ 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 enable this behaviourset :attr:`allow_inheritance` to ``True`` in the
|
To disable this behaviour and remove the dependence on the presence of
|
||||||
:attr:`meta` dictionary.
|
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta`
|
||||||
|
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`
|
||||||
@@ -148,96 +149,69 @@ class Document(BaseDocument):
|
|||||||
|
|
||||||
__slots__ = ('__objects',)
|
__slots__ = ('__objects',)
|
||||||
|
|
||||||
@property
|
def pk():
|
||||||
def pk(self):
|
"""Primary key alias
|
||||||
"""Get the primary key."""
|
"""
|
||||||
if 'id_field' not in self._meta:
|
|
||||||
return None
|
|
||||||
return getattr(self, self._meta['id_field'])
|
|
||||||
|
|
||||||
@pk.setter
|
def fget(self):
|
||||||
def pk(self, value):
|
if 'id_field' not in self._meta:
|
||||||
"""Set the primary key."""
|
return None
|
||||||
return setattr(self, self._meta['id_field'], value)
|
return getattr(self, self._meta['id_field'])
|
||||||
|
|
||||||
|
def fset(self, value):
|
||||||
|
return setattr(self, self._meta['id_field'], value)
|
||||||
|
|
||||||
|
return property(fget, fset)
|
||||||
|
|
||||||
|
pk = pk()
|
||||||
|
|
||||||
@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):
|
||||||
"""Return a PyMongo collection for the document."""
|
"""Returns the collection for the document."""
|
||||||
|
# TODO: use new get_collection() with PyMongo3 ?
|
||||||
if not hasattr(cls, '_collection') or cls._collection is None:
|
if not hasattr(cls, '_collection') or cls._collection is None:
|
||||||
|
db = cls._get_db()
|
||||||
# Get the collection, either capped or regular.
|
collection_name = cls._get_collection_name()
|
||||||
|
# Create collection as a capped collection if specified
|
||||||
if cls._meta.get('max_size') or cls._meta.get('max_documents'):
|
if cls._meta.get('max_size') or cls._meta.get('max_documents'):
|
||||||
cls._collection = cls._get_capped_collection()
|
# Get max document limit and max byte size from meta
|
||||||
else:
|
max_size = cls._meta.get('max_size') or 10 * 2 ** 20 # 10MB default
|
||||||
db = cls._get_db()
|
max_documents = cls._meta.get('max_documents')
|
||||||
collection_name = cls._get_collection_name()
|
# Round up to next 256 bytes as MongoDB would do it to avoid exception
|
||||||
cls._collection = db[collection_name]
|
if max_size % 256:
|
||||||
|
max_size = (max_size // 256 + 1) * 256
|
||||||
|
|
||||||
# Ensure indexes on the collection unless auto_create_index was
|
if collection_name in db.collection_names():
|
||||||
# set to False.
|
cls._collection = db[collection_name]
|
||||||
|
# The collection already exists, check if its capped
|
||||||
|
# options match the specified capped options
|
||||||
|
options = cls._collection.options()
|
||||||
|
if options.get('max') != max_documents or \
|
||||||
|
options.get('size') != max_size:
|
||||||
|
msg = (('Cannot create collection "%s" as a capped '
|
||||||
|
'collection as it already exists')
|
||||||
|
% cls._collection)
|
||||||
|
raise InvalidCollectionError(msg)
|
||||||
|
else:
|
||||||
|
# Create the collection as a capped collection
|
||||||
|
opts = {'capped': True, 'size': max_size}
|
||||||
|
if max_documents:
|
||||||
|
opts['max'] = max_documents
|
||||||
|
cls._collection = db.create_collection(
|
||||||
|
collection_name, **opts
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
cls._collection = db[collection_name]
|
||||||
if cls._meta.get('auto_create_index', True):
|
if cls._meta.get('auto_create_index', True):
|
||||||
cls.ensure_indexes()
|
cls.ensure_indexes()
|
||||||
|
|
||||||
return cls._collection
|
return cls._collection
|
||||||
|
|
||||||
@classmethod
|
def modify(self, query={}, **update):
|
||||||
def _get_capped_collection(cls):
|
|
||||||
"""Create a new or get an existing capped PyMongo collection."""
|
|
||||||
db = cls._get_db()
|
|
||||||
collection_name = cls._get_collection_name()
|
|
||||||
|
|
||||||
# Get max document limit and max byte size from meta.
|
|
||||||
max_size = cls._meta.get('max_size') or 10 * 2 ** 20 # 10MB default
|
|
||||||
max_documents = cls._meta.get('max_documents')
|
|
||||||
|
|
||||||
# MongoDB will automatically raise the size to make it a multiple of
|
|
||||||
# 256 bytes. We raise it here ourselves to be able to reliably compare
|
|
||||||
# the options below.
|
|
||||||
if max_size % 256:
|
|
||||||
max_size = (max_size // 256 + 1) * 256
|
|
||||||
|
|
||||||
# If the collection already exists and has different options
|
|
||||||
# (i.e. isn't capped or has different max/size), raise an error.
|
|
||||||
if collection_name in db.collection_names():
|
|
||||||
collection = db[collection_name]
|
|
||||||
options = collection.options()
|
|
||||||
if (
|
|
||||||
options.get('max') != max_documents or
|
|
||||||
options.get('size') != max_size
|
|
||||||
):
|
|
||||||
raise InvalidCollectionError(
|
|
||||||
'Cannot create collection "{}" as a capped '
|
|
||||||
'collection as it already exists'.format(cls._collection)
|
|
||||||
)
|
|
||||||
|
|
||||||
return collection
|
|
||||||
|
|
||||||
# Create a new capped collection.
|
|
||||||
opts = {'capped': True, 'size': max_size}
|
|
||||||
if max_documents:
|
|
||||||
opts['max'] = max_documents
|
|
||||||
|
|
||||||
return db.create_collection(collection_name, **opts)
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -251,19 +225,17 @@ 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:
|
||||||
@@ -331,9 +303,6 @@ 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)
|
||||||
|
|
||||||
@@ -341,7 +310,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()
|
||||||
|
|
||||||
@@ -350,135 +319,105 @@ 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:
|
||||||
# Save a new document or update an existing one
|
collection = self._get_collection()
|
||||||
|
if self._meta.get('auto_create_index', True):
|
||||||
|
self.ensure_indexes()
|
||||||
if created:
|
if created:
|
||||||
object_id = self._save_create(doc, force_insert, write_concern)
|
if force_insert:
|
||||||
|
object_id = collection.insert(doc, **write_concern)
|
||||||
|
else:
|
||||||
|
object_id = collection.save(doc, **write_concern)
|
||||||
|
# In PyMongo 3.0, the save() call calls internally the _update() call
|
||||||
|
# but they forget to return the _id value passed back, therefore getting it back here
|
||||||
|
# Correct behaviour in 2.X and in 3.0.1+ versions
|
||||||
|
if not object_id and pymongo.version_tuple == (3, 0):
|
||||||
|
pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk)
|
||||||
|
object_id = (
|
||||||
|
self._qs.filter(pk=pk_as_mongo_obj).first() and
|
||||||
|
self._qs.filter(pk=pk_as_mongo_obj).first().pk
|
||||||
|
) # TODO doesn't this make 2 queries?
|
||||||
else:
|
else:
|
||||||
object_id, created = self._save_update(doc, save_condition,
|
object_id = doc['_id']
|
||||||
write_concern)
|
updates, removals = self._delta()
|
||||||
|
# Need to add shard key to query, or you get an error
|
||||||
|
if save_condition is not None:
|
||||||
|
select_dict = transform.query(self.__class__,
|
||||||
|
**save_condition)
|
||||||
|
else:
|
||||||
|
select_dict = {}
|
||||||
|
select_dict['_id'] = object_id
|
||||||
|
shard_key = self.__class__._meta.get('shard_key', tuple())
|
||||||
|
for k in shard_key:
|
||||||
|
path = self._lookup_field(k.split('.'))
|
||||||
|
actual_key = [p.db_field for p in path]
|
||||||
|
val = doc
|
||||||
|
for ak in actual_key:
|
||||||
|
val = val[ak]
|
||||||
|
select_dict['.'.join(actual_key)] = val
|
||||||
|
|
||||||
|
def is_new_object(last_error):
|
||||||
|
if last_error is not None:
|
||||||
|
updated = last_error.get("updatedExisting")
|
||||||
|
if updated is not None:
|
||||||
|
return not updated
|
||||||
|
return created
|
||||||
|
|
||||||
|
update_query = {}
|
||||||
|
|
||||||
|
if updates:
|
||||||
|
update_query["$set"] = updates
|
||||||
|
if removals:
|
||||||
|
update_query["$unset"] = removals
|
||||||
|
if updates or removals:
|
||||||
|
upsert = save_condition is None
|
||||||
|
last_error = collection.update(select_dict, update_query,
|
||||||
|
upsert=upsert, **write_concern)
|
||||||
|
if not upsert and last_error["n"] == 0:
|
||||||
|
raise SaveConditionError('Race condition preventing'
|
||||||
|
' document update detected')
|
||||||
|
created = is_new_object(last_error)
|
||||||
|
|
||||||
if cascade is None:
|
if cascade is None:
|
||||||
cascade = (self._meta.get('cascade', False) or
|
cascade = self._meta.get(
|
||||||
cascade_kwargs is not None)
|
'cascade', False) or 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 % six.text_type(err))
|
raise NotUniqueError(message % unicode(err))
|
||||||
except pymongo.errors.OperationFailure as err:
|
except pymongo.errors.OperationFailure, err:
|
||||||
message = 'Could not save document (%s)'
|
message = 'Could not save document (%s)'
|
||||||
if re.match('^E1100[01] duplicate key', six.text_type(err)):
|
if re.match('^E1100[01] duplicate key', unicode(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 % six.text_type(err))
|
raise NotUniqueError(message % unicode(err))
|
||||||
raise OperationError(message % six.text_type(err))
|
raise OperationError(message % unicode(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 _save_create(self, doc, force_insert, write_concern):
|
def cascade_save(self, *args, **kwargs):
|
||||||
"""Save a new document.
|
"""Recursively saves any references /
|
||||||
|
generic references on the document"""
|
||||||
Helper method, should only be used inside save().
|
_refs = kwargs.get('_refs', []) or []
|
||||||
"""
|
|
||||||
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')
|
||||||
@@ -504,17 +443,16 @@ 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):
|
||||||
"""Get the query dict that can be used to fetch this object from
|
"""Dict to identify object in collection
|
||||||
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())
|
||||||
@@ -534,11 +472,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 self.pk is None:
|
if not self.pk:
|
||||||
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(
|
||||||
@@ -575,7 +513,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 as err:
|
except pymongo.errors.OperationFailure, 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)
|
||||||
@@ -663,12 +601,11 @@ 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)
|
||||||
@@ -676,7 +613,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:
|
||||||
@@ -718,8 +655,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 self.pk is None:
|
if not self.pk:
|
||||||
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)
|
||||||
|
|
||||||
@@ -774,7 +711,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
|
||||||
@@ -800,7 +737,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})
|
||||||
@@ -820,7 +757,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()
|
||||||
@@ -858,7 +795,8 @@ 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 cls._meta.get('allow_inheritance'):
|
if (index_cls and not cls_indexed and
|
||||||
|
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
|
||||||
@@ -877,6 +815,7 @@ 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 []
|
||||||
|
|
||||||
@@ -927,15 +866,16 @@ 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 cls._meta.get('allow_inheritance'):
|
if (cls._meta.get('index_cls', True) and
|
||||||
|
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
|
""" Compares the indexes defined in MongoEngine with the ones existing
|
||||||
existing in the database. Returns any missing/extra indexes.
|
in the database. Returns any missing/extra indexes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required = cls.list_indexes()
|
required = cls.list_indexes()
|
||||||
@@ -979,9 +919,8 @@ class DynamicDocument(Document):
|
|||||||
_dynamic = True
|
_dynamic = True
|
||||||
|
|
||||||
def __delattr__(self, *args, **kwargs):
|
def __delattr__(self, *args, **kwargs):
|
||||||
"""Delete the attribute by setting to None and allowing _delta
|
"""Deletes the attribute by setting to None and allowing _delta to unset
|
||||||
to unset it.
|
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)
|
||||||
@@ -1003,9 +942,8 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
|
|||||||
_dynamic = True
|
_dynamic = True
|
||||||
|
|
||||||
def __delattr__(self, *args, **kwargs):
|
def __delattr__(self, *args, **kwargs):
|
||||||
"""Delete the attribute by setting to None and allowing _delta
|
"""Deletes the attribute by setting to None and allowing _delta to unset
|
||||||
to unset it.
|
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
|
||||||
@@ -1047,10 +985,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,6 +1,7 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import six
|
from mongoengine.python_support import txt_type
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
__all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
|
||||||
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
|
||||||
@@ -50,8 +51,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 set the :attr:`strict` to ``False``
|
you should the :attr:`strict` to ``False``
|
||||||
in the :attr:`meta` dictionary.
|
in the :attr:`meta` dictionnary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@@ -70,13 +71,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 six.text_type(self.message)
|
return txt_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)
|
||||||
@@ -110,20 +111,17 @@ 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 six.text_type(source)
|
return unicode(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):
|
||||||
@@ -136,10 +134,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()])
|
||||||
|
@@ -2,11 +2,10 @@ import datetime
|
|||||||
import decimal
|
import decimal
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
import socket
|
|
||||||
import time
|
import time
|
||||||
|
import urllib2
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Mapping
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from bson import Binary, DBRef, ObjectId, SON
|
from bson import Binary, DBRef, ObjectId, SON
|
||||||
@@ -26,13 +25,13 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
Int64 = long
|
Int64 = long
|
||||||
|
|
||||||
from mongoengine.base import (BaseDocument, BaseField, ComplexBaseField,
|
from .base import (BaseDocument, BaseField, ComplexBaseField, GeoJsonBaseField,
|
||||||
GeoJsonBaseField, ObjectIdField, get_document)
|
ObjectIdField, get_document)
|
||||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
from .connection import DEFAULT_CONNECTION_NAME, get_db
|
||||||
from mongoengine.document import Document, EmbeddedDocument
|
from .document import Document, EmbeddedDocument
|
||||||
from mongoengine.errors import DoesNotExist, InvalidQueryError, ValidationError
|
from .errors import DoesNotExist, ValidationError
|
||||||
from mongoengine.python_support import StringIO
|
from .python_support import PY3, StringIO, bin_type, str_types, txt_type
|
||||||
from mongoengine.queryset import DO_NOTHING, QuerySet
|
from .queryset import DO_NOTHING, QuerySet
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
@@ -40,7 +39,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',
|
||||||
@@ -51,14 +50,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
|
||||||
@@ -67,7 +66,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, six.text_type):
|
if isinstance(value, unicode):
|
||||||
return value
|
return value
|
||||||
try:
|
try:
|
||||||
value = value.decode('utf-8')
|
value = value.decode('utf-8')
|
||||||
@@ -76,7 +75,7 @@ class StringField(BaseField):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
if not isinstance(value, six.string_types):
|
if not isinstance(value, basestring):
|
||||||
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:
|
||||||
@@ -92,7 +91,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, six.string_types):
|
if not isinstance(op, basestring):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
|
if op.lstrip('i') in ('startswith', 'endswith', 'contains', 'exact'):
|
||||||
@@ -141,122 +140,50 @@ 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(u'Invalid scheme {} in URL: {}'.format(scheme, value))
|
self.error('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(u'Invalid URL: {}'.format(value))
|
self.error('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.
|
||||||
|
|
||||||
.. versionadded:: 0.4
|
.. versionadded:: 0.4
|
||||||
"""
|
"""
|
||||||
USER_REGEX = re.compile(
|
|
||||||
# `dot-atom` defined in RFC 5322 Section 3.2.3.
|
EMAIL_REGEX = re.compile(
|
||||||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z"
|
# dot-atom
|
||||||
# `quoted-string` defined in RFC 5322 Section 3.2.4.
|
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*"
|
||||||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)',
|
# quoted-string
|
||||||
re.IGNORECASE
|
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"'
|
||||||
|
# domain (max length of an ICAAN TLD is 22 characters)
|
||||||
|
r')@(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}|[A-Z0-9-]{2,}(?<!-))$', re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
UTF8_USER_REGEX = re.compile(
|
|
||||||
six.u(
|
|
||||||
# RFC 6531 Section 3.3 extends `atext` (used by dot-atom) to
|
|
||||||
# include `UTF8-non-ascii`.
|
|
||||||
r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z\u0080-\U0010FFFF]+)*\Z"
|
|
||||||
# `quoted-string`
|
|
||||||
r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)'
|
|
||||||
), re.IGNORECASE | re.UNICODE
|
|
||||||
)
|
|
||||||
|
|
||||||
DOMAIN_REGEX = re.compile(
|
|
||||||
r'((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+)(?:[A-Z0-9-]{2,63}(?<!-))\Z',
|
|
||||||
re.IGNORECASE
|
|
||||||
)
|
|
||||||
|
|
||||||
error_msg = u'Invalid email address: %s'
|
|
||||||
|
|
||||||
def __init__(self, domain_whitelist=None, allow_utf8_user=False,
|
|
||||||
allow_ip_domain=False, *args, **kwargs):
|
|
||||||
"""Initialize the EmailField.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
domain_whitelist (list) - list of otherwise invalid domain
|
|
||||||
names which you'd like to support.
|
|
||||||
allow_utf8_user (bool) - if True, the user part of the email
|
|
||||||
address can contain UTF8 characters.
|
|
||||||
False by default.
|
|
||||||
allow_ip_domain (bool) - if True, the domain part of the email
|
|
||||||
can be a valid IPv4 or IPv6 address.
|
|
||||||
"""
|
|
||||||
self.domain_whitelist = domain_whitelist or []
|
|
||||||
self.allow_utf8_user = allow_utf8_user
|
|
||||||
self.allow_ip_domain = allow_ip_domain
|
|
||||||
super(EmailField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def validate_user_part(self, user_part):
|
|
||||||
"""Validate the user part of the email address. Return True if
|
|
||||||
valid and False otherwise.
|
|
||||||
"""
|
|
||||||
if self.allow_utf8_user:
|
|
||||||
return self.UTF8_USER_REGEX.match(user_part)
|
|
||||||
return self.USER_REGEX.match(user_part)
|
|
||||||
|
|
||||||
def validate_domain_part(self, domain_part):
|
|
||||||
"""Validate the domain part of the email address. Return True if
|
|
||||||
valid and False otherwise.
|
|
||||||
"""
|
|
||||||
# Skip domain validation if it's in the whitelist.
|
|
||||||
if domain_part in self.domain_whitelist:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.DOMAIN_REGEX.match(domain_part):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Validate IPv4/IPv6, e.g. user@[192.168.0.1]
|
|
||||||
if (
|
|
||||||
self.allow_ip_domain and
|
|
||||||
domain_part[0] == '[' and
|
|
||||||
domain_part[-1] == ']'
|
|
||||||
):
|
|
||||||
for addr_family in (socket.AF_INET, socket.AF_INET6):
|
|
||||||
try:
|
|
||||||
socket.inet_pton(addr_family, domain_part[1:-1])
|
|
||||||
return True
|
|
||||||
except (socket.error, UnicodeEncodeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
if not EmailField.EMAIL_REGEX.match(value):
|
||||||
|
self.error('Invalid email address: %s' % value)
|
||||||
super(EmailField, self).validate(value)
|
super(EmailField, self).validate(value)
|
||||||
|
|
||||||
if '@' not in value:
|
|
||||||
self.error(self.error_msg % value)
|
|
||||||
|
|
||||||
user_part, domain_part = value.rsplit('@', 1)
|
|
||||||
|
|
||||||
# Validate the user part.
|
|
||||||
if not self.validate_user_part(user_part):
|
|
||||||
self.error(self.error_msg % value)
|
|
||||||
|
|
||||||
# Validate the domain and, if invalid, see if it's IDN-encoded.
|
|
||||||
if not self.validate_domain_part(domain_part):
|
|
||||||
try:
|
|
||||||
domain_part = domain_part.encode('idna').decode('ascii')
|
|
||||||
except UnicodeError:
|
|
||||||
self.error(self.error_msg % value)
|
|
||||||
else:
|
|
||||||
if not self.validate_domain_part(domain_part):
|
|
||||||
self.error(self.error_msg % value)
|
|
||||||
|
|
||||||
|
|
||||||
class IntField(BaseField):
|
class IntField(BaseField):
|
||||||
"""32-bit integer field."""
|
"""An 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
|
||||||
@@ -289,7 +216,8 @@ class IntField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class LongField(BaseField):
|
class LongField(BaseField):
|
||||||
"""64-bit integer field."""
|
"""An 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
|
||||||
@@ -325,7 +253,8 @@ class LongField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class FloatField(BaseField):
|
class FloatField(BaseField):
|
||||||
"""Floating point number field."""
|
"""An 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
|
||||||
@@ -362,7 +291,7 @@ class FloatField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class DecimalField(BaseField):
|
class DecimalField(BaseField):
|
||||||
"""Fixed-point decimal number field.
|
"""A fixed-point decimal number field.
|
||||||
|
|
||||||
.. versionchanged:: 0.8
|
.. versionchanged:: 0.8
|
||||||
.. versionadded:: 0.3
|
.. versionadded:: 0.3
|
||||||
@@ -403,25 +332,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 six.text_type(self.to_python(value))
|
return unicode(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, six.string_types):
|
if not isinstance(value, basestring):
|
||||||
value = six.text_type(value)
|
value = unicode(value)
|
||||||
try:
|
try:
|
||||||
value = decimal.Decimal(value)
|
value = decimal.Decimal(value)
|
||||||
except Exception as exc:
|
except Exception, 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:
|
||||||
@@ -435,7 +364,7 @@ class DecimalField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class BooleanField(BaseField):
|
class BooleanField(BaseField):
|
||||||
"""Boolean field type.
|
"""A boolean field type.
|
||||||
|
|
||||||
.. versionadded:: 0.1.2
|
.. versionadded:: 0.1.2
|
||||||
"""
|
"""
|
||||||
@@ -453,7 +382,7 @@ class BooleanField(BaseField):
|
|||||||
|
|
||||||
|
|
||||||
class DateTimeField(BaseField):
|
class DateTimeField(BaseField):
|
||||||
"""Datetime field.
|
"""A 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
|
||||||
@@ -481,7 +410,7 @@ class DateTimeField(BaseField):
|
|||||||
if callable(value):
|
if callable(value):
|
||||||
return value()
|
return value()
|
||||||
|
|
||||||
if not isinstance(value, six.string_types):
|
if not isinstance(value, basestring):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Attempt to parse a datetime:
|
# Attempt to parse a datetime:
|
||||||
@@ -608,19 +537,16 @@ class EmbeddedDocumentField(BaseField):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, document_type, **kwargs):
|
def __init__(self, document_type, **kwargs):
|
||||||
if not (
|
if not isinstance(document_type, basestring):
|
||||||
isinstance(document_type, six.string_types) or
|
if not issubclass(document_type, EmbeddedDocument):
|
||||||
issubclass(document_type, EmbeddedDocument)
|
self.error('Invalid embedded document class provided to an '
|
||||||
):
|
'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, six.string_types):
|
if isinstance(self.document_type_obj, basestring):
|
||||||
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:
|
||||||
@@ -651,12 +577,8 @@ 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 value is not None and not isinstance(value, self.document_type):
|
if not isinstance(value, self.document_type):
|
||||||
try:
|
value = self.document_type._from_son(value)
|
||||||
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)
|
||||||
|
|
||||||
@@ -705,19 +627,11 @@ class DynamicField(BaseField):
|
|||||||
|
|
||||||
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
|
Used by :class:`~mongoengine.DynamicDocument` to handle dynamic data"""
|
||||||
|
|
||||||
def __init__(self, container_class=dict, *args, **kwargs):
|
|
||||||
self._container_cls = container_class
|
|
||||||
if not issubclass(self._container_cls, Mapping):
|
|
||||||
self.error('The class that is specified in `container_class` parameter '
|
|
||||||
'must be a subclass of `dict`.')
|
|
||||||
|
|
||||||
super(DynamicField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def to_mongo(self, value, use_db_field=True, fields=None):
|
def to_mongo(self, value, use_db_field=True, fields=None):
|
||||||
"""Convert a Python type to a MongoDB compatible type.
|
"""Convert a Python type to a MongoDB compatible type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if isinstance(value, six.string_types):
|
if isinstance(value, basestring):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if hasattr(value, 'to_mongo'):
|
if hasattr(value, 'to_mongo'):
|
||||||
@@ -725,7 +639,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
|
||||||
@@ -736,9 +650,9 @@ 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 = {k: v for k, v in enumerate(value)}
|
value = dict([(k, v) for k, v in enumerate(value)])
|
||||||
|
|
||||||
data = self._container_cls()
|
data = {}
|
||||||
for k, v in value.iteritems():
|
for k, v in value.iteritems():
|
||||||
data[k] = self.to_mongo(v, use_db_field, fields)
|
data[k] = self.to_mongo(v, use_db_field, fields)
|
||||||
|
|
||||||
@@ -760,12 +674,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, six.string_types):
|
if isinstance(value, basestring):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -785,27 +699,21 @@ 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, six.string_types)):
|
isinstance(value, basestring)):
|
||||||
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 (
|
||||||
# If the value is iterable and it's not a string nor a
|
not isinstance(value, basestring) and
|
||||||
# BaseDocument, call prepare_query_value for each of its items.
|
not isinstance(value, BaseDocument) and
|
||||||
if (
|
hasattr(value, '__iter__')):
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -818,6 +726,7 @@ 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):
|
||||||
@@ -866,17 +775,17 @@ class SortedListField(ListField):
|
|||||||
|
|
||||||
|
|
||||||
def key_not_string(d):
|
def key_not_string(d):
|
||||||
"""Helper function to recursively determine if any key in a
|
""" Helper function to recursively determine if any key in a dictionary is
|
||||||
dictionary is not a string.
|
not a string.
|
||||||
"""
|
"""
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if not isinstance(k, six.string_types) or (isinstance(v, dict) and key_not_string(v)):
|
if not isinstance(k, basestring) 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
|
""" Helper function to recursively determine if any key in a dictionary
|
||||||
dictionary contains a dot or a dollar sign.
|
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)):
|
||||||
@@ -904,13 +813,14 @@ 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 "."'
|
||||||
@@ -925,15 +835,14 @@ class DictField(ComplexBaseField):
|
|||||||
'istartswith', 'endswith', 'iendswith',
|
'istartswith', 'endswith', 'iendswith',
|
||||||
'exact', 'iexact']
|
'exact', 'iexact']
|
||||||
|
|
||||||
if op in match_operators and isinstance(value, six.string_types):
|
if op in match_operators and isinstance(value, basestring):
|
||||||
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 {
|
return dict(
|
||||||
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)
|
||||||
@@ -982,6 +891,10 @@ 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`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -998,12 +911,10 @@ 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 (
|
if not isinstance(document_type, basestring):
|
||||||
not isinstance(document_type, six.string_types) and
|
if not issubclass(document_type, (Document, basestring)):
|
||||||
not issubclass(document_type, Document)
|
self.error('Argument to ReferenceField constructor must be a '
|
||||||
):
|
'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
|
||||||
@@ -1012,7 +923,7 @@ class ReferenceField(BaseField):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def document_type(self):
|
def document_type(self):
|
||||||
if isinstance(self.document_type_obj, six.string_types):
|
if isinstance(self.document_type_obj, basestring):
|
||||||
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:
|
||||||
@@ -1020,7 +931,8 @@ 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
|
||||||
@@ -1077,7 +989,8 @@ 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()
|
||||||
@@ -1092,8 +1005,8 @@ class ReferenceField(BaseField):
|
|||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
|
|
||||||
if not isinstance(value, (self.document_type, DBRef, ObjectId)):
|
if not isinstance(value, (self.document_type, DBRef)):
|
||||||
self.error('A ReferenceField only accepts DBRef, ObjectId 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 '
|
||||||
@@ -1117,19 +1030,14 @@ class CachedReferenceField(BaseField):
|
|||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, document_type, fields=None, auto_sync=True, **kwargs):
|
def __init__(self, document_type, fields=[], 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 fields is None:
|
if not isinstance(document_type, basestring) and \
|
||||||
fields = []
|
not issubclass(document_type, (Document, basestring)):
|
||||||
|
|
||||||
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')
|
||||||
|
|
||||||
@@ -1145,20 +1053,18 @@ 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 created:
|
if not created:
|
||||||
return None
|
update_kwargs = dict(
|
||||||
|
('set__%s__%s' % (self.name, k), v)
|
||||||
|
for k, v in document._delta()[0].items()
|
||||||
|
if k in self.fields)
|
||||||
|
|
||||||
update_kwargs = {
|
if update_kwargs:
|
||||||
'set__%s__%s' % (self.name, key): val
|
filter_kwargs = {}
|
||||||
for key, val in document._delta()[0].items()
|
filter_kwargs[self.name] = document
|
||||||
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):
|
||||||
@@ -1171,7 +1077,7 @@ class CachedReferenceField(BaseField):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def document_type(self):
|
def document_type(self):
|
||||||
if isinstance(self.document_type_obj, six.string_types):
|
if isinstance(self.document_type_obj, basestring):
|
||||||
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:
|
||||||
@@ -1211,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:
|
||||||
@@ -1237,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 '
|
||||||
@@ -1285,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, six.string_types):
|
if isinstance(choice, basestring):
|
||||||
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 six.string_typess')
|
'Document subclasses and/or basestrings')
|
||||||
|
|
||||||
def _validate_choices(self, value):
|
def _validate_choices(self, value):
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
@@ -1343,7 +1249,7 @@ class GenericReferenceField(BaseField):
|
|||||||
if document is None:
|
if document is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(document, (dict, SON, ObjectId, DBRef)):
|
if isinstance(document, (dict, SON)):
|
||||||
return document
|
return document
|
||||||
|
|
||||||
id_field_name = document.__class__._meta['id_field']
|
id_field_name = document.__class__._meta['id_field']
|
||||||
@@ -1374,7 +1280,8 @@ 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
|
||||||
@@ -1382,18 +1289,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 six.PY3 and isinstance(value, bytearray):
|
if PY3 and isinstance(value, bytearray):
|
||||||
value = six.binary_type(value)
|
value = bin_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, (six.binary_type, six.text_type, Binary)):
|
if not isinstance(value, (bin_type, txt_type, Binary)):
|
||||||
self.error('BinaryField only accepts instances of '
|
self.error("BinaryField only accepts instances of "
|
||||||
'(%s, %s, Binary)' % (
|
"(%s, %s, Binary)" % (
|
||||||
six.binary_type.__name__, six.text_type.__name__))
|
bin_type.__name__, txt_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')
|
||||||
@@ -1477,13 +1384,11 @@ 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, grid_id=None):
|
def get(self, id=None):
|
||||||
if grid_id:
|
if id:
|
||||||
self.grid_id = grid_id
|
self.grid_id = 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)
|
||||||
@@ -1527,7 +1432,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
|
||||||
@@ -1559,8 +1464,9 @@ class FileField(BaseField):
|
|||||||
"""
|
"""
|
||||||
proxy_class = GridFSProxy
|
proxy_class = GridFSProxy
|
||||||
|
|
||||||
def __init__(self, db_alias=DEFAULT_CONNECTION_NAME, collection_name='fs',
|
def __init__(self,
|
||||||
**kwargs):
|
db_alias=DEFAULT_CONNECTION_NAME,
|
||||||
|
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
|
||||||
@@ -1582,10 +1488,8 @@ class FileField(BaseField):
|
|||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
key = self.name
|
key = self.name
|
||||||
if (
|
if ((hasattr(value, 'read') and not
|
||||||
(hasattr(value, 'read') and not isinstance(value, GridFSProxy)) or
|
isinstance(value, GridFSProxy)) or isinstance(value, str_types)):
|
||||||
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
|
||||||
@@ -1654,7 +1558,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 as e:
|
except Exception, e:
|
||||||
raise ValidationError('Invalid image: %s' % e)
|
raise ValidationError('Invalid image: %s' % e)
|
||||||
|
|
||||||
# Progressive JPEG
|
# Progressive JPEG
|
||||||
@@ -1763,10 +1667,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):
|
||||||
@@ -1791,17 +1695,14 @@ 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 = {
|
extra_args = dict(size=size, thumbnail_size=thumbnail_size)
|
||||||
'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 six.PY3:
|
if PY3:
|
||||||
value = dict(itertools.zip_longest(params_size, att,
|
value = dict(itertools.zip_longest(params_size, att,
|
||||||
fillvalue=None))
|
fillvalue=None))
|
||||||
else:
|
else:
|
||||||
@@ -1862,10 +1763,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'])
|
||||||
@@ -1888,9 +1789,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)
|
||||||
@@ -1960,8 +1861,8 @@ class UUIDField(BaseField):
|
|||||||
if not self._binary:
|
if not self._binary:
|
||||||
original_value = value
|
original_value = value
|
||||||
try:
|
try:
|
||||||
if not isinstance(value, six.string_types):
|
if not isinstance(value, basestring):
|
||||||
value = six.text_type(value)
|
value = unicode(value)
|
||||||
return uuid.UUID(value)
|
return uuid.UUID(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
return original_value
|
return original_value
|
||||||
@@ -1969,8 +1870,8 @@ class UUIDField(BaseField):
|
|||||||
|
|
||||||
def to_mongo(self, value):
|
def to_mongo(self, value):
|
||||||
if not self._binary:
|
if not self._binary:
|
||||||
return six.text_type(value)
|
return unicode(value)
|
||||||
elif isinstance(value, six.string_types):
|
elif isinstance(value, basestring):
|
||||||
return uuid.UUID(value)
|
return uuid.UUID(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -1981,11 +1882,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, six.string_types):
|
if not isinstance(value, basestring):
|
||||||
value = str(value)
|
value = str(value)
|
||||||
try:
|
try:
|
||||||
uuid.UUID(value)
|
uuid.UUID(value)
|
||||||
except Exception as exc:
|
except Exception, exc:
|
||||||
self.error('Could not convert to UUID: %s' % exc)
|
self.error('Could not convert to UUID: %s' % exc)
|
||||||
|
|
||||||
|
|
||||||
@@ -2003,18 +1904,19 @@ 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):
|
||||||
@@ -2024,8 +1926,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.
|
||||||
@@ -2034,7 +1936,7 @@ class PointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = 'Point'
|
_type = "Point"
|
||||||
|
|
||||||
|
|
||||||
class LineStringField(GeoJsonBaseField):
|
class LineStringField(GeoJsonBaseField):
|
||||||
@@ -2044,8 +1946,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.
|
||||||
|
|
||||||
@@ -2053,7 +1955,7 @@ class LineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = 'LineString'
|
_type = "LineString"
|
||||||
|
|
||||||
|
|
||||||
class PolygonField(GeoJsonBaseField):
|
class PolygonField(GeoJsonBaseField):
|
||||||
@@ -2063,9 +1965,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
|
||||||
@@ -2075,7 +1977,7 @@ class PolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.8
|
.. versionadded:: 0.8
|
||||||
"""
|
"""
|
||||||
_type = 'Polygon'
|
_type = "Polygon"
|
||||||
|
|
||||||
|
|
||||||
class MultiPointField(GeoJsonBaseField):
|
class MultiPointField(GeoJsonBaseField):
|
||||||
@@ -2085,8 +1987,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.
|
||||||
@@ -2095,7 +1997,7 @@ class MultiPointField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = 'MultiPoint'
|
_type = "MultiPoint"
|
||||||
|
|
||||||
|
|
||||||
class MultiLineStringField(GeoJsonBaseField):
|
class MultiLineStringField(GeoJsonBaseField):
|
||||||
@@ -2105,9 +2007,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.
|
||||||
|
|
||||||
@@ -2115,7 +2017,7 @@ class MultiLineStringField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = 'MultiLineString'
|
_type = "MultiLineString"
|
||||||
|
|
||||||
|
|
||||||
class MultiPolygonField(GeoJsonBaseField):
|
class MultiPolygonField(GeoJsonBaseField):
|
||||||
@@ -2125,14 +2027,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
|
||||||
@@ -2142,4 +2044,4 @@ class MultiPolygonField(GeoJsonBaseField):
|
|||||||
|
|
||||||
.. versionadded:: 0.9
|
.. versionadded:: 0.9
|
||||||
"""
|
"""
|
||||||
_type = 'MultiPolygon'
|
_type = "MultiPolygon"
|
||||||
|
@@ -1,9 +1,7 @@
|
|||||||
"""
|
"""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
|
|
||||||
PyMongo v2.7 - v3.x support.
|
import sys
|
||||||
"""
|
|
||||||
import pymongo
|
import pymongo
|
||||||
import six
|
|
||||||
|
|
||||||
|
|
||||||
if pymongo.version_tuple[0] < 3:
|
if pymongo.version_tuple[0] < 3:
|
||||||
@@ -11,15 +9,29 @@ if pymongo.version_tuple[0] < 3:
|
|||||||
else:
|
else:
|
||||||
IS_PYMONGO_3 = True
|
IS_PYMONGO_3 = True
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
|
if PY3:
|
||||||
StringIO = six.BytesIO
|
import codecs
|
||||||
|
from io import BytesIO as StringIO
|
||||||
|
|
||||||
# Additionally for Py2, try to use the faster cStringIO, if available
|
# return s converted to binary. b('test') should be equivalent to b'test'
|
||||||
if not six.PY3:
|
def b(s):
|
||||||
|
return codecs.latin_1_encode(s)[0]
|
||||||
|
|
||||||
|
bin_type = bytes
|
||||||
|
txt_type = str
|
||||||
|
else:
|
||||||
try:
|
try:
|
||||||
import cStringIO
|
from cStringIO import StringIO
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
from StringIO import StringIO
|
||||||
else:
|
|
||||||
StringIO = cStringIO.StringIO
|
# Conversion to binary only necessary in Python 3
|
||||||
|
def b(s):
|
||||||
|
return s
|
||||||
|
|
||||||
|
bin_type = str
|
||||||
|
txt_type = unicode
|
||||||
|
|
||||||
|
str_types = (bin_type, txt_type)
|
||||||
|
@@ -1,17 +1,11 @@
|
|||||||
from mongoengine.errors import *
|
from mongoengine.errors import (DoesNotExist, InvalidQueryError,
|
||||||
|
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 *
|
||||||
|
|
||||||
# Expose just the public subset of all imported objects and constants.
|
__all__ = (field_list.__all__ + manager.__all__ + queryset.__all__ +
|
||||||
__all__ = (
|
transform.__all__ + visitor.__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 = {field: self.value for field in self.fields}
|
field_list = dict((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,10 +27,9 @@ 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()
|
||||||
|
|
||||||
@@ -43,56 +42,40 @@ 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):
|
||||||
"""Provide a string representation of the QuerySet"""
|
"""Provides the 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
|
Also populates the cache if there are more possible results to yield.
|
||||||
yield. Raises StopIteration when there are no more results.
|
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)
|
||||||
# For all positions lower than the length of the current result
|
while pos < upper:
|
||||||
# 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()
|
||||||
|
|
||||||
@@ -103,22 +86,12 @@ 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:
|
||||||
# Skip populating the cache if we already established there are no
|
try:
|
||||||
# more docs to pull from the database.
|
for i in xrange(ITER_CHUNK_SIZE):
|
||||||
if not self._has_more:
|
self._result_cache.append(self.next())
|
||||||
return
|
except StopIteration:
|
||||||
|
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.
|
||||||
@@ -136,15 +109,13 @@ 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):
|
||||||
@@ -155,7 +126,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
|
||||||
@@ -166,14 +137,13 @@ class QuerySetNoCache(BaseQuerySet):
|
|||||||
return '.. queryset mid-iteration ..'
|
return '.. queryset mid-iteration ..'
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
for _ in xrange(REPR_OUTPUT_SIZE + 1):
|
for i 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,11 +1,9 @@
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from bson import ObjectId, SON
|
from bson import SON
|
||||||
from bson.dbref import DBRef
|
|
||||||
import pymongo
|
import pymongo
|
||||||
import six
|
|
||||||
|
|
||||||
from mongoengine.base import UPDATE_OPERATORS
|
from mongoengine.base.fields 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
|
||||||
@@ -28,13 +26,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
|
||||||
|
|
||||||
@@ -47,7 +45,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
|
||||||
@@ -59,17 +57,16 @@ 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 as e:
|
except Exception, 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, six.string_types):
|
if isinstance(field, basestring):
|
||||||
parts.append(field)
|
parts.append(field)
|
||||||
append_field = False
|
append_field = False
|
||||||
# is last and CachedReferenceField
|
# is last and CachedReferenceField
|
||||||
@@ -87,9 +84,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, six.string_types):
|
if isinstance(field, basestring):
|
||||||
if (op in STRING_OPERATORS and
|
if (op in STRING_OPERATORS and
|
||||||
isinstance(value, six.string_types)):
|
isinstance(value, basestring)):
|
||||||
StringField = _import_class('StringField')
|
StringField = _import_class('StringField')
|
||||||
value = StringField.prepare_query_value(op, value)
|
value = StringField.prepare_query_value(op, value)
|
||||||
else:
|
else:
|
||||||
@@ -101,31 +98,8 @@ 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):
|
||||||
# Raise an error if the in/nin/all/near param is not iterable. We need a
|
# 'in', 'nin' and 'all' require a list of values
|
||||||
# special check for BaseDocument, because - although it's iterable - using
|
value = [field.prepare_query_value(op, v) for v in value]
|
||||||
# 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:
|
||||||
@@ -142,10 +116,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}
|
||||||
|
|
||||||
@@ -154,13 +128,11 @@ 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 isinstance(mongo_query[key], dict):
|
if key in mongo_query and 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]
|
||||||
@@ -210,16 +182,15 @@ 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
|
"""Transform an update spec from Django-style format to Mongo format.
|
||||||
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
|
||||||
@@ -233,25 +204,26 @@ 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'
|
||||||
value = -value
|
if value > 0:
|
||||||
|
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 as e:
|
except Exception, e:
|
||||||
raise InvalidQueryError(e)
|
raise InvalidQueryError(e)
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
@@ -259,7 +231,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, six.string_types):
|
if isinstance(field, basestring):
|
||||||
# Convert the S operator to $
|
# Convert the S operator to $
|
||||||
if field == 'S':
|
if field == 'S':
|
||||||
field = '$'
|
field = '$'
|
||||||
@@ -280,7 +252,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)
|
||||||
|
|
||||||
@@ -294,7 +266,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:
|
||||||
@@ -304,16 +276,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]
|
||||||
@@ -324,7 +296,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)
|
||||||
|
|
||||||
@@ -332,7 +304,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
|
||||||
@@ -346,82 +318,78 @@ 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 been '
|
raise NotImplementedError("Geo method '%s' has not "
|
||||||
'implemented for a GeoPointField' % op)
|
"been 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(
|
raise NotImplementedError("Geo method '%s' has not "
|
||||||
'Geo method "%s" has not been implemented for a %s '
|
"been implemented for a %s " % (op, field._name))
|
||||||
% (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
|
"""Helper method that tries to infer the $geometry shape for a given value"""
|
||||||
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 '
|
raise InvalidQueryError("Invalid $geometry data. Can be either a dictionary "
|
||||||
'dictionary or (nested) lists of coordinate(s)')
|
"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,7 +79,8 @@ 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
|
||||||
@@ -93,8 +94,7 @@ 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
|
"""Combine this node with another node into a QCombination object.
|
||||||
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
|
"""Represents the combination of several conditions by a given logical
|
||||||
logical operator.
|
operator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, operation, children):
|
def __init__(self, operation, children):
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
__all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
|
# -*- coding: utf-8 -*-
|
||||||
'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:
|
||||||
@@ -32,7 +34,6 @@ 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,11 +1,13 @@
|
|||||||
[nosetests]
|
[nosetests]
|
||||||
verbosity=2
|
verbosity = 2
|
||||||
detailed-errors=1
|
detailed-errors = 1
|
||||||
tests=tests
|
cover-erase = 1
|
||||||
cover-package=mongoengine
|
cover-branches = 1
|
||||||
|
cover-package = mongoengine
|
||||||
|
tests = tests
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore=E501,F401,F403,F405,I201
|
ignore=E501,F401,F403,F405,I201
|
||||||
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
|
exclude=build,dist,docs,venv,.tox,.eggs,tests
|
||||||
max-complexity=47
|
max-complexity=42
|
||||||
application-import-names=mongoengine,tests
|
application-import-names=mongoengine,tests
|
||||||
|
25
setup.py
25
setup.py
@@ -21,9 +21,8 @@ except Exception:
|
|||||||
|
|
||||||
|
|
||||||
def get_version(version_tuple):
|
def get_version(version_tuple):
|
||||||
"""Return the version tuple as a string, e.g. for (0, 10, 7),
|
if not isinstance(version_tuple[-1], int):
|
||||||
return '0.10.7'.
|
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
|
||||||
"""
|
|
||||||
return '.'.join(map(str, version_tuple))
|
return '.'.join(map(str, version_tuple))
|
||||||
|
|
||||||
|
|
||||||
@@ -42,29 +41,31 @@ 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 = {
|
extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])}
|
||||||
'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
|
||||||
if 'test' in sys.argv or 'nosetests' in sys.argv:
|
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0']
|
||||||
|
if "test" in sys.argv or "nosetests" in sys.argv:
|
||||||
extra_opts['packages'] = find_packages()
|
extra_opts['packages'] = find_packages()
|
||||||
extra_opts['package_data'] = {
|
extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]}
|
||||||
'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
|
|
||||||
else:
|
else:
|
||||||
extra_opts['tests_require'] += ['python-dateutil']
|
# coverage 4 does not support Python 3.2 anymore
|
||||||
|
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0', 'python-dateutil']
|
||||||
|
|
||||||
|
if sys.version_info[0] == 2 and sys.version_info[1] == 6:
|
||||||
|
extra_opts['tests_require'].append('unittest2')
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='mongoengine',
|
name='mongoengine',
|
||||||
|
@@ -2,3 +2,4 @@ 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,6 +3,8 @@ 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,3 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from class_methods import *
|
from class_methods import *
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
# -*- 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,4 +1,6 @@
|
|||||||
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
|
||||||
@@ -141,9 +143,11 @@ 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.objects.create(
|
p = self.Person()
|
||||||
misc={'hello': {'hello2': 'world'}}
|
p.misc = {'hello': {'hello2': 'world'}}
|
||||||
)
|
p.save()
|
||||||
|
# from pprint import pprint as pp; import pdb; pdb.set_trace();
|
||||||
|
print self.Person.objects(misc__hello__hello2='world')
|
||||||
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,22 +2,26 @@
|
|||||||
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
|
from mongoengine.connection import get_db, get_connection
|
||||||
|
|
||||||
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.connection = connect(db='mongoenginetest')
|
self.db_name = 'mongoenginetest_IndexesTest_' + str(randint(0, self._MAX_RAND))
|
||||||
|
self.connection = connect(db=self.db_name)
|
||||||
self.db = get_db()
|
self.db = get_db()
|
||||||
|
|
||||||
class Person(Document):
|
class Person(Document):
|
||||||
@@ -412,6 +416,7 @@ class IndexesTest(unittest.TestCase):
|
|||||||
User.ensure_indexes()
|
User.ensure_indexes()
|
||||||
info = User.objects._collection.index_information()
|
info = User.objects._collection.index_information()
|
||||||
self.assertEqual(sorted(info.keys()), ['_cls_1_user_guid_1', '_id_'])
|
self.assertEqual(sorted(info.keys()), ['_cls_1_user_guid_1', '_id_'])
|
||||||
|
User.drop_collection()
|
||||||
|
|
||||||
def test_embedded_document_index(self):
|
def test_embedded_document_index(self):
|
||||||
"""Tests settings an index on an embedded document
|
"""Tests settings an index on an embedded document
|
||||||
@@ -433,6 +438,7 @@ class IndexesTest(unittest.TestCase):
|
|||||||
|
|
||||||
info = BlogPost.objects._collection.index_information()
|
info = BlogPost.objects._collection.index_information()
|
||||||
self.assertEqual(sorted(info.keys()), ['_id_', 'date.yr_-1'])
|
self.assertEqual(sorted(info.keys()), ['_id_', 'date.yr_-1'])
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_list_embedded_document_index(self):
|
def test_list_embedded_document_index(self):
|
||||||
"""Ensure list embedded documents can be indexed
|
"""Ensure list embedded documents can be indexed
|
||||||
@@ -459,6 +465,7 @@ class IndexesTest(unittest.TestCase):
|
|||||||
post1 = BlogPost(title="Embedded Indexes tests in place",
|
post1 = BlogPost(title="Embedded Indexes tests in place",
|
||||||
tags=[Tag(name="about"), Tag(name="time")])
|
tags=[Tag(name="about"), Tag(name="time")])
|
||||||
post1.save()
|
post1.save()
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_recursive_embedded_objects_dont_break_indexes(self):
|
def test_recursive_embedded_objects_dont_break_indexes(self):
|
||||||
|
|
||||||
@@ -491,7 +498,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
obj = Test(a=1)
|
obj = Test(a=1)
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
IS_MONGODB_3 = get_mongodb_version()[0] >= 3
|
connection = get_connection()
|
||||||
|
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.
|
||||||
@@ -552,8 +560,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
|
|
||||||
BlogPost.drop_collection()
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
for i in range(0, 10):
|
for i in xrange(0, 10):
|
||||||
tags = [("tag %i" % n) for n in range(0, i % 2)]
|
tags = [("tag %i" % n) for n in xrange(0, i % 2)]
|
||||||
BlogPost(tags=tags).save()
|
BlogPost(tags=tags).save()
|
||||||
|
|
||||||
self.assertEqual(BlogPost.objects.count(), 10)
|
self.assertEqual(BlogPost.objects.count(), 10)
|
||||||
@@ -619,6 +627,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
post3 = BlogPost(title='test3', date=Date(year=2010), slug='test')
|
post3 = BlogPost(title='test3', date=Date(year=2010), slug='test')
|
||||||
self.assertRaises(OperationError, post3.save)
|
self.assertRaises(OperationError, post3.save)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_unique_embedded_document(self):
|
def test_unique_embedded_document(self):
|
||||||
"""Ensure that uniqueness constraints are applied to fields on embedded documents.
|
"""Ensure that uniqueness constraints are applied to fields on embedded documents.
|
||||||
"""
|
"""
|
||||||
@@ -646,6 +656,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
sub=SubDocument(year=2010, slug='test'))
|
sub=SubDocument(year=2010, slug='test'))
|
||||||
self.assertRaises(NotUniqueError, post3.save)
|
self.assertRaises(NotUniqueError, post3.save)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_unique_embedded_document_in_list(self):
|
def test_unique_embedded_document_in_list(self):
|
||||||
"""
|
"""
|
||||||
Ensure that the uniqueness constraints are applied to fields in
|
Ensure that the uniqueness constraints are applied to fields in
|
||||||
@@ -676,6 +688,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(NotUniqueError, post2.save)
|
self.assertRaises(NotUniqueError, post2.save)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_unique_with_embedded_document_and_embedded_unique(self):
|
def test_unique_with_embedded_document_and_embedded_unique(self):
|
||||||
"""Ensure that uniqueness constraints are applied to fields on
|
"""Ensure that uniqueness constraints are applied to fields on
|
||||||
embedded documents. And work with unique_with as well.
|
embedded documents. And work with unique_with as well.
|
||||||
@@ -709,6 +723,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
sub=SubDocument(year=2009, slug='test-1'))
|
sub=SubDocument(year=2009, slug='test-1'))
|
||||||
self.assertRaises(NotUniqueError, post3.save)
|
self.assertRaises(NotUniqueError, post3.save)
|
||||||
|
|
||||||
|
BlogPost.drop_collection()
|
||||||
|
|
||||||
def test_ttl_indexes(self):
|
def test_ttl_indexes(self):
|
||||||
|
|
||||||
class Log(Document):
|
class Log(Document):
|
||||||
@@ -721,6 +737,14 @@ 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()
|
||||||
@@ -748,11 +772,13 @@ class IndexesTest(unittest.TestCase):
|
|||||||
raise AssertionError("We saved a dupe!")
|
raise AssertionError("We saved a dupe!")
|
||||||
except NotUniqueError:
|
except NotUniqueError:
|
||||||
pass
|
pass
|
||||||
|
Customer.drop_collection()
|
||||||
|
|
||||||
def test_unique_and_primary(self):
|
def test_unique_and_primary(self):
|
||||||
"""If you set a field as primary, then unexpected behaviour can occur.
|
"""If you set a field as primary, then unexpected behaviour can occur.
|
||||||
You won't create a duplicate but you will update an existing document.
|
You won't create a duplicate but you will update an existing document.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class User(Document):
|
class User(Document):
|
||||||
name = StringField(primary_key=True, unique=True)
|
name = StringField(primary_key=True, unique=True)
|
||||||
password = StringField()
|
password = StringField()
|
||||||
@@ -768,23 +794,8 @@ class IndexesTest(unittest.TestCase):
|
|||||||
self.assertEqual(User.objects.count(), 1)
|
self.assertEqual(User.objects.count(), 1)
|
||||||
self.assertEqual(User.objects.get().password, 'secret2')
|
self.assertEqual(User.objects.get().password, 'secret2')
|
||||||
|
|
||||||
def test_unique_and_primary_create(self):
|
|
||||||
"""Create a new record with a duplicate primary key
|
|
||||||
throws an exception
|
|
||||||
"""
|
|
||||||
class User(Document):
|
|
||||||
name = StringField(primary_key=True)
|
|
||||||
password = StringField()
|
|
||||||
|
|
||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
|
|
||||||
User.objects.create(name='huangz', password='secret')
|
|
||||||
with self.assertRaises(NotUniqueError):
|
|
||||||
User.objects.create(name='huangz', password='secret2')
|
|
||||||
|
|
||||||
self.assertEqual(User.objects.count(), 1)
|
|
||||||
self.assertEqual(User.objects.get().password, 'secret')
|
|
||||||
|
|
||||||
def test_index_with_pk(self):
|
def test_index_with_pk(self):
|
||||||
"""Ensure you can use `pk` as part of a query"""
|
"""Ensure you can use `pk` as part of a query"""
|
||||||
|
|
||||||
@@ -867,8 +878,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,4 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
@@ -251,17 +253,19 @@ 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 is disabled by default on simple
|
"""Ensure that inheritance may be disabled on simple classes and that
|
||||||
classes and that _cls will not be used.
|
_cls and _subclasses will not be used.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Animal(Document):
|
class Animal(Document):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
# can't inherit because Animal didn't explicitly allow inheritance
|
def create_dog_class():
|
||||||
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'])
|
||||||
@@ -271,15 +275,17 @@ 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}
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
def create_mammal_class():
|
||||||
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
|
||||||
@@ -292,9 +298,10 @@ class InheritanceTest(unittest.TestCase):
|
|||||||
class Animal(FinalDocument):
|
class Animal(FinalDocument):
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
def create_mammal_class():
|
||||||
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')
|
||||||
@@ -353,26 +360,29 @@ 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?
|
||||||
with self.assertRaises(KeyError):
|
self.assertRaises(KeyError, lambda: setattr(bkk, 'pk', 1))
|
||||||
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()
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
def create_special_comment():
|
||||||
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())
|
||||||
|
|
||||||
@@ -444,11 +454,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')
|
||||||
|
|
||||||
# ensure that a subclass of a non-abstract class can't be abstract
|
def create_bad_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
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
@@ -57,7 +60,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
User().validate()
|
User().validate()
|
||||||
except ValidationError as e:
|
except ValidationError, 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',
|
||||||
@@ -67,7 +70,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
user.name = None
|
user.name = None
|
||||||
try:
|
try:
|
||||||
user.save()
|
user.save()
|
||||||
except ValidationError as e:
|
except ValidationError, 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'})
|
||||||
@@ -115,7 +118,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
Doc(id="bad").validate()
|
Doc(id="bad").validate()
|
||||||
except ValidationError as e:
|
except ValidationError, 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'}})
|
||||||
@@ -133,7 +136,7 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
doc.e.val = "OK"
|
doc.e.val = "OK"
|
||||||
try:
|
try:
|
||||||
doc.save()
|
doc.save()
|
||||||
except ValidationError as e:
|
except ValidationError, 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'}})
|
||||||
@@ -153,14 +156,14 @@ class ValidatorErrorTest(unittest.TestCase):
|
|||||||
|
|
||||||
s = SubDoc()
|
s = SubDoc()
|
||||||
|
|
||||||
self.assertRaises(ValidationError, s.validate)
|
self.assertRaises(ValidationError, lambda: s.validate())
|
||||||
|
|
||||||
d1.e = s
|
d1.e = s
|
||||||
d2.e = s
|
d2.e = s
|
||||||
|
|
||||||
del d1
|
del d1
|
||||||
|
|
||||||
self.assertRaises(ValidationError, d2.validate)
|
self.assertRaises(ValidationError, lambda: 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,16 +1,18 @@
|
|||||||
# -*- 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 StringIO
|
from mongoengine.python_support import b, StringIO
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -18,13 +20,15 @@ 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(MongoDBTestCase):
|
class FileTest(unittest.TestCase):
|
||||||
|
|
||||||
|
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')
|
||||||
@@ -45,7 +49,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
|
|
||||||
PutFile.drop_collection()
|
PutFile.drop_collection()
|
||||||
|
|
||||||
text = six.b('Hello, World!')
|
text = b('Hello, World!')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
putfile = PutFile()
|
putfile = PutFile()
|
||||||
@@ -84,8 +88,8 @@ class FileTest(MongoDBTestCase):
|
|||||||
|
|
||||||
StreamFile.drop_collection()
|
StreamFile.drop_collection()
|
||||||
|
|
||||||
text = six.b('Hello, World!')
|
text = b('Hello, World!')
|
||||||
more_text = six.b('Foo Bar')
|
more_text = b('Foo Bar')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
streamfile = StreamFile()
|
streamfile = StreamFile()
|
||||||
@@ -119,8 +123,8 @@ class FileTest(MongoDBTestCase):
|
|||||||
|
|
||||||
StreamFile.drop_collection()
|
StreamFile.drop_collection()
|
||||||
|
|
||||||
text = six.b('Hello, World!')
|
text = b('Hello, World!')
|
||||||
more_text = six.b('Foo Bar')
|
more_text = b('Foo Bar')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
streamfile = StreamFile()
|
streamfile = StreamFile()
|
||||||
@@ -151,8 +155,8 @@ class FileTest(MongoDBTestCase):
|
|||||||
class SetFile(Document):
|
class SetFile(Document):
|
||||||
the_file = FileField()
|
the_file = FileField()
|
||||||
|
|
||||||
text = six.b('Hello, World!')
|
text = b('Hello, World!')
|
||||||
more_text = six.b('Foo Bar')
|
more_text = b('Foo Bar')
|
||||||
|
|
||||||
SetFile.drop_collection()
|
SetFile.drop_collection()
|
||||||
|
|
||||||
@@ -181,7 +185,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
GridDocument.drop_collection()
|
GridDocument.drop_collection()
|
||||||
|
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
f.write(six.b("Hello World!"))
|
f.write(b("Hello World!"))
|
||||||
f.flush()
|
f.flush()
|
||||||
|
|
||||||
# Test without default
|
# Test without default
|
||||||
@@ -198,7 +202,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
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=six.b(''))
|
doc_d = GridDocument(the_file=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)
|
||||||
@@ -224,7 +228,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
# 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(six.b('Hello, World!'))
|
test_file.the_file.put(b('Hello, World!'))
|
||||||
test_file.save()
|
test_file.save()
|
||||||
|
|
||||||
# Second instance
|
# Second instance
|
||||||
@@ -278,7 +282,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
|
|
||||||
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(six.b('Hello, World!'), content_type='text/plain')
|
test_file.the_file.put(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))
|
||||||
|
|
||||||
@@ -293,66 +297,66 @@ class FileTest(MongoDBTestCase):
|
|||||||
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 = six.b('Hello, World!')
|
text = 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 = six.b('Bonjour, World!')
|
text = 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)
|
||||||
@@ -368,14 +372,14 @@ class FileTest(MongoDBTestCase):
|
|||||||
TestImage.drop_collection()
|
TestImage.drop_collection()
|
||||||
|
|
||||||
with tempfile.TemporaryFile() as f:
|
with tempfile.TemporaryFile() as f:
|
||||||
f.write(six.b("Hello World!"))
|
f.write(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 as e:
|
except ValidationError, 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()
|
||||||
@@ -492,7 +496,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
# 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(six.b('Hello, World!'),
|
test_file.the_file.put(b('Hello, World!'),
|
||||||
name="hello.txt")
|
name="hello.txt")
|
||||||
test_file.save()
|
test_file.save()
|
||||||
|
|
||||||
@@ -500,15 +504,16 @@ class FileTest(MongoDBTestCase):
|
|||||||
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(), six.b('Hello, World!'))
|
self.assertEqual(test_file.the_file.read(),
|
||||||
|
b('Hello, World!'))
|
||||||
|
|
||||||
test_file = TestFile.objects.first()
|
test_file = TestFile.objects.first()
|
||||||
test_file.the_file = six.b('HELLO, WORLD!')
|
test_file.the_file = 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(),
|
||||||
six.b('HELLO, WORLD!'))
|
b('HELLO, WORLD!'))
|
||||||
|
|
||||||
def test_copyable(self):
|
def test_copyable(self):
|
||||||
class PutFile(Document):
|
class PutFile(Document):
|
||||||
@@ -516,7 +521,7 @@ class FileTest(MongoDBTestCase):
|
|||||||
|
|
||||||
PutFile.drop_collection()
|
PutFile.drop_collection()
|
||||||
|
|
||||||
text = six.b('Hello, World!')
|
text = b('Hello, World!')
|
||||||
content_type = 'text/plain'
|
content_type = 'text/plain'
|
||||||
|
|
||||||
putfile = PutFile()
|
putfile = PutFile()
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
11
tests/migration/__init__.py
Normal file
11
tests/migration/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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()
|
51
tests/migration/convert_to_new_inheritance_model.py
Normal file
51
tests/migration/convert_to_new_inheritance_model.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# -*- 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()
|
50
tests/migration/decimalfield_as_float.py
Normal file
50
tests/migration/decimalfield_as_float.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# -*- 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']]))
|
52
tests/migration/referencefield_dbref_to_object_id.py
Normal file
52
tests/migration/referencefield_dbref_to_object_id.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# -*- 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'])
|
62
tests/migration/turn_off_inheritance.py
Normal file
62
tests/migration/turn_off_inheritance.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# -*- 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()
|
48
tests/migration/uuidfield_to_binary.py
Normal file
48
tests/migration/uuidfield_to_binary.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# -*- 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,3 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@@ -92,7 +95,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
exclude = ['d', 'e']
|
exclude = ['d', 'e']
|
||||||
only = ['b', 'c']
|
only = ['b', 'c']
|
||||||
|
|
||||||
qs = MyDoc.objects.fields(**{i: 1 for i in include})
|
qs = MyDoc.objects.fields(**dict(((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)
|
||||||
@@ -100,14 +103,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(**{i: 1 for i in include})
|
qs = MyDoc.objects.fields(**dict(((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(**{i: 1 for i in include})
|
qs = qs.fields(**dict(((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})
|
||||||
@@ -126,7 +129,7 @@ class OnlyExcludeAllTest(unittest.TestCase):
|
|||||||
exclude = ['d', 'e']
|
exclude = ['d', 'e']
|
||||||
only = ['b', 'c']
|
only = ['b', 'c']
|
||||||
|
|
||||||
qs = MyDoc.objects.fields(**{i: 1 for i in include})
|
qs = MyDoc.objects.fields(**dict(((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)
|
||||||
@@ -141,16 +144,6 @@ 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,139 +1,109 @@
|
|||||||
import datetime
|
import sys
|
||||||
|
|
||||||
|
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 tests.utils import MongoDBTestCase, needs_mongodb_v3
|
from nose.plugins.skip import SkipTest
|
||||||
|
|
||||||
|
|
||||||
__all__ = ("GeoQueriesTest",)
|
__all__ = ("GeoQueriesTest",)
|
||||||
|
|
||||||
|
|
||||||
class GeoQueriesTest(MongoDBTestCase):
|
class GeoQueriesTest(unittest.TestCase):
|
||||||
|
|
||||||
def _create_event_data(self, point_field_class=GeoPointField):
|
def setUp(self):
|
||||||
"""Create some sample data re-used in many of the tests below."""
|
connect(db='mongoenginetest')
|
||||||
|
|
||||||
|
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 = point_field_class()
|
location = GeoPointField()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
self.Event = Event
|
|
||||||
|
|
||||||
Event.drop_collection()
|
Event.drop_collection()
|
||||||
|
|
||||||
event1 = Event.objects.create(
|
event1 = Event(title="Coltrane Motion @ Double Door",
|
||||||
title="Coltrane Motion @ Double Door",
|
date=datetime.now() - timedelta(days=1),
|
||||||
date=datetime.datetime.now() - datetime.timedelta(days=1),
|
location=[-87.677137, 41.909889]).save()
|
||||||
location=[-87.677137, 41.909889])
|
event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
|
||||||
event2 = Event.objects.create(
|
date=datetime.now() - timedelta(days=10),
|
||||||
title="Coltrane Motion @ Bottom of the Hill",
|
location=[-122.4194155, 37.7749295]).save()
|
||||||
date=datetime.datetime.now() - datetime.timedelta(days=10),
|
event3 = Event(title="Coltrane Motion @ Empty Bottle",
|
||||||
location=[-122.4194155, 37.7749295])
|
date=datetime.now(),
|
||||||
event3 = Event.objects.create(
|
location=[-87.686638, 41.900474]).save()
|
||||||
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 = self.Event.objects(location__near=[-87.67892, 41.9120459])
|
events = 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 = self.Event.objects(
|
events = Event.objects(location__within_distance=point_and_distance)
|
||||||
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 = self.Event.objects(
|
events = Event.objects(location__within_distance=point_and_distance)
|
||||||
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 = self.Event.objects(
|
events = Event.objects(location__within_distance=point_and_distance)
|
||||||
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 = self.Event.objects(
|
events = Event.objects(location__within_distance=point_and_distance)
|
||||||
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 = self.Event.objects(location__within_box=box)
|
events = 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),
|
||||||
@@ -141,7 +111,7 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
(-87.654276, 41.911731),
|
(-87.654276, 41.911731),
|
||||||
(-87.656164, 41.898061),
|
(-87.656164, 41.898061),
|
||||||
]
|
]
|
||||||
events = self.Event.objects(location__within_polygon=polygon)
|
events = 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)
|
||||||
|
|
||||||
@@ -150,151 +120,13 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
(-1.225891, 52.792797),
|
(-1.225891, 52.792797),
|
||||||
(-4.40094, 53.389881)
|
(-4.40094, 53.389881)
|
||||||
]
|
]
|
||||||
events = self.Event.objects(location__within_polygon=polygon2)
|
events = Event.objects(location__within_polygon=polygon2)
|
||||||
self.assertEqual(events.count(), 0)
|
self.assertEqual(events.count(), 0)
|
||||||
|
|
||||||
def test_2dsphere_near(self):
|
def test_geo_spatial_embedded(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 = point_field_class()
|
location = GeoPointField()
|
||||||
name = StringField()
|
name = StringField()
|
||||||
|
|
||||||
class Event(Document):
|
class Event(Document):
|
||||||
@@ -320,18 +152,16 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
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_geo_spatial_embedded(self):
|
|
||||||
"""Make sure GeoPointField works properly in an embedded document."""
|
|
||||||
self._test_embedded(point_field_class=GeoPointField)
|
|
||||||
|
|
||||||
def test_2dsphere_point_embedded(self):
|
|
||||||
"""Make sure PointField works properly in an embedded document."""
|
|
||||||
self._test_embedded(point_field_class=PointField)
|
|
||||||
|
|
||||||
# Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
|
|
||||||
@needs_mongodb_v3
|
|
||||||
def test_spherical_geospatial_operators(self):
|
def test_spherical_geospatial_operators(self):
|
||||||
"""Ensure that spherical geospatial queries are working."""
|
"""Ensure that spherical geospatial queries are working
|
||||||
|
"""
|
||||||
|
# Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
|
||||||
|
connection = get_connection()
|
||||||
|
info = connection.test.command('buildInfo')
|
||||||
|
mongodb_version = tuple([int(i) for i in info['version'].split('.')])
|
||||||
|
if mongodb_version < (2, 6, 4):
|
||||||
|
raise SkipTest("Need MongoDB version 2.6.4+")
|
||||||
|
|
||||||
class Point(Document):
|
class Point(Document):
|
||||||
location = GeoPointField()
|
location = GeoPointField()
|
||||||
|
|
||||||
@@ -351,10 +181,7 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
|
|
||||||
# Same behavior for _within_spherical_distance
|
# Same behavior for _within_spherical_distance
|
||||||
points = Point.objects(
|
points = Point.objects(
|
||||||
location__within_spherical_distance=[
|
location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius]
|
||||||
[-122, 37.5],
|
|
||||||
60 / earth_radius
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
self.assertEqual(points.count(), 2)
|
self.assertEqual(points.count(), 2)
|
||||||
|
|
||||||
@@ -371,9 +198,14 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
# 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)
|
||||||
self.assertEqual(points.count(), 1)
|
# The following real test passes on MongoDB 3 but minDistance seems
|
||||||
far_point = points.first()
|
# buggy on older MongoDB versions
|
||||||
self.assertNotEqual(close_point, far_point)
|
if get_connection().server_info()['versionArray'][0] > 2:
|
||||||
|
self.assertEqual(points.count(), 1)
|
||||||
|
far_point = points.first()
|
||||||
|
self.assertNotEqual(close_point, far_point)
|
||||||
|
else:
|
||||||
|
self.assertTrue(points.count() >= 1)
|
||||||
|
|
||||||
# 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.
|
||||||
@@ -392,15 +224,141 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
# 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=[
|
location__within_spherical_distance=[[-122, 36.5], 60/earth_radius])
|
||||||
[-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()
|
||||||
@@ -456,6 +414,7 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
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()
|
||||||
@@ -552,6 +511,5 @@ class GeoQueriesTest(MongoDBTestCase):
|
|||||||
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,3 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import connect, Document, IntField
|
from mongoengine import connect, Document, IntField
|
||||||
@@ -96,4 +99,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,8 +238,7 @@ 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)
|
||||||
with self.assertRaises(InvalidQueryError):
|
self.assertRaises(InvalidQueryError, lambda: events.count())
|
||||||
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 range(1, 101):
|
for i in xrange(1, 101):
|
||||||
t = TestDoc(x=i)
|
t = TestDoc(x=i)
|
||||||
t.save()
|
t.save()
|
||||||
|
|
||||||
@@ -268,13 +268,14 @@ 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
|
||||||
with self.assertRaises(InvalidQueryError):
|
def wrong_query_objs():
|
||||||
self.Person.objects('user1')
|
self.Person.objects('user1')
|
||||||
|
|
||||||
# filter should fail, too
|
def wrong_query_objs_filter():
|
||||||
with self.assertRaises(InvalidQueryError):
|
self.Person.objects('user1')
|
||||||
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,6 +1,9 @@
|
|||||||
|
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:
|
||||||
@@ -16,8 +19,7 @@ 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 (MongoEngineConnectionError, get_db,
|
from mongoengine.connection import get_db, get_connection, ConnectionError
|
||||||
get_connection)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tz_awareness(connection):
|
def get_tz_awareness(connection):
|
||||||
@@ -35,7 +37,8 @@ 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()
|
||||||
@@ -145,7 +148,8 @@ 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 URIs."""
|
"""Ensure that the connect() method works properly with uri's
|
||||||
|
"""
|
||||||
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({})
|
||||||
@@ -155,10 +159,7 @@ 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(
|
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
|
||||||
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')
|
||||||
|
|
||||||
@@ -173,9 +174,19 @@ 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 if the URI doesn't
|
"""Ensure connect() method works properly with uri's without database_name
|
||||||
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()
|
||||||
@@ -185,35 +196,13 @@ 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')
|
||||||
|
|
||||||
def test_connect_uri_default_db(self):
|
c.admin.system.users.remove({})
|
||||||
"""Ensure connect() defaults to the right database name if
|
c.mongoenginetest.system.users.remove({})
|
||||||
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 `authSource`
|
"""Ensure that the connect() method works well with
|
||||||
option in the URI.
|
the option `authSource` in URI.
|
||||||
|
This feature was introduced in MongoDB 2.4 and removed in 2.6
|
||||||
"""
|
"""
|
||||||
# Create users
|
# Create users
|
||||||
c = connect('mongoenginetest')
|
c = connect('mongoenginetest')
|
||||||
@@ -222,38 +211,36 @@ class ConnectionTest(unittest.TestCase):
|
|||||||
|
|
||||||
# Authentication fails without "authSource"
|
# Authentication fails without "authSource"
|
||||||
if IS_PYMONGO_3:
|
if IS_PYMONGO_3:
|
||||||
test_conn = connect(
|
test_conn = connect('mongoenginetest', alias='test1',
|
||||||
'mongoenginetest', alias='test1',
|
host='mongodb://username2:password@localhost/mongoenginetest')
|
||||||
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(
|
||||||
MongoEngineConnectionError,
|
ConnectionError, connect, 'mongoenginetest', alias='test1',
|
||||||
connect, 'mongoenginetest', alias='test1',
|
|
||||||
host='mongodb://username2:password@localhost/mongoenginetest'
|
host='mongodb://username2:password@localhost/mongoenginetest'
|
||||||
)
|
)
|
||||||
self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
|
self.assertRaises(ConnectionError, get_db, 'test1')
|
||||||
|
|
||||||
# Authentication succeeds with "authSource"
|
# Authentication succeeds with "authSource"
|
||||||
authd_conn = connect(
|
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
|
||||||
authd_conn.admin.system.users.remove({})
|
c.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(MongoEngineConnectionError, get_connection)
|
self.assertRaises(ConnectionError, 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))
|
||||||
|
|
||||||
@@ -270,7 +257,8 @@ 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')
|
||||||
|
|
||||||
@@ -280,77 +268,6 @@ 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,3 +1,5 @@
|
|||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
@@ -77,7 +79,7 @@ class ContextManagersTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
@@ -115,7 +117,7 @@ class ContextManagersTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
User(name='user %s' % i).save()
|
User(name='user %s' % i).save()
|
||||||
|
|
||||||
user = User.objects.first()
|
user = User.objects.first()
|
||||||
@@ -193,7 +195,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 range(1, 51):
|
for i in xrange(1, 51):
|
||||||
db.test.find({}).count()
|
db.test.find({}).count()
|
||||||
|
|
||||||
self.assertEqual(50, q)
|
self.assertEqual(50, q)
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
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):
|
||||||
@@ -14,18 +13,9 @@ 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):
|
||||||
with self.assertRaises(AttributeError):
|
self.assertRaises(AttributeError, lambda: self.dtype(a=1, b=2, d=3))
|
||||||
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)
|
||||||
@@ -34,7 +24,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)
|
||||||
@@ -47,18 +37,20 @@ 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, getattr, d, 'b')
|
self.assertRaises(AttributeError, lambda: 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):
|
|
||||||
d.x = 1
|
|
||||||
|
|
||||||
|
def _f():
|
||||||
|
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)
|
||||||
@@ -96,7 +88,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,16 +1,13 @@
|
|||||||
# -*- 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
|
||||||
from collections import OrderedDict
|
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import get_db
|
from mongoengine.connection import get_db
|
||||||
from mongoengine.context_managers import query_counter
|
from mongoengine.context_managers import query_counter
|
||||||
from mongoengine.python_support import IS_PYMONGO_3
|
|
||||||
from mongoengine.base import TopLevelDocumentMetaclass
|
|
||||||
if IS_PYMONGO_3:
|
|
||||||
from bson import CodecOptions
|
|
||||||
|
|
||||||
|
|
||||||
class FieldTest(unittest.TestCase):
|
class FieldTest(unittest.TestCase):
|
||||||
@@ -35,7 +32,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -93,7 +90,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -165,7 +162,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 26):
|
for i in xrange(1, 26):
|
||||||
user = User(name='user %s' % i)
|
user = User(name='user %s' % i)
|
||||||
user.save()
|
user.save()
|
||||||
|
|
||||||
@@ -443,7 +440,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -534,7 +531,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -617,15 +614,15 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(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={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -690,7 +687,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -702,9 +699,9 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
members += [a, b, c]
|
members += [a, b, c]
|
||||||
|
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -786,16 +783,16 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(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={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -869,7 +866,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(1, 51):
|
||||||
a = UserA(name='User A %s' % i)
|
a = UserA(name='User A %s' % i)
|
||||||
a.save()
|
a.save()
|
||||||
|
|
||||||
@@ -881,9 +878,9 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
members += [a, b, c]
|
members += [a, b, c]
|
||||||
|
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
group = Group(members={str(u.id): u for u in members})
|
group = Group(members=dict([(str(u.id), u) for u in members]))
|
||||||
group.save()
|
group.save()
|
||||||
|
|
||||||
with query_counter() as q:
|
with query_counter() as q:
|
||||||
@@ -1106,7 +1103,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(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()
|
||||||
@@ -1135,7 +1132,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
User.drop_collection()
|
User.drop_collection()
|
||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
for i in range(1, 51):
|
for i in xrange(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()
|
||||||
@@ -1172,7 +1169,7 @@ class FieldTest(unittest.TestCase):
|
|||||||
Group.drop_collection()
|
Group.drop_collection()
|
||||||
|
|
||||||
members = []
|
members = []
|
||||||
for i in range(1, 51):
|
for i in xrange(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()
|
||||||
@@ -1292,70 +1289,5 @@ class FieldTest(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(q, 2)
|
self.assertEqual(q, 2)
|
||||||
|
|
||||||
def test_dynamic_field_dereference(self):
|
|
||||||
class Merchandise(Document):
|
|
||||||
name = StringField()
|
|
||||||
price = IntField()
|
|
||||||
|
|
||||||
class Store(Document):
|
|
||||||
merchandises = DynamicField()
|
|
||||||
|
|
||||||
Merchandise.drop_collection()
|
|
||||||
Store.drop_collection()
|
|
||||||
|
|
||||||
merchandises = {
|
|
||||||
'#1': Merchandise(name='foo', price=100).save(),
|
|
||||||
'#2': Merchandise(name='bar', price=120).save(),
|
|
||||||
'#3': Merchandise(name='baz', price=110).save(),
|
|
||||||
}
|
|
||||||
Store(merchandises=merchandises).save()
|
|
||||||
|
|
||||||
store = Store.objects().first()
|
|
||||||
for obj in store.merchandises.values():
|
|
||||||
self.assertFalse(isinstance(obj, Merchandise))
|
|
||||||
|
|
||||||
store.select_related()
|
|
||||||
for obj in store.merchandises.values():
|
|
||||||
self.assertTrue(isinstance(obj, Merchandise))
|
|
||||||
|
|
||||||
def test_dynamic_field_dereference_with_ordering_guarantee_on_pymongo3(self):
|
|
||||||
# This is because 'codec_options' is supported on pymongo3 or later
|
|
||||||
if IS_PYMONGO_3:
|
|
||||||
class OrderedDocument(Document):
|
|
||||||
my_metaclass = TopLevelDocumentMetaclass
|
|
||||||
__metaclass__ = TopLevelDocumentMetaclass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_collection(cls):
|
|
||||||
collection = super(OrderedDocument, cls)._get_collection()
|
|
||||||
opts = CodecOptions(document_class=OrderedDict)
|
|
||||||
|
|
||||||
return collection.with_options(codec_options=opts)
|
|
||||||
|
|
||||||
class Merchandise(Document):
|
|
||||||
name = StringField()
|
|
||||||
price = IntField()
|
|
||||||
|
|
||||||
class Store(OrderedDocument):
|
|
||||||
merchandises = DynamicField(container_class=OrderedDict)
|
|
||||||
|
|
||||||
Merchandise.drop_collection()
|
|
||||||
Store.drop_collection()
|
|
||||||
|
|
||||||
merchandises = OrderedDict()
|
|
||||||
merchandises['#1'] = Merchandise(name='foo', price=100).save()
|
|
||||||
merchandises['#2'] = Merchandise(name='bar', price=120).save()
|
|
||||||
merchandises['#3'] = Merchandise(name='baz', price=110).save()
|
|
||||||
|
|
||||||
Store(merchandises=merchandises).save()
|
|
||||||
|
|
||||||
store = Store.objects().first()
|
|
||||||
|
|
||||||
store.select_related()
|
|
||||||
|
|
||||||
# confirms that the load data order is same with the one at storing
|
|
||||||
self.assertTrue(type(store.merchandises), OrderedDict)
|
|
||||||
self.assertEqual(','.join(store.merchandises.keys()), '#1,#2,#3')
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from pymongo import ReadPreference
|
from pymongo import ReadPreference
|
||||||
@@ -15,7 +18,7 @@ else:
|
|||||||
|
|
||||||
import mongoengine
|
import mongoengine
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
from mongoengine.connection import MongoEngineConnectionError
|
from mongoengine.connection import ConnectionError
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTest(unittest.TestCase):
|
class ConnectionTest(unittest.TestCase):
|
||||||
@@ -38,7 +41,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 MongoEngineConnectionError as e:
|
except ConnectionError, e:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not isinstance(conn, CONN_CLASS):
|
if not isinstance(conn, CONN_CLASS):
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import sys
|
||||||
|
sys.path[0:0] = [""]
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from mongoengine import *
|
from mongoengine import *
|
||||||
|
@@ -1,78 +0,0 @@
|
|||||||
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 = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30}
|
envlist = {py26,py27,py33,py34,py35,pypy,pypy3}-{mg27,mg28},flake8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
@@ -7,7 +7,16 @@ commands =
|
|||||||
deps =
|
deps =
|
||||||
nose
|
nose
|
||||||
mg27: PyMongo<2.8
|
mg27: PyMongo<2.8
|
||||||
mg28: PyMongo>=2.8,<2.9
|
mg28: PyMongo>=2.8,<3.0
|
||||||
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