merge latest master + test pymongo v2.8 instead of v2.9 + drop dev pymongo + smarter pypi deployment

This commit is contained in:
Stefan Wojcik 2017-02-25 16:54:46 -05:00
commit 0ea1dfddaa
79 changed files with 4805 additions and 2918 deletions

2
.gitignore vendored
View File

@ -15,3 +15,5 @@ env/
.pydevproject .pydevproject
tests/test_bugfix.py tests/test_bugfix.py
htmlcov/ htmlcov/
venv
venv3

22
.landscape.yml Normal file
View File

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

View File

@ -1,54 +1,90 @@
language: python language: python
python: python:
- '2.6'
- '2.7' - '2.7'
- '3.2'
- '3.3' - '3.3'
- '3.4' - '3.4'
- '3.5'
- pypy - pypy
- pypy3 - pypy3
env: env:
# MongoDB 2.4
# MongoDB v2.4 w/ PyMongo v2.7.x, v2.8.x, v3.x
- PYMONGO=2.7 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb' - PYMONGO=2.7 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb'
- PYMONGO=2.8 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb' - PYMONGO=2.8 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb'
- PYMONGO=3.0 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb' - PYMONGO=3.0 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb'
- PYMONGO=dev REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-10gen=2.4.14' SERVICE_NAME='mongodb'
# MongoDB 2.6 # MongoDB v2.6 w/ PyMongo v2.7.x, v2.8.x, v3.x
- PYMONGO=2.7 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10' - PYMONGO=2.7 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10'
- PYMONGO=2.8 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10' - PYMONGO=2.8 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10'
- PYMONGO=3.0 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10' - PYMONGO=3.0 REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10'
- PYMONGO=dev REPOSITORY='deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' PACKAGE_NAME='mongodb-org-server=2.6.10'
# MongoDB 3.0 # MongoDB v3.0 w/ PyMongo v3.x
- PYMONGO=3.0 REPOSITORY='deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse' PACKAGE_NAME='mongodb-org-server=3.0.4' - PYMONGO=3.0 REPOSITORY='deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse' PACKAGE_NAME='mongodb-org-server=3.0.4'
- PYMONGO=dev REPOSITORY='deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse' PACKAGE_NAME='mongodb-org-server=3.0.4'
# Finish the build as soon as one job fails
matrix: matrix:
fast_finish: true fast_finish: true
before_install: before_install:
- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 - travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
- echo $REPOSITORY | sudo tee /etc/apt/sources.list.d/mongodb.list - echo $REPOSITORY | sudo tee /etc/apt/sources.list.d/mongodb.list
- travis_retry sudo apt-get update - travis_retry sudo apt-get update
- travis_retry sudo apt-get install $PACKAGE_NAME - travis_retry sudo apt-get install $PACKAGE_NAME
- if [ -z $SERVICE_NAME ]; then echo "MongoDB is already started"; else sudo service $SERVICE_NAME start; fi - if [ -z $SERVICE_NAME ]; then echo "MongoDB is already started"; else sudo service $SERVICE_NAME start; fi
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
libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev libtiff4-dev libjpeg8-dev libfreetype6-dev liblcms2-dev libwebp-dev tcl8.5-dev tk8.5-dev
python-tk python-tk
- travis_retry pip install tox>=1.9 coveralls - travis_retry pip install --upgrade pip
- travis_retry pip install coveralls
- travis_retry pip install flake8
- travis_retry pip install tox>=1.9
- travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test - travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
# Cache dependencies installed via pip
cache: pip
# Run flake8 for py27
before_script:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e flake8; fi
script: script:
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage - tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
after_script: coveralls --verbose
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
# 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible
# code in a separate dir and runs tests on that.
after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
notifications: notifications:
irc: irc.freenode.org#mongoengine irc: irc.freenode.org#mongoengine
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

15
AUTHORS
View File

@ -228,3 +228,18 @@ that much better:
* Vicki Donchenko (https://github.com/kivistein) * Vicki Donchenko (https://github.com/kivistein)
* Emile Caron (https://github.com/emilecaron) * Emile Caron (https://github.com/emilecaron)
* Amit Lichtenberg (https://github.com/amitlicht) * Amit Lichtenberg (https://github.com/amitlicht)
* Gang Li (https://github.com/iici-gli)
* Lars Butler (https://github.com/larsbutler)
* George Macon (https://github.com/gmacon)
* Ashley Whetter (https://github.com/AWhetter)
* Paul-Armand Verhaegen (https://github.com/paularmand)
* Steven Rossiter (https://github.com/BeardedSteve)
* Luo Peng (https://github.com/RussellLuo)
* Bryan Bennett (https://github.com/bbenne10)
* Gilb's Gilb's (https://github.com/gilbsgilbs)
* Joshua Nedrud (https://github.com/Neurostack)
* Shu Shen (https://github.com/shushen)
* xiaost7 (https://github.com/xiaost7)
* Victor Varvaryuk
* Stanislav Kaledin (https://github.com/sallyruthstruik)
* Dmitry Yantsen (https://github.com/mrTable)

View File

@ -20,7 +20,7 @@ post to the `user group <http://groups.google.com/group/mongoengine-users>`
Supported Interpreters Supported Interpreters
---------------------- ----------------------
MongoEngine supports CPython 2.6 and newer. Language MongoEngine supports CPython 2.7 and newer. Language
features not supported by all interpreters can not be used. features not supported by all interpreters can not be used.
Please also ensure that your code is properly converted by Please also ensure that your code is properly converted by
`2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support. `2to3 <http://docs.python.org/library/2to3.html>`_ for Python 3 support.

View File

@ -4,25 +4,25 @@ MongoEngine
:Info: MongoEngine is an ORM-like layer on top of PyMongo. :Info: MongoEngine is an ORM-like layer on top of PyMongo.
:Repository: https://github.com/MongoEngine/mongoengine :Repository: https://github.com/MongoEngine/mongoengine
:Author: Harry Marr (http://github.com/hmarr) :Author: Harry Marr (http://github.com/hmarr)
:Maintainer: Ross Lawley (http://github.com/rozza) :Maintainer: Stefan Wójcik (http://github.com/wojcikstefan)
.. image:: https://secure.travis-ci.org/MongoEngine/mongoengine.png?branch=master .. image:: https://travis-ci.org/MongoEngine/mongoengine.svg?branch=master
:target: http://travis-ci.org/MongoEngine/mongoengine :target: https://travis-ci.org/MongoEngine/mongoengine
.. image:: https://coveralls.io/repos/MongoEngine/mongoengine/badge.png?branch=master .. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master
:target: https://coveralls.io/r/MongoEngine/mongoengine?branch=master :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master
.. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.png .. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.svg?style=flat
: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 available at http://mongoengine-odm.rtfd.org - there is currently Documentation available at https://mongoengine-odm.readthedocs.io - there is currently
a `tutorial <http://readthedocs.org/docs/mongoengine-odm/en/latest/tutorial.html>`_, a `user guide a `tutorial <https://mongoengine-odm.readthedocs.io/tutorial.html>`_, a `user guide
<https://mongoengine-odm.readthedocs.org/en/latest/guide/index.html>`_ and an `API reference <https://mongoengine-odm.readthedocs.io/guide/index.html>`_ and an `API reference
<http://readthedocs.org/docs/mongoengine-odm/en/latest/apireference.html>`_. <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
Installation Installation
============ ============
@ -35,25 +35,37 @@ setup.py install``.
Dependencies Dependencies
============ ============
- pymongo>=2.7.1 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:
- sphinx (optional - for documentation generation)
- pymongo>=2.7.1
- six>=1.10.0
If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
Optional Dependencies
---------------------
- **Image Fields**: Pillow>=2.0.0
- dateutil>=2.1.0 - dateutil>=2.1.0
.. note If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
MongoEngine always runs it's test suite against the latest patch version of each dependecy. e.g.: PyMongo 3.0.1
- Pillow>=2.0.0
If you want to generate the documentation (e.g. to contribute to it):
- sphinx
Examples Examples
======== ========
Some simple examples of what MongoEngine code looks like:: Some simple examples of what MongoEngine code looks like:
.. code :: python
from mongoengine import *
connect('mydb')
class BlogPost(Document): class BlogPost(Document):
title = StringField(required=True, max_length=200) title = StringField(required=True, max_length=200)
posted = DateTimeField(default=datetime.datetime.now) posted = DateTimeField(default=datetime.datetime.utcnow)
tags = ListField(StringField(max_length=50)) tags = ListField(StringField(max_length=50))
meta = {'allow_inheritance': True}
class TextPost(BlogPost): class TextPost(BlogPost):
content = StringField(required=True) content = StringField(required=True)
@ -81,23 +93,24 @@ Some simple examples of what MongoEngine code looks like::
... print ... print
... ...
>>> len(BlogPost.objects) # Count all blog posts and its subtypes
>>> BlogPost.objects.count()
2 2
>>> len(TextPost.objects) >>> TextPost.objects.count()
1 1
>>> len(LinkPost.objects) >>> LinkPost.objects.count()
1 1
# Find tagged posts # Count tagged posts
>>> len(BlogPost.objects(tags='mongoengine')) >>> BlogPost.objects(tags='mongoengine').count()
2 2
>>> len(BlogPost.objects(tags='mongodb')) >>> BlogPost.objects(tags='mongodb').count()
1 1
Tests Tests
===== =====
To run the test suite, ensure you are running a local instance of MongoDB on To run the test suite, ensure you are running a local instance of MongoDB on
the standard port, and run: ``python setup.py nosetests``. the standard port and have ``nose`` installed. Then, run: ``python setup.py nosetests``.
To run the test suite on every supported Python version and every supported PyMongo version, To run the test suite on every supported Python version and every supported PyMongo version,
you can use ``tox``. you can use ``tox``.
@ -124,8 +137,7 @@ Community
<http://groups.google.com/group/mongoengine-users>`_ <http://groups.google.com/group/mongoengine-users>`_
- `MongoEngine Developers mailing list - `MongoEngine Developers mailing list
<http://groups.google.com/group/mongoengine-dev>`_ <http://groups.google.com/group/mongoengine-dev>`_
- `#mongoengine IRC channel <http://webchat.freenode.net/?channels=mongoengine>`_
Contributing Contributing
============ ============
We welcome contributions! see the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_ We welcome contributions! See the `Contribution guidelines <https://github.com/MongoEngine/mongoengine/blob/master/CONTRIBUTING.rst>`_

View File

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

View File

@ -2,11 +2,101 @@
Changelog Changelog
========= =========
Changes in 0.10.1 - DEV Development
======================= ===========
- (Fill this out as you fix issues and develop your features).
- Fixed using sets in field choices #1481
- POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
- POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
- Fixed connecting to a replica set with PyMongo 2.x #1436
- Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
Changes in 0.11.0
=================
- BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
- BREAKING CHANGE: Dropped Python 2.6 support. #1428
- BREAKING CHANGE: `from mongoengine.base import ErrorClass` won't work anymore for any error from `mongoengine.errors` (e.g. `ValidationError`). Use `from mongoengine.errors import ErrorClass instead`. #1428
- BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
- Fixed absent rounding for DecimalField when `force_string` is set. #1103
Changes in 0.10.8
=================
- Added support for QuerySet.batch_size (#1426)
- Fixed query set iteration within iteration #1427
- Fixed an issue where specifying a MongoDB URI host would override more information than it should #1421
- Added ability to filter the generic reference field by ObjectId and DBRef #1425
- Fixed delete cascade for models with a custom primary key field #1247
- Added ability to specify an authentication mechanism (e.g. X.509) #1333
- Added support for falsey primary keys (e.g. doc.pk = 0) #1354
- Fixed QuerySet#sum/average for fields w/ explicit db_field #1417
- Fixed filtering by embedded_doc=None #1422
- Added support for cursor.comment #1420
- Fixed doc.get_<field>_display #1419
- Fixed __repr__ method of the StrictDict #1424
- Added a deprecation warning for Python 2.6
Changes in 0.10.7
=================
- Dropped Python 3.2 support #1390
- Fixed the bug where dynamic doc has index inside a dict field #1278
- Fixed: ListField minus index assignment does not work #1128
- 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
- Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
- count on ListField of EmbeddedDocumentField fails. #1187
- Fixed long fields stored as int32 in Python 3. #1253
- MapField now handles unicodes keys correctly. #1267
- ListField now handles negative indicies correctly. #1270
- Fixed AttributeError when initializing EmbeddedDocument with positional args. #681
- Fixed no_cursor_timeout error with pymongo 3.0+ #1304
- Replaced map-reduce based QuerySet.sum/average with aggregation-based implementations #1336
- Fixed support for `__` to escape field names that match operators names in `update` #1351
- Fixed BaseDocument#_mark_as_changed #1369
- Added support for pickling QuerySet instances. #1397
- Fixed connecting to a list of hosts #1389
- 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
Changes in 0.10.6
=================
- Add support for mocking MongoEngine based on mongomock. #1151
- Fixed not being able to run tests on Windows. #1153
- Allow creation of sparse compound indexes. #1114
- count on ListField of EmbeddedDocumentField fails. #1187
Changes in 0.10.5
=================
- Fix for reloading of strict with special fields. #1156
Changes in 0.10.4
=================
- SaveConditionError is now importable from the top level package. #1165
- upsert_one method added. #1157
Changes in 0.10.3
=================
- Fix `read_preference` (it had chaining issues with PyMongo 2.x and it didn't work at all with PyMongo 3.x) #1042
Changes in 0.10.2
=================
- Allow shard key to point to a field in an embedded document. #551
- Allow arbirary metadata in fields. #1129
- ReferenceFields now support abstract document types. #837
Changes in 0.10.1
=================
- Fix infinite recursion with CASCADE delete rules under specific conditions. #1046 - Fix infinite recursion with CASCADE delete rules under specific conditions. #1046
- Fix CachedReferenceField bug when loading cached docs as DBRef but failing to save them. #1047 - Fix CachedReferenceField bug when loading cached docs as DBRef but failing to save them. #1047
- Fix ignored chained options #842 - Fix ignored chained options #842
- Document save's save_condition error raises `SaveConditionError` exception #1070
- Fix Document.reload for DynamicDocument. #1050
- StrictDict & SemiStrictDict are shadowed at init time. #1105
- Fix ListField minus index assignment does not work. #1119
- Remove code that marks field as changed when the field has default but not existed in database #1126
- Remove test dependencies (nose and rednose) from install dependencies list. #1079
- Recursively build query when using elemMatch operator. #1130
- Fix instance back references for lists of embedded documents. #1131
Changes in 0.10.0 Changes in 0.10.0
================= =================

View File

@ -17,6 +17,10 @@ class Post(Document):
tags = ListField(StringField(max_length=30)) tags = ListField(StringField(max_length=30))
comments = ListField(EmbeddedDocumentField(Comment)) comments = ListField(EmbeddedDocumentField(Comment))
# bugfix
meta = {'allow_inheritance': True}
class TextPost(Post): class TextPost(Post):
content = StringField() content = StringField()
@ -45,7 +49,8 @@ print 'ALL POSTS'
print print
for post in Post.objects: for post in Post.objects:
print post.title print post.title
print '=' * post.title.count() #print '=' * post.title.count()
print "=" * 20
if isinstance(post, TextPost): if isinstance(post, TextPost):
print post.content print post.content

View File

@ -33,7 +33,7 @@ the :attr:`host` to
corresponding parameters in :func:`~mongoengine.connect`: :: corresponding parameters in :func:`~mongoengine.connect`: ::
connect( connect(
name='test', db='test',
username='user', username='user',
password='12345', password='12345',
host='mongodb://admin:qwerty@localhost/production' host='mongodb://admin:qwerty@localhost/production'

View File

@ -29,7 +29,7 @@ documents are serialized based on their field order.
Dynamic document schemas Dynamic document schemas
======================== ========================
One of the benefits of MongoDb is dynamic schemas for a collection, whilst data One of the benefits of MongoDB is dynamic schemas for a collection, whilst data
should be planned and organised (after all explicit is better than implicit!) should be planned and organised (after all explicit is better than implicit!)
there are scenarios where having dynamic / expando style documents is desirable. there are scenarios where having dynamic / expando style documents is desirable.
@ -75,6 +75,7 @@ are as follows:
* :class:`~mongoengine.fields.DynamicField` * :class:`~mongoengine.fields.DynamicField`
* :class:`~mongoengine.fields.EmailField` * :class:`~mongoengine.fields.EmailField`
* :class:`~mongoengine.fields.EmbeddedDocumentField` * :class:`~mongoengine.fields.EmbeddedDocumentField`
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
* :class:`~mongoengine.fields.FileField` * :class:`~mongoengine.fields.FileField`
* :class:`~mongoengine.fields.FloatField` * :class:`~mongoengine.fields.FloatField`
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField` * :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
@ -149,7 +150,7 @@ arguments can be set on all fields:
.. note:: If set, this field is also accessible through the `pk` field. .. note:: If set, this field is also accessible through the `pk` field.
:attr:`choices` (Default: None) :attr:`choices` (Default: None)
An iterable (e.g. a list or tuple) of choices to which the value of this An iterable (e.g. list, tuple or set) of choices to which the value of this
field should be limited. field should be limited.
Can be either be a nested tuples of value (stored in mongo) and a Can be either be a nested tuples of value (stored in mongo) and a
@ -172,11 +173,11 @@ arguments can be set on all fields:
class Shirt(Document): class Shirt(Document):
size = StringField(max_length=3, choices=SIZE) size = StringField(max_length=3, choices=SIZE)
:attr:`help_text` (Default: None) :attr:`**kwargs` (Optional)
Optional help text to output with the field -- used by form libraries You can supply additional metadata as arbitrary additional keyword
arguments. You can not override existing attributes, however. Common
:attr:`verbose_name` (Default: None) choices include `help_text` and `verbose_name`, commonly used by form and
Optional human-readable name for the field -- used by form libraries widget libraries.
List fields List fields
@ -213,9 +214,9 @@ 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
this is recommended as dictionaries don't support validation or custom field embedded documents are recommended as dictionaries dont support validation
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::
class SurveyResponse(Document): class SurveyResponse(Document):
@ -360,11 +361,6 @@ Its value can take any of the following constants:
In Django, be sure to put all apps that have such delete rule declarations in In Django, be sure to put all apps that have such delete rule declarations in
their :file:`models.py` in the :const:`INSTALLED_APPS` tuple. their :file:`models.py` in the :const:`INSTALLED_APPS` tuple.
.. warning::
Signals are not triggered when doing cascading updates / deletes - if this
is required you must manually handle the update / delete.
Generic reference fields Generic reference fields
'''''''''''''''''''''''' ''''''''''''''''''''''''
A second kind of reference field also exists, A second kind of reference field also exists,

View File

@ -13,3 +13,4 @@ User Guide
gridfs gridfs
signals signals
text-indexes text-indexes
mongomock

21
docs/guide/mongomock.rst Normal file
View File

@ -0,0 +1,21 @@
==============================
Use mongomock for testing
==============================
`mongomock <https://github.com/vmalloc/mongomock/>`_ is a package to do just
what the name implies, mocking a mongo database.
To use with mongoengine, simply specify mongomock when connecting with
mongoengine:
.. code-block:: python
connect('mongoenginetest', host='mongomock://localhost')
conn = get_connection()
or with an alias:
.. code-block:: python
connect('mongoenginetest', host='mongomock://localhost', alias='testdb')
conn = get_connection('testdb')

View File

@ -237,7 +237,7 @@ is preferred for achieving this::
# All except for the first 5 people # All except for the first 5 people
users = User.objects[5:] users = User.objects[5:]
# 5 users, starting from the 10th user found # 5 users, starting from the 11th user found
users = User.objects[10:15] users = User.objects[10:15]
You may also index the query to retrieve a single result. If an item at that You may also index the query to retrieve a single result. If an item at that
@ -347,6 +347,8 @@ way of achieving this::
num_users = len(User.objects) num_users = len(User.objects)
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`.
Further aggregation Further aggregation
------------------- -------------------
You may sum over the values of a specific field on documents using You may sum over the values of a specific field on documents using
@ -477,6 +479,8 @@ operators. To use a :class:`~mongoengine.queryset.Q` object, pass it in as the
first positional argument to :attr:`Document.objects` when you filter it by first positional argument to :attr:`Document.objects` when you filter it by
calling it with keyword arguments:: calling it with keyword arguments::
from mongoengine.queryset.visitor import Q
# Get published posts # Get published posts
Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now())) Post.objects(Q(published=True) | Q(publish_date__lte=datetime.now()))

View File

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

View File

@ -17,7 +17,7 @@ Use the *$* prefix to set a text index, Look the declaration::
meta = {'indexes': [ meta = {'indexes': [
{'fields': ['$title', "$content"], {'fields': ['$title', "$content"],
'default_language': 'english', 'default_language': 'english',
'weight': {'title': 10, 'content': 2} 'weights': {'title': 10, 'content': 2}
} }
]} ]}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,21 +4,17 @@ import weakref
from bson import DBRef, ObjectId, SON from bson import DBRef, ObjectId, SON
import pymongo import pymongo
import six
from mongoengine.base.common import UPDATE_OPERATORS
from mongoengine.base.datastructures import (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
from mongoengine.base.common import ALLOW_INHERITANCE
from mongoengine.base.datastructures import (
BaseDict, BaseList, EmbeddedDocumentList
)
__all__ = ("BaseField", "ComplexBaseField",
"ObjectIdField", "GeoJsonBaseField")
UPDATE_OPERATORS = set(['set', 'unset', 'inc', 'dec', 'pop', 'push', __all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
'push_all', 'pull', 'pull_all', 'add_to_set', 'GeoJsonBaseField')
'set_on_insert', 'min', 'max'])
class BaseField(object): class BaseField(object):
@ -27,7 +23,6 @@ class BaseField(object):
.. versionchanged:: 0.5 - added verbose and help text .. versionchanged:: 0.5 - added verbose and help text
""" """
name = None name = None
_geo_index = False _geo_index = False
_auto_gen = False # Call `generate` to generate a value _auto_gen = False # Call `generate` to generate a value
@ -41,12 +36,12 @@ class BaseField(object):
def __init__(self, db_field=None, name=None, required=False, default=None, def __init__(self, db_field=None, name=None, required=False, default=None,
unique=False, unique_with=None, primary_key=False, unique=False, unique_with=None, primary_key=False,
validation=None, choices=None, verbose_name=None, validation=None, choices=None, null=False, sparse=False,
help_text=None, null=False, sparse=False, custom_data=None): **kwargs):
""" """
:param db_field: The database field to store this field in :param db_field: The database field to store this field in
(defaults to the name of the field) (defaults to the name of the field)
:param name: Depreciated - use db_field :param name: Deprecated - use db_field
:param required: If the field is required. Whether it has to have a :param required: If the field is required. Whether it has to have a
value or not. Defaults to False. value or not. Defaults to False.
:param default: (optional) The default value for this field if no value :param default: (optional) The default value for this field if no value
@ -60,21 +55,20 @@ class BaseField(object):
field. Generally this is deprecated in favour of the field. Generally this is deprecated in favour of the
`FIELD.validate` method `FIELD.validate` method
:param choices: (optional) The valid choices :param choices: (optional) The valid choices
:param verbose_name: (optional) The verbose name for the field.
Designed to be human readable and is often used when generating
model forms from the document model.
:param help_text: (optional) The help text for this field and is often
used when generating model forms from the document model.
:param null: (optional) Is the field value can be null. If no and there is a default value :param null: (optional) Is the field value can be null. If no and there is a default value
then the default value is set then the default value is set
:param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False` :param sparse: (optional) `sparse=True` combined with `unique=True` and `required=False`
means that uniqueness won't be enforced for `None` values means that uniqueness won't be enforced for `None` values
:param custom_data: (optional) Custom metadata for this field. :param **kwargs: (optional) Arbitrary indirection-free metadata for
this field can be supplied as additional keyword arguments and
accessed as attributes of the field. Must not conflict with any
existing attributes. Common metadata includes `verbose_name` and
`help_text`.
""" """
self.db_field = (db_field or name) if not primary_key else '_id' self.db_field = (db_field or name) if not primary_key else '_id'
if name: if name:
msg = "Fields' 'name' attribute deprecated in favour of 'db_field'" msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
self.required = required or primary_key self.required = required or primary_key
self.default = default self.default = default
@ -83,12 +77,30 @@ class BaseField(object):
self.primary_key = primary_key self.primary_key = primary_key
self.validation = validation self.validation = validation
self.choices = choices self.choices = choices
self.verbose_name = verbose_name
self.help_text = help_text
self.null = null self.null = null
self.sparse = sparse self.sparse = sparse
self._owner_document = None self._owner_document = None
self.custom_data = custom_data
# Validate the db_field
if isinstance(self.db_field, six.string_types) and (
'.' in self.db_field or
'\0' in self.db_field or
self.db_field.startswith('$')
):
raise ValueError(
'field names cannot contain dots (".") or null characters '
'("\\0"), and they must not start with a dollar sign ("$").'
)
# Detect and report conflicts between metadata and base properties.
conflicts = set(dir(self)) & set(kwargs)
if conflicts:
raise TypeError('%s already has attribute(s): %s' % (
self.__class__.__name__, ', '.join(conflicts)))
# Assign metadata to the instance
# This efficient method is available because no __slots__ are defined.
self.__dict__.update(kwargs)
# Adjust the appropriate creation counter, and save our local copy. # Adjust the appropriate creation counter, and save our local copy.
if self.db_field == '_id': if self.db_field == '_id':
@ -127,7 +139,7 @@ class BaseField(object):
if (self.name not in instance._data or if (self.name not in instance._data or
instance._data[self.name] != value): instance._data[self.name] != value):
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
except: except Exception:
# Values cant be compared eg: naive and tz datetimes # Values cant be compared eg: naive and tz datetimes
# So mark it as changed # So mark it as changed
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
@ -135,34 +147,45 @@ class BaseField(object):
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument): if isinstance(value, EmbeddedDocument):
value._instance = weakref.proxy(instance) value._instance = weakref.proxy(instance)
elif isinstance(value, (list, tuple)):
for v in value:
if isinstance(v, EmbeddedDocument):
v._instance = weakref.proxy(instance)
instance._data[self.name] = value instance._data[self.name] = value
def error(self, message="", errors=None, field_name=None): def error(self, message='', errors=None, field_name=None):
"""Raises a ValidationError. """Raise a ValidationError."""
"""
field_name = field_name if field_name else self.name field_name = field_name if field_name else self.name
raise ValidationError(message, errors=errors, field_name=field_name) raise ValidationError(message, errors=errors, field_name=field_name)
def to_python(self, value): def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type. """Convert a MongoDB-compatible type to a Python type."""
"""
return value return value
def to_mongo(self, value): def to_mongo(self, value):
"""Convert a Python type to a MongoDB-compatible type. """Convert a Python type to a MongoDB-compatible type."""
"""
return self.to_python(value) return self.to_python(value)
def _to_mongo_safe_call(self, value, use_db_field=True, fields=None):
"""Helper method to call to_mongo with proper inputs."""
f_inputs = self.to_mongo.__code__.co_varnames
ex_vars = {}
if 'fields' in f_inputs:
ex_vars['fields'] = fields
if 'use_db_field' in f_inputs:
ex_vars['use_db_field'] = use_db_field
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):
@ -170,19 +193,21 @@ class BaseField(object):
EmbeddedDocument = _import_class('EmbeddedDocument') EmbeddedDocument = _import_class('EmbeddedDocument')
choice_list = self.choices choice_list = self.choices
if isinstance(choice_list[0], (list, tuple)): if isinstance(next(iter(choice_list)), (list, tuple)):
# next(iter) is useful for sets
choice_list = [k for k, _ in choice_list] choice_list = [k for k, _ in choice_list]
# Choices which are other types of Documents # Choices which are other types of Documents
if isinstance(value, (Document, EmbeddedDocument)): if isinstance(value, (Document, EmbeddedDocument)):
if not any(isinstance(value, c) for c in choice_list): if not any(isinstance(value, c) for c in choice_list):
self.error( self.error(
'Value must be instance of %s' % unicode(choice_list) 'Value must be an instance of %s' % (
six.text_type(choice_list)
)
) )
# Choices which are types other than Documents # Choices which are types other than Documents
elif value not in choice_list: elif value not in choice_list:
self.error('Value must be one of %s' % unicode(choice_list)) self.error('Value must be one of %s' % six.text_type(choice_list))
def _validate(self, value, **kwargs): def _validate(self, value, **kwargs):
# Check the Choices Constraint # Check the Choices Constraint
@ -225,8 +250,7 @@ class ComplexBaseField(BaseField):
field = None field = None
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor to automatically dereference references. """Descriptor to automatically dereference references."""
"""
if instance is None: if instance is None:
# Document class being used rather than a document object # Document class being used rather than a document object
return self return self
@ -238,7 +262,7 @@ class ComplexBaseField(BaseField):
(self.field is None or isinstance(self.field, (self.field is None or isinstance(self.field,
(GenericReferenceField, ReferenceField)))) (GenericReferenceField, ReferenceField))))
_dereference = _import_class("DeReference")() _dereference = _import_class('DeReference')()
self._auto_dereference = instance._fields[self.name]._auto_dereference self._auto_dereference = instance._fields[self.name]._auto_dereference
if instance._initialised and dereference and instance._data.get(self.name): if instance._initialised and dereference and instance._data.get(self.name):
@ -273,11 +297,8 @@ class ComplexBaseField(BaseField):
return value return value
def to_python(self, value): def to_python(self, value):
"""Convert a MongoDB-compatible type to a Python type. """Convert a MongoDB-compatible type to a Python type."""
""" if isinstance(value, six.string_types):
Document = _import_class('Document')
if isinstance(value, basestring):
return value return value
if hasattr(value, 'to_python'): if hasattr(value, 'to_python'):
@ -287,15 +308,16 @@ class ComplexBaseField(BaseField):
if not hasattr(value, 'items'): if not hasattr(value, 'items'):
try: try:
is_list = True is_list = True
value = dict([(k, v) for k, v in enumerate(value)]) value = {k: v for k, v in enumerate(value)}
except TypeError: # Not iterable return the value except TypeError: # Not iterable return the value
return value return value
if self.field: if self.field:
self.field._auto_dereference = self._auto_dereference self.field._auto_dereference = self._auto_dereference
value_dict = dict([(key, self.field.to_python(item)) value_dict = {key: self.field.to_python(item)
for key, item in value.items()]) for key, item in value.items()}
else: else:
Document = _import_class('Document')
value_dict = {} value_dict = {}
for k, v in value.items(): for k, v in value.items():
if isinstance(v, Document): if isinstance(v, Document):
@ -315,21 +337,20 @@ class ComplexBaseField(BaseField):
key=operator.itemgetter(0))] key=operator.itemgetter(0))]
return value_dict return value_dict
def to_mongo(self, value): def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB-compatible type. """Convert a Python type to a MongoDB-compatible type."""
""" Document = _import_class('Document')
Document = _import_class("Document") EmbeddedDocument = _import_class('EmbeddedDocument')
EmbeddedDocument = _import_class("EmbeddedDocument") GenericReferenceField = _import_class('GenericReferenceField')
GenericReferenceField = _import_class("GenericReferenceField")
if isinstance(value, basestring): if isinstance(value, six.string_types):
return value return value
if hasattr(value, 'to_mongo'): if hasattr(value, 'to_mongo'):
if isinstance(value, Document): if isinstance(value, Document):
return GenericReferenceField().to_mongo(value) return GenericReferenceField().to_mongo(value)
cls = value.__class__ cls = value.__class__
val = value.to_mongo() val = value.to_mongo(use_db_field, fields)
# If it's a document that is not inherited add _cls # If it's a document that is not inherited add _cls
if isinstance(value, EmbeddedDocument): if isinstance(value, EmbeddedDocument):
val['_cls'] = cls.__name__ val['_cls'] = cls.__name__
@ -339,13 +360,15 @@ class ComplexBaseField(BaseField):
if not hasattr(value, 'items'): if not hasattr(value, 'items'):
try: try:
is_list = True is_list = True
value = dict([(k, v) for k, v in enumerate(value)]) value = {k: v for k, v in enumerate(value)}
except TypeError: # Not iterable return the value except TypeError: # Not iterable return the value
return value return value
if self.field: if self.field:
value_dict = dict([(key, self.field.to_mongo(item)) value_dict = {
for key, item in value.iteritems()]) key: self.field._to_mongo_safe_call(item, use_db_field, fields)
for key, item in value.iteritems()
}
else: else:
value_dict = {} value_dict = {}
for k, v in value.iteritems(): for k, v in value.iteritems():
@ -359,9 +382,7 @@ class ComplexBaseField(BaseField):
# any _cls data so make it a generic reference allows # any _cls data so make it a generic reference allows
# us to dereference # us to dereference
meta = getattr(v, '_meta', {}) meta = getattr(v, '_meta', {})
allow_inheritance = ( allow_inheritance = meta.get('allow_inheritance')
meta.get('allow_inheritance', ALLOW_INHERITANCE)
is True)
if not allow_inheritance and not self.field: if not allow_inheritance and not self.field:
value_dict[k] = GenericReferenceField().to_mongo(v) value_dict[k] = GenericReferenceField().to_mongo(v)
else: else:
@ -369,13 +390,13 @@ class ComplexBaseField(BaseField):
value_dict[k] = DBRef(collection, v.pk) value_dict[k] = DBRef(collection, v.pk)
elif hasattr(v, 'to_mongo'): elif hasattr(v, 'to_mongo'):
cls = v.__class__ cls = v.__class__
val = v.to_mongo() val = v.to_mongo(use_db_field, fields)
# If it's a document that is not inherited add _cls # If it's a document that is not inherited add _cls
if isinstance(v, (Document, EmbeddedDocument)): if isinstance(v, (Document, EmbeddedDocument)):
val['_cls'] = cls.__name__ val['_cls'] = cls.__name__
value_dict[k] = val value_dict[k] = val
else: else:
value_dict[k] = self.to_mongo(v) value_dict[k] = self.to_mongo(v, use_db_field, fields)
if is_list: # Convert back to a list if is_list: # Convert back to a list
return [v for _, v in sorted(value_dict.items(), return [v for _, v in sorted(value_dict.items(),
@ -383,8 +404,7 @@ class ComplexBaseField(BaseField):
return value_dict return value_dict
def validate(self, value): def validate(self, value):
"""If field is provided ensure the value is valid. """If field is provided ensure the value is valid."""
"""
errors = {} errors = {}
if self.field: if self.field:
if hasattr(value, 'iteritems') or hasattr(value, 'items'): if hasattr(value, 'iteritems') or hasattr(value, 'items'):
@ -394,9 +414,9 @@ class ComplexBaseField(BaseField):
for k, v in sequence: for k, v in sequence:
try: try:
self.field._validate(v) self.field._validate(v)
except ValidationError, error: except ValidationError as error:
errors[k] = error.errors or error errors[k] = error.errors or error
except (ValueError, AssertionError), error: except (ValueError, AssertionError) as error:
errors[k] = error errors[k] = error
if errors: if errors:
@ -422,24 +442,23 @@ 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:
if not isinstance(value, ObjectId): if not isinstance(value, ObjectId):
value = ObjectId(value) value = ObjectId(value)
except: except Exception:
pass pass
return value return value
def to_mongo(self, value): def to_mongo(self, value):
if not isinstance(value, ObjectId): if not isinstance(value, ObjectId):
try: try:
return ObjectId(unicode(value)) return ObjectId(six.text_type(value))
except Exception, e: except Exception as e:
# e.message attribute has been deprecated since Python 2.6 # e.message attribute has been deprecated since Python 2.6
self.error(unicode(e)) self.error(six.text_type(e))
return value return value
def prepare_query_value(self, op, value): def prepare_query_value(self, op, value):
@ -447,8 +466,8 @@ class ObjectIdField(BaseField):
def validate(self, value): def validate(self, value):
try: try:
ObjectId(unicode(value)) ObjectId(six.text_type(value))
except: except Exception:
self.error('Invalid Object ID') self.error('Invalid Object ID')
@ -459,21 +478,20 @@ class GeoJsonBaseField(BaseField):
""" """
_geo_index = pymongo.GEOSPHERE _geo_index = pymongo.GEOSPHERE
_type = "GeoBase" _type = 'GeoBase'
def __init__(self, auto_index=True, *args, **kwargs): def __init__(self, auto_index=True, *args, **kwargs):
""" """
:param bool auto_index: Automatically create a "2dsphere" index.\ :param bool auto_index: Automatically create a '2dsphere' index.\
Defaults to `True`. Defaults to `True`.
""" """
self._name = "%sField" % self._type self._name = '%sField' % self._type
if not auto_index: if not auto_index:
self._geo_index = False self._geo_index = False
super(GeoJsonBaseField, self).__init__(*args, **kwargs) super(GeoJsonBaseField, self).__init__(*args, **kwargs)
def validate(self, value): def validate(self, value):
"""Validate the GeoJson object based on its type """Validate the GeoJson object based on its type."""
"""
if isinstance(value, dict): if isinstance(value, dict):
if set(value.keys()) == set(['type', 'coordinates']): if set(value.keys()) == set(['type', 'coordinates']):
if value['type'] != self._type: if value['type'] != self._type:
@ -488,7 +506,7 @@ class GeoJsonBaseField(BaseField):
self.error('%s can only accept lists of [x, y]' % self._name) self.error('%s can only accept lists of [x, y]' % self._name)
return return
validate = getattr(self, "_validate_%s" % self._type.lower()) validate = getattr(self, '_validate_%s' % self._type.lower())
error = validate(value) error = validate(value)
if error: if error:
self.error(error) self.error(error)
@ -500,8 +518,8 @@ class GeoJsonBaseField(BaseField):
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0] value[0][0][0]
except: 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:
@ -512,20 +530,20 @@ class GeoJsonBaseField(BaseField):
errors.append(error) errors.append(error)
if errors: if errors:
if top_level: if top_level:
return "Invalid Polygon:\n%s" % ", ".join(errors) return 'Invalid Polygon:\n%s' % ', '.join(errors)
else: else:
return "%s" % ", ".join(errors) return '%s' % ', '.join(errors)
def _validate_linestring(self, value, top_level=True): def _validate_linestring(self, value, top_level=True):
"""Validates a linestring""" """Validate a linestring."""
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
return 'LineStrings must contain list of coordinate pairs' return 'LineStrings must contain list of coordinate pairs'
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0] value[0][0]
except: 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:
@ -534,19 +552,19 @@ class GeoJsonBaseField(BaseField):
errors.append(error) errors.append(error)
if errors: if errors:
if top_level: if top_level:
return "Invalid LineString:\n%s" % ", ".join(errors) return 'Invalid LineString:\n%s' % ', '.join(errors)
else: else:
return "%s" % ", ".join(errors) return '%s' % ', '.join(errors)
def _validate_point(self, value): def _validate_point(self, value):
"""Validate each set of coords""" """Validate each set of coords"""
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
return 'Points must be a list of coordinate pairs' return 'Points must be a list of coordinate pairs'
elif not len(value) == 2: elif not len(value) == 2:
return "Value (%s) must be a two-dimensional point" % repr(value) return 'Value (%s) must be a two-dimensional point' % repr(value)
elif (not isinstance(value[0], (float, int)) or elif (not isinstance(value[0], (float, int)) or
not isinstance(value[1], (float, int))): not isinstance(value[1], (float, int))):
return "Both values (%s) in point must be float or int" % repr(value) return 'Both values (%s) in point must be float or int' % repr(value)
def _validate_multipoint(self, value): def _validate_multipoint(self, value):
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
@ -555,8 +573,8 @@ class GeoJsonBaseField(BaseField):
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0] value[0][0]
except: 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:
@ -565,7 +583,7 @@ class GeoJsonBaseField(BaseField):
errors.append(error) errors.append(error)
if errors: if errors:
return "%s" % ", ".join(errors) return '%s' % ', '.join(errors)
def _validate_multilinestring(self, value, top_level=True): def _validate_multilinestring(self, value, top_level=True):
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
@ -574,8 +592,8 @@ class GeoJsonBaseField(BaseField):
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0] value[0][0][0]
except: 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:
@ -585,9 +603,9 @@ class GeoJsonBaseField(BaseField):
if errors: if errors:
if top_level: if top_level:
return "Invalid MultiLineString:\n%s" % ", ".join(errors) return 'Invalid MultiLineString:\n%s' % ', '.join(errors)
else: else:
return "%s" % ", ".join(errors) return '%s' % ', '.join(errors)
def _validate_multipolygon(self, value): def _validate_multipolygon(self, value):
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
@ -596,8 +614,8 @@ class GeoJsonBaseField(BaseField):
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0][0] value[0][0][0][0]
except: 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:
@ -606,9 +624,9 @@ class GeoJsonBaseField(BaseField):
errors.append(error) errors.append(error)
if errors: if errors:
return "Invalid MultiPolygon:\n%s" % ", ".join(errors) return 'Invalid MultiPolygon:\n%s' % ', '.join(errors)
def to_mongo(self, value): def to_mongo(self, value):
if isinstance(value, dict): if isinstance(value, dict):
return value return value
return SON([("type", self._type), ("coordinates", value)]) return SON([('type', self._type), ('coordinates', value)])

View File

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

View File

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

View File

@ -1,11 +1,14 @@
from pymongo import MongoClient, ReadPreference, uri_parser from pymongo import MongoClient, ReadPreference, uri_parser
import six
from mongoengine.python_support import IS_PYMONGO_3 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:
@ -13,7 +16,10 @@ else:
READ_PREFERENCE = False READ_PREFERENCE = False
class ConnectionError(Exception): class MongoEngineConnectionError(Exception):
"""Error raised when the database connection can't be established or
when a connection with a requested alias can't be retrieved.
"""
pass pass
@ -24,7 +30,9 @@ _dbs = {}
def register_connection(alias, name=None, host=None, port=None, def register_connection(alias, name=None, host=None, port=None,
read_preference=READ_PREFERENCE, read_preference=READ_PREFERENCE,
username=None, password=None, authentication_source=None, username=None, password=None,
authentication_source=None,
authentication_mechanism=None,
**kwargs): **kwargs):
"""Add a connection. """Add a connection.
@ -38,11 +46,17 @@ def register_connection(alias, name=None, host=None, port=None,
:param username: username to authenticate with :param username: username to authenticate with
:param password: password to authenticate with :param password: password to authenticate with
:param authentication_source: database to authenticate against :param authentication_source: database to authenticate against
:param kwargs: allow ad-hoc parameters to be passed into the pymongo driver :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
(can also be done by using `mongomock://` as db host prefix)
:param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list.
.. versionchanged:: 0.10.6 - added mongomock support
""" """
global _connection_settings
conn_settings = { conn_settings = {
'name': name or 'test', 'name': name or 'test',
'host': host or 'localhost', 'host': host or 'localhost',
@ -50,23 +64,48 @@ 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
} }
# Handle uri style connections conn_host = conn_settings['host']
if "://" in conn_settings['host']:
uri_dict = uri_parser.parse_uri(conn_settings['host']) # Host can be a list or a string, so if string, force to a list.
conn_settings.update({ if isinstance(conn_host, six.string_types):
'name': uri_dict.get('database') or name, conn_host = [conn_host]
'username': uri_dict.get('username'),
'password': uri_dict.get('password'), resolved_hosts = []
'read_preference': read_preference, for entity in conn_host:
})
# Handle Mongomock
if entity.startswith('mongomock://'):
conn_settings['is_mock'] = True
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
# Handle URI style connections, only updating connection params which
# were explicitly specified in the URI.
elif '://' in entity:
uri_dict = uri_parser.parse_uri(entity)
resolved_hosts.append(entity)
if uri_dict.get('database'):
conn_settings['name'] = uri_dict.get('database')
for param in ('read_preference', 'username', 'password'):
if uri_dict.get(param):
conn_settings[param] = uri_dict[param]
uri_options = uri_dict['options'] uri_options = uri_dict['options']
if 'replicaset' in uri_options: if 'replicaset' in uri_options:
conn_settings['replicaSet'] = True conn_settings['replicaSet'] = uri_options['replicaset']
if 'authsource' in uri_options: if 'authsource' in uri_options:
conn_settings['authentication_source'] = uri_options['authsource'] conn_settings['authentication_source'] = uri_options['authsource']
if 'authmechanism' in uri_options:
conn_settings['authentication_mechanism'] = uri_options['authmechanism']
else:
resolved_hosts.append(entity)
conn_settings['host'] = resolved_hosts
# Deprecated parameters that should not be passed on # Deprecated parameters that should not be passed on
kwargs.pop('slaves', None) kwargs.pop('slaves', None)
@ -77,67 +116,108 @@ def register_connection(alias, name=None, host=None, port=None,
def disconnect(alias=DEFAULT_CONNECTION_NAME): def disconnect(alias=DEFAULT_CONNECTION_NAME):
global _connections """Close the connection with a given alias."""
global _dbs
if alias in _connections: if alias in _connections:
get_connection(alias=alias).disconnect() get_connection(alias=alias).close()
del _connections[alias] del _connections[alias]
if alias in _dbs: if alias in _dbs:
del _dbs[alias] del _dbs[alias]
def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False): def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
global _connections """Return a connection with a given alias."""
# Connect to the database if not already connected # Connect to the database if not already connected
if reconnect: if reconnect:
disconnect(alias) disconnect(alias)
if alias not in _connections: # If the requested alias already exists in the _connections list, return
# it immediately.
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 not in _connection_settings:
msg = 'Connection with alias "%s" has not been defined' % alias
if alias == DEFAULT_CONNECTION_NAME: if alias == DEFAULT_CONNECTION_NAME:
msg = 'You have not defined a default connection' msg = 'You have not defined a default connection'
raise ConnectionError(msg) else:
conn_settings = _connection_settings[alias].copy() msg = 'Connection with alias "%s" has not been defined' % alias
raise MongoEngineConnectionError(msg)
conn_settings.pop('name', None) def _clean_settings(settings_dict):
conn_settings.pop('username', None) irrelevant_fields = set([
conn_settings.pop('password', None) 'name', 'username', 'password', 'authentication_source',
conn_settings.pop('authentication_source', None) 'authentication_mechanism'
])
return {
k: v for k, v in settings_dict.items()
if k not in irrelevant_fields
}
# Retrieve a copy of the connection settings associated with the requested
# alias and remove the database name and authentication info (we don't
# care about them at this point).
conn_settings = _clean_settings(_connection_settings[alias].copy())
# Determine if we should use PyMongo's or mongomock's MongoClient.
is_mock = conn_settings.pop('is_mock', False)
if is_mock:
try:
import mongomock
except ImportError:
raise RuntimeError('You need mongomock installed to mock '
'MongoEngine.')
connection_class = mongomock.MongoClient
else:
connection_class = MongoClient connection_class = MongoClient
if 'replicaSet' in conn_settings:
# 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) 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'])
# 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
try: # Iterate over all of the connection settings and if a connection with
connection = None # the same parameters is already established, use it instead of creating
# check for shared connections # a new one.
existing_connection = None
connection_settings_iterator = ( connection_settings_iterator = (
(db_alias, settings.copy()) for db_alias, settings in _connection_settings.iteritems()) (db_alias, settings.copy())
for db_alias, settings in _connection_settings.items()
)
for db_alias, connection_settings in connection_settings_iterator: for db_alias, connection_settings in connection_settings_iterator:
connection_settings.pop('name', None) connection_settings = _clean_settings(connection_settings)
connection_settings.pop('username', None) if conn_settings == connection_settings and _connections.get(db_alias):
connection_settings.pop('password', None) existing_connection = _connections[db_alias]
if conn_settings == connection_settings and _connections.get(db_alias, None):
connection = _connections[db_alias]
break break
_connections[alias] = connection if connection else connection_class(**conn_settings) # If an existing connection was found, assign it to the new alias
except Exception, e: if existing_connection:
raise ConnectionError("Cannot connect to database %s :\n%s" % (alias, e)) _connections[alias] = existing_connection
else:
# Otherwise, create the new connection for this alias. Raise
# MongoEngineConnectionError if it can't be established.
try:
_connections[alias] = connection_class(**conn_settings)
except Exception as e:
raise MongoEngineConnectionError(
'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)
@ -145,11 +225,13 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
conn = get_connection(alias) conn = get_connection(alias)
conn_settings = _connection_settings[alias] conn_settings = _connection_settings[alias]
db = conn[conn_settings['name']] db = conn[conn_settings['name']]
auth_kwargs = {'source': conn_settings['authentication_source']}
if conn_settings['authentication_mechanism'] is not None:
auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
# Authenticate if necessary # Authenticate if necessary
if conn_settings['username'] and conn_settings['password']: if conn_settings['username'] and (conn_settings['password'] or
db.authenticate(conn_settings['username'], conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
conn_settings['password'], db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
source=conn_settings['authentication_source'])
_dbs[alias] = db _dbs[alias] = db
return _dbs[alias] return _dbs[alias]
@ -164,9 +246,11 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
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)

View File

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

View File

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

View File

@ -1,27 +1,23 @@
import warnings
import pymongo
import re import re
import warnings
from pymongo.read_preferences import ReadPreference
from bson.dbref import DBRef from bson.dbref import DBRef
import pymongo
from pymongo.read_preferences import ReadPreference
import six
from mongoengine import signals from mongoengine import signals
from mongoengine.base import (BaseDict, BaseDocument, BaseList,
DocumentMetaclass, EmbeddedDocumentList,
TopLevelDocumentMetaclass, get_document)
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.base import ( from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
DocumentMetaclass, from mongoengine.context_managers import switch_collection, switch_db
TopLevelDocumentMetaclass, from mongoengine.errors import (InvalidDocumentError, InvalidQueryError,
BaseDocument, SaveConditionError)
BaseDict,
BaseList,
EmbeddedDocumentList,
ALLOW_INHERITANCE,
get_document
)
from mongoengine.errors import InvalidQueryError, InvalidDocumentError
from mongoengine.python_support import IS_PYMONGO_3 from mongoengine.python_support import IS_PYMONGO_3
from mongoengine.queryset import (OperationError, NotUniqueError, from mongoengine.queryset import (NotUniqueError, OperationError,
QuerySet, transform) QuerySet, transform)
from mongoengine.connection import get_db, DEFAULT_CONNECTION_NAME
from mongoengine.context_managers import switch_db, switch_collection
__all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument', __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument',
'DynamicEmbeddedDocument', 'OperationError', 'DynamicEmbeddedDocument', 'OperationError',
@ -29,12 +25,10 @@ __all__ = ('Document', 'EmbeddedDocument', 'DynamicDocument',
def includes_cls(fields): def includes_cls(fields):
""" Helper function used for ensuring and comparing indexes """Helper function used for ensuring and comparing indexes."""
"""
first_field = None first_field = None
if len(fields): if len(fields):
if isinstance(fields[0], basestring): if isinstance(fields[0], six.string_types):
first_field = fields[0] first_field = fields[0]
elif isinstance(fields[0], (list, tuple)) and len(fields[0]): elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
first_field = fields[0][0] first_field = fields[0][0]
@ -55,9 +49,8 @@ class EmbeddedDocument(BaseDocument):
to create a specialised version of the embedded document that will be to create a specialised version of the embedded document that will be
stored in the same collection. To facilitate this behaviour a `_cls` stored in the same collection. To facilitate this behaviour a `_cls`
field is added to documents (hidden though the MongoEngine interface). field is added to documents (hidden though the MongoEngine interface).
To disable this behaviour and remove the dependence on the presence of To enable this behaviour set :attr:`allow_inheritance` to ``True`` in the
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta` :attr:`meta` dictionary.
dictionary.
""" """
__slots__ = ('_instance', ) __slots__ = ('_instance', )
@ -80,6 +73,15 @@ class EmbeddedDocument(BaseDocument):
def __ne__(self, other): def __ne__(self, other):
return not self.__eq__(other) return not self.__eq__(other)
def to_mongo(self, *args, **kwargs):
data = super(EmbeddedDocument, self).to_mongo(*args, **kwargs)
# remove _id from the SON if it's in it and it's None
if '_id' in data and data['_id'] is None:
del data['_id']
return data
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self._instance.save(*args, **kwargs) self._instance.save(*args, **kwargs)
@ -104,9 +106,8 @@ class Document(BaseDocument):
create a specialised version of the document that will be stored in the create a specialised version of the document that will be stored in the
same collection. To facilitate this behaviour a `_cls` same collection. To facilitate this behaviour a `_cls`
field is added to documents (hidden though the MongoEngine interface). field is added to documents (hidden though the MongoEngine interface).
To disable this behaviour and remove the dependence on the presence of To enable this behaviourset :attr:`allow_inheritance` to ``True`` in the
`_cls` set :attr:`allow_inheritance` to ``False`` in the :attr:`meta` :attr:`meta` dictionary.
dictionary.
A :class:`~mongoengine.Document` may use a **Capped Collection** by A :class:`~mongoengine.Document` may use a **Capped Collection** by
specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta` specifying :attr:`max_documents` and :attr:`max_size` in the :attr:`meta`
@ -147,26 +148,22 @@ class Document(BaseDocument):
__slots__ = ('__objects',) __slots__ = ('__objects',)
def pk(): @property
"""Primary key alias def pk(self):
""" """Get the primary key."""
def fget(self):
if 'id_field' not in self._meta: if 'id_field' not in self._meta:
return None return None
return getattr(self, self._meta['id_field']) return getattr(self, self._meta['id_field'])
def fset(self, value): @pk.setter
def pk(self, value):
"""Set the primary key."""
return setattr(self, self._meta['id_field'], 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):
@ -209,31 +206,46 @@ class Document(BaseDocument):
cls.ensure_indexes() cls.ensure_indexes()
return cls._collection return cls._collection
def modify(self, query={}, **update): def to_mongo(self, *args, **kwargs):
data = super(Document, self).to_mongo(*args, **kwargs)
# If '_id' is None, try and set it from self._data. If that
# doesn't exist either, remote '_id' from the SON completely.
if data['_id'] is None:
if self._data.get('id') is None:
del data['_id']
else:
data['_id'] = self._data['id']
return data
def modify(self, query=None, **update):
"""Perform an atomic update of the document in the database and reload """Perform an atomic update of the document in the database and reload
the document object using updated version. the document object using updated version.
Returns True if the document has been updated or False if the document Returns True if the document has been updated or False if the document
in the database doesn't match the query. in the database doesn't match the query.
.. note:: All unsaved changes that has been made to the document are .. note:: All unsaved changes that have been made to the document are
rejected if the method returns True. rejected if the method returns True.
:param query: the update will be performed only if the document in the :param query: the update will be performed only if the document in the
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:
@ -249,7 +261,7 @@ class Document(BaseDocument):
def save(self, force_insert=False, validate=True, clean=True, def save(self, force_insert=False, validate=True, clean=True,
write_concern=None, cascade=None, cascade_kwargs=None, write_concern=None, cascade=None, cascade_kwargs=None,
_refs=None, save_condition=None, **kwargs): _refs=None, save_condition=None, signal_kwargs=None, **kwargs):
"""Save the :class:`~mongoengine.Document` to the database. If the """Save the :class:`~mongoengine.Document` to the database. If the
document already exists, it will be updated, otherwise it will be document already exists, it will be updated, otherwise it will be
created. created.
@ -275,6 +287,8 @@ class Document(BaseDocument):
:param save_condition: only perform save if matching record in db :param save_condition: only perform save if matching record in db
satisfies condition(s) (e.g. version number). satisfies condition(s) (e.g. version number).
Raises :class:`OperationError` if the conditions are not satisfied Raises :class:`OperationError` if the conditions are not satisfied
:parm signal_kwargs: (optional) kwargs dictionary to be passed to
the signal calls.
.. versionchanged:: 0.5 .. versionchanged:: 0.5
In existing documents it only saves changed fields using In existing documents it only saves changed fields using
@ -294,114 +308,159 @@ class Document(BaseDocument):
if the condition is satisfied in the current db record. if the condition is satisfied in the current db record.
.. versionchanged:: 0.10 .. versionchanged:: 0.10
:class:`OperationError` exception raised if save_condition fails. :class:`OperationError` exception raised if save_condition fails.
.. versionchanged:: 0.10.1
:class: save_condition failure now raises a `SaveConditionError`
.. versionchanged:: 0.10.7
Add signal_kwargs argument
""" """
signals.pre_save.send(self.__class__, document=self) if self._meta.get('abstract'):
raise InvalidDocumentError('Cannot save an abstract document.')
signal_kwargs = signal_kwargs or {}
signals.pre_save.send(self.__class__, document=self, **signal_kwargs)
if validate: if validate:
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()
created = ('_id' not in doc or self._created or force_insert) created = ('_id' not in doc or self._created or force_insert)
signals.pre_save_post_validation.send(self.__class__, document=self, signals.pre_save_post_validation.send(self.__class__, document=self,
created=created) created=created, **signal_kwargs)
try:
collection = self._get_collection()
if self._meta.get('auto_create_index', True): if self._meta.get('auto_create_index', True):
self.ensure_indexes() self.ensure_indexes()
try:
# Save a new document or update an existing one
if created: if created:
if force_insert: object_id = self._save_create(doc, force_insert, write_concern)
object_id = collection.insert(doc, **write_concern)
else: else:
object_id = collection.save(doc, **write_concern) object_id, created = self._save_update(doc, save_condition,
# In PyMongo 3.0, the save() call calls internally the _update() call write_concern)
# 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
else:
object_id = doc['_id']
updates, removals = self._delta()
# Need to add shard key to query, or you get an error
if save_condition is not None:
select_dict = transform.query(self.__class__,
**save_condition)
else:
select_dict = {}
select_dict['_id'] = object_id
shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key:
actual_key = self._db_field_map.get(k, k)
select_dict[actual_key] = doc[actual_key]
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['nModified'] == 0:
raise OperationError('Race condition preventing'
' document update detected')
created = is_new_object(last_error)
if cascade is None: if cascade is None:
cascade = self._meta.get( cascade = (self._meta.get('cascade', False) or
'cascade', False) or cascade_kwargs is not None cascade_kwargs is not None)
if cascade: if cascade:
kwargs = { kwargs = {
"force_insert": force_insert, 'force_insert': force_insert,
"validate": validate, 'validate': validate,
"write_concern": write_concern, 'write_concern': write_concern,
"cascade": cascade 'cascade': cascade
} }
if cascade_kwargs: # Allow granular control over cascades if cascade_kwargs: # Allow granular control over cascades
kwargs.update(cascade_kwargs) kwargs.update(cascade_kwargs)
kwargs['_refs'] = _refs kwargs['_refs'] = _refs
self.cascade_save(**kwargs) self.cascade_save(**kwargs)
except pymongo.errors.DuplicateKeyError, err:
except pymongo.errors.DuplicateKeyError as err:
message = u'Tried to save duplicate unique keys (%s)' message = u'Tried to save duplicate unique keys (%s)'
raise NotUniqueError(message % unicode(err)) raise NotUniqueError(message % six.text_type(err))
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure as err:
message = 'Could not save document (%s)' message = 'Could not save document (%s)'
if re.match('^E1100[01] duplicate key', unicode(err)): if re.match('^E1100[01] duplicate key', six.text_type(err)):
# E11000 - duplicate key error index # E11000 - duplicate key error index
# E11001 - duplicate key on update # E11001 - duplicate key on update
message = u'Tried to save duplicate unique keys (%s)' message = u'Tried to save duplicate unique keys (%s)'
raise NotUniqueError(message % unicode(err)) raise NotUniqueError(message % six.text_type(err))
raise OperationError(message % unicode(err)) raise OperationError(message % six.text_type(err))
# Make sure we store the PK on this document now that it's saved
id_field = self._meta['id_field'] id_field = self._meta['id_field']
if created or id_field not in self._meta.get('shard_key', []): if created or id_field not in self._meta.get('shard_key', []):
self[id_field] = self._fields[id_field].to_python(object_id) self[id_field] = self._fields[id_field].to_python(object_id)
signals.post_save.send(self.__class__, document=self, created=created) signals.post_save.send(self.__class__, document=self,
created=created, **signal_kwargs)
self._clear_changed_fields() self._clear_changed_fields()
self._created = False self._created = False
return self return self
def cascade_save(self, *args, **kwargs): def _save_create(self, doc, force_insert, write_concern):
"""Recursively saves any references / """Save a new document.
generic references on an objects"""
_refs = kwargs.get('_refs', []) or [] Helper method, should only be used inside save().
"""
collection = self._get_collection()
if force_insert:
return collection.insert(doc, **write_concern)
object_id = collection.save(doc, **write_concern)
# In PyMongo 3.0, the save() call calls internally the _update() call
# but they forget to return the _id value passed back, therefore getting it back here
# Correct behaviour in 2.X and in 3.0.1+ versions
if not object_id and pymongo.version_tuple == (3, 0):
pk_as_mongo_obj = self._fields.get(self._meta['id_field']).to_mongo(self.pk)
object_id = (
self._qs.filter(pk=pk_as_mongo_obj).first() and
self._qs.filter(pk=pk_as_mongo_obj).first().pk
) # TODO doesn't this make 2 queries?
return object_id
def _save_update(self, doc, save_condition, write_concern):
"""Update an existing document.
Helper method, should only be used inside save().
"""
collection = self._get_collection()
object_id = doc['_id']
created = False
select_dict = {}
if save_condition is not None:
select_dict = transform.query(self.__class__, **save_condition)
select_dict['_id'] = object_id
# Need to add shard key to query, or you get an error
shard_key = self._meta.get('shard_key', tuple())
for k in shard_key:
path = self._lookup_field(k.split('.'))
actual_key = [p.db_field for p in path]
val = doc
for ak in actual_key:
val = val[ak]
select_dict['.'.join(actual_key)] = val
updates, removals = self._delta()
update_query = {}
if updates:
update_query['$set'] = updates
if removals:
update_query['$unset'] = removals
if updates or removals:
upsert = save_condition is None
last_error = collection.update(select_dict, update_query,
upsert=upsert, **write_concern)
if not upsert and last_error['n'] == 0:
raise SaveConditionError('Race condition preventing'
' document update detected')
if last_error is not None:
updated_existing = last_error.get('updatedExisting')
if updated_existing is False:
created = True
# !!! This is bad, means we accidentally created a new,
# potentially corrupted document. See
# https://github.com/MongoEngine/mongoengine/issues/564
return object_id, created
def cascade_save(self, **kwargs):
"""Recursively save any references and generic references on the
document.
"""
_refs = kwargs.get('_refs') or []
ReferenceField = _import_class('ReferenceField') ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class('GenericReferenceField') GenericReferenceField = _import_class('GenericReferenceField')
@ -427,21 +486,27 @@ class Document(BaseDocument):
@property @property
def _qs(self): def _qs(self):
""" """Return the queryset to use for updating / reloading / deletions."""
Returns the queryset to use for updating / reloading / deletions
"""
if not hasattr(self, '__objects'): if not hasattr(self, '__objects'):
self.__objects = QuerySet(self, self._get_collection()) self.__objects = QuerySet(self, self._get_collection())
return self.__objects return self.__objects
@property @property
def _object_key(self): def _object_key(self):
"""Dict to identify object in collection """Get the query dict that can be used to fetch this object from
the database. Most of the time it's a simple PK lookup, but in
case of a sharded collection with a compound shard key, it can
contain a more complex query.
""" """
select_dict = {'pk': self.pk} select_dict = {'pk': self.pk}
shard_key = self.__class__._meta.get('shard_key', tuple()) shard_key = self.__class__._meta.get('shard_key', tuple())
for k in shard_key: for k in shard_key:
select_dict[k] = getattr(self, k) path = self._lookup_field(k.split('.'))
actual_key = [p.db_field for p in path]
val = self
for ak in actual_key:
val = getattr(val, ak)
select_dict['__'.join(actual_key)] = val
return select_dict return select_dict
def update(self, **kwargs): def update(self, **kwargs):
@ -451,11 +516,11 @@ class Document(BaseDocument):
Raises :class:`OperationError` if called on an object that has not yet Raises :class:`OperationError` if called on an object that has not yet
been saved. been saved.
""" """
if not self.pk: if self.pk is None:
if kwargs.get('upsert', False): if kwargs.get('upsert', False):
query = self.to_mongo() query = self.to_mongo()
if "_cls" in query: if '_cls' in query:
del query["_cls"] del query['_cls']
return self._qs.filter(**query).update_one(**kwargs) return self._qs.filter(**query).update_one(**kwargs)
else: else:
raise OperationError( raise OperationError(
@ -464,18 +529,24 @@ class Document(BaseDocument):
# Need to add shard key to query, or you get an error # Need to add shard key to query, or you get an error
return self._qs.filter(**self._object_key).update_one(**kwargs) return self._qs.filter(**self._object_key).update_one(**kwargs)
def delete(self, **write_concern): def delete(self, signal_kwargs=None, **write_concern):
"""Delete the :class:`~mongoengine.Document` from the database. This """Delete the :class:`~mongoengine.Document` from the database. This
will only take effect if the document has been previously saved. will only take effect if the document has been previously saved.
:parm signal_kwargs: (optional) kwargs dictionary to be passed to
the signal calls.
:param write_concern: Extra keyword arguments are passed down which :param write_concern: Extra keyword arguments are passed down which
will be used as options for the resultant will be used as options for the resultant
``getLastError`` command. For example, ``getLastError`` command. For example,
``save(..., write_concern={w: 2, fsync: True}, ...)`` will ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
wait until at least two servers have recorded the write and wait until at least two servers have recorded the write and
will force an fsync on the primary server. will force an fsync on the primary server.
.. versionchanged:: 0.10.7
Add signal_kwargs argument
""" """
signals.pre_delete.send(self.__class__, document=self) signal_kwargs = signal_kwargs or {}
signals.pre_delete.send(self.__class__, document=self, **signal_kwargs)
# Delete FileFields separately # Delete FileFields separately
FileField = _import_class('FileField') FileField = _import_class('FileField')
@ -486,10 +557,10 @@ class Document(BaseDocument):
try: try:
self._qs.filter( self._qs.filter(
**self._object_key).delete(write_concern=write_concern, _from_doc_delete=True) **self._object_key).delete(write_concern=write_concern, _from_doc_delete=True)
except pymongo.errors.OperationFailure, err: except pymongo.errors.OperationFailure as err:
message = u'Could not delete document (%s)' % err.message message = u'Could not delete document (%s)' % err.message
raise OperationError(message) raise OperationError(message)
signals.post_delete.send(self.__class__, document=self) signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
def switch_db(self, db_alias, keep_created=True): def switch_db(self, db_alias, keep_created=True):
""" """
@ -574,11 +645,12 @@ class Document(BaseDocument):
if fields and isinstance(fields[0], int): if fields and isinstance(fields[0], int):
max_depth = fields[0] max_depth = fields[0]
fields = fields[1:] fields = fields[1:]
elif "max_depth" in kwargs: elif 'max_depth' in kwargs:
max_depth = kwargs["max_depth"] max_depth = kwargs['max_depth']
if self.pk is None:
raise self.DoesNotExist('Document does not exist')
if not self.pk:
raise self.DoesNotExist("Document does not exist")
obj = self._qs.read_preference(ReadPreference.PRIMARY).filter( obj = self._qs.read_preference(ReadPreference.PRIMARY).filter(
**self._object_key).only(*fields).limit( **self._object_key).only(*fields).limit(
1).select_related(max_depth=max_depth) 1).select_related(max_depth=max_depth)
@ -586,12 +658,17 @@ 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 self._fields_ordered: for field in obj._data:
if not fields or field in fields: if not fields or field in fields:
try: try:
setattr(self, field, self._reload(field, obj[field])) setattr(self, field, self._reload(field, obj[field]))
except (KeyError, AttributeError):
try:
# If field is a special field, e.g. items is stored as _reserved_items,
# an KeyError is thrown. So try to retrieve the field from _data
setattr(self, field, self._reload(field, obj._data.get(field)))
except KeyError: except KeyError:
# If field is removed from the database while the object # If field is removed from the database while the object
# is in memory, a reload would cause a KeyError # is in memory, a reload would cause a KeyError
@ -623,8 +700,8 @@ class Document(BaseDocument):
def to_dbref(self): def to_dbref(self):
"""Returns an instance of :class:`~bson.dbref.DBRef` useful in """Returns an instance of :class:`~bson.dbref.DBRef` useful in
`__raw__` queries.""" `__raw__` queries."""
if not self.pk: if self.pk is None:
msg = "Only saved documents can have a valid dbref" msg = 'Only saved documents can have a valid dbref'
raise OperationError(msg) raise OperationError(msg)
return DBRef(self.__class__._get_collection_name(), self.pk) return DBRef(self.__class__._get_collection_name(), self.pk)
@ -650,10 +727,20 @@ class Document(BaseDocument):
def drop_collection(cls): def drop_collection(cls):
"""Drops the entire collection associated with this """Drops the entire collection associated with this
:class:`~mongoengine.Document` type from the database. :class:`~mongoengine.Document` type from the database.
Raises :class:`OperationError` if the document has no collection set
(i.g. if it is `abstract`)
.. versionchanged:: 0.10.7
:class:`OperationError` exception raised if no collection available
""" """
col_name = cls._get_collection_name()
if not col_name:
raise OperationError('Document %s has no collection defined '
'(is it abstract ?)' % cls)
cls._collection = None cls._collection = None
db = cls._get_db() db = cls._get_db()
db.drop_collection(cls._get_collection_name()) db.drop_collection(col_name)
@classmethod @classmethod
def create_index(cls, keys, background=False, **kwargs): def create_index(cls, keys, background=False, **kwargs):
@ -669,7 +756,7 @@ class Document(BaseDocument):
fields = index_spec.pop('fields') fields = index_spec.pop('fields')
drop_dups = kwargs.get('drop_dups', False) drop_dups = kwargs.get('drop_dups', False)
if IS_PYMONGO_3 and drop_dups: if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+." msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
elif not IS_PYMONGO_3: elif not IS_PYMONGO_3:
index_spec['drop_dups'] = drop_dups index_spec['drop_dups'] = drop_dups
@ -695,7 +782,7 @@ class Document(BaseDocument):
will be removed if PyMongo3+ is used will be removed if PyMongo3+ is used
""" """
if IS_PYMONGO_3 and drop_dups: if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+." msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
elif not IS_PYMONGO_3: elif not IS_PYMONGO_3:
kwargs.update({'drop_dups': drop_dups}) kwargs.update({'drop_dups': drop_dups})
@ -715,7 +802,7 @@ class Document(BaseDocument):
index_opts = cls._meta.get('index_opts') or {} index_opts = cls._meta.get('index_opts') or {}
index_cls = cls._meta.get('index_cls', True) index_cls = cls._meta.get('index_cls', True)
if IS_PYMONGO_3 and drop_dups: if IS_PYMONGO_3 and drop_dups:
msg = "drop_dups is deprecated and is removed when using PyMongo 3+." msg = 'drop_dups is deprecated and is removed when using PyMongo 3+.'
warnings.warn(msg, DeprecationWarning) warnings.warn(msg, DeprecationWarning)
collection = cls._get_collection() collection = cls._get_collection()
@ -753,8 +840,7 @@ class Document(BaseDocument):
# If _cls is being used (for polymorphism), it needs an index, # If _cls is being used (for polymorphism), it needs an index,
# only if another index doesn't begin with _cls # only if another index doesn't begin with _cls
if (index_cls and not cls_indexed and if index_cls and not cls_indexed and cls._meta.get('allow_inheritance'):
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
# we shouldn't pass 'cls' to the collection.ensureIndex options # we shouldn't pass 'cls' to the collection.ensureIndex options
# because of https://jira.mongodb.org/browse/SERVER-769 # because of https://jira.mongodb.org/browse/SERVER-769
@ -773,7 +859,6 @@ class Document(BaseDocument):
""" Lists all of the indexes that should be created for given """ Lists all of the indexes that should be created for given
collection. It includes all the indexes from super- and sub-classes. collection. It includes all the indexes from super- and sub-classes.
""" """
if cls._meta.get('abstract'): if cls._meta.get('abstract'):
return [] return []
@ -824,16 +909,15 @@ class Document(BaseDocument):
# finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed # finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
if [(u'_id', 1)] not in indexes: if [(u'_id', 1)] not in indexes:
indexes.append([(u'_id', 1)]) indexes.append([(u'_id', 1)])
if (cls._meta.get('index_cls', True) and if cls._meta.get('index_cls', True) and cls._meta.get('allow_inheritance'):
cls._meta.get('allow_inheritance', ALLOW_INHERITANCE) is True):
indexes.append([(u'_cls', 1)]) indexes.append([(u'_cls', 1)])
return indexes return indexes
@classmethod @classmethod
def compare_indexes(cls): def compare_indexes(cls):
""" Compares the indexes defined in MongoEngine with the ones existing """ Compares the indexes defined in MongoEngine with the ones
in the database. Returns any missing/extra indexes. existing in the database. Returns any missing/extra indexes.
""" """
required = cls.list_indexes() required = cls.list_indexes()
@ -877,8 +961,9 @@ class DynamicDocument(Document):
_dynamic = True _dynamic = True
def __delattr__(self, *args, **kwargs): def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset """Delete the attribute by setting to None and allowing _delta
it""" to unset it.
"""
field_name = args[0] field_name = args[0]
if field_name in self._dynamic_fields: if field_name in self._dynamic_fields:
setattr(self, field_name, None) setattr(self, field_name, None)
@ -900,8 +985,9 @@ class DynamicEmbeddedDocument(EmbeddedDocument):
_dynamic = True _dynamic = True
def __delattr__(self, *args, **kwargs): def __delattr__(self, *args, **kwargs):
"""Deletes the attribute by setting to None and allowing _delta to unset """Delete the attribute by setting to None and allowing _delta
it""" to unset it.
"""
field_name = args[0] field_name = args[0]
if field_name in self._fields: if field_name in self._fields:
default = self._fields[field_name].default default = self._fields[field_name].default
@ -942,11 +1028,11 @@ class MapReduceDocument(object):
if not isinstance(self.key, id_field_type): if not isinstance(self.key, id_field_type):
try: try:
self.key = id_field_type(self.key) self.key = id_field_type(self.key)
except: 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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -29,7 +29,7 @@ class QuerySetManager(object):
Document.objects is accessed. Document.objects is accessed.
""" """
if instance is not None: if instance is not None:
# Document class being used rather than a document object # Document object being used rather than a document class
return self return self
# owner is the document that contains the QuerySetManager # owner is the document that contains the QuerySetManager

View File

@ -1,6 +1,6 @@
from mongoengine.errors import OperationError from mongoengine.errors import OperationError
from mongoengine.queryset.base import (BaseQuerySet, DO_NOTHING, NULLIFY, from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING,
CASCADE, DENY, PULL) NULLIFY, PULL)
__all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE', __all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE',
'DENY', 'PULL') 'DENY', 'PULL')
@ -30,6 +30,7 @@ class QuerySet(BaseQuerySet):
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()
@ -38,44 +39,60 @@ class QuerySet(BaseQuerySet):
def __len__(self): def __len__(self):
"""Since __len__ is called quite frequently (for example, as part of """Since __len__ is called quite frequently (for example, as part of
list(qs) we populate the result cache and cache the length. list(qs)), we populate the result cache and cache the length.
""" """
if self._len is not None: if self._len is not None:
return self._len return self._len
# Populate the result cache with *all* of the docs in the cursor
if self._has_more: if self._has_more:
# populate the cache
list(self._iter_results()) list(self._iter_results())
# Cache the length of the complete result cache and return it
self._len = len(self._result_cache) self._len = len(self._result_cache)
return self._len return self._len
def __repr__(self): def __repr__(self):
"""Provides the string representation of the QuerySet """Provide a string representation of the QuerySet"""
"""
if self._iter: if self._iter:
return '.. queryset mid-iteration ..' return '.. queryset mid-iteration ..'
self._populate_cache() self._populate_cache()
data = self._result_cache[:REPR_OUTPUT_SIZE + 1] data = self._result_cache[:REPR_OUTPUT_SIZE + 1]
if len(data) > REPR_OUTPUT_SIZE: if len(data) > REPR_OUTPUT_SIZE:
data[-1] = "...(remaining elements truncated)..." data[-1] = '...(remaining elements truncated)...'
return repr(data) return repr(data)
def _iter_results(self): def _iter_results(self):
"""A generator for iterating over the result cache. """A generator for iterating over the result cache.
Also populates the cache if there are more possible results to yield. Also populates the cache if there are more possible results to
Raises StopIteration when there are no more results""" yield. Raises StopIteration when there are no more results.
"""
if self._result_cache is None: if self._result_cache is None:
self._result_cache = [] self._result_cache = []
pos = 0 pos = 0
while True: while True:
upper = len(self._result_cache)
while pos < upper: # For all positions lower than the length of the current result
# cache, serve the docs straight from the cache w/o hitting the
# database.
# XXX it's VERY important to compute the len within the `while`
# condition because the result cache might expand mid-iteration
# (e.g. if we call len(qs) inside a loop that iterates over the
# queryset). Fortunately len(list) is O(1) in Python, so this
# doesn't cause performance issues.
while pos < len(self._result_cache):
yield self._result_cache[pos] yield self._result_cache[pos]
pos += 1 pos += 1
# Raise StopIteration if we already established there were no more
# docs in the db cursor.
if not self._has_more: if not self._has_more:
raise StopIteration raise StopIteration
# Otherwise, populate more of the cache and repeat.
if len(self._result_cache) <= pos: if len(self._result_cache) <= pos:
self._populate_cache() self._populate_cache()
@ -86,11 +103,21 @@ 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
# more docs to pull from the database.
if not self._has_more:
return
# Pull in ITER_CHUNK_SIZE docs from the database and store them in
# the result cache.
try: try:
for i in xrange(ITER_CHUNK_SIZE): for _ in xrange(ITER_CHUNK_SIZE):
self._result_cache.append(self.next()) self._result_cache.append(self.next())
except StopIteration: 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 self._has_more = False
def count(self, with_limit_and_skip=False): def count(self, with_limit_and_skip=False):
@ -109,13 +136,15 @@ class QuerySet(BaseQuerySet):
return self._len return self._len
def no_cache(self): def no_cache(self):
"""Convert to a non_caching queryset """Convert to a non-caching queryset
.. versionadded:: 0.8.3 Convert to non caching queryset .. versionadded:: 0.8.3 Convert to non caching queryset
""" """
if self._result_cache is not None: if self._result_cache is not None:
raise OperationError("QuerySet already cached") raise OperationError('QuerySet already cached')
return self.clone_into(QuerySetNoCache(self._document, self._collection))
return self._clone_into(QuerySetNoCache(self._document,
self._collection))
class QuerySetNoCache(BaseQuerySet): class QuerySetNoCache(BaseQuerySet):
@ -126,7 +155,7 @@ class QuerySetNoCache(BaseQuerySet):
.. versionadded:: 0.8.3 Convert to caching queryset .. versionadded:: 0.8.3 Convert to caching queryset
""" """
return self.clone_into(QuerySet(self._document, self._collection)) return self._clone_into(QuerySet(self._document, self._collection))
def __repr__(self): def __repr__(self):
"""Provides the string representation of the QuerySet """Provides the string representation of the QuerySet
@ -137,13 +166,14 @@ class QuerySetNoCache(BaseQuerySet):
return '.. queryset mid-iteration ..' return '.. queryset mid-iteration ..'
data = [] data = []
for i in xrange(REPR_OUTPUT_SIZE + 1): for _ in xrange(REPR_OUTPUT_SIZE + 1):
try: try:
data.append(self.next()) data.append(self.next())
except StopIteration: except StopIteration:
break break
if len(data) > REPR_OUTPUT_SIZE: if len(data) > REPR_OUTPUT_SIZE:
data[-1] = "...(remaining elements truncated)..." data[-1] = '...(remaining elements truncated)...'
self.rewind() self.rewind()
return repr(data) return repr(data)

View File

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

View File

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

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- __all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
'post_save', 'pre_delete', 'post_delete')
__all__ = ['pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
'post_save', 'pre_delete', 'post_delete']
signals_available = False signals_available = False
try: try:
@ -29,11 +27,12 @@ except ImportError:
'because the blinker library is ' 'because the blinker library is '
'not installed.') 'not installed.')
send = lambda *a, **kw: None send = lambda *a, **kw: None # noqa
connect = disconnect = has_receivers_for = receivers_for = \ connect = disconnect = has_receivers_for = receivers_for = \
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()

View File

@ -1,2 +1,5 @@
pymongo>=2.7.1
nose nose
pymongo>=2.7.1
six==1.10.0
flake8
flake8-import-order

View File

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

View File

@ -1,6 +1,6 @@
import os import os
import sys import sys
from setuptools import setup, find_packages from setuptools import find_packages, setup
# Hack to silence atexit traceback in newer python versions # Hack to silence atexit traceback in newer python versions
try: try:
@ -8,20 +8,25 @@ try:
except ImportError: except ImportError:
pass pass
DESCRIPTION = 'MongoEngine is a Python Object-Document ' + \ DESCRIPTION = (
'Mapper for working with MongoDB.' 'MongoEngine is a Python Object-Document '
LONG_DESCRIPTION = None 'Mapper for working with MongoDB.'
)
try: try:
LONG_DESCRIPTION = open('README.rst').read() with open('README.rst') as fin:
except: LONG_DESCRIPTION = fin.read()
pass except Exception:
LONG_DESCRIPTION = None
def get_version(version_tuple): def get_version(version_tuple):
if not isinstance(version_tuple[-1], int): """Return the version tuple as a string, e.g. for (0, 10, 7),
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1] return '0.10.7'.
"""
return '.'.join(map(str, version_tuple)) return '.'.join(map(str, version_tuple))
# Dirty hack to get version number from monogengine/__init__.py - we can't # Dirty hack to get version number from monogengine/__init__.py - we can't
# import it as it depends on PyMongo and PyMongo isn't installed until this # import it as it depends on PyMongo and PyMongo isn't installed until this
# file is read # file is read
@ -37,33 +42,32 @@ CLASSIFIERS = [
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
"Programming Language :: Python :: 2", "Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
'Topic :: Database', 'Topic :: Database',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
extra_opts = {"packages": find_packages(exclude=["tests", "tests.*"])} extra_opts = {
'packages': find_packages(exclude=['tests', 'tests.*']),
'tests_require': ['nose', 'coverage==4.2', 'blinker', 'Pillow>=2.0.0']
}
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
extra_opts['use_2to3'] = True extra_opts['use_2to3'] = True
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0'] if 'test' in sys.argv or 'nosetests' in sys.argv:
if "test" in sys.argv or "nosetests" in sys.argv:
extra_opts['packages'] = find_packages() extra_opts['packages'] = find_packages()
extra_opts['package_data'] = {"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]} extra_opts['package_data'] = {
'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
else: else:
# coverage 4 does not support Python 3.2 anymore extra_opts['tests_require'] += ['python-dateutil']
extra_opts['tests_require'] = ['nose', 'coverage==3.7.1', 'blinker', 'Pillow>=2.0.0', 'python-dateutil']
if sys.version_info[0] == 2 and sys.version_info[1] == 6: setup(
extra_opts['tests_require'].append('unittest2') name='mongoengine',
setup(name='mongoengine',
version=VERSION, version=VERSION,
author='Harry Marr', author='Harry Marr',
author_email='harry.marr@{nospam}gmail.com', author_email='harry.marr@{nospam}gmail.com',
@ -77,8 +81,7 @@ setup(name='mongoengine',
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
platforms=['any'], platforms=['any'],
classifiers=CLASSIFIERS, classifiers=CLASSIFIERS,
install_requires=['pymongo>=2.7.1'], install_requires=['pymongo>=2.7.1', 'six'],
test_suite='nose.collector', test_suite='nose.collector',
setup_requires=['nose', 'rednose'], # Allow proper nose usage with setuptols and tox
**extra_opts **extra_opts
) )

View File

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

View File

@ -3,8 +3,6 @@ This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the 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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
import unittest import unittest
import sys
sys.path[0:0] = [""]
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
@ -88,6 +86,18 @@ class DynamicTest(unittest.TestCase):
p.update(unset__misc=1) p.update(unset__misc=1)
p.reload() p.reload()
def test_reload_dynamic_field(self):
self.Person.objects.delete()
p = self.Person.objects.create()
p.update(age=1)
self.assertEqual(len(p._data), 3)
self.assertEqual(sorted(p._data.keys()), ['_cls', 'id', 'name'])
p.reload()
self.assertEqual(len(p._data), 4)
self.assertEqual(sorted(p._data.keys()), ['_cls', 'age', 'id', 'name'])
def test_dynamic_document_queries(self): def test_dynamic_document_queries(self):
"""Ensure we can query dynamic fields""" """Ensure we can query dynamic fields"""
p = self.Person() p = self.Person()
@ -131,11 +141,9 @@ class DynamicTest(unittest.TestCase):
def test_three_level_complex_data_lookups(self): def test_three_level_complex_data_lookups(self):
"""Ensure you can query three level document dynamic fields""" """Ensure you can query three level document dynamic fields"""
p = self.Person() p = self.Person.objects.create(
p.misc = {'hello': {'hello2': 'world'}} misc={'hello': {'hello2': 'world'}}
p.save() )
# from pprint import pprint as pp; import pdb; pdb.set_trace();
print self.Person.objects(misc__hello__hello2='world')
self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count()) self.assertEqual(1, self.Person.objects(misc__hello__hello2='world').count())
def test_complex_embedded_document_validation(self): def test_complex_embedded_document_validation(self):

View File

@ -2,7 +2,6 @@
import unittest import unittest
import sys import sys
sys.path[0:0] = [""]
import pymongo import pymongo
@ -32,10 +31,7 @@ class IndexesTest(unittest.TestCase):
self.Person = Person self.Person = Person
def tearDown(self): def tearDown(self):
for collection in self.db.collection_names(): self.connection.drop_database(self.db)
if 'system.' in collection:
continue
self.db.drop_collection(collection)
def test_indexes_document(self): def test_indexes_document(self):
"""Ensure that indexes are used when meta[indexes] is specified for """Ensure that indexes are used when meta[indexes] is specified for
@ -560,8 +556,8 @@ class IndexesTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
for i in xrange(0, 10): for i in range(0, 10):
tags = [("tag %i" % n) for n in xrange(0, i % 2)] tags = [("tag %i" % n) for n in range(0, i % 2)]
BlogPost(tags=tags).save() BlogPost(tags=tags).save()
self.assertEqual(BlogPost.objects.count(), 10) self.assertEqual(BlogPost.objects.count(), 10)
@ -822,33 +818,34 @@ class IndexesTest(unittest.TestCase):
name = StringField(required=True) name = StringField(required=True)
term = StringField(required=True) term = StringField(required=True)
class Report(Document): class ReportEmbedded(Document):
key = EmbeddedDocumentField(CompoundKey, primary_key=True) key = EmbeddedDocumentField(CompoundKey, primary_key=True)
text = StringField() text = StringField()
Report.drop_collection()
my_key = CompoundKey(name="n", term="ok") my_key = CompoundKey(name="n", term="ok")
report = Report(text="OK", key=my_key).save() report = ReportEmbedded(text="OK", key=my_key).save()
self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}}, self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}},
report.to_mongo()) report.to_mongo())
self.assertEqual(report, Report.objects.get(pk=my_key)) self.assertEqual(report, ReportEmbedded.objects.get(pk=my_key))
def test_compound_key_dictfield(self): def test_compound_key_dictfield(self):
class Report(Document): class ReportDictField(Document):
key = DictField(primary_key=True) key = DictField(primary_key=True)
text = StringField() text = StringField()
Report.drop_collection()
my_key = {"name": "n", "term": "ok"} my_key = {"name": "n", "term": "ok"}
report = Report(text="OK", key=my_key).save() report = ReportDictField(text="OK", key=my_key).save()
self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}}, self.assertEqual({'text': 'OK', '_id': {'term': 'ok', 'name': 'n'}},
report.to_mongo()) report.to_mongo())
self.assertEqual(report, Report.objects.get(pk=my_key))
# We can't directly call ReportDictField.objects.get(pk=my_key),
# because dicts are unordered, and if the order in MongoDB is
# different than the one in `my_key`, this test will fail.
self.assertEqual(report, ReportDictField.objects.get(pk__name=my_key['name']))
self.assertEqual(report, ReportDictField.objects.get(pk__term=my_key['term']))
def test_string_indexes(self): def test_string_indexes(self):
@ -863,6 +860,20 @@ class IndexesTest(unittest.TestCase):
self.assertTrue([('provider_ids.foo', 1)] in info) self.assertTrue([('provider_ids.foo', 1)] in info)
self.assertTrue([('provider_ids.bar', 1)] in info) self.assertTrue([('provider_ids.bar', 1)] in info)
def test_sparse_compound_indexes(self):
class MyDoc(Document):
provider_ids = DictField()
meta = {
"indexes": [{'fields': ("provider_ids.foo", "provider_ids.bar"),
'sparse': True}],
}
info = MyDoc.objects._collection.index_information()
self.assertEqual([('provider_ids.foo', 1), ('provider_ids.bar', 1)],
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
def test_text_indexes(self): def test_text_indexes(self):
class Book(Document): class Book(Document):
@ -895,10 +906,18 @@ class IndexesTest(unittest.TestCase):
Issue #812 Issue #812
""" """
# Use a new connection and database since dropping the database could
# cause concurrent tests to fail.
connection = connect(db='tempdatabase',
alias='test_indexes_after_database_drop')
class BlogPost(Document): class BlogPost(Document):
title = StringField() title = StringField()
slug = StringField(unique=True) slug = StringField(unique=True)
meta = {'db_alias': 'test_indexes_after_database_drop'}
try:
BlogPost.drop_collection() BlogPost.drop_collection()
# Create Post #1 # Create Post #1
@ -906,7 +925,7 @@ class IndexesTest(unittest.TestCase):
post1.save() post1.save()
# Drop the Database # Drop the Database
self.connection.drop_database(BlogPost._get_db().name) connection.drop_database('tempdatabase')
# Re-create Post #1 # Re-create Post #1
post1 = BlogPost(title='test1', slug='test') post1 = BlogPost(title='test1', slug='test')
@ -915,6 +934,10 @@ class IndexesTest(unittest.TestCase):
# Create Post #2 # Create Post #2
post2 = BlogPost(title='test2', slug='test') post2 = BlogPost(title='test2', slug='test')
self.assertRaises(NotUniqueError, post2.save) self.assertRaises(NotUniqueError, post2.save)
finally:
# Drop the temporary database at the end
connection.drop_database('tempdatabase')
def test_index_dont_send_cls_option(self): def test_index_dont_send_cls_option(self):
""" """

View File

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

View File

@ -1,26 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import bson import bson
import os import os
import pickle import pickle
import unittest import unittest
import uuid import uuid
import weakref
from datetime import datetime from datetime import datetime
from bson import DBRef, ObjectId from bson import DBRef, ObjectId
from tests import fixtures from tests import fixtures
from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest, from tests.fixtures import (PickleEmbedded, PickleTest, PickleSignalsTest,
PickleDyanmicEmbedded, PickleDynamicTest) PickleDynamicEmbedded, PickleDynamicTest)
from mongoengine import * from mongoengine import *
from mongoengine.base import get_document, _document_registry
from mongoengine.connection import get_db
from mongoengine.errors import (NotRegistered, InvalidDocumentError, from mongoengine.errors import (NotRegistered, InvalidDocumentError,
InvalidQueryError, NotUniqueError, InvalidQueryError, NotUniqueError,
FieldDoesNotExist) FieldDoesNotExist, SaveConditionError)
from mongoengine.queryset import NULLIFY, Q from mongoengine.queryset import NULLIFY, Q
from mongoengine.connection import get_db
from mongoengine.base import get_document
from mongoengine.context_managers import switch_db, query_counter from mongoengine.context_managers import switch_db, query_counter
from mongoengine import signals from mongoengine import signals
@ -30,6 +28,8 @@ TEST_IMAGE_PATH = os.path.join(os.path.dirname(__file__),
__all__ = ("InstanceTest",) __all__ = ("InstanceTest",)
class InstanceTest(unittest.TestCase): class InstanceTest(unittest.TestCase):
def setUp(self): def setUp(self):
@ -63,6 +63,14 @@ class InstanceTest(unittest.TestCase):
list(self.Person._get_collection().find().sort("id")), list(self.Person._get_collection().find().sort("id")),
sorted(docs, key=lambda doc: doc["_id"])) sorted(docs, key=lambda doc: doc["_id"]))
def assertHasInstance(self, field, instance):
self.assertTrue(hasattr(field, "_instance"))
self.assertTrue(field._instance is not None)
if isinstance(field._instance, weakref.ProxyType):
self.assertTrue(field._instance.__eq__(instance))
else:
self.assertEqual(field._instance, instance)
def test_capped_collection(self): def test_capped_collection(self):
"""Ensure that capped collections work properly. """Ensure that capped collections work properly.
""" """
@ -91,21 +99,18 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(options['size'], 4096) self.assertEqual(options['size'], 4096)
# Check that the document cannot be redefined with different options # Check that the document cannot be redefined with different options
def recreate_log_document():
class Log(Document): class Log(Document):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)
meta = { meta = {
'max_documents': 11, 'max_documents': 11,
} }
# Create the collection by accessing Document.objects
Log.objects
self.assertRaises(InvalidCollectionError, recreate_log_document)
Log.drop_collection() # Accessing Document.objects creates the collection
with self.assertRaises(InvalidCollectionError):
Log.objects
def test_capped_collection_default(self): def test_capped_collection_default(self):
"""Ensure that capped collections defaults work properly. """Ensure that capped collections defaults work properly."""
"""
class Log(Document): class Log(Document):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)
meta = { meta = {
@ -123,16 +128,14 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(options['size'], 10 * 2**20) self.assertEqual(options['size'], 10 * 2**20)
# Check that the document with default value can be recreated # Check that the document with default value can be recreated
def recreate_log_document():
class Log(Document): class Log(Document):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)
meta = { meta = {
'max_documents': 10, 'max_documents': 10,
} }
# Create the collection by accessing Document.objects # Create the collection by accessing Document.objects
Log.objects Log.objects
recreate_log_document()
Log.drop_collection()
def test_capped_collection_no_max_size_problems(self): def test_capped_collection_no_max_size_problems(self):
"""Ensure that capped collections with odd max_size work properly. """Ensure that capped collections with odd max_size work properly.
@ -155,16 +158,14 @@ class InstanceTest(unittest.TestCase):
self.assertTrue(options['size'] >= 10000) self.assertTrue(options['size'] >= 10000)
# Check that the document with odd max_size value can be recreated # Check that the document with odd max_size value can be recreated
def recreate_log_document():
class Log(Document): class Log(Document):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)
meta = { meta = {
'max_size': 10000, 'max_size': 10000,
} }
# Create the collection by accessing Document.objects # Create the collection by accessing Document.objects
Log.objects Log.objects
recreate_log_document()
Log.drop_collection()
def test_repr(self): def test_repr(self):
"""Ensure that unicode representation works """Ensure that unicode representation works
@ -275,7 +276,7 @@ class InstanceTest(unittest.TestCase):
list_stats = [] list_stats = []
for i in xrange(10): for i in range(10):
s = Stats() s = Stats()
s.save() s.save()
list_stats.append(s) list_stats.append(s)
@ -345,14 +346,14 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(User._fields['username'].db_field, '_id') self.assertEqual(User._fields['username'].db_field, '_id')
self.assertEqual(User._meta['id_field'], 'username') self.assertEqual(User._meta['id_field'], 'username')
def create_invalid_user(): # test no primary key field
User(name='test').save() # no primary key field self.assertRaises(ValidationError, User(name='test').save)
self.assertRaises(ValidationError, create_invalid_user)
def define_invalid_user(): # define a subclass with a different primary key field than the
# parent
with self.assertRaises(ValueError):
class EmailUser(User): class EmailUser(User):
email = StringField(primary_key=True) email = StringField(primary_key=True)
self.assertRaises(ValueError, define_invalid_user)
class EmailUser(User): class EmailUser(User):
email = StringField() email = StringField()
@ -400,12 +401,10 @@ class InstanceTest(unittest.TestCase):
# Mimic Place and NicePlace definitions being in a different file # Mimic Place and NicePlace definitions being in a different file
# and the NicePlace model not being imported in at query time. # and the NicePlace model not being imported in at query time.
from mongoengine.base import _document_registry
del(_document_registry['Place.NicePlace']) del(_document_registry['Place.NicePlace'])
def query_without_importing_nice_place(): with self.assertRaises(NotRegistered):
print Place.objects.all() list(Place.objects.all())
self.assertRaises(NotRegistered, query_without_importing_nice_place)
def test_document_registry_regressions(self): def test_document_registry_regressions(self):
@ -436,6 +435,15 @@ class InstanceTest(unittest.TestCase):
person.to_dbref() person.to_dbref()
def test_save_abstract_document(self):
"""Saving an abstract document should fail."""
class Doc(Document):
name = StringField()
meta = {'abstract': True}
with self.assertRaises(InvalidDocumentError):
Doc(name='aaa').save()
def test_reload(self): def test_reload(self):
"""Ensure that attributes may be reloaded. """Ensure that attributes may be reloaded.
""" """
@ -473,6 +481,20 @@ class InstanceTest(unittest.TestCase):
doc.reload() doc.reload()
Animal.drop_collection() Animal.drop_collection()
def test_reload_sharded_nested(self):
class SuperPhylum(EmbeddedDocument):
name = StringField()
class Animal(Document):
superphylum = EmbeddedDocumentField(SuperPhylum)
meta = {'shard_key': ('superphylum.name',)}
Animal.drop_collection()
doc = Animal(superphylum=SuperPhylum(name='Deuterostomia'))
doc.save()
doc.reload()
Animal.drop_collection()
def test_reload_referencing(self): def test_reload_referencing(self):
"""Ensures reloading updates weakrefs correctly """Ensures reloading updates weakrefs correctly
""" """
@ -546,6 +568,28 @@ class InstanceTest(unittest.TestCase):
except Exception: except Exception:
self.assertFalse("Threw wrong exception") self.assertFalse("Threw wrong exception")
def test_reload_of_non_strict_with_special_field_name(self):
"""Ensures reloading works for documents with meta strict == False
"""
class Post(Document):
meta = {
'strict': False
}
title = StringField()
items = ListField()
Post.drop_collection()
Post._get_collection().insert({
"title": "Items eclipse",
"items": ["more lorem", "even more ipsum"]
})
post = Post.objects.first()
post.reload()
self.assertEqual(post.title, "Items eclipse")
self.assertEqual(post.items, ["more lorem", "even more ipsum"])
def test_dictionary_access(self): def test_dictionary_access(self):
"""Ensure that dictionary-style field access works properly. """Ensure that dictionary-style field access works properly.
""" """
@ -608,10 +652,12 @@ class InstanceTest(unittest.TestCase):
embedded_field = EmbeddedDocumentField(Embedded) embedded_field = EmbeddedDocumentField(Embedded)
Doc.drop_collection() Doc.drop_collection()
Doc(embedded_field=Embedded(string="Hi")).save() doc = Doc(embedded_field=Embedded(string="Hi"))
self.assertHasInstance(doc.embedded_field, doc)
doc.save()
doc = Doc.objects.get() doc = Doc.objects.get()
self.assertEqual(doc, doc.embedded_field._instance) self.assertHasInstance(doc.embedded_field, doc)
def test_embedded_document_complex_instance(self): def test_embedded_document_complex_instance(self):
"""Ensure that embedded documents in complex fields can reference """Ensure that embedded documents in complex fields can reference
@ -623,10 +669,25 @@ class InstanceTest(unittest.TestCase):
embedded_field = ListField(EmbeddedDocumentField(Embedded)) embedded_field = ListField(EmbeddedDocumentField(Embedded))
Doc.drop_collection() Doc.drop_collection()
Doc(embedded_field=[Embedded(string="Hi")]).save() doc = Doc(embedded_field=[Embedded(string="Hi")])
self.assertHasInstance(doc.embedded_field[0], doc)
doc.save()
doc = Doc.objects.get() doc = Doc.objects.get()
self.assertEqual(doc, doc.embedded_field[0]._instance) self.assertHasInstance(doc.embedded_field[0], doc)
def test_embedded_document_complex_instance_no_use_db_field(self):
"""Ensure that use_db_field is propagated to list of Emb Docs
"""
class Embedded(EmbeddedDocument):
string = StringField(db_field='s')
class Doc(Document):
embedded_field = ListField(EmbeddedDocumentField(Embedded))
d = Doc(embedded_field=[Embedded(string="Hi")]).to_mongo(
use_db_field=False).to_dict()
self.assertEqual(d['embedded_field'], [{'string': 'Hi'}])
def test_instance_is_set_on_setattr(self): def test_instance_is_set_on_setattr(self):
@ -639,11 +700,28 @@ class InstanceTest(unittest.TestCase):
Account.drop_collection() Account.drop_collection()
acc = Account() acc = Account()
acc.email = Email(email='test@example.com') acc.email = Email(email='test@example.com')
self.assertTrue(hasattr(acc._data["email"], "_instance")) self.assertHasInstance(acc._data["email"], acc)
acc.save() acc.save()
acc1 = Account.objects.first() acc1 = Account.objects.first()
self.assertTrue(hasattr(acc1._data["email"], "_instance")) self.assertHasInstance(acc1._data["email"], acc1)
def test_instance_is_set_on_setattr_on_embedded_document_list(self):
class Email(EmbeddedDocument):
email = EmailField()
class Account(Document):
emails = EmbeddedDocumentListField(Email)
Account.drop_collection()
acc = Account()
acc.emails = [Email(email='test@example.com')]
self.assertHasInstance(acc._data["emails"][0], acc)
acc.save()
acc1 = Account.objects.first()
self.assertHasInstance(acc1._data["emails"][0], acc1)
def test_document_clean(self): def test_document_clean(self):
class TestDocument(Document): class TestDocument(Document):
@ -664,7 +742,7 @@ class InstanceTest(unittest.TestCase):
try: try:
t.save() t.save()
except ValidationError, e: except ValidationError as e:
expect_msg = "Draft entries may not have a publication date." expect_msg = "Draft entries may not have a publication date."
self.assertTrue(expect_msg in e.message) self.assertTrue(expect_msg in e.message)
self.assertEqual(e.to_dict(), {'__all__': expect_msg}) self.assertEqual(e.to_dict(), {'__all__': expect_msg})
@ -703,7 +781,7 @@ class InstanceTest(unittest.TestCase):
t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25, z=15)) t = TestDocument(doc=TestEmbeddedDocument(x=10, y=25, z=15))
try: try:
t.save() t.save()
except ValidationError, e: except ValidationError as e:
expect_msg = "Value of z != x + y" expect_msg = "Value of z != x + y"
self.assertTrue(expect_msg in e.message) self.assertTrue(expect_msg in e.message)
self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}}) self.assertEqual(e.to_dict(), {'doc': {'__all__': expect_msg}})
@ -717,8 +795,10 @@ class InstanceTest(unittest.TestCase):
def test_modify_empty(self): def test_modify_empty(self):
doc = self.Person(name="bob", age=10).save() doc = self.Person(name="bob", age=10).save()
self.assertRaises(
InvalidDocumentError, lambda: self.Person().modify(set__age=10)) with self.assertRaises(InvalidDocumentError):
self.Person().modify(set__age=10)
self.assertDbEqual([dict(doc.to_mongo())]) self.assertDbEqual([dict(doc.to_mongo())])
def test_modify_invalid_query(self): def test_modify_invalid_query(self):
@ -726,9 +806,8 @@ class InstanceTest(unittest.TestCase):
doc2 = self.Person(name="jim", age=20).save() doc2 = self.Person(name="jim", age=20).save()
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
self.assertRaises( with self.assertRaises(InvalidQueryError):
InvalidQueryError, doc1.modify({'id': doc2.id}, set__value=20)
lambda: doc1.modify(dict(id=doc2.id), set__value=20))
self.assertDbEqual(docs) self.assertDbEqual(docs)
@ -737,7 +816,7 @@ class InstanceTest(unittest.TestCase):
doc2 = self.Person(name="jim", age=20).save() doc2 = self.Person(name="jim", age=20).save()
docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())] docs = [dict(doc1.to_mongo()), dict(doc2.to_mongo())]
assert not doc1.modify(dict(name=doc2.name), set__age=100) assert not doc1.modify({'name': doc2.name}, set__age=100)
self.assertDbEqual(docs) self.assertDbEqual(docs)
@ -746,7 +825,7 @@ class InstanceTest(unittest.TestCase):
doc2 = self.Person(id=ObjectId(), name="jim", age=20) doc2 = self.Person(id=ObjectId(), name="jim", age=20)
docs = [dict(doc1.to_mongo())] docs = [dict(doc1.to_mongo())]
assert not doc2.modify(dict(name=doc2.name), set__age=100) assert not doc2.modify({'name': doc2.name}, set__age=100)
self.assertDbEqual(docs) self.assertDbEqual(docs)
@ -1021,7 +1100,7 @@ class InstanceTest(unittest.TestCase):
flip(w1) flip(w1)
self.assertTrue(w1.toggle) self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 1) self.assertEqual(w1.count, 1)
self.assertRaises(OperationError, self.assertRaises(SaveConditionError,
w1.save, save_condition={'save_id': UUID(42)}) w1.save, save_condition={'save_id': UUID(42)})
w1.reload() w1.reload()
self.assertFalse(w1.toggle) self.assertFalse(w1.toggle)
@ -1050,7 +1129,7 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(w1.count, 2) self.assertEqual(w1.count, 2)
flip(w2) flip(w2)
flip(w2) flip(w2)
self.assertRaises(OperationError, self.assertRaises(SaveConditionError,
w2.save, save_condition={'save_id': old_id}) w2.save, save_condition={'save_id': old_id})
w2.reload() w2.reload()
self.assertFalse(w2.toggle) self.assertFalse(w2.toggle)
@ -1063,7 +1142,7 @@ class InstanceTest(unittest.TestCase):
self.assertTrue(w1.toggle) self.assertTrue(w1.toggle)
self.assertEqual(w1.count, 3) self.assertEqual(w1.count, 3)
flip(w1) flip(w1)
self.assertRaises(OperationError, self.assertRaises(SaveConditionError,
w1.save, save_condition={'count__gte': w1.count}) w1.save, save_condition={'count__gte': w1.count})
w1.reload() w1.reload()
self.assertTrue(w1.toggle) self.assertTrue(w1.toggle)
@ -1153,6 +1232,19 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(person.name, None) self.assertEqual(person.name, None)
self.assertEqual(person.age, None) self.assertEqual(person.age, None)
def test_update_rename_operator(self):
"""Test the $rename operator."""
coll = self.Person._get_collection()
doc = self.Person(name='John').save()
raw_doc = coll.find_one({'_id': doc.pk})
self.assertEqual(set(raw_doc.keys()), set(['_id', '_cls', 'name']))
doc.update(rename__name='first_name')
raw_doc = coll.find_one({'_id': doc.pk})
self.assertEqual(set(raw_doc.keys()),
set(['_id', '_cls', 'first_name']))
self.assertEqual(raw_doc['first_name'], 'John')
def test_inserts_if_you_set_the_pk(self): def test_inserts_if_you_set_the_pk(self):
p1 = self.Person(name='p1', id=bson.ObjectId()).save() p1 = self.Person(name='p1', id=bson.ObjectId()).save()
p2 = self.Person(name='p2') p2 = self.Person(name='p2')
@ -1212,12 +1304,11 @@ class InstanceTest(unittest.TestCase):
def test_document_update(self): def test_document_update(self):
def update_not_saved_raises(): # try updating a non-saved document
with self.assertRaises(OperationError):
person = self.Person(name='dcrosta') person = self.Person(name='dcrosta')
person.update(set__name='Dan Crosta') person.update(set__name='Dan Crosta')
self.assertRaises(OperationError, update_not_saved_raises)
author = self.Person(name='dcrosta') author = self.Person(name='dcrosta')
author.save() author.save()
@ -1227,19 +1318,17 @@ class InstanceTest(unittest.TestCase):
p1 = self.Person.objects.first() p1 = self.Person.objects.first()
self.assertEqual(p1.name, author.name) self.assertEqual(p1.name, author.name)
def update_no_value_raises(): # try sending an empty update
with self.assertRaises(OperationError):
person = self.Person.objects.first() person = self.Person.objects.first()
person.update() person.update()
self.assertRaises(OperationError, update_no_value_raises) # update that doesn't explicitly specify an operator should default
# to 'set__'
def update_no_op_should_default_to_set():
person = self.Person.objects.first() person = self.Person.objects.first()
person.update(name="Dan") person.update(name="Dan")
person.reload() person.reload()
return person.name self.assertEqual("Dan", person.name)
self.assertEqual("Dan", update_no_op_should_default_to_set())
def test_update_unique_field(self): def test_update_unique_field(self):
class Doc(Document): class Doc(Document):
@ -1248,8 +1337,8 @@ class InstanceTest(unittest.TestCase):
doc1 = Doc(name="first").save() doc1 = Doc(name="first").save()
doc2 = Doc(name="second").save() doc2 = Doc(name="second").save()
self.assertRaises(NotUniqueError, lambda: with self.assertRaises(NotUniqueError):
doc2.update(set__name=doc1.name)) doc2.update(set__name=doc1.name)
def test_embedded_update(self): def test_embedded_update(self):
""" """
@ -1767,15 +1856,13 @@ class InstanceTest(unittest.TestCase):
def test_duplicate_db_fields_raise_invalid_document_error(self): def test_duplicate_db_fields_raise_invalid_document_error(self):
"""Ensure a InvalidDocumentError is thrown if duplicate fields """Ensure a InvalidDocumentError is thrown if duplicate fields
declare the same db_field""" declare the same db_field.
"""
def throw_invalid_document_error(): with self.assertRaises(InvalidDocumentError):
class Foo(Document): class Foo(Document):
name = StringField() name = StringField()
name2 = StringField(db_field='name') name2 = StringField(db_field='name')
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
def test_invalid_son(self): def test_invalid_son(self):
"""Raise an error if loading invalid data""" """Raise an error if loading invalid data"""
class Occurrence(EmbeddedDocument): class Occurrence(EmbeddedDocument):
@ -1787,11 +1874,17 @@ class InstanceTest(unittest.TestCase):
forms = ListField(StringField(), default=list) forms = ListField(StringField(), default=list)
occurs = ListField(EmbeddedDocumentField(Occurrence), default=list) occurs = ListField(EmbeddedDocumentField(Occurrence), default=list)
def raise_invalid_document(): with self.assertRaises(InvalidDocumentError):
Word._from_son({'stem': [1, 2, 3], 'forms': 1, 'count': 'one', Word._from_son({
'occurs': {"hello": None}}) 'stem': [1, 2, 3],
'forms': 1,
'count': 'one',
'occurs': {"hello": None}
})
self.assertRaises(InvalidDocumentError, raise_invalid_document) # Tests for issue #1438: https://github.com/MongoEngine/mongoengine/issues/1438
with self.assertRaises(ValueError):
Word._from_son('this is not a valid SON dict')
def test_reverse_delete_rule_cascade_and_nullify(self): def test_reverse_delete_rule_cascade_and_nullify(self):
"""Ensure that a referenced document is also deleted upon deletion. """Ensure that a referenced document is also deleted upon deletion.
@ -1825,6 +1918,62 @@ class InstanceTest(unittest.TestCase):
author.delete() author.delete()
self.assertEqual(BlogPost.objects.count(), 0) self.assertEqual(BlogPost.objects.count(), 0)
def test_reverse_delete_rule_with_custom_id_field(self):
"""Ensure that a referenced document with custom primary key
is also deleted upon deletion.
"""
class User(Document):
name = StringField(primary_key=True)
class Book(Document):
author = ReferenceField(User, reverse_delete_rule=CASCADE)
reviewer = ReferenceField(User, reverse_delete_rule=NULLIFY)
User.drop_collection()
Book.drop_collection()
user = User(name='Mike').save()
reviewer = User(name='John').save()
book = Book(author=user, reviewer=reviewer).save()
reviewer.delete()
self.assertEqual(Book.objects.count(), 1)
self.assertEqual(Book.objects.get().reviewer, None)
user.delete()
self.assertEqual(Book.objects.count(), 0)
def test_reverse_delete_rule_with_shared_id_among_collections(self):
"""Ensure that cascade delete rule doesn't mix id among collections.
"""
class User(Document):
id = IntField(primary_key=True)
class Book(Document):
id = IntField(primary_key=True)
author = ReferenceField(User, reverse_delete_rule=CASCADE)
User.drop_collection()
Book.drop_collection()
user_1 = User(id=1).save()
user_2 = User(id=2).save()
book_1 = Book(id=1, author=user_2).save()
book_2 = Book(id=2, author=user_1).save()
user_2.delete()
# Deleting user_2 should also delete book_1 but not book_2
self.assertEqual(Book.objects.count(), 1)
self.assertEqual(Book.objects.get(), book_2)
user_3 = User(id=3).save()
book_3 = Book(id=3, author=user_3).save()
user_3.delete()
# Deleting user_3 should also delete book_3
self.assertEqual(Book.objects.count(), 1)
self.assertEqual(Book.objects.get(), book_2)
def test_reverse_delete_rule_with_document_inheritance(self): def test_reverse_delete_rule_with_document_inheritance(self):
"""Ensure that a referenced document is also deleted upon deletion """Ensure that a referenced document is also deleted upon deletion
of a child document. of a child document.
@ -1966,8 +2115,7 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(Bar.objects.get().foo, None) self.assertEqual(Bar.objects.get().foo, None)
def test_invalid_reverse_delete_rule_raise_errors(self): def test_invalid_reverse_delete_rule_raise_errors(self):
with self.assertRaises(InvalidDocumentError):
def throw_invalid_document_error():
class Blog(Document): class Blog(Document):
content = StringField() content = StringField()
authors = MapField(ReferenceField( authors = MapField(ReferenceField(
@ -1977,21 +2125,15 @@ class InstanceTest(unittest.TestCase):
self.Person, self.Person,
reverse_delete_rule=NULLIFY)) reverse_delete_rule=NULLIFY))
self.assertRaises(InvalidDocumentError, throw_invalid_document_error) with self.assertRaises(InvalidDocumentError):
def throw_invalid_document_error_embedded():
class Parents(EmbeddedDocument): class Parents(EmbeddedDocument):
father = ReferenceField('Person', reverse_delete_rule=DENY) father = ReferenceField('Person', reverse_delete_rule=DENY)
mother = ReferenceField('Person', reverse_delete_rule=DENY) mother = ReferenceField('Person', reverse_delete_rule=DENY)
self.assertRaises(
InvalidDocumentError, throw_invalid_document_error_embedded)
def test_reverse_delete_rule_cascade_recurs(self): def test_reverse_delete_rule_cascade_recurs(self):
"""Ensure that a chain of documents is also deleted upon cascaded """Ensure that a chain of documents is also deleted upon cascaded
deletion. deletion.
""" """
class BlogPost(Document): class BlogPost(Document):
content = StringField() content = StringField()
author = ReferenceField(self.Person, reverse_delete_rule=CASCADE) author = ReferenceField(self.Person, reverse_delete_rule=CASCADE)
@ -2180,7 +2322,7 @@ class InstanceTest(unittest.TestCase):
pickle_doc = PickleDynamicTest( pickle_doc = PickleDynamicTest(
name="test", number=1, string="One", lists=['1', '2']) name="test", number=1, string="One", lists=['1', '2'])
pickle_doc.embedded = PickleDyanmicEmbedded(foo="Bar") pickle_doc.embedded = PickleDynamicEmbedded(foo="Bar")
pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved pickled_doc = pickle.dumps(pickle_doc) # make sure pickling works even before the doc is saved
pickle_doc.save() pickle_doc.save()
@ -2207,15 +2349,14 @@ class InstanceTest(unittest.TestCase):
pickle_doc.save() pickle_doc.save()
pickle_doc.delete() pickle_doc.delete()
def test_throw_invalid_document_error(self): def test_override_method_with_field(self):
"""Test creating a field with a field name that would override
# test handles people trying to upsert the "validate" method.
def throw_invalid_document_error(): """
with self.assertRaises(InvalidDocumentError):
class Blog(Document): class Blog(Document):
validate = DictField() validate = DictField()
self.assertRaises(InvalidDocumentError, throw_invalid_document_error)
def test_mutating_documents(self): def test_mutating_documents(self):
class B(EmbeddedDocument): class B(EmbeddedDocument):
@ -2678,10 +2819,34 @@ class InstanceTest(unittest.TestCase):
log.log = "Saving" log.log = "Saving"
log.save() log.save()
def change_shard_key(): # try to change the shard key
with self.assertRaises(OperationError):
log.machine = "127.0.0.1" log.machine = "127.0.0.1"
self.assertRaises(OperationError, change_shard_key) def test_shard_key_in_embedded_document(self):
class Foo(EmbeddedDocument):
foo = StringField()
class Bar(Document):
meta = {
'shard_key': ('foo.foo',)
}
foo = EmbeddedDocumentField(Foo)
bar = StringField()
foo_doc = Foo(foo='hello')
bar_doc = Bar(foo=foo_doc, bar='world')
bar_doc.save()
self.assertTrue(bar_doc.id is not None)
bar_doc.bar = 'baz'
bar_doc.save()
# try to change the shard key
with self.assertRaises(OperationError):
bar_doc.foo.foo = 'something'
bar_doc.save()
def test_shard_key_primary(self): def test_shard_key_primary(self):
class LogEntry(Document): class LogEntry(Document):
@ -2703,11 +2868,10 @@ class InstanceTest(unittest.TestCase):
log.log = "Saving" log.log = "Saving"
log.save() log.save()
def change_shard_key(): # try to change the shard key
with self.assertRaises(OperationError):
log.machine = "127.0.0.1" log.machine = "127.0.0.1"
self.assertRaises(OperationError, change_shard_key)
def test_kwargs_simple(self): def test_kwargs_simple(self):
class Embedded(EmbeddedDocument): class Embedded(EmbeddedDocument):
@ -2765,6 +2929,20 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(person.name, "Test User") self.assertEqual(person.name, "Test User")
self.assertEqual(person.age, 42) self.assertEqual(person.age, 42)
def test_positional_creation_embedded(self):
"""Ensure that embedded document may be created using positional arguments.
"""
job = self.Job("Test Job", 4)
self.assertEqual(job.name, "Test Job")
self.assertEqual(job.years, 4)
def test_mixed_creation_embedded(self):
"""Ensure that embedded document may be created using mixed arguments.
"""
job = self.Job("Test Job", years=4)
self.assertEqual(job.name, "Test Job")
self.assertEqual(job.years, 4)
def test_mixed_creation_dynamic(self): def test_mixed_creation_dynamic(self):
"""Ensure that document may be created using mixed arguments. """Ensure that document may be created using mixed arguments.
""" """
@ -2778,11 +2956,9 @@ class InstanceTest(unittest.TestCase):
def test_bad_mixed_creation(self): def test_bad_mixed_creation(self):
"""Ensure that document gives correct error when duplicating arguments """Ensure that document gives correct error when duplicating arguments
""" """
def construct_bad_instance(): with self.assertRaises(TypeError):
return self.Person("Test User", 42, name="Bad User") return self.Person("Test User", 42, name="Bad User")
self.assertRaises(TypeError, construct_bad_instance)
def test_data_contains_id_field(self): def test_data_contains_id_field(self):
"""Ensure that asking for _data returns 'id' """Ensure that asking for _data returns 'id'
""" """
@ -2941,6 +3117,17 @@ class InstanceTest(unittest.TestCase):
p4 = Person.objects()[0] p4 = Person.objects()[0]
p4.save() p4.save()
self.assertEquals(p4.height, 189) self.assertEquals(p4.height, 189)
# However the default will not be fixed in DB
self.assertEquals(Person.objects(height=189).count(), 0)
# alter DB for the new default
coll = Person._get_collection()
for person in Person.objects.as_pymongo():
if 'height' not in person:
person['height'] = 189
coll.save(person)
self.assertEquals(Person.objects(height=189).count(), 1) self.assertEquals(Person.objects(height=189).count(), 1)
def test_from_son(self): def test_from_son(self):
@ -3014,5 +3201,20 @@ class InstanceTest(unittest.TestCase):
self.assertEqual(b._instance, a) self.assertEqual(b._instance, a)
self.assertEqual(idx, 2) self.assertEqual(idx, 2)
def test_falsey_pk(self):
"""Ensure that we can create and update a document with Falsey PK.
"""
class Person(Document):
age = IntField(primary_key=True)
height = FloatField()
person = Person()
person.age = 0
person.height = 1.89
person.save()
person.update(set__height=2.0)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -26,7 +26,7 @@ class NewDocumentPickleTest(Document):
new_field = StringField() new_field = StringField()
class PickleDyanmicEmbedded(DynamicEmbeddedDocument): class PickleDynamicEmbedded(DynamicEmbeddedDocument):
date = DateTimeField(default=datetime.now) date = DateTimeField(default=datetime.now)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,5 @@
import sys
sys.path[0:0] = [""]
import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
import unittest
from pymongo.errors import OperationFailure from pymongo.errors import OperationFailure
from mongoengine import * from mongoengine import *

View File

@ -1,6 +1,3 @@
import sys
sys.path[0:0] = [""]
import unittest import unittest
from mongoengine import connect, Document, IntField from mongoengine import connect, Document, IntField

View File

@ -0,0 +1,78 @@
import pickle
import unittest
from pymongo.mongo_client import MongoClient
from mongoengine import Document, StringField, IntField
from mongoengine.connection import connect
__author__ = 'stas'
class Person(Document):
name = StringField()
age = IntField()
class TestQuerysetPickable(unittest.TestCase):
"""
Test for adding pickling support for QuerySet instances
See issue https://github.com/MongoEngine/mongoengine/issues/442
"""
def setUp(self):
super(TestQuerysetPickable, self).setUp()
connection = connect(db="test") #type: pymongo.mongo_client.MongoClient
connection.drop_database("test")
self.john = Person.objects.create(
name="John",
age=21
)
def test_picke_simple_qs(self):
qs = Person.objects.all()
pickle.dumps(qs)
def _get_loaded(self, qs):
s = pickle.dumps(qs)
return pickle.loads(s)
def test_unpickle(self):
qs = Person.objects.all()
loadedQs = self._get_loaded(qs)
self.assertEqual(qs.count(), loadedQs.count())
#can update loadedQs
loadedQs.update(age=23)
#check
self.assertEqual(Person.objects.first().age, 23)
def test_pickle_support_filtration(self):
Person.objects.create(
name="Alice",
age=22
)
Person.objects.create(
name="Bob",
age=23
)
qs = Person.objects.filter(age__gte=22)
self.assertEqual(qs.count(), 2)
loaded = self._get_loaded(qs)
self.assertEqual(loaded.count(), 2)
self.assertEqual(loaded.filter(name="Bob").first().age, 23)

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,7 @@
import sys
sys.path[0:0] = [""]
import unittest import unittest
from mongoengine import * from mongoengine import *
from mongoengine.queryset import Q from mongoengine.queryset import Q, transform
from mongoengine.queryset import transform
__all__ = ("TransformTest",) __all__ = ("TransformTest",)
@ -41,8 +37,8 @@ class TransformTest(unittest.TestCase):
DicDoc.drop_collection() DicDoc.drop_collection()
Doc.drop_collection() Doc.drop_collection()
DicDoc().save()
doc = Doc().save() doc = Doc().save()
dic_doc = DicDoc().save()
for k, v in (("set", "$set"), ("set_on_insert", "$setOnInsert"), ("push", "$push")): for k, v in (("set", "$set"), ("set_on_insert", "$setOnInsert"), ("push", "$push")):
update = transform.update(DicDoc, **{"%s__dictField__test" % k: doc}) update = transform.update(DicDoc, **{"%s__dictField__test" % k: doc})
@ -55,7 +51,6 @@ class TransformTest(unittest.TestCase):
update = transform.update(DicDoc, pull__dictField__test=doc) update = transform.update(DicDoc, pull__dictField__test=doc)
self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict)) self.assertTrue(isinstance(update["$pull"]["dictField"]["test"], dict))
def test_query_field_name(self): def test_query_field_name(self):
"""Ensure that the correct field name is used when querying. """Ensure that the correct field name is used when querying.
""" """
@ -156,26 +151,33 @@ class TransformTest(unittest.TestCase):
class Doc(Document): class Doc(Document):
meta = {'allow_inheritance': False} meta = {'allow_inheritance': False}
raw_query = Doc.objects(__raw__={'deleted': False, raw_query = Doc.objects(__raw__={
'deleted': False,
'scraped': 'yes', 'scraped': 'yes',
'$nor': [{'views.extracted': 'no'}, '$nor': [
{'attachments.views.extracted':'no'}] {'views.extracted': 'no'},
{'attachments.views.extracted': 'no'}
]
})._query })._query
expected = {'deleted': False, 'scraped': 'yes', self.assertEqual(raw_query, {
'$nor': [{'views.extracted': 'no'}, 'deleted': False,
{'attachments.views.extracted': 'no'}]} 'scraped': 'yes',
self.assertEqual(expected, raw_query) '$nor': [
{'views.extracted': 'no'},
{'attachments.views.extracted': 'no'}
]
})
def test_geojson_PointField(self): def test_geojson_PointField(self):
class Location(Document): class Location(Document):
loc = PointField() loc = PointField()
update = transform.update(Location, set__loc=[1, 2]) update = transform.update(Location, set__loc=[1, 2])
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1, 2]}}})
update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1,2]}) update = transform.update(Location, set__loc={"type": "Point", "coordinates": [1, 2]})
self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1,2]}}}) self.assertEqual(update, {'$set': {'loc': {"type": "Point", "coordinates": [1, 2]}}})
def test_geojson_LineStringField(self): def test_geojson_LineStringField(self):
class Location(Document): class Location(Document):
@ -224,6 +226,10 @@ class TransformTest(unittest.TestCase):
self.assertEqual(1, Doc.objects(item__type__="axe").count()) self.assertEqual(1, Doc.objects(item__type__="axe").count())
self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count()) self.assertEqual(1, Doc.objects(item__name__="Heroic axe").count())
Doc.objects(id=doc.id).update(set__item__type__='sword')
self.assertEqual(1, Doc.objects(item__type__="sword").count())
self.assertEqual(0, Doc.objects(item__type__="axe").count())
def test_understandable_error_raised(self): def test_understandable_error_raised(self):
class Event(Document): class Event(Document):
title = StringField() title = StringField()
@ -232,7 +238,9 @@ class TransformTest(unittest.TestCase):
box = [(35.0, -125.0), (40.0, -100.0)] box = [(35.0, -125.0), (40.0, -100.0)]
# I *meant* to execute location__within_box=box # I *meant* to execute location__within_box=box
events = Event.objects(location__within=box) events = Event.objects(location__within=box)
self.assertRaises(InvalidQueryError, lambda: events.count()) with self.assertRaises(InvalidQueryError):
events.count()
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,14 +1,12 @@
import sys import datetime
sys.path[0:0] = [""] import re
import unittest import unittest
from bson import ObjectId from bson import ObjectId
from datetime import datetime
from mongoengine import * from mongoengine import *
from mongoengine.queryset import Q
from mongoengine.errors import InvalidQueryError from mongoengine.errors import InvalidQueryError
from mongoengine.queryset import Q
__all__ = ("QTest",) __all__ = ("QTest",)
@ -132,12 +130,12 @@ class QTest(unittest.TestCase):
TestDoc(x=10).save() TestDoc(x=10).save()
TestDoc(y=True).save() TestDoc(y=True).save()
self.assertEqual(query, self.assertEqual(query, {
{'$and': [ '$and': [
{'$or': [{'x': {'$gt': 0}}, {'x': {'$exists': False}}]}, {'$or': [{'x': {'$gt': 0}}, {'x': {'$exists': False}}]},
{'$or': [{'x': {'$lt': 100}}, {'y': True}]} {'$or': [{'x': {'$lt': 100}}, {'y': True}]}
]}) ]
})
self.assertEqual(2, TestDoc.objects(q1 & q2).count()) self.assertEqual(2, TestDoc.objects(q1 & q2).count())
def test_or_and_or_combination(self): def test_or_and_or_combination(self):
@ -157,15 +155,14 @@ class QTest(unittest.TestCase):
q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False))) q2 = (Q(x__lt=100) & (Q(y=False) | Q(y__exists=False)))
query = (q1 | q2).to_query(TestDoc) query = (q1 | q2).to_query(TestDoc)
self.assertEqual(query, self.assertEqual(query, {
{'$or': [ '$or': [
{'$and': [{'x': {'$gt': 0}}, {'$and': [{'x': {'$gt': 0}},
{'$or': [{'y': True}, {'y': {'$exists': False}}]}]}, {'$or': [{'y': True}, {'y': {'$exists': False}}]}]},
{'$and': [{'x': {'$lt': 100}}, {'$and': [{'x': {'$lt': 100}},
{'$or': [{'y': False}, {'y': {'$exists': False}}]}]} {'$or': [{'y': False}, {'y': {'$exists': False}}]}]}
]} ]
) })
self.assertEqual(2, TestDoc.objects(q1 | q2).count()) self.assertEqual(2, TestDoc.objects(q1 | q2).count())
def test_multiple_occurence_in_field(self): def test_multiple_occurence_in_field(self):
@ -188,7 +185,7 @@ class QTest(unittest.TestCase):
x = IntField() x = IntField()
TestDoc.drop_collection() TestDoc.drop_collection()
for i in xrange(1, 101): for i in range(1, 101):
t = TestDoc(x=i) t = TestDoc(x=i)
t.save() t.save()
@ -215,19 +212,19 @@ class QTest(unittest.TestCase):
BlogPost.drop_collection() BlogPost.drop_collection()
post1 = BlogPost(title='Test 1', publish_date=datetime(2010, 1, 8), published=False) post1 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 8), published=False)
post1.save() post1.save()
post2 = BlogPost(title='Test 2', publish_date=datetime(2010, 1, 15), published=True) post2 = BlogPost(title='Test 2', publish_date=datetime.datetime(2010, 1, 15), published=True)
post2.save() post2.save()
post3 = BlogPost(title='Test 3', published=True) post3 = BlogPost(title='Test 3', published=True)
post3.save() post3.save()
post4 = BlogPost(title='Test 4', publish_date=datetime(2010, 1, 8)) post4 = BlogPost(title='Test 4', publish_date=datetime.datetime(2010, 1, 8))
post4.save() post4.save()
post5 = BlogPost(title='Test 1', publish_date=datetime(2010, 1, 15)) post5 = BlogPost(title='Test 1', publish_date=datetime.datetime(2010, 1, 15))
post5.save() post5.save()
post6 = BlogPost(title='Test 1', published=False) post6 = BlogPost(title='Test 1', published=False)
@ -250,7 +247,7 @@ class QTest(unittest.TestCase):
self.assertTrue(all(obj.id in posts for obj in published_posts)) self.assertTrue(all(obj.id in posts for obj in published_posts))
# Check Q object combination # Check Q object combination
date = datetime(2010, 1, 10) date = datetime.datetime(2010, 1, 10)
q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True)) q = BlogPost.objects(Q(publish_date__lte=date) | Q(published=True))
posts = [post.id for post in q] posts = [post.id for post in q]
@ -271,12 +268,13 @@ class QTest(unittest.TestCase):
self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3) self.assertEqual(self.Person.objects(Q(age__in=[20, 30])).count(), 3)
# Test invalid query objs # Test invalid query objs
def wrong_query_objs(): with self.assertRaises(InvalidQueryError):
self.Person.objects('user1') self.Person.objects('user1')
def wrong_query_objs_filter():
self.Person.objects('user1') # filter should fail, too
self.assertRaises(InvalidQueryError, wrong_query_objs) with self.assertRaises(InvalidQueryError):
self.assertRaises(InvalidQueryError, wrong_query_objs_filter) self.Person.objects.filter('user1')
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.
@ -284,7 +282,6 @@ class QTest(unittest.TestCase):
person = self.Person(name='Guido van Rossum') person = self.Person(name='Guido van Rossum')
person.save() person.save()
import re
obj = self.Person.objects(Q(name=re.compile('^Gui'))).first() obj = self.Person.objects(Q(name=re.compile('^Gui'))).first()
self.assertEqual(obj, person) self.assertEqual(obj, person)
obj = self.Person.objects(Q(name=re.compile('^gui'))).first() obj = self.Person.objects(Q(name=re.compile('^gui'))).first()

View File

@ -1,13 +1,11 @@
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:
import unittest import unittest
from nose.plugins.skip import SkipTest
import pymongo import pymongo
from bson.tz_util import utc from bson.tz_util import utc
@ -18,7 +16,8 @@ from mongoengine import (
) )
from mongoengine.python_support import IS_PYMONGO_3 from mongoengine.python_support import IS_PYMONGO_3
import mongoengine.connection import mongoengine.connection
from mongoengine.connection import get_db, get_connection, ConnectionError from mongoengine.connection import (MongoEngineConnectionError, get_db,
get_connection)
def get_tz_awareness(connection): def get_tz_awareness(connection):
@ -51,6 +50,84 @@ class ConnectionTest(unittest.TestCase):
conn = get_connection('testdb') conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
def test_connect_in_mocking(self):
"""Ensure that the connect() method works properly in mocking.
"""
try:
import mongomock
except ImportError:
raise SkipTest('you need mongomock installed to run this testcase')
connect('mongoenginetest', host='mongomock://localhost')
conn = get_connection()
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect('mongoenginetest2', host='mongomock://localhost', alias='testdb2')
conn = get_connection('testdb2')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect('mongoenginetest3', host='mongodb://localhost', is_mock=True, alias='testdb3')
conn = get_connection('testdb3')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect('mongoenginetest4', is_mock=True, alias='testdb4')
conn = get_connection('testdb4')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host='mongodb://localhost:27017/mongoenginetest5', is_mock=True, alias='testdb5')
conn = get_connection('testdb5')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host='mongomock://localhost:27017/mongoenginetest6', alias='testdb6')
conn = get_connection('testdb6')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host='mongomock://localhost:27017/mongoenginetest7', is_mock=True, alias='testdb7')
conn = get_connection('testdb7')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
def test_connect_with_host_list(self):
"""Ensure that the connect() method works when host is a list
Uses mongomock to test w/o needing multiple mongod/mongos processes
"""
try:
import mongomock
except ImportError:
raise SkipTest('you need mongomock installed to run this testcase')
connect(host=['mongomock://localhost'])
conn = get_connection()
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host=['mongodb://localhost'], is_mock=True, alias='testdb2')
conn = get_connection('testdb2')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host=['localhost'], is_mock=True, alias='testdb3')
conn = get_connection('testdb3')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host=['mongomock://localhost:27017', 'mongomock://localhost:27018'], alias='testdb4')
conn = get_connection('testdb4')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host=['mongodb://localhost:27017', 'mongodb://localhost:27018'], is_mock=True, alias='testdb5')
conn = get_connection('testdb5')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
connect(host=['localhost:27017', 'localhost:27018'], is_mock=True, alias='testdb6')
conn = get_connection('testdb6')
self.assertTrue(isinstance(conn, mongomock.MongoClient))
def test_disconnect(self):
"""Ensure that the disconnect() method works properly
"""
conn1 = connect('mongoenginetest')
mongoengine.connection.disconnect()
conn2 = connect('mongoenginetest')
self.assertTrue(conn1 is not conn2)
def test_sharing_connections(self): def test_sharing_connections(self):
"""Ensure that connections are shared when the connection settings are exactly the same """Ensure that connections are shared when the connection settings are exactly the same
""" """
@ -80,7 +157,10 @@ class ConnectionTest(unittest.TestCase):
c.mongoenginetest.add_user("username", "password") c.mongoenginetest.add_user("username", "password")
if not IS_PYMONGO_3: if not IS_PYMONGO_3:
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost') self.assertRaises(
MongoEngineConnectionError, connect, 'testdb_uri_bad',
host='mongodb://test:password@localhost'
)
connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest') connect("testdb_uri", host='mongodb://username:password@localhost/mongoenginetest')
@ -95,19 +175,9 @@ class ConnectionTest(unittest.TestCase):
c.mongoenginetest.system.users.remove({}) c.mongoenginetest.system.users.remove({})
def test_connect_uri_without_db(self): def test_connect_uri_without_db(self):
"""Ensure connect() method works properly with uri's without database_name """Ensure connect() method works properly if the URI doesn't
include a database name.
""" """
c = connect(db='mongoenginetest', alias='admin')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
c.admin.add_user("admin", "password")
c.admin.authenticate("admin", "password")
c.mongoenginetest.add_user("username", "password")
if not IS_PYMONGO_3:
self.assertRaises(ConnectionError, connect, "testdb_uri_bad", host='mongodb://test:password@localhost')
connect("mongoenginetest", host='mongodb://localhost/') connect("mongoenginetest", host='mongodb://localhost/')
conn = get_connection() conn = get_connection()
@ -117,8 +187,44 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(isinstance(db, pymongo.database.Database)) self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest') self.assertEqual(db.name, 'mongoenginetest')
c.admin.system.users.remove({}) def test_connect_uri_default_db(self):
c.mongoenginetest.system.users.remove({}) """Ensure connect() defaults to the right database name if
the URI and the database_name don't explicitly specify it.
"""
connect(host='mongodb://localhost/')
conn = get_connection()
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
db = get_db()
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'test')
def test_connect_uri_with_replicaset(self):
"""Ensure connect() works when specifying a replicaSet."""
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_uri_without_credentials_doesnt_override_conn_settings(self):
"""Ensure connect() uses the username & password params if the URI
doesn't explicitly specify them.
"""
c = connect(host='mongodb://localhost/mongoenginetest',
username='user',
password='pass')
# OperationFailure means that mongoengine attempted authentication
# w/ the provided username/password and failed - that's the desired
# behavior. If the MongoDB URI would override the credentials
self.assertRaises(OperationFailure, get_db)
def test_connect_uri_with_authsource(self): def test_connect_uri_with_authsource(self):
"""Ensure that the connect() method works well with """Ensure that the connect() method works well with
@ -137,13 +243,14 @@ class ConnectionTest(unittest.TestCase):
self.assertRaises(OperationFailure, test_conn.server_info) self.assertRaises(OperationFailure, test_conn.server_info)
else: else:
self.assertRaises( self.assertRaises(
ConnectionError, connect, 'mongoenginetest', alias='test1', MongoEngineConnectionError, connect, 'mongoenginetest',
alias='test1',
host='mongodb://username2:password@localhost/mongoenginetest' host='mongodb://username2:password@localhost/mongoenginetest'
) )
self.assertRaises(ConnectionError, get_db, 'test1') self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
# Authentication succeeds with "authSource" # Authentication succeeds with "authSource"
test_conn2 = connect( connect(
'mongoenginetest', alias='test2', 'mongoenginetest', alias='test2',
host=('mongodb://username2:password@localhost/' host=('mongodb://username2:password@localhost/'
'mongoenginetest?authSource=admin') 'mongoenginetest?authSource=admin')
@ -161,7 +268,7 @@ class ConnectionTest(unittest.TestCase):
""" """
register_connection('testdb', 'mongoenginetest2') register_connection('testdb', 'mongoenginetest2')
self.assertRaises(ConnectionError, get_connection) self.assertRaises(MongoEngineConnectionError, get_connection)
conn = get_connection('testdb') conn = get_connection('testdb')
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
@ -178,8 +285,7 @@ class ConnectionTest(unittest.TestCase):
self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient)) self.assertTrue(isinstance(conn, pymongo.mongo_client.MongoClient))
def test_connection_kwargs(self): def test_connection_kwargs(self):
"""Ensure that connection kwargs get passed to pymongo. """Ensure that connection kwargs get passed to pymongo."""
"""
connect('mongoenginetest', alias='t1', tz_aware=True) connect('mongoenginetest', alias='t1', tz_aware=True)
conn = get_connection('t1') conn = get_connection('t1')
@ -189,6 +295,45 @@ 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_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)

View File

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

View File

@ -1,4 +1,5 @@
import unittest import unittest
from mongoengine.base.datastructures import StrictDict, SemiStrictDict from mongoengine.base.datastructures import StrictDict, SemiStrictDict
@ -13,8 +14,17 @@ class TestStrictDict(unittest.TestCase):
d = self.dtype(a=1, b=1, c=1) d = self.dtype(a=1, b=1, c=1)
self.assertEqual((d.a, d.b, d.c), (1, 1, 1)) self.assertEqual((d.a, d.b, d.c), (1, 1, 1))
def test_repr(self):
d = self.dtype(a=1, b=2, c=3)
self.assertEqual(repr(d), '{"a": 1, "b": 2, "c": 3}')
# make sure quotes are escaped properly
d = self.dtype(a='"', b="'", c="")
self.assertEqual(repr(d), '{"a": \'"\', "b": "\'", "c": \'\'}')
def test_init_fails_on_nonexisting_attrs(self): def test_init_fails_on_nonexisting_attrs(self):
self.assertRaises(AttributeError, lambda: self.dtype(a=1, b=2, d=3)) with self.assertRaises(AttributeError):
self.dtype(a=1, b=2, d=3)
def test_eq(self): def test_eq(self):
d = self.dtype(a=1, b=1, c=1) d = self.dtype(a=1, b=1, c=1)
@ -37,14 +47,12 @@ class TestStrictDict(unittest.TestCase):
d = self.dtype() d = self.dtype()
d.a = 1 d.a = 1
self.assertEqual(d.a, 1) self.assertEqual(d.a, 1)
self.assertRaises(AttributeError, lambda: d.b) self.assertRaises(AttributeError, getattr, d, 'b')
def test_setattr_raises_on_nonexisting_attr(self): def test_setattr_raises_on_nonexisting_attr(self):
d = self.dtype() d = self.dtype()
with self.assertRaises(AttributeError):
def _f():
d.x = 1 d.x = 1
self.assertRaises(AttributeError, _f)
def test_setattr_getattr_special(self): def test_setattr_getattr_special(self):
d = self.strict_dict_class(["items"]) d = self.strict_dict_class(["items"])

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest import unittest
from bson import DBRef, ObjectId from bson import DBRef, ObjectId
@ -12,9 +10,13 @@ from mongoengine.context_managers import query_counter
class FieldTest(unittest.TestCase): class FieldTest(unittest.TestCase):
def setUp(self): @classmethod
connect(db='mongoenginetest') def setUpClass(cls):
self.db = get_db() cls.db = connect(db='mongoenginetest')
@classmethod
def tearDownClass(cls):
cls.db.drop_database('mongoenginetest')
def test_list_item_dereference(self): def test_list_item_dereference(self):
"""Ensure that DBRef items in ListFields are dereferenced. """Ensure that DBRef items in ListFields are dereferenced.
@ -28,7 +30,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Group.drop_collection() Group.drop_collection()
for i in xrange(1, 51): for i in range(1, 51):
user = User(name='user %s' % i) user = User(name='user %s' % i)
user.save() user.save()
@ -86,7 +88,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Group.drop_collection() Group.drop_collection()
for i in xrange(1, 51): for i in range(1, 51):
user = User(name='user %s' % i) user = User(name='user %s' % i)
user.save() user.save()
@ -158,7 +160,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Group.drop_collection() Group.drop_collection()
for i in xrange(1, 26): for i in range(1, 26):
user = User(name='user %s' % i) user = User(name='user %s' % i)
user.save() user.save()
@ -304,6 +306,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Post.drop_collection() Post.drop_collection()
SimpleList.drop_collection()
u1 = User.objects.create(name='u1') u1 = User.objects.create(name='u1')
u2 = User.objects.create(name='u2') u2 = User.objects.create(name='u2')
@ -435,7 +438,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i) a = UserA(name='User A %s' % i)
a.save() a.save()
@ -526,7 +529,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i) a = UserA(name='User A %s' % i)
a.save() a.save()
@ -609,15 +612,15 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
user = User(name='user %s' % i) user = User(name='user %s' % i)
user.save() user.save()
members.append(user) members.append(user)
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
with query_counter() as q: with query_counter() as q:
@ -682,7 +685,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i) a = UserA(name='User A %s' % i)
a.save() a.save()
@ -694,9 +697,9 @@ class FieldTest(unittest.TestCase):
members += [a, b, c] members += [a, b, c]
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
with query_counter() as q: with query_counter() as q:
@ -778,16 +781,16 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i) a = UserA(name='User A %s' % i)
a.save() a.save()
members += [a] members += [a]
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
with query_counter() as q: with query_counter() as q:
@ -861,7 +864,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i) a = UserA(name='User A %s' % i)
a.save() a.save()
@ -873,9 +876,9 @@ class FieldTest(unittest.TestCase):
members += [a, b, c] members += [a, b, c]
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
group = Group(members=dict([(str(u.id), u) for u in members])) group = Group(members={str(u.id): u for u in members})
group.save() group.save()
with query_counter() as q: with query_counter() as q:
@ -1098,7 +1101,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Group.drop_collection() Group.drop_collection()
for i in xrange(1, 51): for i in range(1, 51):
User(name='user %s' % i).save() User(name='user %s' % i).save()
Group(name="Test", members=User.objects).save() Group(name="Test", members=User.objects).save()
@ -1127,7 +1130,7 @@ class FieldTest(unittest.TestCase):
User.drop_collection() User.drop_collection()
Group.drop_collection() Group.drop_collection()
for i in xrange(1, 51): for i in range(1, 51):
User(name='user %s' % i).save() User(name='user %s' % i).save()
Group(name="Test", members=User.objects).save() Group(name="Test", members=User.objects).save()
@ -1164,7 +1167,7 @@ class FieldTest(unittest.TestCase):
Group.drop_collection() Group.drop_collection()
members = [] members = []
for i in xrange(1, 51): for i in range(1, 51):
a = UserA(name='User A %s' % i).save() a = UserA(name='User A %s' % i).save()
b = UserB(name='User B %s' % i).save() b = UserB(name='User B %s' % i).save()
c = UserC(name='User C %s' % i).save() c = UserC(name='User C %s' % i).save()

View File

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

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
sys.path[0:0] = [""]
import unittest import unittest
from mongoengine import * from mongoengine import *
@ -25,6 +23,8 @@ class SignalTests(unittest.TestCase):
connect(db='mongoenginetest') connect(db='mongoenginetest')
class Author(Document): class Author(Document):
# Make the id deterministic for easier testing
id = SequenceField(primary_key=True)
name = StringField() name = StringField()
def __unicode__(self): def __unicode__(self):
@ -33,7 +33,7 @@ class SignalTests(unittest.TestCase):
@classmethod @classmethod
def pre_init(cls, sender, document, *args, **kwargs): def pre_init(cls, sender, document, *args, **kwargs):
signal_output.append('pre_init signal, %s' % cls.__name__) signal_output.append('pre_init signal, %s' % cls.__name__)
signal_output.append(str(kwargs['values'])) signal_output.append(kwargs['values'])
@classmethod @classmethod
def post_init(cls, sender, document, **kwargs): def post_init(cls, sender, document, **kwargs):
@ -43,48 +43,55 @@ class SignalTests(unittest.TestCase):
@classmethod @classmethod
def pre_save(cls, sender, document, **kwargs): def pre_save(cls, sender, document, **kwargs):
signal_output.append('pre_save signal, %s' % document) signal_output.append('pre_save signal, %s' % document)
signal_output.append(kwargs)
@classmethod @classmethod
def pre_save_post_validation(cls, sender, document, **kwargs): def pre_save_post_validation(cls, sender, document, **kwargs):
signal_output.append('pre_save_post_validation signal, %s' % document) signal_output.append('pre_save_post_validation signal, %s' % document)
if 'created' in kwargs: if kwargs.pop('created', False):
if kwargs['created']:
signal_output.append('Is created') signal_output.append('Is created')
else: else:
signal_output.append('Is updated') signal_output.append('Is updated')
signal_output.append(kwargs)
@classmethod @classmethod
def post_save(cls, sender, document, **kwargs): def post_save(cls, sender, document, **kwargs):
dirty_keys = document._delta()[0].keys() + document._delta()[1].keys() dirty_keys = document._delta()[0].keys() + document._delta()[1].keys()
signal_output.append('post_save signal, %s' % document) signal_output.append('post_save signal, %s' % document)
signal_output.append('post_save dirty keys, %s' % dirty_keys) signal_output.append('post_save dirty keys, %s' % dirty_keys)
if 'created' in kwargs: if kwargs.pop('created', False):
if kwargs['created']:
signal_output.append('Is created') signal_output.append('Is created')
else: else:
signal_output.append('Is updated') signal_output.append('Is updated')
signal_output.append(kwargs)
@classmethod @classmethod
def pre_delete(cls, sender, document, **kwargs): def pre_delete(cls, sender, document, **kwargs):
signal_output.append('pre_delete signal, %s' % document) signal_output.append('pre_delete signal, %s' % document)
signal_output.append(kwargs)
@classmethod @classmethod
def post_delete(cls, sender, document, **kwargs): def post_delete(cls, sender, document, **kwargs):
signal_output.append('post_delete signal, %s' % document) signal_output.append('post_delete signal, %s' % document)
signal_output.append(kwargs)
@classmethod @classmethod
def pre_bulk_insert(cls, sender, documents, **kwargs): def pre_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('pre_bulk_insert signal, %s' % documents) signal_output.append('pre_bulk_insert signal, %s' % documents)
signal_output.append(kwargs)
@classmethod @classmethod
def post_bulk_insert(cls, sender, documents, **kwargs): def post_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('post_bulk_insert signal, %s' % documents) signal_output.append('post_bulk_insert signal, %s' % documents)
if kwargs.get('loaded', False): if kwargs.pop('loaded', False):
signal_output.append('Is loaded') signal_output.append('Is loaded')
else: else:
signal_output.append('Not loaded') signal_output.append('Not loaded')
signal_output.append(kwargs)
self.Author = Author self.Author = Author
Author.drop_collection() Author.drop_collection()
Author.id.set_next_value(0)
class Another(Document): class Another(Document):
@ -96,10 +103,12 @@ class SignalTests(unittest.TestCase):
@classmethod @classmethod
def pre_delete(cls, sender, document, **kwargs): def pre_delete(cls, sender, document, **kwargs):
signal_output.append('pre_delete signal, %s' % document) signal_output.append('pre_delete signal, %s' % document)
signal_output.append(kwargs)
@classmethod @classmethod
def post_delete(cls, sender, document, **kwargs): def post_delete(cls, sender, document, **kwargs):
signal_output.append('post_delete signal, %s' % document) signal_output.append('post_delete signal, %s' % document)
signal_output.append(kwargs)
self.Another = Another self.Another = Another
Another.drop_collection() Another.drop_collection()
@ -118,6 +127,41 @@ class SignalTests(unittest.TestCase):
self.ExplicitId = ExplicitId self.ExplicitId = ExplicitId
ExplicitId.drop_collection() ExplicitId.drop_collection()
class Post(Document):
title = StringField()
content = StringField()
active = BooleanField(default=False)
def __unicode__(self):
return self.title
@classmethod
def pre_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('pre_bulk_insert signal, %s' %
[(doc, {'active': documents[n].active})
for n, doc in enumerate(documents)])
# make changes here, this is just an example -
# it could be anything that needs pre-validation or looks-ups before bulk bulk inserting
for document in documents:
if not document.active:
document.active = True
signal_output.append(kwargs)
@classmethod
def post_bulk_insert(cls, sender, documents, **kwargs):
signal_output.append('post_bulk_insert signal, %s' %
[(doc, {'active': documents[n].active})
for n, doc in enumerate(documents)])
if kwargs.pop('loaded', False):
signal_output.append('Is loaded')
else:
signal_output.append('Not loaded')
signal_output.append(kwargs)
self.Post = Post
Post.drop_collection()
# Save up the number of connected signals so that we can check at the # Save up the number of connected signals so that we can check at the
# end that all the signals we register get properly unregistered # end that all the signals we register get properly unregistered
self.pre_signals = ( self.pre_signals = (
@ -147,6 +191,9 @@ class SignalTests(unittest.TestCase):
signals.post_save.connect(ExplicitId.post_save, sender=ExplicitId) signals.post_save.connect(ExplicitId.post_save, sender=ExplicitId)
signals.pre_bulk_insert.connect(Post.pre_bulk_insert, sender=Post)
signals.post_bulk_insert.connect(Post.post_bulk_insert, sender=Post)
def tearDown(self): def tearDown(self):
signals.pre_init.disconnect(self.Author.pre_init) signals.pre_init.disconnect(self.Author.pre_init)
signals.post_init.disconnect(self.Author.post_init) signals.post_init.disconnect(self.Author.post_init)
@ -163,6 +210,9 @@ class SignalTests(unittest.TestCase):
signals.post_save.disconnect(self.ExplicitId.post_save) signals.post_save.disconnect(self.ExplicitId.post_save)
signals.pre_bulk_insert.disconnect(self.Post.pre_bulk_insert)
signals.post_bulk_insert.disconnect(self.Post.post_bulk_insert)
# Check that all our signals got disconnected properly. # Check that all our signals got disconnected properly.
post_signals = ( post_signals = (
len(signals.pre_init.receivers), len(signals.pre_init.receivers),
@ -202,63 +252,118 @@ class SignalTests(unittest.TestCase):
self.assertEqual(self.get_signal_output(create_author), [ self.assertEqual(self.get_signal_output(create_author), [
"pre_init signal, Author", "pre_init signal, Author",
"{'name': 'Bill Shakespeare'}", {'name': 'Bill Shakespeare'},
"post_init signal, Bill Shakespeare, document._created = True", "post_init signal, Bill Shakespeare, document._created = True",
]) ])
a1 = self.Author(name='Bill Shakespeare') a1 = self.Author(name='Bill Shakespeare')
self.assertEqual(self.get_signal_output(a1.save), [ self.assertEqual(self.get_signal_output(a1.save), [
"pre_save signal, Bill Shakespeare", "pre_save signal, Bill Shakespeare",
{},
"pre_save_post_validation signal, Bill Shakespeare", "pre_save_post_validation signal, Bill Shakespeare",
"Is created", "Is created",
{},
"post_save signal, Bill Shakespeare", "post_save signal, Bill Shakespeare",
"post_save dirty keys, ['name']", "post_save dirty keys, ['name']",
"Is created" "Is created",
{}
]) ])
a1.reload() a1.reload()
a1.name = 'William Shakespeare' a1.name = 'William Shakespeare'
self.assertEqual(self.get_signal_output(a1.save), [ self.assertEqual(self.get_signal_output(a1.save), [
"pre_save signal, William Shakespeare", "pre_save signal, William Shakespeare",
{},
"pre_save_post_validation signal, William Shakespeare", "pre_save_post_validation signal, William Shakespeare",
"Is updated", "Is updated",
{},
"post_save signal, William Shakespeare", "post_save signal, William Shakespeare",
"post_save dirty keys, ['name']", "post_save dirty keys, ['name']",
"Is updated" "Is updated",
{}
]) ])
self.assertEqual(self.get_signal_output(a1.delete), [ self.assertEqual(self.get_signal_output(a1.delete), [
'pre_delete signal, William Shakespeare', 'pre_delete signal, William Shakespeare',
{},
'post_delete signal, William Shakespeare', 'post_delete signal, William Shakespeare',
{}
]) ])
signal_output = self.get_signal_output(load_existing_author) self.assertEqual(self.get_signal_output(load_existing_author), [
# test signal_output lines separately, because of random ObjectID after object load
self.assertEqual(signal_output[0],
"pre_init signal, Author", "pre_init signal, Author",
) {'id': 2, 'name': 'Bill Shakespeare'},
self.assertEqual(signal_output[2], "post_init signal, Bill Shakespeare, document._created = False"
"post_init signal, Bill Shakespeare, document._created = False", ])
)
self.assertEqual(self.get_signal_output(bulk_create_author_with_load), [
signal_output = self.get_signal_output(bulk_create_author_with_load) 'pre_init signal, Author',
{'name': 'Bill Shakespeare'},
# The output of this signal is not entirely deterministic. The reloaded 'post_init signal, Bill Shakespeare, document._created = True',
# object will have an object ID. Hence, we only check part of the output 'pre_bulk_insert signal, [<Author: Bill Shakespeare>]',
self.assertEqual(signal_output[3], "pre_bulk_insert signal, [<Author: Bill Shakespeare>]" {},
) 'pre_init signal, Author',
self.assertEqual(signal_output[-2:], {'id': 3, 'name': 'Bill Shakespeare'},
["post_bulk_insert signal, [<Author: Bill Shakespeare>]", 'post_init signal, Bill Shakespeare, document._created = False',
"Is loaded",]) 'post_bulk_insert signal, [<Author: Bill Shakespeare>]',
'Is loaded',
{}
])
self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [ self.assertEqual(self.get_signal_output(bulk_create_author_without_load), [
"pre_init signal, Author", "pre_init signal, Author",
"{'name': 'Bill Shakespeare'}", {'name': 'Bill Shakespeare'},
"post_init signal, Bill Shakespeare, document._created = True", "post_init signal, Bill Shakespeare, document._created = True",
"pre_bulk_insert signal, [<Author: Bill Shakespeare>]", "pre_bulk_insert signal, [<Author: Bill Shakespeare>]",
{},
"post_bulk_insert signal, [<Author: Bill Shakespeare>]", "post_bulk_insert signal, [<Author: Bill Shakespeare>]",
"Not loaded", "Not loaded",
{}
])
def test_signal_kwargs(self):
""" Make sure signal_kwargs is passed to signals calls. """
def live_and_let_die():
a = self.Author(name='Bill Shakespeare')
a.save(signal_kwargs={'live': True, 'die': False})
a.delete(signal_kwargs={'live': False, 'die': True})
self.assertEqual(self.get_signal_output(live_and_let_die), [
"pre_init signal, Author",
{'name': 'Bill Shakespeare'},
"post_init signal, Bill Shakespeare, document._created = True",
"pre_save signal, Bill Shakespeare",
{'die': False, 'live': True},
"pre_save_post_validation signal, Bill Shakespeare",
"Is created",
{'die': False, 'live': True},
"post_save signal, Bill Shakespeare",
"post_save dirty keys, ['name']",
"Is created",
{'die': False, 'live': True},
'pre_delete signal, Bill Shakespeare',
{'die': True, 'live': False},
'post_delete signal, Bill Shakespeare',
{'die': True, 'live': False}
])
def bulk_create_author():
a1 = self.Author(name='Bill Shakespeare')
self.Author.objects.insert([a1], signal_kwargs={'key': True})
self.assertEqual(self.get_signal_output(bulk_create_author), [
'pre_init signal, Author',
{'name': 'Bill Shakespeare'},
'post_init signal, Bill Shakespeare, document._created = True',
'pre_bulk_insert signal, [<Author: Bill Shakespeare>]',
{'key': True},
'pre_init signal, Author',
{'id': 2, 'name': 'Bill Shakespeare'},
'post_init signal, Bill Shakespeare, document._created = False',
'post_bulk_insert signal, [<Author: Bill Shakespeare>]',
'Is loaded',
{'key': True}
]) ])
def test_queryset_delete_signals(self): def test_queryset_delete_signals(self):
@ -267,7 +372,9 @@ class SignalTests(unittest.TestCase):
self.Another(name='Bill Shakespeare').save() self.Another(name='Bill Shakespeare').save()
self.assertEqual(self.get_signal_output(self.Another.objects.delete), [ self.assertEqual(self.get_signal_output(self.Another.objects.delete), [
'pre_delete signal, Bill Shakespeare', 'pre_delete signal, Bill Shakespeare',
{},
'post_delete signal, Bill Shakespeare', 'post_delete signal, Bill Shakespeare',
{}
]) ])
def test_signals_with_explicit_doc_ids(self): def test_signals_with_explicit_doc_ids(self):
@ -306,6 +413,23 @@ class SignalTests(unittest.TestCase):
ei.switch_db("testdb-1", keep_created=False) ei.switch_db("testdb-1", keep_created=False)
self.assertEqual(self.get_signal_output(ei.save), ['Is created']) self.assertEqual(self.get_signal_output(ei.save), ['Is created'])
def test_signals_bulk_insert(self):
def bulk_set_active_post():
posts = [
self.Post(title='Post 1'),
self.Post(title='Post 2'),
self.Post(title='Post 3')
]
self.Post.objects.insert(posts)
results = self.get_signal_output(bulk_set_active_post)
self.assertEqual(results, [
"pre_bulk_insert signal, [(<Post: Post 1>, {'active': False}), (<Post: Post 2>, {'active': False}), (<Post: Post 3>, {'active': False})]",
{},
"post_bulk_insert signal, [(<Post: Post 1>, {'active': True}), (<Post: Post 2>, {'active': True}), (<Post: Post 3>, {'active': True})]",
'Is loaded',
{}
])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

22
tests/utils.py Normal file
View File

@ -0,0 +1,22 @@
import unittest
from mongoengine import connect
from mongoengine.connection import get_db
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)

16
tox.ini
View File

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