Compare commits

..

No commits in common. "master" and "v0.18.2" have entirely different histories.

136 changed files with 13608 additions and 16810 deletions

View File

@ -1,143 +0,0 @@
name: MongoengineCI
on:
# All PR
pull_request:
# master branch merge
push:
branches:
- master
# release tags
create:
tags:
- 'v[0-9]+\.[0-9]+\.[0-9]+*'
env:
MONGODB_3_6: 3.6.14
MONGODB_4_0: 4.0.23
MONGODB_4_2: 4.2
MONGODB_4_4: 4.4
PYMONGO_3_4: 3.4
PYMONGO_3_6: 3.6
PYMONGO_3_9: 3.9
PYMONGO_3_11: 3.11
MAIN_PYTHON_VERSION: 3.7
jobs:
linting:
# Run pre-commit (https://pre-commit.com/)
# which runs pre-configured linter & autoformatter
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- run: bash .github/workflows/install_ci_python_dep.sh
- run: pre-commit run -a
test:
# Test suite run against recent python versions
# and against a few combination of MongoDB and pymongo
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3]
MONGODB: [$MONGODB_4_0]
PYMONGO: [$PYMONGO_3_11]
include:
- python-version: 3.7
MONGODB: $MONGODB_3_6
PYMONGO: $PYMONGO_3_9
- python-version: 3.7
MONGODB: $MONGODB_4_2
PYMONGO: $PYMONGO_3_6
- python-version: 3.7
MONGODB: $MONGODB_4_4
PYMONGO: $PYMONGO_3_11
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: install mongo and ci dependencies
run: |
bash .github/workflows/install_mongo.sh ${{ matrix.MONGODB }}
bash .github/workflows/install_ci_python_dep.sh
bash .github/workflows/start_mongo.sh ${{ matrix.MONGODB }}
- name: tox dry-run (to pre-install venv)
run: tox -e $(echo py${{ matrix.python-version }}-mg${{ matrix.PYMONGO }} | tr -d . | sed -e 's/pypypy/pypy/') -- -a "-k=test_ci_placeholder"
- name: Run test suite
run: tox -e $(echo py${{ matrix.python-version }}-mg${{ matrix.PYMONGO }} | tr -d . | sed -e 's/pypypy/pypy/') -- -a "--cov=mongoengine"
- name: Send coverage to Coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github
if: ${{ matrix.python-version == env.MAIN_PYTHON_VERSION }}
run: coveralls
build_doc_dryrun:
# ensures that readthedocs can be built continuously
# to avoid that it breaks when new releases are being created
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: install python dep
run: |
pip install -e .
pip install -r docs/requirements.txt
- name: build doc
run: |
cd docs
make html-readthedocs
build-n-publish-dummy:
runs-on: ubuntu-latest
needs: [linting, test, build_doc_dryrun]
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: build dummy wheel for test-pypi
run: |
pip install wheel
python setup.py egg_info -b ".dev`date '+%Y%m%d%H%M%S'`" build sdist bdist_wheel
# - name: publish test-pypi
# # Although working and recommended, test-pypi has a limit
# # in the size of projects so it's better to avoid publishing
# # until there is a way to garbage collect these dummy releases
# uses: pypa/gh-action-pypi-publish@master
# with:
# password: ${{ secrets.test_pypi_token }}
# repository_url: https://test.pypi.org/legacy/
build-n-publish:
runs-on: ubuntu-latest
needs: [linting, test, build_doc_dryrun, build-n-publish-dummy]
if: github.event_name == 'create' && startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@master
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
# todo separate build from publish
# https://stackoverflow.com/questions/59349905/which-properties-does-github-event-in-a-github-workflow-have
- name: build dummy wheel for test-pypi
run: |
pip install wheel
python setup.py sdist bdist_wheel
- name: publish pypi
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.pypi_token }}

View File

@ -1,5 +0,0 @@
#!/bin/bash
pip install --upgrade pip
pip install coveralls
pip install pre-commit
pip install tox

View File

@ -1,18 +0,0 @@
#!/bin/bash
MONGODB=$1
# Mongo > 4.0 follows different name convention for download links
mongo_build=mongodb-linux-x86_64-${MONGODB}
if [[ "$MONGODB" == *"4.2"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
elif [[ "$MONGODB" == *"4.4"* ]]; then
mongo_build=mongodb-linux-x86_64-ubuntu1804-v${MONGODB}-latest
fi
wget http://fastdl.mongodb.org/linux/$mongo_build.tgz
tar xzf $mongo_build.tgz
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
$mongodb_dir/bin/mongod --version

View File

@ -1,9 +0,0 @@
#!/bin/bash
MONGODB=$1
mongodb_dir=$(find ${PWD}/ -type d -name "mongodb-linux-x86_64*")
mkdir $mongodb_dir/data
$mongodb_dir/bin/mongod --dbpath $mongodb_dir/data --logpath $mongodb_dir/mongodb.log --fork
mongo --eval 'db.version();' # Make sure mongo is awake

11
.gitignore vendored
View File

@ -1,15 +1,8 @@
.*
!.gitignore !.gitignore
*~ *~
*.py[co] *.py[co]
.*.sw[po] .*.sw[po]
.cache/
.coverage
.coveragerc
.env
.idea/
.pytest_cache/
.tox/
.eggs/
*.egg *.egg
docs/.build docs/.build
docs/_build docs/_build
@ -20,6 +13,8 @@ env/
.settings .settings
.project .project
.pydevproject .pydevproject
tests/test_bugfix.py
htmlcov/ htmlcov/
venv venv
venv3 venv3
scratchpad

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,26 +0,0 @@
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: check-merge-conflict
- id: debug-statements
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/ambv/black
rev: 21.5b2
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
- repo: https://github.com/asottile/pyupgrade
rev: v2.19.1
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/pycqa/isort
rev: 5.8.0
hooks:
- id: isort

View File

@ -1,20 +0,0 @@
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: docs/requirements.txt
# docs/conf.py is importing mongoengine
# so mongoengine needs to be installed as well
- method: setuptools
path: .

106
.travis.yml Normal file
View File

@ -0,0 +1,106 @@
# For full coverage, we'd have to test all supported Python, MongoDB, and
# PyMongo combinations. However, that would result in an overly long build
# with a very large number of jobs, hence we only test a subset of all the
# combinations:
# * MongoDB v3.4 & the latest PyMongo v3.x is currently the "main" setup,
# tested against Python v2.7, v3.5, v3.6, and PyPy.
# * Besides that, we test the lowest actively supported Python/MongoDB/PyMongo
# combination: MongoDB v3.4, PyMongo v3.4, Python v2.7.
# * MongoDB v3.6 is tested against Python v3.6, and PyMongo v3.6, v3.7, v3.8.
#
# We should periodically check MongoDB Server versions supported by MongoDB
# Inc., add newly released versions to the test matrix, and remove versions
# which have reached their End of Life. See:
# 1. https://www.mongodb.com/support-policy.
# 2. https://docs.mongodb.com/ecosystem/drivers/driver-compatibility-reference/#python-driver-compatibility
#
# Reminder: Update README.rst if you change MongoDB versions we test.
language: python
python:
- 2.7
- 3.5
- 3.6
- pypy
dist: xenial
env:
global:
- MONGODB_3_4=3.4.17
- MONGODB_3_6=3.6.12
matrix:
- MONGODB=${MONGODB_3_4} PYMONGO=3.x
matrix:
# Finish the build as soon as one job fails
fast_finish: true
include:
- python: 2.7
env: MONGODB=${MONGODB_3_4} PYMONGO=3.4.x
- python: 3.6
env: MONGODB=${MONGODB_3_6} PYMONGO=3.x
- python: 3.7
env: MONGODB=${MONGODB_3_6} PYMONGO=3.x
install:
# Install Mongo
- wget http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-${MONGODB}.tgz
- tar xzf mongodb-linux-x86_64-${MONGODB}.tgz
- ${PWD}/mongodb-linux-x86_64-${MONGODB}/bin/mongod --version
# Install python dependencies
- pip install --upgrade pip
- pip install coveralls
- pip install flake8 flake8-import-order
- pip install tox # tox 3.11.0 has requirement virtualenv>=14.0.0
- pip install virtualenv # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
# Install the tox venv
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
before_script:
- mkdir ${PWD}/mongodb-linux-x86_64-${MONGODB}/data
- ${PWD}/mongodb-linux-x86_64-${MONGODB}/bin/mongod --dbpath ${PWD}/mongodb-linux-x86_64-${MONGODB}/data --logpath ${PWD}/mongodb-linux-x86_64-${MONGODB}/mongodb.log --fork
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi # Run flake8 for py27
- mongo --eval 'db.version();' # Make sure mongo is awake
script:
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
# For now only submit coveralls for Python v2.7. Python v3.x currently shows
# 0% coverage. That's caused by 'use_2to3', which builds the py3-compatible
# code in a separate dir and runs tests on that.
after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
notifications:
irc: irc.freenode.org#mongoengine
# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
branches:
only:
- master
- /^v.*$/
# Whenever a new release is created via GitHub, publish it on PyPI.
deploy:
provider: pypi
user: the_drow
password:
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
# Create a source distribution and a pure python wheel for faster installs.
distributions: "sdist bdist_wheel"
# Only deploy on tagged commits (aka GitHub releases) and only for the parent
# repo's builds running Python v2.7 along with PyMongo v3.x and MongoDB v3.4.
# We run Travis against many different Python, PyMongo, and MongoDB versions
# and we don't want the deploy to occur multiple times).
on:
tags: true
repo: MongoEngine/mongoengine
condition: ($PYMONGO = 3.x) && ($MONGODB = 3.4.17)
python: 2.7

11
AUTHORS
View File

@ -252,14 +252,3 @@ that much better:
* Paulo Amaral (https://github.com/pauloAmaral) * Paulo Amaral (https://github.com/pauloAmaral)
* Gaurav Dadhania (https://github.com/GVRV) * Gaurav Dadhania (https://github.com/GVRV)
* Yurii Andrieiev (https://github.com/yandrieiev) * Yurii Andrieiev (https://github.com/yandrieiev)
* Filip Kucharczyk (https://github.com/Pacu2)
* Eric Timmons (https://github.com/daewok)
* Matthew Simpson (https://github.com/mcsimps2)
* Leonardo Domingues (https://github.com/leodmgs)
* Agustin Barto (https://github.com/abarto)
* Stankiewicz Mateusz (https://github.com/mas15)
* Felix Schultheiß (https://github.com/felix-smashdocs)
* Jan Stein (https://github.com/janste63)
* Timothé Perez (https://github.com/AchilleAsh)
* oleksandr-l5 (https://github.com/oleksandr-l5)
* Ido Shraga (https://github.com/idoshr)

View File

@ -20,47 +20,23 @@ post to the `user group <http://groups.google.com/group/mongoengine-users>`
Supported Interpreters Supported Interpreters
---------------------- ----------------------
MongoEngine supports CPython 3.5 and newer as well as Pypy3. MongoEngine supports CPython 2.7 and newer. Language
Language features not supported by all interpreters can not be used. features not supported by all interpreters can not be used.
The codebase is written in python 2 so you must be using python 2
Python3 codebase when developing new features. Compatibility of the library with Python 3
---------------------- relies on the 2to3 package that gets executed as part of the installation
build. You should ensure that your code is properly converted by
Since 0.20, the codebase is exclusively Python 3. `2to3 <http://docs.python.org/library/2to3.html>`_.
Earlier versions were exclusively Python2, and were relying on 2to3 to support Python3 installs.
Travis runs the tests against the main Python 3.x versions.
Style Guide Style Guide
----------- -----------
MongoEngine's codebase is auto-formatted with `black <https://github.com/python/black>`_, imports are ordered with `isort <https://pycqa.github.io/isort/>`_ MongoEngine aims to follow `PEP8 <http://www.python.org/dev/peps/pep-0008/>`_
and other tools like flake8 are also used. Those tools will run as part of the CI and will fail in case the code is not formatted properly. including 4 space indents. When possible we try to stick to 79 character line
limits. However, screens got bigger and an ORM has a strong focus on
To install all development tools, simply run the following commands: readability and if it can help, we accept 119 as maximum line length, in a
similar way as `django does
.. code-block:: console <https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/#python-style>`_
$ python -m pip install -r requirements-dev.txt
You can install `pre-commit <https://pre-commit.com/>`_ into your git hooks,
to automatically check and fix any formatting issue before creating a
git commit.
To enable ``pre-commit`` simply run:
.. code-block:: console
$ pre-commit install
See the ``.pre-commit-config.yaml`` configuration file for more information
on how it works.
pre-commit will now run upon every commit and will reject anything that doesn't comply.
You can also run all the checks with ``pre-commit run -a``, this is what is used in the CI.
Testing Testing
------- -------
@ -82,7 +58,7 @@ General Guidelines
should adapt to the breaking change in docs/upgrade.rst. should adapt to the breaking change in docs/upgrade.rst.
- Write inline documentation for new classes and methods. - Write inline documentation for new classes and methods.
- Write tests and make sure they pass (make sure you have a mongod - Write tests and make sure they pass (make sure you have a mongod
running on the default port, then execute ``python setup.py test`` running on the default port, then execute ``python setup.py nosetests``
from the cmd line to run the test suite). from the cmd line to run the test suite).
- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions. - Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
You can test various Python and PyMongo versions locally by executing You can test various Python and PyMongo versions locally by executing

View File

@ -12,8 +12,9 @@ MongoEngine
.. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/MongoEngine/mongoengine/badge.svg?branch=master
:target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master :target: https://coveralls.io/github/MongoEngine/mongoengine?branch=master
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg .. image:: https://landscape.io/github/MongoEngine/mongoengine/master/landscape.svg?style=flat
:target: https://github.com/ambv/black :target: https://landscape.io/github/MongoEngine/mongoengine/master
:alt: Code Health
About About
===== =====
@ -25,15 +26,15 @@ an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
Supported MongoDB Versions Supported MongoDB Versions
========================== ==========================
MongoEngine is currently tested against MongoDB v3.4, v3.6 and v4.0. Future versions MongoEngine is currently tested against MongoDB v3.4 and v3.6. Future versions
should be supported as well, but aren't actively tested at the moment. Make should be supported as well, but aren't actively tested at the moment. Make
sure to open an issue or submit a pull request if you experience any problems sure to open an issue or submit a pull request if you experience any problems
with MongoDB version > 4.0. with MongoDB version > 3.6.
Installation Installation
============ ============
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
`pip <https://pip.pypa.io/>`_. You can then use ``python -m pip install -U mongoengine``. `pip <https://pip.pypa.io/>`_. You can then use ``pip install -U mongoengine``.
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
and thus you can use ``easy_install -U mongoengine``. Another option is and thus you can use ``easy_install -U mongoengine``. Another option is
`pipenv <https://docs.pipenv.org/>`_. You can then use ``pipenv install mongoengine`` `pipenv <https://docs.pipenv.org/>`_. You can then use ``pipenv install mongoengine``
@ -41,14 +42,13 @@ to both create the virtual environment and install the package. Otherwise, you c
download the source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and download the source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and
run ``python setup.py install``. run ``python setup.py install``.
The support for Python2 was dropped with MongoEngine 0.20.0
Dependencies Dependencies
============ ============
All of the dependencies can easily be installed via `python -m pip <https://pip.pypa.io/>`_. 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: At the very least, you'll need these two packages to use MongoEngine:
- pymongo>=3.4 - pymongo>=3.4
- six>=1.10.0
If you utilize a ``DateTimeField``, you might also use a more flexible date parser: If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
@ -58,10 +58,6 @@ If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
- Pillow>=2.0.0 - Pillow>=2.0.0
If you need to use signals:
- blinker>=1.3
Examples Examples
======== ========
Some simple examples of what MongoEngine code looks like: Some simple examples of what MongoEngine code looks like:
@ -95,11 +91,12 @@ Some simple examples of what MongoEngine code looks like:
# Iterate over all posts using the BlogPost superclass # Iterate over all posts using the BlogPost superclass
>>> for post in BlogPost.objects: >>> for post in BlogPost.objects:
... print('===', post.title, '===') ... print '===', post.title, '==='
... if isinstance(post, TextPost): ... if isinstance(post, TextPost):
... print(post.content) ... print post.content
... elif isinstance(post, LinkPost): ... elif isinstance(post, LinkPost):
... print('Link:', post.url) ... print 'Link:', post.url
... print
... ...
# Count all blog posts and its subtypes # Count all blog posts and its subtypes
@ -119,8 +116,7 @@ Some simple examples of what MongoEngine code looks like:
Tests Tests
===== =====
To run the test suite, ensure you are running a local instance of MongoDB on To run the test suite, ensure you are running a local instance of MongoDB on
the standard port and have ``pytest`` installed. Then, run ``python setup.py test`` the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``.
or simply ``pytest``.
To run the test suite on every supported Python and PyMongo version, you can To run the test suite on every supported Python and PyMongo version, you can
use ``tox``. You'll need to make sure you have each supported Python version use ``tox``. You'll need to make sure you have each supported Python version
@ -129,18 +125,20 @@ installed in your environment and then:
.. code-block:: shell .. code-block:: shell
# Install tox # Install tox
$ python -m pip install tox $ pip install tox
# Run the test suites # Run the test suites
$ tox $ tox
If you wish to run a subset of tests, use the pytest convention: If you wish to run a subset of tests, use the nosetests convention:
.. code-block:: shell .. code-block:: shell
# Run all the tests in a particular test file # Run all the tests in a particular test file
$ pytest tests/fields/test_fields.py $ python setup.py nosetests --tests tests/fields/fields.py
# Run only particular test class in that file # Run only particular test class in that file
$ pytest tests/fields/test_fields.py::TestField $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest
# Use the -s option if you want to print some debug statements or use pdb
$ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s
Community Community
========= =========

View File

@ -1,18 +1,11 @@
from timeit import repeat from timeit import repeat
import mongoengine import mongoengine
from mongoengine import ( from mongoengine import (BooleanField, Document, EmailField, EmbeddedDocument,
BooleanField, EmbeddedDocumentField, IntField, ListField,
Document, StringField)
EmailField,
EmbeddedDocument,
EmbeddedDocumentField,
IntField,
ListField,
StringField,
)
mongoengine.connect(db="mongoengine_benchmark_test") mongoengine.connect(db='mongoengine_benchmark_test')
def timeit(f, n=10000): def timeit(f, n=10000):
@ -31,41 +24,46 @@ def test_basic():
def init_book(): def init_book():
return Book( return Book(
name="Always be closing", name='Always be closing',
pages=100, pages=100,
tags=["self-help", "sales"], tags=['self-help', 'sales'],
is_published=True, is_published=True,
author_email="alec@example.com", author_email='alec@example.com',
) )
print("Doc initialization: %.3fus" % (timeit(init_book, 1000) * 10 ** 6)) print('Doc initialization: %.3fus' % (timeit(init_book, 1000) * 10**6))
b = init_book() b = init_book()
print("Doc getattr: %.3fus" % (timeit(lambda: b.name, 10000) * 10 ** 6)) print('Doc getattr: %.3fus' % (timeit(lambda: b.name, 10000) * 10**6))
print( print(
"Doc setattr: %.3fus" 'Doc setattr: %.3fus' % (
% (timeit(lambda: setattr(b, "name", "New name"), 10000) * 10 ** 6) # noqa B010 timeit(lambda: setattr(b, 'name', 'New name'), 10000) * 10**6
)
) )
print("Doc to mongo: %.3fus" % (timeit(b.to_mongo, 1000) * 10 ** 6)) print('Doc to mongo: %.3fus' % (timeit(b.to_mongo, 1000) * 10**6))
print("Doc validation: %.3fus" % (timeit(b.validate, 1000) * 10 ** 6)) print('Doc validation: %.3fus' % (timeit(b.validate, 1000) * 10**6))
def save_book(): def save_book():
b._mark_as_changed("name") b._mark_as_changed('name')
b._mark_as_changed("tags") b._mark_as_changed('tags')
b.save() b.save()
print("Save to database: %.3fus" % (timeit(save_book, 100) * 10 ** 6)) print('Save to database: %.3fus' % (timeit(save_book, 100) * 10**6))
son = b.to_mongo() son = b.to_mongo()
print( print(
"Load from SON: %.3fus" % (timeit(lambda: Book._from_son(son), 1000) * 10 ** 6) 'Load from SON: %.3fus' % (
timeit(lambda: Book._from_son(son), 1000) * 10**6
)
) )
print( print(
"Load from database: %.3fus" % (timeit(lambda: Book.objects[0], 100) * 10 ** 6) 'Load from database: %.3fus' % (
timeit(lambda: Book.objects[0], 100) * 10**6
)
) )
def create_and_delete_book(): def create_and_delete_book():
@ -74,8 +72,9 @@ def test_basic():
b.delete() b.delete()
print( print(
"Init + save to database + delete: %.3fms" 'Init + save to database + delete: %.3fms' % (
% (timeit(create_and_delete_book, 10) * 10 ** 3) timeit(create_and_delete_book, 10) * 10**3
)
) )
@ -93,36 +92,42 @@ def test_big_doc():
def init_company(): def init_company():
return Company( return Company(
name="MongoDB, Inc.", name='MongoDB, Inc.',
contacts=[ contacts=[
Contact(name="Contact %d" % x, title="CEO", address="Address %d" % x) Contact(
name='Contact %d' % x,
title='CEO',
address='Address %d' % x,
)
for x in range(1000) for x in range(1000)
], ]
) )
company = init_company() company = init_company()
print("Big doc to mongo: %.3fms" % (timeit(company.to_mongo, 100) * 10 ** 3)) print('Big doc to mongo: %.3fms' % (timeit(company.to_mongo, 100) * 10**3))
print("Big doc validation: %.3fms" % (timeit(company.validate, 1000) * 10 ** 3)) print('Big doc validation: %.3fms' % (timeit(company.validate, 1000) * 10**3))
company.save() company.save()
def save_company(): def save_company():
company._mark_as_changed("name") company._mark_as_changed('name')
company._mark_as_changed("contacts") company._mark_as_changed('contacts')
company.save() company.save()
print("Save to database: %.3fms" % (timeit(save_company, 100) * 10 ** 3)) print('Save to database: %.3fms' % (timeit(save_company, 100) * 10**3))
son = company.to_mongo() son = company.to_mongo()
print( print(
"Load from SON: %.3fms" 'Load from SON: %.3fms' % (
% (timeit(lambda: Company._from_son(son), 100) * 10 ** 3) timeit(lambda: Company._from_son(son), 100) * 10**3
)
) )
print( print(
"Load from database: %.3fms" 'Load from database: %.3fms' % (
% (timeit(lambda: Company.objects[0], 100) * 10 ** 3) timeit(lambda: Company.objects[0], 100) * 10**3
)
) )
def create_and_delete_company(): def create_and_delete_company():
@ -131,12 +136,13 @@ def test_big_doc():
c.delete() c.delete()
print( print(
"Init + save to database + delete: %.3fms" 'Init + save to database + delete: %.3fms' % (
% (timeit(create_and_delete_company, 10) * 10 ** 3) timeit(create_and_delete_company, 10) * 10**3
)
) )
if __name__ == "__main__": if __name__ == '__main__':
test_basic() test_basic()
print("-" * 100) print('-' * 100)
test_big_doc() test_big_doc()

View File

@ -4,14 +4,12 @@ import timeit
def main(): def main():
setup = """ setup = """
from pymongo import MongoClient from pymongo import MongoClient
connection = MongoClient() connection = MongoClient()
connection.drop_database('mongoengine_benchmark_test') connection.drop_database('mongoengine_benchmark_test')
""" """
stmt = """ stmt = """
from pymongo import MongoClient from pymongo import MongoClient
connection = MongoClient() connection = MongoClient()
db = connection.mongoengine_benchmark_test db = connection.mongoengine_benchmark_test
@ -28,10 +26,10 @@ myNoddys = noddy.find()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print("PyMongo: Creating 10000 dictionaries.") print('PyMongo: Creating 10000 dictionaries.')
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
stmt = """ stmt = """
from pymongo import MongoClient, WriteConcern from pymongo import MongoClient, WriteConcern
@ -51,14 +49,13 @@ myNoddys = noddy.find()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print('PyMongo: Creating 10000 dictionaries (write_concern={"w": 0}).') print('PyMongo: Creating 10000 dictionaries (write_concern={"w": 0}).')
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
setup = """ setup = """
from pymongo import MongoClient from pymongo import MongoClient
connection = MongoClient() connection = MongoClient()
connection.drop_database('mongoengine_benchmark_test') connection.drop_database('mongoengine_benchmark_test')
connection.close() connection.close()
@ -81,10 +78,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print("MongoEngine: Creating 10000 dictionaries.") print('MongoEngine: Creating 10000 dictionaries.')
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
stmt = """ stmt = """
for i in range(10000): for i in range(10000):
@ -99,10 +96,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print("MongoEngine: Creating 10000 dictionaries (using a single field assignment).") print('MongoEngine: Creating 10000 dictionaries (using a single field assignment).')
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
stmt = """ stmt = """
for i in range(10000): for i in range(10000):
@ -115,10 +112,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print('MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}).') print('MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}).')
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
stmt = """ stmt = """
for i in range(10000): for i in range(10000):
@ -131,12 +128,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print( print('MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}, validate=False).')
'MongoEngine: Creating 10000 dictionaries (write_concern={"w": 0}, validate=False).'
)
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
stmt = """ stmt = """
for i in range(10000): for i in range(10000):
@ -149,12 +144,10 @@ myNoddys = Noddy.objects()
[n for n in myNoddys] # iterate [n for n in myNoddys] # iterate
""" """
print("-" * 100) print('-' * 100)
print( print('MongoEngine: Creating 10000 dictionaries (force_insert=True, write_concern={"w": 0}, validate=False).')
'MongoEngine: Creating 10000 dictionaries (force_insert=True, write_concern={"w": 0}, validate=False).'
)
t = timeit.Timer(stmt=stmt, setup=setup) t = timeit.Timer(stmt=stmt, setup=setup)
print(f"{t.timeit(1)}s") print('{}s'.format(t.timeit(1)))
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -33,14 +33,8 @@ clean:
html: html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo @echo
@echo "Build finished. Check $(BUILDDIR)/html/index.html"
html-readthedocs:
$(SPHINXBUILD) -T -E -b readthedocs $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html." @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml: dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo @echo

View File

@ -75,7 +75,6 @@ Fields
.. autoclass:: mongoengine.fields.StringField .. autoclass:: mongoengine.fields.StringField
.. autoclass:: mongoengine.fields.URLField .. autoclass:: mongoengine.fields.URLField
.. autoclass:: mongoengine.fields.EmailField .. autoclass:: mongoengine.fields.EmailField
.. autoclass:: mongoengine.fields.EnumField
.. autoclass:: mongoengine.fields.IntField .. autoclass:: mongoengine.fields.IntField
.. autoclass:: mongoengine.fields.LongField .. autoclass:: mongoengine.fields.LongField
.. autoclass:: mongoengine.fields.FloatField .. autoclass:: mongoengine.fields.FloatField

View File

@ -1,5 +1,4 @@
========= =========
Changelog Changelog
========= =========
@ -7,300 +6,213 @@ Changelog
Development Development
=========== ===========
- (Fill this out as you fix issues and develop your features). - (Fill this out as you fix issues and develop your features).
- EnumField improvements: now `choices` limits the values of an enum to allow
- Fix deepcopy of EmbeddedDocument #2202
- Fix error when using precision=0 with DecimalField #2535
- Add support for regex and whole word text search query #2568
Changes in 0.23.1
===========
- Bug fix: ignore LazyReferenceFields when clearing _changed_fields #2484
- Improve connection doc #2481
Changes in 0.23.0
=================
- Bugfix: manually setting SequenceField in DynamicDocument doesn't increment the counter #2471
- Add MongoDB 4.2 and 4.4 to CI
- Add support for allowDiskUse on querysets #2468
Changes in 0.22.1
=================
- Declare that Py3.5 is not supported in package metadata #2449
- Moved CI from Travis to Github-Actions
Changes in 0.22.0
=================
- Fix LazyReferenceField dereferencing in embedded documents #2426
- Fix regarding the recent use of Cursor.__spec in .count() that was interfering with mongomock #2425
- Drop support for Python 3.5 by introducing f-strings in the codebase
Changes in 0.21.0
=================
- Bug fix in DynamicDocument which is not parsing known fields in constructor like Document do #2412
- When using pymongo >= 3.7, make use of Collection.count_documents instead of Collection.count
and Cursor.count that got deprecated in pymongo >= 3.7.
This should have a negative impact on performance of count see Issue #2219
- Fix a bug that made the queryset drop the read_preference after clone().
- Remove Py3.5 from CI as it reached EOL and add Python 3.9
- Fix some issues related with db_field/field conflict in constructor #2414
- BREAKING CHANGE: Fix the behavior of Doc.objects.limit(0) which should return all documents (similar to mongodb) #2311
- Bug fix in ListField when updating the first item, it was saving the whole list, instead of
just replacing the first item (as usually done when updating 1 item of the list) #2392
- Add EnumField: ``mongoengine.fields.EnumField``
- Refactoring - Remove useless code related to Document.__only_fields and Queryset.only_fields
- Fix query transformation regarding special operators #2365
- Bug Fix: Document.save() fails when shard_key is not _id #2154
Changes in 0.20.0
=================
- ATTENTION: Drop support for Python2
- Add Mongo 4.0 to Travis
- Fix error when setting a string as a ComplexDateTimeField #2253
- Bump development Status classifier to Production/Stable #2232
- Improve Queryset.get to avoid confusing MultipleObjectsReturned message in case multiple match are found #630
- Fixed a bug causing inaccurate query results, while combining ``__raw__`` and regular filters for the same field #2264
- Add support for the `elemMatch` projection operator in .fields() (e.g BlogPost.objects.fields(elemMatch__comments="test")) #2267
- DictField validate failed without default connection (bug introduced in 0.19.0) #2239
- Remove methods that were deprecated years ago:
- name parameter in Field constructor e.g `StringField(name="...")`, was replaced by db_field
- Queryset.slave_okay() was deprecated since pymongo3
- dropDups was dropped with MongoDB3
- ``Queryset._ensure_indexes`` and ``Queryset.ensure_indexes``, the right method to use is ``Document.ensure_indexes``
- Added pre-commit for development/CI #2212
- Renamed requirements-lint.txt to requirements-dev.txt #2212
- Support for setting ReadConcern #2255
Changes in 0.19.1
=================
- Tests require Pillow < 7.0.0 as it dropped Python2 support
- DEPRECATION: The interface of ``QuerySet.aggregate`` method was changed, it no longer takes an unpacked list of
pipeline steps (*pipeline) but simply takes the pipeline list just like ``pymongo.Collection.aggregate`` does. #2079
Changes in 0.19.0
=================
- BREAKING CHANGE: ``class_check`` and ``read_preference`` keyword arguments are no longer available when filtering a ``QuerySet``. #2112
- Instead of ``Doc.objects(foo=bar, read_preference=...)`` use ``Doc.objects(foo=bar).read_preference(...)``.
- Instead of ``Doc.objects(foo=bar, class_check=False)`` use ``Doc.objects(foo=bar).clear_cls_query(...)``.
- This change also renames the private ``QuerySet._initial_query`` attribute to ``_cls_query``.
- BREAKING CHANGE: Removed the deprecated ``format`` param from ``QuerySet.explain``. #2113
- BREAKING CHANGE: Renamed ``MongoEngineConnectionError`` to ``ConnectionFailure``. #2111
- If you catch/use ``MongoEngineConnectionError`` in your code, you'll have to rename it.
- BREAKING CHANGE: Positional arguments when instantiating a document are no longer supported. #2103
- From now on keyword arguments (e.g. ``Doc(field_name=value)``) are required.
- BREAKING CHANGE: A ``LazyReferenceField`` is now stored in the ``_data`` field of its parent as a ``DBRef``, ``Document``, or ``EmbeddedDocument`` (``ObjectId`` is no longer allowed). #2182
- DEPRECATION: ``Q.empty`` & ``QNode.empty`` are marked as deprecated and will be removed in a next version of MongoEngine. #2210
- Added ability to check if Q or QNode are empty by parsing them to bool.
- Instead of ``Q(name="John").empty`` use ``not Q(name="John")``.
- Fix updating/modifying/deleting/reloading a document that's sharded by a field with ``db_field`` specified. #2125
- Only set no_cursor_timeout when requested (fixes an incompatibility with MongoDB 4.2) #2148
- ``ListField`` now accepts an optional ``max_length`` parameter. #2110
- Improve error message related to InvalidDocumentError #2180
- Added BulkWriteError to replace NotUniqueError which was misleading in bulk write insert #2152
- Added ability to compare Q and Q operations #2204
- Added ability to use a db alias on query_counter #2194
- Added ability to specify collations for querysets with ``Doc.objects.collation`` #2024
- Fix updates of a list field by negative index #2094
- Switch from nosetest to pytest as test runner #2114
- The codebase is now formatted using ``black``. #2109
- Documentation improvements:
- Documented how `pymongo.monitoring` can be used to log all queries issued by MongoEngine to the driver.
Changes in 0.18.2 Changes in 0.18.2
================= =================
- Replace deprecated PyMongo v2.x methods with their v3.x equivalents in the ``SequenceField``. #2097 - Replace some of the deprecated PyMongo v2.x methods with their v3.x equivalents #2097
- Various code clarity and documentation improvements. - Various code clarity and documentation improvements
Changes in 0.18.1 Changes in 0.18.1
================= =================
- Fix a bug introduced in 0.18.0 which was causing ``Document.save`` to update all the fields instead of updating only the modified fields. This bug only occurred when using a custom PK. #2082 - Fix a bug introduced in 0.18.0 which was causing `.save()` to update all the fields
- Add Python 3.7 to Travis CI. #2058 instead of updating only the modified fields. This bug only occurs when using custom pk #2082
- Add Python 3.7 in travis #2058
Changes in 0.18.0 Changes in 0.18.0
================= =================
- Drop support for EOL'd MongoDB v2.6, v3.0, and v3.2. - Drop support for EOL'd MongoDB v2.6, v3.0, and v3.2.
- MongoEngine now requires PyMongo >= v3.4. Travis CI now tests against MongoDB v3.4 v3.6 and PyMongo v3.4 v3.6. #2017 #2066 - MongoEngine now requires PyMongo >= v3.4. Travis CI now tests against MongoDB v3.4 v3.6 and PyMongo v3.4 v3.6 (#2017 #2066).
- Improve performance by avoiding a call to ``to_mongo`` in ``Document.save``. #2049 - Improve performance by avoiding a call to `to_mongo` in `Document.save()` #2049
- Connection/disconnection improvements: - Connection/disconnection improvements:
- Expose ``mongoengine.connection.disconnect`` and ``mongoengine.connection.disconnect_all``. - Expose `mongoengine.connection.disconnect` and `mongoengine.connection.disconnect_all`
- Fix disconnecting. #566 #1599 #605 #607 #1213 #565 - Fix disconnecting #566 #1599 #605 #607 #1213 #565
- Improve documentation of ``connect``/``disconnect``. - Improve documentation of `connect`/`disconnect`
- Fix issue when using multiple connections to the same mongo with different credentials. #2047 - Fix issue when using multiple connections to the same mongo with different credentials #2047
- ``connect`` fails immediately when db name contains invalid characters. #2031 #1718 - `connect` fails immediately when db name contains invalid characters #2031 #1718
- Fix the default write concern of ``Document.save`` that was overwriting the connection write concern. #568 - Fix the default write concern of `Document.save` that was overwriting the connection write concern #568
- Fix querying on ``List(EmbeddedDocument)`` subclasses fields. #1961 #1492 - Fix querying on `List(EmbeddedDocument)` subclasses fields #1961 #1492
- Fix querying on ``(Generic)EmbeddedDocument`` subclasses fields. #475 - Fix querying on `(Generic)EmbeddedDocument` subclasses fields #475
- Fix ``QuerySet.aggregate`` so that it takes limit and skip value into account. #2029 - Fix `QuerySet.aggregate` so that it takes limit and skip value into account #2029
- Generate unique indices for ``SortedListField`` and ``EmbeddedDocumentListFields``. #2020 - Generate unique indices for `SortedListField` and `EmbeddedDocumentListFields` #2020
- BREAKING CHANGE: Changed the behavior of a custom field validator (i.e ``validation`` parameter of a ``Field``). It is now expected to raise a ``ValidationError`` instead of returning ``True``/``False``. #2050 - BREAKING CHANGE: Changed the behavior of a custom field validator (i.e `validation` parameter of a `Field`). It is now expected to raise a `ValidationError` instead of returning True/False #2050
- BREAKING CHANGES (associated with connection/disconnection fixes): - BREAKING CHANGES (associated with connect/disconnect fixes):
- Calling ``connect`` 2 times with the same alias and different parameter will raise an error (should call ``disconnect`` first). - Calling `connect` 2 times with the same alias and different parameter will raise an error (should call `disconnect` first).
- ``disconnect`` now clears ``mongoengine.connection._connection_settings``. - `disconnect` now clears `mongoengine.connection._connection_settings`.
- ``disconnect`` now clears the cached attribute ``Document._collection``. - `disconnect` now clears the cached attribute `Document._collection`.
- BREAKING CHANGE: ``EmbeddedDocument.save`` & ``.reload`` no longer exist. #1552 - BREAKING CHANGE: `EmbeddedDocument.save` & `.reload` is no longier exist #1552
Changes in 0.17.0 Changes in 0.17.0
================= =================
- POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (``_cls``, ``_id``) when using ``QuerySet.as_pymongo``. #1976 - Fix .only() working improperly after using .count() of the same instance of QuerySet
- Document a BREAKING CHANGE introduced in 0.15.3 and not reported at that time. #1995 - Fix batch_size that was not copied when cloning a queryset object #2011
- DEPRECATION: ``EmbeddedDocument.save`` & ``.reload`` are marked as deprecated and will be removed in a next version of MongoEngine. #1552 - POTENTIAL BREAKING CHANGE: All result fields are now passed, including internal fields (_cls, _id) when using `QuerySet.as_pymongo` #1976
- Fix ``QuerySet.only`` working improperly after using ``QuerySet.count`` of the same instance of a ``QuerySet``. - Document a BREAKING CHANGE introduced in 0.15.3 and not reported at that time (#1995)
- Fix ``batch_size`` that was not copied when cloning a ``QuerySet`` object. #2011 - Fix InvalidStringData error when using modify on a BinaryField #1127
- Fix ``InvalidStringData`` error when using ``modify`` on a ``BinaryField``. #1127 - DEPRECATION: `EmbeddedDocument.save` & `.reload` are marked as deprecated and will be removed in a next version of mongoengine #1552
- Fix test suite and CI to support MongoDB v3.4. #1445 - Fix test suite and CI to support MongoDB 3.4 #1445
- Fix reference fields querying the database on each access if value contains orphan DBRefs. - Fix reference fields querying the database on each access if value contains orphan DBRefs
=================
Changes in 0.16.3 Changes in 0.16.3
================= =================
- Fix ``$push`` with the ``$position`` operator not working with lists in embedded documents. #1965 - Fix $push with $position operator not working with lists in embedded document #1965
=================
Changes in 0.16.2 Changes in 0.16.2
================= =================
- Fix ``Document.save`` that fails when called with ``write_concern=None`` (regression of 0.16.1). #1958 - Fix .save() that fails when called with write_concern=None (regression of 0.16.1) #1958
=================
Changes in 0.16.1 Changes in 0.16.1
================= =================
- Fix ``_cls`` that is not set properly in the ``Document`` constructor (regression). #1950 - Fix `_cls` that is not set properly in Document constructor (regression) #1950
- Fix a bug in the ``_delta`` method - update of a ``ListField`` depends on an unrelated dynamic field update. #1733 - Fix bug in _delta method - Update of a ListField depends on an unrelated dynamic field update #1733
- Remove PyMongo's deprecated ``Collection.save`` method and use ``Collection.insert_one`` instead. #1899 - Remove deprecated `save()` method and used `insert_one()` #1899
=================
Changes in 0.16.0 Changes in 0.16.0
================= =================
- Various improvements to the doc
- Improvement to code quality
- POTENTIAL BREAKING CHANGES: - POTENTIAL BREAKING CHANGES:
- ``EmbeddedDocumentField`` will no longer accept references to Document classes in its constructor. #1661 - EmbeddedDocumentField will no longer accept references to Document classes in its constructor #1661
- Get rid of the ``basecls`` parameter from the ``DictField`` constructor (dead code). #1876 - Get rid of the `basecls` parameter from the DictField constructor (dead code) #1876
- Default value of the ``ComplexDateTime`` field is now ``None`` (and no longer the current datetime). #1368 - default value of ComplexDateTime is now None (and no longer the current datetime) #1368
- Fix an unhashable ``TypeError`` when referencing a ``Document`` with a compound key in an ``EmbeddedDocument``. #1685 - Fix unhashable TypeError when referencing a Document with a compound key in an EmbeddedDocument #1685
- Fix a bug where an ``EmbeddedDocument`` with the same id as its parent would not be tracked for changes. #1768 - Fix bug where an EmbeddedDocument with the same id as its parent would not be tracked for changes #1768
- Fix the fact that a bulk ``QuerySet.insert`` was not setting primary keys of inserted document instances. #1919 - Fix the fact that bulk `insert()` was not setting primary keys of inserted documents instances #1919
- Fix a bug when referencing an abstract class in a ``ReferenceField``. #1920 - Fix bug when referencing the abstract class in a ReferenceField #1920
- Allow modifications to the document made in ``pre_save_post_validation`` to be taken into account. #1202 - Allow modification to the document made in pre_save_post_validation to be taken into account #1202
- Replace MongoDB v2.4 tests in Travis CI with MongoDB v3.2. #1903 - Replaced MongoDB 2.4 tests in CI by MongoDB 3.2 #1903
- Fix side effects of using ``QuerySet.no_dereference`` on other documents. #1677 - Fix side effects of using queryset.`no_dereference` on other documents #1677
- Fix ``TypeError`` when using lazy Django translation objects as translated choices. #1879 - Fix TypeError when using lazy django translation objects as translated choices #1879
- Improve Python 2-3 codebase compatibility. #1889 - Improve 2-3 codebase compatibility #1889
- Fix support for changing the default value of the ``ComplexDateTime`` field. #1368 - Fix the support for changing the default value of ComplexDateTime #1368
- Improve error message in case an ``EmbeddedDocumentListField`` receives an ``EmbeddedDocument`` instance instead of a list. #1877 - Improves error message in case an EmbeddedDocumentListField receives an EmbeddedDocument instance
- Fix the ``inc`` and ``dec`` operators for the ``DecimalField``. #1517 #1320 instead of a list #1877
- Ignore ``killcursors`` queries in ``query_counter`` context manager. #1869 - Fix the Decimal operator inc/dec #1517 #1320
- Fix the fact that ``query_counter`` was modifying the initial profiling level in case it was != 0. #1870 - Ignore killcursors queries in `query_counter` context manager #1869
- Repair the ``no_sub_classes`` context manager + fix the fact that it was swallowing exceptions. #1865 - Fix the fact that `query_counter` was modifying the initial profiling_level in case it was != 0 #1870
- Fix index creation error that was swallowed by ``hasattr`` under Python 2. #1688 - Repaired the `no_sub_classes` context manager + fix the fact that it was swallowing exceptions #1865
- ``QuerySet.limit`` function behaviour: Passing 0 as parameter will return all the documents in the cursor. #1611 - Fix index creation error that was swallowed by hasattr under python2 #1688
- Bulk insert updates the IDs of the input documents instances. #1919 - QuerySet limit function behaviour: Passing 0 as parameter will return all the documents in the cursor #1611
- Fix a harmless bug related to ``GenericReferenceField`` where modifications in the generic-referenced document were tracked in the parent. #1934 - bulk insert updates the ids of the input documents instances #1919
- Improve validation of the ``BinaryField``. #273 - Fix an harmless bug related to GenericReferenceField where modifications in the generic-referenced document
- Implement lazy regex compiling in Field classes to improve ``import mongoengine`` performance. #1806 were tracked in the parent #1934
- Update ``GridFSProxy.__str__`` so that it would always print both the filename and grid_id. #710 - Improve validator of BinaryField #273
- Add ``__repr__`` to ``Q`` and ``QCombination`` classes. #1843 - Implemented lazy regex compiling in Field classes to improve 'import mongoengine' performance #1806
- Fix bug in the ``BaseList.__iter__`` operator (was occuring when modifying a BaseList while iterating over it). #1676 - Updated GridFSProxy.__str__ so that it would always print both the filename and grid_id #710
- Add a ``DateField``. #513 - Add __repr__ to Q and QCombination #1843
- Various improvements to the documentation. - fix bug in BaseList.__iter__ operator (was occuring when modifying a BaseList while iterating over it) #1676
- Various code quality improvements. - Added field `DateField`#513
Changes in 0.15.3 Changes in 0.15.3
================= =================
- ``Queryset.update/update_one`` methods now return an ``UpdateResult`` when ``full_result=True`` is provided and no longer a dict. #1491 - BREAKING CHANGES: `Queryset.update/update_one` methods now returns an UpdateResult when `full_result=True` is provided and no longer a dict (relates to #1491)
- Improve ``LazyReferenceField`` and ``GenericLazyReferenceField`` with nested fields. #1704 - Subfield resolve error in generic_emdedded_document query #1651 #1652
- Fix the subfield resolve error in ``generic_emdedded_document`` query. #1651 #1652 - use each modifier only with $position #1673 #1675
- Use each modifier only with ``$position``. #1673 #1675 - Improve LazyReferenceField and GenericLazyReferenceField with nested fields #1704
- Fix validation errors in the ``GenericEmbeddedDocumentField``. #1067 - Fix validation error instance in GenericEmbeddedDocumentField #1067
- Update cached fields when a ``fields`` argument is given. #1712 - Update cached fields when fields argument is given #1712
- Add a ``db`` parameter to ``register_connection`` for compatibility with ``connect``. - Add a db parameter to register_connection for compatibility with connect
- Use PyMongo v3.x's ``insert_one`` and ``insert_many`` in ``Document.insert``. #1491 - Use insert_one, insert_many in Document.insert #1491
- Use PyMongo v3.x's ``update_one`` and ``update_many`` in ``Document.update`` and ``QuerySet.update``. #1491 - Use new update_one, update_many on document/queryset update #1491
- Fix how ``reload(fields)`` affects changed fields. #1371 - Use insert_one, insert_many in Document.insert #1491
- Fix a bug where the read-only access to the database fails when trying to create indexes. #1338 - Fix reload(fields) affect changed fields #1371
- Fix Read-only access to database fails when trying to create indexes #1338
Changes in 0.15.0 Changes in 0.15.0
================= =================
- Add ``LazyReferenceField`` and ``GenericLazyReferenceField``. #1230 - Add LazyReferenceField and GenericLazyReferenceField to address #1230
Changes in 0.14.1 Changes in 0.14.1
================= =================
- Remove ``SemiStrictDict`` and start using a regular dict for ``BaseDocument._data``. #1630 - Removed SemiStrictDict and started using a regular dict for `BaseDocument._data` #1630
- Add support for the ``$position`` param in the ``$push`` operator. #1566 - Added support for the `$position` param in the `$push` operator #1566
- Fix ``DateTimeField`` interpreting an empty string as today. #1533 - Fixed `DateTimeField` interpreting an empty string as today #1533
- Add a missing ``__ne__`` method to the ``GridFSProxy`` class. #1632 - Added a missing `__ne__` method to the `GridFSProxy` class #1632
- Fix ``BaseQuerySet._fields_to_db_fields``. #1553 - Fixed `BaseQuerySet._fields_to_db_fields` #1553
Changes in 0.14.0 Changes in 0.14.0
================= =================
- BREAKING CHANGE: Remove the ``coerce_types`` param from ``QuerySet.as_pymongo``. #1549 - BREAKING CHANGE: Removed the `coerce_types` param from `QuerySet.as_pymongo` #1549
- POTENTIAL BREAKING CHANGE: Make ``EmbeddedDocument`` not hashable by default. #1528 - POTENTIAL BREAKING CHANGE: Made EmbeddedDocument not hashable by default #1528
- Improve code quality. #1531, #1540, #1541, #1547 - Improved code quality #1531, #1540, #1541, #1547
Changes in 0.13.0 Changes in 0.13.0
================= =================
- POTENTIAL BREAKING CHANGE: Added Unicode support to the ``EmailField``, see docs/upgrade.rst for details. - POTENTIAL BREAKING CHANGE: Added Unicode support to the `EmailField`, see
docs/upgrade.rst for details.
Changes in 0.12.0 Changes in 0.12.0
================= =================
- POTENTIAL BREAKING CHANGE: Fix ``limit``/``skip``/``hint``/``batch_size`` chaining. #1476 - POTENTIAL BREAKING CHANGE: Fixed limit/skip/hint/batch_size chaining #1476
- POTENTIAL BREAKING CHANGE: Change a public ``QuerySet.clone_into`` method to a private ``QuerySet._clone_into``. #1476 - POTENTIAL BREAKING CHANGE: Changed a public `QuerySet.clone_into` method to a private `QuerySet._clone_into` #1476
- Fix the way ``Document.objects.create`` works with duplicate IDs. #1485 - Fixed the way `Document.objects.create` works with duplicate IDs #1485
- Fix connecting to a replica set with PyMongo 2.x. #1436 - Fixed connecting to a replica set with PyMongo 2.x #1436
- Fix using sets in field choices. #1481 - Fixed using sets in field choices #1481
- Fix deleting items from a ``ListField``. #1318 - Fixed deleting items from a `ListField` #1318
- Fix an obscure error message when filtering by ``field__in=non_iterable``. #1237 - Fixed an obscure error message when filtering by `field__in=non_iterable`. #1237
- Fix behavior of a ``dec`` update operator. #1450 - Fixed behavior of a `dec` update operator #1450
- Add a ``rename`` update operator. #1454 - Added a `rename` update operator #1454
- Add validation for the ``db_field`` parameter. #1448 - Added validation for the `db_field` parameter #1448
- Fix the error message displayed when querying an ``EmbeddedDocumentField`` by an invalid value. #1440 - Fixed the error message displayed when querying an `EmbeddedDocumentField` by an invalid value #1440
- Fix the error message displayed when validating Unicode URLs. #1486 - Fixed the error message displayed when validating unicode URLs #1486
- Raise an error when trying to save an abstract document. #1449 - Raise an error when trying to save an abstract document #1449
Changes in 0.11.0 Changes in 0.11.0
================= =================
- BREAKING CHANGE: Rename ``ConnectionError`` to ``MongoEngineConnectionError`` since the former is a built-in exception name in Python v3.x. #1428 - BREAKING CHANGE: Renamed `ConnectionError` to `MongoEngineConnectionError` since the former is a built-in exception name in Python v3.x. #1428
- BREAKING CHANGE: Drop Python v2.6 support. #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: `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 - BREAKING CHANGE: Accessing a broken reference will raise a `DoesNotExist` error. In the past it used to return `None`. #1334
- Fix absent rounding for the ``DecimalField`` when ``force_string`` is set. #1103 - Fixed absent rounding for DecimalField when `force_string` is set. #1103
Changes in 0.10.8 Changes in 0.10.8
================= =================
- Add support for ``QuerySet.batch_size``. (#1426) - Added support for QuerySet.batch_size (#1426)
- Fix a query set iteration within an iteration. #1427 - Fixed query set iteration within iteration #1427
- Fix an issue where specifying a MongoDB URI host would override more information than it should. #1421 - Fixed an issue where specifying a MongoDB URI host would override more information than it should #1421
- Add an ability to filter the ``GenericReferenceField`` by an ``ObjectId`` and a ``DBRef``. #1425 - Added ability to filter the generic reference field by ObjectId and DBRef #1425
- Fix cascading deletes for models with a custom primary key field. #1247 - Fixed delete cascade for models with a custom primary key field #1247
- Add ability to specify an authentication mechanism (e.g. X.509). #1333 - Added ability to specify an authentication mechanism (e.g. X.509) #1333
- Add support for falsy primary keys (e.g. ``doc.pk = 0``). #1354 - Added support for falsey primary keys (e.g. doc.pk = 0) #1354
- Fix ``QuerySet.sum/average`` for fields w/ an explicit ``db_field``. #1417 - Fixed QuerySet#sum/average for fields w/ explicit db_field #1417
- Fix filtering by ``embedded_doc=None``. #1422 - Fixed filtering by embedded_doc=None #1422
- Add support for ``Cursor.comment``. #1420 - Added support for cursor.comment #1420
- Fix ``doc.get_<field>_display`` methods. #1419 - Fixed doc.get_<field>_display #1419
- Fix the ``__repr__`` method of the ``StrictDict`` #1424 - Fixed __repr__ method of the StrictDict #1424
- Add a deprecation warning for Python v2.6. - Added a deprecation warning for Python 2.6
Changes in 0.10.7 Changes in 0.10.7
================= =================
- Drop Python 3.2 support #1390 - Dropped Python 3.2 support #1390
- Fix a bug where a dynamic doc has an index inside a dict field. #1278 - Fixed the bug where dynamic doc has index inside a dict field #1278
- Fix: ``ListField`` minus index assignment does not work. #1128 - Fixed: ListField minus index assignment does not work #1128
- Fix cascade delete mixing among collections. #1224 - Fixed cascade delete mixing among collections #1224
- Add ``signal_kwargs`` argument to ``Document.save``, ``Document.delete`` and ``BaseQuerySet.insert`` to be passed to signals calls. #1206 - Add `signal_kwargs` argument to `Document.save`, `Document.delete` and `BaseQuerySet.insert` to be passed to signals calls #1206
- Raise ``OperationError`` when trying to do a ``drop_collection`` on document with no collection set. - Raise `OperationError` when trying to do a `drop_collection` on document with no collection set.
- Fix a bug where a count on ``ListField`` of ``EmbeddedDocumentField`` fails. #1187 - count on ListField of EmbeddedDocumentField fails. #1187
- Fix ``LongField`` values stored as int32 in Python 3. #1253 - Fixed long fields stored as int32 in Python 3. #1253
- ``MapField`` now handles unicode keys correctly. #1267 - MapField now handles unicodes keys correctly. #1267
- ``ListField`` now handles negative indicies correctly. #1270 - ListField now handles negative indicies correctly. #1270
- Fix an ``AttributeError`` when initializing an ``EmbeddedDocument`` with positional args. #681 - Fixed AttributeError when initializing EmbeddedDocument with positional args. #681
- Fix a ``no_cursor_timeout`` error with PyMongo v3.x. #1304 - Fixed no_cursor_timeout error with pymongo 3.0+ #1304
- Replace map-reduce based ``QuerySet.sum/average`` with aggregation-based implementations. #1336 - Replaced map-reduce based QuerySet.sum/average with aggregation-based implementations #1336
- Fix support for ``__`` to escape field names that match operators' names in ``update``. #1351 - Fixed support for `__` to escape field names that match operators names in `update` #1351
- Fix ``BaseDocument._mark_as_changed``. #1369 - Fixed BaseDocument#_mark_as_changed #1369
- Add support for pickling ``QuerySet`` instances. #1397 - Added support for pickling QuerySet instances. #1397
- Fix connecting to a list of hosts. #1389 - Fixed connecting to a list of hosts #1389
- Fix a bug where accessing broken references wouldn't raise a ``DoesNotExist`` error. #1334 - Fixed a bug where accessing broken references wouldn't raise a DoesNotExist error #1334
- Fix not being able to specify ``use_db_field=False`` on ``ListField(EmbeddedDocumentField)`` instances. #1218 - Fixed not being able to specify use_db_field=False on ListField(EmbeddedDocumentField) instances #1218
- Improvements to the dictionary field's docs. #1383 - Improvements to the dictionary fields docs #1383
Changes in 0.10.6 Changes in 0.10.6
================= =================
- Add support for mocking MongoEngine based on mongomock. #1151 - Add support for mocking MongoEngine based on mongomock. #1151
- Fix not being able to run tests on Windows. #1153 - Fixed not being able to run tests on Windows. #1153
- Allow creation of sparse compound indexes. #1114 - Allow creation of sparse compound indexes. #1114
- count on ListField of EmbeddedDocumentField fails. #1187
Changes in 0.10.5 Changes in 0.10.5
================= =================
@ -308,12 +220,12 @@ Changes in 0.10.5
Changes in 0.10.4 Changes in 0.10.4
================= =================
- ``SaveConditionError`` is now importable from the top level package. #1165 - SaveConditionError is now importable from the top level package. #1165
- Add a ``QuerySet.upsert_one`` method. #1157 - upsert_one method added. #1157
Changes in 0.10.3 Changes in 0.10.3
================= =================
- Fix ``read_preference`` (it had chaining issues with PyMongo v2.x and it didn't work at all with PyMongo v3.x). #1042 - 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 Changes in 0.10.2
================= =================
@ -323,16 +235,16 @@ Changes in 0.10.2
Changes in 0.10.1 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 a ``SaveConditionError`` exception. #1070 - Document save's save_condition error raises `SaveConditionError` exception #1070
- Fix ``Document.reload`` for the ``DynamicDocument``. #1050 - Fix Document.reload for DynamicDocument. #1050
- ``StrictDict`` & ``SemiStrictDict`` are shadowed at init time. #1105 - StrictDict & SemiStrictDict are shadowed at init time. #1105
- Fix ``ListField`` negative index assignment not working. #1119 - Fix ListField minus index assignment does not work. #1119
- Remove code that marks a field as changed when the field has a default value but does not exist in the database. #1126 - 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. #1079 - Remove test dependencies (nose and rednose) from install dependencies list. #1079
- Recursively build a query when using the ``elemMatch`` operator. #1130 - Recursively build query when using elemMatch operator. #1130
- Fix instance back references for lists of embedded documents. #1131 - Fix instance back references for lists of embedded documents. #1131
Changes in 0.10.0 Changes in 0.10.0
@ -343,7 +255,7 @@ Changes in 0.10.0
- Removed get_or_create() deprecated since 0.8.0. #300 - Removed get_or_create() deprecated since 0.8.0. #300
- Improve Document._created status when switch collection and db #1020 - Improve Document._created status when switch collection and db #1020
- Queryset update doesn't go through field validation #453 - Queryset update doesn't go through field validation #453
- Added support for specifying authentication source as option ``authSource`` in URI. #967 - Added support for specifying authentication source as option `authSource` in URI. #967
- Fixed mark_as_changed to handle higher/lower level fields changed. #927 - Fixed mark_as_changed to handle higher/lower level fields changed. #927
- ListField of embedded docs doesn't set the _instance attribute when iterating over it #914 - ListField of embedded docs doesn't set the _instance attribute when iterating over it #914
- Support += and *= for ListField #595 - Support += and *= for ListField #595
@ -359,7 +271,7 @@ Changes in 0.10.0
- Fixes some internal _id handling issue. #961 - Fixes some internal _id handling issue. #961
- Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652 - Updated URL and Email Field regex validators, added schemes argument to URLField validation. #652
- Capped collection multiple of 256. #1011 - Capped collection multiple of 256. #1011
- Added ``BaseQuerySet.aggregate_sum`` and ``BaseQuerySet.aggregate_average`` methods. - Added `BaseQuerySet.aggregate_sum` and `BaseQuerySet.aggregate_average` methods.
- Fix for delete with write_concern {'w': 0}. #1008 - Fix for delete with write_concern {'w': 0}. #1008
- Allow dynamic lookup for more than two parts. #882 - Allow dynamic lookup for more than two parts. #882
- Added support for min_distance on geo queries. #831 - Added support for min_distance on geo queries. #831
@ -368,10 +280,10 @@ Changes in 0.10.0
Changes in 0.9.0 Changes in 0.9.0
================ ================
- Update FileField when creating a new file #714 - Update FileField when creating a new file #714
- Added ``EmbeddedDocumentListField`` for Lists of Embedded Documents. #826 - Added `EmbeddedDocumentListField` for Lists of Embedded Documents. #826
- ComplexDateTimeField should fall back to None when null=True #864 - ComplexDateTimeField should fall back to None when null=True #864
- Request Support for $min, $max Field update operators #863 - Request Support for $min, $max Field update operators #863
- ``BaseDict`` does not follow ``setdefault`` #866 - `BaseDict` does not follow `setdefault` #866
- Add support for $type operator # 766 - Add support for $type operator # 766
- Fix tests for pymongo 2.8+ #877 - Fix tests for pymongo 2.8+ #877
- No module named 'django.utils.importlib' (Django dev) #872 - No module named 'django.utils.importlib' (Django dev) #872
@ -392,13 +304,13 @@ Changes in 0.9.0
- Stop ensure_indexes running on a secondaries unless connection is through mongos #746 - Stop ensure_indexes running on a secondaries unless connection is through mongos #746
- Not overriding default values when loading a subset of fields #399 - Not overriding default values when loading a subset of fields #399
- Saving document doesn't create new fields in existing collection #620 - Saving document doesn't create new fields in existing collection #620
- Added ``Queryset.aggregate`` wrapper to aggregation framework #703 - Added `Queryset.aggregate` wrapper to aggregation framework #703
- Added support to show original model fields on to_json calls instead of db_field #697 - Added support to show original model fields on to_json calls instead of db_field #697
- Added Queryset.search_text to Text indexes searchs #700 - Added Queryset.search_text to Text indexes searchs #700
- Fixed tests for Django 1.7 #696 - Fixed tests for Django 1.7 #696
- Follow ReferenceFields in EmbeddedDocuments with select_related #690 - Follow ReferenceFields in EmbeddedDocuments with select_related #690
- Added preliminary support for text indexes #680 - Added preliminary support for text indexes #680
- Added ``elemMatch`` operator as well - ``match`` is too obscure #653 - Added `elemMatch` operator as well - `match` is too obscure #653
- Added support for progressive JPEG #486 #548 - Added support for progressive JPEG #486 #548
- Allow strings to be used in index creation #675 - Allow strings to be used in index creation #675
- Fixed EmbeddedDoc weakref proxy issue #592 - Fixed EmbeddedDoc weakref proxy issue #592
@ -434,11 +346,11 @@ Changes in 0.9.0
- Increase email field length to accommodate new TLDs #726 - Increase email field length to accommodate new TLDs #726
- index_cls is ignored when deciding to set _cls as index prefix #733 - index_cls is ignored when deciding to set _cls as index prefix #733
- Make 'db' argument to connection optional #737 - Make 'db' argument to connection optional #737
- Allow atomic update for the entire ``DictField`` #742 - Allow atomic update for the entire `DictField` #742
- Added MultiPointField, MultiLineField, MultiPolygonField - Added MultiPointField, MultiLineField, MultiPolygonField
- Fix multiple connections aliases being rewritten #748 - Fix multiple connections aliases being rewritten #748
- Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791 - Fixed a few instances where reverse_delete_rule was written as reverse_delete_rules. #791
- Make ``in_bulk()`` respect ``no_dereference()`` #775 - Make `in_bulk()` respect `no_dereference()` #775
- Handle None from model __str__; Fixes #753 #754 - Handle None from model __str__; Fixes #753 #754
- _get_changed_fields fix for embedded documents with id field. #925 - _get_changed_fields fix for embedded documents with id field. #925
@ -492,15 +404,18 @@ Changes in 0.8.4
Changes in 0.8.3 Changes in 0.8.3
================ ================
- Fixed EmbeddedDocuments with ``id`` also storing ``_id`` (#402) - Fixed EmbeddedDocuments with `id` also storing `_id` (#402)
- Added get_proxy_object helper to filefields (#391) - Added get_proxy_object helper to filefields (#391)
- Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365) - Added QuerySetNoCache and QuerySet.no_cache() for lower memory consumption (#365)
- Fixed sum and average mapreduce dot notation support (#375, #376, #393) - Fixed sum and average mapreduce dot notation support (#375, #376, #393)
- Fixed as_pymongo to return the id (#386) - Fixed as_pymongo to return the id (#386)
- Document.select_related() now respects ``db_alias`` (#377) - Document.select_related() now respects `db_alias` (#377)
- Reload uses shard_key if applicable (#384) - Reload uses shard_key if applicable (#384)
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396) - Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
- Fixed pickling dynamic documents ``_dynamic_fields`` (#387)
**Potential breaking change:** http://docs.mongoengine.org/en/latest/upgrade.html#to-0-8-3
- Fixed pickling dynamic documents `_dynamic_fields` (#387)
- Fixed ListField setslice and delslice dirty tracking (#390) - Fixed ListField setslice and delslice dirty tracking (#390)
- Added Django 1.5 PY3 support (#392) - Added Django 1.5 PY3 support (#392)
- Added match ($elemMatch) support for EmbeddedDocuments (#379) - Added match ($elemMatch) support for EmbeddedDocuments (#379)
@ -541,7 +456,7 @@ Changes in 0.8.0
================ ================
- Fixed querying ReferenceField custom_id (#317) - Fixed querying ReferenceField custom_id (#317)
- Fixed pickle issues with collections (#316) - Fixed pickle issues with collections (#316)
- Added ``get_next_value`` preview for SequenceFields (#319) - Added `get_next_value` preview for SequenceFields (#319)
- Added no_sub_classes context manager and queryset helper (#312) - Added no_sub_classes context manager and queryset helper (#312)
- Querysets now utilises a local cache - Querysets now utilises a local cache
- Changed __len__ behaviour in the queryset (#247, #311) - Changed __len__ behaviour in the queryset (#247, #311)
@ -570,7 +485,7 @@ Changes in 0.8.0
- Updated connection to use MongoClient (#262, #274) - Updated connection to use MongoClient (#262, #274)
- Fixed db_alias and inherited Documents (#143) - Fixed db_alias and inherited Documents (#143)
- Documentation update for document errors (#124) - Documentation update for document errors (#124)
- Deprecated ``get_or_create`` (#35) - Deprecated `get_or_create` (#35)
- Updated inheritable objects created by upsert now contain _cls (#118) - Updated inheritable objects created by upsert now contain _cls (#118)
- Added support for creating documents with embedded documents in a single operation (#6) - Added support for creating documents with embedded documents in a single operation (#6)
- Added to_json and from_json to Document (#1) - Added to_json and from_json to Document (#1)
@ -691,7 +606,7 @@ Changes in 0.7.0
- Fixed UnboundLocalError in composite index with pk field (#88) - Fixed UnboundLocalError in composite index with pk field (#88)
- Updated ReferenceField's to optionally store ObjectId strings - Updated ReferenceField's to optionally store ObjectId strings
this will become the default in 0.8 (#89) this will become the default in 0.8 (#89)
- Added FutureWarning - save will default to ``cascade=False`` in 0.8 - Added FutureWarning - save will default to `cascade=False` in 0.8
- Added example of indexing embedded document fields (#75) - Added example of indexing embedded document fields (#75)
- Fixed ImageField resizing when forcing size (#80) - Fixed ImageField resizing when forcing size (#80)
- Add flexibility for fields handling bad data (#78) - Add flexibility for fields handling bad data (#78)
@ -787,7 +702,7 @@ Changes in 0.6.8
================ ================
- Fixed FileField losing reference when no default set - Fixed FileField losing reference when no default set
- Removed possible race condition from FileField (grid_file) - Removed possible race condition from FileField (grid_file)
- Added assignment to save, can now do: ``b = MyDoc(**kwargs).save()`` - Added assignment to save, can now do: `b = MyDoc(**kwargs).save()`
- Added support for pull operations on nested EmbeddedDocuments - Added support for pull operations on nested EmbeddedDocuments
- Added support for choices with GenericReferenceFields - Added support for choices with GenericReferenceFields
- Added support for choices with GenericEmbeddedDocumentFields - Added support for choices with GenericEmbeddedDocumentFields
@ -802,7 +717,7 @@ Changes in 0.6.7
- Fixed indexing on '_id' or 'pk' or 'id' - Fixed indexing on '_id' or 'pk' or 'id'
- Invalid data from the DB now raises a InvalidDocumentError - Invalid data from the DB now raises a InvalidDocumentError
- Cleaned up the Validation Error - docs and code - Cleaned up the Validation Error - docs and code
- Added meta ``auto_create_index`` so you can disable index creation - Added meta `auto_create_index` so you can disable index creation
- Added write concern options to inserts - Added write concern options to inserts
- Fixed typo in meta for index options - Fixed typo in meta for index options
- Bug fix Read preference now passed correctly - Bug fix Read preference now passed correctly
@ -843,6 +758,7 @@ Changes in 0.6.1
Changes in 0.6 Changes in 0.6
============== ==============
- Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7 - Added FutureWarning to inherited classes not declaring 'allow_inheritance' as the default will change in 0.7
- Added support for covered indexes when inheritance is off - Added support for covered indexes when inheritance is off
- No longer always upsert on save for items with a '_id' - No longer always upsert on save for items with a '_id'
@ -1067,6 +983,7 @@ Changes in v0.1.3
querying takes place querying takes place
- A few minor bugfixes - A few minor bugfixes
Changes in v0.1.2 Changes in v0.1.2
================= =================
- Query values may be processed before before being used in queries - Query values may be processed before before being used in queries
@ -1075,6 +992,7 @@ Changes in v0.1.2
- Added ``BooleanField`` - Added ``BooleanField``
- Added ``Document.reload()`` method - Added ``Document.reload()`` method
Changes in v0.1.1 Changes in v0.1.1
================= =================
- Documents may now use capped collections - Documents may now use capped collections

View File

@ -1,19 +1,16 @@
from mongoengine import * from mongoengine import *
connect("tumblelog") connect('tumblelog')
class Comment(EmbeddedDocument): class Comment(EmbeddedDocument):
content = StringField() content = StringField()
name = StringField(max_length=120) name = StringField(max_length=120)
class User(Document): class User(Document):
email = StringField(required=True) email = StringField(required=True)
first_name = StringField(max_length=50) first_name = StringField(max_length=50)
last_name = StringField(max_length=50) last_name = StringField(max_length=50)
class Post(Document): class Post(Document):
title = StringField(max_length=120, required=True) title = StringField(max_length=120, required=True)
author = ReferenceField(User) author = ReferenceField(User)
@ -21,57 +18,54 @@ class Post(Document):
comments = ListField(EmbeddedDocumentField(Comment)) comments = ListField(EmbeddedDocumentField(Comment))
# bugfix # bugfix
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
class TextPost(Post): class TextPost(Post):
content = StringField() content = StringField()
class ImagePost(Post): class ImagePost(Post):
image_path = StringField() image_path = StringField()
class LinkPost(Post): class LinkPost(Post):
link_url = StringField() link_url = StringField()
Post.drop_collection() Post.drop_collection()
john = User(email="jdoe@example.com", first_name="John", last_name="Doe") john = User(email='jdoe@example.com', first_name='John', last_name='Doe')
john.save() john.save()
post1 = TextPost(title="Fun with MongoEngine", author=john) post1 = TextPost(title='Fun with MongoEngine', author=john)
post1.content = "Took a look at MongoEngine today, looks pretty cool." post1.content = 'Took a look at MongoEngine today, looks pretty cool.'
post1.tags = ["mongodb", "mongoengine"] post1.tags = ['mongodb', 'mongoengine']
post1.save() post1.save()
post2 = LinkPost(title="MongoEngine Documentation", author=john) post2 = LinkPost(title='MongoEngine Documentation', author=john)
post2.link_url = "http://tractiondigital.com/labs/mongoengine/docs" post2.link_url = 'http://tractiondigital.com/labs/mongoengine/docs'
post2.tags = ["mongoengine"] post2.tags = ['mongoengine']
post2.save() post2.save()
print("ALL POSTS") 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) print("=" * 20)
if isinstance(post, TextPost): if isinstance(post, TextPost):
print(post.content) print(post.content)
if isinstance(post, LinkPost): if isinstance(post, LinkPost):
print("Link:", post.link_url) print('Link:', post.link_url)
print() print()
print() print()
print("POSTS TAGGED 'MONGODB'") print('POSTS TAGGED \'MONGODB\'')
print() print()
for post in Post.objects(tags="mongodb"): for post in Post.objects(tags='mongodb'):
print(post.title) print(post.title)
print() print()
num_posts = Post.objects(tags="mongodb").count() num_posts = Post.objects(tags='mongodb').count()
print('Found %d posts with tag "mongodb"' % num_posts) print('Found %d posts with tag "mongodb"' % num_posts)

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# #
# MongoEngine documentation build configuration file, created by # MongoEngine documentation build configuration file, created by
# sphinx-quickstart on Sun Nov 22 18:14:13 2009. # sphinx-quickstart on Sun Nov 22 18:14:13 2009.
@ -10,8 +11,7 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import os import sys, os
import sys
import sphinx_rtd_theme import sphinx_rtd_theme
@ -20,29 +20,29 @@ import mongoengine
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath("..")) sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ----------------------------------------------------- # -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ["sphinx.ext.autodoc", "sphinx.ext.todo", "readthedocs_ext.readthedocs"] extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"] templates_path = ['_templates']
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = ".rst" source_suffix = '.rst'
# The encoding of source files. # The encoding of source files.
# source_encoding = 'utf-8' #source_encoding = 'utf-8'
# The master toctree document. # The master toctree document.
master_doc = "index" master_doc = 'index'
# General information about the project. # General information about the project.
project = "MongoEngine" project = u'MongoEngine'
copyright = "2009, MongoEngine Authors" # noqa: A001 copyright = u'2009, MongoEngine Authors'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
@ -55,66 +55,68 @@ release = mongoengine.get_version()
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
# language = None #language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
# today = '' #today = ''
# Else, today_fmt is used as the format for a strftime call. # Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y' #today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build. # List of documents that shouldn't be included in the build.
# unused_docs = [] #unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched # List of directories, relative to source directory, that shouldn't be searched
# for source files. # for source files.
exclude_trees = ["_build"] exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents. # The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None #default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text. # If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True #add_function_parentheses = True
# If true, the current module name will be prepended to all description # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
# add_module_names = True #add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
# show_authors = False #show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx" pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting. # A list of ignored prefixes for module index sorting.
# modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with # The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'. # Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = "sphinx_rtd_theme" html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
html_theme_options = {"canonical_url": "http://docs.mongoengine.org/en/latest/"} html_theme_options = {
'canonical_url': 'http://docs.mongoengine.org/en/latest/'
}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# The name for this set of Sphinx documents. If None, it defaults to # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
# html_title = None #html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None #html_short_title = None
# The name of an image file (relative to this directory) to place at the top # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # of the sidebar.
# html_logo = None #html_logo = None
# The name of an image file (within the static path) to use as favicon of the # The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
@ -124,11 +126,11 @@ html_favicon = "favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['_static'] #html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y' #html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
@ -136,68 +138,69 @@ html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # Custom sidebar templates, maps document names to template names.
html_sidebars = { html_sidebars = {
"index": ["globaltoc.html", "searchbox.html"], 'index': ['globaltoc.html', 'searchbox.html'],
"**": ["localtoc.html", "relations.html", "searchbox.html"], '**': ['localtoc.html', 'relations.html', 'searchbox.html']
} }
# Additional templates that should be rendered to pages, maps page names to # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
# html_additional_pages = {} #html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
# html_use_modindex = True #html_use_modindex = True
# If false, no index is generated. # If false, no index is generated.
# html_use_index = True #html_use_index = True
# If true, the index is split into individual pages for each letter. # If true, the index is split into individual pages for each letter.
# html_split_index = False #html_split_index = False
# If true, links to the reST sources are added to the pages. # If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True #html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will # If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # base URL from which the finished HTML is served.
# html_use_opensearch = '' #html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = '' #html_file_suffix = ''
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = "MongoEnginedoc" htmlhelp_basename = 'MongoEnginedoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
latex_paper_size = "a4" latex_paper_size = 'a4'
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# latex_font_size = '10pt' #latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
("index", "MongoEngine.tex", "MongoEngine Documentation", "Ross Lawley", "manual") ('index', 'MongoEngine.tex', 'MongoEngine Documentation',
'Ross Lawley', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
# latex_logo = None #latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
# latex_use_parts = False #latex_use_parts = False
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# latex_preamble = '' #latex_preamble = ''
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
# latex_appendices = [] #latex_appendices = []
# If false, no module index is generated. # If false, no module index is generated.
# latex_use_modindex = True #latex_use_modindex = True
autoclass_content = "both" autoclass_content = 'both'

View File

@ -1,12 +0,0 @@
==========================
Frequently Asked Questions
==========================
Does MongoEngine support asynchronous drivers (Motor, TxMongo)?
---------------------------------------------------------------
No, MongoEngine is exclusively based on PyMongo and isn't designed to support other driver.
If this is a requirement for your project, check the alternative: `uMongo`_ and `MotorEngine`_.
.. _uMongo: https://umongo.readthedocs.io/
.. _MotorEngine: https://motorengine.readthedocs.io/

View File

@ -5,7 +5,7 @@ Connecting to MongoDB
===================== =====================
Connections in MongoEngine are registered globally and are identified with aliases. Connections in MongoEngine are registered globally and are identified with aliases.
If no ``alias`` is provided during the connection, it will use "default" as alias. If no `alias` is provided during the connection, it will use "default" as alias.
To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect` To connect to a running instance of :program:`mongod`, use the :func:`~mongoengine.connect`
function. The first argument is the name of the database to connect to:: function. The first argument is the name of the database to connect to::
@ -14,66 +14,25 @@ function. The first argument is the name of the database to connect to::
connect('project1') connect('project1')
By default, MongoEngine assumes that the :program:`mongod` instance is running By default, MongoEngine assumes that the :program:`mongod` instance is running
on **localhost** on port **27017**. on **localhost** on port **27017**. If MongoDB is running elsewhere, you should
provide the :attr:`host` and :attr:`port` arguments to
:func:`~mongoengine.connect`::
If MongoDB is running elsewhere, you need to provide details on how to connect. There are two ways of connect('project1', host='192.168.1.35', port=12345)
doing this. Using a connection string in URI format (**this is the preferred method**) or individual attributes
provided as keyword arguments.
Connect with URI string
=======================
When using a connection string in URI format you should specify the connection details
as the :attr:`host` to :func:`~mongoengine.connect`. In a web application context for instance, the URI
is typically read from the config file::
connect(host="mongodb://127.0.0.1:27017/my_db")
If the database requires authentication, you can specify it in the
URI. As each database can have its own users configured, you need to tell MongoDB
where to look for the user you are working with, that's what the ``?authSource=admin`` bit
of the MongoDB connection string is for::
# Connects to 'my_db' database by authenticating
# with given credentials against the 'admin' database (by default as authSource isn't provided)
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db")
# Equivalent to previous connection but explicitly states that
# it should use admin as the authentication source database
connect(host="mongodb://my_user:my_password@hostname:port/my_db?authSource=admin")
# Connects to 'my_db' database by authenticating
# with given credentials against that same database
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=my_db")
The URI string can also be used to configure advanced parameters like ssl, replicaSet, etc. For more
information or example about URI string, you can refer to the `official doc <https://docs.mongodb.com/manual/reference/connection-string/>`_::
connect(host="mongodb://my_user:my_password@127.0.0.1:27017/my_db?authSource=admin&ssl=true&replicaSet=globaldb")
.. note:: URI containing SRV records (e.g "mongodb+srv://server.example.com/") can be used as well
Connect with keyword attributes
===============================
The second option for specifying the connection details is to provide the information as keyword
attributes to :func:`~mongoengine.connect`::
connect('my_db', host='127.0.0.1', port=27017)
If the database requires authentication, :attr:`username`, :attr:`password` If the database requires authentication, :attr:`username`, :attr:`password`
and :attr:`authentication_source` arguments should be provided:: and :attr:`authentication_source` arguments should be provided::
connect('my_db', username='my_user', password='my_password', authentication_source='admin') connect('project1', username='webapp', password='pwd123', authentication_source='admin')
The set of attributes that :func:`~mongoengine.connect` recognizes includes but is not limited to: URI style connections are also supported -- just supply the URI as
:attr:`host`, :attr:`port`, :attr:`read_preference`, :attr:`username`, :attr:`password`, :attr:`authentication_source`, :attr:`authentication_mechanism`, the :attr:`host` to
:attr:`replicaset`, :attr:`tls`, etc. Most of the parameters accepted by `pymongo.MongoClient <https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient>`_ :func:`~mongoengine.connect`::
can be used with :func:`~mongoengine.connect` and will simply be forwarded when instantiating the `pymongo.MongoClient`.
connect('project1', host='mongodb://localhost/database_name')
.. note:: Database, username and password from URI string overrides .. note:: Database, username and password from URI string overrides
corresponding parameters in :func:`~mongoengine.connect`, this should corresponding parameters in :func:`~mongoengine.connect`: ::
obviously be avoided: ::
connect( connect(
db='test', db='test',
@ -82,19 +41,28 @@ can be used with :func:`~mongoengine.connect` and will simply be forwarded when
host='mongodb://admin:qwerty@localhost/production' host='mongodb://admin:qwerty@localhost/production'
) )
will establish connection to ``production`` database using ``admin`` username and ``qwerty`` password. will establish connection to ``production`` database using
``admin`` username and ``qwerty`` password.
.. note:: Calling :func:`~mongoengine.connect` without argument will establish .. note:: Calling :func:`~mongoengine.connect` without argument will establish
a connection to the "test" database by default a connection to the "test" database by default
Read Preferences Replica Sets
================ ============
As stated above, Read preferences are supported through the connection but also via individual MongoEngine supports connecting to replica sets::
from mongoengine import connect
# Regular connect
connect('dbname', replicaset='rs-name')
# MongoDB URI-style connect
connect(host='mongodb://localhost/dbname?replicaSet=rs-name')
Read preferences are supported through the connection or via individual
queries by passing the read_preference :: queries by passing the read_preference ::
from pymongo import ReadPreference
Bar.objects().read_preference(ReadPreference.PRIMARY) Bar.objects().read_preference(ReadPreference.PRIMARY)
Bar.objects(read_preference=ReadPreference.PRIMARY) Bar.objects(read_preference=ReadPreference.PRIMARY)

View File

@ -27,8 +27,6 @@ objects** as class attributes to the document class::
As BSON (the binary format for storing data in mongodb) is order dependent, As BSON (the binary format for storing data in mongodb) is order dependent,
documents are serialized based on their field order. documents are serialized based on their field order.
.. _dynamic-document-schemas:
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
@ -78,7 +76,6 @@ are as follows:
* :class:`~mongoengine.fields.EmailField` * :class:`~mongoengine.fields.EmailField`
* :class:`~mongoengine.fields.EmbeddedDocumentField` * :class:`~mongoengine.fields.EmbeddedDocumentField`
* :class:`~mongoengine.fields.EmbeddedDocumentListField` * :class:`~mongoengine.fields.EmbeddedDocumentListField`
* :class:`~mongoengine.fields.EnumField`
* :class:`~mongoengine.fields.FileField` * :class:`~mongoengine.fields.FileField`
* :class:`~mongoengine.fields.FloatField` * :class:`~mongoengine.fields.FloatField`
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField` * :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
@ -233,9 +230,6 @@ document class as the first argument::
comment2 = Comment(content='Nice article!') comment2 = Comment(content='Nice article!')
page = Page(comments=[comment1, comment2]) page = Page(comments=[comment1, comment2])
Embedded documents can also leverage the flexibility of :ref:`dynamic-document-schemas:`
by inheriting :class:`~mongoengine.DynamicEmbeddedDocument`.
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
@ -295,12 +289,12 @@ as the constructor's argument::
content = StringField() content = StringField()
.. _many-to-many-with-listfields: .. _one-to-many-with-listfields:
Many to Many with ListFields One to Many with ListFields
''''''''''''''''''''''''''' '''''''''''''''''''''''''''
If you are implementing a many to many relationship via a list of references, If you are implementing a one to many relationship via a list of references,
then the references are stored as DBRefs and to query you need to pass an then the references are stored as DBRefs and to query you need to pass an
instance of the object to the query:: instance of the object to the query::
@ -341,6 +335,7 @@ supplying the :attr:`reverse_delete_rule` attributes on the
:class:`ReferenceField` definition, like this:: :class:`ReferenceField` definition, like this::
class ProfilePage(Document): class ProfilePage(Document):
...
employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE) employee = ReferenceField('Employee', reverse_delete_rule=mongoengine.CASCADE)
The declaration in this example means that when an :class:`Employee` object is The declaration in this example means that when an :class:`Employee` object is
@ -357,7 +352,7 @@ Its value can take any of the following constants:
Deletion is denied if there still exist references to the object being Deletion is denied if there still exist references to the object being
deleted. deleted.
:const:`mongoengine.NULLIFY` :const:`mongoengine.NULLIFY`
Any object's fields still referring to the object being deleted are set to None Any object's fields still referring to the object being deleted are removed
(using MongoDB's "unset" operation), effectively nullifying the relationship. (using MongoDB's "unset" operation), effectively nullifying the relationship.
:const:`mongoengine.CASCADE` :const:`mongoengine.CASCADE`
Any object containing fields that are referring to the object being deleted Any object containing fields that are referring to the object being deleted
@ -431,15 +426,28 @@ either a single field name, or a list or tuple of field names::
first_name = StringField() first_name = StringField()
last_name = StringField(unique_with='first_name') last_name = StringField(unique_with='first_name')
Skipping Document validation on save
------------------------------------
You can also skip the whole document validation process by setting
``validate=False`` when calling the :meth:`~mongoengine.document.Document.save`
method::
class Recipient(Document):
name = StringField()
email = EmailField()
recipient = Recipient(name='admin', email='root@localhost')
recipient.save() # will raise a ValidationError while
recipient.save(validate=False) # won't
Document collections Document collections
==================== ====================
Document classes that inherit **directly** from :class:`~mongoengine.Document` Document classes that inherit **directly** from :class:`~mongoengine.Document`
will have their own **collection** in the database. The name of the collection will have their own **collection** in the database. The name of the collection
is by default the name of the class converted to snake_case (e.g if your Document class is by default the name of the class, converted to lowercase (so in the example
is named `CompanyUser`, the corresponding collection would be `company_user`). If you need above, the collection would be called `page`). If you need to change the name
to change the name of the collection (e.g. to use MongoEngine with an existing database), of the collection (e.g. to use MongoEngine with an existing database), then
then create a class dictionary attribute called :attr:`meta` on your document, and create a class dictionary attribute called :attr:`meta` on your document, and
set :attr:`collection` to the name of the collection that you want your set :attr:`collection` to the name of the collection that you want your
document class to use:: document class to use::
@ -477,7 +485,7 @@ dictionary containing a full index definition.
A direction may be specified on fields by prefixing the field name with a A direction may be specified on fields by prefixing the field name with a
**+** (for ascending) or a **-** sign (for descending). Note that direction **+** (for ascending) or a **-** sign (for descending). Note that direction
only matters on compound indexes. Text indexes may be specified by prefixing only matters on multi-field indexes. Text indexes may be specified by prefixing
the field name with a **$**. Hashed indexes may be specified by prefixing the field name with a **$**. Hashed indexes may be specified by prefixing
the field name with a **#**:: the field name with a **#**::
@ -488,14 +496,14 @@ the field name with a **#**::
created = DateTimeField() created = DateTimeField()
meta = { meta = {
'indexes': [ 'indexes': [
'title', # single-field index 'title',
'$title', # text index '$title', # text index
'#title', # hashed index '#title', # hashed index
('title', '-rating'), # compound index ('title', '-rating'),
('category', '_cls'), # compound index ('category', '_cls'),
{ {
'fields': ['created'], 'fields': ['created'],
'expireAfterSeconds': 3600 # ttl index 'expireAfterSeconds': 3600
} }
] ]
} }
@ -547,6 +555,7 @@ There are a few top level defaults for all indexes that can be set::
'index_background': True, 'index_background': True,
'index_cls': False, 'index_cls': False,
'auto_create_index': True, 'auto_create_index': True,
'index_drop_dups': True,
} }
@ -565,6 +574,11 @@ There are a few top level defaults for all indexes that can be set::
in systems where indexes are managed separately. Disabling this will improve in systems where indexes are managed separately. Disabling this will improve
performance. performance.
:attr:`index_drop_dups` (Optional)
Set the default value for if an index should drop duplicates
Since MongoDB 3.0 drop_dups is not supported anymore. Raises a Warning
and has no effect
Compound Indexes and Indexing sub documents Compound Indexes and Indexing sub documents
------------------------------------------- -------------------------------------------
@ -628,8 +642,8 @@ point. To create a geospatial index you must prefix the field with the
], ],
} }
Time To Live (TTL) indexes Time To Live indexes
-------------------------- --------------------
A special index type that allows you to automatically expire data from a A special index type that allows you to automatically expire data from a
collection after a given period. See the official collection after a given period. See the official
@ -730,7 +744,7 @@ Document inheritance
To create a specialised type of a :class:`~mongoengine.Document` you have To create a specialised type of a :class:`~mongoengine.Document` you have
defined, you may subclass it and add any extra fields or methods you may need. defined, you may subclass it and add any extra fields or methods you may need.
As this new class is not a direct subclass of As this is new class is not a direct subclass of
:class:`~mongoengine.Document`, it will not be stored in its own collection; it :class:`~mongoengine.Document`, it will not be stored in its own collection; it
will use the same collection as its superclass uses. This allows for more will use the same collection as its superclass uses. This allows for more
convenient and efficient retrieval of related documents -- all you need do is convenient and efficient retrieval of related documents -- all you need do is
@ -753,27 +767,6 @@ document.::
Setting :attr:`allow_inheritance` to True should also be used in Setting :attr:`allow_inheritance` to True should also be used in
:class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it :class:`~mongoengine.EmbeddedDocument` class in case you need to subclass it
When it comes to querying using :attr:`.objects()`, querying `Page.objects()` will query
both `Page` and `DatedPage` whereas querying `DatedPage` will only query the `DatedPage` documents.
Behind the scenes, MongoEngine deals with inheritance by adding a :attr:`_cls` attribute that contains
the class name in every documents. When a document is loaded, MongoEngine checks
it's :attr:`_cls` attribute and use that class to construct the instance.::
Page(title='a funky title').save()
DatedPage(title='another title', date=datetime.utcnow()).save()
print(Page.objects().count()) # 2
print(DatedPage.objects().count()) # 1
# print documents in their native form
# we remove 'id' to avoid polluting the output with unnecessary detail
qs = Page.objects.exclude('id').as_pymongo()
print(list(qs))
# [
# {'_cls': u 'Page', 'title': 'a funky title'},
# {'_cls': u 'Page.DatedPage', 'title': u 'another title', 'date': datetime.datetime(2019, 12, 13, 20, 16, 59, 993000)}
# ]
Working with existing data Working with existing data
-------------------------- --------------------------
As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and As MongoEngine no longer defaults to needing :attr:`_cls`, you can quickly and

View File

@ -41,6 +41,35 @@ already exist, then any changes will be updated atomically. For example::
.. seealso:: .. seealso::
:ref:`guide-atomic-updates` :ref:`guide-atomic-updates`
Pre save data validation and cleaning
-------------------------------------
MongoEngine allows you to create custom cleaning rules for your documents when
calling :meth:`~mongoengine.Document.save`. By providing a custom
:meth:`~mongoengine.Document.clean` method you can do any pre validation / data
cleaning.
This might be useful if you want to ensure a default value based on other
document values for example::
class Essay(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
pub_date = DateTimeField()
def clean(self):
"""Ensures that only published essays have a `pub_date` and
automatically sets `pub_date` if essay is published and `pub_date`
is not set"""
if self.status == 'Draft' and self.pub_date is not None:
msg = 'Draft entries should not have a publication date.'
raise ValidationError(msg)
# Set the pub_date for published items if not set.
if self.status == 'Published' and self.pub_date is None:
self.pub_date = datetime.now()
.. note::
Cleaning is only called if validation is turned on and when calling
:meth:`~mongoengine.Document.save`.
Cascading Saves Cascading Saves
--------------- ---------------
If your document contains :class:`~mongoengine.fields.ReferenceField` or If your document contains :class:`~mongoengine.fields.ReferenceField` or

View File

@ -2,15 +2,16 @@
GridFS GridFS
====== ======
.. versionadded:: 0.4
Writing Writing
------- -------
GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field GridFS support comes in the form of the :class:`~mongoengine.fields.FileField` field
object. This field acts as a file-like object and provides a couple of object. This field acts as a file-like object and provides a couple of
different ways of inserting and retrieving data. Arbitrary metadata such as different ways of inserting and retrieving data. Arbitrary metadata such as
content type can also be stored alongside the files. The object returned when accessing a content type can also be stored alongside the files. In the following example,
FileField is a proxy to `Pymongo's GridFS <https://api.mongodb.com/python/current/examples/gridfs.html#gridfs-example>`_ a document is created to store details about animals, including a photo::
In the following example, a document is created to store details about animals, including a photo::
class Animal(Document): class Animal(Document):
genus = StringField() genus = StringField()
@ -19,8 +20,8 @@ In the following example, a document is created to store details about animals,
marmot = Animal(genus='Marmota', family='Sciuridae') marmot = Animal(genus='Marmota', family='Sciuridae')
with open('marmot.jpg', 'rb') as fd: marmot_photo = open('marmot.jpg', 'rb')
marmot.photo.put(fd, content_type = 'image/jpeg') marmot.photo.put(marmot_photo, content_type = 'image/jpeg')
marmot.save() marmot.save()
Retrieval Retrieval
@ -33,20 +34,6 @@ field. The file can also be retrieved just as easily::
photo = marmot.photo.read() photo = marmot.photo.read()
content_type = marmot.photo.content_type content_type = marmot.photo.content_type
.. note:: If you need to read() the content of a file multiple times, you'll need to "rewind"
the file-like object using `seek`::
marmot = Animal.objects(genus='Marmota').first()
content1 = marmot.photo.read()
assert content1 != ""
content2 = marmot.photo.read() # will be empty
assert content2 == ""
marmot.photo.seek(0) # rewind the file by setting the current position of the cursor in the file to 0
content3 = marmot.photo.read()
assert content3 == content1
Streaming Streaming
--------- ---------

View File

@ -10,10 +10,7 @@ User Guide
defining-documents defining-documents
document-instances document-instances
querying querying
validation
gridfs gridfs
signals signals
text-indexes text-indexes
migration
logging-monitoring
mongomock mongomock

View File

@ -12,7 +12,7 @@ MongoEngine is available on PyPI, so you can use :program:`pip`:
.. code-block:: console .. code-block:: console
$ python -m pip install mongoengine $ pip install mongoengine
Alternatively, if you don't have setuptools installed, `download it from PyPi Alternatively, if you don't have setuptools installed, `download it from PyPi
<http://pypi.python.org/pypi/mongoengine/>`_ and run <http://pypi.python.org/pypi/mongoengine/>`_ and run

View File

@ -1,80 +0,0 @@
==================
Logging/Monitoring
==================
It is possible to use `pymongo.monitoring <https://api.mongodb.com/python/current/api/pymongo/monitoring.html>`_ to monitor
the driver events (e.g: queries, connections, etc). This can be handy if you want to monitor the queries issued by
MongoEngine to the driver.
To use `pymongo.monitoring` with MongoEngine, you need to make sure that you are registering the listeners
**before** establishing the database connection (i.e calling `connect`):
The following snippet provides a basic logging of all command events:
.. code-block:: python
import logging
from pymongo import monitoring
from mongoengine import *
log = logging.getLogger()
log.setLevel(logging.DEBUG)
logging.basicConfig(level=logging.DEBUG)
class CommandLogger(monitoring.CommandListener):
def started(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} started on server "
"{0.connection_id}".format(event))
def succeeded(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"succeeded in {0.duration_micros} "
"microseconds".format(event))
def failed(self, event):
log.debug("Command {0.command_name} with request id "
"{0.request_id} on server {0.connection_id} "
"failed in {0.duration_micros} "
"microseconds".format(event))
monitoring.register(CommandLogger())
class Jedi(Document):
name = StringField()
connect()
log.info('GO!')
log.info('Saving an item through MongoEngine...')
Jedi(name='Obi-Wan Kenobii').save()
log.info('Querying through MongoEngine...')
obiwan = Jedi.objects.first()
log.info('Updating through MongoEngine...')
obiwan.name = 'Obi-Wan Kenobi'
obiwan.save()
Executing this prints the following output::
INFO:root:GO!
INFO:root:Saving an item through MongoEngine...
DEBUG:root:Command insert with request id 1681692777 started on server ('localhost', 27017)
DEBUG:root:Command insert with request id 1681692777 on server ('localhost', 27017) succeeded in 562 microseconds
INFO:root:Querying through MongoEngine...
DEBUG:root:Command find with request id 1714636915 started on server ('localhost', 27017)
DEBUG:root:Command find with request id 1714636915 on server ('localhost', 27017) succeeded in 341 microseconds
INFO:root:Updating through MongoEngine...
DEBUG:root:Command update with request id 1957747793 started on server ('localhost', 27017)
DEBUG:root:Command update with request id 1957747793 on server ('localhost', 27017) succeeded in 455 microseconds
More details can of course be obtained by checking the `event` argument from the `CommandListener`.

View File

@ -1,308 +0,0 @@
===================
Documents migration
===================
The structure of your documents and their associated mongoengine schemas are likely
to change over the lifetime of an application. This section provides guidance and
recommendations on how to deal with migrations.
Due to the very flexible nature of mongodb, migrations of models aren't trivial and
for people that know about `alembic` for `sqlalchemy`, there is unfortunately no equivalent
library that will manage the migration in an automatic fashion for mongoengine.
Example 1: Addition of a field
==============================
Let's start by taking a simple example of a model change and review the different option you
have to deal with the migration.
Let's assume we start with the following schema and save an instance:
.. code-block:: python
class User(Document):
name = StringField()
User(name="John Doe").save()
# print the objects as they exist in mongodb
print(User.objects().as_pymongo()) # [{u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John Doe'}]
On the next version of your application, let's now assume that a new field `enabled` gets added to the
existing ``User`` model with a `default=True`. Thus you simply update the ``User`` class to the following:
.. code-block:: python
class User(Document):
name = StringField(required=True)
enabled = BooleanField(default=True)
Without applying any migration, we now reload an object from the database into the ``User`` class
and checks its `enabled` attribute:
.. code-block:: python
assert User.objects.count() == 1
user = User.objects().first()
assert user.enabled is True
assert User.objects(enabled=True).count() == 0 # uh?
assert User.objects(enabled=False).count() == 0 # uh?
# this is consistent with what we have in the database
# in fact, 'enabled' does not exist
print(User.objects().as_pymongo().first()) # {u'_id': ObjectId('5d06b9c3d7c1f18db3e7c874'), u'name': u'John'}
assert User.objects(enabled=None).count() == 1
As you can see, even if the document wasn't updated, mongoengine applies the default value seamlessly when it
loads the pymongo dict into a ``User`` instance. At first sight it looks like you don't need to migrate the
existing documents when adding new fields but this actually leads to inconsistencies when it comes to querying.
In fact, when querying, mongoengine isn't trying to account for the default value of the new field and so
if you don't actually migrate the existing documents, you are taking a risk that querying/updating
will be missing relevant record.
When adding fields/modifying default values, you can use any of the following to do the migration
as a standalone script:
.. code-block:: python
# Use mongoengine to set a default value for a given field
User.objects().update(enabled=True)
# or use pymongo
user_coll = User._get_collection()
user_coll.update_many({}, {'$set': {'enabled': True}})
Example 2: Inheritance change
=============================
Let's consider the following example:
.. code-block:: python
class Human(Document):
name = StringField()
meta = {"allow_inheritance": True}
class Jedi(Human):
dark_side = BooleanField()
light_saber_color = StringField()
Jedi(name="Darth Vader", dark_side=True, light_saber_color="red").save()
Jedi(name="Obi Wan Kenobi", dark_side=False, light_saber_color="blue").save()
assert Human.objects.count() == 2
assert Jedi.objects.count() == 2
# Let's check how these documents got stored in mongodb
print(Jedi.objects.as_pymongo())
# [
# {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'},
# {'_id': ObjectId('5fac4ac4f61d7fb06046e0fa'), '_cls': 'Human.Jedi', 'name': 'Obi Wan Kenobi', 'dark_side': False, 'light_saber_color': 'blue'}
# ]
As you can observe, when you use inheritance, MongoEngine stores a field named '_cls' behind the scene to keep
track of the Document class.
Let's now take the scenario that you want to refactor the inheritance schema and:
- Have the Jedi's with dark_side=True/False become GoodJedi's/DarkSith
- get rid of the 'dark_side' field
move to the following schemas:
.. code-block:: python
# unchanged
class Human(Document):
name = StringField()
meta = {"allow_inheritance": True}
# attribute 'dark_side' removed
class GoodJedi(Human):
light_saber_color = StringField()
# new class
class BadSith(Human):
light_saber_color = StringField()
MongoEngine doesn't know about the change or how to map them with the existing data
so if you don't apply any migration, you will observe a strange behavior, as if the collection was suddenly
empty.
.. code-block:: python
# As a reminder, the documents that we inserted
# have the _cls field = 'Human.Jedi'
# Following has no match
# because the query that is used behind the scene is
# filtering on {'_cls': 'Human.GoodJedi'}
assert GoodJedi.objects().count() == 0
# Following has also no match
# because it is filtering on {'_cls': {'$in': ('Human', 'Human.GoodJedi', 'Human.BadSith')}}
# which has no match
assert Human.objects.count() == 0
assert Human.objects.first() is None
# If we bypass MongoEngine and make use of underlying driver (PyMongo)
# we can see that the documents are there
humans_coll = Human._get_collection()
assert humans_coll.count_documents({}) == 2
# print first document
print(humans_coll.find_one())
# {'_id': ObjectId('5fac4aaaf61d7fb06046e0f9'), '_cls': 'Human.Jedi', 'name': 'Darth Vader', 'dark_side': True, 'light_saber_color': 'red'}
As you can see, first obvious problem is that we need to modify '_cls' values based on existing values of
'dark_side' documents.
.. code-block:: python
humans_coll = Human._get_collection()
old_class = 'Human.Jedi'
good_jedi_class = 'Human.GoodJedi'
bad_sith_class = 'Human.BadSith'
humans_coll.update_many({'_cls': old_class, 'dark_side': False}, {'$set': {'_cls': good_jedi_class}})
humans_coll.update_many({'_cls': old_class, 'dark_side': True}, {'$set': {'_cls': bad_sith_class}})
Let's now check if querying improved in MongoEngine:
.. code-block:: python
assert GoodJedi.objects().count() == 1 # Hoorah!
assert BadSith.objects().count() == 1 # Hoorah!
assert Human.objects.count() == 2 # Hoorah!
# let's now check that documents load correctly
jedi = GoodJedi.objects().first()
# raises FieldDoesNotExist: The fields "{'dark_side'}" do not exist on the document "Human.GoodJedi"
In fact we only took care of renaming the _cls values but we havn't removed the 'dark_side' fields
which does not exist anymore on the GoodJedi's and BadSith's models.
Let's remove the field from the collections:
.. code-block:: python
humans_coll = Human._get_collection()
humans_coll.update_many({}, {'$unset': {'dark_side': 1}})
.. note:: We did this migration in 2 different steps for the sake of example but it could have been combined
with the migration of the _cls fields: ::
humans_coll.update_many(
{'_cls': old_class, 'dark_side': False},
{
'$set': {'_cls': good_jedi_class},
'$unset': {'dark_side': 1}
}
)
And verify that the documents now load correctly:
.. code-block:: python
jedi = GoodJedi.objects().first()
assert jedi.name == "Obi Wan Kenobi"
sith = BadSith.objects().first()
assert sith.name == "Darth Vader"
An other way of dealing with this migration is to iterate over
the documents and update/replace them one by one. This is way slower but
it is often useful for complex migrations of Document models.
.. code-block:: python
for doc in humans_coll.find():
if doc['_cls'] == 'Human.Jedi':
doc['_cls'] = 'Human.BadSith' if doc['dark_side'] else 'Human.GoodJedi'
doc.pop('dark_side')
humans_coll.replace_one({'_id': doc['_id']}, doc)
.. warning:: Be aware of this `flaw <https://groups.google.com/g/mongodb-user/c/AFC1ia7MHzk>`_ if you modify documents while iterating
Example 4: Index removal
========================
If you remove an index from your Document class, or remove an indexed Field from your Document class,
you'll need to manually drop the corresponding index. MongoEngine will not do that for you.
The way to deal with this case is to identify the name of the index to drop with `index_information()`, and then drop
it with `drop_index()`
Let's for instance assume that you start with the following Document class
.. code-block:: python
class User(Document):
name = StringField(index=True)
meta = {"indexes": ["name"]}
User(name="John Doe").save()
As soon as you start interacting with the Document collection (when `.save()` is called in this case),
it would create the following indexes:
.. code-block:: python
print(User._get_collection().index_information())
# {
# '_id_': {'key': [('_id', 1)], 'v': 2},
# 'name_1': {'background': False, 'key': [('name', 1)], 'v': 2},
# }
Thus: '_id' which is the default index and 'name_1' which is our custom index.
If you would remove the 'name' field or its index, you would have to call:
.. code-block:: python
User._get_collection().drop_index('name_1')
.. note:: When adding new fields or new indexes, MongoEngine will take care of creating them
(unless `auto_create_index` is disabled) ::
Recommendations
===============
- Write migration scripts whenever you do changes to the model schemas
- Using :class:`~mongoengine.DynamicDocument` or ``meta = {"strict": False}`` may help to avoid some migrations or to have the 2 versions of your application to co-exist.
- Write post-processing checks to verify that migrations script worked. See below
Post-processing checks
======================
The following recipe can be used to sanity check a Document collection after you applied migration.
It does not make any assumption on what was migrated, it will fetch 1000 objects randomly and
run some quick checks on the documents to make sure the document looks OK. As it is, it will fail
on the first occurrence of an error but this is something that can be adapted based on your needs.
.. code-block:: python
def get_random_oids(collection, sample_size):
pipeline = [{"$project": {'_id': 1}}, {"$sample": {"size": sample_size}}]
return [s['_id'] for s in collection.aggregate(pipeline)]
def get_random_documents(DocCls, sample_size):
doc_collection = DocCls._get_collection()
random_oids = get_random_oids(doc_collection, sample_size)
return DocCls.objects(id__in=random_oids)
def check_documents(DocCls, sample_size):
for doc in get_random_documents(DocCls, sample_size):
# general validation (types and values)
doc.validate()
# load all subfields,
# this may trigger additional queries if you have ReferenceFields
# so it may be slow
for field in doc._fields:
try:
getattr(doc, field)
except Exception:
LOG.warning(f"Could not load field {field} in Document {doc.id}")
raise
check_documents(Human, sample_size=1000)

View File

@ -21,7 +21,7 @@ or with an alias:
conn = get_connection('testdb') conn = get_connection('testdb')
Example of test file: Example of test file:
--------------------- --------
.. code-block:: python .. code-block:: python
import unittest import unittest
@ -45,4 +45,4 @@ Example of test file:
pers.save() pers.save()
fresh_pers = Person.objects().first() fresh_pers = Person.objects().first()
assert fresh_pers.name == 'John' self.assertEqual(fresh_pers.name, 'John')

View File

@ -86,10 +86,6 @@ expressions:
* ``istartswith`` -- string field starts with value (case insensitive) * ``istartswith`` -- string field starts with value (case insensitive)
* ``endswith`` -- string field ends with value * ``endswith`` -- string field ends with value
* ``iendswith`` -- string field ends with value (case insensitive) * ``iendswith`` -- string field ends with value (case insensitive)
* ``wholeword`` -- string field contains whole word
* ``iwholeword`` -- string field contains whole word (case insensitive)
* ``regex`` -- string field match by regex
* ``iregex`` -- string field match by regex (case insensitive)
* ``match`` -- performs an $elemMatch so you can match an entire document within an array * ``match`` -- performs an $elemMatch so you can match an entire document within an array
@ -226,24 +222,12 @@ keyword argument::
.. versionadded:: 0.4 .. versionadded:: 0.4
Sorting/Ordering results
========================
It is possible to order the results by 1 or more keys using :meth:`~mongoengine.queryset.QuerySet.order_by`.
The order may be specified by prepending each of the keys by "+" or "-". Ascending order is assumed if there's no prefix.::
# Order by ascending date
blogs = BlogPost.objects().order_by('date') # equivalent to .order_by('+date')
# Order by ascending date first, then descending title
blogs = BlogPost.objects().order_by('+date', '-title')
Limiting and skipping results Limiting and skipping results
============================= =============================
Just as with traditional ORMs, you may limit the number of results returned or Just as with traditional ORMs, you may limit the number of results returned or
skip a number or results in you query. skip a number or results in you query.
:meth:`~mongoengine.queryset.QuerySet.limit` and :meth:`~mongoengine.queryset.QuerySet.limit` and
:meth:`~mongoengine.queryset.QuerySet.skip` methods are available on :meth:`~mongoengine.queryset.QuerySet.skip` and methods are available on
:class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax :class:`~mongoengine.queryset.QuerySet` objects, but the `array-slicing` syntax
is preferred for achieving this:: is preferred for achieving this::
@ -365,9 +349,9 @@ Just as with limiting and skipping results, there is a method on a
You could technically use ``len(User.objects)`` to get the same result, but it You could technically use ``len(User.objects)`` to get the same result, but it
would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`. would be significantly slower than :meth:`~mongoengine.queryset.QuerySet.count`.
When you execute a server-side count query, you let MongoDB do the heavy When you execute a server-side count query, you let MongoDB do the heavy
lifting and you receive a single integer over the wire. Meanwhile, ``len()`` lifting and you receive a single integer over the wire. Meanwhile, len()
retrieves all the results, places them in a local cache, and finally counts retrieves all the results, places them in a local cache, and finally counts
them. If we compare the performance of the two operations, ``len()`` is much slower them. If we compare the performance of the two operations, len() is much slower
than :meth:`~mongoengine.queryset.QuerySet.count`. than :meth:`~mongoengine.queryset.QuerySet.count`.
Further aggregation Further aggregation
@ -402,25 +386,6 @@ would be generating "tag-clouds"::
top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10] top_tags = sorted(tag_freqs.items(), key=itemgetter(1), reverse=True)[:10]
MongoDB aggregation API
-----------------------
If you need to run aggregation pipelines, MongoEngine provides an entry point to `Pymongo's aggregation framework <https://api.mongodb.com/python/current/examples/aggregation.html#aggregation-framework>`_
through :meth:`~mongoengine.queryset.QuerySet.aggregate`. Check out Pymongo's documentation for the syntax and pipeline.
An example of its use would be::
class Person(Document):
name = StringField()
Person(name='John').save()
Person(name='Bob').save()
pipeline = [
{"$sort" : {"name" : -1}},
{"$project": {"_id": 0, "name": {"$toUpper": "$name"}}}
]
data = Person.objects().aggregate(pipeline)
assert data == [{'name': 'BOB'}, {'name': 'JOHN'}]
Query efficiency and performance Query efficiency and performance
================================ ================================
@ -547,10 +512,7 @@ Documents may be updated atomically by using the
There are several different "modifiers" that you may use with these methods: There are several different "modifiers" that you may use with these methods:
* ``set`` -- set a particular value * ``set`` -- set a particular value
* ``set_on_insert`` -- set only if this is new document `need to add upsert=True`_
* ``unset`` -- delete a particular value (since MongoDB v1.3) * ``unset`` -- delete a particular value (since MongoDB v1.3)
* ``max`` -- update only if value is bigger
* ``min`` -- update only if value is smaller
* ``inc`` -- increment a value by a given amount * ``inc`` -- increment a value by a given amount
* ``dec`` -- decrement a value by a given amount * ``dec`` -- decrement a value by a given amount
* ``push`` -- append a value to a list * ``push`` -- append a value to a list
@ -559,7 +521,6 @@ There are several different "modifiers" that you may use with these methods:
* ``pull`` -- remove a value from a list * ``pull`` -- remove a value from a list
* ``pull_all`` -- remove several values from a list * ``pull_all`` -- remove several values from a list
* ``add_to_set`` -- add value to a list only if its not in the list already * ``add_to_set`` -- add value to a list only if its not in the list already
* ``rename`` -- rename the key name
.. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/ .. _depending on the value: http://docs.mongodb.org/manual/reference/operator/update/pop/
@ -605,8 +566,7 @@ cannot use the `$` syntax in keyword arguments it has been mapped to `S`::
['database', 'mongodb'] ['database', 'mongodb']
From MongoDB version 2.6, push operator supports $position value which allows From MongoDB version 2.6, push operator supports $position value which allows
to push values with index:: to push values with index.
>>> post = BlogPost(title="Test", tags=["mongo"]) >>> post = BlogPost(title="Test", tags=["mongo"])
>>> post.save() >>> post.save()
>>> post.update(push__tags__0=["database", "code"]) >>> post.update(push__tags__0=["database", "code"])
@ -617,7 +577,7 @@ to push values with index::
.. note:: .. note::
Currently only top level lists are handled, future versions of mongodb / Currently only top level lists are handled, future versions of mongodb /
pymongo plan to support nested positional operators. See `The $ positional pymongo plan to support nested positional operators. See `The $ positional
operator <https://docs.mongodb.com/manual/tutorial/update-documents/#Updating-The%24positionaloperator>`_. operator <http://www.mongodb.org/display/DOCS/Updating#Updating-The%24positionaloperator>`_.
Server-side javascript execution Server-side javascript execution
================================ ================================

View File

@ -1,122 +0,0 @@
====================
Document Validation
====================
By design, MongoEngine strictly validates the documents right before they are inserted in MongoDB
and makes sure they are consistent with the fields defined in your models.
MongoEngine makes the assumption that the documents that exists in the DB are compliant with the schema.
This means that Mongoengine will not validate a document when an object is loaded from the DB into an instance
of your model but this operation may fail under some circumstances (e.g. if there is a field in
the document fetched from the database that is not defined in your model).
Built-in validation
===================
Mongoengine provides different fields that encapsulate the corresponding validation
out of the box. Validation runs when calling `.validate()` or `.save()`
.. code-block:: python
from mongoengine import Document, EmailField
class User(Document):
email = EmailField()
age = IntField(min_value=0, max_value=99)
user = User(email='invalid@', age=24)
user.validate() # raises ValidationError (Invalid email address: ['email'])
user.save() # raises ValidationError (Invalid email address: ['email'])
user2 = User(email='john.doe@garbage.com', age=1000)
user2.save() # raises ValidationError (Integer value is too large: ['age'])
Custom validation
=================
The following feature can be used to customize the validation:
* Field `validation` parameter
.. code-block:: python
def not_john_doe(name):
if name == 'John Doe':
raise ValidationError("John Doe is not a valid name")
class Person(Document):
full_name = StringField(validation=not_john_doe)
Person(full_name='Billy Doe').save()
Person(full_name='John Doe').save() # raises ValidationError (John Doe is not a valid name)
* Document `clean` method
This method is called as part of :meth:`~mongoengine.document.Document.save` and should be used to provide
custom model validation and/or to modify some of the field values prior to validation.
For instance, you could use it to automatically provide a value for a field, or to do validation
that requires access to more than a single field.
.. code-block:: python
class Essay(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
pub_date = DateTimeField()
def clean(self):
# Validate that only published essays have a `pub_date`
if self.status == 'Draft' and self.pub_date is not None:
raise ValidationError('Draft entries should not have a publication date.')
# Set the pub_date for published items if not set.
if self.status == 'Published' and self.pub_date is None:
self.pub_date = datetime.now()
.. note::
Cleaning is only called if validation is turned on and when calling
:meth:`~mongoengine.Document.save`.
* Adding custom Field classes
We recommend as much as possible to use fields provided by MongoEngine. However, it is also possible
to subclass a Field and encapsulate some validation by overriding the `validate` method
.. code-block:: python
class AgeField(IntField):
def validate(self, value):
super(AgeField, self).validate(value) # let IntField.validate run first
if value == 60:
self.error('60 is not allowed')
class Person(Document):
age = AgeField(min_value=0, max_value=99)
Person(age=20).save() # passes
Person(age=1000).save() # raises ValidationError (Integer value is too large: ['age'])
Person(age=60).save() # raises ValidationError (Person:None) (60 is not allowed: ['age'])
.. note::
When overriding `validate`, use `self.error("your-custom-error")` instead of raising ValidationError explicitly,
it will provide a better context with the error message
Skipping validation
====================
Although discouraged as it allows to violate fields constraints, if for some reason you need to disable
the validation and cleaning of a document when you call :meth:`~mongoengine.document.Document.save`, you can use `.save(validate=False)`.
.. code-block:: python
class Person(Document):
age = IntField(max_value=100)
Person(age=1000).save() # raises ValidationError (Integer value is too large)
Person(age=1000).save(validate=False)
person = Person.objects.first()
assert person.age == 1000

View File

@ -7,7 +7,7 @@ MongoDB. To install it, simply run
.. code-block:: console .. code-block:: console
$ python -m pip install -U mongoengine $ pip install -U mongoengine
:doc:`tutorial` :doc:`tutorial`
A quick tutorial building a tumblelog to get you up and running with A quick tutorial building a tumblelog to get you up and running with
@ -23,18 +23,9 @@ MongoDB. To install it, simply run
:doc:`upgrade` :doc:`upgrade`
How to upgrade MongoEngine. How to upgrade MongoEngine.
:doc:`faq`
Frequently Asked Questions
:doc:`django` :doc:`django`
Using MongoEngine and Django Using MongoEngine and Django
MongoDB and driver support
--------------------------
MongoEngine is based on the PyMongo driver and tested against multiple versions of MongoDB.
For further details, please refer to the `readme <https://github.com/MongoEngine/mongoengine#mongoengine>`_.
Community Community
--------- ---------
@ -82,7 +73,6 @@ formats for offline reading.
apireference apireference
changelog changelog
upgrade upgrade
faq
django django
Indices and tables Indices and tables
@ -91,3 +81,4 @@ Indices and tables
* :ref:`genindex` * :ref:`genindex`
* :ref:`modindex` * :ref:`modindex`
* :ref:`search` * :ref:`search`

View File

@ -1,3 +0,0 @@
Sphinx==3.3.0
sphinx-rtd-theme==0.5.0
readthedocs-sphinx-ext==2.1.1

View File

@ -18,7 +18,7 @@ location --- running it locally will be easier, but if that is not an option
then it may be run on a remote server. If you haven't installed MongoEngine, then it may be run on a remote server. If you haven't installed MongoEngine,
simply use pip to install it like so:: simply use pip to install it like so::
$ python -m pip install mongoengine $ pip install mongoengine
Before we can start using MongoEngine, we need to tell it how to connect to our Before we can start using MongoEngine, we need to tell it how to connect to our
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect` instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`

View File

@ -52,7 +52,7 @@ rename its occurrences.
This release includes a major rehaul of MongoEngine's code quality and This release includes a major rehaul of MongoEngine's code quality and
introduces a few breaking changes. It also touches many different parts of introduces a few breaking changes. It also touches many different parts of
the package and although all the changes have been tested and scrutinized, the package and although all the changes have been tested and scrutinized,
you're encouraged to thoroughly test the upgrade. you're encouraged to thorougly test the upgrade.
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`. First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
If you import or catch this exception, you'll need to rename it in your code. If you import or catch this exception, you'll need to rename it in your code.
@ -85,10 +85,10 @@ by default from now on.
The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: :: The 0.8.7 package on pypi was corrupted. If upgrading from 0.8.7 to 0.9.0 please follow: ::
python -m pip uninstall pymongo pip uninstall pymongo
python -m pip uninstall mongoengine pip uninstall mongoengine
python -m pip install pymongo==2.8 pip install pymongo==2.8
python -m pip install mongoengine pip install mongoengine
0.8.7 0.8.7
***** *****
@ -153,7 +153,7 @@ inherited classes like so: ::
# 4. Remove indexes # 4. Remove indexes
info = collection.index_information() info = collection.index_information()
indexes_to_drop = [key for key, value in info.items() indexes_to_drop = [key for key, value in info.iteritems()
if '_types' in dict(value['key'])] if '_types' in dict(value['key'])]
for index in indexes_to_drop: for index in indexes_to_drop:
collection.drop_index(index) collection.drop_index(index)

View File

@ -1,35 +1,29 @@
# Import submodules so that we can expose their __all__ # Import submodules so that we can expose their __all__
from mongoengine import ( from mongoengine import connection
connection, from mongoengine import document
document, from mongoengine import errors
errors, from mongoengine import fields
fields, from mongoengine import queryset
queryset, from mongoengine import signals
signals,
)
# Import everything from each submodule so that it can be accessed via # Import everything from each submodule so that it can be accessed via
# mongoengine, e.g. instead of `from mongoengine.connection import connect`, # mongoengine, e.g. instead of `from mongoengine.connection import connect`,
# users can simply use `from mongoengine import connect`, or even # users can simply use `from mongoengine import connect`, or even
# `from mongoengine import *` and then `connect('testdb')`. # `from mongoengine import *` and then `connect('testdb')`.
from mongoengine.connection import * # noqa: F401 from mongoengine.connection import *
from mongoengine.document import * # noqa: F401 from mongoengine.document import *
from mongoengine.errors import * # noqa: F401 from mongoengine.errors import *
from mongoengine.fields import * # noqa: F401 from mongoengine.fields import *
from mongoengine.queryset import * # noqa: F401 from mongoengine.queryset import *
from mongoengine.signals import * # noqa: F401 from mongoengine.signals import *
__all__ = (
list(document.__all__)
+ list(fields.__all__)
+ list(connection.__all__)
+ list(queryset.__all__)
+ list(signals.__all__)
+ list(errors.__all__)
)
VERSION = (0, 23, 1) __all__ = (list(document.__all__) + list(fields.__all__) +
list(connection.__all__) + list(queryset.__all__) +
list(signals.__all__) + list(errors.__all__))
VERSION = (0, 18, 2)
def get_version(): def get_version():
@ -37,7 +31,7 @@ def get_version():
For example, if `VERSION == (0, 10, 7)`, return '0.10.7'. For example, if `VERSION == (0, 10, 7)`, return '0.10.7'.
""" """
return ".".join(map(str, VERSION)) return '.'.join(map(str, VERSION))
__version__ = get_version() __version__ = get_version()

View File

@ -12,22 +12,17 @@ from mongoengine.base.metaclasses import *
__all__ = ( __all__ = (
# common # common
"UPDATE_OPERATORS", 'UPDATE_OPERATORS', '_document_registry', 'get_document',
"_document_registry",
"get_document",
# datastructures # datastructures
"BaseDict", 'BaseDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference',
"BaseList",
"EmbeddedDocumentList",
"LazyReference",
# document # document
"BaseDocument", 'BaseDocument',
# fields # fields
"BaseField", 'BaseField', 'ComplexBaseField', 'ObjectIdField', 'GeoJsonBaseField',
"ComplexBaseField",
"ObjectIdField",
"GeoJsonBaseField",
# metaclasses # metaclasses
"DocumentMetaclass", 'DocumentMetaclass', 'TopLevelDocumentMetaclass'
"TopLevelDocumentMetaclass",
) )

View File

@ -1,25 +1,12 @@
from mongoengine.errors import NotRegistered from mongoengine.errors import NotRegistered
__all__ = ("UPDATE_OPERATORS", "get_document", "_document_registry") __all__ = ('UPDATE_OPERATORS', 'get_document', '_document_registry')
UPDATE_OPERATORS = { UPDATE_OPERATORS = {'set', 'unset', 'inc', 'dec', 'mul',
"set", 'pop', 'push', 'push_all', 'pull',
"unset", 'pull_all', 'add_to_set', 'set_on_insert',
"inc", 'min', 'max', 'rename'}
"dec",
"mul",
"pop",
"push",
"push_all",
"pull",
"pull_all",
"add_to_set",
"set_on_insert",
"min",
"max",
"rename",
}
_document_registry = {} _document_registry = {}
@ -30,33 +17,25 @@ def get_document(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
single_end = name.split(".")[-1] single_end = name.split('.')[-1]
compound_end = ".%s" % single_end compound_end = '.%s' % single_end
possible_match = [ possible_match = [k for k in _document_registry
k for k in _document_registry if k.endswith(compound_end) or k == single_end if k.endswith(compound_end) or k == single_end]
]
if len(possible_match) == 1: if len(possible_match) == 1:
doc = _document_registry.get(possible_match.pop(), None) doc = _document_registry.get(possible_match.pop(), None)
if not doc: if not doc:
raise NotRegistered( raise NotRegistered("""
"""
`%s` has not been registered in the document registry. `%s` has not been registered in the document registry.
Importing the document class automatically registers it, has it Importing the document class automatically registers it, has it
been imported? been imported?
""".strip() """.strip() % name)
% name
)
return doc return doc
def _get_documents_by_db(connection_alias, default_connection_alias): def _get_documents_by_db(connection_alias, default_connection_alias):
"""Get all registered Documents class attached to a given database""" """Get all registered Documents class attached to a given database"""
def get_doc_alias(doc_cls): def get_doc_alias(doc_cls):
return doc_cls._meta.get("db_alias", default_connection_alias) return doc_cls._meta.get('db_alias', default_connection_alias)
return [ return [doc_cls for doc_cls in _document_registry.values()
doc_cls if get_doc_alias(doc_cls) == connection_alias]
for doc_cls in _document_registry.values()
if get_doc_alias(doc_cls) == connection_alias
]

View File

@ -1,40 +1,32 @@
import weakref import weakref
from bson import DBRef from bson import DBRef
import six
from six import iteritems
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__ = ( __all__ = ('BaseDict', 'StrictDict', 'BaseList', 'EmbeddedDocumentList', 'LazyReference')
"BaseDict",
"StrictDict",
"BaseList",
"EmbeddedDocumentList",
"LazyReference",
)
def mark_as_changed_wrapper(parent_method): def mark_as_changed_wrapper(parent_method):
"""Decorator that ensures _mark_as_changed method gets called.""" """Decorator that ensures _mark_as_changed method gets called."""
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
# Can't use super() in the decorator. # Can't use super() in the decorator.
result = parent_method(self, *args, **kwargs) result = parent_method(self, *args, **kwargs)
self._mark_as_changed() self._mark_as_changed()
return result return result
return wrapper return wrapper
def mark_key_as_changed_wrapper(parent_method): def mark_key_as_changed_wrapper(parent_method):
"""Decorator that ensures _mark_as_changed method gets called with the key argument""" """Decorator that ensures _mark_as_changed method gets called with the key argument"""
def wrapper(self, key, *args, **kwargs): def wrapper(self, key, *args, **kwargs):
# Can't use super() in the decorator. # Can't use super() in the decorator.
result = parent_method(self, key, *args, **kwargs) result = parent_method(self, key, *args, **kwargs)
self._mark_as_changed(key) self._mark_as_changed(key)
return result return result
return wrapper return wrapper
@ -46,12 +38,12 @@ class BaseDict(dict):
_name = None _name = None
def __init__(self, dict_items, instance, name): def __init__(self, dict_items, instance, name):
BaseDocument = _import_class("BaseDocument") BaseDocument = _import_class('BaseDocument')
if isinstance(instance, BaseDocument): if isinstance(instance, BaseDocument):
self._instance = weakref.proxy(instance) self._instance = weakref.proxy(instance)
self._name = name self._name = name
super().__init__(dict_items) super(BaseDict, self).__init__(dict_items)
def get(self, key, default=None): def get(self, key, default=None):
# get does not use __getitem__ by default so we must override it as well # get does not use __getitem__ by default so we must override it as well
@ -61,18 +53,18 @@ class BaseDict(dict):
return default return default
def __getitem__(self, key): def __getitem__(self, key):
value = super().__getitem__(key) value = super(BaseDict, self).__getitem__(key)
EmbeddedDocument = _import_class("EmbeddedDocument") EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument) and value._instance is None: if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance value._instance = self._instance
elif isinstance(value, dict) and not isinstance(value, BaseDict): elif isinstance(value, dict) and not isinstance(value, BaseDict):
value = BaseDict(value, None, f"{self._name}.{key}") value = BaseDict(value, None, '%s.%s' % (self._name, key))
super().__setitem__(key, value) super(BaseDict, self).__setitem__(key, value)
value._instance = self._instance value._instance = self._instance
elif isinstance(value, list) and not isinstance(value, BaseList): elif isinstance(value, list) and not isinstance(value, BaseList):
value = BaseList(value, None, f"{self._name}.{key}") value = BaseList(value, None, '%s.%s' % (self._name, key))
super().__setitem__(key, value) super(BaseDict, self).__setitem__(key, value)
value._instance = self._instance value._instance = self._instance
return value return value
@ -95,9 +87,9 @@ class BaseDict(dict):
setdefault = mark_as_changed_wrapper(dict.setdefault) setdefault = mark_as_changed_wrapper(dict.setdefault)
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(f"{self._name}.{key}") self._instance._mark_as_changed('%s.%s' % (self._name, key))
else: else:
self._instance._mark_as_changed(self._name) self._instance._mark_as_changed(self._name)
@ -110,41 +102,39 @@ class BaseList(list):
_name = None _name = None
def __init__(self, list_items, instance, name): def __init__(self, list_items, instance, name):
BaseDocument = _import_class("BaseDocument") BaseDocument = _import_class('BaseDocument')
if isinstance(instance, BaseDocument): if isinstance(instance, BaseDocument):
self._instance = weakref.proxy(instance) self._instance = weakref.proxy(instance)
self._name = name self._name = name
super().__init__(list_items) super(BaseList, self).__init__(list_items)
def __getitem__(self, key): def __getitem__(self, key):
# change index to positive value because MongoDB does not support negative one value = super(BaseList, self).__getitem__(key)
if isinstance(key, int) and key < 0:
key = len(self) + key
value = super().__getitem__(key)
if isinstance(key, slice): if isinstance(key, slice):
# When receiving a slice operator, we don't convert the structure and bind # When receiving a slice operator, we don't convert the structure and bind
# to parent's instance. This is buggy for now but would require more work to be handled properly # to parent's instance. This is buggy for now but would require more work to be handled properly
return value return value
EmbeddedDocument = _import_class("EmbeddedDocument") EmbeddedDocument = _import_class('EmbeddedDocument')
if isinstance(value, EmbeddedDocument) and value._instance is None: if isinstance(value, EmbeddedDocument) and value._instance is None:
value._instance = self._instance value._instance = self._instance
elif isinstance(value, dict) and not isinstance(value, BaseDict): elif isinstance(value, dict) and not isinstance(value, BaseDict):
# Replace dict by BaseDict # Replace dict by BaseDict
value = BaseDict(value, None, f"{self._name}.{key}") value = BaseDict(value, None, '%s.%s' % (self._name, key))
super().__setitem__(key, value) super(BaseList, self).__setitem__(key, value)
value._instance = self._instance value._instance = self._instance
elif isinstance(value, list) and not isinstance(value, BaseList): elif isinstance(value, list) and not isinstance(value, BaseList):
# Replace list by BaseList # Replace list by BaseList
value = BaseList(value, None, f"{self._name}.{key}") value = BaseList(value, None, '%s.%s' % (self._name, key))
super().__setitem__(key, value) super(BaseList, self).__setitem__(key, value)
value._instance = self._instance value._instance = self._instance
return value return value
def __iter__(self): def __iter__(self):
yield from super().__iter__() for v in super(BaseList, self).__iter__():
yield v
def __getstate__(self): def __getstate__(self):
self.instance = None self.instance = None
@ -162,7 +152,7 @@ class BaseList(list):
# instead, we simply marks the whole list as changed # instead, we simply marks the whole list as changed
changed_key = None changed_key = None
result = super().__setitem__(key, value) result = super(BaseList, self).__setitem__(key, value)
self._mark_as_changed(changed_key) self._mark_as_changed(changed_key)
return result return result
@ -177,17 +167,33 @@ class BaseList(list):
__iadd__ = mark_as_changed_wrapper(list.__iadd__) __iadd__ = mark_as_changed_wrapper(list.__iadd__)
__imul__ = mark_as_changed_wrapper(list.__imul__) __imul__ = mark_as_changed_wrapper(list.__imul__)
if six.PY2:
# Under py3 __setslice__, __delslice__ and __getslice__
# are replaced by __setitem__, __delitem__ and __getitem__ with a slice as parameter
# so we mimic this under python 2
def __setslice__(self, i, j, sequence):
return self.__setitem__(slice(i, j), sequence)
def __delslice__(self, i, j):
return self.__delitem__(slice(i, j))
def __getslice__(self, i, j):
return self.__getitem__(slice(i, j))
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 is not None: if key:
self._instance._mark_as_changed(f"{self._name}.{key % len(self)}") 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)
class EmbeddedDocumentList(BaseList): class EmbeddedDocumentList(BaseList):
def __init__(self, list_items, instance, name): def __init__(self, list_items, instance, name):
super().__init__(list_items, instance, name) super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
self._instance = instance self._instance = instance
@classmethod @classmethod
@ -197,7 +203,7 @@ class EmbeddedDocumentList(BaseList):
""" """
for key, expected_value in kwargs.items(): for key, expected_value in kwargs.items():
doc_val = getattr(embedded_doc, key) doc_val = getattr(embedded_doc, key)
if doc_val != expected_value and str(doc_val) != expected_value: if doc_val != expected_value and six.text_type(doc_val) != expected_value:
return False return False
return True return True
@ -213,7 +219,7 @@ class EmbeddedDocumentList(BaseList):
Filters the list by only including embedded documents with the Filters the list by only including embedded documents with the
given keyword arguments. given keyword arguments.
This method only supports simple comparison (e.g. .filter(name='John Doe')) This method only supports simple comparison (e.g: .filter(name='John Doe'))
and does not support operators like __gte, __lte, __icontains like queryset.filter does and does not support operators like __gte, __lte, __icontains like queryset.filter does
:param kwargs: The keyword arguments corresponding to the fields to :param kwargs: The keyword arguments corresponding to the fields to
@ -270,10 +276,12 @@ 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("%s matching query does not exist." % self._name) raise DoesNotExist(
'%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]
@ -287,11 +295,11 @@ class EmbeddedDocumentList(BaseList):
def create(self, **values): def create(self, **values):
""" """
Creates a new instance of the EmbeddedDocument and appends it to this EmbeddedDocumentList. Creates a new embedded document and saves it to the database.
.. note:: .. note::
the instance of the EmbeddedDocument is not automatically saved to the database. The embedded document changes are not automatically saved
You still need to call .save() on the parent Document. to the database after calling this method.
:param values: A dictionary of values for the embedded document. :param values: A dictionary of values for the embedded document.
:return: The new embedded document instance. :return: The new embedded document instance.
@ -352,24 +360,24 @@ class EmbeddedDocumentList(BaseList):
return len(values) return len(values)
class StrictDict: class StrictDict(object):
__slots__ = () __slots__ = ()
_special_fields = {"get", "pop", "iteritems", "items", "keys", "create"} _special_fields = {'get', 'pop', 'iteritems', 'items', 'keys', 'create'}
_classes = {} _classes = {}
def __init__(self, **kwargs): def __init__(self, **kwargs):
for k, v in kwargs.items(): for k, v in iteritems(kwargs):
setattr(self, k, v) setattr(self, k, v)
def __getitem__(self, key): def __getitem__(self, key):
key = "_reserved_" + key if key in self._special_fields else key key = '_reserved_' + key if key in self._special_fields else key
try: try:
return getattr(self, key) return getattr(self, key)
except AttributeError: except AttributeError:
raise KeyError(key) raise KeyError(key)
def __setitem__(self, key, value): def __setitem__(self, key, value):
key = "_reserved_" + key if key in self._special_fields else key key = '_reserved_' + key if key in self._special_fields else key
return setattr(self, key, value) return setattr(self, key, value)
def __contains__(self, key): def __contains__(self, key):
@ -406,42 +414,37 @@ class StrictDict:
return (key for key in self.__slots__ if hasattr(self, key)) return (key for key in self.__slots__ if hasattr(self, key))
def __len__(self): def __len__(self):
return len(list(self.items())) return len(list(iteritems(self)))
def __eq__(self, other): def __eq__(self, other):
return list(self.items()) == list(other.items()) return self.items() == other.items()
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return self.items() != other.items()
@classmethod @classmethod
def create(cls, allowed_keys): def create(cls, allowed_keys):
allowed_keys_tuple = tuple( allowed_keys_tuple = tuple(('_reserved_' + k if k in cls._special_fields else k) for k in allowed_keys)
("_reserved_" + k if k in cls._special_fields else k) for k in allowed_keys
)
allowed_keys = frozenset(allowed_keys_tuple) allowed_keys = frozenset(allowed_keys_tuple)
if allowed_keys not in cls._classes: if allowed_keys not in cls._classes:
class SpecificStrictDict(cls): class SpecificStrictDict(cls):
__slots__ = allowed_keys_tuple __slots__ = allowed_keys_tuple
def __repr__(self): def __repr__(self):
return "{%s}" % ", ".join( return '{%s}' % ', '.join('"{0!s}": {1!r}'.format(k, v) for k, v in self.items())
f'"{k!s}": {v!r}' 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]
class LazyReference(DBRef): class LazyReference(DBRef):
__slots__ = ("_cached_doc", "passthrough", "document_type") __slots__ = ('_cached_doc', 'passthrough', 'document_type')
def fetch(self, force=False): def fetch(self, force=False):
if not self._cached_doc or force: if not self._cached_doc or force:
self._cached_doc = self.document_type.objects.get(pk=self.pk) self._cached_doc = self.document_type.objects.get(pk=self.pk)
if not self._cached_doc: if not self._cached_doc:
raise DoesNotExist("Trying to dereference unknown document %s" % (self)) raise DoesNotExist('Trying to dereference unknown document %s' % (self))
return self._cached_doc return self._cached_doc
@property @property
@ -452,7 +455,7 @@ class LazyReference(DBRef):
self.document_type = document_type self.document_type = document_type
self._cached_doc = cached_doc self._cached_doc = cached_doc
self.passthrough = passthrough self.passthrough = passthrough
super().__init__(self.document_type._get_collection_name(), pk) super(LazyReference, self).__init__(self.document_type._get_collection_name(), pk)
def __getitem__(self, name): def __getitem__(self, name):
if not self.passthrough: if not self.passthrough:
@ -461,7 +464,7 @@ class LazyReference(DBRef):
return document[name] return document[name]
def __getattr__(self, name): def __getattr__(self, name):
if not object.__getattribute__(self, "passthrough"): if not object.__getattribute__(self, 'passthrough'):
raise AttributeError() raise AttributeError()
document = self.fetch() document = self.fetch()
try: try:
@ -470,4 +473,4 @@ class LazyReference(DBRef):
raise AttributeError() raise AttributeError()
def __repr__(self): def __repr__(self):
return f"<LazyReference({self.document_type}, {self.pk!r})>" return "<LazyReference(%s, %r)>" % (self.document_type, self.pk)

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,29 @@
import operator import operator
import warnings
import weakref import weakref
from bson import DBRef, ObjectId, SON
import pymongo import pymongo
from bson import SON, DBRef, ObjectId import six
from six import iteritems
from mongoengine.base.common import UPDATE_OPERATORS from mongoengine.base.common import UPDATE_OPERATORS
from mongoengine.base.datastructures import ( from mongoengine.base.datastructures import (BaseDict, BaseList,
BaseDict, EmbeddedDocumentList)
BaseList,
EmbeddedDocumentList,
)
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.errors import DeprecatedError, ValidationError from mongoengine.errors import DeprecatedError, ValidationError
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField") __all__ = ('BaseField', 'ComplexBaseField', 'ObjectIdField',
'GeoJsonBaseField')
class BaseField: class BaseField(object):
"""A base class for fields in a MongoDB document. Instances of this class """A base class for fields in a MongoDB document. Instances of this class
may be added to subclasses of `Document` to define a document's schema. may be added to subclasses of `Document` to define a document's schema.
"""
name = None # set in TopLevelDocumentMetaclass .. versionchanged:: 0.5 - added verbose and help text
"""
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
_auto_dereference = True _auto_dereference = True
@ -32,23 +34,14 @@ class BaseField:
creation_counter = 0 creation_counter = 0
auto_creation_counter = -1 auto_creation_counter = -1
def __init__( def __init__(self, db_field=None, name=None, required=False, default=None,
self, unique=False, unique_with=None, primary_key=False,
db_field=None, validation=None, choices=None, null=False, sparse=False,
required=False, **kwargs):
default=None,
unique=False,
unique_with=None,
primary_key=False,
validation=None,
choices=None,
null=False,
sparse=False,
**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: 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
@ -72,8 +65,11 @@ class BaseField:
existing attributes. Common metadata includes `verbose_name` and existing attributes. Common metadata includes `verbose_name` and
`help_text`. `help_text`.
""" """
self.db_field = db_field if not primary_key else "_id" self.db_field = (db_field or name) if not primary_key else '_id'
if name:
msg = 'Field\'s "name" attribute deprecated in favour of "db_field"'
warnings.warn(msg, DeprecationWarning)
self.required = required or primary_key self.required = required or primary_key
self.default = default self.default = default
self.unique = bool(unique or unique_with) self.unique = bool(unique or unique_with)
@ -86,14 +82,17 @@ class BaseField:
self._owner_document = None self._owner_document = None
# Make sure db_field is a string (if it's explicitly defined). # Make sure db_field is a string (if it's explicitly defined).
if self.db_field is not None and not isinstance(self.db_field, str): if (
raise TypeError("db_field should be a string.") self.db_field is not None and
not isinstance(self.db_field, six.string_types)
):
raise TypeError('db_field should be a string.')
# Make sure db_field doesn't contain any forbidden characters. # Make sure db_field doesn't contain any forbidden characters.
if isinstance(self.db_field, str) and ( if isinstance(self.db_field, six.string_types) and (
"." in self.db_field '.' in self.db_field or
or "\0" in self.db_field '\0' in self.db_field or
or self.db_field.startswith("$") self.db_field.startswith('$')
): ):
raise ValueError( raise ValueError(
'field names cannot contain dots (".") or null characters ' 'field names cannot contain dots (".") or null characters '
@ -103,17 +102,15 @@ class BaseField:
# Detect and report conflicts between metadata and base properties. # Detect and report conflicts between metadata and base properties.
conflicts = set(dir(self)) & set(kwargs) conflicts = set(dir(self)) & set(kwargs)
if conflicts: if conflicts:
raise TypeError( raise TypeError('%s already has attribute(s): %s' % (
"%s already has attribute(s): %s" self.__class__.__name__, ', '.join(conflicts)))
% (self.__class__.__name__, ", ".join(conflicts))
)
# Assign metadata to the instance # Assign metadata to the instance
# This efficient method is available because no __slots__ are defined. # This efficient method is available because no __slots__ are defined.
self.__dict__.update(kwargs) 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':
self.creation_counter = BaseField.auto_creation_counter self.creation_counter = BaseField.auto_creation_counter
BaseField.auto_creation_counter -= 1 BaseField.auto_creation_counter -= 1
else: else:
@ -121,7 +118,8 @@ class BaseField:
BaseField.creation_counter += 1 BaseField.creation_counter += 1
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor for retrieving a value from a field in a document.""" """Descriptor for retrieving a value from a field in a document.
"""
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
@ -144,8 +142,8 @@ class BaseField:
if instance._initialised: if instance._initialised:
try: try:
value_has_changed = ( value_has_changed = (
self.name not in instance._data self.name not in instance._data or
or instance._data[self.name] != value instance._data[self.name] != value
) )
if value_has_changed: if value_has_changed:
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
@ -155,7 +153,7 @@ class BaseField:
# Mark the field as changed in such cases. # Mark the field as changed in such cases.
instance._mark_as_changed(self.name) instance._mark_as_changed(self.name)
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)): elif isinstance(value, (list, tuple)):
@ -165,7 +163,7 @@ class BaseField:
instance._data[self.name] = value instance._data[self.name] = value
def error(self, message="", errors=None, field_name=None): def error(self, message='', errors=None, field_name=None):
"""Raise a ValidationError.""" """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)
@ -182,11 +180,11 @@ class BaseField:
"""Helper method to call to_mongo with proper inputs.""" """Helper method to call to_mongo with proper inputs."""
f_inputs = self.to_mongo.__code__.co_varnames f_inputs = self.to_mongo.__code__.co_varnames
ex_vars = {} ex_vars = {}
if "fields" in f_inputs: if 'fields' in f_inputs:
ex_vars["fields"] = fields ex_vars['fields'] = fields
if "use_db_field" in f_inputs: if 'use_db_field' in f_inputs:
ex_vars["use_db_field"] = use_db_field ex_vars['use_db_field'] = use_db_field
return self.to_mongo(value, **ex_vars) return self.to_mongo(value, **ex_vars)
@ -201,8 +199,8 @@ class BaseField:
pass pass
def _validate_choices(self, value): def _validate_choices(self, value):
Document = _import_class("Document") Document = _import_class('Document')
EmbeddedDocument = _import_class("EmbeddedDocument") EmbeddedDocument = _import_class('EmbeddedDocument')
choice_list = self.choices choice_list = self.choices
if isinstance(next(iter(choice_list)), (list, tuple)): if isinstance(next(iter(choice_list)), (list, tuple)):
@ -212,12 +210,16 @@ class BaseField:
# 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("Value must be an instance of %s" % (choice_list)) self.error(
'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
else: else:
values = value if isinstance(value, (list, tuple)) else [value] values = value if isinstance(value, (list, tuple)) else [value]
if len(set(values) - set(choice_list)): if len(set(values) - set(choice_list)):
self.error("Value must be one of %s" % str(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
@ -233,17 +235,13 @@ class BaseField:
# in favor of having validation raising a ValidationError # in favor of having validation raising a ValidationError
ret = self.validation(value) ret = self.validation(value)
if ret is not None: if ret is not None:
raise DeprecatedError( raise DeprecatedError('validation argument for `%s` must not return anything, '
"validation argument for `%s` must not return anything, " 'it should raise a ValidationError if validation fails' % self.name)
"it should raise a ValidationError if validation fails"
% self.name
)
except ValidationError as ex: except ValidationError as ex:
self.error(str(ex)) self.error(str(ex))
else: else:
raise ValueError( raise ValueError('validation argument for `"%s"` must be a '
'validation argument for `"%s"` must be a ' "callable." % self.name 'callable.' % self.name)
)
self.validate(value, **kwargs) self.validate(value, **kwargs)
@ -265,22 +263,11 @@ class ComplexBaseField(BaseField):
Allows for nesting of embedded documents inside complex types. Allows for nesting of embedded documents inside complex types.
Handles the lazy dereferencing of a queryset by lazily dereferencing all Handles the lazy dereferencing of a queryset by lazily dereferencing all
items in a list / dict rather than one at a time. items in a list / dict rather than one at a time.
.. versionadded:: 0.5
""" """
def __init__(self, field=None, **kwargs): field = None
self.field = field
super().__init__(**kwargs)
@staticmethod
def _lazy_load_refs(instance, name, ref_values, *, max_depth):
_dereference = _import_class("DeReference")()
documents = _dereference(
ref_values,
max_depth=max_depth,
instance=instance,
name=name,
)
return documents
def __get__(self, instance, owner): def __get__(self, instance, owner):
"""Descriptor to automatically dereference references.""" """Descriptor to automatically dereference references."""
@ -288,37 +275,35 @@ class ComplexBaseField(BaseField):
# Document class being used rather than a document object # Document class being used rather than a document object
return self return self
ReferenceField = _import_class("ReferenceField") ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class("GenericReferenceField") GenericReferenceField = _import_class('GenericReferenceField')
EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField") EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
auto_dereference = instance._fields[self.name]._auto_dereference auto_dereference = instance._fields[self.name]._auto_dereference
dereference = auto_dereference and ( dereference = (auto_dereference and
self.field is None (self.field is None or isinstance(self.field,
or isinstance(self.field, (GenericReferenceField, ReferenceField)) (GenericReferenceField, ReferenceField))))
)
if ( _dereference = _import_class('DeReference')()
instance._initialised
and dereference if (instance._initialised and
and instance._data.get(self.name) dereference and
and not getattr(instance._data[self.name], "_dereferenced", False) instance._data.get(self.name) and
): not getattr(instance._data[self.name], '_dereferenced', False)):
ref_values = instance._data.get(self.name) instance._data[self.name] = _dereference(
instance._data[self.name] = self._lazy_load_refs( instance._data.get(self.name), max_depth=1, instance=instance,
ref_values=ref_values, instance=instance, name=self.name, max_depth=1 name=self.name
) )
if hasattr(instance._data[self.name], "_dereferenced"): if hasattr(instance._data[self.name], '_dereferenced'):
instance._data[self.name]._dereferenced = True instance._data[self.name]._dereferenced = True
value = super().__get__(instance, owner) value = super(ComplexBaseField, self).__get__(instance, owner)
# Convert lists / values so we can watch for any changes on them # Convert lists / values so we can watch for any changes on them
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
if issubclass(type(self), EmbeddedDocumentListField) and not isinstance( if (issubclass(type(self), EmbeddedDocumentListField) and
value, EmbeddedDocumentList not isinstance(value, EmbeddedDocumentList)):
):
value = EmbeddedDocumentList(value, instance, self.name) value = EmbeddedDocumentList(value, instance, self.name)
elif not isinstance(value, BaseList): elif not isinstance(value, BaseList):
value = BaseList(value, instance, self.name) value = BaseList(value, instance, self.name)
@ -327,14 +312,11 @@ class ComplexBaseField(BaseField):
value = BaseDict(value, instance, self.name) value = BaseDict(value, instance, self.name)
instance._data[self.name] = value instance._data[self.name] = value
if ( if (auto_dereference and instance._initialised and
auto_dereference isinstance(value, (BaseList, BaseDict)) and
and instance._initialised not value._dereferenced):
and isinstance(value, (BaseList, BaseDict)) value = _dereference(
and not value._dereferenced value, max_depth=1, instance=instance, name=self.name
):
value = self._lazy_load_refs(
ref_values=value, instance=instance, name=self.name, max_depth=1
) )
value._dereferenced = True value._dereferenced = True
instance._data[self.name] = value instance._data[self.name] = value
@ -343,19 +325,19 @@ class ComplexBaseField(BaseField):
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, str): if isinstance(value, six.string_types):
return value return value
if hasattr(value, "to_python"): if hasattr(value, 'to_python'):
return value.to_python() return value.to_python()
BaseDocument = _import_class("BaseDocument") BaseDocument = _import_class('BaseDocument')
if isinstance(value, BaseDocument): if isinstance(value, BaseDocument):
# Something is wrong, return the value as it is # Something is wrong, return the value as it is
return value return value
is_list = False is_list = False
if not hasattr(value, "items"): if not hasattr(value, 'items'):
try: try:
is_list = True is_list = True
value = {idx: v for idx, v in enumerate(value)} value = {idx: v for idx, v in enumerate(value)}
@ -364,54 +346,50 @@ class ComplexBaseField(BaseField):
if self.field: if self.field:
self.field._auto_dereference = self._auto_dereference self.field._auto_dereference = self._auto_dereference
value_dict = { value_dict = {key: self.field.to_python(item)
key: self.field.to_python(item) for key, item in value.items() for key, item in value.items()}
}
else: else:
Document = _import_class("Document") Document = _import_class('Document')
value_dict = {} value_dict = {}
for k, v in value.items(): for k, v in value.items():
if isinstance(v, Document): if isinstance(v, Document):
# We need the id from the saved object to create the DBRef # We need the id from the saved object to create the DBRef
if v.pk is None: if v.pk is None:
self.error( self.error('You can only reference documents once they'
"You can only reference documents once they" ' have been saved to the database')
" have been saved to the database"
)
collection = v._get_collection_name() collection = v._get_collection_name()
value_dict[k] = DBRef(collection, v.pk) value_dict[k] = DBRef(collection, v.pk)
elif hasattr(v, "to_python"): elif hasattr(v, 'to_python'):
value_dict[k] = v.to_python() value_dict[k] = v.to_python()
else: else:
value_dict[k] = self.to_python(v) value_dict[k] = self.to_python(v)
if is_list: # Convert back to a list if is_list: # Convert back to a list
return [ return [v for _, v in sorted(value_dict.items(),
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) key=operator.itemgetter(0))]
]
return value_dict return value_dict
def to_mongo(self, value, use_db_field=True, fields=None): def to_mongo(self, value, use_db_field=True, fields=None):
"""Convert a Python type to a MongoDB-compatible type.""" """Convert a Python type to a MongoDB-compatible type."""
Document = _import_class("Document") Document = _import_class('Document')
EmbeddedDocument = _import_class("EmbeddedDocument") EmbeddedDocument = _import_class('EmbeddedDocument')
GenericReferenceField = _import_class("GenericReferenceField") GenericReferenceField = _import_class('GenericReferenceField')
if isinstance(value, str): 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(use_db_field, fields) 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__
return val return val
is_list = False is_list = False
if not hasattr(value, "items"): if not hasattr(value, 'items'):
try: try:
is_list = True is_list = True
value = {k: v for k, v in enumerate(value)} value = {k: v for k, v in enumerate(value)}
@ -421,51 +399,48 @@ class ComplexBaseField(BaseField):
if self.field: if self.field:
value_dict = { value_dict = {
key: self.field._to_mongo_safe_call(item, use_db_field, fields) key: self.field._to_mongo_safe_call(item, use_db_field, fields)
for key, item in value.items() for key, item in iteritems(value)
} }
else: else:
value_dict = {} value_dict = {}
for k, v in value.items(): for k, v in iteritems(value):
if isinstance(v, Document): if isinstance(v, Document):
# We need the id from the saved object to create the DBRef # We need the id from the saved object to create the DBRef
if v.pk is None: if v.pk is None:
self.error( self.error('You can only reference documents once they'
"You can only reference documents once they" ' have been saved to the database')
" have been saved to the database"
)
# If its a document that is not inheritable it won't have # If its a document that is not inheritable it won't have
# any _cls data so make it a generic reference allows # any _cls data so make it a generic reference allows
# us to dereference # us to dereference
meta = getattr(v, "_meta", {}) meta = getattr(v, '_meta', {})
allow_inheritance = meta.get("allow_inheritance") allow_inheritance = meta.get('allow_inheritance')
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:
collection = v._get_collection_name() collection = v._get_collection_name()
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(use_db_field, fields) 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, use_db_field, fields) 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 [ return [v for _, v in sorted(value_dict.items(),
v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) key=operator.itemgetter(0))]
]
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, "items"): if hasattr(value, 'iteritems') or hasattr(value, 'items'):
sequence = value.items() sequence = iteritems(value)
else: else:
sequence = enumerate(value) sequence = enumerate(value)
for k, v in sequence: for k, v in sequence:
@ -478,10 +453,11 @@ class ComplexBaseField(BaseField):
if errors: if errors:
field_class = self.field.__class__.__name__ field_class = self.field.__class__.__name__
self.error(f"Invalid {field_class} item ({value})", errors=errors) self.error('Invalid %s item (%s)' % (field_class, value),
errors=errors)
# Don't allow empty values if required # Don't allow empty values if required
if self.required and not value: if self.required and not value:
self.error("Field is required and cannot be empty") self.error('Field is required and cannot be empty')
def prepare_query_value(self, op, value): def prepare_query_value(self, op, value):
return self.to_mongo(value) return self.to_mongo(value)
@ -511,9 +487,10 @@ class ObjectIdField(BaseField):
def to_mongo(self, value): def to_mongo(self, value):
if not isinstance(value, ObjectId): if not isinstance(value, ObjectId):
try: try:
return ObjectId(str(value)) return ObjectId(six.text_type(value))
except Exception as e: except Exception as e:
self.error(str(e)) # e.message attribute has been deprecated since Python 2.6
self.error(six.text_type(e))
return value return value
def prepare_query_value(self, op, value): def prepare_query_value(self, op, value):
@ -521,82 +498,84 @@ class ObjectIdField(BaseField):
def validate(self, value): def validate(self, value):
try: try:
ObjectId(str(value)) ObjectId(six.text_type(value))
except Exception: except Exception:
self.error("Invalid ObjectID") self.error('Invalid Object ID')
class GeoJsonBaseField(BaseField): class GeoJsonBaseField(BaseField):
"""A geo json field storing a geojson style object.""" """A geo json field storing a geojson style object.
.. versionadded:: 0.8
"""
_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().__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()) == {"type", "coordinates"}: if set(value.keys()) == {'type', 'coordinates'}:
if value["type"] != self._type: if value['type'] != self._type:
self.error(f'{self._name} type must be "{self._type}"') self.error('%s type must be "%s"' %
return self.validate(value["coordinates"]) (self._name, self._type))
return self.validate(value['coordinates'])
else: else:
self.error( self.error('%s can only accept a valid GeoJson dictionary'
"%s can only accept a valid GeoJson dictionary" ' or lists of (x, y)' % self._name)
" or lists of (x, y)" % self._name
)
return return
elif not isinstance(value, (list, tuple)): elif not isinstance(value, (list, tuple)):
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)
def _validate_polygon(self, value, top_level=True): def _validate_polygon(self, value, top_level=True):
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
return "Polygons must contain list of linestrings" return 'Polygons must contain list of linestrings'
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0] value[0][0][0]
except (TypeError, IndexError): except (TypeError, IndexError):
return "Invalid Polygon must contain at least one valid linestring" return 'Invalid Polygon must contain at least one valid linestring'
errors = [] errors = []
for val in value: for val in value:
error = self._validate_linestring(val, False) error = self._validate_linestring(val, False)
if not error and val[0] != val[-1]: if not error and val[0] != val[-1]:
error = "LineStrings must start and end at the same point" error = 'LineStrings must start and end at the same point'
if error and error not in errors: if error and error not in errors:
errors.append(error) errors.append(error)
if errors: if errors:
if top_level: if top_level:
return "Invalid Polygon:\n%s" % ", ".join(errors) return 'Invalid Polygon:\n%s' % ', '.join(errors)
else: else:
return "%s" % ", ".join(errors) return '%s' % ', '.join(errors)
def _validate_linestring(self, value, top_level=True): def _validate_linestring(self, value, top_level=True):
"""Validate a linestring.""" """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 (TypeError, IndexError): except (TypeError, IndexError):
return "Invalid LineString must contain at least one valid point" return 'Invalid LineString must contain at least one valid point'
errors = [] errors = []
for val in value: for val in value:
@ -605,30 +584,29 @@ 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 not isinstance( elif (not isinstance(value[0], (float, int)) or
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)):
return "MultiPoint must be a list of Point" return 'MultiPoint must be a list of Point'
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0] value[0][0]
except (TypeError, IndexError): except (TypeError, IndexError):
return "Invalid MultiPoint must contain at least one valid point" return 'Invalid MultiPoint must contain at least one valid point'
errors = [] errors = []
for point in value: for point in value:
@ -637,17 +615,17 @@ 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)):
return "MultiLineString must be a list of LineString" return 'MultiLineString must be a list of LineString'
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0] value[0][0][0]
except (TypeError, IndexError): except (TypeError, IndexError):
return "Invalid MultiLineString must contain at least one valid linestring" return 'Invalid MultiLineString must contain at least one valid linestring'
errors = [] errors = []
for linestring in value: for linestring in value:
@ -657,19 +635,19 @@ 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)):
return "MultiPolygon must be a list of Polygon" return 'MultiPolygon must be a list of Polygon'
# Quick and dirty validator # Quick and dirty validator
try: try:
value[0][0][0][0] value[0][0][0][0]
except (TypeError, IndexError): except (TypeError, IndexError):
return "Invalid MultiPolygon must contain at least one valid Polygon" return 'Invalid MultiPolygon must contain at least one valid Polygon'
errors = [] errors = []
for polygon in value: for polygon in value:
@ -678,9 +656,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,18 @@
import itertools
import warnings import warnings
import six
from six import iteritems, itervalues
from mongoengine.base.common import _document_registry from mongoengine.base.common import _document_registry
from mongoengine.base.fields import ( from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
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.queryset import ( from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
DO_NOTHING,
DoesNotExist,
MultipleObjectsReturned, MultipleObjectsReturned,
QuerySetManager, QuerySetManager)
)
__all__ = ("DocumentMetaclass", "TopLevelDocumentMetaclass")
__all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
class DocumentMetaclass(type): class DocumentMetaclass(type):
@ -25,51 +21,49 @@ class DocumentMetaclass(type):
# TODO lower complexity of this method # TODO lower complexity of this method
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
flattened_bases = mcs._get_bases(bases) flattened_bases = mcs._get_bases(bases)
super_new = super().__new__ super_new = super(DocumentMetaclass, mcs).__new__
# If a base class just call super # If a base class just call super
metaclass = attrs.get("my_metaclass") metaclass = attrs.get('my_metaclass')
if metaclass and issubclass(metaclass, DocumentMetaclass): if metaclass and issubclass(metaclass, DocumentMetaclass):
return super_new(mcs, name, bases, attrs) return super_new(mcs, name, bases, attrs)
attrs["_is_document"] = attrs.get("_is_document", False) attrs['_is_document'] = attrs.get('_is_document', False)
attrs["_cached_reference_fields"] = [] attrs['_cached_reference_fields'] = []
# EmbeddedDocuments could have meta data for inheritance # EmbeddedDocuments could have meta data for inheritance
if "meta" in attrs: if 'meta' in attrs:
attrs["_meta"] = attrs.pop("meta") attrs['_meta'] = attrs.pop('meta')
# EmbeddedDocuments should inherit meta data # EmbeddedDocuments should inherit meta data
if "_meta" not in attrs: if '_meta' not in attrs:
meta = MetaDict() meta = MetaDict()
for base in flattened_bases[::-1]: for base in flattened_bases[::-1]:
# Add any mixin metadata from plain objects # Add any mixin metadata from plain objects
if hasattr(base, "meta"): if hasattr(base, 'meta'):
meta.merge(base.meta) meta.merge(base.meta)
elif hasattr(base, "_meta"): elif hasattr(base, '_meta'):
meta.merge(base._meta) meta.merge(base._meta)
attrs["_meta"] = meta attrs['_meta'] = meta
attrs["_meta"][ attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
"abstract"
] = False # 789: EmbeddedDocument shouldn't inherit abstract
# If allow_inheritance is True, add a "_cls" string field to the attrs # If allow_inheritance is True, add a "_cls" string field to the attrs
if attrs["_meta"].get("allow_inheritance"): if attrs['_meta'].get('allow_inheritance'):
StringField = _import_class("StringField") StringField = _import_class('StringField')
attrs["_cls"] = StringField() attrs['_cls'] = StringField()
# Handle document Fields # Handle document Fields
# Merge all fields from subclasses # Merge all fields from subclasses
doc_fields = {} doc_fields = {}
for base in flattened_bases[::-1]: for base in flattened_bases[::-1]:
if hasattr(base, "_fields"): if hasattr(base, '_fields'):
doc_fields.update(base._fields) doc_fields.update(base._fields)
# Standard object mixin - merge in any Fields # Standard object mixin - merge in any Fields
if not hasattr(base, "_meta"): if not hasattr(base, '_meta'):
base_fields = {} base_fields = {}
for attr_name, attr_value in base.__dict__.items(): for attr_name, attr_value in iteritems(base.__dict__):
if not isinstance(attr_value, BaseField): if not isinstance(attr_value, BaseField):
continue continue
attr_value.name = attr_name attr_value.name = attr_name
@ -81,7 +75,7 @@ class DocumentMetaclass(type):
# Discover any document fields # Discover any document fields
field_names = {} field_names = {}
for attr_name, attr_value in attrs.items(): for attr_name, attr_value in iteritems(attrs):
if not isinstance(attr_value, BaseField): if not isinstance(attr_value, BaseField):
continue continue
attr_value.name = attr_name attr_value.name = attr_name
@ -90,29 +84,27 @@ class DocumentMetaclass(type):
doc_fields[attr_name] = attr_value doc_fields[attr_name] = attr_value
# Count names to ensure no db_field redefinitions # Count names to ensure no db_field redefinitions
field_names[attr_value.db_field] = ( field_names[attr_value.db_field] = field_names.get(
field_names.get(attr_value.db_field, 0) + 1 attr_value.db_field, 0) + 1
)
# 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 " % ", ".join(duplicate_db_fields) msg = ('Multiple db_fields defined for: %s ' %
', '.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"] = { attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
k: getattr(v, "db_field", k) for k, v in doc_fields.items() for k, v in doc_fields.items()}
} attrs['_reverse_db_field_map'] = {
attrs["_reverse_db_field_map"] = { v: k for k, v in attrs['_db_field_map'].items()
v: k for k, v in attrs["_db_field_map"].items()
} }
attrs["_fields_ordered"] = tuple( attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
i[1] (v.creation_counter, v.name)
for i in sorted((v.creation_counter, v.name) for v in doc_fields.values()) for v in itervalues(doc_fields)))
)
# #
# Set document hierarchy # Set document hierarchy
@ -120,34 +112,32 @@ class DocumentMetaclass(type):
superclasses = () superclasses = ()
class_name = [name] class_name = [name]
for base in flattened_bases: for base in flattened_bases:
if not getattr(base, "_is_base_cls", True) and not getattr( if (not getattr(base, '_is_base_cls', True) and
base, "_meta", {} not getattr(base, '_meta', {}).get('abstract', True)):
).get("abstract", True):
# Collate hierarchy for _cls and _subclasses # Collate hierarchy for _cls and _subclasses
class_name.append(base.__name__) class_name.append(base.__name__)
if hasattr(base, "_meta"): if hasattr(base, '_meta'):
# Warn if allow_inheritance isn't set and prevent # Warn if allow_inheritance isn't set and prevent
# inheritance of classes where inheritance is set to False # inheritance of classes where inheritance is set to False
allow_inheritance = base._meta.get("allow_inheritance") allow_inheritance = base._meta.get('allow_inheritance')
if not allow_inheritance and not base._meta.get("abstract"): if not allow_inheritance and not base._meta.get('abstract'):
raise ValueError( raise ValueError('Document %s may not be subclassed. '
"Document %s may not be subclassed. " 'To enable inheritance, use the "allow_inheritance" meta attribute.' %
'To enable inheritance, use the "allow_inheritance" meta attribute.' base.__name__)
% base.__name__
)
# Get superclasses from last base superclass # Get superclasses from last base superclass
document_bases = [b for b in flattened_bases if hasattr(b, "_class_name")] document_bases = [b for b in flattened_bases
if hasattr(b, '_class_name')]
if document_bases: if document_bases:
superclasses = document_bases[0]._superclasses superclasses = document_bases[0]._superclasses
superclasses += (document_bases[0]._class_name,) superclasses += (document_bases[0]._class_name, )
_cls = ".".join(reversed(class_name)) _cls = '.'.join(reversed(class_name))
attrs["_class_name"] = _cls attrs['_class_name'] = _cls
attrs["_superclasses"] = superclasses attrs['_superclasses'] = superclasses
attrs["_subclasses"] = (_cls,) attrs['_subclasses'] = (_cls, )
attrs["_types"] = attrs["_subclasses"] # TODO depreciate _types attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types
# Create the new_class # Create the new_class
new_class = super_new(mcs, name, bases, attrs) new_class = super_new(mcs, name, bases, attrs)
@ -158,12 +148,8 @@ class DocumentMetaclass(type):
base._subclasses += (_cls,) base._subclasses += (_cls,)
base._types = base._subclasses # TODO depreciate _types base._types = base._subclasses # TODO depreciate _types
( (Document, EmbeddedDocument, DictField,
Document, CachedReferenceField) = mcs._import_classes()
EmbeddedDocument,
DictField,
CachedReferenceField,
) = mcs._import_classes()
if issubclass(new_class, Document): if issubclass(new_class, Document):
new_class._collection = None new_class._collection = None
@ -171,50 +157,63 @@ class DocumentMetaclass(type):
# Add class to the _document_registry # Add class to the _document_registry
_document_registry[new_class._class_name] = new_class _document_registry[new_class._class_name] = new_class
# In Python 2, User-defined methods objects have special read-only
# attributes 'im_func' and 'im_self' which contain the function obj
# and class instance object respectively. With Python 3 these special
# attributes have been replaced by __func__ and __self__. The Blinker
# module continues to use im_func and im_self, so the code below
# copies __func__ into im_func and __self__ into im_self for
# classmethod objects in Document derived classes.
if six.PY3:
for val in new_class.__dict__.values():
if isinstance(val, classmethod):
f = val.__get__(new_class)
if hasattr(f, '__func__') and not hasattr(f, 'im_func'):
f.__dict__.update({'im_func': getattr(f, '__func__')})
if hasattr(f, '__self__') and not hasattr(f, 'im_self'):
f.__dict__.update({'im_self': getattr(f, '__self__')})
# Handle delete rules # Handle delete rules
for field in new_class._fields.values(): for field in itervalues(new_class._fields):
f = field f = field
if f.owner_document is None: if f.owner_document is None:
f.owner_document = new_class f.owner_document = new_class
delete_rule = getattr(f, "reverse_delete_rule", DO_NOTHING) delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
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 f.auto_sync: if f.auto_sync:
f.start_listener() f.start_listener()
f.document_type._cached_reference_fields.append(f) f.document_type._cached_reference_fields.append(f)
if isinstance(f, ComplexBaseField) and hasattr(f, "field"): if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
delete_rule = getattr(f.field, "reverse_delete_rule", DO_NOTHING) delete_rule = getattr(f.field,
'reverse_delete_rule',
DO_NOTHING)
if isinstance(f, DictField) and delete_rule != DO_NOTHING: if isinstance(f, DictField) and delete_rule != DO_NOTHING:
msg = ( msg = ('Reverse delete rules are not supported '
"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)
f = field.field f = field.field
if delete_rule != DO_NOTHING: if delete_rule != DO_NOTHING:
if issubclass(new_class, EmbeddedDocument): if issubclass(new_class, EmbeddedDocument):
msg = ( msg = ('Reverse delete rules are not supported for '
"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, field.name, delete_rule) f.document_type.register_delete_rule(new_class,
field.name, delete_rule)
if ( if (field.name and hasattr(Document, field.name) and
field.name EmbeddedDocument not in new_class.mro()):
and hasattr(Document, field.name) msg = ('%s is a document method and not a valid '
and EmbeddedDocument not in new_class.mro() 'field name' % field.name)
):
msg = "%s is a document method and not a valid field name" % field.name
raise InvalidDocumentError(msg) raise InvalidDocumentError(msg)
return new_class return new_class
@ -234,14 +233,15 @@ class DocumentMetaclass(type):
if base is object: if base is object:
continue continue
yield base yield base
yield from mcs.__get_bases(base.__bases__) for child_base in mcs.__get_bases(base.__bases__):
yield child_base
@classmethod @classmethod
def _import_classes(mcs): def _import_classes(mcs):
Document = _import_class("Document") Document = _import_class('Document')
EmbeddedDocument = _import_class("EmbeddedDocument") EmbeddedDocument = _import_class('EmbeddedDocument')
DictField = _import_class("DictField") DictField = _import_class('DictField')
CachedReferenceField = _import_class("CachedReferenceField") CachedReferenceField = _import_class('CachedReferenceField')
return Document, EmbeddedDocument, DictField, CachedReferenceField return Document, EmbeddedDocument, DictField, CachedReferenceField
@ -252,69 +252,68 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
flattened_bases = mcs._get_bases(bases) flattened_bases = mcs._get_bases(bases)
super_new = super().__new__ super_new = super(TopLevelDocumentMetaclass, mcs).__new__
# Set default _meta data if base class, otherwise get user defined meta # Set default _meta data if base class, otherwise get user defined meta
if attrs.get("my_metaclass") == TopLevelDocumentMetaclass: if attrs.get('my_metaclass') == TopLevelDocumentMetaclass:
# defaults # defaults
attrs["_meta"] = { attrs['_meta'] = {
"abstract": True, 'abstract': True,
"max_documents": None, 'max_documents': None,
"max_size": None, 'max_size': None,
"ordering": [], # default ordering applied at runtime 'ordering': [], # default ordering applied at runtime
"indexes": [], # indexes to be ensured at runtime 'indexes': [], # indexes to be ensured at runtime
"id_field": None, 'id_field': None,
"index_background": False, 'index_background': False,
"index_opts": None, 'index_drop_dups': False,
"delete_rules": None, 'index_opts': None,
'delete_rules': None,
# allow_inheritance can be True, False, and None. True means # allow_inheritance can be True, False, and None. True means
# "allow inheritance", False means "don't allow inheritance", # "allow inheritance", False means "don't allow inheritance",
# None means "do whatever your parent does, or don't allow # None means "do whatever your parent does, or don't allow
# inheritance if you're a top-level class". # inheritance if you're a top-level class".
"allow_inheritance": None, 'allow_inheritance': None,
} }
attrs["_is_base_cls"] = True attrs['_is_base_cls'] = True
attrs["_meta"].update(attrs.get("meta", {})) attrs['_meta'].update(attrs.get('meta', {}))
else: else:
attrs["_meta"] = attrs.get("meta", {}) attrs['_meta'] = attrs.get('meta', {})
# Explicitly set abstract to false unless set # Explicitly set abstract to false unless set
attrs["_meta"]["abstract"] = attrs["_meta"].get("abstract", False) attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False)
attrs["_is_base_cls"] = False attrs['_is_base_cls'] = False
# Set flag marking as document class - as opposed to an object mixin # Set flag marking as document class - as opposed to an object mixin
attrs["_is_document"] = True attrs['_is_document'] = True
# Ensure queryset_class is inherited # Ensure queryset_class is inherited
if "objects" in attrs: if 'objects' in attrs:
manager = attrs["objects"] manager = attrs['objects']
if hasattr(manager, "queryset_class"): if hasattr(manager, 'queryset_class'):
attrs["_meta"]["queryset_class"] = manager.queryset_class attrs['_meta']['queryset_class'] = manager.queryset_class
# Clean up top level meta # Clean up top level meta
if "meta" in attrs: if 'meta' in attrs:
del attrs["meta"] del attrs['meta']
# Find the parent document class # Find the parent document class
parent_doc_cls = [ parent_doc_cls = [b for b in flattened_bases
b for b in flattened_bases if b.__class__ == TopLevelDocumentMetaclass if b.__class__ == TopLevelDocumentMetaclass]
]
parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0] parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0]
# Prevent classes setting collection different to their parents # Prevent classes setting collection different to their parents
# If parent wasn't an abstract class # If parent wasn't an abstract class
if ( if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
parent_doc_cls not parent_doc_cls._meta.get('abstract', True)):
and "collection" in attrs.get("_meta", {}) msg = 'Trying to set a collection on a subclass (%s)' % name
and not parent_doc_cls._meta.get("abstract", True)
):
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']
# Ensure abstract documents have abstract bases # Ensure abstract documents have abstract bases
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 not parent_doc_cls._meta.get("abstract", False): if (parent_doc_cls and
msg = "Abstract document cannot have non-abstract base" not parent_doc_cls._meta.get('abstract', False)):
msg = 'Abstract document cannot have non-abstract base'
raise ValueError(msg) raise ValueError(msg)
return super_new(mcs, name, bases, attrs) return super_new(mcs, name, bases, attrs)
@ -323,43 +322,38 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
meta = MetaDict() meta = MetaDict()
for base in flattened_bases[::-1]: for base in flattened_bases[::-1]:
# Add any mixin metadata from plain objects # Add any mixin metadata from plain objects
if hasattr(base, "meta"): if hasattr(base, 'meta'):
meta.merge(base.meta) meta.merge(base.meta)
elif hasattr(base, "_meta"): elif hasattr(base, '_meta'):
meta.merge(base._meta) meta.merge(base._meta)
# Set collection in the meta if its callable # Set collection in the meta if its callable
if getattr(base, "_is_document", False) and not base._meta.get("abstract"): if (getattr(base, '_is_document', False) and
collection = meta.get("collection", None) not base._meta.get('abstract')):
collection = meta.get('collection', None)
if callable(collection): if callable(collection):
meta["collection"] = collection(base) meta['collection'] = collection(base)
meta.merge(attrs.get("_meta", {})) # Top level meta meta.merge(attrs.get('_meta', {})) # Top level meta
# Only simple classes (i.e. direct subclasses of Document) may set # Only simple classes (i.e. direct subclasses of Document) may set
# allow_inheritance to False. If the base Document allows inheritance, # allow_inheritance to False. If the base Document allows inheritance,
# none of its subclasses can override allow_inheritance to False. # none of its subclasses can override allow_inheritance to False.
simple_class = all( simple_class = all([b._meta.get('abstract')
b._meta.get("abstract") for b in flattened_bases if hasattr(b, "_meta") for b in flattened_bases if hasattr(b, '_meta')])
)
if ( if (
not simple_class not simple_class and
and meta["allow_inheritance"] is False meta['allow_inheritance'] is False and
and not meta["abstract"] not meta['abstract']
): ):
raise ValueError( raise ValueError('Only direct subclasses of Document may set '
"Only direct subclasses of Document may set " '"allow_inheritance" to False')
'"allow_inheritance" to False'
)
# Set default collection name # Set default collection name
if "collection" not in meta: if 'collection' not in meta:
meta["collection"] = ( meta['collection'] = ''.join('_%s' % c if c.isupper() else c
"".join("_%s" % c if c.isupper() else c for c in name) for c in name).strip('_').lower()
.strip("_") attrs['_meta'] = meta
.lower()
)
attrs["_meta"] = meta
# Call super and get the new class # Call super and get the new class
new_class = super_new(mcs, name, bases, attrs) new_class = super_new(mcs, name, bases, attrs)
@ -367,84 +361,71 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
meta = new_class._meta meta = new_class._meta
# Set index specifications # Set index specifications
meta["index_specs"] = new_class._build_index_specs(meta["indexes"]) meta['index_specs'] = new_class._build_index_specs(meta['indexes'])
# If collection is a callable - call it and set the value # If collection is a callable - call it and set the value
collection = meta.get("collection") collection = meta.get('collection')
if callable(collection): if callable(collection):
new_class._meta["collection"] = collection(new_class) new_class._meta['collection'] = collection(new_class)
# Provide a default queryset unless exists or one has been set # Provide a default queryset unless exists or one has been set
if "objects" not in dir(new_class): if 'objects' not in dir(new_class):
new_class.objects = QuerySetManager() new_class.objects = QuerySetManager()
# Validate the fields and set primary key if needed # Validate the fields and set primary key if needed
for field_name, field in new_class._fields.items(): for field_name, field in iteritems(new_class._fields):
if field.primary_key: if field.primary_key:
# Ensure only one primary key is set # Ensure only one primary key is set
current_pk = new_class._meta.get("id_field") current_pk = new_class._meta.get('id_field')
if current_pk and current_pk != field_name: if current_pk and current_pk != field_name:
raise ValueError("Cannot override primary key field") raise ValueError('Cannot override primary key field')
# Set primary key # Set primary key
if not current_pk: if not current_pk:
new_class._meta["id_field"] = field_name new_class._meta['id_field'] = field_name
new_class.id = field new_class.id = field
# If the document doesn't explicitly define a primary key field, create # Set primary key if not defined by the document
# one. Make it an ObjectIdField and give it a non-clashing name ("id" new_class._auto_id_field = getattr(parent_doc_cls,
# by default, but can be different if that one's taken). '_auto_id_field', False)
if not new_class._meta.get("id_field"): if not new_class._meta.get('id_field'):
# After 0.10, find not existing names, instead of overwriting
id_name, id_db_name = mcs.get_auto_id_names(new_class) id_name, id_db_name = mcs.get_auto_id_names(new_class)
new_class._meta["id_field"] = id_name new_class._auto_id_field = True
new_class._meta['id_field'] = id_name
new_class._fields[id_name] = ObjectIdField(db_field=id_db_name) new_class._fields[id_name] = ObjectIdField(db_field=id_db_name)
new_class._fields[id_name].name = id_name new_class._fields[id_name].name = id_name
new_class.id = new_class._fields[id_name] new_class.id = new_class._fields[id_name]
new_class._db_field_map[id_name] = id_db_name new_class._db_field_map[id_name] = id_db_name
new_class._reverse_db_field_map[id_db_name] = id_name new_class._reverse_db_field_map[id_db_name] = id_name
# Prepend id field to _fields_ordered
new_class._fields_ordered = (id_name, ) + new_class._fields_ordered
# Prepend the ID field to _fields_ordered (so that it's *always* # Merge in exceptions with parent hierarchy
# the first field).
new_class._fields_ordered = (id_name,) + new_class._fields_ordered
# Merge in exceptions with parent hierarchy.
exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned) exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
module = attrs.get("__module__") module = attrs.get('__module__')
for exc in exceptions_to_merge: for exc in exceptions_to_merge:
name = exc.__name__ name = exc.__name__
parents = tuple( parents = tuple(getattr(base, name) for base in flattened_bases
getattr(base, name) for base in flattened_bases if hasattr(base, name) if hasattr(base, name)) or (exc,)
) or (exc,) # Create new exception and set to new_class
exception = type(name, parents, {'__module__': module})
# Create a new exception and set it as an attribute on the new
# class.
exception = type(name, parents, {"__module__": module})
setattr(new_class, name, exception) setattr(new_class, name, exception)
return new_class return new_class
@classmethod @classmethod
def get_auto_id_names(mcs, new_class): def get_auto_id_names(mcs, new_class):
"""Find a name for the automatic ID field for the given new class. id_name, id_db_name = ('id', '_id')
if id_name not in new_class._fields and \
Return a two-element tuple where the first item is the field name (i.e. id_db_name not in (v.db_field for v in new_class._fields.values()):
the attribute name on the object) and the second element is the DB
field name (i.e. the name of the key stored in MongoDB).
Defaults to ('id', '_id'), or generates a non-clashing name in the form
of ('auto_id_X', '_auto_id_X') if the default name is already taken.
"""
id_name, id_db_name = ("id", "_id")
existing_fields = {field_name for field_name in new_class._fields}
existing_db_fields = {v.db_field for v in new_class._fields.values()}
if id_name not in existing_fields and id_db_name not in existing_db_fields:
return id_name, id_db_name return id_name, id_db_name
id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0
id_basename, id_db_basename, i = ("auto_id", "_auto_id", 0) while id_name in new_class._fields or \
for i in itertools.count(): id_db_name in (v.db_field for v in new_class._fields.values()):
id_name = f"{id_basename}_{i}" id_name = '{0}_{1}'.format(id_basename, i)
id_db_name = f"{id_db_basename}_{i}" id_db_name = '{0}_{1}'.format(id_db_basename, i)
if id_name not in existing_fields and id_db_name not in existing_db_fields: i += 1
return id_name, id_db_name return id_name, id_db_name
@ -452,11 +433,10 @@ class MetaDict(dict):
"""Custom dictionary for meta classes. """Custom dictionary for meta classes.
Handles the merging of set indexes Handles the merging of set indexes
""" """
_merge_options = ('indexes',)
_merge_options = ("indexes",)
def merge(self, new_options): def merge(self, new_options):
for k, v in new_options.items(): for k, v in iteritems(new_options):
if k in self._merge_options: if k in self._merge_options:
self[k] = self.get(k, []) + v self[k] = self.get(k, []) + v
else: else:
@ -465,5 +445,4 @@ class MetaDict(dict):
class BasesTuple(tuple): class BasesTuple(tuple):
"""Special class to handle introspection of bases tuple in __new__""" """Special class to handle introspection of bases tuple in __new__"""
pass pass

View File

@ -1,7 +1,7 @@
import re import re
class LazyRegexCompiler: class LazyRegexCompiler(object):
"""Descriptor to allow lazy compilation of regex""" """Descriptor to allow lazy compilation of regex"""
def __init__(self, pattern, flags=0): def __init__(self, pattern, flags=0):

View File

@ -19,44 +19,34 @@ def _import_class(cls_name):
if cls_name in _class_registry_cache: if cls_name in _class_registry_cache:
return _class_registry_cache.get(cls_name) return _class_registry_cache.get(cls_name)
doc_classes = ( doc_classes = ('Document', 'DynamicEmbeddedDocument', 'EmbeddedDocument',
"Document", 'MapReduceDocument')
"DynamicEmbeddedDocument",
"EmbeddedDocument",
"MapReduceDocument",
)
# Field Classes # Field Classes
if not _field_list_cache: if not _field_list_cache:
from mongoengine.fields import __all__ as fields from mongoengine.fields import __all__ as fields
_field_list_cache.extend(fields) _field_list_cache.extend(fields)
from mongoengine.base.fields import __all__ as fields from mongoengine.base.fields import __all__ as fields
_field_list_cache.extend(fields) _field_list_cache.extend(fields)
field_classes = _field_list_cache field_classes = _field_list_cache
deref_classes = ("DeReference",) deref_classes = ('DeReference',)
if cls_name == "BaseDocument": if cls_name == 'BaseDocument':
from mongoengine.base import document as module from mongoengine.base import document as module
import_classes = ['BaseDocument']
import_classes = ["BaseDocument"]
elif cls_name in doc_classes: 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:
from mongoengine import fields as module from mongoengine import fields as module
import_classes = field_classes import_classes = field_classes
elif cls_name in deref_classes: elif cls_name in deref_classes:
from mongoengine import dereference as module from mongoengine import dereference as module
import_classes = deref_classes import_classes = deref_classes
else: else:
raise ValueError("No import set for: %s" % cls_name) raise ValueError('No import set for: %s' % cls_name)
for cls in import_classes: for cls in import_classes:
_class_registry_cache[cls] = getattr(module, cls) _class_registry_cache[cls] = getattr(module, cls)

View File

@ -1,22 +1,23 @@
from pymongo import MongoClient, ReadPreference, uri_parser from pymongo import MongoClient, ReadPreference, uri_parser
from pymongo.database import _check_name from pymongo.database import _check_name
import six
__all__ = [ __all__ = [
"DEFAULT_CONNECTION_NAME", 'DEFAULT_CONNECTION_NAME',
"DEFAULT_DATABASE_NAME", 'DEFAULT_DATABASE_NAME',
"ConnectionFailure", 'MongoEngineConnectionError',
"connect", 'connect',
"disconnect", 'disconnect',
"disconnect_all", 'disconnect_all',
"get_connection", 'get_connection',
"get_db", 'get_db',
"register_connection", 'register_connection',
] ]
DEFAULT_CONNECTION_NAME = "default" DEFAULT_CONNECTION_NAME = 'default'
DEFAULT_DATABASE_NAME = "test" DEFAULT_DATABASE_NAME = 'test'
DEFAULT_HOST = "localhost" DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 27017 DEFAULT_PORT = 27017
_connection_settings = {} _connection_settings = {}
@ -26,11 +27,10 @@ _dbs = {}
READ_PREFERENCE = ReadPreference.PRIMARY READ_PREFERENCE = ReadPreference.PRIMARY
class ConnectionFailure(Exception): class MongoEngineConnectionError(Exception):
"""Error raised when the database connection can't be established or """Error raised when the database connection can't be established or
when a connection with a requested alias can't be retrieved. when a connection with a requested alias can't be retrieved.
""" """
pass pass
@ -38,99 +38,88 @@ def _check_db_name(name):
"""Check if a database name is valid. """Check if a database name is valid.
This functionality is copied from pymongo Database class constructor. This functionality is copied from pymongo Database class constructor.
""" """
if not isinstance(name, str): if not isinstance(name, six.string_types):
raise TypeError("name must be an instance of %s" % str) raise TypeError('name must be an instance of %s' % six.string_types)
elif name != "$external": elif name != '$external':
_check_name(name) _check_name(name)
def _get_connection_settings( def _get_connection_settings(
db=None, db=None, name=None, host=None, port=None,
name=None,
host=None,
port=None,
read_preference=READ_PREFERENCE, read_preference=READ_PREFERENCE,
username=None, username=None, password=None,
password=None,
authentication_source=None, authentication_source=None,
authentication_mechanism=None, authentication_mechanism=None,
**kwargs, **kwargs):
):
"""Get the connection settings as a dict """Get the connection settings as a dict
:param db: the name of the database to use, for compatibility with connect : param db: the name of the database to use, for compatibility with connect
:param name: the name of the specific database to use : param name: the name of the specific database to use
:param host: the host name of the: program: `mongod` instance to connect to : param host: the host name of the: program: `mongod` instance to connect to
:param port: the port that the: program: `mongod` instance is running on : param port: the port that the: program: `mongod` instance is running on
:param read_preference: The read preference for the collection : param read_preference: The read preference for the collection
:param username: username to authenticate with : param username: username to authenticate with
:param password: password to authenticate with : param password: password to authenticate with
:param authentication_source: database to authenticate against : param authentication_source: database to authenticate against
:param authentication_mechanism: database authentication mechanisms. : param authentication_mechanism: database authentication mechanisms.
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later, By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
MONGODB-CR (MongoDB Challenge Response protocol) for older servers. MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
:param is_mock: explicitly use mongomock for this connection : param is_mock: explicitly use mongomock for this connection
(can also be done by using `mongomock: // ` as db host prefix) (can also be done by using `mongomock: // ` as db host prefix)
:param kwargs: ad-hoc parameters to be passed into the pymongo driver, : param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list. for pymongo's `MongoClient` for a full list.
.. versionchanged:: 0.10.6 - added mongomock support
""" """
conn_settings = { conn_settings = {
"name": name or db or DEFAULT_DATABASE_NAME, 'name': name or db or DEFAULT_DATABASE_NAME,
"host": host or DEFAULT_HOST, 'host': host or DEFAULT_HOST,
"port": port or DEFAULT_PORT, 'port': port or DEFAULT_PORT,
"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, 'authentication_mechanism': authentication_mechanism
} }
_check_db_name(conn_settings["name"]) _check_db_name(conn_settings['name'])
conn_host = conn_settings["host"] conn_host = conn_settings['host']
# Host can be a list or a string, so if string, force to a list. # Host can be a list or a string, so if string, force to a list.
if isinstance(conn_host, str): if isinstance(conn_host, six.string_types):
conn_host = [conn_host] conn_host = [conn_host]
resolved_hosts = [] resolved_hosts = []
for entity in conn_host: for entity in conn_host:
# Handle Mongomock # Handle Mongomock
if entity.startswith("mongomock://"): if entity.startswith('mongomock://'):
conn_settings["is_mock"] = True conn_settings['is_mock'] = True
# `mongomock://` is not a valid url prefix and must be replaced by `mongodb://` # `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
new_entity = entity.replace("mongomock://", "mongodb://", 1) resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
resolved_hosts.append(new_entity)
uri_dict = uri_parser.parse_uri(new_entity)
database = uri_dict.get("database")
if database:
conn_settings["name"] = database
# Handle URI style connections, only updating connection params which # Handle URI style connections, only updating connection params which
# were explicitly specified in the URI. # were explicitly specified in the URI.
elif "://" in entity: elif '://' in entity:
uri_dict = uri_parser.parse_uri(entity) uri_dict = uri_parser.parse_uri(entity)
resolved_hosts.append(entity) resolved_hosts.append(entity)
database = uri_dict.get("database") if uri_dict.get('database'):
if database: conn_settings['name'] = uri_dict.get('database')
conn_settings["name"] = database
for param in ("read_preference", "username", "password"): for param in ('read_preference', 'username', 'password'):
if uri_dict.get(param): if uri_dict.get(param):
conn_settings[param] = uri_dict[param] conn_settings[param] = uri_dict[param]
uri_options = uri_dict["options"] uri_options = uri_dict['options']
if "replicaset" in uri_options: if 'replicaset' in uri_options:
conn_settings["replicaSet"] = uri_options["replicaset"] conn_settings['replicaSet'] = 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: if 'authmechanism' in uri_options:
conn_settings["authentication_mechanism"] = uri_options["authmechanism"] conn_settings['authentication_mechanism'] = uri_options['authmechanism']
if "readpreference" in uri_options: if 'readpreference' in uri_options:
read_preferences = ( read_preferences = (
ReadPreference.NEAREST, ReadPreference.NEAREST,
ReadPreference.PRIMARY, ReadPreference.PRIMARY,
@ -144,80 +133,71 @@ def _get_connection_settings(
# int (e.g. 3). # int (e.g. 3).
# TODO simplify the code below once we drop support for # TODO simplify the code below once we drop support for
# PyMongo v3.4. # PyMongo v3.4.
read_pf_mode = uri_options["readpreference"] read_pf_mode = uri_options['readpreference']
if isinstance(read_pf_mode, str): if isinstance(read_pf_mode, six.string_types):
read_pf_mode = read_pf_mode.lower() read_pf_mode = read_pf_mode.lower()
for preference in read_preferences: for preference in read_preferences:
if ( if (
preference.name.lower() == read_pf_mode preference.name.lower() == read_pf_mode or
or preference.mode == read_pf_mode preference.mode == read_pf_mode
): ):
conn_settings["read_preference"] = preference conn_settings['read_preference'] = preference
break break
else: else:
resolved_hosts.append(entity) resolved_hosts.append(entity)
conn_settings["host"] = resolved_hosts 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)
kwargs.pop("is_slave", None) kwargs.pop('is_slave', None)
conn_settings.update(kwargs) conn_settings.update(kwargs)
return conn_settings return conn_settings
def register_connection( def register_connection(alias, db=None, name=None, host=None, port=None,
alias,
db=None,
name=None,
host=None,
port=None,
read_preference=READ_PREFERENCE, read_preference=READ_PREFERENCE,
username=None, username=None, password=None,
password=None,
authentication_source=None, authentication_source=None,
authentication_mechanism=None, authentication_mechanism=None,
**kwargs, **kwargs):
):
"""Register the connection settings. """Register the connection settings.
:param alias: the name that will be used to refer to this connection throughout MongoEngine : param alias: the name that will be used to refer to this connection
:param db: the name of the database to use, for compatibility with connect throughout MongoEngine
:param name: the name of the specific database to use : param name: the name of the specific database to use
:param host: the host name of the: program: `mongod` instance to connect to : param db: the name of the database to use, for compatibility with connect
:param port: the port that the: program: `mongod` instance is running on : param host: the host name of the: program: `mongod` instance to connect to
:param read_preference: The read preference for the collection : param port: the port that the: program: `mongod` instance is running on
:param username: username to authenticate with : param read_preference: The read preference for the collection
:param password: password to authenticate with : param username: username to authenticate with
:param authentication_source: database to authenticate against : param password: password to authenticate with
:param authentication_mechanism: database authentication mechanisms. : param authentication_source: database to authenticate against
: param authentication_mechanism: database authentication mechanisms.
By default, use SCRAM-SHA-1 with MongoDB 3.0 and later, By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
MONGODB-CR (MongoDB Challenge Response protocol) for older servers. MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
:param is_mock: explicitly use mongomock for this connection : param is_mock: explicitly use mongomock for this connection
(can also be done by using `mongomock: // ` as db host prefix) (can also be done by using `mongomock: // ` as db host prefix)
:param kwargs: ad-hoc parameters to be passed into the pymongo driver, : param kwargs: ad-hoc parameters to be passed into the pymongo driver,
for example maxpoolsize, tz_aware, etc. See the documentation for example maxpoolsize, tz_aware, etc. See the documentation
for pymongo's `MongoClient` for a full list. for pymongo's `MongoClient` for a full list.
.. versionchanged:: 0.10.6 - added mongomock support
""" """
conn_settings = _get_connection_settings( conn_settings = _get_connection_settings(
db=db, db=db, name=name, host=host, port=port,
name=name,
host=host,
port=port,
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, authentication_mechanism=authentication_mechanism,
**kwargs, **kwargs)
)
_connection_settings[alias] = conn_settings _connection_settings[alias] = conn_settings
def disconnect(alias=DEFAULT_CONNECTION_NAME): def disconnect(alias=DEFAULT_CONNECTION_NAME):
"""Close the connection with a given alias.""" """Close the connection with a given alias."""
from mongoengine import Document
from mongoengine.base.common import _get_documents_by_db from mongoengine.base.common import _get_documents_by_db
from mongoengine import Document
if alias in _connections: if alias in _connections:
get_connection(alias=alias).close() get_connection(alias=alias).close()
@ -254,24 +234,22 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
return _connections[alias] return _connections[alias]
# Validate that the requested alias exists in the _connection_settings. # Validate that the requested alias exists in the _connection_settings.
# Raise ConnectionFailure if it doesn't. # Raise MongoEngineConnectionError if it doesn't.
if alias not in _connection_settings: if alias not in _connection_settings:
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'
else: else:
msg = 'Connection with alias "%s" has not been defined' % alias msg = 'Connection with alias "%s" has not been defined' % alias
raise ConnectionFailure(msg) raise MongoEngineConnectionError(msg)
def _clean_settings(settings_dict): def _clean_settings(settings_dict):
irrelevant_fields_set = { irrelevant_fields_set = {
"name", 'name', 'username', 'password',
"username", 'authentication_source', 'authentication_mechanism'
"password",
"authentication_source",
"authentication_mechanism",
} }
return { return {
k: v for k, v in settings_dict.items() if k not in irrelevant_fields_set k: v for k, v in settings_dict.items()
if k not in irrelevant_fields_set
} }
raw_conn_settings = _connection_settings[alias].copy() raw_conn_settings = _connection_settings[alias].copy()
@ -282,12 +260,13 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
conn_settings = _clean_settings(raw_conn_settings) conn_settings = _clean_settings(raw_conn_settings)
# Determine if we should use PyMongo's or mongomock's MongoClient. # Determine if we should use PyMongo's or mongomock's MongoClient.
is_mock = conn_settings.pop("is_mock", False) is_mock = conn_settings.pop('is_mock', False)
if is_mock: if is_mock:
try: try:
import mongomock import mongomock
except ImportError: except ImportError:
raise RuntimeError("You need mongomock installed to mock MongoEngine.") raise RuntimeError('You need mongomock installed to mock '
'MongoEngine.')
connection_class = mongomock.MongoClient connection_class = mongomock.MongoClient
else: else:
connection_class = MongoClient connection_class = MongoClient
@ -298,7 +277,9 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
connection = existing_connection connection = existing_connection
else: else:
connection = _create_connection( connection = _create_connection(
alias=alias, connection_class=connection_class, **conn_settings alias=alias,
connection_class=connection_class,
**conn_settings
) )
_connections[alias] = connection _connections[alias] = connection
return _connections[alias] return _connections[alias]
@ -307,12 +288,13 @@ def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
def _create_connection(alias, connection_class, **connection_settings): def _create_connection(alias, connection_class, **connection_settings):
""" """
Create the new connection for this alias. Raise Create the new connection for this alias. Raise
ConnectionFailure if it can't be established. MongoEngineConnectionError if it can't be established.
""" """
try: try:
return connection_class(**connection_settings) return connection_class(**connection_settings)
except Exception as e: except Exception as e:
raise ConnectionFailure(f"Cannot connect to database {alias} :\n{e}") raise MongoEngineConnectionError(
'Cannot connect to database %s :\n%s' % (alias, e))
def _find_existing_connection(connection_settings): def _find_existing_connection(connection_settings):
@ -334,7 +316,7 @@ def _find_existing_connection(connection_settings):
# Only remove the name but it's important to # Only remove the name but it's important to
# keep the username/password/authentication_source/authentication_mechanism # keep the username/password/authentication_source/authentication_mechanism
# to identify if the connection could be shared (cfr https://github.com/MongoEngine/mongoengine/issues/2047) # to identify if the connection could be shared (cfr https://github.com/MongoEngine/mongoengine/issues/2047)
return {k: v for k, v in settings_dict.items() if k != "name"} return {k: v for k, v in settings_dict.items() if k != 'name'}
cleaned_conn_settings = _clean_settings(connection_settings) cleaned_conn_settings = _clean_settings(connection_settings)
for db_alias, connection_settings in connection_settings_bis: for db_alias, connection_settings in connection_settings_bis:
@ -350,18 +332,14 @@ def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
if alias not in _dbs: if alias not in _dbs:
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"]} auth_kwargs = {'source': conn_settings['authentication_source']}
if conn_settings["authentication_mechanism"] is not None: if conn_settings['authentication_mechanism'] is not None:
auth_kwargs["mechanism"] = conn_settings["authentication_mechanism"] auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
# Authenticate if necessary # Authenticate if necessary
if conn_settings["username"] and ( if conn_settings['username'] and (conn_settings['password'] or
conn_settings["password"] conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
or conn_settings["authentication_mechanism"] == "MONGODB-X509" db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
):
db.authenticate(
conn_settings["username"], conn_settings["password"], **auth_kwargs
)
_dbs[alias] = db _dbs[alias] = db
return _dbs[alias] return _dbs[alias]
@ -381,6 +359,8 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
See the docstring for `register_connection` for more details about all See the docstring for `register_connection` for more details about all
supported kwargs. supported kwargs.
.. versionchanged:: 0.6 - added multiple database support.
""" """
if alias in _connections: if alias in _connections:
prev_conn_setting = _connection_settings[alias] prev_conn_setting = _connection_settings[alias]
@ -388,10 +368,10 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
if new_conn_settings != prev_conn_setting: if new_conn_settings != prev_conn_setting:
err_msg = ( err_msg = (
"A different connection with alias `{}` was already " u'A different connection with alias `{}` was already '
"registered. Use disconnect() first" u'registered. Use disconnect() first'
).format(alias) ).format(alias)
raise ConnectionFailure(err_msg) raise MongoEngineConnectionError(err_msg)
else: else:
register_connection(alias, db, **kwargs) register_connection(alias, db, **kwargs)

View File

@ -1,24 +1,17 @@
from contextlib import contextmanager from contextlib import contextmanager
from pymongo.read_concern import ReadConcern
from pymongo.write_concern import WriteConcern from pymongo.write_concern import WriteConcern
from six import iteritems
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
from mongoengine.pymongo_support import count_documents from mongoengine.pymongo_support import count_documents
__all__ = ( __all__ = ('switch_db', 'switch_collection', 'no_dereference',
"switch_db", 'no_sub_classes', 'query_counter', 'set_write_concern')
"switch_collection",
"no_dereference",
"no_sub_classes",
"query_counter",
"set_write_concern",
"set_read_write_concern",
)
class switch_db: class switch_db(object):
"""switch_db alias context manager. """switch_db alias context manager.
Example :: Example ::
@ -45,21 +38,21 @@ class switch_db:
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: class switch_collection(object):
"""switch_collection alias context manager. """switch_collection alias context manager.
Example :: Example ::
@ -101,7 +94,7 @@ class switch_collection:
self.cls._get_collection_name = self.ori_get_collection_name self.cls._get_collection_name = self.ori_get_collection_name
class no_dereference: 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
@ -118,15 +111,14 @@ class no_dereference:
""" """
self.cls = cls self.cls = cls
ReferenceField = _import_class("ReferenceField") ReferenceField = _import_class('ReferenceField')
GenericReferenceField = _import_class("GenericReferenceField") GenericReferenceField = _import_class('GenericReferenceField')
ComplexBaseField = _import_class("ComplexBaseField") ComplexBaseField = _import_class('ComplexBaseField')
self.deref_fields = [ self.deref_fields = [k for k, v in iteritems(self.cls._fields)
k if isinstance(v, (ReferenceField,
for k, v in self.cls._fields.items() GenericReferenceField,
if isinstance(v, (ReferenceField, GenericReferenceField, ComplexBaseField)) ComplexBaseField))]
]
def __enter__(self): def __enter__(self):
"""Change the objects default and _auto_dereference values.""" """Change the objects default and _auto_dereference values."""
@ -141,7 +133,7 @@ class no_dereference:
return self.cls return self.cls
class no_sub_classes: 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::
@ -169,44 +161,34 @@ class no_sub_classes:
self.cls._subclasses = self.cls_initial_subclasses self.cls._subclasses = self.cls_initial_subclasses
class query_counter: class query_counter(object):
"""Query_counter context manager to get the number of queries. """Query_counter context manager to get the number of queries.
This works by updating the `profiling_level` of the database so that all queries get logged, This works by updating the `profiling_level` of the database so that all queries get logged,
resetting the db.system.profile collection at the beginning of the context and counting the new entries. resetting the db.system.profile collection at the beginnig of the context and counting the new entries.
This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes This was designed for debugging purpose. In fact it is a global counter so queries issued by other threads/processes
can interfere with it can interfere with it
Usage:
.. code-block:: python
class User(Document):
name = StringField()
with query_counter() as q:
user = User(name='Bob')
assert q == 0 # no query fired yet
user.save()
assert q == 1 # 1 query was fired, an 'insert'
user_bis = User.objects().first()
assert q == 2 # a 2nd query was fired, a 'find_one'
Be aware that: Be aware that:
- Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of
- Iterating over large amount of documents (>101) makes pymongo issue `getmore` queries to fetch the next batch of documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches) documents (https://docs.mongodb.com/manual/tutorial/iterate-a-cursor/#cursor-batches)
- Some queries are ignored by default by the counter (killcursors, db.system.indexes) - Some queries are ignored by default by the counter (killcursors, db.system.indexes)
""" """
def __init__(self, alias=DEFAULT_CONNECTION_NAME): def __init__(self):
self.db = get_db(alias=alias) """Construct the query_counter
"""
self.db = get_db()
self.initial_profiling_level = None self.initial_profiling_level = None
self._ctx_query_counter = 0 # number of queries issued by the context self._ctx_query_counter = 0 # number of queries issued by the context
self._ignored_query = { self._ignored_query = {
"ns": {"$ne": "%s.system.indexes" % self.db.name}, 'ns':
"op": {"$ne": "killcursors"}, # MONGODB < 3.2 {'$ne': '%s.system.indexes' % self.db.name},
"command.killCursors": {"$exists": False}, # MONGODB >= 3.2 'op': # MONGODB < 3.2
{'$ne': 'killcursors'},
'command.killCursors': # MONGODB >= 3.2
{'$exists': False}
} }
def _turn_on_profiling(self): def _turn_on_profiling(self):
@ -249,20 +231,15 @@ class query_counter:
def __repr__(self): def __repr__(self):
"""repr query_counter as the number of queries.""" """repr query_counter as the number of queries."""
return "%s" % self._get_count() return u"%s" % self._get_count()
def _get_count(self): def _get_count(self):
"""Get the number of queries by counting the current number of entries in db.system.profile """Get the number of queries by counting the current number of entries in db.system.profile
and substracting the queries issued by this context. In fact everytime this is called, 1 query is and substracting the queries issued by this context. In fact everytime this is called, 1 query is
issued so we need to balance that issued so we need to balance that
""" """
count = ( count = count_documents(self.db.system.profile, self._ignored_query) - self._ctx_query_counter
count_documents(self.db.system.profile, self._ignored_query) self._ctx_query_counter += 1 # Account for the query we just issued to gather the information
- self._ctx_query_counter
)
self._ctx_query_counter += (
1 # Account for the query we just issued to gather the information
)
return count return count
@ -271,21 +248,3 @@ def set_write_concern(collection, write_concerns):
combined_concerns = dict(collection.write_concern.document.items()) combined_concerns = dict(collection.write_concern.document.items())
combined_concerns.update(write_concerns) combined_concerns.update(write_concerns)
yield collection.with_options(write_concern=WriteConcern(**combined_concerns)) yield collection.with_options(write_concern=WriteConcern(**combined_concerns))
@contextmanager
def set_read_write_concern(collection, write_concerns, read_concerns):
combined_write_concerns = dict(collection.write_concern.document.items())
if write_concerns is not None:
combined_write_concerns.update(write_concerns)
combined_read_concerns = dict(collection.read_concern.document.items())
if read_concerns is not None:
combined_read_concerns.update(read_concerns)
yield collection.with_options(
write_concern=WriteConcern(**combined_write_concerns),
read_concern=ReadConcern(**combined_read_concerns),
)

View File

@ -1,25 +1,17 @@
from bson import SON, DBRef from bson import DBRef, SON
import six
from six import iteritems
from mongoengine.base import ( from mongoengine.base import (BaseDict, BaseList, EmbeddedDocumentList,
BaseDict, TopLevelDocumentMetaclass, get_document)
BaseList,
EmbeddedDocumentList,
TopLevelDocumentMetaclass,
get_document,
)
from mongoengine.base.datastructures import LazyReference from mongoengine.base.datastructures import LazyReference
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.document import Document, EmbeddedDocument from mongoengine.document import Document, EmbeddedDocument
from mongoengine.fields import ( from mongoengine.fields import DictField, ListField, MapField, ReferenceField
DictField,
ListField,
MapField,
ReferenceField,
)
from mongoengine.queryset import QuerySet from mongoengine.queryset import QuerySet
class DeReference: class DeReference(object):
def __call__(self, items, max_depth=1, instance=None, name=None): def __call__(self, items, max_depth=1, instance=None, name=None):
""" """
Cheaply dereferences the items to a set depth. Cheaply dereferences the items to a set depth.
@ -33,7 +25,7 @@ class DeReference:
: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, str): 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
@ -44,23 +36,21 @@ class DeReference:
self.max_depth = max_depth self.max_depth = max_depth
doc_type = None doc_type = None
if instance and isinstance( if instance and isinstance(instance, (Document, EmbeddedDocument,
instance, (Document, EmbeddedDocument, TopLevelDocumentMetaclass) TopLevelDocumentMetaclass)):
):
doc_type = instance._fields.get(name) doc_type = instance._fields.get(name)
while hasattr(doc_type, "field"): while hasattr(doc_type, 'field'):
doc_type = doc_type.field doc_type = doc_type.field
if isinstance(doc_type, ReferenceField): if isinstance(doc_type, ReferenceField):
field = doc_type field = doc_type
doc_type = doc_type.document_type doc_type = doc_type.document_type
is_list = not hasattr(items, "items") is_list = not hasattr(items, 'items')
if is_list and all(i.__class__ == doc_type for i in items): if is_list and all([i.__class__ == doc_type for i in items]):
return items return items
elif not is_list and all( elif not is_list and all(
i.__class__ == doc_type for i in items.values() [i.__class__ == doc_type for i in items.values()]):
):
return items return items
elif not field.dbref: elif not field.dbref:
# We must turn the ObjectIds into DBRefs # We must turn the ObjectIds into DBRefs
@ -82,7 +72,7 @@ class DeReference:
def _get_items_from_dict(items): def _get_items_from_dict(items):
new_items = {} new_items = {}
for k, v in items.items(): for k, v in iteritems(items):
value = v value = v
if isinstance(v, list): if isinstance(v, list):
value = _get_items_from_list(v) value = _get_items_from_list(v)
@ -93,7 +83,7 @@ class DeReference:
new_items[k] = value new_items[k] = value
return new_items return new_items
if not hasattr(items, "items"): if not hasattr(items, 'items'):
items = _get_items_from_list(items) items = _get_items_from_list(items)
else: else:
items = _get_items_from_dict(items) items = _get_items_from_dict(items)
@ -123,26 +113,20 @@ class DeReference:
depth += 1 depth += 1
for item in iterator: for item in iterator:
if isinstance(item, (Document, EmbeddedDocument)): if isinstance(item, (Document, EmbeddedDocument)):
for field_name, field in item._fields.items(): for field_name, field in iteritems(item._fields):
v = item._data.get(field_name, None) v = item._data.get(field_name, None)
if isinstance(v, LazyReference): if isinstance(v, LazyReference):
# LazyReference inherits DBRef but should not be dereferenced here ! # LazyReference inherits DBRef but should not be dereferenced here !
continue continue
elif isinstance(v, DBRef): elif isinstance(v, DBRef):
reference_map.setdefault(field.document_type, set()).add(v.id) reference_map.setdefault(field.document_type, set()).add(v.id)
elif isinstance(v, (dict, SON)) and "_ref" in v: elif isinstance(v, (dict, SON)) and '_ref' in v:
reference_map.setdefault(get_document(v["_cls"]), set()).add( reference_map.setdefault(get_document(v['_cls']), set()).add(v['_ref'].id)
v["_ref"].id
)
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth: elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
field_cls = getattr( field_cls = getattr(getattr(field, 'field', None), 'document_type', None)
getattr(field, "field", None), "document_type", None
)
references = self._find_references(v, depth) references = self._find_references(v, depth)
for key, refs in references.items(): for key, refs in iteritems(references):
if isinstance( if isinstance(field_cls, (Document, TopLevelDocumentMetaclass)):
field_cls, (Document, TopLevelDocumentMetaclass)
):
key = field_cls key = field_cls
reference_map.setdefault(key, set()).update(refs) reference_map.setdefault(key, set()).update(refs)
elif isinstance(item, LazyReference): elif isinstance(item, LazyReference):
@ -150,58 +134,53 @@ class DeReference:
continue continue
elif isinstance(item, DBRef): elif isinstance(item, DBRef):
reference_map.setdefault(item.collection, set()).add(item.id) reference_map.setdefault(item.collection, set()).add(item.id)
elif isinstance(item, (dict, SON)) and "_ref" in item: elif isinstance(item, (dict, SON)) and '_ref' in item:
reference_map.setdefault(get_document(item["_cls"]), set()).add( reference_map.setdefault(get_document(item['_cls']), set()).add(item['_ref'].id)
item["_ref"].id
)
elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth: elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth:
references = self._find_references(item, depth - 1) references = self._find_references(item, depth - 1)
for key, refs in references.items(): for key, refs in iteritems(references):
reference_map.setdefault(key, set()).update(refs) reference_map.setdefault(key, set()).update(refs)
return reference_map return reference_map
def _fetch_objects(self, doc_type=None): def _fetch_objects(self, doc_type=None):
"""Fetch all references and convert to their document objects""" """Fetch all references and convert to their document objects
"""
object_map = {} object_map = {}
for collection, dbrefs in self.reference_map.items(): for collection, dbrefs in iteritems(self.reference_map):
# we use getattr instead of hasattr because hasattr swallows any exception under python2 # we use getattr instead of hasattr because hasattr swallows any exception under python2
# so it could hide nasty things without raising exceptions (cfr bug #1688)) # so it could hide nasty things without raising exceptions (cfr bug #1688))
ref_document_cls_exists = getattr(collection, "objects", None) is not None ref_document_cls_exists = (getattr(collection, 'objects', None) is not None)
if ref_document_cls_exists: if ref_document_cls_exists:
col_name = collection._get_collection_name() col_name = collection._get_collection_name()
refs = [ refs = [dbref for dbref in dbrefs
dbref for dbref in dbrefs if (col_name, dbref) not in object_map if (col_name, dbref) not in object_map]
]
references = collection.objects.in_bulk(refs) references = collection.objects.in_bulk(refs)
for key, doc in references.items(): for key, doc in iteritems(references):
object_map[(col_name, key)] = doc object_map[(col_name, key)] = doc
else: # Generic reference: use the refs data to convert to document else: # Generic reference: use the refs data to convert to document
if isinstance(doc_type, (ListField, DictField, MapField)): if isinstance(doc_type, (ListField, DictField, MapField)):
continue continue
refs = [ refs = [dbref for dbref in dbrefs
dbref for dbref in dbrefs if (collection, dbref) not in object_map if (collection, dbref) not in object_map]
]
if doc_type: if doc_type:
references = doc_type._get_db()[collection].find( references = doc_type._get_db()[collection].find({'_id': {'$in': refs}})
{"_id": {"$in": refs}}
)
for ref in references: for ref in references:
doc = doc_type._from_son(ref) doc = doc_type._from_son(ref)
object_map[(collection, doc.id)] = doc object_map[(collection, doc.id)] = doc
else: else:
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() for x in collection.split("_")) ''.join(x.capitalize()
)._from_son(ref) for x in collection.split('_')))._from_son(ref)
else: else:
doc = doc_type._from_son(ref) doc = doc_type._from_son(ref)
object_map[(collection, doc.id)] = doc object_map[(collection, doc.id)] = doc
@ -229,20 +208,19 @@ class DeReference:
return BaseList(items, instance, name) return BaseList(items, instance, name)
if isinstance(items, (dict, SON)): if isinstance(items, (dict, SON)):
if "_ref" in items: if '_ref' in items:
return self.object_map.get( return self.object_map.get(
(items["_ref"].collection, items["_ref"].id), items (items['_ref'].collection, items['_ref'].id), items)
) elif '_cls' in items:
elif "_cls" in items: doc = get_document(items['_cls'])._from_son(items)
doc = get_document(items["_cls"])._from_son(items) _cls = doc._data.pop('_cls', None)
_cls = doc._data.pop("_cls", None) del items['_cls']
del items["_cls"]
doc._data = self._attach_objects(doc._data, depth, doc, None) doc._data = self._attach_objects(doc._data, depth, doc, None)
if _cls is not None: if _cls is not None:
doc._data["_cls"] = _cls doc._data['_cls'] = _cls
return doc return doc
if not hasattr(items, "items"): if not hasattr(items, 'items'):
is_list = True is_list = True
list_type = BaseList list_type = BaseList
if isinstance(items, EmbeddedDocumentList): if isinstance(items, EmbeddedDocumentList):
@ -252,7 +230,7 @@ class DeReference:
data = [] data = []
else: else:
is_list = False is_list = False
iterator = items.items() iterator = iteritems(items)
data = {} data = {}
depth += 1 depth += 1
@ -269,23 +247,17 @@ class DeReference:
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(
(v.collection, v.id), v (v.collection, v.id), v)
) elif isinstance(v, (dict, SON)) and '_ref' in v:
elif isinstance(v, (dict, SON)) and "_ref" in v:
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 = f"{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( data[k]._data[field_name] = self._attach_objects(v, depth, instance=instance, name=item_name)
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 = f"{name}.{k}" if name else name item_name = '%s.%s' % (name, k) if name else name
data[k] = self._attach_objects( data[k] = self._attach_objects(v, depth - 1, instance=instance, name=item_name)
v, depth - 1, instance=instance, name=item_name elif isinstance(v, DBRef) and hasattr(v, 'id'):
)
elif isinstance(v, DBRef) and hasattr(v, "id"):
data[k] = self.object_map.get((v.collection, v.id), v) data[k] = self.object_map.get((v.collection, v.id), v)
if instance and name: if instance and name:

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,19 @@
from collections import defaultdict from collections import defaultdict
__all__ = ( import six
"NotRegistered", from six import iteritems
"InvalidDocumentError",
"LookUpError", __all__ = ('NotRegistered', 'InvalidDocumentError', 'LookUpError',
"DoesNotExist", 'DoesNotExist', 'MultipleObjectsReturned', 'InvalidQueryError',
"MultipleObjectsReturned", 'OperationError', 'NotUniqueError', 'FieldDoesNotExist',
"InvalidQueryError", 'ValidationError', 'SaveConditionError', 'DeprecatedError')
"OperationError",
"NotUniqueError",
"BulkWriteError",
"FieldDoesNotExist",
"ValidationError",
"SaveConditionError",
"DeprecatedError",
)
class MongoEngineException(Exception): class NotRegistered(Exception):
pass pass
class NotRegistered(MongoEngineException): class InvalidDocumentError(Exception):
pass
class InvalidDocumentError(MongoEngineException):
pass pass
@ -33,19 +21,19 @@ class LookUpError(AttributeError):
pass pass
class DoesNotExist(MongoEngineException): class DoesNotExist(Exception):
pass pass
class MultipleObjectsReturned(MongoEngineException): class MultipleObjectsReturned(Exception):
pass pass
class InvalidQueryError(MongoEngineException): class InvalidQueryError(Exception):
pass pass
class OperationError(MongoEngineException): class OperationError(Exception):
pass pass
@ -53,15 +41,11 @@ class NotUniqueError(OperationError):
pass pass
class BulkWriteError(OperationError):
pass
class SaveConditionError(OperationError): class SaveConditionError(OperationError):
pass pass
class FieldDoesNotExist(MongoEngineException): 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`.
@ -87,25 +71,25 @@ class ValidationError(AssertionError):
field_name = None field_name = None
_message = None _message = None
def __init__(self, message="", **kwargs): def __init__(self, message='', **kwargs):
super().__init__(message) super(ValidationError, self).__init__(message)
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 str(self.message) return six.text_type(self.message)
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.message},)" return '%s(%s,)' % (self.__class__.__name__, self.message)
def __getattribute__(self, name): def __getattribute__(self, name):
message = super().__getattribute__(name) message = super(ValidationError, self).__getattribute__(name)
if name == "message": if name == 'message':
if self.field_name: if self.field_name:
message = "%s" % message message = '%s' % message
if self.errors: if self.errors:
message = f"{message}({self._format_errors()})" message = '%s(%s)' % (message, self._format_errors())
return message return message
def _get_message(self): def _get_message(self):
@ -127,12 +111,12 @@ class ValidationError(AssertionError):
def build_dict(source): def build_dict(source):
errors_dict = {} errors_dict = {}
if isinstance(source, dict): if isinstance(source, dict):
for field_name, error in source.items(): for field_name, error in iteritems(source):
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 str(source) return six.text_type(source)
return errors_dict return errors_dict
@ -144,22 +128,22 @@ class ValidationError(AssertionError):
def _format_errors(self): def _format_errors(self):
"""Returns a string listing all errors within a document""" """Returns a string listing all errors within a document"""
def generate_key(value, prefix=""): def generate_key(value, prefix=''):
if isinstance(value, list): if isinstance(value, list):
value = " ".join([generate_key(k) for k in value]) value = ' '.join([generate_key(k) for k in value])
elif isinstance(value, dict): elif isinstance(value, dict):
value = " ".join([generate_key(v, k) for k, v in value.items()]) value = ' '.join(
[generate_key(v, k) for k, v in iteritems(value)])
results = f"{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().items(): for k, v in iteritems(self.to_dict()):
error_dict[generate_key(v)].append(k) error_dict[generate_key(v)].append(k)
return " ".join([f"{k}: {v}" for k, v in error_dict.items()]) return ' '.join(['%s: %s' % (k, v) for k, v in iteritems(error_dict)])
class DeprecatedError(MongoEngineException): class DeprecatedError(Exception):
"""Raise when a user uses a feature that has been Deprecated""" """Raise when a user uses a feature that has been Deprecated"""
pass pass

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,17 @@ Helper functions, constants, and types to aid with MongoDB version support
""" """
from mongoengine.connection import get_connection from mongoengine.connection import get_connection
# Constant that can be used to compare the version retrieved with # Constant that can be used to compare the version retrieved with
# get_mongodb_version() # get_mongodb_version()
MONGODB_34 = (3, 4) MONGODB_34 = (3, 4)
MONGODB_36 = (3, 6) MONGODB_36 = (3, 6)
MONGODB_42 = (4, 2)
MONGODB_44 = (4, 4)
def get_mongodb_version(): def get_mongodb_version():
"""Return the version of the default connected mongoDB (first 2 digits) """Return the version of the connected mongoDB (first 2 digits)
:return: tuple(int, int) :return: tuple(int, int)
""" """
version_list = get_connection().server_info()["versionArray"][:2] # e.g: (3, 2) version_list = get_connection().server_info()['versionArray'][:2] # e.g: (3, 2)
return tuple(version_list) return tuple(version_list)

View File

@ -2,7 +2,6 @@
Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support. Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support.
""" """
import pymongo import pymongo
from pymongo.errors import OperationFailure
_PYMONGO_37 = (3, 7) _PYMONGO_37 = (3, 7)
@ -11,40 +10,13 @@ PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])
IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37 IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37
def count_documents( def count_documents(collection, filter):
collection, filter, skip=None, limit=None, hint=None, collation=None
):
"""Pymongo>3.7 deprecates count in favour of count_documents""" """Pymongo>3.7 deprecates count in favour of count_documents"""
if limit == 0:
return 0 # Pymongo raises an OperationFailure if called with limit=0
kwargs = {}
if skip is not None:
kwargs["skip"] = skip
if limit is not None:
kwargs["limit"] = limit
if hint not in (-1, None):
kwargs["hint"] = hint
if collation is not None:
kwargs["collation"] = collation
# count_documents appeared in pymongo 3.7
if IS_PYMONGO_GTE_37: if IS_PYMONGO_GTE_37:
try: return collection.count_documents(filter)
return collection.count_documents(filter=filter, **kwargs) else:
except OperationFailure: count = collection.find(filter).count()
# OperationFailure - accounts for some operators that used to work return count
# with .count but are no longer working with count_documents (i.e $geoNear, $near, and $nearSphere)
# fallback to deprecated Cursor.count
# Keeping this should be reevaluated the day pymongo removes .count entirely
pass
cursor = collection.find(filter)
for option, option_value in kwargs.items():
cursor_method = getattr(cursor, option)
cursor = cursor_method(option_value)
with_limit_and_skip = "skip" in kwargs or "limit" in kwargs
return cursor.count(with_limit_and_skip=with_limit_and_skip)
def list_collection_names(db, include_system_collections=False): def list_collection_names(db, include_system_collections=False):
@ -55,6 +27,6 @@ def list_collection_names(db, include_system_collections=False):
collections = db.collection_names() collections = db.collection_names()
if not include_system_collections: if not include_system_collections:
collections = [c for c in collections if not c.startswith("system.")] collections = [c for c in collections if not c.startswith('system.')]
return collections return collections

View File

@ -0,0 +1,23 @@
"""
Helper functions, constants, and types to aid with Python v2.7 - v3.x support
"""
import six
# six.BytesIO resolves to StringIO.StringIO in Py2 and io.BytesIO in Py3.
StringIO = six.BytesIO
# Additionally for Py2, try to use the faster cStringIO, if available
if not six.PY3:
try:
import cStringIO
except ImportError:
pass
else:
StringIO = cStringIO.StringIO
if six.PY3:
from collections.abc import Hashable
else:
# raises DeprecationWarnings in Python >=3.7
from collections import Hashable

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,12 @@
__all__ = ("QueryFieldList",) __all__ = ('QueryFieldList',)
class QueryFieldList: class QueryFieldList(object):
"""Object that handles combinations of .only() and .exclude() calls""" """Object that handles combinations of .only() and .exclude() calls"""
ONLY = 1 ONLY = 1
EXCLUDE = 0 EXCLUDE = 0
def __init__( def __init__(self, fields=None, value=ONLY, always_include=None, _only_called=False):
self, fields=None, value=ONLY, always_include=None, _only_called=False
):
"""The QueryFieldList builder """The QueryFieldList builder
:param fields: A list of fields used in `.only()` or `.exclude()` :param fields: A list of fields used in `.only()` or `.exclude()`
@ -52,7 +49,7 @@ class QueryFieldList:
self.fields = f.fields - self.fields self.fields = f.fields - self.fields
self._clean_slice() self._clean_slice()
if "_id" in f.fields: if '_id' in f.fields:
self._id = f.value self._id = f.value
if self.always_include: if self.always_include:
@ -62,23 +59,25 @@ class QueryFieldList:
else: else:
self.fields -= self.always_include self.fields -= self.always_include
if getattr(f, "_only_called", False): if getattr(f, '_only_called', False):
self._only_called = True self._only_called = True
return self return self
def __bool__(self): def __bool__(self):
return bool(self.fields) return bool(self.fields)
__nonzero__ = __bool__ # For Py2 support
def as_dict(self): def as_dict(self):
field_list = {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:
field_list["_id"] = self._id field_list['_id'] = self._id
return field_list return field_list
def reset(self): def reset(self):
self.fields = set() self.fields = set([])
self.slice = {} self.slice = {}
self.value = self.ONLY self.value = self.ONLY

View File

@ -1,11 +1,10 @@
from functools import partial from functools import partial
from mongoengine.queryset.queryset import QuerySet from mongoengine.queryset.queryset import QuerySet
__all__ = ("queryset_manager", "QuerySetManager") __all__ = ('queryset_manager', 'QuerySetManager')
class QuerySetManager: class QuerySetManager(object):
""" """
The default QuerySet Manager. The default QuerySet Manager.
@ -34,7 +33,7 @@ class QuerySetManager:
return self return self
# owner is the document that contains the QuerySetManager # owner is the document that contains the QuerySetManager
queryset_class = owner._meta.get("queryset_class", self.default) queryset_class = owner._meta.get('queryset_class', self.default)
queryset = queryset_class(owner, owner._get_collection()) queryset = queryset_class(owner, owner._get_collection())
if self.get_queryset: if self.get_queryset:
arg_count = self.get_queryset.__code__.co_argcount arg_count = self.get_queryset.__code__.co_argcount

View File

@ -1,22 +1,11 @@
from mongoengine.errors import OperationError import six
from mongoengine.queryset.base import (
CASCADE,
DENY,
DO_NOTHING,
NULLIFY,
PULL,
BaseQuerySet,
)
__all__ = ( from mongoengine.errors import OperationError
"QuerySet", from mongoengine.queryset.base import (BaseQuerySet, CASCADE, DENY, DO_NOTHING,
"QuerySetNoCache", NULLIFY, PULL)
"DO_NOTHING",
"NULLIFY", __all__ = ('QuerySet', 'QuerySetNoCache', 'DO_NOTHING', 'NULLIFY', 'CASCADE',
"CASCADE", 'DENY', 'PULL')
"DENY",
"PULL",
)
# The maximum number of items to display in a QuerySet.__repr__ # The maximum number of items to display in a QuerySet.__repr__
REPR_OUTPUT_SIZE = 20 REPR_OUTPUT_SIZE = 20
@ -68,12 +57,12 @@ class QuerySet(BaseQuerySet):
def __repr__(self): def __repr__(self):
"""Provide a 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):
@ -125,8 +114,8 @@ class QuerySet(BaseQuerySet):
# Pull in ITER_CHUNK_SIZE docs from the database and store them in # Pull in ITER_CHUNK_SIZE docs from the database and store them in
# the result cache. # the result cache.
try: try:
for _ in range(ITER_CHUNK_SIZE): for _ in six.moves.range(ITER_CHUNK_SIZE):
self._result_cache.append(next(self)) self._result_cache.append(six.next(self))
except StopIteration: except StopIteration:
# Getting this exception means there are no more docs in the # Getting this exception means there are no more docs in the
# db cursor. Set _has_more to False so that we can use that # db cursor. Set _has_more to False so that we can use that
@ -141,43 +130,52 @@ class QuerySet(BaseQuerySet):
getting the count getting the count
""" """
if with_limit_and_skip is False: if with_limit_and_skip is False:
return super().count(with_limit_and_skip) return super(QuerySet, self).count(with_limit_and_skip)
if self._len is None: if self._len is None:
# cache the length self._len = super(QuerySet, self).count(with_limit_and_skip)
self._len = super().count(with_limit_and_skip)
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
if self._result_cache is not None:
raise OperationError("QuerySet already cached")
return self._clone_into(QuerySetNoCache(self._document, self._collection)) .. versionadded:: 0.8.3 Convert to non caching queryset
"""
if self._result_cache is not None:
raise OperationError('QuerySet already cached')
return self._clone_into(QuerySetNoCache(self._document,
self._collection))
class QuerySetNoCache(BaseQuerySet): class QuerySetNoCache(BaseQuerySet):
"""A non caching QuerySet""" """A non caching QuerySet"""
def cache(self): def cache(self):
"""Convert to a caching queryset""" """Convert to a 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
.. versionchanged:: 0.6.13 Now doesnt modify the cursor
"""
if self._iter: if self._iter:
return ".. queryset mid-iteration .." return '.. queryset mid-iteration ..'
data = [] data = []
for _ in range(REPR_OUTPUT_SIZE + 1): for _ in six.moves.range(REPR_OUTPUT_SIZE + 1):
try: try:
data.append(next(self)) data.append(six.next(self))
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,65 +1,30 @@
from collections import defaultdict from collections import defaultdict
import pymongo from bson import ObjectId, SON
from bson import SON, ObjectId
from bson.dbref import DBRef from bson.dbref import DBRef
import pymongo
import six
from six import iteritems
from mongoengine.base import UPDATE_OPERATORS from mongoengine.base import UPDATE_OPERATORS
from mongoengine.common import _import_class from mongoengine.common import _import_class
from mongoengine.errors import InvalidQueryError from mongoengine.errors import InvalidQueryError
__all__ = ("query", "update", "STRING_OPERATORS") __all__ = ('query', 'update')
COMPARISON_OPERATORS = ( COMPARISON_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'mod',
"ne", 'all', 'size', 'exists', 'not', 'elemMatch', 'type')
"gt", GEO_OPERATORS = ('within_distance', 'within_spherical_distance',
"gte", 'within_box', 'within_polygon', 'near', 'near_sphere',
"lt", 'max_distance', 'min_distance', 'geo_within', 'geo_within_box',
"lte", 'geo_within_polygon', 'geo_within_center',
"in", 'geo_within_sphere', 'geo_intersects')
"nin", STRING_OPERATORS = ('contains', 'icontains', 'startswith',
"mod", 'istartswith', 'endswith', 'iendswith',
"all", 'exact', 'iexact')
"size", CUSTOM_OPERATORS = ('match',)
"exists", MATCH_OPERATORS = (COMPARISON_OPERATORS + GEO_OPERATORS +
"not", STRING_OPERATORS + CUSTOM_OPERATORS)
"elemMatch",
"type",
)
GEO_OPERATORS = (
"within_distance",
"within_spherical_distance",
"within_box",
"within_polygon",
"near",
"near_sphere",
"max_distance",
"min_distance",
"geo_within",
"geo_within_box",
"geo_within_polygon",
"geo_within_center",
"geo_within_sphere",
"geo_intersects",
)
STRING_OPERATORS = (
"contains",
"icontains",
"startswith",
"istartswith",
"endswith",
"iendswith",
"exact",
"iexact",
"regex",
"iregex",
"wholeword",
"iwholeword",
)
CUSTOM_OPERATORS = ("match",)
MATCH_OPERATORS = (
COMPARISON_OPERATORS + GEO_OPERATORS + STRING_OPERATORS + CUSTOM_OPERATORS
)
# TODO make this less complex # TODO make this less complex
@ -68,11 +33,11 @@ def query(_doc_cls=None, **kwargs):
mongo_query = {} mongo_query = {}
merge_query = defaultdict(list) merge_query = defaultdict(list)
for key, value in sorted(kwargs.items()): for key, value in sorted(kwargs.items()):
if key == "__raw__": if key == '__raw__':
mongo_query.update(value) mongo_query.update(value)
continue continue
parts = key.rsplit("__") parts = key.rsplit('__')
indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()] indices = [(i, p) for i, p in enumerate(parts) if p.isdigit()]
parts = [part for part in parts if not part.isdigit()] parts = [part for part in parts if not part.isdigit()]
# Check for an operator and transform to mongo-style if there is # Check for an operator and transform to mongo-style if there is
@ -81,11 +46,11 @@ def query(_doc_cls=None, **kwargs):
op = parts.pop() op = parts.pop()
# Allow to escape operator-like field name by __ # Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == "": if len(parts) > 1 and parts[-1] == '':
parts.pop() parts.pop()
negate = False negate = False
if len(parts) > 1 and parts[-1] == "not": if len(parts) > 1 and parts[-1] == 'not':
parts.pop() parts.pop()
negate = True negate = True
@ -97,18 +62,18 @@ def query(_doc_cls=None, **kwargs):
raise InvalidQueryError(e) raise InvalidQueryError(e)
parts = [] parts = []
CachedReferenceField = _import_class("CachedReferenceField") CachedReferenceField = _import_class('CachedReferenceField')
GenericReferenceField = _import_class("GenericReferenceField") 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, str): 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
elif isinstance(field, CachedReferenceField) and fields[-1] == field: elif isinstance(field, CachedReferenceField) and fields[-1] == field:
parts.append("%s._id" % field.db_field) parts.append('%s._id' % field.db_field)
else: else:
parts.append(field.db_field) parts.append(field.db_field)
@ -118,15 +83,15 @@ def query(_doc_cls=None, **kwargs):
# Convert value to proper value # Convert value to proper value
field = cleaned_fields[-1] field = cleaned_fields[-1]
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:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
if isinstance(field, CachedReferenceField) and value: if isinstance(field, CachedReferenceField) and value:
value = value["_id"] value = value['_id']
elif op in ("in", "nin", "all", "near") and not isinstance(value, dict): elif op in ('in', 'nin', 'all', 'near') and not isinstance(value, dict):
# Raise an error if the in/nin/all/near param is not iterable. # Raise an error if the in/nin/all/near param is not iterable.
value = _prepare_query_for_iterable(field, op, value) value = _prepare_query_for_iterable(field, op, value)
@ -136,77 +101,71 @@ def query(_doc_cls=None, **kwargs):
# * If the value is an ObjectId, the key should be "field_name._ref.$id". # * If the value is an ObjectId, the key should be "field_name._ref.$id".
if isinstance(field, GenericReferenceField): if isinstance(field, GenericReferenceField):
if isinstance(value, DBRef): if isinstance(value, DBRef):
parts[-1] += "._ref" parts[-1] += '._ref'
elif isinstance(value, ObjectId): elif isinstance(value, ObjectId):
parts[-1] += "._ref.$id" 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 ("match", "elemMatch"): elif op in ('match', 'elemMatch'):
ListField = _import_class("ListField") ListField = _import_class('ListField')
EmbeddedDocumentField = _import_class("EmbeddedDocumentField") EmbeddedDocumentField = _import_class('EmbeddedDocumentField')
if ( if (
isinstance(value, dict) isinstance(value, dict) and
and isinstance(field, ListField) isinstance(field, ListField) and
and isinstance(field.field, EmbeddedDocumentField) isinstance(field.field, EmbeddedDocumentField)
): ):
value = query(field.field.document_type, **value) value = query(field.field.document_type, **value)
else: else:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
value = {"$elemMatch": value} value = {'$elemMatch': value}
elif op in CUSTOM_OPERATORS: elif op in CUSTOM_OPERATORS:
NotImplementedError( NotImplementedError('Custom method "%s" has not '
'Custom method "%s" has not ' "been implemented" % op 'been implemented' % op)
)
elif op not in STRING_OPERATORS: elif op not in STRING_OPERATORS:
value = {"$" + op: value} value = {'$' + op: value}
if negate: if negate:
value = {"$not": value} value = {'$not': value}
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 key not in mongo_query: if op is None or key not in mongo_query:
mongo_query[key] = value mongo_query[key] = value
else: elif key in mongo_query:
if isinstance(mongo_query[key], dict) and isinstance(value, dict): if isinstance(mongo_query[key], dict) and isinstance(value, 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]
if ("$maxDistance" in value_dict or "$minDistance" in value_dict) and ( if ('$maxDistance' in value_dict or '$minDistance' in value_dict) and \
"$near" in value_dict or "$nearSphere" in value_dict ('$near' in value_dict or '$nearSphere' in value_dict):
):
value_son = SON() value_son = SON()
for k, v in value_dict.items(): for k, v in iteritems(value_dict):
if k == "$maxDistance" or k == "$minDistance": if k == '$maxDistance' or k == '$minDistance':
continue continue
value_son[k] = v value_son[k] = v
# Required for MongoDB >= 2.6, may fail when combining # Required for MongoDB >= 2.6, may fail when combining
# PyMongo 3+ and MongoDB < 2.6 # PyMongo 3+ and MongoDB < 2.6
near_embedded = False near_embedded = False
for near_op in ("$near", "$nearSphere"): for near_op in ('$near', '$nearSphere'):
if isinstance(value_dict.get(near_op), dict): if isinstance(value_dict.get(near_op), dict):
value_son[near_op] = SON(value_son[near_op]) value_son[near_op] = SON(value_son[near_op])
if "$maxDistance" in value_dict: if '$maxDistance' in value_dict:
value_son[near_op]["$maxDistance"] = value_dict[ value_son[near_op]['$maxDistance'] = value_dict['$maxDistance']
"$maxDistance" if '$minDistance' in value_dict:
] value_son[near_op]['$minDistance'] = value_dict['$minDistance']
if "$minDistance" in value_dict:
value_son[near_op]["$minDistance"] = value_dict[
"$minDistance"
]
near_embedded = True near_embedded = True
if not near_embedded: if not near_embedded:
if "$maxDistance" in value_dict: if '$maxDistance' in value_dict:
value_son["$maxDistance"] = value_dict["$maxDistance"] value_son['$maxDistance'] = value_dict['$maxDistance']
if "$minDistance" in value_dict: if '$minDistance' in value_dict:
value_son["$minDistance"] = value_dict["$minDistance"] value_son['$minDistance'] = value_dict['$minDistance']
mongo_query[key] = value_son mongo_query[key] = value_son
else: else:
# Store for manually merging later # Store for manually merging later
@ -218,10 +177,10 @@ def query(_doc_cls=None, **kwargs):
del mongo_query[k] del mongo_query[k]
if isinstance(v, list): if isinstance(v, list):
value = [{k: val} for val in v] value = [{k: val} for val in v]
if "$and" in mongo_query.keys(): if '$and' in mongo_query.keys():
mongo_query["$and"].extend(value) mongo_query['$and'].extend(value)
else: else:
mongo_query["$and"] = value mongo_query['$and'] = value
return mongo_query return mongo_query
@ -233,15 +192,15 @@ def update(_doc_cls=None, **update):
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
op = None op = None
@ -249,13 +208,13 @@ def update(_doc_cls=None, **update):
op = parts.pop(0) op = parts.pop(0)
# Convert Pythonic names to Mongo equivalents # Convert Pythonic names to Mongo equivalents
operator_map = { operator_map = {
"push_all": "pushAll", 'push_all': 'pushAll',
"pull_all": "pullAll", 'pull_all': 'pullAll',
"dec": "inc", 'dec': 'inc',
"add_to_set": "addToSet", 'add_to_set': 'addToSet',
"set_on_insert": "setOnInsert", 'set_on_insert': 'setOnInsert'
} }
if op == "dec": if op == 'dec':
# Support decrement by flipping a positive value's sign # Support decrement by flipping a positive value's sign
# and using 'inc' # and using 'inc'
value = -value value = -value
@ -268,7 +227,7 @@ def update(_doc_cls=None, **update):
match = parts.pop() match = parts.pop()
# Allow to escape operator-like field name by __ # Allow to escape operator-like field name by __
if len(parts) > 1 and parts[-1] == "": if len(parts) > 1 and parts[-1] == '':
parts.pop() parts.pop()
if _doc_cls: if _doc_cls:
@ -283,10 +242,10 @@ 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, str): if isinstance(field, six.string_types):
# Convert the S operator to $ # Convert the S operator to $
if field == "S": if field == 'S':
field = "$" field = '$'
parts.append(field) parts.append(field)
append_field = False append_field = False
else: else:
@ -294,7 +253,7 @@ def update(_doc_cls=None, **update):
if append_field: if append_field:
appended_sub_field = False appended_sub_field = False
cleaned_fields.append(field) cleaned_fields.append(field)
if hasattr(field, "field"): if hasattr(field, 'field'):
cleaned_fields.append(field.field) cleaned_fields.append(field.field)
appended_sub_field = True appended_sub_field = True
@ -304,53 +263,52 @@ 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)
if op == "pull": if op == 'pull':
if field.required or value is not None: if field.required or value is not None:
if match in ("in", "nin") and not isinstance(value, dict): if match in ('in', 'nin') and not isinstance(value, dict):
value = _prepare_query_for_iterable(field, op, value) value = _prepare_query_for_iterable(field, op, value)
else: else:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
elif op == "push" and isinstance(value, (list, tuple, set)): elif op == 'push' and isinstance(value, (list, tuple, set)):
value = [field.prepare_query_value(op, v) for v in value] value = [field.prepare_query_value(op, v) for v in value]
elif op in (None, "set", "push"): elif op in (None, 'set', 'push'):
if field.required or value is not None: if field.required or value is not None:
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
elif op in ("pushAll", "pullAll"): elif op in ('pushAll', 'pullAll'):
value = [field.prepare_query_value(op, v) for v in value] value = [field.prepare_query_value(op, v) for v in value]
elif op in ("addToSet", "setOnInsert"): elif op in ('addToSet', 'setOnInsert'):
if isinstance(value, (list, tuple, set)): if isinstance(value, (list, tuple, set)):
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
elif op == "inc": elif op == 'inc':
value = field.prepare_query_value(op, value) value = field.prepare_query_value(op, value)
if match: if match:
match = "$" + match match = '$' + match
value = {match: value} value = {match: value}
key = ".".join(parts) key = '.'.join(parts)
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( raise InvalidQueryError('pullAll operations only support '
"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]
field_classes.reverse() field_classes.reverse()
ListField = _import_class("ListField") ListField = _import_class('ListField')
EmbeddedDocumentListField = _import_class("EmbeddedDocumentListField") EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField')
if ListField in field_classes or EmbeddedDocumentListField in field_classes: if ListField in field_classes or EmbeddedDocumentListField in field_classes:
# Join all fields via dot notation to the last ListField or EmbeddedDocumentListField # Join all fields via dot notation to the last ListField or EmbeddedDocumentListField
# Then process as normal # Then process as normal
@ -359,36 +317,37 @@ def update(_doc_cls=None, **update):
else: else:
_check_field = EmbeddedDocumentListField _check_field = EmbeddedDocumentListField
last_listField = len(cleaned_fields) - field_classes.index(_check_field) last_listField = len(
key = ".".join(parts[:last_listField]) cleaned_fields) - field_classes.index(_check_field)
key = '.'.join(parts[:last_listField])
parts = parts[last_listField:] parts = parts[last_listField:]
parts.insert(0, key) parts.insert(0, key)
parts.reverse() parts.reverse()
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}}
elif op in ("push", "pushAll"): elif op in ('push', 'pushAll'):
if parts[-1].isdigit(): if parts[-1].isdigit():
key = ".".join(parts[0:-1]) key = '.'.join(parts[0:-1])
position = int(parts[-1]) position = int(parts[-1])
# $position expects an iterable. If pushing a single value, # $position expects an iterable. If pushing a single value,
# wrap it in a list. # wrap it in a list.
if not isinstance(value, (set, tuple, list)): if not isinstance(value, (set, tuple, list)):
value = [value] value = [value]
value = {key: {"$each": value, "$position": position}} value = {key: {'$each': value, '$position': position}}
else: else:
if op == "pushAll": if op == 'pushAll':
op = "push" # convert to non-deprecated keyword op = 'push' # convert to non-deprecated keyword
if not isinstance(value, (set, tuple, list)): if not isinstance(value, (set, tuple, list)):
value = [value] value = [value]
value = {key: {"$each": value}} value = {key: {'$each': value}}
else: else:
value = {key: value} value = {key: value}
else: else:
value = {key: value} value = {key: value}
key = "$" + op key = '$' + op
if key not in mongo_update: if key not in mongo_update:
mongo_update[key] = value mongo_update[key] = value
elif key in mongo_update and isinstance(mongo_update[key], dict): elif key in mongo_update and isinstance(mongo_update[key], dict):
@ -399,47 +358,45 @@ 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:
raise NotImplementedError('Geo method "%s" has not been '
'implemented for a GeoPointField' % op)
else:
if op == 'geo_within':
value = {'$geoWithin': _infer_geometry(value)}
elif op == 'geo_within_box':
value = {'$geoWithin': {'$box': value}}
elif op == 'geo_within_polygon':
value = {'$geoWithin': {'$polygon': value}}
elif op == 'geo_within_center':
value = {'$geoWithin': {'$center': value}}
elif op == 'geo_within_sphere':
value = {'$geoWithin': {'$centerSphere': value}}
elif op == 'geo_intersects':
value = {'$geoIntersects': _infer_geometry(value)}
elif op == 'near':
value = {'$near': _infer_geometry(value)}
else: else:
raise NotImplementedError( raise NotImplementedError(
'Geo method "%s" has not been ' "implemented for a GeoPointField" % op 'Geo method "%s" has not been implemented for a %s '
) % (op, field._name)
else:
if op == "geo_within":
value = {"$geoWithin": _infer_geometry(value)}
elif op == "geo_within_box":
value = {"$geoWithin": {"$box": value}}
elif op == "geo_within_polygon":
value = {"$geoWithin": {"$polygon": value}}
elif op == "geo_within_center":
value = {"$geoWithin": {"$center": value}}
elif op == "geo_within_sphere":
value = {"$geoWithin": {"$centerSphere": value}}
elif op == "geo_intersects":
value = {"$geoIntersects": _infer_geometry(value)}
elif op == "near":
value = {"$near": _infer_geometry(value)}
else:
raise NotImplementedError(
'Geo method "{}" has not been implemented for a {} '.format(
op, field._name
)
) )
return value return value
@ -449,58 +406,51 @@ def _infer_geometry(value):
given value. given value.
""" """
if isinstance(value, dict): if isinstance(value, dict):
if "$geometry" in value: if '$geometry' in value:
return value return value
elif "coordinates" in value and "type" in value: elif 'coordinates' in value and 'type' in value:
return {"$geometry": value} return {'$geometry': value}
raise InvalidQueryError( raise InvalidQueryError('Invalid $geometry dictionary should have '
"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?
try: try:
value[0][0][0] value[0][0][0]
return {"$geometry": {"type": "Polygon", "coordinates": value}} return {'$geometry': {'type': 'Polygon', 'coordinates': value}}
except (TypeError, IndexError): except (TypeError, IndexError):
pass pass
try: try:
value[0][0] value[0][0]
return {"$geometry": {"type": "LineString", "coordinates": value}} return {'$geometry': {'type': 'LineString', 'coordinates': value}}
except (TypeError, IndexError): except (TypeError, IndexError):
pass pass
try: try:
value[0] value[0]
return {"$geometry": {"type": "Point", "coordinates": value}} return {'$geometry': {'type': 'Point', 'coordinates': value}}
except (TypeError, IndexError): except (TypeError, IndexError):
pass pass
raise InvalidQueryError( raise InvalidQueryError('Invalid $geometry data. Can be either a '
"Invalid $geometry data. Can be either a " 'dictionary or (nested) lists of coordinate(s)')
"dictionary or (nested) lists of coordinate(s)"
)
def _prepare_query_for_iterable(field, op, value): def _prepare_query_for_iterable(field, op, value):
# We need a special check for BaseDocument, because - although it's iterable - using # 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. # it as such in the context of this method is most definitely a mistake.
BaseDocument = _import_class("BaseDocument") BaseDocument = _import_class('BaseDocument')
if isinstance(value, BaseDocument): if isinstance(value, BaseDocument):
raise TypeError( raise TypeError("When using the `in`, `nin`, `all`, or "
"When using the `in`, `nin`, `all`, or " "`near`-operators you can\'t use a "
"`near`-operators you can't use a "
"`Document`, you must wrap your object " "`Document`, you must wrap your object "
"in a list (object -> [object])." "in a list (object -> [object]).")
)
if not hasattr(value, "__iter__"): if not hasattr(value, '__iter__'):
raise TypeError( raise TypeError("The `in`, `nin`, `all`, or "
"The `in`, `nin`, `all`, or "
"`near`-operators must be applied to an " "`near`-operators must be applied to an "
"iterable (e.g. a list)." "iterable (e.g. a list).")
)
return [field.prepare_query_value(op, v) for v in value] return [field.prepare_query_value(op, v) for v in value]

View File

@ -1,26 +1,23 @@
import copy import copy
import warnings
from mongoengine.errors import InvalidQueryError from mongoengine.errors import InvalidQueryError
from mongoengine.queryset import transform from mongoengine.queryset import transform
__all__ = ("Q", "QNode") __all__ = ('Q', 'QNode')
def warn_empty_is_deprecated(): class QNodeVisitor(object):
msg = "'empty' property is deprecated in favour of using 'not bool(filter)'" """Base visitor class for visiting Q-object nodes in a query tree.
warnings.warn(msg, DeprecationWarning, stacklevel=2) """
class QNodeVisitor:
"""Base visitor class for visiting Q-object nodes in a query tree."""
def visit_combination(self, combination): def visit_combination(self, combination):
"""Called by QCombination objects.""" """Called by QCombination objects.
"""
return combination return combination
def visit_query(self, query): def visit_query(self, query):
"""Called by (New)Q objects.""" """Called by (New)Q objects.
"""
return query return query
@ -46,7 +43,8 @@ class SimplificationVisitor(QNodeVisitor):
return combination return combination
def _query_conjunction(self, queries): def _query_conjunction(self, queries):
"""Merges query dicts - effectively &ing them together.""" """Merges query dicts - effectively &ing them together.
"""
query_ops = set() query_ops = set()
combined_query = {} combined_query = {}
for query in queries: for query in queries:
@ -71,16 +69,16 @@ 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):
return transform.query(self.document, **query.query) return transform.query(self.document, **query.query)
class QNode: class QNode(object):
"""Base class for nodes in query trees.""" """Base class for nodes in query trees."""
AND = 0 AND = 0
@ -98,19 +96,16 @@ class QNode:
"""Combine this node with another node into a QCombination """Combine this node with another node into a QCombination
object. object.
""" """
# If the other Q() is empty, ignore it and just use `self`. if getattr(other, 'empty', True):
if not bool(other):
return self return self
# Or if this Q is empty, ignore it and just use `other`. if self.empty:
if not bool(self):
return other return other
return QCombination(operation, [self, other]) return QCombination(operation, [self, other])
@property @property
def empty(self): def empty(self):
warn_empty_is_deprecated()
return False return False
def __or__(self, other): def __or__(self, other):
@ -137,11 +132,8 @@ class QCombination(QNode):
self.children.append(node) self.children.append(node)
def __repr__(self): def __repr__(self):
op = " & " if self.operation is self.AND else " | " op = ' & ' if self.operation is self.AND else ' | '
return "(%s)" % op.join([repr(node) for node in self.children]) return '(%s)' % op.join([repr(node) for node in self.children])
def __bool__(self):
return bool(self.children)
def accept(self, visitor): def accept(self, visitor):
for i in range(len(self.children)): for i in range(len(self.children)):
@ -152,16 +144,8 @@ class QCombination(QNode):
@property @property
def empty(self): def empty(self):
warn_empty_is_deprecated()
return not bool(self.children) return not bool(self.children)
def __eq__(self, other):
return (
self.__class__ == other.__class__
and self.operation == other.operation
and self.children == other.children
)
class Q(QNode): class Q(QNode):
"""A simple query object, used in a query tree to build up more complex """A simple query object, used in a query tree to build up more complex
@ -172,18 +156,11 @@ class Q(QNode):
self.query = query self.query = query
def __repr__(self): def __repr__(self):
return "Q(**%s)" % repr(self.query) return 'Q(**%s)' % repr(self.query)
def __bool__(self):
return bool(self.query)
def __eq__(self, other):
return self.__class__ == other.__class__ and self.query == other.query
def accept(self, visitor): def accept(self, visitor):
return visitor.visit_query(self) return visitor.visit_query(self)
@property @property
def empty(self): def empty(self):
warn_empty_is_deprecated()
return not bool(self.query) return not bool(self.query)

View File

@ -1,12 +1,5 @@
__all__ = ( __all__ = ('pre_init', 'post_init', 'pre_save', 'pre_save_post_validation',
"pre_init", 'post_save', 'pre_delete', 'post_delete')
"post_init",
"pre_save",
"pre_save_post_validation",
"post_save",
"pre_delete",
"post_delete",
)
signals_available = False signals_available = False
try: try:
@ -14,12 +7,11 @@ try:
signals_available = True signals_available = True
except ImportError: except ImportError:
class Namespace(object):
class Namespace:
def signal(self, name, doc=None): def signal(self, name, doc=None):
return _FakeSignal(name, doc) return _FakeSignal(name, doc)
class _FakeSignal: class _FakeSignal(object):
"""If blinker is unavailable, create a fake class with the same """If blinker is unavailable, create a fake class with the same
interface that allows sending of signals but will fail with an interface that allows sending of signals but will fail with an
error on anything else. Instead of doing anything on send, it error on anything else. Instead of doing anything on send, it
@ -31,16 +23,13 @@ except ImportError:
self.__doc__ = doc self.__doc__ = doc
def _fail(self, *args, **kwargs): def _fail(self, *args, **kwargs):
raise RuntimeError( raise RuntimeError('signalling support is unavailable '
"signalling support is unavailable " 'because the blinker library is '
"because the blinker library is " 'not installed.')
"not installed."
)
send = lambda *a, **kw: None # noqa send = lambda *a, **kw: None # noqa
connect = ( connect = disconnect = has_receivers_for = receivers_for = \
disconnect temporarily_connected_to = _fail
) = has_receivers_for = receivers_for = temporarily_connected_to = _fail
del _fail del _fail
@ -48,12 +37,12 @@ except ImportError:
# not put signals in here. Create your own namespace instead. # not put signals in here. Create your own namespace instead.
_signals = Namespace() _signals = Namespace()
pre_init = _signals.signal("pre_init") pre_init = _signals.signal('pre_init')
post_init = _signals.signal("post_init") post_init = _signals.signal('post_init')
pre_save = _signals.signal("pre_save") pre_save = _signals.signal('pre_save')
pre_save_post_validation = _signals.signal("pre_save_post_validation") pre_save_post_validation = _signals.signal('pre_save_post_validation')
post_save = _signals.signal("post_save") post_save = _signals.signal('post_save')
pre_delete = _signals.signal("pre_delete") pre_delete = _signals.signal('pre_delete')
post_delete = _signals.signal("post_delete") post_delete = _signals.signal('post_delete')
pre_bulk_insert = _signals.signal("pre_bulk_insert") pre_bulk_insert = _signals.signal('pre_bulk_insert')
post_bulk_insert = _signals.signal("post_bulk_insert") post_bulk_insert = _signals.signal('post_bulk_insert')

View File

@ -1,7 +0,0 @@
black
flake8
pre-commit
pytest
ipdb
ipython
tox

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
nose
pymongo>=3.4
six==1.10.0
flake8
flake8-import-order
Sphinx==1.5.5
sphinx-rtd-theme==0.2.4

View File

@ -1,18 +1,11 @@
[nosetests]
verbosity=2
detailed-errors=1
#tests=tests
cover-package=mongoengine
[flake8] [flake8]
ignore=E501,F403,F405,I201,I202,W504,W605,W503,B007 ignore=E501,F401,F403,F405,I201,I202,W504, W605
exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests exclude=build,dist,docs,venv,venv3,.tox,.eggs,tests
max-complexity=47 max-complexity=47
application-import-names=mongoengine,tests
[tool:pytest]
# Limits the discovery to tests directory
# avoids that it runs for instance the benchmark
testpaths = tests
[isort]
known_first_party = mongoengine,tests
default_section = THIRDPARTY
multi_line_output = 3
include_trailing_comma = True
combine_as_imports = True
line_length = 70
ensure_newline_before_comments = 1

140
setup.py
View File

@ -1,20 +1,20 @@
import os import os
import sys import sys
from pkg_resources import normalize_path
from setuptools import find_packages, setup from setuptools import find_packages, setup
from setuptools.command.test import test as TestCommand
# Hack to silence atexit traceback in newer python versions # Hack to silence atexit traceback in newer python versions
try: try:
import multiprocessing # noqa: F401 import multiprocessing
except ImportError: except ImportError:
pass pass
DESCRIPTION = "MongoEngine is a Python Object-Document Mapper for working with MongoDB." DESCRIPTION = (
'MongoEngine is a Python Object-Document '
'Mapper for working with MongoDB.'
)
try: try:
with open("README.rst") as fin: with open('README.rst') as fin:
LONG_DESCRIPTION = fin.read() LONG_DESCRIPTION = fin.read()
except Exception: except Exception:
LONG_DESCRIPTION = None LONG_DESCRIPTION = None
@ -24,125 +24,63 @@ def get_version(version_tuple):
"""Return the version tuple as a string, e.g. for (0, 10, 7), """Return the version tuple as a string, e.g. for (0, 10, 7),
return '0.10.7'. return '0.10.7'.
""" """
return ".".join(map(str, version_tuple)) return '.'.join(map(str, version_tuple))
class PyTest(TestCommand):
"""Will force pytest to search for tests inside the build directory
for 2to3 converted code (used by tox), instead of the current directory.
Required as long as we need 2to3
Known Limitation: https://tox.readthedocs.io/en/latest/example/pytest.html#known-issues-and-limitations
Source: https://www.hackzine.org/python-testing-with-pytest-and-2to3-plus-tox-and-travis-ci.html
"""
# https://pytest.readthedocs.io/en/2.7.3/goodpractises.html#integration-with-setuptools-test-commands
# Allows to provide pytest command argument through the test runner command `python setup.py test`
# e.g: `python setup.py test -a "-k=test"`
# This only works for 1 argument though
user_options = [("pytest-args=", "a", "Arguments to pass to py.test")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ""
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = ["tests"]
self.test_suite = True
def run_tests(self):
# import here, cause outside the eggs aren't loaded
import pytest
from pkg_resources import _namespace_packages
# Purge modules under test from sys.modules. The test loader will
# re-import them from the build location. Required when 2to3 is used
# with namespace packages.
if sys.version_info >= (3,) and getattr(self.distribution, "use_2to3", False):
module = self.test_args[-1].split(".")[0]
if module in _namespace_packages:
del_modules = []
if module in sys.modules:
del_modules.append(module)
module += "."
for name in sys.modules:
if name.startswith(module):
del_modules.append(name)
map(sys.modules.__delitem__, del_modules)
# Run on the build directory for 2to3-built code
# This will prevent the old 2.x code from being found
# by py.test discovery mechanism, that apparently
# ignores sys.path..
ei_cmd = self.get_finalized_command("egg_info")
self.test_args = [normalize_path(ei_cmd.egg_base)]
cmd_args = self.test_args + ([self.pytest_args] if self.pytest_args else [])
errno = pytest.main(cmd_args)
sys.exit(errno)
# 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
init = os.path.join(os.path.dirname(__file__), "mongoengine", "__init__.py") init = os.path.join(os.path.dirname(__file__), 'mongoengine', '__init__.py')
version_line = list(filter(lambda l: l.startswith("VERSION"), open(init)))[0] version_line = list(filter(lambda l: l.startswith('VERSION'), open(init)))[0]
VERSION = get_version(eval(version_line.split("=")[-1])) VERSION = get_version(eval(version_line.split('=')[-1]))
CLASSIFIERS = [ CLASSIFIERS = [
"Development Status :: 5 - Production/Stable", 'Development Status :: 4 - Beta',
"Intended Audience :: Developers", 'Intended Audience :: Developers',
"License :: OSI Approved :: MIT License", 'License :: OSI Approved :: MIT License',
"Operating System :: OS Independent", 'Operating System :: OS Independent',
"Programming Language :: Python", 'Programming Language :: Python',
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Database", 'Topic :: Database',
"Topic :: Software Development :: Libraries :: Python Modules", 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
extra_opts = { extra_opts = {
"packages": find_packages(exclude=["tests", "tests.*"]), 'packages': find_packages(exclude=['tests', 'tests.*']),
"tests_require": [ 'tests_require': ['nose', 'coverage==4.2', 'blinker', 'Pillow>=2.0.0']
"pytest",
"pytest-cov",
"coverage",
"blinker",
"Pillow>=7.0.0",
],
} }
if sys.version_info[0] == 3:
if "test" in sys.argv: extra_opts['use_2to3'] = True
extra_opts["packages"] = find_packages() if 'test' in sys.argv or 'nosetests' in sys.argv:
extra_opts["package_data"] = { extra_opts['packages'] = find_packages()
"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"] extra_opts['package_data'] = {
} 'tests': ['fields/mongoengine.png', 'fields/mongodb_leaf.png']}
else:
extra_opts['tests_require'] += ['python-dateutil']
setup( setup(
name="mongoengine", name='mongoengine',
version=VERSION, version=VERSION,
author="Harry Marr", author='Harry Marr',
author_email="harry.marr@gmail.com", author_email='harry.marr@gmail.com',
maintainer="Stefan Wojcik", maintainer="Stefan Wojcik",
maintainer_email="wojcikstefan@gmail.com", maintainer_email="wojcikstefan@gmail.com",
url="http://mongoengine.org/", url='http://mongoengine.org/',
download_url="https://github.com/MongoEngine/mongoengine/tarball/master", download_url='https://github.com/MongoEngine/mongoengine/tarball/master',
license="MIT", license='MIT',
include_package_data=True, include_package_data=True,
description=DESCRIPTION, description=DESCRIPTION,
long_description=LONG_DESCRIPTION, long_description=LONG_DESCRIPTION,
platforms=["any"], platforms=['any'],
classifiers=CLASSIFIERS, classifiers=CLASSIFIERS,
python_requires=">=3.6", install_requires=['pymongo>=3.4', 'six'],
install_requires=["pymongo>=3.4, <4.0"], test_suite='nose.collector',
cmdclass={"test": PyTest},
**extra_opts **extra_opts
) )

View File

@ -0,0 +1,4 @@
from .all_warnings import AllWarnings
from .document import *
from .queryset import *
from .fields import *

View File

@ -0,0 +1,42 @@
"""
This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the
top level and called first by the test suite.
"""
import unittest
import warnings
from mongoengine import *
__all__ = ('AllWarnings', )
class AllWarnings(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.warning_list = []
self.showwarning_default = warnings.showwarning
warnings.showwarning = self.append_to_warning_list
def append_to_warning_list(self, message, category, *args):
self.warning_list.append({"message": message,
"category": category})
def tearDown(self):
# restore default handling of warnings
warnings.showwarning = self.showwarning_default
def test_document_collection_syntax_warning(self):
class NonAbstractBase(Document):
meta = {'allow_inheritance': True}
class InheritedDocumentFailTest(NonAbstractBase):
meta = {'collection': 'fail'}
warning = self.warning_list[0]
self.assertEqual(SyntaxWarning, warning["category"])
self.assertEqual('non_abstract_base',
InheritedDocumentFailTest._get_collection_name())

View File

@ -1,35 +0,0 @@
"""
This test has been put into a module. This is because it tests warnings that
only get triggered on first hit. This way we can ensure its imported into the
top level and called first by the test suite.
"""
import unittest
import warnings
from mongoengine import *
class TestAllWarnings(unittest.TestCase):
def setUp(self):
connect(db="mongoenginetest")
self.warning_list = []
self.showwarning_default = warnings.showwarning
warnings.showwarning = self.append_to_warning_list
def append_to_warning_list(self, message, category, *args):
self.warning_list.append({"message": message, "category": category})
def tearDown(self):
# restore default handling of warnings
warnings.showwarning = self.showwarning_default
def test_document_collection_syntax_warning(self):
class NonAbstractBase(Document):
meta = {"allow_inheritance": True}
class InheritedDocumentFailTest(NonAbstractBase):
meta = {"collection": "fail"}
warning = self.warning_list[0]
assert SyntaxWarning == warning["category"]
assert "non_abstract_base" == InheritedDocumentFailTest._get_collection_name()

View File

@ -0,0 +1,13 @@
import unittest
from .class_methods import *
from .delta import *
from .dynamic import *
from .indexes import *
from .inheritance import *
from .instance import *
from .json_serialisation import *
from .validation import *
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,368 @@
# -*- coding: utf-8 -*-
import unittest
from mongoengine import *
from mongoengine.pymongo_support import list_collection_names
from mongoengine.queryset import NULLIFY, PULL
from mongoengine.connection import get_db
__all__ = ("ClassMethodsTest", )
class ClassMethodsTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_definition(self):
"""Ensure that document may be defined using fields.
"""
self.assertEqual(['_cls', 'age', 'id', 'name'],
sorted(self.Person._fields.keys()))
self.assertEqual(["IntField", "ObjectIdField", "StringField", "StringField"],
sorted([x.__class__.__name__ for x in
self.Person._fields.values()]))
def test_get_db(self):
"""Ensure that get_db returns the expected db.
"""
db = self.Person._get_db()
self.assertEqual(self.db, db)
def test_get_collection_name(self):
"""Ensure that get_collection_name returns the expected collection
name.
"""
collection_name = 'person'
self.assertEqual(collection_name, self.Person._get_collection_name())
def test_get_collection(self):
"""Ensure that get_collection returns the expected collection.
"""
collection_name = 'person'
collection = self.Person._get_collection()
self.assertEqual(self.db[collection_name], collection)
def test_drop_collection(self):
"""Ensure that the collection may be dropped from the database.
"""
collection_name = 'person'
self.Person(name='Test').save()
self.assertIn(collection_name, list_collection_names(self.db))
self.Person.drop_collection()
self.assertNotIn(collection_name, list_collection_names(self.db))
def test_register_delete_rule(self):
"""Ensure that register delete rule adds a delete rule to the document
meta.
"""
class Job(Document):
employee = ReferenceField(self.Person)
self.assertEqual(self.Person._meta.get('delete_rules'), None)
self.Person.register_delete_rule(Job, 'employee', NULLIFY)
self.assertEqual(self.Person._meta['delete_rules'],
{(Job, 'employee'): NULLIFY})
def test_compare_indexes(self):
""" Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
tags = StringField()
meta = {
'indexes': [('author', 'title')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': []})
BlogPost.ensure_index(['author', 'description'])
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': [[('author', 1), ('description', 1)]]})
BlogPost._get_collection().drop_index('author_1_description_1')
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': []})
BlogPost._get_collection().drop_index('author_1_title_1')
self.assertEqual(BlogPost.compare_indexes(), {'missing': [[('author', 1), ('title', 1)]], 'extra': []})
def test_compare_indexes_inheritance(self):
""" Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes for subclassed
documents (_cls included)
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {
'indexes': [('author', 'tags')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': []})
BlogPostWithTags.ensure_index(['author', 'tag_list'])
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': [[('_cls', 1), ('author', 1), ('tag_list', 1)]]})
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tag_list_1')
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': []})
BlogPostWithTags._get_collection().drop_index('_cls_1_author_1_tags_1')
self.assertEqual(BlogPost.compare_indexes(), {'missing': [[('_cls', 1), ('author', 1), ('tags', 1)]], 'extra': []})
def test_compare_indexes_multiple_subclasses(self):
""" Ensure that compare_indexes behaves correctly if called from a
class, which base class has multiple subclasses
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {
'indexes': [('author', 'tags')]
}
class BlogPostWithCustomField(BlogPost):
custom = DictField()
meta = {
'indexes': [('author', 'custom')]
}
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithCustomField.ensure_indexes()
self.assertEqual(BlogPost.compare_indexes(), {'missing': [], 'extra': []})
self.assertEqual(BlogPostWithTags.compare_indexes(), {'missing': [], 'extra': []})
self.assertEqual(BlogPostWithCustomField.compare_indexes(), {'missing': [], 'extra': []})
def test_compare_indexes_for_text_indexes(self):
""" Ensure that compare_indexes behaves correctly for text indexes """
class Doc(Document):
a = StringField()
b = StringField()
meta = {'indexes': [
{'fields': ['$a', "$b"],
'default_language': 'english',
'weights': {'a': 10, 'b': 2}
}
]}
Doc.drop_collection()
Doc.ensure_indexes()
actual = Doc.compare_indexes()
expected = {'missing': [], 'extra': []}
self.assertEqual(actual, expected)
def test_list_indexes_inheritance(self):
""" ensure that all of the indexes are listed regardless of the super-
or sub-class that we call it from
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {
'allow_inheritance': True
}
class BlogPostWithTags(BlogPost):
tags = StringField()
meta = {
'indexes': [('author', 'tags')]
}
class BlogPostWithTagsAndExtraText(BlogPostWithTags):
extra_text = StringField()
meta = {
'indexes': [('author', 'tags', 'extra_text')]
}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithTagsAndExtraText.ensure_indexes()
self.assertEqual(BlogPost.list_indexes(),
BlogPostWithTags.list_indexes())
self.assertEqual(BlogPost.list_indexes(),
BlogPostWithTagsAndExtraText.list_indexes())
self.assertEqual(BlogPost.list_indexes(),
[[('_cls', 1), ('author', 1), ('tags', 1)],
[('_cls', 1), ('author', 1), ('tags', 1), ('extra_text', 1)],
[(u'_id', 1)], [('_cls', 1)]])
def test_register_delete_rule_inherited(self):
class Vaccine(Document):
name = StringField(required=True)
meta = {"indexes": ["name"]}
class Animal(Document):
family = StringField(required=True)
vaccine_made = ListField(ReferenceField("Vaccine", reverse_delete_rule=PULL))
meta = {"allow_inheritance": True, "indexes": ["family"]}
class Cat(Animal):
name = StringField(required=True)
self.assertEqual(Vaccine._meta['delete_rules'][(Animal, 'vaccine_made')], PULL)
self.assertEqual(Vaccine._meta['delete_rules'][(Cat, 'vaccine_made')], PULL)
def test_collection_naming(self):
"""Ensure that a collection with a specified name may be used.
"""
class DefaultNamingTest(Document):
pass
self.assertEqual('default_naming_test',
DefaultNamingTest._get_collection_name())
class CustomNamingTest(Document):
meta = {'collection': 'pimp_my_collection'}
self.assertEqual('pimp_my_collection',
CustomNamingTest._get_collection_name())
class DynamicNamingTest(Document):
meta = {'collection': lambda c: "DYNAMO"}
self.assertEqual('DYNAMO', DynamicNamingTest._get_collection_name())
# Use Abstract class to handle backwards compatibility
class BaseDocument(Document):
meta = {
'abstract': True,
'collection': lambda c: c.__name__.lower()
}
class OldNamingConvention(BaseDocument):
pass
self.assertEqual('oldnamingconvention',
OldNamingConvention._get_collection_name())
class InheritedAbstractNamingTest(BaseDocument):
meta = {'collection': 'wibble'}
self.assertEqual('wibble',
InheritedAbstractNamingTest._get_collection_name())
# Mixin tests
class BaseMixin(object):
meta = {
'collection': lambda c: c.__name__.lower()
}
class OldMixinNamingConvention(Document, BaseMixin):
pass
self.assertEqual('oldmixinnamingconvention',
OldMixinNamingConvention._get_collection_name())
class BaseMixin(object):
meta = {
'collection': lambda c: c.__name__.lower()
}
class BaseDocument(Document, BaseMixin):
meta = {'allow_inheritance': True}
class MyDocument(BaseDocument):
pass
self.assertEqual('basedocument', MyDocument._get_collection_name())
def test_custom_collection_name_operations(self):
"""Ensure that a collection with a specified name is used as expected.
"""
collection_name = 'personCollTest'
class Person(Document):
name = StringField()
meta = {'collection': collection_name}
Person(name="Test User").save()
self.assertIn(collection_name, list_collection_names(self.db))
user_obj = self.db[collection_name].find_one()
self.assertEqual(user_obj['name'], "Test User")
user_obj = Person.objects[0]
self.assertEqual(user_obj.name, "Test User")
Person.drop_collection()
self.assertNotIn(collection_name, list_collection_names(self.db))
def test_collection_name_and_primary(self):
"""Ensure that a collection with a specified name may be used.
"""
class Person(Document):
name = StringField(primary_key=True)
meta = {'collection': 'app'}
Person(name="Test User").save()
user_obj = Person.objects.first()
self.assertEqual(user_obj.name, "Test User")
Person.drop_collection()
if __name__ == '__main__':
unittest.main()

864
tests/document/delta.py Normal file
View File

@ -0,0 +1,864 @@
# -*- coding: utf-8 -*-
import unittest
from bson import SON
from mongoengine import *
from mongoengine.pymongo_support import list_collection_names
from tests.utils import MongoDBTestCase
class DeltaTest(MongoDBTestCase):
def setUp(self):
super(DeltaTest, self).setUp()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_delta(self):
self.delta(Document)
self.delta(DynamicDocument)
def delta(self, DocClass):
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['string_field'])
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['int_field'])
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
def test_delta_recursive(self):
self.delta_recursive(Document, EmbeddedDocument)
self.delta_recursive(DynamicDocument, EmbeddedDocument)
self.delta_recursive(Document, DynamicEmbeddedDocument)
self.delta_recursive(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
id = StringField()
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
embedded_field = EmbeddedDocumentField(Embedded)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
embedded_1 = Embedded()
embedded_1.id = "010101"
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEqual(doc._get_changed_fields(), ['embedded_field'])
embedded_delta = {
'id': "010101",
'string_field': 'hello',
'int_field': 1,
'dict_field': {'hello': 'world'},
'list_field': ['1', 2, {'hello': 'world'}]
}
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
self.assertEqual(doc._delta(),
({'embedded_field': embedded_delta}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
self.assertEqual(doc._get_changed_fields(),
['embedded_field.dict_field'])
self.assertEqual(doc.embedded_field._delta(), ({}, {'dict_field': 1}))
self.assertEqual(doc._delta(), ({}, {'embedded_field.dict_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.dict_field, {})
doc.embedded_field.list_field = []
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field'])
self.assertEqual(doc.embedded_field._delta(), ({}, {'list_field': 1}))
self.assertEqual(doc._delta(), ({}, {'embedded_field.list_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field, [])
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field.list_field = ['1', 2, embedded_2]
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field'])
self.assertEqual(doc.embedded_field._delta(), ({
'list_field': ['1', 2, {
'_cls': 'Embedded',
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
self.assertEqual(doc._delta(), ({
'embedded_field.list_field': ['1', 2, {
'_cls': 'Embedded',
'string_field': 'hello',
'dict_field': {'hello': 'world'},
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[0], '1')
self.assertEqual(doc.embedded_field.list_field[1], 2)
for k in doc.embedded_field.list_field[2]._fields:
self.assertEqual(doc.embedded_field.list_field[2][k],
embedded_2[k])
doc.embedded_field.list_field[2].string_field = 'world'
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field.2.string_field'])
self.assertEqual(doc.embedded_field._delta(),
({'list_field.2.string_field': 'world'}, {}))
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.string_field': 'world'}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'world')
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(),
['embedded_field.list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({'list_field.2': {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
}, {}))
self.assertEqual(doc._delta(), ({'embedded_field.list_field.2': {
'_cls': 'Embedded',
'string_field': 'hello world',
'int_field': 1,
'list_field': ['1', 2, {'hello': 'world'}],
'dict_field': {'hello': 'world'}}
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'hello world')
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.list_field':
[2, {'hello': 'world'}]}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
self.assertEqual(doc._delta(),
({'embedded_field.list_field.2.list_field':
[2, {'hello': 'world'}, 1]}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[2, {'hello': 'world'}, 1])
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[1, 2, {'hello': 'world'}])
del doc.embedded_field.list_field[2].list_field[2]['hello']
self.assertEqual(doc._delta(),
({}, {'embedded_field.list_field.2.list_field.2.hello': 1}))
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
self.assertEqual(doc._delta(),
({}, {'embedded_field.list_field.2.list_field': 1}))
doc.save()
doc = doc.reload(10)
doc.dict_field['Embedded'] = embedded_1
doc.save()
doc = doc.reload(10)
doc.dict_field['Embedded'].string_field = 'Hello World'
self.assertEqual(doc._get_changed_fields(),
['dict_field.Embedded.string_field'])
self.assertEqual(doc._delta(),
({'dict_field.Embedded.string_field': 'Hello World'}, {}))
def test_circular_reference_deltas(self):
self.circular_reference_deltas(Document, Document)
self.circular_reference_deltas(Document, DynamicDocument)
self.circular_reference_deltas(DynamicDocument, Document)
self.circular_reference_deltas(DynamicDocument, DynamicDocument)
def circular_reference_deltas(self, DocClass1, DocClass2):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField('Organization'))
class Organization(DocClass2):
name = StringField()
owner = ReferenceField('Person')
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
person.save()
organization.save()
p = Person.objects[0].select_related()
o = Organization.objects.first()
self.assertEqual(p.owns[0], o)
self.assertEqual(o.owner, p)
def test_circular_reference_deltas_2(self):
self.circular_reference_deltas_2(Document, Document)
self.circular_reference_deltas_2(Document, DynamicDocument)
self.circular_reference_deltas_2(DynamicDocument, Document)
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField('Organization', dbref=dbref))
employer = ReferenceField('Organization', dbref=dbref)
class Organization(DocClass2):
name = StringField()
owner = ReferenceField('Person', dbref=dbref)
employees = ListField(ReferenceField('Person', dbref=dbref))
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
employee = Person(name="employee").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
organization.employees.append(employee)
employee.employer = organization
person.save()
organization.save()
employee.save()
p = Person.objects.get(name="owner")
e = Person.objects.get(name="employee")
o = Organization.objects.first()
self.assertEqual(p.owns[0], o)
self.assertEqual(o.owner, p)
self.assertEqual(e.employer, o)
return person, organization, employee
def test_delta_db_field(self):
self.delta_db_field(Document)
self.delta_db_field(DynamicDocument)
def delta_db_field(self, DocClass):
class Doc(DocClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['db_string_field'])
self.assertEqual(doc._delta(), ({'db_string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['db_int_field'])
self.assertEqual(doc._delta(), ({'db_int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
self.assertEqual(doc._delta(), ({'db_dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
self.assertEqual(doc._delta(), ({'db_list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['db_dict_field'])
self.assertEqual(doc._delta(), ({}, {'db_dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['db_list_field'])
self.assertEqual(doc._delta(), ({}, {'db_list_field': 1}))
# Test it saves that data
doc = Doc()
doc.save()
doc.string_field = 'hello'
doc.int_field = 1
doc.dict_field = {'hello': 'world'}
doc.list_field = ['1', 2, {'hello': 'world'}]
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.string_field, 'hello')
self.assertEqual(doc.int_field, 1)
self.assertEqual(doc.dict_field, {'hello': 'world'})
self.assertEqual(doc.list_field, ['1', 2, {'hello': 'world'}])
def test_delta_recursive_db_field(self):
self.delta_recursive_db_field(Document, EmbeddedDocument)
self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive_db_field(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
class Doc(DocClass):
string_field = StringField(db_field='db_string_field')
int_field = IntField(db_field='db_int_field')
dict_field = DictField(db_field='db_dict_field')
list_field = ListField(db_field='db_list_field')
embedded_field = EmbeddedDocumentField(Embedded,
db_field='db_embedded_field')
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
embedded_1 = Embedded()
embedded_1.string_field = 'hello'
embedded_1.int_field = 1
embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1
self.assertEqual(doc._get_changed_fields(), ['db_embedded_field'])
embedded_delta = {
'db_string_field': 'hello',
'db_int_field': 1,
'db_dict_field': {'hello': 'world'},
'db_list_field': ['1', 2, {'hello': 'world'}]
}
self.assertEqual(doc.embedded_field._delta(), (embedded_delta, {}))
self.assertEqual(doc._delta(),
({'db_embedded_field': embedded_delta}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_dict_field'])
self.assertEqual(doc.embedded_field._delta(),
({}, {'db_dict_field': 1}))
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_dict_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.dict_field, {})
doc.embedded_field.list_field = []
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field'])
self.assertEqual(doc.embedded_field._delta(),
({}, {'db_list_field': 1}))
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_list_field': 1}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field, [])
embedded_2 = Embedded()
embedded_2.string_field = 'hello'
embedded_2.int_field = 1
embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field.list_field = ['1', 2, embedded_2]
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field'])
self.assertEqual(doc.embedded_field._delta(), ({
'db_list_field': ['1', 2, {
'_cls': 'Embedded',
'db_string_field': 'hello',
'db_dict_field': {'hello': 'world'},
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
self.assertEqual(doc._delta(), ({
'db_embedded_field.db_list_field': ['1', 2, {
'_cls': 'Embedded',
'db_string_field': 'hello',
'db_dict_field': {'hello': 'world'},
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
}]
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[0], '1')
self.assertEqual(doc.embedded_field.list_field[1], 2)
for k in doc.embedded_field.list_field[2]._fields:
self.assertEqual(doc.embedded_field.list_field[2][k],
embedded_2[k])
doc.embedded_field.list_field[2].string_field = 'world'
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field.2.db_string_field'])
self.assertEqual(doc.embedded_field._delta(),
({'db_list_field.2.db_string_field': 'world'}, {}))
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_string_field': 'world'},
{}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'world')
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = 'hello world'
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
self.assertEqual(doc._get_changed_fields(),
['db_embedded_field.db_list_field.2'])
self.assertEqual(doc.embedded_field._delta(), ({'db_list_field.2': {
'_cls': 'Embedded',
'db_string_field': 'hello world',
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}}}, {}))
self.assertEqual(doc._delta(), ({
'db_embedded_field.db_list_field.2': {
'_cls': 'Embedded',
'db_string_field': 'hello world',
'db_int_field': 1,
'db_list_field': ['1', 2, {'hello': 'world'}],
'db_dict_field': {'hello': 'world'}}
}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].string_field,
'hello world')
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_list_field':
[2, {'hello': 'world'}]}, {}))
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
self.assertEqual(doc._delta(),
({'db_embedded_field.db_list_field.2.db_list_field':
[2, {'hello': 'world'}, 1]}, {}))
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[2, {'hello': 'world'}, 1])
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
self.assertEqual(doc.embedded_field.list_field[2].list_field,
[1, 2, {'hello': 'world'}])
del doc.embedded_field.list_field[2].list_field[2]['hello']
self.assertEqual(doc._delta(),
({}, {'db_embedded_field.db_list_field.2.db_list_field.2.hello': 1}))
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
self.assertEqual(doc._delta(), ({},
{'db_embedded_field.db_list_field.2.db_list_field': 1}))
def test_delta_for_dynamic_documents(self):
class Person(DynamicDocument):
name = StringField()
meta = {'allow_inheritance': True}
Person.drop_collection()
p = Person(name="James", age=34)
self.assertEqual(p._delta(), (
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
p.doc = 123
del p.doc
self.assertEqual(p._delta(), (
SON([('_cls', 'Person'), ('name', 'James'), ('age', 34)]), {}))
p = Person()
p.name = "Dean"
p.age = 22
p.save()
p.age = 24
self.assertEqual(p.age, 24)
self.assertEqual(p._get_changed_fields(), ['age'])
self.assertEqual(p._delta(), ({'age': 24}, {}))
p = Person.objects(age=22).get()
p.age = 24
self.assertEqual(p.age, 24)
self.assertEqual(p._get_changed_fields(), ['age'])
self.assertEqual(p._delta(), ({'age': 24}, {}))
p.save()
self.assertEqual(1, Person.objects(age=24).count())
def test_dynamic_delta(self):
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
self.assertEqual(doc._get_changed_fields(), [])
self.assertEqual(doc._delta(), ({}, {}))
doc.string_field = 'hello'
self.assertEqual(doc._get_changed_fields(), ['string_field'])
self.assertEqual(doc._delta(), ({'string_field': 'hello'}, {}))
doc._changed_fields = []
doc.int_field = 1
self.assertEqual(doc._get_changed_fields(), ['int_field'])
self.assertEqual(doc._delta(), ({'int_field': 1}, {}))
doc._changed_fields = []
dict_value = {'hello': 'world', 'ping': 'pong'}
doc.dict_field = dict_value
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({'dict_field': dict_value}, {}))
doc._changed_fields = []
list_value = ['1', 2, {'hello': 'world'}]
doc.list_field = list_value
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({'list_field': list_value}, {}))
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
self.assertEqual(doc._get_changed_fields(), ['dict_field'])
self.assertEqual(doc._delta(), ({}, {'dict_field': 1}))
doc._changed_fields = []
doc.list_field = []
self.assertEqual(doc._get_changed_fields(), ['list_field'])
self.assertEqual(doc._delta(), ({}, {'list_field': 1}))
def test_delta_with_dbref_true(self):
person, organization, employee = self.circular_reference_deltas_2(Document, Document, True)
employee.name = 'test'
self.assertEqual(organization._get_changed_fields(), [])
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertEqual({}, updates)
organization.employees.append(person)
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertIn('employees', updates)
def test_delta_with_dbref_false(self):
person, organization, employee = self.circular_reference_deltas_2(Document, Document, False)
employee.name = 'test'
self.assertEqual(organization._get_changed_fields(), [])
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertEqual({}, updates)
organization.employees.append(person)
updates, removals = organization._delta()
self.assertEqual({}, removals)
self.assertIn('employees', updates)
def test_nested_nested_fields_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
name = StringField()
MyDoc.drop_collection()
mydoc = MyDoc(name='testcase1', subs={'a': {'b': EmbeddedDoc(name='foo')}}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs['a']['b']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a.b.name"], mydoc._get_changed_fields())
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_lower_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc().save()
mydoc = MyDoc.objects.first()
mydoc.subs['a'] = EmbeddedDoc()
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
subdoc = mydoc.subs['a']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
mydoc.save()
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_upper_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc(subs={'a': EmbeddedDoc(name='foo')}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs['a']
subdoc.name = 'bar'
self.assertEqual(["name"], subdoc._get_changed_fields())
self.assertEqual(["subs.a.name"], mydoc._get_changed_fields())
mydoc.subs['a'] = EmbeddedDoc()
self.assertEqual(["subs.a"], mydoc._get_changed_fields())
mydoc.save()
mydoc._clear_changed_fields()
self.assertEqual([], mydoc._get_changed_fields())
def test_referenced_object_changed_attributes(self):
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
class Organization(Document):
name = StringField()
class User(Document):
name = StringField()
org = ReferenceField('Organization', required=True)
Organization.drop_collection()
User.drop_collection()
org1 = Organization(name='Org 1')
org1.save()
org2 = Organization(name='Org 2')
org2.save()
user = User(name='Fred', org=org1)
user.save()
org1.reload()
org2.reload()
user.reload()
self.assertEqual(org1.name, 'Org 1')
self.assertEqual(org2.name, 'Org 2')
self.assertEqual(user.name, 'Fred')
user.name = 'Harold'
user.org = org2
org2.name = 'New Org 2'
self.assertEqual(org2.name, 'New Org 2')
user.save()
org2.save()
self.assertEqual(org2.name, 'New Org 2')
org2.reload()
self.assertEqual(org2.name, 'New Org 2')
def test_delta_for_nested_map_fields(self):
class UInfoDocument(Document):
phone = StringField()
class EmbeddedRole(EmbeddedDocument):
type = StringField()
class EmbeddedUser(EmbeddedDocument):
name = StringField()
roles = MapField(field=EmbeddedDocumentField(EmbeddedRole))
rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole))
info = ReferenceField(UInfoDocument)
class Doc(Document):
users = MapField(field=EmbeddedDocumentField(EmbeddedUser))
num = IntField(default=-1)
Doc.drop_collection()
doc = Doc(num=1)
doc.users["007"] = EmbeddedUser(name="Agent007")
doc.save()
uinfo = UInfoDocument(phone="79089269066")
uinfo.save()
d = Doc.objects(num=1).first()
d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin")
d.users["007"]["rolist"].append(EmbeddedRole(type="oops"))
d.users["007"]["info"] = uinfo
delta = d._delta()
self.assertEqual(True, "users.007.roles.666" in delta[0])
self.assertEqual(True, "users.007.rolist" in delta[0])
self.assertEqual(True, "users.007.info" in delta[0])
self.assertEqual('superadmin', delta[0]["users.007.roles.666"]["type"])
self.assertEqual('oops', delta[0]["users.007.rolist"][0]["type"])
self.assertEqual(uinfo.id, delta[0]["users.007.info"])
if __name__ == '__main__':
unittest.main()

View File

@ -1,20 +1,19 @@
import unittest import unittest
import pytest
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
__all__ = ("TestDynamicDocument",) __all__ = ("TestDynamicDocument", )
class TestDynamicDocument(MongoDBTestCase): class TestDynamicDocument(MongoDBTestCase):
def setUp(self): def setUp(self):
super().setUp() super(TestDynamicDocument, self).setUp()
class Person(DynamicDocument): class Person(DynamicDocument):
name = StringField() name = StringField()
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
Person.drop_collection() Person.drop_collection()
@ -27,28 +26,16 @@ class TestDynamicDocument(MongoDBTestCase):
p.name = "James" p.name = "James"
p.age = 34 p.age = 34
assert p.to_mongo() == {"_cls": "Person", "name": "James", "age": 34} self.assertEqual(p.to_mongo(), {"_cls": "Person", "name": "James",
assert p.to_mongo().keys() == ["_cls", "name", "age"] "age": 34})
self.assertEqual(p.to_mongo().keys(), ["_cls", "name", "age"])
p.save() p.save()
assert p.to_mongo().keys() == ["_id", "_cls", "name", "age"] self.assertEqual(p.to_mongo().keys(), ["_id", "_cls", "name", "age"])
assert self.Person.objects.first().age == 34 self.assertEqual(self.Person.objects.first().age, 34)
# Confirm no changes to self.Person # Confirm no changes to self.Person
assert not hasattr(self.Person, "age") self.assertFalse(hasattr(self.Person, 'age'))
def test_dynamic_document_parse_values_in_constructor_like_document_do(self):
class ProductDynamicDocument(DynamicDocument):
title = StringField()
price = FloatField()
class ProductDocument(Document):
title = StringField()
price = FloatField()
product = ProductDocument(title="Blabla", price="12.5")
dyn_product = ProductDynamicDocument(title="Blabla", price="12.5")
assert product.price == dyn_product.price == 12.5
def test_change_scope_of_variable(self): def test_change_scope_of_variable(self):
"""Test changing the scope of a dynamic field has no adverse effects""" """Test changing the scope of a dynamic field has no adverse effects"""
@ -58,11 +45,11 @@ class TestDynamicDocument(MongoDBTestCase):
p.save() p.save()
p = self.Person.objects.get() p = self.Person.objects.get()
p.misc = {"hello": "world"} p.misc = {'hello': 'world'}
p.save() p.save()
p = self.Person.objects.get() p = self.Person.objects.get()
assert p.misc == {"hello": "world"} self.assertEqual(p.misc, {'hello': 'world'})
def test_delete_dynamic_field(self): def test_delete_dynamic_field(self):
"""Test deleting a dynamic field works""" """Test deleting a dynamic field works"""
@ -73,23 +60,23 @@ class TestDynamicDocument(MongoDBTestCase):
p.save() p.save()
p = self.Person.objects.get() p = self.Person.objects.get()
p.misc = {"hello": "world"} p.misc = {'hello': 'world'}
p.save() p.save()
p = self.Person.objects.get() p = self.Person.objects.get()
assert p.misc == {"hello": "world"} self.assertEqual(p.misc, {'hello': 'world'})
collection = self.db[self.Person._get_collection_name()] collection = self.db[self.Person._get_collection_name()]
obj = collection.find_one() obj = collection.find_one()
assert sorted(obj.keys()) == ["_cls", "_id", "misc", "name"] self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'misc', 'name'])
del p.misc del p.misc
p.save() p.save()
p = self.Person.objects.get() p = self.Person.objects.get()
assert not hasattr(p, "misc") self.assertFalse(hasattr(p, 'misc'))
obj = collection.find_one() obj = collection.find_one()
assert sorted(obj.keys()) == ["_cls", "_id", "name"] self.assertEqual(sorted(obj.keys()), ['_cls', '_id', 'name'])
def test_reload_after_unsetting(self): def test_reload_after_unsetting(self):
p = self.Person() p = self.Person()
@ -103,55 +90,78 @@ class TestDynamicDocument(MongoDBTestCase):
p = self.Person.objects.create() p = self.Person.objects.create()
p.update(age=1) p.update(age=1)
assert len(p._data) == 3 self.assertEqual(len(p._data), 3)
assert sorted(p._data.keys()) == ["_cls", "id", "name"] self.assertEqual(sorted(p._data.keys()), ['_cls', 'id', 'name'])
p.reload() p.reload()
assert len(p._data) == 4 self.assertEqual(len(p._data), 4)
assert sorted(p._data.keys()) == ["_cls", "age", "id", "name"] self.assertEqual(sorted(p._data.keys()), ['_cls', 'age', 'id', 'name'])
def test_fields_without_underscore(self): def test_fields_without_underscore(self):
"""Ensure we can query dynamic fields""" """Ensure we can query dynamic fields"""
Person = self.Person Person = self.Person
p = self.Person(name="Dean") p = self.Person(name='Dean')
p.save() p.save()
raw_p = Person.objects.as_pymongo().get(id=p.id) raw_p = Person.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_cls": "Person", "_id": p.id, "name": "Dean"} self.assertEqual(
raw_p,
p.name = "OldDean" {
p.newattr = "garbage" '_cls': u'Person',
p.save() '_id': p.id,
raw_p = Person.objects.as_pymongo().get(id=p.id) 'name': u'Dean'
assert raw_p == {
"_cls": "Person",
"_id": p.id,
"name": "OldDean",
"newattr": "garbage",
} }
)
p.name = 'OldDean'
p.newattr = 'garbage'
p.save()
raw_p = Person.objects.as_pymongo().get(id=p.id)
self.assertEqual(
raw_p,
{
'_cls': u'Person',
'_id': p.id,
'name': 'OldDean',
'newattr': u'garbage'
}
)
def test_fields_containing_underscore(self): def test_fields_containing_underscore(self):
"""Ensure we can query dynamic fields""" """Ensure we can query dynamic fields"""
class WeirdPerson(DynamicDocument): class WeirdPerson(DynamicDocument):
name = StringField() name = StringField()
_name = StringField() _name = StringField()
WeirdPerson.drop_collection() WeirdPerson.drop_collection()
p = WeirdPerson(name="Dean", _name="Dean") p = WeirdPerson(name='Dean', _name='Dean')
p.save() p.save()
raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id) raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_id": p.id, "_name": "Dean", "name": "Dean"} self.assertEqual(
raw_p,
{
'_id': p.id,
'_name': u'Dean',
'name': u'Dean'
}
)
p.name = "OldDean" p.name = 'OldDean'
p._name = "NewDean" p._name = 'NewDean'
p._newattr1 = "garbage" # Unknown fields won't be added p._newattr1 = 'garbage' # Unknown fields won't be added
p.save() p.save()
raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id) raw_p = WeirdPerson.objects.as_pymongo().get(id=p.id)
assert raw_p == {"_id": p.id, "_name": "NewDean", "name": "OldDean"} self.assertEqual(
raw_p,
{
'_id': p.id,
'_name': u'NewDean',
'name': u'OldDean',
}
)
def test_dynamic_document_queries(self): def test_dynamic_document_queries(self):
"""Ensure we can query dynamic fields""" """Ensure we can query dynamic fields"""
@ -160,10 +170,10 @@ class TestDynamicDocument(MongoDBTestCase):
p.age = 22 p.age = 22
p.save() p.save()
assert 1 == self.Person.objects(age=22).count() self.assertEqual(1, self.Person.objects(age=22).count())
p = self.Person.objects(age=22) p = self.Person.objects(age=22)
p = p.get() p = p.get()
assert 22 == p.age self.assertEqual(22, p.age)
def test_complex_dynamic_document_queries(self): def test_complex_dynamic_document_queries(self):
class Person(DynamicDocument): class Person(DynamicDocument):
@ -183,25 +193,26 @@ class TestDynamicDocument(MongoDBTestCase):
p2.age = 10 p2.age = 10
p2.save() p2.save()
assert Person.objects(age__icontains="ten").count() == 2 self.assertEqual(Person.objects(age__icontains='ten').count(), 2)
assert Person.objects(age__gte=10).count() == 1 self.assertEqual(Person.objects(age__gte=10).count(), 1)
def test_complex_data_lookups(self): def test_complex_data_lookups(self):
"""Ensure you can query dynamic document dynamic fields""" """Ensure you can query dynamic document dynamic fields"""
p = self.Person() p = self.Person()
p.misc = {"hello": "world"} p.misc = {'hello': 'world'}
p.save() p.save()
assert 1 == self.Person.objects(misc__hello="world").count() self.assertEqual(1, self.Person.objects(misc__hello='world').count())
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"""
self.Person.objects.create(misc={"hello": {"hello2": "world"}}) p = self.Person.objects.create(
assert 1 == self.Person.objects(misc__hello__hello2="world").count() misc={'hello': {'hello2': 'world'}}
)
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):
"""Ensure embedded dynamic documents may be validated""" """Ensure embedded dynamic documents may be validated"""
class Embedded(DynamicEmbeddedDocument): class Embedded(DynamicEmbeddedDocument):
content = URLField() content = URLField()
@ -211,29 +222,27 @@ class TestDynamicDocument(MongoDBTestCase):
Doc.drop_collection() Doc.drop_collection()
doc = Doc() doc = Doc()
embedded_doc_1 = Embedded(content="http://mongoengine.org") embedded_doc_1 = Embedded(content='http://mongoengine.org')
embedded_doc_1.validate() embedded_doc_1.validate()
embedded_doc_2 = Embedded(content="this is not a url") embedded_doc_2 = Embedded(content='this is not a url')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, embedded_doc_2.validate)
embedded_doc_2.validate()
doc.embedded_field_1 = embedded_doc_1 doc.embedded_field_1 = embedded_doc_1
doc.embedded_field_2 = embedded_doc_2 doc.embedded_field_2 = embedded_doc_2
with pytest.raises(ValidationError): self.assertRaises(ValidationError, doc.validate)
doc.validate()
def test_inheritance(self): def test_inheritance(self):
"""Ensure that dynamic document plays nice with inheritance""" """Ensure that dynamic document plays nice with inheritance"""
class Employee(self.Person): class Employee(self.Person):
salary = IntField() salary = IntField()
Employee.drop_collection() Employee.drop_collection()
assert "name" in Employee._fields self.assertIn('name', Employee._fields)
assert "salary" in Employee._fields self.assertIn('salary', Employee._fields)
assert Employee._get_collection_name() == self.Person._get_collection_name() self.assertEqual(Employee._get_collection_name(),
self.Person._get_collection_name())
joe_bloggs = Employee() joe_bloggs = Employee()
joe_bloggs.name = "Joe Bloggs" joe_bloggs.name = "Joe Bloggs"
@ -241,15 +250,14 @@ class TestDynamicDocument(MongoDBTestCase):
joe_bloggs.age = 20 joe_bloggs.age = 20
joe_bloggs.save() joe_bloggs.save()
assert 1 == self.Person.objects(age=20).count() self.assertEqual(1, self.Person.objects(age=20).count())
assert 1 == Employee.objects(age=20).count() self.assertEqual(1, Employee.objects(age=20).count())
joe_bloggs = self.Person.objects.first() joe_bloggs = self.Person.objects.first()
assert isinstance(joe_bloggs, Employee) self.assertIsInstance(joe_bloggs, Employee)
def test_embedded_dynamic_document(self): def test_embedded_dynamic_document(self):
"""Test dynamic embedded documents""" """Test dynamic embedded documents"""
class Embedded(DynamicEmbeddedDocument): class Embedded(DynamicEmbeddedDocument):
pass pass
@ -260,33 +268,33 @@ class TestDynamicDocument(MongoDBTestCase):
doc = Doc() doc = Doc()
embedded_1 = Embedded() embedded_1 = Embedded()
embedded_1.string_field = "hello" embedded_1.string_field = 'hello'
embedded_1.int_field = 1 embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"} embedded_1.dict_field = {'hello': 'world'}
embedded_1.list_field = ["1", 2, {"hello": "world"}] embedded_1.list_field = ['1', 2, {'hello': 'world'}]
doc.embedded_field = embedded_1 doc.embedded_field = embedded_1
assert doc.to_mongo() == { self.assertEqual(doc.to_mongo(), {
"embedded_field": { "embedded_field": {
"_cls": "Embedded", "_cls": "Embedded",
"string_field": "hello", "string_field": "hello",
"int_field": 1, "int_field": 1,
"dict_field": {"hello": "world"}, "dict_field": {"hello": "world"},
"list_field": ["1", 2, {"hello": "world"}], "list_field": ['1', 2, {'hello': 'world'}]
}
} }
})
doc.save() doc.save()
doc = Doc.objects.first() doc = Doc.objects.first()
assert doc.embedded_field.__class__ == Embedded self.assertEqual(doc.embedded_field.__class__, Embedded)
assert doc.embedded_field.string_field == "hello" self.assertEqual(doc.embedded_field.string_field, "hello")
assert doc.embedded_field.int_field == 1 self.assertEqual(doc.embedded_field.int_field, 1)
assert doc.embedded_field.dict_field == {"hello": "world"} self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
assert doc.embedded_field.list_field == ["1", 2, {"hello": "world"}] self.assertEqual(doc.embedded_field.list_field,
['1', 2, {'hello': 'world'}])
def test_complex_embedded_documents(self): def test_complex_embedded_documents(self):
"""Test complex dynamic embedded documents setups""" """Test complex dynamic embedded documents setups"""
class Embedded(DynamicEmbeddedDocument): class Embedded(DynamicEmbeddedDocument):
pass pass
@ -297,54 +305,51 @@ class TestDynamicDocument(MongoDBTestCase):
doc = Doc() doc = Doc()
embedded_1 = Embedded() embedded_1 = Embedded()
embedded_1.string_field = "hello" embedded_1.string_field = 'hello'
embedded_1.int_field = 1 embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"} embedded_1.dict_field = {'hello': 'world'}
embedded_2 = Embedded() embedded_2 = Embedded()
embedded_2.string_field = "hello" embedded_2.string_field = 'hello'
embedded_2.int_field = 1 embedded_2.int_field = 1
embedded_2.dict_field = {"hello": "world"} embedded_2.dict_field = {'hello': 'world'}
embedded_2.list_field = ["1", 2, {"hello": "world"}] embedded_2.list_field = ['1', 2, {'hello': 'world'}]
embedded_1.list_field = ["1", 2, embedded_2] embedded_1.list_field = ['1', 2, embedded_2]
doc.embedded_field = embedded_1 doc.embedded_field = embedded_1
assert doc.to_mongo() == { self.assertEqual(doc.to_mongo(), {
"embedded_field": { "embedded_field": {
"_cls": "Embedded", "_cls": "Embedded",
"string_field": "hello", "string_field": "hello",
"int_field": 1, "int_field": 1,
"dict_field": {"hello": "world"}, "dict_field": {"hello": "world"},
"list_field": [ "list_field": ['1', 2,
"1", {"_cls": "Embedded",
2,
{
"_cls": "Embedded",
"string_field": "hello", "string_field": "hello",
"int_field": 1, "int_field": 1,
"dict_field": {"hello": "world"}, "dict_field": {"hello": "world"},
"list_field": ["1", 2, {"hello": "world"}], "list_field": ['1', 2, {'hello': 'world'}]}
}, ]
],
}
} }
})
doc.save() doc.save()
doc = Doc.objects.first() doc = Doc.objects.first()
assert doc.embedded_field.__class__ == Embedded self.assertEqual(doc.embedded_field.__class__, Embedded)
assert doc.embedded_field.string_field == "hello" self.assertEqual(doc.embedded_field.string_field, "hello")
assert doc.embedded_field.int_field == 1 self.assertEqual(doc.embedded_field.int_field, 1)
assert doc.embedded_field.dict_field == {"hello": "world"} self.assertEqual(doc.embedded_field.dict_field, {'hello': 'world'})
assert doc.embedded_field.list_field[0] == "1" self.assertEqual(doc.embedded_field.list_field[0], '1')
assert doc.embedded_field.list_field[1] == 2 self.assertEqual(doc.embedded_field.list_field[1], 2)
embedded_field = doc.embedded_field.list_field[2] embedded_field = doc.embedded_field.list_field[2]
assert embedded_field.__class__ == Embedded self.assertEqual(embedded_field.__class__, Embedded)
assert embedded_field.string_field == "hello" self.assertEqual(embedded_field.string_field, "hello")
assert embedded_field.int_field == 1 self.assertEqual(embedded_field.int_field, 1)
assert embedded_field.dict_field == {"hello": "world"} self.assertEqual(embedded_field.dict_field, {'hello': 'world'})
assert embedded_field.list_field == ["1", 2, {"hello": "world"}] self.assertEqual(embedded_field.list_field, ['1', 2,
{'hello': 'world'}])
def test_dynamic_and_embedded(self): def test_dynamic_and_embedded(self):
"""Ensure embedded documents play nicely""" """Ensure embedded documents play nicely"""
@ -363,18 +368,18 @@ class TestDynamicDocument(MongoDBTestCase):
person.address.city = "Lundenne" person.address.city = "Lundenne"
person.save() person.save()
assert Person.objects.first().address.city == "Lundenne" self.assertEqual(Person.objects.first().address.city, "Lundenne")
person = Person.objects.first() person = Person.objects.first()
person.address = Address(city="Londinium") person.address = Address(city="Londinium")
person.save() person.save()
assert Person.objects.first().address.city == "Londinium" self.assertEqual(Person.objects.first().address.city, "Londinium")
person = Person.objects.first() person = Person.objects.first()
person.age = 35 person.age = 35
person.save() person.save()
assert Person.objects.first().age == 35 self.assertEqual(Person.objects.first().age, 35)
def test_dynamic_embedded_works_with_only(self): def test_dynamic_embedded_works_with_only(self):
"""Ensure custom fieldnames on a dynamic embedded document are found by qs.only()""" """Ensure custom fieldnames on a dynamic embedded document are found by qs.only()"""
@ -387,15 +392,10 @@ class TestDynamicDocument(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
Person( Person(name="Eric", address=Address(city="San Francisco", street_number="1337")).save()
name="Eric", address=Address(city="San Francisco", street_number="1337")
).save()
assert Person.objects.first().address.street_number == "1337" self.assertEqual(Person.objects.first().address.street_number, '1337')
assert ( self.assertEqual(Person.objects.only('address__street_number').first().address.street_number, '1337')
Person.objects.only("address__street_number").first().address.street_number
== "1337"
)
def test_dynamic_and_embedded_dict_access(self): def test_dynamic_and_embedded_dict_access(self):
"""Ensure embedded dynamic documents work with dict[] style access""" """Ensure embedded dynamic documents work with dict[] style access"""
@ -419,21 +419,21 @@ class TestDynamicDocument(MongoDBTestCase):
person["address"]["city"] = "Lundenne" person["address"]["city"] = "Lundenne"
person.save() person.save()
assert Person.objects.first().address.city == "Lundenne" self.assertEqual(Person.objects.first().address.city, "Lundenne")
assert Person.objects.first().phone == "555-1212" self.assertEqual(Person.objects.first().phone, "555-1212")
person = Person.objects.first() person = Person.objects.first()
person.address = Address(city="Londinium") person.address = Address(city="Londinium")
person.save() person.save()
assert Person.objects.first().address.city == "Londinium" self.assertEqual(Person.objects.first().address.city, "Londinium")
person = Person.objects.first() person = Person.objects.first()
person["age"] = 35 person["age"] = 35
person.save() person.save()
assert Person.objects.first().age == 35 self.assertEqual(Person.objects.first().age, 35)
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,564 @@
# -*- coding: utf-8 -*-
import unittest
import warnings
from six import iteritems
from mongoengine import (BooleanField, Document, EmbeddedDocument,
EmbeddedDocumentField, GenericReferenceField,
IntField, ReferenceField, StringField)
from mongoengine.pymongo_support import list_collection_names
from tests.utils import MongoDBTestCase
from tests.fixtures import Base
__all__ = ('InheritanceTest', )
class InheritanceTest(MongoDBTestCase):
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_constructor_cls(self):
# Ensures _cls is properly set during construction
# and when object gets reloaded (prevent regression of #1950)
class EmbedData(EmbeddedDocument):
data = StringField()
meta = {'allow_inheritance': True}
class DataDoc(Document):
name = StringField()
embed = EmbeddedDocumentField(EmbedData)
meta = {'allow_inheritance': True}
test_doc = DataDoc(name='test', embed=EmbedData(data='data'))
self.assertEqual(test_doc._cls, 'DataDoc')
self.assertEqual(test_doc.embed._cls, 'EmbedData')
test_doc.save()
saved_doc = DataDoc.objects.with_id(test_doc.id)
self.assertEqual(test_doc._cls, saved_doc._cls)
self.assertEqual(test_doc.embed._cls, saved_doc.embed._cls)
test_doc.delete()
def test_superclasses(self):
"""Ensure that the correct list of superclasses is assembled.
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Fish._superclasses, ('Animal',))
self.assertEqual(Guppy._superclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Mammal._superclasses, ('Animal',))
self.assertEqual(Dog._superclasses, ('Animal', 'Animal.Mammal'))
self.assertEqual(Human._superclasses, ('Animal', 'Animal.Mammal'))
def test_external_superclasses(self):
"""Ensure that the correct list of super classes is assembled when
importing part of the model.
"""
class Animal(Base): pass
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._superclasses, ('Base', ))
self.assertEqual(Fish._superclasses, ('Base', 'Base.Animal',))
self.assertEqual(Guppy._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Fish'))
self.assertEqual(Mammal._superclasses, ('Base', 'Base.Animal',))
self.assertEqual(Dog._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Mammal'))
self.assertEqual(Human._superclasses, ('Base', 'Base.Animal',
'Base.Animal.Mammal'))
def test_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled.
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._subclasses, ('Animal',
'Animal.Fish',
'Animal.Fish.Guppy',
'Animal.Mammal',
'Animal.Mammal.Dog',
'Animal.Mammal.Human'))
self.assertEqual(Fish._subclasses, ('Animal.Fish',
'Animal.Fish.Guppy',))
self.assertEqual(Guppy._subclasses, ('Animal.Fish.Guppy',))
self.assertEqual(Mammal._subclasses, ('Animal.Mammal',
'Animal.Mammal.Dog',
'Animal.Mammal.Human'))
self.assertEqual(Human._subclasses, ('Animal.Mammal.Human',))
def test_external_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled when importing part of the model.
"""
class Animal(Base): pass
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
self.assertEqual(Animal._subclasses, ('Base.Animal',
'Base.Animal.Fish',
'Base.Animal.Fish.Guppy',
'Base.Animal.Mammal',
'Base.Animal.Mammal.Dog',
'Base.Animal.Mammal.Human'))
self.assertEqual(Fish._subclasses, ('Base.Animal.Fish',
'Base.Animal.Fish.Guppy',))
self.assertEqual(Guppy._subclasses, ('Base.Animal.Fish.Guppy',))
self.assertEqual(Mammal._subclasses, ('Base.Animal.Mammal',
'Base.Animal.Mammal.Dog',
'Base.Animal.Mammal.Human'))
self.assertEqual(Human._subclasses, ('Base.Animal.Mammal.Human',))
def test_dynamic_declarations(self):
"""Test that declaring an extra class updates meta data"""
class Animal(Document):
meta = {'allow_inheritance': True}
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal',))
# Test dynamically adding a class changes the meta data
class Fish(Animal):
pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Fish._superclasses, ('Animal', ))
self.assertEqual(Fish._subclasses, ('Animal.Fish',))
# Test dynamically adding an inherited class changes the meta data
class Pike(Fish):
pass
self.assertEqual(Animal._superclasses, ())
self.assertEqual(Animal._subclasses, ('Animal', 'Animal.Fish',
'Animal.Fish.Pike'))
self.assertEqual(Fish._superclasses, ('Animal', ))
self.assertEqual(Fish._subclasses, ('Animal.Fish', 'Animal.Fish.Pike'))
self.assertEqual(Pike._superclasses, ('Animal', 'Animal.Fish'))
self.assertEqual(Pike._subclasses, ('Animal.Fish.Pike',))
def test_inheritance_meta_data(self):
"""Ensure that document may inherit fields from a superclass document.
"""
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
class Employee(Person):
salary = IntField()
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys()))
self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name())
def test_inheritance_to_mongo_keys(self):
"""Ensure that document may inherit fields from a superclass document.
"""
class Person(Document):
name = StringField()
age = IntField()
meta = {'allow_inheritance': True}
class Employee(Person):
salary = IntField()
self.assertEqual(['_cls', 'age', 'id', 'name', 'salary'],
sorted(Employee._fields.keys()))
self.assertEqual(Person(name="Bob", age=35).to_mongo().keys(),
['_cls', 'name', 'age'])
self.assertEqual(Employee(name="Bob", age=35, salary=0).to_mongo().keys(),
['_cls', 'name', 'age', 'salary'])
self.assertEqual(Employee._get_collection_name(),
Person._get_collection_name())
def test_indexes_and_multiple_inheritance(self):
""" Ensure that all of the indexes are created for a document with
multiple inheritance.
"""
class A(Document):
a = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['a']
}
class B(Document):
b = StringField()
meta = {
'allow_inheritance': True,
'indexes': ['b']
}
class C(A, B):
pass
A.drop_collection()
B.drop_collection()
C.drop_collection()
C.ensure_indexes()
self.assertEqual(
sorted([idx['key'] for idx in C._get_collection().index_information().values()]),
sorted([[(u'_cls', 1), (u'b', 1)], [(u'_id', 1)], [(u'_cls', 1), (u'a', 1)]])
)
def test_polymorphic_queries(self):
"""Ensure that the correct subclasses are returned from a query
"""
class Animal(Document):
meta = {'allow_inheritance': True}
class Fish(Animal): pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Human(Mammal): pass
Animal.drop_collection()
Animal().save()
Fish().save()
Mammal().save()
Dog().save()
Human().save()
classes = [obj.__class__ for obj in Animal.objects]
self.assertEqual(classes, [Animal, Fish, Mammal, Dog, Human])
classes = [obj.__class__ for obj in Mammal.objects]
self.assertEqual(classes, [Mammal, Dog, Human])
classes = [obj.__class__ for obj in Human.objects]
self.assertEqual(classes, [Human])
def test_allow_inheritance(self):
"""Ensure that inheritance is disabled by default on simple
classes and that _cls will not be used.
"""
class Animal(Document):
name = StringField()
# can't inherit because Animal didn't explicitly allow inheritance
with self.assertRaises(ValueError) as cm:
class Dog(Animal):
pass
self.assertIn("Document Animal may not be subclassed", str(cm.exception))
# Check that _cls etc aren't present on simple documents
dog = Animal(name='dog').save()
self.assertEqual(dog.to_mongo().keys(), ['_id', 'name'])
collection = self.db[Animal._get_collection_name()]
obj = collection.find_one()
self.assertNotIn('_cls', obj)
def test_cant_turn_off_inheritance_on_subclass(self):
"""Ensure if inheritance is on in a subclass you cant turn it off.
"""
class Animal(Document):
name = StringField()
meta = {'allow_inheritance': True}
with self.assertRaises(ValueError) as cm:
class Mammal(Animal):
meta = {'allow_inheritance': False}
self.assertEqual(str(cm.exception), 'Only direct subclasses of Document may set "allow_inheritance" to False')
def test_allow_inheritance_abstract_document(self):
"""Ensure that abstract documents can set inheritance rules and that
_cls will not be used.
"""
class FinalDocument(Document):
meta = {'abstract': True,
'allow_inheritance': False}
class Animal(FinalDocument):
name = StringField()
with self.assertRaises(ValueError) as cm:
class Mammal(Animal):
pass
# Check that _cls isn't present in simple documents
doc = Animal(name='dog')
self.assertNotIn('_cls', doc.to_mongo())
def test_using_abstract_class_in_reference_field(self):
# Ensures no regression of #1920
class AbstractHuman(Document):
meta = {'abstract': True}
class Dad(AbstractHuman):
name = StringField()
class Home(Document):
dad = ReferenceField(AbstractHuman) # Referencing the abstract class
address = StringField()
dad = Dad(name='5').save()
Home(dad=dad, address='street').save()
home = Home.objects.first()
home.address = 'garbage'
home.save() # Was failing with ValidationError
def test_abstract_class_referencing_self(self):
# Ensures no regression of #1920
class Human(Document):
meta = {'abstract': True}
creator = ReferenceField('self', dbref=True)
class User(Human):
name = StringField()
user = User(name='John').save()
user2 = User(name='Foo', creator=user).save()
user2 = User.objects.with_id(user2.id)
user2.name = 'Bar'
user2.save() # Was failing with ValidationError
def test_abstract_handle_ids_in_metaclass_properly(self):
class City(Document):
continent = StringField()
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 3)
self.assertEqual(berlin._fields_ordered[0], 'id')
def test_auto_id_not_set_if_specific_in_parent_class(self):
class City(Document):
continent = StringField()
city_id = IntField(primary_key=True)
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 3)
self.assertEqual(berlin._fields_ordered[0], 'city_id')
def test_auto_id_vs_non_pk_id_field(self):
class City(Document):
continent = StringField()
id = IntField()
meta = {'abstract': True,
'allow_inheritance': False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name='Berlin', continent='Europe')
self.assertEqual(len(berlin._db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._reverse_db_field_map), len(berlin._fields_ordered))
self.assertEqual(len(berlin._fields_ordered), 4)
self.assertEqual(berlin._fields_ordered[0], 'auto_id_0')
berlin.save()
self.assertEqual(berlin.pk, berlin.auto_id_0)
def test_abstract_document_creation_does_not_fail(self):
class City(Document):
continent = StringField()
meta = {'abstract': True,
'allow_inheritance': False}
city = City(continent='asia')
self.assertEqual(None, city.pk)
# TODO: expected error? Shouldn't we create a new error type?
with self.assertRaises(KeyError):
setattr(city, 'pk', 1)
def test_allow_inheritance_embedded_document(self):
"""Ensure embedded documents respect inheritance."""
class Comment(EmbeddedDocument):
content = StringField()
with self.assertRaises(ValueError):
class SpecialComment(Comment):
pass
doc = Comment(content='test')
self.assertNotIn('_cls', doc.to_mongo())
class Comment(EmbeddedDocument):
content = StringField()
meta = {'allow_inheritance': True}
doc = Comment(content='test')
self.assertIn('_cls', doc.to_mongo())
def test_document_inheritance(self):
"""Ensure mutliple inheritance of abstract documents
"""
class DateCreatedDocument(Document):
meta = {
'allow_inheritance': True,
'abstract': True,
}
class DateUpdatedDocument(Document):
meta = {
'allow_inheritance': True,
'abstract': True,
}
try:
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
pass
except Exception:
self.assertTrue(False, "Couldn't create MyDocument class")
def test_abstract_documents(self):
"""Ensure that a document superclass can be marked as abstract
thereby not using it as the name for the collection."""
defaults = {'index_background': True,
'index_drop_dups': True,
'index_opts': {'hello': 'world'},
'allow_inheritance': True,
'queryset_class': 'QuerySet',
'db_alias': 'myDB',
'shard_key': ('hello', 'world')}
meta_settings = {'abstract': True}
meta_settings.update(defaults)
class Animal(Document):
name = StringField()
meta = meta_settings
class Fish(Animal): pass
class Guppy(Fish): pass
class Mammal(Animal):
meta = {'abstract': True}
class Human(Mammal): pass
for k, v in iteritems(defaults):
for cls in [Animal, Fish, Guppy]:
self.assertEqual(cls._meta[k], v)
self.assertNotIn('collection', Animal._meta)
self.assertNotIn('collection', Mammal._meta)
self.assertEqual(Animal._get_collection_name(), None)
self.assertEqual(Mammal._get_collection_name(), None)
self.assertEqual(Fish._get_collection_name(), 'fish')
self.assertEqual(Guppy._get_collection_name(), 'fish')
self.assertEqual(Human._get_collection_name(), 'human')
# ensure that a subclass of a non-abstract class can't be abstract
with self.assertRaises(ValueError):
class EvilHuman(Human):
evil = BooleanField(default=True)
meta = {'abstract': True}
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract
class A(EmbeddedDocument):
meta = {"abstract": True}
class B(A):
pass
self.assertFalse(B._meta["abstract"])
def test_inherited_collections(self):
"""Ensure that subclassed documents don't override parents'
collections
"""
class Drink(Document):
name = StringField()
meta = {'allow_inheritance': True}
class Drinker(Document):
drink = GenericReferenceField()
try:
warnings.simplefilter("error")
class AcloholicDrink(Drink):
meta = {'collection': 'booze'}
except SyntaxWarning:
warnings.simplefilter("ignore")
class AlcoholicDrink(Drink):
meta = {'collection': 'booze'}
else:
raise AssertionError("SyntaxWarning should be triggered")
warnings.resetwarnings()
Drink.drop_collection()
AlcoholicDrink.drop_collection()
Drinker.drop_collection()
red_bull = Drink(name='Red Bull')
red_bull.save()
programmer = Drinker(drink=red_bull)
programmer.save()
beer = AlcoholicDrink(name='Beer')
beer.save()
real_person = Drinker(drink=beer)
real_person.save()
self.assertEqual(Drinker.objects[0].drink.name, red_bull.name)
self.assertEqual(Drinker.objects[1].drink.name, beer.name)
if __name__ == '__main__':
unittest.main()

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,22 @@
import unittest import unittest
import uuid import uuid
from datetime import datetime
from nose.plugins.skip import SkipTest
from datetime import datetime
from bson import ObjectId from bson import ObjectId
import pymongo
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase
__all__ = ("TestJson",)
class TestJson(MongoDBTestCase): class TestJson(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
def test_json_names(self): def test_json_names(self):
""" """
Going to test reported issue: Going to test reported issue:
@ -17,24 +25,22 @@ class TestJson(MongoDBTestCase):
a to_json with the original class names and not the abreviated a to_json with the original class names and not the abreviated
mongodb document keys mongodb document keys
""" """
class Embedded(EmbeddedDocument): class Embedded(EmbeddedDocument):
string = StringField(db_field="s") string = StringField(db_field='s')
class Doc(Document): class Doc(Document):
string = StringField(db_field="s") string = StringField(db_field='s')
embedded = EmbeddedDocumentField(Embedded, db_field="e") embedded = EmbeddedDocumentField(Embedded, db_field='e')
doc = Doc(string="Hello", embedded=Embedded(string="Inner Hello")) doc = Doc(string="Hello", embedded=Embedded(string="Inner Hello"))
doc_json = doc.to_json( doc_json = doc.to_json(sort_keys=True, use_db_field=False, separators=(',', ':'))
sort_keys=True, use_db_field=False, separators=(",", ":")
)
expected_json = """{"embedded":{"string":"Inner Hello"},"string":"Hello"}""" expected_json = """{"embedded":{"string":"Inner Hello"},"string":"Hello"}"""
assert doc_json == expected_json self.assertEqual(doc_json, expected_json)
def test_json_simple(self): def test_json_simple(self):
class Embedded(EmbeddedDocument): class Embedded(EmbeddedDocument):
string = StringField() string = StringField()
@ -43,18 +49,16 @@ class TestJson(MongoDBTestCase):
embedded_field = EmbeddedDocumentField(Embedded) embedded_field = EmbeddedDocumentField(Embedded)
def __eq__(self, other): def __eq__(self, other):
return ( return (self.string == other.string and
self.string == other.string self.embedded_field == other.embedded_field)
and self.embedded_field == other.embedded_field
)
doc = Doc(string="Hi", embedded_field=Embedded(string="Hi")) doc = Doc(string="Hi", embedded_field=Embedded(string="Hi"))
doc_json = doc.to_json(sort_keys=True, separators=(",", ":")) doc_json = doc.to_json(sort_keys=True, separators=(',', ':'))
expected_json = """{"embedded_field":{"string":"Hi"},"string":"Hi"}""" expected_json = """{"embedded_field":{"string":"Hi"},"string":"Hi"}"""
assert doc_json == expected_json self.assertEqual(doc_json, expected_json)
assert doc == Doc.from_json(doc.to_json()) self.assertEqual(doc, Doc.from_json(doc.to_json()))
def test_json_complex(self): def test_json_complex(self):
class EmbeddedDoc(EmbeddedDocument): class EmbeddedDoc(EmbeddedDocument):
@ -64,43 +68,41 @@ class TestJson(MongoDBTestCase):
pass pass
class Doc(Document): class Doc(Document):
string_field = StringField(default="1") string_field = StringField(default='1')
int_field = IntField(default=1) int_field = IntField(default=1)
float_field = FloatField(default=1.1) float_field = FloatField(default=1.1)
boolean_field = BooleanField(default=True) boolean_field = BooleanField(default=True)
datetime_field = DateTimeField(default=datetime.now) datetime_field = DateTimeField(default=datetime.now)
embedded_document_field = EmbeddedDocumentField( embedded_document_field = EmbeddedDocumentField(EmbeddedDoc,
EmbeddedDoc, default=lambda: EmbeddedDoc() default=lambda: EmbeddedDoc())
)
list_field = ListField(default=lambda: [1, 2, 3]) list_field = ListField(default=lambda: [1, 2, 3])
dict_field = DictField(default=lambda: {"hello": "world"}) dict_field = DictField(default=lambda: {"hello": "world"})
objectid_field = ObjectIdField(default=ObjectId) objectid_field = ObjectIdField(default=ObjectId)
reference_field = ReferenceField(Simple, default=lambda: Simple().save()) reference_field = ReferenceField(Simple, default=lambda:
Simple().save())
map_field = MapField(IntField(), default=lambda: {"simple": 1}) map_field = MapField(IntField(), default=lambda: {"simple": 1})
decimal_field = DecimalField(default=1.0) decimal_field = DecimalField(default=1.0)
complex_datetime_field = ComplexDateTimeField(default=datetime.now) complex_datetime_field = ComplexDateTimeField(default=datetime.now)
url_field = URLField(default="http://mongoengine.org") url_field = URLField(default="http://mongoengine.org")
dynamic_field = DynamicField(default=1) dynamic_field = DynamicField(default=1)
generic_reference_field = GenericReferenceField( generic_reference_field = GenericReferenceField(
default=lambda: Simple().save() default=lambda: Simple().save())
) sorted_list_field = SortedListField(IntField(),
sorted_list_field = SortedListField(IntField(), default=lambda: [1, 2, 3]) default=lambda: [1, 2, 3])
email_field = EmailField(default="ross@example.com") email_field = EmailField(default="ross@example.com")
geo_point_field = GeoPointField(default=lambda: [1, 2]) geo_point_field = GeoPointField(default=lambda: [1, 2])
sequence_field = SequenceField() sequence_field = SequenceField()
uuid_field = UUIDField(default=uuid.uuid4) uuid_field = UUIDField(default=uuid.uuid4)
generic_embedded_document_field = GenericEmbeddedDocumentField( generic_embedded_document_field = GenericEmbeddedDocumentField(
default=lambda: EmbeddedDoc() default=lambda: EmbeddedDoc())
)
def __eq__(self, other): def __eq__(self, other):
import json import json
return json.loads(self.to_json()) == json.loads(other.to_json()) return json.loads(self.to_json()) == json.loads(other.to_json())
doc = Doc() doc = Doc()
assert doc == Doc.from_json(doc.to_json()) self.assertEqual(doc, Doc.from_json(doc.to_json()))
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,349 +0,0 @@
import unittest
from mongoengine import *
from mongoengine.connection import get_db
from mongoengine.pymongo_support import list_collection_names
from mongoengine.queryset import NULLIFY, PULL
class TestClassMethods(unittest.TestCase):
def setUp(self):
connect(db="mongoenginetest")
self.db = get_db()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_definition(self):
"""Ensure that document may be defined using fields."""
assert ["_cls", "age", "id", "name"] == sorted(self.Person._fields.keys())
assert ["IntField", "ObjectIdField", "StringField", "StringField"] == sorted(
x.__class__.__name__ for x in self.Person._fields.values()
)
def test_get_db(self):
"""Ensure that get_db returns the expected db."""
db = self.Person._get_db()
assert self.db == db
def test_get_collection_name(self):
"""Ensure that get_collection_name returns the expected collection
name.
"""
collection_name = "person"
assert collection_name == self.Person._get_collection_name()
def test_get_collection(self):
"""Ensure that get_collection returns the expected collection."""
collection_name = "person"
collection = self.Person._get_collection()
assert self.db[collection_name] == collection
def test_drop_collection(self):
"""Ensure that the collection may be dropped from the database."""
collection_name = "person"
self.Person(name="Test").save()
assert collection_name in list_collection_names(self.db)
self.Person.drop_collection()
assert collection_name not in list_collection_names(self.db)
def test_register_delete_rule(self):
"""Ensure that register delete rule adds a delete rule to the document
meta.
"""
class Job(Document):
employee = ReferenceField(self.Person)
assert self.Person._meta.get("delete_rules") is None
self.Person.register_delete_rule(Job, "employee", NULLIFY)
assert self.Person._meta["delete_rules"] == {(Job, "employee"): NULLIFY}
def test_compare_indexes(self):
"""Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
tags = StringField()
meta = {"indexes": [("author", "title")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPost.ensure_index(["author", "description"])
assert BlogPost.compare_indexes() == {
"missing": [],
"extra": [[("author", 1), ("description", 1)]],
}
BlogPost._get_collection().drop_index("author_1_description_1")
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPost._get_collection().drop_index("author_1_title_1")
assert BlogPost.compare_indexes() == {
"missing": [[("author", 1), ("title", 1)]],
"extra": [],
}
def test_compare_indexes_inheritance(self):
"""Ensure that the indexes are properly created and that
compare_indexes identifies the missing/extra indexes for subclassed
documents (_cls included)
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {"indexes": [("author", "tags")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPostWithTags.ensure_index(["author", "tag_list"])
assert BlogPost.compare_indexes() == {
"missing": [],
"extra": [[("_cls", 1), ("author", 1), ("tag_list", 1)]],
}
BlogPostWithTags._get_collection().drop_index("_cls_1_author_1_tag_list_1")
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
BlogPostWithTags._get_collection().drop_index("_cls_1_author_1_tags_1")
assert BlogPost.compare_indexes() == {
"missing": [[("_cls", 1), ("author", 1), ("tags", 1)]],
"extra": [],
}
def test_compare_indexes_multiple_subclasses(self):
"""Ensure that compare_indexes behaves correctly if called from a
class, which base class has multiple subclasses
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
tag_list = ListField(StringField())
meta = {"indexes": [("author", "tags")]}
class BlogPostWithCustomField(BlogPost):
custom = DictField()
meta = {"indexes": [("author", "custom")]}
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithCustomField.ensure_indexes()
assert BlogPost.compare_indexes() == {"missing": [], "extra": []}
assert BlogPostWithTags.compare_indexes() == {"missing": [], "extra": []}
assert BlogPostWithCustomField.compare_indexes() == {"missing": [], "extra": []}
def test_compare_indexes_for_text_indexes(self):
"""Ensure that compare_indexes behaves correctly for text indexes"""
class Doc(Document):
a = StringField()
b = StringField()
meta = {
"indexes": [
{
"fields": ["$a", "$b"],
"default_language": "english",
"weights": {"a": 10, "b": 2},
}
]
}
Doc.drop_collection()
Doc.ensure_indexes()
actual = Doc.compare_indexes()
expected = {"missing": [], "extra": []}
assert actual == expected
def test_list_indexes_inheritance(self):
"""ensure that all of the indexes are listed regardless of the super-
or sub-class that we call it from
"""
class BlogPost(Document):
author = StringField()
title = StringField()
description = StringField()
meta = {"allow_inheritance": True}
class BlogPostWithTags(BlogPost):
tags = StringField()
meta = {"indexes": [("author", "tags")]}
class BlogPostWithTagsAndExtraText(BlogPostWithTags):
extra_text = StringField()
meta = {"indexes": [("author", "tags", "extra_text")]}
BlogPost.drop_collection()
BlogPost.ensure_indexes()
BlogPostWithTags.ensure_indexes()
BlogPostWithTagsAndExtraText.ensure_indexes()
assert BlogPost.list_indexes() == BlogPostWithTags.list_indexes()
assert BlogPost.list_indexes() == BlogPostWithTagsAndExtraText.list_indexes()
assert BlogPost.list_indexes() == [
[("_cls", 1), ("author", 1), ("tags", 1)],
[("_cls", 1), ("author", 1), ("tags", 1), ("extra_text", 1)],
[("_id", 1)],
[("_cls", 1)],
]
def test_register_delete_rule_inherited(self):
class Vaccine(Document):
name = StringField(required=True)
meta = {"indexes": ["name"]}
class Animal(Document):
family = StringField(required=True)
vaccine_made = ListField(
ReferenceField("Vaccine", reverse_delete_rule=PULL)
)
meta = {"allow_inheritance": True, "indexes": ["family"]}
class Cat(Animal):
name = StringField(required=True)
assert Vaccine._meta["delete_rules"][(Animal, "vaccine_made")] == PULL
assert Vaccine._meta["delete_rules"][(Cat, "vaccine_made")] == PULL
def test_collection_naming(self):
"""Ensure that a collection with a specified name may be used."""
class DefaultNamingTest(Document):
pass
assert "default_naming_test" == DefaultNamingTest._get_collection_name()
class CustomNamingTest(Document):
meta = {"collection": "pimp_my_collection"}
assert "pimp_my_collection" == CustomNamingTest._get_collection_name()
class DynamicNamingTest(Document):
meta = {"collection": lambda c: "DYNAMO"}
assert "DYNAMO" == DynamicNamingTest._get_collection_name()
# Use Abstract class to handle backwards compatibility
class BaseDocument(Document):
meta = {"abstract": True, "collection": lambda c: c.__name__.lower()}
class OldNamingConvention(BaseDocument):
pass
assert "oldnamingconvention" == OldNamingConvention._get_collection_name()
class InheritedAbstractNamingTest(BaseDocument):
meta = {"collection": "wibble"}
assert "wibble" == InheritedAbstractNamingTest._get_collection_name()
# Mixin tests
class BaseMixin:
meta = {"collection": lambda c: c.__name__.lower()}
class OldMixinNamingConvention(Document, BaseMixin):
pass
assert (
"oldmixinnamingconvention"
== OldMixinNamingConvention._get_collection_name()
)
class BaseMixin:
meta = {"collection": lambda c: c.__name__.lower()}
class BaseDocument(Document, BaseMixin):
meta = {"allow_inheritance": True}
class MyDocument(BaseDocument):
pass
assert "basedocument" == MyDocument._get_collection_name()
def test_custom_collection_name_operations(self):
"""Ensure that a collection with a specified name is used as expected."""
collection_name = "personCollTest"
class Person(Document):
name = StringField()
meta = {"collection": collection_name}
Person(name="Test User").save()
assert collection_name in list_collection_names(self.db)
user_obj = self.db[collection_name].find_one()
assert user_obj["name"] == "Test User"
user_obj = Person.objects[0]
assert user_obj.name == "Test User"
Person.drop_collection()
assert collection_name not in list_collection_names(self.db)
def test_collection_name_and_primary(self):
"""Ensure that a collection with a specified name may be used."""
class Person(Document):
name = StringField(primary_key=True)
meta = {"collection": "app"}
Person(name="Test User").save()
user_obj = Person.objects.first()
assert user_obj.name == "Test User"
Person.drop_collection()
if __name__ == "__main__":
unittest.main()

View File

@ -1,957 +0,0 @@
import unittest
from bson import SON
from mongoengine import *
from mongoengine.pymongo_support import list_collection_names
from tests.utils import MongoDBTestCase
class TestDelta(MongoDBTestCase):
def setUp(self):
super().setUp()
class Person(Document):
name = StringField()
age = IntField()
non_field = True
meta = {"allow_inheritance": True}
self.Person = Person
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_delta(self):
self.delta(Document)
self.delta(DynamicDocument)
@staticmethod
def delta(DocClass):
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["string_field"]
assert doc._delta() == ({"string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["int_field"]
assert doc._delta() == ({"int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({"dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({"list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({}, {"dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({}, {"list_field": 1})
def test_delta_recursive(self):
self.delta_recursive(Document, EmbeddedDocument)
self.delta_recursive(DynamicDocument, EmbeddedDocument)
self.delta_recursive(Document, DynamicEmbeddedDocument)
self.delta_recursive(DynamicDocument, DynamicEmbeddedDocument)
def delta_recursive(self, DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
id = StringField()
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
class Doc(DocClass):
string_field = StringField()
int_field = IntField()
dict_field = DictField()
list_field = ListField()
embedded_field = EmbeddedDocumentField(Embedded)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
embedded_1 = Embedded()
embedded_1.id = "010101"
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"}
embedded_1.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field = embedded_1
assert doc._get_changed_fields() == ["embedded_field"]
embedded_delta = {
"id": "010101",
"string_field": "hello",
"int_field": 1,
"dict_field": {"hello": "world"},
"list_field": ["1", 2, {"hello": "world"}],
}
assert doc.embedded_field._delta() == (embedded_delta, {})
assert doc._delta() == ({"embedded_field": embedded_delta}, {})
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
assert doc._get_changed_fields() == ["embedded_field.dict_field"]
assert doc.embedded_field._delta() == ({}, {"dict_field": 1})
assert doc._delta() == ({}, {"embedded_field.dict_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.dict_field == {}
doc.embedded_field.list_field = []
assert doc._get_changed_fields() == ["embedded_field.list_field"]
assert doc.embedded_field._delta() == ({}, {"list_field": 1})
assert doc._delta() == ({}, {"embedded_field.list_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field == []
embedded_2 = Embedded()
embedded_2.string_field = "hello"
embedded_2.int_field = 1
embedded_2.dict_field = {"hello": "world"}
embedded_2.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field.list_field = ["1", 2, embedded_2]
assert doc._get_changed_fields() == ["embedded_field.list_field"]
assert doc.embedded_field._delta() == (
{
"list_field": [
"1",
2,
{
"_cls": "Embedded",
"string_field": "hello",
"dict_field": {"hello": "world"},
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
assert doc._delta() == (
{
"embedded_field.list_field": [
"1",
2,
{
"_cls": "Embedded",
"string_field": "hello",
"dict_field": {"hello": "world"},
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[0] == "1"
assert doc.embedded_field.list_field[1] == 2
for k in doc.embedded_field.list_field[2]._fields:
assert doc.embedded_field.list_field[2][k] == embedded_2[k]
doc.embedded_field.list_field[2].string_field = "world"
assert doc._get_changed_fields() == ["embedded_field.list_field.2.string_field"]
assert doc.embedded_field._delta() == (
{"list_field.2.string_field": "world"},
{},
)
assert doc._delta() == (
{"embedded_field.list_field.2.string_field": "world"},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "world"
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = "hello world"
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
assert doc._get_changed_fields() == ["embedded_field.list_field.2"]
assert doc.embedded_field._delta() == (
{
"list_field.2": {
"_cls": "Embedded",
"string_field": "hello world",
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
"dict_field": {"hello": "world"},
}
},
{},
)
assert doc._delta() == (
{
"embedded_field.list_field.2": {
"_cls": "Embedded",
"string_field": "hello world",
"int_field": 1,
"list_field": ["1", 2, {"hello": "world"}],
"dict_field": {"hello": "world"},
}
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "hello world"
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
assert doc._delta() == (
{"embedded_field.list_field.2.list_field": [2, {"hello": "world"}]},
{},
)
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
assert doc._delta() == (
{"embedded_field.list_field.2.list_field": [2, {"hello": "world"}, 1]},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [2, {"hello": "world"}, 1]
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [1, 2, {"hello": "world"}]
del doc.embedded_field.list_field[2].list_field[2]["hello"]
assert doc._delta() == (
{},
{"embedded_field.list_field.2.list_field.2.hello": 1},
)
doc.save()
doc = doc.reload(10)
del doc.embedded_field.list_field[2].list_field
assert doc._delta() == ({}, {"embedded_field.list_field.2.list_field": 1})
doc.save()
doc = doc.reload(10)
doc.dict_field["Embedded"] = embedded_1
doc.save()
doc = doc.reload(10)
doc.dict_field["Embedded"].string_field = "Hello World"
assert doc._get_changed_fields() == ["dict_field.Embedded.string_field"]
assert doc._delta() == ({"dict_field.Embedded.string_field": "Hello World"}, {})
def test_circular_reference_deltas(self):
self.circular_reference_deltas(Document, Document)
self.circular_reference_deltas(Document, DynamicDocument)
self.circular_reference_deltas(DynamicDocument, Document)
self.circular_reference_deltas(DynamicDocument, DynamicDocument)
def circular_reference_deltas(self, DocClass1, DocClass2):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField("Organization"))
class Organization(DocClass2):
name = StringField()
owner = ReferenceField("Person")
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
person.save()
organization.save()
p = Person.objects[0].select_related()
o = Organization.objects.first()
assert p.owns[0] == o
assert o.owner == p
def test_circular_reference_deltas_2(self):
self.circular_reference_deltas_2(Document, Document)
self.circular_reference_deltas_2(Document, DynamicDocument)
self.circular_reference_deltas_2(DynamicDocument, Document)
self.circular_reference_deltas_2(DynamicDocument, DynamicDocument)
def circular_reference_deltas_2(self, DocClass1, DocClass2, dbref=True):
class Person(DocClass1):
name = StringField()
owns = ListField(ReferenceField("Organization", dbref=dbref))
employer = ReferenceField("Organization", dbref=dbref)
class Organization(DocClass2):
name = StringField()
owner = ReferenceField("Person", dbref=dbref)
employees = ListField(ReferenceField("Person", dbref=dbref))
Person.drop_collection()
Organization.drop_collection()
person = Person(name="owner").save()
employee = Person(name="employee").save()
organization = Organization(name="company").save()
person.owns.append(organization)
organization.owner = person
organization.employees.append(employee)
employee.employer = organization
person.save()
organization.save()
employee.save()
p = Person.objects.get(name="owner")
e = Person.objects.get(name="employee")
o = Organization.objects.first()
assert p.owns[0] == o
assert o.owner == p
assert e.employer == o
return person, organization, employee
def test_delta_db_field(self):
self.delta_db_field(Document)
self.delta_db_field(DynamicDocument)
def delta_db_field(self, DocClass):
class Doc(DocClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["db_string_field"]
assert doc._delta() == ({"db_string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["db_int_field"]
assert doc._delta() == ({"db_int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["db_dict_field"]
assert doc._delta() == ({"db_dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["db_list_field"]
assert doc._delta() == ({"db_list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["db_dict_field"]
assert doc._delta() == ({}, {"db_dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["db_list_field"]
assert doc._delta() == ({}, {"db_list_field": 1})
# Test it saves that data
doc = Doc()
doc.save()
doc.string_field = "hello"
doc.int_field = 1
doc.dict_field = {"hello": "world"}
doc.list_field = ["1", 2, {"hello": "world"}]
doc.save()
doc = doc.reload(10)
assert doc.string_field == "hello"
assert doc.int_field == 1
assert doc.dict_field == {"hello": "world"}
assert doc.list_field == ["1", 2, {"hello": "world"}]
def test_delta_recursive_db_field_on_doc_and_embeddeddoc(self):
self.delta_recursive_db_field(Document, EmbeddedDocument)
def test_delta_recursive_db_field_on_doc_and_dynamicembeddeddoc(self):
self.delta_recursive_db_field(Document, DynamicEmbeddedDocument)
def test_delta_recursive_db_field_on_dynamicdoc_and_embeddeddoc(self):
self.delta_recursive_db_field(DynamicDocument, EmbeddedDocument)
def test_delta_recursive_db_field_on_dynamicdoc_and_dynamicembeddeddoc(self):
self.delta_recursive_db_field(DynamicDocument, DynamicEmbeddedDocument)
@staticmethod
def delta_recursive_db_field(DocClass, EmbeddedClass):
class Embedded(EmbeddedClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
class Doc(DocClass):
string_field = StringField(db_field="db_string_field")
int_field = IntField(db_field="db_int_field")
dict_field = DictField(db_field="db_dict_field")
list_field = ListField(db_field="db_list_field")
embedded_field = EmbeddedDocumentField(
Embedded, db_field="db_embedded_field"
)
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
embedded_1 = Embedded()
embedded_1.string_field = "hello"
embedded_1.int_field = 1
embedded_1.dict_field = {"hello": "world"}
embedded_1.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field = embedded_1
assert doc._get_changed_fields() == ["db_embedded_field"]
embedded_delta = {
"db_string_field": "hello",
"db_int_field": 1,
"db_dict_field": {"hello": "world"},
"db_list_field": ["1", 2, {"hello": "world"}],
}
assert doc.embedded_field._delta() == (embedded_delta, {})
assert doc._delta() == ({"db_embedded_field": embedded_delta}, {})
doc.save()
doc = doc.reload(10)
doc.embedded_field.dict_field = {}
assert doc._get_changed_fields() == ["db_embedded_field.db_dict_field"]
assert doc.embedded_field._delta() == ({}, {"db_dict_field": 1})
assert doc._delta() == ({}, {"db_embedded_field.db_dict_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.dict_field == {}
assert doc._get_changed_fields() == []
doc.embedded_field.list_field = []
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field"]
assert doc.embedded_field._delta() == ({}, {"db_list_field": 1})
assert doc._delta() == ({}, {"db_embedded_field.db_list_field": 1})
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field == []
embedded_2 = Embedded()
embedded_2.string_field = "hello"
embedded_2.int_field = 1
embedded_2.dict_field = {"hello": "world"}
embedded_2.list_field = ["1", 2, {"hello": "world"}]
doc.embedded_field.list_field = ["1", 2, embedded_2]
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field"]
assert doc.embedded_field._delta() == (
{
"db_list_field": [
"1",
2,
{
"_cls": "Embedded",
"db_string_field": "hello",
"db_dict_field": {"hello": "world"},
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
assert doc._delta() == (
{
"db_embedded_field.db_list_field": [
"1",
2,
{
"_cls": "Embedded",
"db_string_field": "hello",
"db_dict_field": {"hello": "world"},
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
},
]
},
{},
)
doc.save()
assert doc._get_changed_fields() == []
doc = doc.reload(10)
assert doc.embedded_field.list_field[0] == "1"
assert doc.embedded_field.list_field[1] == 2
for k in doc.embedded_field.list_field[2]._fields:
assert doc.embedded_field.list_field[2][k] == embedded_2[k]
doc.embedded_field.list_field[2].string_field = "world"
assert doc._get_changed_fields() == [
"db_embedded_field.db_list_field.2.db_string_field"
]
assert doc.embedded_field._delta() == (
{"db_list_field.2.db_string_field": "world"},
{},
)
assert doc._delta() == (
{"db_embedded_field.db_list_field.2.db_string_field": "world"},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "world"
# Test multiple assignments
doc.embedded_field.list_field[2].string_field = "hello world"
doc.embedded_field.list_field[2] = doc.embedded_field.list_field[2]
assert doc._get_changed_fields() == ["db_embedded_field.db_list_field.2"]
assert doc.embedded_field._delta() == (
{
"db_list_field.2": {
"_cls": "Embedded",
"db_string_field": "hello world",
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
"db_dict_field": {"hello": "world"},
}
},
{},
)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2": {
"_cls": "Embedded",
"db_string_field": "hello world",
"db_int_field": 1,
"db_list_field": ["1", 2, {"hello": "world"}],
"db_dict_field": {"hello": "world"},
}
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].string_field == "hello world"
# Test list native methods
doc.embedded_field.list_field[2].list_field.pop(0)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2.db_list_field": [
2,
{"hello": "world"},
]
},
{},
)
doc.save()
doc = doc.reload(10)
doc.embedded_field.list_field[2].list_field.append(1)
assert doc._delta() == (
{
"db_embedded_field.db_list_field.2.db_list_field": [
2,
{"hello": "world"},
1,
]
},
{},
)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [2, {"hello": "world"}, 1]
doc.embedded_field.list_field[2].list_field.sort(key=str)
doc.save()
doc = doc.reload(10)
assert doc.embedded_field.list_field[2].list_field == [1, 2, {"hello": "world"}]
del doc.embedded_field.list_field[2].list_field[2]["hello"]
assert doc._delta() == (
{},
{"db_embedded_field.db_list_field.2.db_list_field.2.hello": 1},
)
doc.save()
doc = doc.reload(10)
assert doc._delta() == (
{},
{},
)
del doc.embedded_field.list_field[2].list_field
assert doc._delta() == (
{},
{"db_embedded_field.db_list_field.2.db_list_field": 1},
)
def test_delta_for_dynamic_documents(self):
class Person(DynamicDocument):
name = StringField()
meta = {"allow_inheritance": True}
Person.drop_collection()
p = Person(name="James", age=34)
assert p._delta() == (
SON([("_cls", "Person"), ("name", "James"), ("age", 34)]),
{},
)
p.doc = 123
del p.doc
assert p._delta() == (
SON([("_cls", "Person"), ("name", "James"), ("age", 34)]),
{},
)
p = Person()
p.name = "Dean"
p.age = 22
p.save()
p.age = 24
assert p.age == 24
assert p._get_changed_fields() == ["age"]
assert p._delta() == ({"age": 24}, {})
p = Person.objects(age=22).get()
p.age = 24
assert p.age == 24
assert p._get_changed_fields() == ["age"]
assert p._delta() == ({"age": 24}, {})
p.save()
assert 1 == Person.objects(age=24).count()
def test_dynamic_delta(self):
class Doc(DynamicDocument):
pass
Doc.drop_collection()
doc = Doc()
doc.save()
doc = Doc.objects.first()
assert doc._get_changed_fields() == []
assert doc._delta() == ({}, {})
doc.string_field = "hello"
assert doc._get_changed_fields() == ["string_field"]
assert doc._delta() == ({"string_field": "hello"}, {})
doc._changed_fields = []
doc.int_field = 1
assert doc._get_changed_fields() == ["int_field"]
assert doc._delta() == ({"int_field": 1}, {})
doc._changed_fields = []
dict_value = {"hello": "world", "ping": "pong"}
doc.dict_field = dict_value
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({"dict_field": dict_value}, {})
doc._changed_fields = []
list_value = ["1", 2, {"hello": "world"}]
doc.list_field = list_value
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({"list_field": list_value}, {})
# Test unsetting
doc._changed_fields = []
doc.dict_field = {}
assert doc._get_changed_fields() == ["dict_field"]
assert doc._delta() == ({}, {"dict_field": 1})
doc._changed_fields = []
doc.list_field = []
assert doc._get_changed_fields() == ["list_field"]
assert doc._delta() == ({}, {"list_field": 1})
def test_delta_with_dbref_true(self):
person, organization, employee = self.circular_reference_deltas_2(
Document, Document, True
)
employee.name = "test"
assert organization._get_changed_fields() == []
updates, removals = organization._delta()
assert removals == {}
assert updates == {}
organization.employees.append(person)
updates, removals = organization._delta()
assert removals == {}
assert "employees" in updates
def test_delta_with_dbref_false(self):
person, organization, employee = self.circular_reference_deltas_2(
Document, Document, False
)
employee.name = "test"
assert organization._get_changed_fields() == []
updates, removals = organization._delta()
assert removals == {}
assert updates == {}
organization.employees.append(person)
updates, removals = organization._delta()
assert removals == {}
assert "employees" in updates
def test_nested_nested_fields_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(MapField(EmbeddedDocumentField(EmbeddedDoc)))
name = StringField()
MyDoc.drop_collection()
MyDoc(name="testcase1", subs={"a": {"b": EmbeddedDoc(name="foo")}}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs["a"]["b"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a.b.name"]
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_nested_nested_fields_db_field_set__gets_mark_as_changed_and_cleaned(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField(db_field="db_name")
class MyDoc(Document):
embed = EmbeddedDocumentField(EmbeddedDoc, db_field="db_embed")
name = StringField(db_field="db_name")
MyDoc.drop_collection()
MyDoc(name="testcase1", embed=EmbeddedDoc(name="foo")).save()
mydoc = MyDoc.objects.first()
mydoc.embed.name = "foo1"
assert mydoc.embed._get_changed_fields() == ["db_name"]
assert mydoc._get_changed_fields() == ["db_embed.db_name"]
mydoc = MyDoc.objects.first()
embed = EmbeddedDoc(name="foo2")
embed.name = "bar"
mydoc.embed = embed
assert embed._get_changed_fields() == ["db_name"]
assert mydoc._get_changed_fields() == ["db_embed"]
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_lower_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc().save()
mydoc = MyDoc.objects.first()
mydoc.subs["a"] = EmbeddedDoc()
assert mydoc._get_changed_fields() == ["subs.a"]
subdoc = mydoc.subs["a"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a"]
mydoc.save()
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_upper_level_mark_as_changed(self):
class EmbeddedDoc(EmbeddedDocument):
name = StringField()
class MyDoc(Document):
subs = MapField(EmbeddedDocumentField(EmbeddedDoc))
MyDoc.drop_collection()
MyDoc(subs={"a": EmbeddedDoc(name="foo")}).save()
mydoc = MyDoc.objects.first()
subdoc = mydoc.subs["a"]
subdoc.name = "bar"
assert subdoc._get_changed_fields() == ["name"]
assert mydoc._get_changed_fields() == ["subs.a.name"]
mydoc.subs["a"] = EmbeddedDoc()
assert mydoc._get_changed_fields() == ["subs.a"]
mydoc.save()
mydoc._clear_changed_fields()
assert mydoc._get_changed_fields() == []
def test_referenced_object_changed_attributes(self):
"""Ensures that when you save a new reference to a field, the referenced object isn't altered"""
class Organization(Document):
name = StringField()
class User(Document):
name = StringField()
org = ReferenceField("Organization", required=True)
Organization.drop_collection()
User.drop_collection()
org1 = Organization(name="Org 1")
org1.save()
org2 = Organization(name="Org 2")
org2.save()
user = User(name="Fred", org=org1)
user.save()
org1.reload()
org2.reload()
user.reload()
assert org1.name == "Org 1"
assert org2.name == "Org 2"
assert user.name == "Fred"
user.name = "Harold"
user.org = org2
org2.name = "New Org 2"
assert org2.name == "New Org 2"
user.save()
org2.save()
assert org2.name == "New Org 2"
org2.reload()
assert org2.name == "New Org 2"
def test_delta_for_nested_map_fields(self):
class UInfoDocument(Document):
phone = StringField()
class EmbeddedRole(EmbeddedDocument):
type = StringField()
class EmbeddedUser(EmbeddedDocument):
name = StringField()
roles = MapField(field=EmbeddedDocumentField(EmbeddedRole))
rolist = ListField(field=EmbeddedDocumentField(EmbeddedRole))
info = ReferenceField(UInfoDocument)
class Doc(Document):
users = MapField(field=EmbeddedDocumentField(EmbeddedUser))
num = IntField(default=-1)
Doc.drop_collection()
doc = Doc(num=1)
doc.users["007"] = EmbeddedUser(name="Agent007")
doc.save()
uinfo = UInfoDocument(phone="79089269066")
uinfo.save()
d = Doc.objects(num=1).first()
d.users["007"]["roles"]["666"] = EmbeddedRole(type="superadmin")
d.users["007"]["rolist"].append(EmbeddedRole(type="oops"))
d.users["007"]["info"] = uinfo
delta = d._delta()
assert True == ("users.007.roles.666" in delta[0])
assert True == ("users.007.rolist" in delta[0])
assert True == ("users.007.info" in delta[0])
assert "superadmin" == delta[0]["users.007.roles.666"]["type"]
assert "oops" == delta[0]["users.007.rolist"][0]["type"]
assert uinfo.id == delta[0]["users.007.info"]
if __name__ == "__main__":
unittest.main()

View File

@ -1,617 +0,0 @@
import unittest
import warnings
import pytest
from mongoengine import (
BooleanField,
Document,
EmbeddedDocument,
EmbeddedDocumentField,
GenericReferenceField,
IntField,
ReferenceField,
StringField,
)
from mongoengine.pymongo_support import list_collection_names
from tests.fixtures import Base
from tests.utils import MongoDBTestCase
class TestInheritance(MongoDBTestCase):
def tearDown(self):
for collection in list_collection_names(self.db):
self.db.drop_collection(collection)
def test_constructor_cls(self):
# Ensures _cls is properly set during construction
# and when object gets reloaded (prevent regression of #1950)
class EmbedData(EmbeddedDocument):
data = StringField()
meta = {"allow_inheritance": True}
class DataDoc(Document):
name = StringField()
embed = EmbeddedDocumentField(EmbedData)
meta = {"allow_inheritance": True}
test_doc = DataDoc(name="test", embed=EmbedData(data="data"))
assert test_doc._cls == "DataDoc"
assert test_doc.embed._cls == "EmbedData"
test_doc.save()
saved_doc = DataDoc.objects.with_id(test_doc.id)
assert test_doc._cls == saved_doc._cls
assert test_doc.embed._cls == saved_doc.embed._cls
test_doc.delete()
def test_superclasses(self):
"""Ensure that the correct list of superclasses is assembled."""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._superclasses == ()
assert Fish._superclasses == ("Animal",)
assert Guppy._superclasses == ("Animal", "Animal.Fish")
assert Mammal._superclasses == ("Animal",)
assert Dog._superclasses == ("Animal", "Animal.Mammal")
assert Human._superclasses == ("Animal", "Animal.Mammal")
def test_external_superclasses(self):
"""Ensure that the correct list of super classes is assembled when
importing part of the model.
"""
class Animal(Base):
pass
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._superclasses == ("Base",)
assert Fish._superclasses == ("Base", "Base.Animal")
assert Guppy._superclasses == ("Base", "Base.Animal", "Base.Animal.Fish")
assert Mammal._superclasses == ("Base", "Base.Animal")
assert Dog._superclasses == ("Base", "Base.Animal", "Base.Animal.Mammal")
assert Human._superclasses == ("Base", "Base.Animal", "Base.Animal.Mammal")
def test_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled.
"""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._subclasses == (
"Animal",
"Animal.Fish",
"Animal.Fish.Guppy",
"Animal.Mammal",
"Animal.Mammal.Dog",
"Animal.Mammal.Human",
)
assert Fish._subclasses == ("Animal.Fish", "Animal.Fish.Guppy")
assert Guppy._subclasses == ("Animal.Fish.Guppy",)
assert Mammal._subclasses == (
"Animal.Mammal",
"Animal.Mammal.Dog",
"Animal.Mammal.Human",
)
assert Human._subclasses == ("Animal.Mammal.Human",)
def test_external_subclasses(self):
"""Ensure that the correct list of _subclasses (subclasses) is
assembled when importing part of the model.
"""
class Animal(Base):
pass
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
assert Animal._subclasses == (
"Base.Animal",
"Base.Animal.Fish",
"Base.Animal.Fish.Guppy",
"Base.Animal.Mammal",
"Base.Animal.Mammal.Dog",
"Base.Animal.Mammal.Human",
)
assert Fish._subclasses == ("Base.Animal.Fish", "Base.Animal.Fish.Guppy")
assert Guppy._subclasses == ("Base.Animal.Fish.Guppy",)
assert Mammal._subclasses == (
"Base.Animal.Mammal",
"Base.Animal.Mammal.Dog",
"Base.Animal.Mammal.Human",
)
assert Human._subclasses == ("Base.Animal.Mammal.Human",)
def test_dynamic_declarations(self):
"""Test that declaring an extra class updates meta data"""
class Animal(Document):
meta = {"allow_inheritance": True}
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal",)
# Test dynamically adding a class changes the meta data
class Fish(Animal):
pass
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal", "Animal.Fish")
assert Fish._superclasses == ("Animal",)
assert Fish._subclasses == ("Animal.Fish",)
# Test dynamically adding an inherited class changes the meta data
class Pike(Fish):
pass
assert Animal._superclasses == ()
assert Animal._subclasses == ("Animal", "Animal.Fish", "Animal.Fish.Pike")
assert Fish._superclasses == ("Animal",)
assert Fish._subclasses == ("Animal.Fish", "Animal.Fish.Pike")
assert Pike._superclasses == ("Animal", "Animal.Fish")
assert Pike._subclasses == ("Animal.Fish.Pike",)
def test_inheritance_meta_data(self):
"""Ensure that document may inherit fields from a superclass document."""
class Person(Document):
name = StringField()
age = IntField()
meta = {"allow_inheritance": True}
class Employee(Person):
salary = IntField()
assert ["_cls", "age", "id", "name", "salary"] == sorted(
Employee._fields.keys()
)
assert Employee._get_collection_name() == Person._get_collection_name()
def test_inheritance_to_mongo_keys(self):
"""Ensure that document may inherit fields from a superclass document."""
class Person(Document):
name = StringField()
age = IntField()
meta = {"allow_inheritance": True}
class Employee(Person):
salary = IntField()
assert ["_cls", "age", "id", "name", "salary"] == sorted(
Employee._fields.keys()
)
assert Person(name="Bob", age=35).to_mongo().keys() == ["_cls", "name", "age"]
assert Employee(name="Bob", age=35, salary=0).to_mongo().keys() == [
"_cls",
"name",
"age",
"salary",
]
assert Employee._get_collection_name() == Person._get_collection_name()
def test_indexes_and_multiple_inheritance(self):
"""Ensure that all of the indexes are created for a document with
multiple inheritance.
"""
class A(Document):
a = StringField()
meta = {"allow_inheritance": True, "indexes": ["a"]}
class B(Document):
b = StringField()
meta = {"allow_inheritance": True, "indexes": ["b"]}
class C(A, B):
pass
A.drop_collection()
B.drop_collection()
C.drop_collection()
C.ensure_indexes()
assert sorted(
idx["key"] for idx in C._get_collection().index_information().values()
) == sorted([[("_cls", 1), ("b", 1)], [("_id", 1)], [("_cls", 1), ("a", 1)]])
def test_polymorphic_queries(self):
"""Ensure that the correct subclasses are returned from a query"""
class Animal(Document):
meta = {"allow_inheritance": True}
class Fish(Animal):
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
class Human(Mammal):
pass
Animal.drop_collection()
Animal().save()
Fish().save()
Mammal().save()
Dog().save()
Human().save()
classes = [obj.__class__ for obj in Animal.objects]
assert classes == [Animal, Fish, Mammal, Dog, Human]
classes = [obj.__class__ for obj in Mammal.objects]
assert classes == [Mammal, Dog, Human]
classes = [obj.__class__ for obj in Human.objects]
assert classes == [Human]
def test_allow_inheritance(self):
"""Ensure that inheritance is disabled by default on simple
classes and that _cls will not be used.
"""
class Animal(Document):
name = StringField()
# can't inherit because Animal didn't explicitly allow inheritance
with pytest.raises(ValueError, match="Document Animal may not be subclassed"):
class Dog(Animal):
pass
# Check that _cls etc aren't present on simple documents
dog = Animal(name="dog").save()
assert dog.to_mongo().keys() == ["_id", "name"]
collection = self.db[Animal._get_collection_name()]
obj = collection.find_one()
assert "_cls" not in obj
def test_cant_turn_off_inheritance_on_subclass(self):
"""Ensure if inheritance is on in a subclass you cant turn it off."""
class Animal(Document):
name = StringField()
meta = {"allow_inheritance": True}
with pytest.raises(ValueError) as exc_info:
class Mammal(Animal):
meta = {"allow_inheritance": False}
assert (
str(exc_info.value)
== 'Only direct subclasses of Document may set "allow_inheritance" to False'
)
def test_allow_inheritance_abstract_document(self):
"""Ensure that abstract documents can set inheritance rules and that
_cls will not be used.
"""
class FinalDocument(Document):
meta = {"abstract": True, "allow_inheritance": False}
class Animal(FinalDocument):
name = StringField()
with pytest.raises(ValueError):
class Mammal(Animal):
pass
# Check that _cls isn't present in simple documents
doc = Animal(name="dog")
assert "_cls" not in doc.to_mongo()
def test_using_abstract_class_in_reference_field(self):
# Ensures no regression of #1920
class AbstractHuman(Document):
meta = {"abstract": True}
class Dad(AbstractHuman):
name = StringField()
class Home(Document):
dad = ReferenceField(AbstractHuman) # Referencing the abstract class
address = StringField()
dad = Dad(name="5").save()
Home(dad=dad, address="street").save()
home = Home.objects.first()
home.address = "garbage"
home.save() # Was failing with ValidationError
def test_abstract_class_referencing_self(self):
# Ensures no regression of #1920
class Human(Document):
meta = {"abstract": True}
creator = ReferenceField("self", dbref=True)
class User(Human):
name = StringField()
user = User(name="John").save()
user2 = User(name="Foo", creator=user).save()
user2 = User.objects.with_id(user2.id)
user2.name = "Bar"
user2.save() # Was failing with ValidationError
def test_abstract_handle_ids_in_metaclass_properly(self):
class City(Document):
continent = StringField()
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 3
assert berlin._fields_ordered[0] == "id"
def test_auto_id_not_set_if_specific_in_parent_class(self):
class City(Document):
continent = StringField()
city_id = IntField(primary_key=True)
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 3
assert berlin._fields_ordered[0] == "city_id"
def test_auto_id_vs_non_pk_id_field(self):
class City(Document):
continent = StringField()
id = IntField()
meta = {"abstract": True, "allow_inheritance": False}
class EuropeanCity(City):
name = StringField()
berlin = EuropeanCity(name="Berlin", continent="Europe")
assert len(berlin._db_field_map) == len(berlin._fields_ordered)
assert len(berlin._reverse_db_field_map) == len(berlin._fields_ordered)
assert len(berlin._fields_ordered) == 4
assert berlin._fields_ordered[0] == "auto_id_0"
berlin.save()
assert berlin.pk == berlin.auto_id_0
def test_abstract_document_creation_does_not_fail(self):
class City(Document):
continent = StringField()
meta = {"abstract": True, "allow_inheritance": False}
city = City(continent="asia")
assert city.pk is None
# TODO: expected error? Shouldn't we create a new error type?
with pytest.raises(KeyError):
city.pk = 1
def test_allow_inheritance_embedded_document(self):
"""Ensure embedded documents respect inheritance."""
class Comment(EmbeddedDocument):
content = StringField()
with pytest.raises(ValueError):
class SpecialComment(Comment):
pass
doc = Comment(content="test")
assert "_cls" not in doc.to_mongo()
class Comment(EmbeddedDocument):
content = StringField()
meta = {"allow_inheritance": True}
doc = Comment(content="test")
assert "_cls" in doc.to_mongo()
def test_document_inheritance(self):
"""Ensure mutliple inheritance of abstract documents"""
class DateCreatedDocument(Document):
meta = {"allow_inheritance": True, "abstract": True}
class DateUpdatedDocument(Document):
meta = {"allow_inheritance": True, "abstract": True}
class MyDocument(DateCreatedDocument, DateUpdatedDocument):
pass
def test_abstract_documents(self):
"""Ensure that a document superclass can be marked as abstract
thereby not using it as the name for the collection."""
defaults = {
"index_background": True,
"index_opts": {"hello": "world"},
"allow_inheritance": True,
"queryset_class": "QuerySet",
"db_alias": "myDB",
"shard_key": ("hello", "world"),
}
meta_settings = {"abstract": True}
meta_settings.update(defaults)
class Animal(Document):
name = StringField()
meta = meta_settings
class Fish(Animal):
pass
class Guppy(Fish):
pass
class Mammal(Animal):
meta = {"abstract": True}
class Human(Mammal):
pass
for k, v in defaults.items():
for cls in [Animal, Fish, Guppy]:
assert cls._meta[k] == v
assert "collection" not in Animal._meta
assert "collection" not in Mammal._meta
assert Animal._get_collection_name() is None
assert Mammal._get_collection_name() is None
assert Fish._get_collection_name() == "fish"
assert Guppy._get_collection_name() == "fish"
assert Human._get_collection_name() == "human"
# ensure that a subclass of a non-abstract class can't be abstract
with pytest.raises(ValueError):
class EvilHuman(Human):
evil = BooleanField(default=True)
meta = {"abstract": True}
def test_abstract_embedded_documents(self):
# 789: EmbeddedDocument shouldn't inherit abstract
class A(EmbeddedDocument):
meta = {"abstract": True}
class B(A):
pass
assert not B._meta["abstract"]
def test_inherited_collections(self):
"""Ensure that subclassed documents don't override parents'
collections
"""
class Drink(Document):
name = StringField()
meta = {"allow_inheritance": True}
class Drinker(Document):
drink = GenericReferenceField()
try:
warnings.simplefilter("error")
class AcloholicDrink(Drink):
meta = {"collection": "booze"}
except SyntaxWarning:
warnings.simplefilter("ignore")
class AlcoholicDrink(Drink):
meta = {"collection": "booze"}
else:
raise AssertionError("SyntaxWarning should be triggered")
warnings.resetwarnings()
Drink.drop_collection()
AlcoholicDrink.drop_collection()
Drinker.drop_collection()
red_bull = Drink(name="Red Bull")
red_bull.save()
programmer = Drinker(drink=red_bull)
programmer.save()
beer = AlcoholicDrink(name="Beer")
beer.save()
real_person = Drinker(drink=beer)
real_person.save()
assert Drinker.objects[0].drink.name == red_bull.name
assert Drinker.objects[1].drink.name == beer.name
if __name__ == "__main__":
unittest.main()

View File

@ -1,217 +0,0 @@
import unittest
from datetime import datetime
import pytest
from mongoengine import *
from tests.utils import MongoDBTestCase
class TestValidatorError(MongoDBTestCase):
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly."""
error = ValidationError("root")
assert error.to_dict() == {}
# 1st level error schema
error.errors = {"1st": ValidationError("bad 1st")}
assert "1st" in error.to_dict()
assert error.to_dict()["1st"] == "bad 1st"
# 2nd level error schema
error.errors = {
"1st": ValidationError(
"bad 1st", errors={"2nd": ValidationError("bad 2nd")}
)
}
assert "1st" in error.to_dict()
assert isinstance(error.to_dict()["1st"], dict)
assert "2nd" in error.to_dict()["1st"]
assert error.to_dict()["1st"]["2nd"] == "bad 2nd"
# moar levels
error.errors = {
"1st": ValidationError(
"bad 1st",
errors={
"2nd": ValidationError(
"bad 2nd",
errors={
"3rd": ValidationError(
"bad 3rd", errors={"4th": ValidationError("Inception")}
)
},
)
},
)
}
assert "1st" in error.to_dict()
assert "2nd" in error.to_dict()["1st"]
assert "3rd" in error.to_dict()["1st"]["2nd"]
assert "4th" in error.to_dict()["1st"]["2nd"]["3rd"]
assert error.to_dict()["1st"]["2nd"]["3rd"]["4th"] == "Inception"
assert error.message == "root(2nd.3rd.4th.Inception: ['1st'])"
def test_model_validation(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField(required=True)
try:
User().validate()
except ValidationError as e:
assert "User:None" in e.message
assert e.to_dict() == {
"username": "Field is required",
"name": "Field is required",
}
user = User(username="RossC0", name="Ross").save()
user.name = None
try:
user.save()
except ValidationError as e:
assert "User:RossC0" in e.message
assert e.to_dict() == {"name": "Field is required"}
def test_fields_rewrite(self):
class BasePerson(Document):
name = StringField()
age = IntField()
meta = {"abstract": True}
class Person(BasePerson):
name = StringField(required=True)
p = Person(age=15)
with pytest.raises(ValidationError):
p.validate()
def test_embedded_document_validation(self):
"""Ensure that embedded documents may be validated."""
class Comment(EmbeddedDocument):
date = DateTimeField()
content = StringField(required=True)
comment = Comment()
with pytest.raises(ValidationError):
comment.validate()
comment.content = "test"
comment.validate()
comment.date = 4
with pytest.raises(ValidationError):
comment.validate()
comment.date = datetime.now()
comment.validate()
assert comment._instance is None
def test_embedded_db_field_validate(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
id = StringField(primary_key=True)
e = EmbeddedDocumentField(SubDoc, db_field="eb")
try:
Doc(id="bad").validate()
except ValidationError as e:
assert "SubDoc:None" in e.message
assert e.to_dict() == {"e": {"val": "OK could not be converted to int"}}
Doc.drop_collection()
Doc(id="test", e=SubDoc(val=15)).save()
doc = Doc.objects.first()
keys = doc._data.keys()
assert 2 == len(keys)
assert "e" in keys
assert "id" in keys
doc.e.val = "OK"
try:
doc.save()
except ValidationError as e:
assert "Doc:test" in e.message
assert e.to_dict() == {"e": {"val": "OK could not be converted to int"}}
def test_embedded_weakref(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
e = EmbeddedDocumentField(SubDoc, db_field="eb")
Doc.drop_collection()
d1 = Doc()
d2 = Doc()
s = SubDoc()
with pytest.raises(ValidationError):
s.validate()
d1.e = s
d2.e = s
del d1
with pytest.raises(ValidationError):
d2.validate()
def test_parent_reference_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited. Issue #954.
"""
class Parent(Document):
meta = {"allow_inheritance": True}
reference = ReferenceField("self")
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child(reference=parent)
# Saving child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
def test_parent_reference_set_as_attribute_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited and when set via attribute. Issue #954.
"""
class Parent(Document):
meta = {"allow_inheritance": True}
reference = ReferenceField("self")
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child()
child.reference = parent
# Saving the child should not raise a ValidationError
child.save()
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
import unittest
from datetime import datetime
from mongoengine import *
__all__ = ("ValidatorErrorTest",)
class ValidatorErrorTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
def test_to_dict(self):
"""Ensure a ValidationError handles error to_dict correctly.
"""
error = ValidationError('root')
self.assertEqual(error.to_dict(), {})
# 1st level error schema
error.errors = {'1st': ValidationError('bad 1st'), }
self.assertIn('1st', error.to_dict())
self.assertEqual(error.to_dict()['1st'], 'bad 1st')
# 2nd level error schema
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd'),
})}
self.assertIn('1st', error.to_dict())
self.assertIsInstance(error.to_dict()['1st'], dict)
self.assertIn('2nd', error.to_dict()['1st'])
self.assertEqual(error.to_dict()['1st']['2nd'], 'bad 2nd')
# moar levels
error.errors = {'1st': ValidationError('bad 1st', errors={
'2nd': ValidationError('bad 2nd', errors={
'3rd': ValidationError('bad 3rd', errors={
'4th': ValidationError('Inception'),
}),
}),
})}
self.assertIn('1st', error.to_dict())
self.assertIn('2nd', error.to_dict()['1st'])
self.assertIn('3rd', error.to_dict()['1st']['2nd'])
self.assertIn('4th', error.to_dict()['1st']['2nd']['3rd'])
self.assertEqual(error.to_dict()['1st']['2nd']['3rd']['4th'],
'Inception')
self.assertEqual(error.message, "root(2nd.3rd.4th.Inception: ['1st'])")
def test_model_validation(self):
class User(Document):
username = StringField(primary_key=True)
name = StringField(required=True)
try:
User().validate()
except ValidationError as e:
self.assertIn("User:None", e.message)
self.assertEqual(e.to_dict(), {
'username': 'Field is required',
'name': 'Field is required'})
user = User(username="RossC0", name="Ross").save()
user.name = None
try:
user.save()
except ValidationError as e:
self.assertIn("User:RossC0", e.message)
self.assertEqual(e.to_dict(), {
'name': 'Field is required'})
def test_fields_rewrite(self):
class BasePerson(Document):
name = StringField()
age = IntField()
meta = {'abstract': True}
class Person(BasePerson):
name = StringField(required=True)
p = Person(age=15)
self.assertRaises(ValidationError, p.validate)
def test_embedded_document_validation(self):
"""Ensure that embedded documents may be validated.
"""
class Comment(EmbeddedDocument):
date = DateTimeField()
content = StringField(required=True)
comment = Comment()
self.assertRaises(ValidationError, comment.validate)
comment.content = 'test'
comment.validate()
comment.date = 4
self.assertRaises(ValidationError, comment.validate)
comment.date = datetime.now()
comment.validate()
self.assertEqual(comment._instance, None)
def test_embedded_db_field_validate(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
id = StringField(primary_key=True)
e = EmbeddedDocumentField(SubDoc, db_field='eb')
try:
Doc(id="bad").validate()
except ValidationError as e:
self.assertIn("SubDoc:None", e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
Doc.drop_collection()
Doc(id="test", e=SubDoc(val=15)).save()
doc = Doc.objects.first()
keys = doc._data.keys()
self.assertEqual(2, len(keys))
self.assertIn('e', keys)
self.assertIn('id', keys)
doc.e.val = "OK"
try:
doc.save()
except ValidationError as e:
self.assertIn("Doc:test", e.message)
self.assertEqual(e.to_dict(), {
"e": {'val': 'OK could not be converted to int'}})
def test_embedded_weakref(self):
class SubDoc(EmbeddedDocument):
val = IntField(required=True)
class Doc(Document):
e = EmbeddedDocumentField(SubDoc, db_field='eb')
Doc.drop_collection()
d1 = Doc()
d2 = Doc()
s = SubDoc()
self.assertRaises(ValidationError, s.validate)
d1.e = s
d2.e = s
del d1
self.assertRaises(ValidationError, d2.validate)
def test_parent_reference_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited. Issue #954.
"""
class Parent(Document):
meta = {'allow_inheritance': True}
reference = ReferenceField('self')
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child(reference=parent)
# Saving child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
def test_parent_reference_set_as_attribute_in_child_document(self):
"""
Test to ensure a ReferenceField can store a reference to a parent
class when inherited and when set via attribute. Issue #954.
"""
class Parent(Document):
meta = {'allow_inheritance': True}
reference = ReferenceField('self')
class Child(Parent):
pass
parent = Parent()
parent.save()
child = Child()
child.reference = parent
# Saving the child should not raise a ValidationError
try:
child.save()
except ValidationError as e:
self.fail("ValidationError raised: %s" % e.message)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,3 @@
from .fields import *
from .file_tests import *
from .geo import *

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,72 @@
# -*- coding: utf-8 -*-
import copy import copy
import os import os
import tempfile
import unittest import unittest
from io import BytesIO import tempfile
import gridfs import gridfs
import pytest import six
from nose.plugins.skip import SkipTest
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
from mongoengine.python_support import StringIO
try: try:
from PIL import Image # noqa: F401 from PIL import Image
HAS_PIL = True HAS_PIL = True
except ImportError: except ImportError:
HAS_PIL = False HAS_PIL = False
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
require_pil = pytest.mark.skipif(not HAS_PIL, reason="PIL not installed") 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_IMAGE_PATH = os.path.join(os.path.dirname(__file__), "mongoengine.png")
TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), "mongodb_leaf.png")
def get_file(path): def get_file(path):
"""Use a BytesIO instead of a file to allow """Use a BytesIO instead of a file to allow
to have a one-liner and avoid that the file remains opened""" to have a one-liner and avoid that the file remains opened"""
bytes_io = BytesIO() bytes_io = StringIO()
with open(path, "rb") as f: with open(path, 'rb') as f:
bytes_io.write(f.read()) bytes_io.write(f.read())
bytes_io.seek(0) bytes_io.seek(0)
return bytes_io return bytes_io
class TestFileField(MongoDBTestCase): class FileTest(MongoDBTestCase):
def tearDown(self): def tearDown(self):
self.db.drop_collection("fs.files") self.db.drop_collection('fs.files')
self.db.drop_collection("fs.chunks") self.db.drop_collection('fs.chunks')
def test_file_field_optional(self): def test_file_field_optional(self):
# Make sure FileField is optional and not required # Make sure FileField is optional and not required
class DemoFile(Document): class DemoFile(Document):
the_file = FileField() the_file = FileField()
DemoFile.objects.create() DemoFile.objects.create()
def test_file_fields(self): def test_file_fields(self):
"""Ensure that file fields can be written to and their data retrieved""" """Ensure that file fields can be written to and their data retrieved
"""
class PutFile(Document): class PutFile(Document):
the_file = FileField() the_file = FileField()
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()
putfile.the_file.put(text, content_type=content_type, filename="hello") putfile.the_file.put(text, content_type=content_type, filename="hello")
putfile.save() putfile.save()
result = PutFile.objects.first() result = PutFile.objects.first()
assert putfile == result self.assertEqual(putfile, result)
assert ( self.assertEqual("%s" % result.the_file, "<GridFSProxy: hello (%s)>" % result.the_file.grid_id)
"%s" % result.the_file self.assertEqual(result.the_file.read(), text)
== "<GridFSProxy: hello (%s)>" % result.the_file.grid_id self.assertEqual(result.the_file.content_type, content_type)
)
assert result.the_file.read() == text
assert result.the_file.content_type == content_type
result.the_file.delete() # Remove file from GridFS result.the_file.delete() # Remove file from GridFS
PutFile.objects.delete() PutFile.objects.delete()
@ -77,29 +74,29 @@ class TestFileField(MongoDBTestCase):
PutFile.drop_collection() PutFile.drop_collection()
putfile = PutFile() putfile = PutFile()
putstring = BytesIO() putstring = StringIO()
putstring.write(text) putstring.write(text)
putstring.seek(0) putstring.seek(0)
putfile.the_file.put(putstring, content_type=content_type) putfile.the_file.put(putstring, content_type=content_type)
putfile.save() putfile.save()
result = PutFile.objects.first() result = PutFile.objects.first()
assert putfile == result self.assertEqual(putfile, result)
assert result.the_file.read() == text self.assertEqual(result.the_file.read(), text)
assert result.the_file.content_type == content_type self.assertEqual(result.the_file.content_type, content_type)
result.the_file.delete() result.the_file.delete()
def test_file_fields_stream(self): def test_file_fields_stream(self):
"""Ensure that file fields can be written to and their data retrieved""" """Ensure that file fields can be written to and their data retrieved
"""
class StreamFile(Document): class StreamFile(Document):
the_file = FileField() the_file = FileField()
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()
streamfile.the_file.new_file(content_type=content_type) streamfile.the_file.new_file(content_type=content_type)
@ -109,32 +106,32 @@ class TestFileField(MongoDBTestCase):
streamfile.save() streamfile.save()
result = StreamFile.objects.first() result = StreamFile.objects.first()
assert streamfile == result self.assertEqual(streamfile, result)
assert result.the_file.read() == text + more_text self.assertEqual(result.the_file.read(), text + more_text)
assert result.the_file.content_type == content_type self.assertEqual(result.the_file.content_type, content_type)
result.the_file.seek(0) result.the_file.seek(0)
assert result.the_file.tell() == 0 self.assertEqual(result.the_file.tell(), 0)
assert result.the_file.read(len(text)) == text self.assertEqual(result.the_file.read(len(text)), text)
assert result.the_file.tell() == len(text) self.assertEqual(result.the_file.tell(), len(text))
assert result.the_file.read(len(more_text)) == more_text self.assertEqual(result.the_file.read(len(more_text)), more_text)
assert result.the_file.tell() == len(text + more_text) self.assertEqual(result.the_file.tell(), len(text + more_text))
result.the_file.delete() result.the_file.delete()
# Ensure deleted file returns None # Ensure deleted file returns None
assert result.the_file.read() is None self.assertTrue(result.the_file.read() is None)
def test_file_fields_stream_after_none(self): def test_file_fields_stream_after_none(self):
"""Ensure that a file field can be written to after it has been saved as """Ensure that a file field can be written to after it has been saved as
None None
""" """
class StreamFile(Document): class StreamFile(Document):
the_file = FileField() the_file = FileField()
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'
streamfile = StreamFile() streamfile = StreamFile()
streamfile.save() streamfile.save()
@ -145,26 +142,27 @@ class TestFileField(MongoDBTestCase):
streamfile.save() streamfile.save()
result = StreamFile.objects.first() result = StreamFile.objects.first()
assert streamfile == result self.assertEqual(streamfile, result)
assert result.the_file.read() == text + more_text self.assertEqual(result.the_file.read(), text + more_text)
# assert result.the_file.content_type == content_type # self.assertEqual(result.the_file.content_type, content_type)
result.the_file.seek(0) result.the_file.seek(0)
assert result.the_file.tell() == 0 self.assertEqual(result.the_file.tell(), 0)
assert result.the_file.read(len(text)) == text self.assertEqual(result.the_file.read(len(text)), text)
assert result.the_file.tell() == len(text) self.assertEqual(result.the_file.tell(), len(text))
assert result.the_file.read(len(more_text)) == more_text self.assertEqual(result.the_file.read(len(more_text)), more_text)
assert result.the_file.tell() == len(text + more_text) self.assertEqual(result.the_file.tell(), len(text + more_text))
result.the_file.delete() result.the_file.delete()
# Ensure deleted file returns None # Ensure deleted file returns None
assert result.the_file.read() is None self.assertTrue(result.the_file.read() is None)
def test_file_fields_set(self): def test_file_fields_set(self):
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()
@ -173,26 +171,27 @@ class TestFileField(MongoDBTestCase):
setfile.save() setfile.save()
result = SetFile.objects.first() result = SetFile.objects.first()
assert setfile == result self.assertEqual(setfile, result)
assert result.the_file.read() == text self.assertEqual(result.the_file.read(), text)
# Try replacing file with new one # Try replacing file with new one
result.the_file.replace(more_text) result.the_file.replace(more_text)
result.save() result.save()
result = SetFile.objects.first() result = SetFile.objects.first()
assert setfile == result self.assertEqual(setfile, result)
assert result.the_file.read() == more_text self.assertEqual(result.the_file.read(), more_text)
result.the_file.delete() result.the_file.delete()
def test_file_field_no_default(self): def test_file_field_no_default(self):
class GridDocument(Document): class GridDocument(Document):
the_file = FileField() the_file = FileField()
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
@ -200,34 +199,34 @@ class TestFileField(MongoDBTestCase):
doc_a.save() doc_a.save()
doc_b = GridDocument.objects.with_id(doc_a.id) doc_b = GridDocument.objects.with_id(doc_a.id)
doc_b.the_file.replace(f, filename="doc_b") doc_b.the_file.replace(f, filename='doc_b')
doc_b.save() doc_b.save()
assert doc_b.the_file.grid_id is not None self.assertNotEqual(doc_b.the_file.grid_id, None)
# Test it matches # Test it matches
doc_c = GridDocument.objects.with_id(doc_b.id) doc_c = GridDocument.objects.with_id(doc_b.id)
assert 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)
assert doc_d.the_file.grid_id == doc_e.the_file.grid_id self.assertEqual(doc_d.the_file.grid_id, doc_e.the_file.grid_id)
doc_e.the_file.replace(f, filename="doc_e") doc_e.the_file.replace(f, filename='doc_e')
doc_e.save() doc_e.save()
doc_f = GridDocument.objects.with_id(doc_e.id) doc_f = GridDocument.objects.with_id(doc_e.id)
assert doc_e.the_file.grid_id == doc_f.the_file.grid_id self.assertEqual(doc_e.the_file.grid_id, doc_f.the_file.grid_id)
db = GridDocument._get_db() db = GridDocument._get_db()
grid_fs = gridfs.GridFS(db) grid_fs = gridfs.GridFS(db)
assert ["doc_b", "doc_e"] == grid_fs.list() self.assertEqual(['doc_b', 'doc_e'], grid_fs.list())
def test_file_uniqueness(self): def test_file_uniqueness(self):
"""Ensure that each instance of a FileField is unique""" """Ensure that each instance of a FileField is unique
"""
class TestFile(Document): class TestFile(Document):
name = StringField() name = StringField()
the_file = FileField() the_file = FileField()
@ -235,15 +234,15 @@ class TestFileField(MongoDBTestCase):
# First instance # First instance
test_file = TestFile() test_file = TestFile()
test_file.name = "Hello, World!" test_file.name = "Hello, World!"
test_file.the_file.put(b"Hello, World!") test_file.the_file.put(six.b('Hello, World!'))
test_file.save() test_file.save()
# Second instance # Second instance
test_file_dupe = TestFile() test_file_dupe = TestFile()
data = test_file_dupe.the_file.read() # Should be None data = test_file_dupe.the_file.read() # Should be None
assert test_file.name != test_file_dupe.name self.assertNotEqual(test_file.name, test_file_dupe.name)
assert test_file.the_file.read() != data self.assertNotEqual(test_file.the_file.read(), data)
TestFile.drop_collection() TestFile.drop_collection()
@ -256,65 +255,61 @@ class TestFileField(MongoDBTestCase):
photo = FileField() photo = FileField()
Animal.drop_collection() Animal.drop_collection()
marmot = Animal(genus="Marmota", family="Sciuridae") marmot = Animal(genus='Marmota', family='Sciuridae')
marmot_photo_content = get_file(TEST_IMAGE_PATH) # Retrieve a photo from disk marmot_photo_content = get_file(TEST_IMAGE_PATH) # Retrieve a photo from disk
marmot.photo.put(marmot_photo_content, content_type="image/jpeg", foo="bar") marmot.photo.put(marmot_photo_content, content_type='image/jpeg', foo='bar')
marmot.photo.close() marmot.photo.close()
marmot.save() marmot.save()
marmot = Animal.objects.get() marmot = Animal.objects.get()
assert marmot.photo.content_type == "image/jpeg" self.assertEqual(marmot.photo.content_type, 'image/jpeg')
assert marmot.photo.foo == "bar" self.assertEqual(marmot.photo.foo, 'bar')
def test_file_reassigning(self): def test_file_reassigning(self):
class TestFile(Document): class TestFile(Document):
the_file = FileField() the_file = FileField()
TestFile.drop_collection() TestFile.drop_collection()
test_file = TestFile(the_file=get_file(TEST_IMAGE_PATH)).save() test_file = TestFile(the_file=get_file(TEST_IMAGE_PATH)).save()
assert test_file.the_file.get().length == 8313 self.assertEqual(test_file.the_file.get().length, 8313)
test_file = TestFile.objects.first() test_file = TestFile.objects.first()
test_file.the_file = get_file(TEST_IMAGE2_PATH) test_file.the_file = get_file(TEST_IMAGE2_PATH)
test_file.save() test_file.save()
assert test_file.the_file.get().length == 4971 self.assertEqual(test_file.the_file.get().length, 4971)
def test_file_boolean(self): def test_file_boolean(self):
"""Ensure that a boolean test of a FileField indicates its presence""" """Ensure that a boolean test of a FileField indicates its presence
"""
class TestFile(Document): class TestFile(Document):
the_file = FileField() the_file = FileField()
TestFile.drop_collection() TestFile.drop_collection()
test_file = TestFile() test_file = TestFile()
assert not 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()
assert bool(test_file.the_file) self.assertTrue(bool(test_file.the_file))
test_file = TestFile.objects.first() test_file = TestFile.objects.first()
assert test_file.the_file.content_type == "text/plain" self.assertEqual(test_file.the_file.content_type, "text/plain")
def test_file_cmp(self): def test_file_cmp(self):
"""Test comparing against other types""" """Test comparing against other types"""
class TestFile(Document): class TestFile(Document):
the_file = FileField() the_file = FileField()
test_file = TestFile() test_file = TestFile()
assert test_file.the_file not in [{"test": 1}] self.assertNotIn(test_file.the_file, [{"test": 1}])
def test_file_disk_space(self): def test_file_disk_space(self):
"""Test disk space usage when we delete/replace a file""" """ Test disk space usage when we delete/replace a file """
class TestFile(Document): class TestFile(Document):
the_file = FileField() the_file = FileField()
text = b"Hello, World!" text = six.b('Hello, World!')
content_type = "text/plain" content_type = 'text/plain'
testfile = TestFile() testfile = TestFile()
testfile.the_file.put(text, content_type=content_type, filename="hello") testfile.the_file.put(text, content_type=content_type, filename="hello")
@ -325,16 +320,16 @@ class TestFileField(MongoDBTestCase):
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 1 self.assertEqual(len(list(files)), 1)
assert len(list(chunks)) == 1 self.assertEqual(len(list(chunks)), 1)
# Deleting the docoument should delete the files # Deleting the docoument should delete the files
testfile.delete() testfile.delete()
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 0 self.assertEqual(len(list(files)), 0)
assert len(list(chunks)) == 0 self.assertEqual(len(list(chunks)), 0)
# Test case where we don't store a file in the first place # Test case where we don't store a file in the first place
testfile = TestFile() testfile = TestFile()
@ -342,46 +337,48 @@ class TestFileField(MongoDBTestCase):
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 0 self.assertEqual(len(list(files)), 0)
assert len(list(chunks)) == 0 self.assertEqual(len(list(chunks)), 0)
testfile.delete() testfile.delete()
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 0 self.assertEqual(len(list(files)), 0)
assert len(list(chunks)) == 0 self.assertEqual(len(list(chunks)), 0)
# Test case where we overwrite the file # Test case where we overwrite the file
testfile = TestFile() testfile = TestFile()
testfile.the_file.put(text, content_type=content_type, filename="hello") testfile.the_file.put(text, content_type=content_type, filename="hello")
testfile.save() testfile.save()
text = b"Bonjour, World!" text = six.b('Bonjour, World!')
testfile.the_file.replace(text, content_type=content_type, filename="hello") testfile.the_file.replace(text, content_type=content_type, filename="hello")
testfile.save() testfile.save()
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 1 self.assertEqual(len(list(files)), 1)
assert len(list(chunks)) == 1 self.assertEqual(len(list(chunks)), 1)
testfile.delete() testfile.delete()
files = db.fs.files.find() files = db.fs.files.find()
chunks = db.fs.chunks.find() chunks = db.fs.chunks.find()
assert len(list(files)) == 0 self.assertEqual(len(list(files)), 0)
assert len(list(chunks)) == 0 self.assertEqual(len(list(chunks)), 0)
@require_pil
def test_image_field(self): def test_image_field(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestImage(Document): class TestImage(Document):
image = ImageField() image = ImageField()
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()
@ -389,7 +386,7 @@ class TestFileField(MongoDBTestCase):
t.image.put(f) t.image.put(f)
self.fail("Should have raised an invalidation error") self.fail("Should have raised an invalidation error")
except ValidationError as e: except ValidationError as e:
assert "%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()
t.image.put(get_file(TEST_IMAGE_PATH)) t.image.put(get_file(TEST_IMAGE_PATH))
@ -397,33 +394,36 @@ class TestFileField(MongoDBTestCase):
t = TestImage.objects.first() t = TestImage.objects.first()
assert t.image.format == "PNG" self.assertEqual(t.image.format, 'PNG')
w, h = t.image.size w, h = t.image.size
assert w == 371 self.assertEqual(w, 371)
assert h == 76 self.assertEqual(h, 76)
t.image.delete() t.image.delete()
@require_pil
def test_image_field_reassigning(self): def test_image_field_reassigning(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestFile(Document): class TestFile(Document):
the_file = ImageField() the_file = ImageField()
TestFile.drop_collection() TestFile.drop_collection()
test_file = TestFile(the_file=get_file(TEST_IMAGE_PATH)).save() test_file = TestFile(the_file=get_file(TEST_IMAGE_PATH)).save()
assert test_file.the_file.size == (371, 76) self.assertEqual(test_file.the_file.size, (371, 76))
test_file = TestFile.objects.first() test_file = TestFile.objects.first()
test_file.the_file = get_file(TEST_IMAGE2_PATH) test_file.the_file = get_file(TEST_IMAGE2_PATH)
test_file.save() test_file.save()
assert test_file.the_file.size == (45, 101) self.assertEqual(test_file.the_file.size, (45, 101))
@require_pil
def test_image_field_resize(self): def test_image_field_resize(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestImage(Document): class TestImage(Document):
image = ImageField(size=(185, 37, True)) image = ImageField(size=(185, 37))
TestImage.drop_collection() TestImage.drop_collection()
@ -433,16 +433,18 @@ class TestFileField(MongoDBTestCase):
t = TestImage.objects.first() t = TestImage.objects.first()
assert t.image.format == "PNG" self.assertEqual(t.image.format, 'PNG')
w, h = t.image.size w, h = t.image.size
assert w == 185 self.assertEqual(w, 185)
assert h == 37 self.assertEqual(h, 37)
t.image.delete() t.image.delete()
@require_pil
def test_image_field_resize_force(self): def test_image_field_resize_force(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestImage(Document): class TestImage(Document):
image = ImageField(size=(185, 37, True)) image = ImageField(size=(185, 37, True))
@ -454,18 +456,20 @@ class TestFileField(MongoDBTestCase):
t = TestImage.objects.first() t = TestImage.objects.first()
assert t.image.format == "PNG" self.assertEqual(t.image.format, 'PNG')
w, h = t.image.size w, h = t.image.size
assert w == 185 self.assertEqual(w, 185)
assert h == 37 self.assertEqual(h, 37)
t.image.delete() t.image.delete()
@require_pil
def test_image_field_thumbnail(self): def test_image_field_thumbnail(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestImage(Document): class TestImage(Document):
image = ImageField(thumbnail_size=(92, 18, True)) image = ImageField(thumbnail_size=(92, 18))
TestImage.drop_collection() TestImage.drop_collection()
@ -475,18 +479,19 @@ class TestFileField(MongoDBTestCase):
t = TestImage.objects.first() t = TestImage.objects.first()
assert t.image.thumbnail.format == "PNG" self.assertEqual(t.image.thumbnail.format, 'PNG')
assert t.image.thumbnail.width == 92 self.assertEqual(t.image.thumbnail.width, 92)
assert t.image.thumbnail.height == 18 self.assertEqual(t.image.thumbnail.height, 18)
t.image.delete() t.image.delete()
def test_file_multidb(self): def test_file_multidb(self):
register_connection("test_files", "test_files") register_connection('test_files', 'test_files')
class TestFile(Document): class TestFile(Document):
name = StringField() name = StringField()
the_file = FileField(db_alias="test_files", collection_name="macumba") the_file = FileField(db_alias="test_files",
collection_name="macumba")
TestFile.drop_collection() TestFile.drop_collection()
@ -497,21 +502,23 @@ class TestFileField(MongoDBTestCase):
# First instance # First instance
test_file = TestFile() test_file = TestFile()
test_file.name = "Hello, World!" test_file.name = "Hello, World!"
test_file.the_file.put(b"Hello, World!", name="hello.txt") test_file.the_file.put(six.b('Hello, World!'),
name="hello.txt")
test_file.save() test_file.save()
data = get_db("test_files").macumba.files.find_one() data = get_db("test_files").macumba.files.find_one()
assert data.get("name") == "hello.txt" self.assertEqual(data.get('name'), 'hello.txt')
test_file = TestFile.objects.first() test_file = TestFile.objects.first()
assert test_file.the_file.read() == b"Hello, World!" self.assertEqual(test_file.the_file.read(), six.b('Hello, World!'))
test_file = TestFile.objects.first() test_file = 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()
assert test_file.the_file.read() == b"Hello, World!" self.assertEqual(test_file.the_file.read(),
six.b('HELLO, WORLD!'))
def test_copyable(self): def test_copyable(self):
class PutFile(Document): class PutFile(Document):
@ -519,8 +526,8 @@ class TestFileField(MongoDBTestCase):
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()
putfile.the_file.put(text, content_type=content_type) putfile.the_file.put(text, content_type=content_type)
@ -529,11 +536,14 @@ class TestFileField(MongoDBTestCase):
class TestFile(Document): class TestFile(Document):
name = StringField() name = StringField()
assert putfile == copy.copy(putfile) self.assertEqual(putfile, copy.copy(putfile))
assert putfile == copy.deepcopy(putfile) self.assertEqual(putfile, copy.deepcopy(putfile))
@require_pil
def test_get_image_by_grid_id(self): def test_get_image_by_grid_id(self):
if not HAS_PIL:
raise SkipTest('PIL not installed')
class TestImage(Document): class TestImage(Document):
image1 = ImageField() image1 = ImageField()
@ -549,7 +559,8 @@ class TestFileField(MongoDBTestCase):
test = TestImage.objects.first() test = TestImage.objects.first()
grid_id = test.image1.grid_id grid_id = test.image1.grid_id
assert 1 == TestImage.objects(Q(image1=grid_id) or Q(image2=grid_id)).count() self.assertEqual(1, TestImage.objects(Q(image1=grid_id)
or Q(image2=grid_id)).count())
def test_complex_field_filefield(self): def test_complex_field_filefield(self):
"""Ensure you can add meta data to file""" """Ensure you can add meta data to file"""
@ -560,21 +571,21 @@ class TestFileField(MongoDBTestCase):
photos = ListField(FileField()) photos = ListField(FileField())
Animal.drop_collection() Animal.drop_collection()
marmot = Animal(genus="Marmota", family="Sciuridae") marmot = Animal(genus='Marmota', family='Sciuridae')
with open(TEST_IMAGE_PATH, "rb") as marmot_photo: # Retrieve a photo from disk with open(TEST_IMAGE_PATH, 'rb') as marmot_photo: # Retrieve a photo from disk
photos_field = marmot._fields["photos"].field photos_field = marmot._fields['photos'].field
new_proxy = photos_field.get_proxy_obj("photos", marmot) new_proxy = photos_field.get_proxy_obj('photos', marmot)
new_proxy.put(marmot_photo, content_type="image/jpeg", foo="bar") new_proxy.put(marmot_photo, content_type='image/jpeg', foo='bar')
marmot.photos.append(new_proxy) marmot.photos.append(new_proxy)
marmot.save() marmot.save()
marmot = Animal.objects.get() marmot = Animal.objects.get()
assert marmot.photos[0].content_type == "image/jpeg" self.assertEqual(marmot.photos[0].content_type, 'image/jpeg')
assert marmot.photos[0].foo == "bar" self.assertEqual(marmot.photos[0].foo, 'bar')
assert marmot.photos[0].get().length == 8313 self.assertEqual(marmot.photos[0].get().length, 8313)
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,23 +1,31 @@
# -*- coding: utf-8 -*-
import unittest import unittest
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from mongoengine.connection import get_db
__all__ = ("GeoFieldTest", )
class TestGeoField(MongoDBTestCase): class GeoFieldTest(unittest.TestCase):
def setUp(self):
connect(db='mongoenginetest')
self.db = get_db()
def _test_for_expected_error(self, Cls, loc, expected): def _test_for_expected_error(self, Cls, loc, expected):
try: try:
Cls(loc=loc).validate() Cls(loc=loc).validate()
self.fail(f"Should not validate the location {loc}") self.fail('Should not validate the location {0}'.format(loc))
except ValidationError as e: except ValidationError as e:
assert expected == e.to_dict()["loc"] self.assertEqual(expected, e.to_dict()['loc'])
def test_geopoint_validation(self): def test_geopoint_validation(self):
class Location(Document): class Location(Document):
loc = GeoPointField() loc = GeoPointField()
invalid_coords = [{"x": 1, "y": 2}, 5, "a"] invalid_coords = [{"x": 1, "y": 2}, 5, "a"]
expected = "GeoPointField can only accept tuples or lists of (x, y)" expected = 'GeoPointField can only accept tuples or lists of (x, y)'
for coord in invalid_coords: for coord in invalid_coords:
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
@ -32,7 +40,7 @@ class TestGeoField(MongoDBTestCase):
expected = "Both values (%s) in point must be float or int" % repr(coord) expected = "Both values (%s) in point must be float or int" % repr(coord)
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
invalid_coords = [21, 4, "a"] invalid_coords = [21, 4, 'a']
for coord in invalid_coords: for coord in invalid_coords:
expected = "GeoPointField can only accept tuples or lists of (x, y)" expected = "GeoPointField can only accept tuples or lists of (x, y)"
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
@ -42,9 +50,7 @@ class TestGeoField(MongoDBTestCase):
loc = PointField() loc = PointField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = ( expected = 'PointField can only accept a valid GeoJson dictionary or lists of (x, y)'
"PointField can only accept a valid GeoJson dictionary or lists of (x, y)"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": []} invalid_coords = {"type": "MadeUp", "coordinates": []}
@ -71,16 +77,19 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
Location(loc=[1, 2]).validate() Location(loc=[1, 2]).validate()
Location( Location(loc={
loc={"type": "Point", "coordinates": [81.4471435546875, 23.61432859499169]} "type": "Point",
).validate() "coordinates": [
81.4471435546875,
23.61432859499169
]}).validate()
def test_linestring_validation(self): def test_linestring_validation(self):
class Location(Document): class Location(Document):
loc = LineStringField() loc = LineStringField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = "LineStringField can only accept a valid GeoJson dictionary or lists of (x, y)" expected = 'LineStringField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]} invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
@ -88,9 +97,7 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "LineString", "coordinates": [[1, 2, 3]]} invalid_coords = {"type": "LineString", "coordinates": [[1, 2, 3]]}
expected = ( expected = "Invalid LineString:\nValue ([1, 2, 3]) must be a two-dimensional point"
"Invalid LineString:\nValue ([1, 2, 3]) must be a two-dimensional point"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [5, "a"] invalid_coords = [5, "a"]
@ -98,25 +105,16 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[1]] invalid_coords = [[1]]
expected = ( expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0])
"Invalid LineString:\nValue (%s) must be a two-dimensional point"
% repr(invalid_coords[0])
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[1, 2, 3]] invalid_coords = [[1, 2, 3]]
expected = ( expected = "Invalid LineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0])
"Invalid LineString:\nValue (%s) must be a two-dimensional point"
% repr(invalid_coords[0])
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[{}, {}]], [("a", "b")]] invalid_coords = [[[{}, {}]], [("a", "b")]]
for coord in invalid_coords: for coord in invalid_coords:
expected = ( expected = "Invalid LineString:\nBoth values (%s) in point must be float or int" % repr(coord[0])
"Invalid LineString:\nBoth values (%s) in point must be float or int"
% repr(coord[0])
)
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
Location(loc=[[1, 2], [3, 4], [5, 6], [1, 2]]).validate() Location(loc=[[1, 2], [3, 4], [5, 6], [1, 2]]).validate()
@ -126,9 +124,7 @@ class TestGeoField(MongoDBTestCase):
loc = PolygonField() loc = PolygonField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = ( expected = 'PolygonField can only accept a valid GeoJson dictionary or lists of (x, y)'
"PolygonField can only accept a valid GeoJson dictionary or lists of (x, y)"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]} invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
@ -140,9 +136,7 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[5, "a"]]] invalid_coords = [[[5, "a"]]]
expected = ( expected = "Invalid Polygon:\nBoth values ([5, 'a']) in point must be float or int"
"Invalid Polygon:\nBoth values ([5, 'a']) in point must be float or int"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[]]] invalid_coords = [[[]]]
@ -168,7 +162,7 @@ class TestGeoField(MongoDBTestCase):
loc = MultiPointField() loc = MultiPointField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = "MultiPointField can only accept a valid GeoJson dictionary or lists of (x, y)" expected = 'MultiPointField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]} invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
@ -194,19 +188,19 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
Location(loc=[[1, 2]]).validate() Location(loc=[[1, 2]]).validate()
Location( Location(loc={
loc={
"type": "MultiPoint", "type": "MultiPoint",
"coordinates": [[1, 2], [81.4471435546875, 23.61432859499169]], "coordinates": [
} [1, 2],
).validate() [81.4471435546875, 23.61432859499169]
]}).validate()
def test_multilinestring_validation(self): def test_multilinestring_validation(self):
class Location(Document): class Location(Document):
loc = MultiLineStringField() loc = MultiLineStringField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = "MultiLineStringField can only accept a valid GeoJson dictionary or lists of (x, y)" expected = 'MultiLineStringField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]} invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
@ -222,25 +216,16 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[1]]] invalid_coords = [[[1]]]
expected = ( expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
"Invalid MultiLineString:\nValue (%s) must be a two-dimensional point"
% repr(invalid_coords[0][0])
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[1, 2, 3]]] invalid_coords = [[[1, 2, 3]]]
expected = ( expected = "Invalid MultiLineString:\nValue (%s) must be a two-dimensional point" % repr(invalid_coords[0][0])
"Invalid MultiLineString:\nValue (%s) must be a two-dimensional point"
% repr(invalid_coords[0][0])
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]] invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
for coord in invalid_coords: for coord in invalid_coords:
expected = ( expected = "Invalid MultiLineString:\nBoth values (%s) in point must be float or int" % repr(coord[0][0])
"Invalid MultiLineString:\nBoth values (%s) in point must be float or int"
% repr(coord[0][0])
)
self._test_for_expected_error(Location, coord, expected) self._test_for_expected_error(Location, coord, expected)
Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate() Location(loc=[[[1, 2], [3, 4], [5, 6], [1, 2]]]).validate()
@ -250,7 +235,7 @@ class TestGeoField(MongoDBTestCase):
loc = MultiPolygonField() loc = MultiPolygonField()
invalid_coords = {"x": 1, "y": 2} invalid_coords = {"x": 1, "y": 2}
expected = "MultiPolygonField can only accept a valid GeoJson dictionary or lists of (x, y)" expected = 'MultiPolygonField can only accept a valid GeoJson dictionary or lists of (x, y)'
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MadeUp", "coordinates": [[]]} invalid_coords = {"type": "MadeUp", "coordinates": [[]]}
@ -258,9 +243,7 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = {"type": "MultiPolygon", "coordinates": [[[[1, 2, 3]]]]} invalid_coords = {"type": "MultiPolygon", "coordinates": [[[[1, 2, 3]]]]}
expected = ( expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
"Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[5, "a"]]]] invalid_coords = [[[[5, "a"]]]]
@ -272,9 +255,7 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[1, 2, 3]]]] invalid_coords = [[[[1, 2, 3]]]]
expected = ( expected = "Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
"Invalid MultiPolygon:\nValue ([1, 2, 3]) must be a two-dimensional point"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[{}, {}]]], [[("a", "b")]]] invalid_coords = [[[[{}, {}]]], [[("a", "b")]]]
@ -282,28 +263,25 @@ class TestGeoField(MongoDBTestCase):
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
invalid_coords = [[[[1, 2], [3, 4]]]] invalid_coords = [[[[1, 2], [3, 4]]]]
expected = ( expected = "Invalid MultiPolygon:\nLineStrings must start and end at the same point"
"Invalid MultiPolygon:\nLineStrings must start and end at the same point"
)
self._test_for_expected_error(Location, invalid_coords, expected) self._test_for_expected_error(Location, invalid_coords, expected)
Location(loc=[[[[1, 2], [3, 4], [5, 6], [1, 2]]]]).validate() Location(loc=[[[[1, 2], [3, 4], [5, 6], [1, 2]]]]).validate()
def test_indexes_geopoint(self): def test_indexes_geopoint(self):
"""Ensure that indexes are created automatically for GeoPointFields.""" """Ensure that indexes are created automatically for GeoPointFields.
"""
class Event(Document): class Event(Document):
title = StringField() title = StringField()
location = GeoPointField() location = GeoPointField()
geo_indicies = Event._geo_indices() geo_indicies = Event._geo_indices()
assert geo_indicies == [{"fields": [("location", "2d")]}] self.assertEqual(geo_indicies, [{'fields': [('location', '2d')]}])
def test_geopoint_embedded_indexes(self): def test_geopoint_embedded_indexes(self):
"""Ensure that indexes are created automatically for GeoPointFields on """Ensure that indexes are created automatically for GeoPointFields on
embedded documents. embedded documents.
""" """
class Venue(EmbeddedDocument): class Venue(EmbeddedDocument):
location = GeoPointField() location = GeoPointField()
name = StringField() name = StringField()
@ -313,11 +291,11 @@ class TestGeoField(MongoDBTestCase):
venue = EmbeddedDocumentField(Venue) venue = EmbeddedDocumentField(Venue)
geo_indicies = Event._geo_indices() geo_indicies = Event._geo_indices()
assert geo_indicies == [{"fields": [("venue.location", "2d")]}] self.assertEqual(geo_indicies, [{'fields': [('venue.location', '2d')]}])
def test_indexes_2dsphere(self): def test_indexes_2dsphere(self):
"""Ensure that indexes are created automatically for GeoPointFields.""" """Ensure that indexes are created automatically for GeoPointFields.
"""
class Event(Document): class Event(Document):
title = StringField() title = StringField()
point = PointField() point = PointField()
@ -325,13 +303,13 @@ class TestGeoField(MongoDBTestCase):
polygon = PolygonField() polygon = PolygonField()
geo_indicies = Event._geo_indices() geo_indicies = Event._geo_indices()
assert {"fields": [("line", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('line', '2dsphere')]}, geo_indicies)
assert {"fields": [("polygon", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('polygon', '2dsphere')]}, geo_indicies)
assert {"fields": [("point", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('point', '2dsphere')]}, geo_indicies)
def test_indexes_2dsphere_embedded(self): def test_indexes_2dsphere_embedded(self):
"""Ensure that indexes are created automatically for GeoPointFields.""" """Ensure that indexes are created automatically for GeoPointFields.
"""
class Venue(EmbeddedDocument): class Venue(EmbeddedDocument):
name = StringField() name = StringField()
point = PointField() point = PointField()
@ -343,11 +321,12 @@ class TestGeoField(MongoDBTestCase):
venue = EmbeddedDocumentField(Venue) venue = EmbeddedDocumentField(Venue)
geo_indicies = Event._geo_indices() geo_indicies = Event._geo_indices()
assert {"fields": [("venue.line", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('venue.line', '2dsphere')]}, geo_indicies)
assert {"fields": [("venue.polygon", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('venue.polygon', '2dsphere')]}, geo_indicies)
assert {"fields": [("venue.point", "2dsphere")]} in geo_indicies self.assertIn({'fields': [('venue.point', '2dsphere')]}, geo_indicies)
def test_geo_indexes_recursion(self): def test_geo_indexes_recursion(self):
class Location(Document): class Location(Document):
name = StringField() name = StringField()
location = GeoPointField() location = GeoPointField()
@ -359,14 +338,14 @@ class TestGeoField(MongoDBTestCase):
Location.drop_collection() Location.drop_collection()
Parent.drop_collection() Parent.drop_collection()
Parent(name="Berlin").save() Parent(name='Berlin').save()
info = Parent._get_collection().index_information() info = Parent._get_collection().index_information()
assert "location_2d" not in info self.assertNotIn('location_2d', info)
info = Location._get_collection().index_information() info = Location._get_collection().index_information()
assert "location_2d" in info self.assertIn('location_2d', info)
assert len(Parent._geo_indices()) == 0 self.assertEqual(len(Parent._geo_indices()), 0)
assert len(Location._geo_indices()) == 1 self.assertEqual(len(Location._geo_indices()), 1)
def test_geo_indexes_auto_index(self): def test_geo_indexes_auto_index(self):
@ -375,18 +354,18 @@ class TestGeoField(MongoDBTestCase):
location = PointField(auto_index=False) location = PointField(auto_index=False)
datetime = DateTimeField() datetime = DateTimeField()
meta = {"indexes": [[("location", "2dsphere"), ("datetime", 1)]]} meta = {
'indexes': [[("location", "2dsphere"), ("datetime", 1)]]
}
assert Log._geo_indices() == [] self.assertEqual([], Log._geo_indices())
Log.drop_collection() Log.drop_collection()
Log.ensure_indexes() Log.ensure_indexes()
info = Log._get_collection().index_information() info = Log._get_collection().index_information()
assert info["location_2dsphere_datetime_1"]["key"] == [ self.assertEqual(info["location_2dsphere_datetime_1"]["key"],
("location", "2dsphere"), [('location', '2dsphere'), ('datetime', 1)])
("datetime", 1),
]
# Test listing explicitly # Test listing explicitly
class Log(Document): class Log(Document):
@ -394,20 +373,20 @@ class TestGeoField(MongoDBTestCase):
datetime = DateTimeField() datetime = DateTimeField()
meta = { meta = {
"indexes": [{"fields": [("location", "2dsphere"), ("datetime", 1)]}] 'indexes': [
{'fields': [("location", "2dsphere"), ("datetime", 1)]}
]
} }
assert Log._geo_indices() == [] self.assertEqual([], Log._geo_indices())
Log.drop_collection() Log.drop_collection()
Log.ensure_indexes() Log.ensure_indexes()
info = Log._get_collection().index_information() info = Log._get_collection().index_information()
assert info["location_2dsphere_datetime_1"]["key"] == [ self.assertEqual(info["location_2dsphere_datetime_1"]["key"],
("location", "2dsphere"), [('location', '2dsphere'), ('datetime', 1)])
("datetime", 1),
]
if __name__ == "__main__": if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -1,26 +1,27 @@
# -*- coding: utf-8 -*-
import uuid import uuid
import pytest from nose.plugins.skip import SkipTest
import six
from bson import Binary from bson import Binary
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
BIN_VALUE = "\xa9\xf3\x8d(\xd7\x03\x84\xb4k[\x0f\xe3\xa2\x19\x85p[J\xa3\xd2>\xde\xe6\x87\xb1\x7f\xc6\xe6\xd9r\x18\xf5".encode( BIN_VALUE = six.b('\xa9\xf3\x8d(\xd7\x03\x84\xb4k[\x0f\xe3\xa2\x19\x85p[J\xa3\xd2>\xde\xe6\x87\xb1\x7f\xc6\xe6\xd9r\x18\xf5')
"latin-1"
)
class TestBinaryField(MongoDBTestCase): class TestBinaryField(MongoDBTestCase):
def test_binary_fields(self): def test_binary_fields(self):
"""Ensure that binary fields can be stored and retrieved.""" """Ensure that binary fields can be stored and retrieved.
"""
class Attachment(Document): class Attachment(Document):
content_type = StringField() content_type = StringField()
blob = BinaryField() blob = BinaryField()
BLOB = b"\xe6\x00\xc4\xff\x07" BLOB = six.b('\xe6\x00\xc4\xff\x07')
MIME_TYPE = "application/octet-stream" MIME_TYPE = 'application/octet-stream'
Attachment.drop_collection() Attachment.drop_collection()
@ -28,12 +29,12 @@ class TestBinaryField(MongoDBTestCase):
attachment.save() attachment.save()
attachment_1 = Attachment.objects().first() attachment_1 = Attachment.objects().first()
assert MIME_TYPE == attachment_1.content_type self.assertEqual(MIME_TYPE, attachment_1.content_type)
assert BLOB == bytes(attachment_1.blob) self.assertEqual(BLOB, six.binary_type(attachment_1.blob))
def test_validation_succeeds(self): def test_validation_succeeds(self):
"""Ensure that valid values can be assigned to binary fields.""" """Ensure that valid values can be assigned to binary fields.
"""
class AttachmentRequired(Document): class AttachmentRequired(Document):
blob = BinaryField(required=True) blob = BinaryField(required=True)
@ -41,15 +42,13 @@ class TestBinaryField(MongoDBTestCase):
blob = BinaryField(max_bytes=4) blob = BinaryField(max_bytes=4)
attachment_required = AttachmentRequired() attachment_required = AttachmentRequired()
with pytest.raises(ValidationError): self.assertRaises(ValidationError, attachment_required.validate)
attachment_required.validate() attachment_required.blob = Binary(six.b('\xe6\x00\xc4\xff\x07'))
attachment_required.blob = Binary(b"\xe6\x00\xc4\xff\x07")
attachment_required.validate() attachment_required.validate()
_5_BYTES = b"\xe6\x00\xc4\xff\x07" _5_BYTES = six.b('\xe6\x00\xc4\xff\x07')
_4_BYTES = b"\xe6\x00\xc4\xff" _4_BYTES = six.b('\xe6\x00\xc4\xff')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, AttachmentSizeLimit(blob=_5_BYTES).validate)
AttachmentSizeLimit(blob=_5_BYTES).validate()
AttachmentSizeLimit(blob=_4_BYTES).validate() AttachmentSizeLimit(blob=_4_BYTES).validate()
def test_validation_fails(self): def test_validation_fails(self):
@ -58,9 +57,8 @@ class TestBinaryField(MongoDBTestCase):
class Attachment(Document): class Attachment(Document):
blob = BinaryField() blob = BinaryField()
for invalid_data in (2, "Im_a_unicode", ["some_str"]): for invalid_data in (2, u'Im_a_unicode', ['some_str']):
with pytest.raises(ValidationError): self.assertRaises(ValidationError, Attachment(blob=invalid_data).validate)
Attachment(blob=invalid_data).validate()
def test__primary(self): def test__primary(self):
class Attachment(Document): class Attachment(Document):
@ -69,21 +67,23 @@ class TestBinaryField(MongoDBTestCase):
Attachment.drop_collection() Attachment.drop_collection()
binary_id = uuid.uuid4().bytes binary_id = uuid.uuid4().bytes
att = Attachment(id=binary_id).save() att = Attachment(id=binary_id).save()
assert 1 == Attachment.objects.count() self.assertEqual(1, Attachment.objects.count())
assert 1 == Attachment.objects.filter(id=att.id).count() self.assertEqual(1, Attachment.objects.filter(id=att.id).count())
att.delete() att.delete()
assert 0 == Attachment.objects.count() self.assertEqual(0, Attachment.objects.count())
def test_primary_filter_by_binary_pk_as_str(self): def test_primary_filter_by_binary_pk_as_str(self):
raise SkipTest("Querying by id as string is not currently supported")
class Attachment(Document): class Attachment(Document):
id = BinaryField(primary_key=True) id = BinaryField(primary_key=True)
Attachment.drop_collection() Attachment.drop_collection()
binary_id = uuid.uuid4().bytes binary_id = uuid.uuid4().bytes
att = Attachment(id=binary_id).save() att = Attachment(id=binary_id).save()
assert 1 == Attachment.objects.filter(id=binary_id).count() self.assertEqual(1, Attachment.objects.filter(id=binary_id).count())
att.delete() att.delete()
assert 0 == Attachment.objects.count() self.assertEqual(0, Attachment.objects.count())
def test_match_querying_with_bytes(self): def test_match_querying_with_bytes(self):
class MyDocument(Document): class MyDocument(Document):
@ -93,7 +93,7 @@ class TestBinaryField(MongoDBTestCase):
doc = MyDocument(bin_field=BIN_VALUE).save() doc = MyDocument(bin_field=BIN_VALUE).save()
matched_doc = MyDocument.objects(bin_field=BIN_VALUE).first() matched_doc = MyDocument.objects(bin_field=BIN_VALUE).first()
assert matched_doc.id == doc.id self.assertEqual(matched_doc.id, doc.id)
def test_match_querying_with_binary(self): def test_match_querying_with_binary(self):
class MyDocument(Document): class MyDocument(Document):
@ -104,37 +104,40 @@ class TestBinaryField(MongoDBTestCase):
doc = MyDocument(bin_field=BIN_VALUE).save() doc = MyDocument(bin_field=BIN_VALUE).save()
matched_doc = MyDocument.objects(bin_field=Binary(BIN_VALUE)).first() matched_doc = MyDocument.objects(bin_field=Binary(BIN_VALUE)).first()
assert matched_doc.id == doc.id self.assertEqual(matched_doc.id, doc.id)
def test_modify_operation__set(self): def test_modify_operation__set(self):
"""Ensures no regression of bug #1127""" """Ensures no regression of bug #1127"""
class MyDocument(Document): class MyDocument(Document):
some_field = StringField() some_field = StringField()
bin_field = BinaryField() bin_field = BinaryField()
MyDocument.drop_collection() MyDocument.drop_collection()
doc = MyDocument.objects(some_field="test").modify( doc = MyDocument.objects(some_field='test').modify(
upsert=True, new=True, set__bin_field=BIN_VALUE upsert=True, new=True,
set__bin_field=BIN_VALUE
) )
assert doc.some_field == "test" self.assertEqual(doc.some_field, 'test')
assert doc.bin_field == BIN_VALUE if six.PY3:
self.assertEqual(doc.bin_field, BIN_VALUE)
else:
self.assertEqual(doc.bin_field, Binary(BIN_VALUE))
def test_update_one(self): def test_update_one(self):
"""Ensures no regression of bug #1127""" """Ensures no regression of bug #1127"""
class MyDocument(Document): class MyDocument(Document):
bin_field = BinaryField() bin_field = BinaryField()
MyDocument.drop_collection() MyDocument.drop_collection()
bin_data = b"\xe6\x00\xc4\xff\x07" bin_data = six.b('\xe6\x00\xc4\xff\x07')
doc = MyDocument(bin_field=bin_data).save() doc = MyDocument(bin_field=bin_data).save()
n_updated = MyDocument.objects(bin_field=bin_data).update_one( n_updated = MyDocument.objects(bin_field=bin_data).update_one(bin_field=BIN_VALUE)
bin_field=BIN_VALUE self.assertEqual(n_updated, 1)
)
assert n_updated == 1
fetched = MyDocument.objects.with_id(doc.id) fetched = MyDocument.objects.with_id(doc.id)
assert fetched.bin_field == BIN_VALUE if six.PY3:
self.assertEqual(fetched.bin_field, BIN_VALUE)
else:
self.assertEqual(fetched.bin_field, Binary(BIN_VALUE))

View File

@ -1,6 +1,6 @@
import pytest # -*- coding: utf-8 -*-
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase, get_as_pymongo from tests.utils import MongoDBTestCase, get_as_pymongo
@ -11,24 +11,15 @@ class TestBooleanField(MongoDBTestCase):
person = Person(admin=True) person = Person(admin=True)
person.save() person.save()
assert get_as_pymongo(person) == {"_id": person.id, "admin": True} self.assertEqual(
get_as_pymongo(person),
def test_construction_does_not_fail_uncastable_value(self): {'_id': person.id,
class BoolFail: 'admin': True})
def __bool__(self):
return "bogus"
class Person(Document):
admin = BooleanField()
person = Person(admin=BoolFail())
person.admin == "bogus"
def test_validation(self): def test_validation(self):
"""Ensure that invalid values cannot be assigned to boolean """Ensure that invalid values cannot be assigned to boolean
fields. fields.
""" """
class Person(Document): class Person(Document):
admin = BooleanField() admin = BooleanField()
@ -37,26 +28,22 @@ class TestBooleanField(MongoDBTestCase):
person.validate() person.validate()
person.admin = 2 person.admin = 2
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate() person.admin = 'Yes'
person.admin = "Yes" self.assertRaises(ValidationError, person.validate)
with pytest.raises(ValidationError): person.admin = 'False'
person.validate() self.assertRaises(ValidationError, person.validate)
person.admin = "False"
with pytest.raises(ValidationError):
person.validate()
def test_weirdness_constructor(self): def test_weirdness_constructor(self):
"""When attribute is set in contructor, it gets cast into a bool """When attribute is set in contructor, it gets cast into a bool
which causes some weird behavior. We dont necessarily want to maintain this behavior which causes some weird behavior. We dont necessarily want to maintain this behavior
but its a known issue but its a known issue
""" """
class Person(Document): class Person(Document):
admin = BooleanField() admin = BooleanField()
new_person = Person(admin="False") new_person = Person(admin='False')
assert new_person.admin self.assertTrue(new_person.admin)
new_person = Person(admin="0") new_person = Person(admin='0')
assert new_person.admin self.assertTrue(new_person.admin)

View File

@ -1,35 +1,18 @@
# -*- coding: utf-8 -*-
from decimal import Decimal from decimal import Decimal
import pytest from mongoengine import *
from mongoengine import (
CachedReferenceField,
DecimalField,
Document,
EmbeddedDocument,
EmbeddedDocumentField,
InvalidDocumentError,
ListField,
ReferenceField,
StringField,
ValidationError,
)
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
class TestCachedReferenceField(MongoDBTestCase): class TestCachedReferenceField(MongoDBTestCase):
def test_constructor_fail_bad_document_type(self):
with pytest.raises(
ValidationError, match="must be a document class or a string"
):
CachedReferenceField(document_type=0)
def test_get_and_save(self): def test_get_and_save(self):
""" """
Tests #1047: CachedReferenceField creates DBRefs on to_python, Tests #1047: CachedReferenceField creates DBRefs on to_python,
but can't save them on to_mongo. but can't save them on to_mongo.
""" """
class Animal(Document): class Animal(Document):
name = StringField() name = StringField()
tag = StringField() tag = StringField()
@ -41,11 +24,10 @@ class TestCachedReferenceField(MongoDBTestCase):
Animal.drop_collection() Animal.drop_collection()
Ocorrence.drop_collection() Ocorrence.drop_collection()
Ocorrence( Ocorrence(person="testte",
person="testte", animal=Animal(name="Leopard", tag="heavy").save() animal=Animal(name="Leopard", tag="heavy").save()).save()
).save()
p = Ocorrence.objects.get() p = Ocorrence.objects.get()
p.person = "new_testte" p.person = 'new_testte'
p.save() p.save()
def test_general_things(self): def test_general_things(self):
@ -55,7 +37,8 @@ class TestCachedReferenceField(MongoDBTestCase):
class Ocorrence(Document): class Ocorrence(Document):
person = StringField() person = StringField()
animal = CachedReferenceField(Animal, fields=["tag"]) animal = CachedReferenceField(
Animal, fields=['tag'])
Animal.drop_collection() Animal.drop_collection()
Ocorrence.drop_collection() Ocorrence.drop_collection()
@ -63,29 +46,30 @@ class TestCachedReferenceField(MongoDBTestCase):
a = Animal(name="Leopard", tag="heavy") a = Animal(name="Leopard", tag="heavy")
a.save() a.save()
assert Animal._cached_reference_fields == [Ocorrence.animal] self.assertEqual(Animal._cached_reference_fields, [Ocorrence.animal])
o = Ocorrence(person="teste", animal=a) o = Ocorrence(person="teste", animal=a)
o.save() o.save()
p = Ocorrence(person="Wilson") p = Ocorrence(person="Wilson")
p.save() p.save()
assert Ocorrence.objects(animal=None).count() == 1 self.assertEqual(Ocorrence.objects(animal=None).count(), 1)
assert a.to_mongo(fields=["tag"]) == {"tag": "heavy", "_id": a.pk} self.assertEqual(
a.to_mongo(fields=['tag']), {'tag': 'heavy', "_id": a.pk})
assert o.to_mongo()["animal"]["tag"] == "heavy" self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy')
# counts # counts
Ocorrence(person="teste 2").save() Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save() Ocorrence(person="teste 3").save()
count = Ocorrence.objects(animal__tag="heavy").count() count = Ocorrence.objects(animal__tag='heavy').count()
assert count == 1 self.assertEqual(count, 1)
ocorrence = Ocorrence.objects(animal__tag="heavy").first() ocorrence = Ocorrence.objects(animal__tag='heavy').first()
assert ocorrence.person == "teste" self.assertEqual(ocorrence.person, "teste")
assert isinstance(ocorrence.animal, Animal) self.assertIsInstance(ocorrence.animal, Animal)
def test_with_decimal(self): def test_with_decimal(self):
class PersonAuto(Document): class PersonAuto(Document):
@ -94,22 +78,28 @@ class TestCachedReferenceField(MongoDBTestCase):
class SocialTest(Document): class SocialTest(Document):
group = StringField() group = StringField()
person = CachedReferenceField(PersonAuto, fields=("salary",)) person = CachedReferenceField(
PersonAuto,
fields=('salary',))
PersonAuto.drop_collection() PersonAuto.drop_collection()
SocialTest.drop_collection() SocialTest.drop_collection()
p = PersonAuto(name="Alberto", salary=Decimal("7000.00")) p = PersonAuto(name="Alberto", salary=Decimal('7000.00'))
p.save() p.save()
s = SocialTest(group="dev", person=p) s = SocialTest(group="dev", person=p)
s.save() s.save()
assert SocialTest.objects._collection.find_one({"person.salary": 7000.00}) == { self.assertEqual(
"_id": s.pk, SocialTest.objects._collection.find_one({'person.salary': 7000.00}), {
"group": s.group, '_id': s.pk,
"person": {"_id": p.pk, "salary": 7000.00}, 'group': s.group,
'person': {
'_id': p.pk,
'salary': 7000.00
} }
})
def test_cached_reference_field_reference(self): def test_cached_reference_field_reference(self):
class Group(Document): class Group(Document):
@ -121,14 +111,17 @@ class TestCachedReferenceField(MongoDBTestCase):
class SocialData(Document): class SocialData(Document):
obs = StringField() obs = StringField()
tags = ListField(StringField()) tags = ListField(
person = CachedReferenceField(Person, fields=("group",)) StringField())
person = CachedReferenceField(
Person,
fields=('group',))
Group.drop_collection() Group.drop_collection()
Person.drop_collection() Person.drop_collection()
SocialData.drop_collection() SocialData.drop_collection()
g1 = Group(name="dev") g1 = Group(name='dev')
g1.save() g1.save()
g2 = Group(name="designers") g2 = Group(name="designers")
@ -143,21 +136,25 @@ class TestCachedReferenceField(MongoDBTestCase):
p3 = Person(name="Afro design", group=g2) p3 = Person(name="Afro design", group=g2)
p3.save() p3.save()
s1 = SocialData(obs="testing 123", person=p1, tags=["tag1", "tag2"]) s1 = SocialData(obs="testing 123", person=p1, tags=['tag1', 'tag2'])
s1.save() s1.save()
s2 = SocialData(obs="testing 321", person=p3, tags=["tag3", "tag4"]) s2 = SocialData(obs="testing 321", person=p3, tags=['tag3', 'tag4'])
s2.save() s2.save()
assert SocialData.objects._collection.find_one({"tags": "tag2"}) == { self.assertEqual(SocialData.objects._collection.find_one(
"_id": s1.pk, {'tags': 'tag2'}), {
"obs": "testing 123", '_id': s1.pk,
"tags": ["tag1", "tag2"], 'obs': 'testing 123',
"person": {"_id": p1.pk, "group": g1.pk}, 'tags': ['tag1', 'tag2'],
'person': {
'_id': p1.pk,
'group': g1.pk
} }
})
assert SocialData.objects(person__group=g2).count() == 1 self.assertEqual(SocialData.objects(person__group=g2).count(), 1)
assert SocialData.objects(person__group=g2).first() == s2 self.assertEqual(SocialData.objects(person__group=g2).first(), s2)
def test_cached_reference_field_push_with_fields(self): def test_cached_reference_field_push_with_fields(self):
class Product(Document): class Product(Document):
@ -166,136 +163,185 @@ class TestCachedReferenceField(MongoDBTestCase):
Product.drop_collection() Product.drop_collection()
class Basket(Document): class Basket(Document):
products = ListField(CachedReferenceField(Product, fields=["name"])) products = ListField(CachedReferenceField(Product, fields=['name']))
Basket.drop_collection() Basket.drop_collection()
product1 = Product(name="abc").save() product1 = Product(name='abc').save()
product2 = Product(name="def").save() product2 = Product(name='def').save()
basket = Basket(products=[product1]).save() basket = Basket(products=[product1]).save()
assert Basket.objects._collection.find_one() == { self.assertEqual(
"_id": basket.pk, Basket.objects._collection.find_one(),
"products": [{"_id": product1.pk, "name": product1.name}], {
'_id': basket.pk,
'products': [
{
'_id': product1.pk,
'name': product1.name
} }
]
}
)
# push to list # push to list
basket.update(push__products=product2) basket.update(push__products=product2)
basket.reload() basket.reload()
assert Basket.objects._collection.find_one() == { self.assertEqual(
"_id": basket.pk, Basket.objects._collection.find_one(),
"products": [ {
{"_id": product1.pk, "name": product1.name}, '_id': basket.pk,
{"_id": product2.pk, "name": product2.name}, 'products': [
], {
'_id': product1.pk,
'name': product1.name
},
{
'_id': product2.pk,
'name': product2.name
} }
]
}
)
def test_cached_reference_field_update_all(self): def test_cached_reference_field_update_all(self):
class Person(Document): class Person(Document):
TYPES = (("pf", "PF"), ("pj", "PJ")) TYPES = (
('pf', "PF"),
('pj', "PJ")
)
name = StringField() name = StringField()
tp = StringField(choices=TYPES) tp = StringField(choices=TYPES)
father = CachedReferenceField("self", fields=("tp",)) father = CachedReferenceField('self', fields=('tp',))
Person.drop_collection() Person.drop_collection()
a1 = Person(name="Wilson Father", tp="pj") a1 = Person(name="Wilson Father", tp="pj")
a1.save() a1.save()
a2 = Person(name="Wilson Junior", tp="pf", father=a1) a2 = Person(name='Wilson Junior', tp='pf', father=a1)
a2.save() a2.save()
a2 = Person.objects.with_id(a2.id) a2 = Person.objects.with_id(a2.id)
assert a2.father.tp == a1.tp self.assertEqual(a2.father.tp, a1.tp)
assert dict(a2.to_mongo()) == { self.assertEqual(dict(a2.to_mongo()), {
"_id": a2.pk, "_id": a2.pk,
"name": "Wilson Junior", "name": u"Wilson Junior",
"tp": "pf", "tp": u"pf",
"father": {"_id": a1.pk, "tp": "pj"}, "father": {
"_id": a1.pk,
"tp": u"pj"
} }
})
assert Person.objects(father=a1)._query == {"father._id": a1.pk} self.assertEqual(Person.objects(father=a1)._query, {
assert Person.objects(father=a1).count() == 1 'father._id': a1.pk
})
self.assertEqual(Person.objects(father=a1).count(), 1)
Person.objects.update(set__tp="pf") Person.objects.update(set__tp="pf")
Person.father.sync_all() Person.father.sync_all()
a2.reload() a2.reload()
assert dict(a2.to_mongo()) == { self.assertEqual(dict(a2.to_mongo()), {
"_id": a2.pk, "_id": a2.pk,
"name": "Wilson Junior", "name": u"Wilson Junior",
"tp": "pf", "tp": u"pf",
"father": {"_id": a1.pk, "tp": "pf"}, "father": {
"_id": a1.pk,
"tp": u"pf"
} }
})
def test_cached_reference_fields_on_embedded_documents(self): def test_cached_reference_fields_on_embedded_documents(self):
with pytest.raises(InvalidDocumentError): with self.assertRaises(InvalidDocumentError):
class Test(Document): class Test(Document):
name = StringField() name = StringField()
type( type('WrongEmbeddedDocument', (
"WrongEmbeddedDocument", EmbeddedDocument,), {
(EmbeddedDocument,), 'test': CachedReferenceField(Test)
{"test": CachedReferenceField(Test)}, })
)
def test_cached_reference_auto_sync(self): def test_cached_reference_auto_sync(self):
class Person(Document): class Person(Document):
TYPES = (("pf", "PF"), ("pj", "PJ")) TYPES = (
('pf', "PF"),
('pj', "PJ")
)
name = StringField() name = StringField()
tp = StringField(choices=TYPES) tp = StringField(
choices=TYPES
)
father = CachedReferenceField("self", fields=("tp",)) father = CachedReferenceField('self', fields=('tp',))
Person.drop_collection() Person.drop_collection()
a1 = Person(name="Wilson Father", tp="pj") a1 = Person(name="Wilson Father", tp="pj")
a1.save() a1.save()
a2 = Person(name="Wilson Junior", tp="pf", father=a1) a2 = Person(name='Wilson Junior', tp='pf', father=a1)
a2.save() a2.save()
a1.tp = "pf" a1.tp = 'pf'
a1.save() a1.save()
a2.reload() a2.reload()
assert dict(a2.to_mongo()) == { self.assertEqual(dict(a2.to_mongo()), {
"_id": a2.pk, '_id': a2.pk,
"name": "Wilson Junior", 'name': 'Wilson Junior',
"tp": "pf", 'tp': 'pf',
"father": {"_id": a1.pk, "tp": "pf"}, 'father': {
'_id': a1.pk,
'tp': 'pf'
} }
})
def test_cached_reference_auto_sync_disabled(self): def test_cached_reference_auto_sync_disabled(self):
class Persone(Document): class Persone(Document):
TYPES = (("pf", "PF"), ("pj", "PJ")) TYPES = (
('pf', "PF"),
('pj', "PJ")
)
name = StringField() name = StringField()
tp = StringField(choices=TYPES) tp = StringField(
choices=TYPES
)
father = CachedReferenceField("self", fields=("tp",), auto_sync=False) father = CachedReferenceField(
'self', fields=('tp',), auto_sync=False)
Persone.drop_collection() Persone.drop_collection()
a1 = Persone(name="Wilson Father", tp="pj") a1 = Persone(name="Wilson Father", tp="pj")
a1.save() a1.save()
a2 = Persone(name="Wilson Junior", tp="pf", father=a1) a2 = Persone(name='Wilson Junior', tp='pf', father=a1)
a2.save() a2.save()
a1.tp = "pf" a1.tp = 'pf'
a1.save() a1.save()
assert Persone.objects._collection.find_one({"_id": a2.pk}) == { self.assertEqual(Persone.objects._collection.find_one({'_id': a2.pk}), {
"_id": a2.pk, '_id': a2.pk,
"name": "Wilson Junior", 'name': 'Wilson Junior',
"tp": "pf", 'tp': 'pf',
"father": {"_id": a1.pk, "tp": "pj"}, 'father': {
'_id': a1.pk,
'tp': 'pj'
} }
})
def test_cached_reference_embedded_fields(self): def test_cached_reference_embedded_fields(self):
class Owner(EmbeddedDocument): class Owner(EmbeddedDocument):
TPS = (("n", "Normal"), ("u", "Urgent")) TPS = (
('n', "Normal"),
('u', "Urgent")
)
name = StringField() name = StringField()
tp = StringField(verbose_name="Type", db_field="t", choices=TPS) tp = StringField(
verbose_name="Type",
db_field="t",
choices=TPS)
class Animal(Document): class Animal(Document):
name = StringField() name = StringField()
@ -305,41 +351,45 @@ class TestCachedReferenceField(MongoDBTestCase):
class Ocorrence(Document): class Ocorrence(Document):
person = StringField() person = StringField()
animal = CachedReferenceField(Animal, fields=["tag", "owner.tp"]) animal = CachedReferenceField(
Animal, fields=['tag', 'owner.tp'])
Animal.drop_collection() Animal.drop_collection()
Ocorrence.drop_collection() Ocorrence.drop_collection()
a = Animal( a = Animal(name="Leopard", tag="heavy",
name="Leopard", tag="heavy", owner=Owner(tp="u", name="Wilson Júnior") owner=Owner(tp='u', name="Wilson Júnior")
) )
a.save() a.save()
o = Ocorrence(person="teste", animal=a) o = Ocorrence(person="teste", animal=a)
o.save() o.save()
assert dict(a.to_mongo(fields=["tag", "owner.tp"])) == { self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tp'])), {
"_id": a.pk, '_id': a.pk,
"tag": "heavy", 'tag': 'heavy',
"owner": {"t": "u"}, 'owner': {
't': 'u'
} }
assert o.to_mongo()["animal"]["tag"] == "heavy" })
assert o.to_mongo()["animal"]["owner"]["t"] == "u" self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy')
self.assertEqual(o.to_mongo()['animal']['owner']['t'], 'u')
# Check to_mongo with fields # Check to_mongo with fields
assert "animal" not in o.to_mongo(fields=["person"]) self.assertNotIn('animal', o.to_mongo(fields=['person']))
# counts # counts
Ocorrence(person="teste 2").save() Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save() Ocorrence(person="teste 3").save()
count = Ocorrence.objects(animal__tag="heavy", animal__owner__tp="u").count() count = Ocorrence.objects(
assert count == 1 animal__tag='heavy', animal__owner__tp='u').count()
self.assertEqual(count, 1)
ocorrence = Ocorrence.objects( ocorrence = Ocorrence.objects(
animal__tag="heavy", animal__owner__tp="u" animal__tag='heavy',
).first() animal__owner__tp='u').first()
assert ocorrence.person == "teste" self.assertEqual(ocorrence.person, "teste")
assert isinstance(ocorrence.animal, Animal) self.assertIsInstance(ocorrence.animal, Animal)
def test_cached_reference_embedded_list_fields(self): def test_cached_reference_embedded_list_fields(self):
class Owner(EmbeddedDocument): class Owner(EmbeddedDocument):
@ -354,40 +404,43 @@ class TestCachedReferenceField(MongoDBTestCase):
class Ocorrence(Document): class Ocorrence(Document):
person = StringField() person = StringField()
animal = CachedReferenceField(Animal, fields=["tag", "owner.tags"]) animal = CachedReferenceField(
Animal, fields=['tag', 'owner.tags'])
Animal.drop_collection() Animal.drop_collection()
Ocorrence.drop_collection() Ocorrence.drop_collection()
a = Animal( a = Animal(name="Leopard", tag="heavy",
name="Leopard", owner=Owner(tags=['cool', 'funny'],
tag="heavy", name="Wilson Júnior")
owner=Owner(tags=["cool", "funny"], name="Wilson Júnior"),
) )
a.save() a.save()
o = Ocorrence(person="teste 2", animal=a) o = Ocorrence(person="teste 2", animal=a)
o.save() o.save()
assert dict(a.to_mongo(fields=["tag", "owner.tags"])) == { self.assertEqual(dict(a.to_mongo(fields=['tag', 'owner.tags'])), {
"_id": a.pk, '_id': a.pk,
"tag": "heavy", 'tag': 'heavy',
"owner": {"tags": ["cool", "funny"]}, 'owner': {
'tags': ['cool', 'funny']
} }
})
assert o.to_mongo()["animal"]["tag"] == "heavy" self.assertEqual(o.to_mongo()['animal']['tag'], 'heavy')
assert o.to_mongo()["animal"]["owner"]["tags"] == ["cool", "funny"] self.assertEqual(o.to_mongo()['animal']['owner']['tags'],
['cool', 'funny'])
# counts # counts
Ocorrence(person="teste 2").save() Ocorrence(person="teste 2").save()
Ocorrence(person="teste 3").save() Ocorrence(person="teste 3").save()
query = Ocorrence.objects( query = Ocorrence.objects(
animal__tag="heavy", animal__owner__tags="cool" animal__tag='heavy', animal__owner__tags='cool')._query
)._query self.assertEqual(
assert query == {"animal.owner.tags": "cool", "animal.tag": "heavy"} query, {'animal.owner.tags': 'cool', 'animal.tag': 'heavy'})
ocorrence = Ocorrence.objects( ocorrence = Ocorrence.objects(
animal__tag="heavy", animal__owner__tags="cool" animal__tag='heavy',
).first() animal__owner__tags='cool').first()
assert ocorrence.person == "teste 2" self.assertEqual(ocorrence.person, "teste 2")
assert isinstance(ocorrence.animal, Animal) self.assertIsInstance(ocorrence.animal, Animal)

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
import datetime import datetime
import itertools
import math import math
import itertools
import re import re
import pytest
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -14,10 +14,9 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
"""Tests for complex datetime fields - which can handle """Tests for complex datetime fields - which can handle
microseconds without rounding. microseconds without rounding.
""" """
class LogEntry(Document): class LogEntry(Document):
date = ComplexDateTimeField() date = ComplexDateTimeField()
date_with_dots = ComplexDateTimeField(separator=".") date_with_dots = ComplexDateTimeField(separator='.')
LogEntry.drop_collection() LogEntry.drop_collection()
@ -28,7 +27,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1 self.assertEqual(log.date, d1)
# Post UTC - microseconds are rounded (down) nearest millisecond - with # Post UTC - microseconds are rounded (down) nearest millisecond - with
# default datetimefields # default datetimefields
@ -36,7 +35,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1 self.assertEqual(log.date, d1)
# Pre UTC dates microseconds below 1000 are dropped - with default # Pre UTC dates microseconds below 1000 are dropped - with default
# datetimefields # datetimefields
@ -44,7 +43,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1 self.assertEqual(log.date, d1)
# Pre UTC microseconds above 1000 is wonky - with default datetimefields # Pre UTC microseconds above 1000 is wonky - with default datetimefields
# log.date has an invalid microsecond value so I can't construct # log.date has an invalid microsecond value so I can't construct
@ -54,34 +53,26 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1 self.assertEqual(log.date, d1)
log1 = LogEntry.objects.get(date=d1) log1 = LogEntry.objects.get(date=d1)
assert log == log1 self.assertEqual(log, log1)
# Test string padding # Test string padding
microsecond = map(int, (math.pow(10, x) for x in range(6))) microsecond = map(int, [math.pow(10, x) for x in range(6)])
mm = dd = hh = ii = ss = [1, 10] mm = dd = hh = ii = ss = [1, 10]
for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond): for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond):
stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"] stored = LogEntry(date=datetime.datetime(*values)).to_mongo()['date']
assert ( self.assertTrue(re.match('^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$', stored) is not None)
re.match(r"^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored)
is not None
)
# Test separator # Test separator
stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()[ stored = LogEntry(date_with_dots=datetime.datetime(2014, 1, 1)).to_mongo()['date_with_dots']
"date_with_dots" self.assertTrue(re.match('^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$', stored) is not None)
]
assert (
re.match(r"^\d{4}.\d{2}.\d{2}.\d{2}.\d{2}.\d{2}.\d{6}$", stored) is not None
)
def test_complexdatetime_usage(self): def test_complexdatetime_usage(self):
"""Tests for complex datetime fields - which can handle """Tests for complex datetime fields - which can handle
microseconds without rounding. microseconds without rounding.
""" """
class LogEntry(Document): class LogEntry(Document):
date = ComplexDateTimeField() date = ComplexDateTimeField()
@ -93,61 +84,62 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
log.save() log.save()
log1 = LogEntry.objects.get(date=d1) log1 = LogEntry.objects.get(date=d1)
assert log == log1 self.assertEqual(log, log1)
# create extra 59 log entries for a total of 60 # create extra 59 log entries for a total of 60
for i in range(1951, 2010): for i in range(1951, 2010):
d = datetime.datetime(i, 1, 1, 0, 0, 1, 999) d = datetime.datetime(i, 1, 1, 0, 0, 1, 999)
LogEntry(date=d).save() LogEntry(date=d).save()
assert LogEntry.objects.count() == 60 self.assertEqual(LogEntry.objects.count(), 60)
# Test ordering # Test ordering
logs = LogEntry.objects.order_by("date") logs = LogEntry.objects.order_by("date")
i = 0 i = 0
while i < 59: while i < 59:
assert logs[i].date <= logs[i + 1].date self.assertTrue(logs[i].date <= logs[i + 1].date)
i += 1 i += 1
logs = LogEntry.objects.order_by("-date") logs = LogEntry.objects.order_by("-date")
i = 0 i = 0
while i < 59: while i < 59:
assert logs[i].date >= logs[i + 1].date self.assertTrue(logs[i].date >= logs[i + 1].date)
i += 1 i += 1
# Test searching # Test searching
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1))
assert logs.count() == 30 self.assertEqual(logs.count(), 30)
logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1)) logs = LogEntry.objects.filter(date__lte=datetime.datetime(1980, 1, 1))
assert logs.count() == 30 self.assertEqual(logs.count(), 30)
logs = LogEntry.objects.filter( logs = LogEntry.objects.filter(
date__lte=datetime.datetime(2011, 1, 1), date__lte=datetime.datetime(2011, 1, 1),
date__gte=datetime.datetime(2000, 1, 1), date__gte=datetime.datetime(2000, 1, 1),
) )
assert logs.count() == 10 self.assertEqual(logs.count(), 10)
LogEntry.drop_collection() LogEntry.drop_collection()
# Test microsecond-level ordering/filtering # Test microsecond-level ordering/filtering
for microsecond in (99, 999, 9999, 10000): for microsecond in (99, 999, 9999, 10000):
LogEntry(date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)).save() LogEntry(
date=datetime.datetime(2015, 1, 1, 0, 0, 0, microsecond)
).save()
logs = list(LogEntry.objects.order_by("date")) logs = list(LogEntry.objects.order_by('date'))
for next_idx, log in enumerate(logs[:-1], start=1): for next_idx, log in enumerate(logs[:-1], start=1):
next_log = logs[next_idx] next_log = logs[next_idx]
assert log.date < next_log.date self.assertTrue(log.date < next_log.date)
logs = list(LogEntry.objects.order_by("-date")) logs = list(LogEntry.objects.order_by('-date'))
for next_idx, log in enumerate(logs[:-1], start=1): for next_idx, log in enumerate(logs[:-1], start=1):
next_log = logs[next_idx] next_log = logs[next_idx]
assert log.date > next_log.date self.assertTrue(log.date > next_log.date)
logs = LogEntry.objects.filter( logs = LogEntry.objects.filter(
date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000) date__lte=datetime.datetime(2015, 1, 1, 0, 0, 0, 10000))
) self.assertEqual(logs.count(), 4)
assert logs.count() == 4
def test_no_default_value(self): def test_no_default_value(self):
class Log(Document): class Log(Document):
@ -156,26 +148,25 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
Log.drop_collection() Log.drop_collection()
log = Log() log = Log()
assert log.timestamp is None self.assertIsNone(log.timestamp)
log.save() log.save()
fetched_log = Log.objects.with_id(log.id) fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp is None self.assertIsNone(fetched_log.timestamp)
def test_default_static_value(self): def test_default_static_value(self):
NOW = datetime.datetime.utcnow() NOW = datetime.datetime.utcnow()
class Log(Document): class Log(Document):
timestamp = ComplexDateTimeField(default=NOW) timestamp = ComplexDateTimeField(default=NOW)
Log.drop_collection() Log.drop_collection()
log = Log() log = Log()
assert log.timestamp == NOW self.assertEqual(log.timestamp, NOW)
log.save() log.save()
fetched_log = Log.objects.with_id(log.id) fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp == NOW self.assertEqual(fetched_log.timestamp, NOW)
def test_default_callable(self): def test_default_callable(self):
NOW = datetime.datetime.utcnow() NOW = datetime.datetime.utcnow()
@ -186,23 +177,8 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
Log.drop_collection() Log.drop_collection()
log = Log() log = Log()
assert log.timestamp >= NOW self.assertGreaterEqual(log.timestamp, NOW)
log.save() log.save()
fetched_log = Log.objects.with_id(log.id) fetched_log = Log.objects.with_id(log.id)
assert fetched_log.timestamp >= NOW self.assertGreaterEqual(fetched_log.timestamp, NOW)
def test_setting_bad_value_does_not_raise_unless_validate_is_called(self):
# test regression of #2253
class Log(Document):
timestamp = ComplexDateTimeField()
Log.drop_collection()
log = Log(timestamp="garbage")
with pytest.raises(ValidationError):
log.validate()
with pytest.raises(ValidationError):
log.save()

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import datetime import datetime
import six
import pytest
try: try:
import dateutil import dateutil
@ -8,6 +8,7 @@ except ImportError:
dateutil = None dateutil = None
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -17,47 +18,41 @@ class TestDateField(MongoDBTestCase):
Ensure an exception is raised when trying to Ensure an exception is raised when trying to
cast an empty string to datetime. cast an empty string to datetime.
""" """
class MyDoc(Document): class MyDoc(Document):
dt = DateField() dt = DateField()
md = MyDoc(dt="") md = MyDoc(dt='')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, md.save)
md.save()
def test_date_from_whitespace_string(self): def test_date_from_whitespace_string(self):
""" """
Ensure an exception is raised when trying to Ensure an exception is raised when trying to
cast a whitespace-only string to datetime. cast a whitespace-only string to datetime.
""" """
class MyDoc(Document): class MyDoc(Document):
dt = DateField() dt = DateField()
md = MyDoc(dt=" ") md = MyDoc(dt=' ')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, md.save)
md.save()
def test_default_values_today(self): def test_default_values_today(self):
"""Ensure that default field values are used when creating """Ensure that default field values are used when creating
a document. a document.
""" """
class Person(Document): class Person(Document):
day = DateField(default=datetime.date.today) day = DateField(default=datetime.date.today)
person = Person() person = Person()
person.validate() person.validate()
assert person.day == person.day self.assertEqual(person.day, person.day)
assert person.day == datetime.date.today() self.assertEqual(person.day, datetime.date.today())
assert person._data["day"] == person.day self.assertEqual(person._data['day'], person.day)
def test_date(self): def test_date(self):
"""Tests showing pymongo date fields """Tests showing pymongo date fields
See: http://api.mongodb.org/python/current/api/bson/son.html#dt See: http://api.mongodb.org/python/current/api/bson/son.html#dt
""" """
class LogEntry(Document): class LogEntry(Document):
date = DateField() date = DateField()
@ -68,7 +63,7 @@ class TestDateField(MongoDBTestCase):
log.date = datetime.date.today() log.date = datetime.date.today()
log.save() log.save()
log.reload() log.reload()
assert log.date == datetime.date.today() self.assertEqual(log.date, datetime.date.today())
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999) d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 999)
d2 = datetime.datetime(1970, 1, 1, 0, 0, 1) d2 = datetime.datetime(1970, 1, 1, 0, 0, 1)
@ -76,20 +71,30 @@ class TestDateField(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1.date() self.assertEqual(log.date, d1.date())
assert log.date == d2.date() self.assertEqual(log.date, d2.date())
d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999) d1 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9999)
d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000) d2 = datetime.datetime(1970, 1, 1, 0, 0, 1, 9000)
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date == d1.date() self.assertEqual(log.date, d1.date())
assert log.date == d2.date() self.assertEqual(log.date, d2.date())
if not six.PY3:
# Pre UTC dates microseconds below 1000 are dropped
# This does not seem to be true in PY3
d1 = datetime.datetime(1969, 12, 31, 23, 59, 59, 999)
d2 = datetime.datetime(1969, 12, 31, 23, 59, 59)
log.date = d1
log.save()
log.reload()
self.assertEqual(log.date, d1.date())
self.assertEqual(log.date, d2.date())
def test_regular_usage(self): def test_regular_usage(self):
"""Tests for regular datetime fields""" """Tests for regular datetime fields"""
class LogEntry(Document): class LogEntry(Document):
date = DateField() date = DateField()
@ -101,43 +106,42 @@ class TestDateField(MongoDBTestCase):
log.validate() log.validate()
log.save() log.save()
for query in (d1, d1.isoformat(" ")): for query in (d1, d1.isoformat(' ')):
log1 = LogEntry.objects.get(date=query) log1 = LogEntry.objects.get(date=query)
assert log == log1 self.assertEqual(log, log1)
if dateutil: if dateutil:
log1 = LogEntry.objects.get(date=d1.isoformat("T")) log1 = LogEntry.objects.get(date=d1.isoformat('T'))
assert log == log1 self.assertEqual(log, log1)
# create additional 19 log entries for a total of 20 # create additional 19 log entries for a total of 20
for i in range(1971, 1990): for i in range(1971, 1990):
d = datetime.datetime(i, 1, 1, 0, 0, 1) d = datetime.datetime(i, 1, 1, 0, 0, 1)
LogEntry(date=d).save() LogEntry(date=d).save()
assert LogEntry.objects.count() == 20 self.assertEqual(LogEntry.objects.count(), 20)
# Test ordering # Test ordering
logs = LogEntry.objects.order_by("date") logs = LogEntry.objects.order_by("date")
i = 0 i = 0
while i < 19: while i < 19:
assert logs[i].date <= logs[i + 1].date self.assertTrue(logs[i].date <= logs[i + 1].date)
i += 1 i += 1
logs = LogEntry.objects.order_by("-date") logs = LogEntry.objects.order_by("-date")
i = 0 i = 0
while i < 19: while i < 19:
assert logs[i].date >= logs[i + 1].date self.assertTrue(logs[i].date >= logs[i + 1].date)
i += 1 i += 1
# Test searching # Test searching
logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1)) logs = LogEntry.objects.filter(date__gte=datetime.datetime(1980, 1, 1))
assert logs.count() == 10 self.assertEqual(logs.count(), 10)
def test_validation(self): def test_validation(self):
"""Ensure that invalid values cannot be assigned to datetime """Ensure that invalid values cannot be assigned to datetime
fields. fields.
""" """
class LogEntry(Document): class LogEntry(Document):
time = DateField() time = DateField()
@ -148,16 +152,14 @@ class TestDateField(MongoDBTestCase):
log.time = datetime.date.today() log.time = datetime.date.today()
log.validate() log.validate()
log.time = datetime.datetime.now().isoformat(" ") log.time = datetime.datetime.now().isoformat(' ')
log.validate() log.validate()
if dateutil: if dateutil:
log.time = datetime.datetime.now().isoformat("T") log.time = datetime.datetime.now().isoformat('T')
log.validate() log.validate()
log.time = -1 log.time = -1
with pytest.raises(ValidationError): self.assertRaises(ValidationError, log.validate)
log.validate() log.time = 'ABC'
log.time = "ABC" self.assertRaises(ValidationError, log.validate)
with pytest.raises(ValidationError):
log.validate()

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import datetime as dt import datetime as dt
import six
import pytest
try: try:
import dateutil import dateutil
@ -9,6 +9,7 @@ except ImportError:
from mongoengine import * from mongoengine import *
from mongoengine import connection from mongoengine import connection
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -18,32 +19,27 @@ class TestDateTimeField(MongoDBTestCase):
Ensure an exception is raised when trying to Ensure an exception is raised when trying to
cast an empty string to datetime. cast an empty string to datetime.
""" """
class MyDoc(Document): class MyDoc(Document):
dt = DateTimeField() dt = DateTimeField()
md = MyDoc(dt="") md = MyDoc(dt='')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, md.save)
md.save()
def test_datetime_from_whitespace_string(self): def test_datetime_from_whitespace_string(self):
""" """
Ensure an exception is raised when trying to Ensure an exception is raised when trying to
cast a whitespace-only string to datetime. cast a whitespace-only string to datetime.
""" """
class MyDoc(Document): class MyDoc(Document):
dt = DateTimeField() dt = DateTimeField()
md = MyDoc(dt=" ") md = MyDoc(dt=' ')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, md.save)
md.save()
def test_default_value_utcnow(self): def test_default_value_utcnow(self):
"""Ensure that default field values are used when creating """Ensure that default field values are used when creating
a document. a document.
""" """
class Person(Document): class Person(Document):
created = DateTimeField(default=dt.datetime.utcnow) created = DateTimeField(default=dt.datetime.utcnow)
@ -51,9 +47,9 @@ class TestDateTimeField(MongoDBTestCase):
person = Person() person = Person()
person.validate() person.validate()
person_created_t0 = person.created person_created_t0 = person.created
assert person.created - utcnow < dt.timedelta(seconds=1) self.assertLess(person.created - utcnow, dt.timedelta(seconds=1))
assert person_created_t0 == person.created # make sure it does not change self.assertEqual(person_created_t0, person.created) # make sure it does not change
assert person._data["created"] == person.created self.assertEqual(person._data['created'], person.created)
def test_handling_microseconds(self): def test_handling_microseconds(self):
"""Tests showing pymongo datetime fields handling of microseconds. """Tests showing pymongo datetime fields handling of microseconds.
@ -62,7 +58,6 @@ class TestDateTimeField(MongoDBTestCase):
See: http://api.mongodb.org/python/current/api/bson/son.html#dt See: http://api.mongodb.org/python/current/api/bson/son.html#dt
""" """
class LogEntry(Document): class LogEntry(Document):
date = DateTimeField() date = DateTimeField()
@ -73,7 +68,7 @@ class TestDateTimeField(MongoDBTestCase):
log.date = dt.date.today() log.date = dt.date.today()
log.save() log.save()
log.reload() log.reload()
assert log.date.date() == dt.date.today() self.assertEqual(log.date.date(), dt.date.today())
# Post UTC - microseconds are rounded (down) nearest millisecond and # Post UTC - microseconds are rounded (down) nearest millisecond and
# dropped # dropped
@ -83,8 +78,8 @@ class TestDateTimeField(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date != d1 self.assertNotEqual(log.date, d1)
assert log.date == d2 self.assertEqual(log.date, d2)
# Post UTC - microseconds are rounded (down) nearest millisecond # Post UTC - microseconds are rounded (down) nearest millisecond
d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 9999) d1 = dt.datetime(1970, 1, 1, 0, 0, 1, 9999)
@ -92,12 +87,22 @@ class TestDateTimeField(MongoDBTestCase):
log.date = d1 log.date = d1
log.save() log.save()
log.reload() log.reload()
assert log.date != d1 self.assertNotEqual(log.date, d1)
assert log.date == d2 self.assertEqual(log.date, d2)
if not six.PY3:
# Pre UTC dates microseconds below 1000 are dropped
# This does not seem to be true in PY3
d1 = dt.datetime(1969, 12, 31, 23, 59, 59, 999)
d2 = dt.datetime(1969, 12, 31, 23, 59, 59)
log.date = d1
log.save()
log.reload()
self.assertNotEqual(log.date, d1)
self.assertEqual(log.date, d2)
def test_regular_usage(self): def test_regular_usage(self):
"""Tests for regular datetime fields""" """Tests for regular datetime fields"""
class LogEntry(Document): class LogEntry(Document):
date = DateTimeField() date = DateTimeField()
@ -109,51 +114,51 @@ class TestDateTimeField(MongoDBTestCase):
log.validate() log.validate()
log.save() log.save()
for query in (d1, d1.isoformat(" ")): for query in (d1, d1.isoformat(' ')):
log1 = LogEntry.objects.get(date=query) log1 = LogEntry.objects.get(date=query)
assert log == log1 self.assertEqual(log, log1)
if dateutil: if dateutil:
log1 = LogEntry.objects.get(date=d1.isoformat("T")) log1 = LogEntry.objects.get(date=d1.isoformat('T'))
assert log == log1 self.assertEqual(log, log1)
# create additional 19 log entries for a total of 20 # create additional 19 log entries for a total of 20
for i in range(1971, 1990): for i in range(1971, 1990):
d = dt.datetime(i, 1, 1, 0, 0, 1) d = dt.datetime(i, 1, 1, 0, 0, 1)
LogEntry(date=d).save() LogEntry(date=d).save()
assert LogEntry.objects.count() == 20 self.assertEqual(LogEntry.objects.count(), 20)
# Test ordering # Test ordering
logs = LogEntry.objects.order_by("date") logs = LogEntry.objects.order_by("date")
i = 0 i = 0
while i < 19: while i < 19:
assert logs[i].date <= logs[i + 1].date self.assertTrue(logs[i].date <= logs[i + 1].date)
i += 1 i += 1
logs = LogEntry.objects.order_by("-date") logs = LogEntry.objects.order_by("-date")
i = 0 i = 0
while i < 19: while i < 19:
assert logs[i].date >= logs[i + 1].date self.assertTrue(logs[i].date >= logs[i + 1].date)
i += 1 i += 1
# Test searching # Test searching
logs = LogEntry.objects.filter(date__gte=dt.datetime(1980, 1, 1)) logs = LogEntry.objects.filter(date__gte=dt.datetime(1980, 1, 1))
assert logs.count() == 10 self.assertEqual(logs.count(), 10)
logs = LogEntry.objects.filter(date__lte=dt.datetime(1980, 1, 1)) logs = LogEntry.objects.filter(date__lte=dt.datetime(1980, 1, 1))
assert logs.count() == 10 self.assertEqual(logs.count(), 10)
logs = LogEntry.objects.filter( logs = LogEntry.objects.filter(
date__lte=dt.datetime(1980, 1, 1), date__gte=dt.datetime(1975, 1, 1) date__lte=dt.datetime(1980, 1, 1),
date__gte=dt.datetime(1975, 1, 1),
) )
assert logs.count() == 5 self.assertEqual(logs.count(), 5)
def test_datetime_validation(self): def test_datetime_validation(self):
"""Ensure that invalid values cannot be assigned to datetime """Ensure that invalid values cannot be assigned to datetime
fields. fields.
""" """
class LogEntry(Document): class LogEntry(Document):
time = DateTimeField() time = DateTimeField()
@ -164,51 +169,45 @@ class TestDateTimeField(MongoDBTestCase):
log.time = dt.date.today() log.time = dt.date.today()
log.validate() log.validate()
log.time = dt.datetime.now().isoformat(" ") log.time = dt.datetime.now().isoformat(' ')
log.validate() log.validate()
log.time = "2019-05-16 21:42:57.897847" log.time = '2019-05-16 21:42:57.897847'
log.validate() log.validate()
if dateutil: if dateutil:
log.time = dt.datetime.now().isoformat("T") log.time = dt.datetime.now().isoformat('T')
log.validate() log.validate()
log.time = -1 log.time = -1
with pytest.raises(ValidationError): self.assertRaises(ValidationError, log.validate)
log.validate() log.time = 'ABC'
log.time = "ABC" self.assertRaises(ValidationError, log.validate)
with pytest.raises(ValidationError): log.time = '2019-05-16 21:GARBAGE:12'
log.validate() self.assertRaises(ValidationError, log.validate)
log.time = "2019-05-16 21:GARBAGE:12" log.time = '2019-05-16 21:42:57.GARBAGE'
with pytest.raises(ValidationError): self.assertRaises(ValidationError, log.validate)
log.validate() log.time = '2019-05-16 21:42:57.123.456'
log.time = "2019-05-16 21:42:57.GARBAGE" self.assertRaises(ValidationError, log.validate)
with pytest.raises(ValidationError):
log.validate()
log.time = "2019-05-16 21:42:57.123.456"
with pytest.raises(ValidationError):
log.validate()
def test_parse_datetime_as_str(self): def test_parse_datetime_as_str(self):
class DTDoc(Document): class DTDoc(Document):
date = DateTimeField() date = DateTimeField()
date_str = "2019-03-02 22:26:01" date_str = '2019-03-02 22:26:01'
# make sure that passing a parsable datetime works # make sure that passing a parsable datetime works
dtd = DTDoc() dtd = DTDoc()
dtd.date = date_str dtd.date = date_str
assert isinstance(dtd.date, str) self.assertIsInstance(dtd.date, six.string_types)
dtd.save() dtd.save()
dtd.reload() dtd.reload()
assert isinstance(dtd.date, dt.datetime) self.assertIsInstance(dtd.date, dt.datetime)
assert str(dtd.date) == date_str self.assertEqual(str(dtd.date), date_str)
dtd.date = "January 1st, 9999999999" dtd.date = 'January 1st, 9999999999'
with pytest.raises(ValidationError): self.assertRaises(ValidationError, dtd.validate)
dtd.validate()
class TestDateTimeTzAware(MongoDBTestCase): class TestDateTimeTzAware(MongoDBTestCase):
@ -218,7 +217,7 @@ class TestDateTimeTzAware(MongoDBTestCase):
connection._connections = {} connection._connections = {}
connection._dbs = {} connection._dbs = {}
connect(db="mongoenginetest", tz_aware=True) connect(db='mongoenginetest', tz_aware=True)
class LogEntry(Document): class LogEntry(Document):
time = DateTimeField() time = DateTimeField()
@ -229,4 +228,4 @@ class TestDateTimeTzAware(MongoDBTestCase):
log = LogEntry.objects.first() log = LogEntry.objects.first()
log.time = dt.datetime(2013, 1, 1, 0, 0, 0) log.time = dt.datetime(2013, 1, 1, 0, 0, 0)
assert ["time"] == log._changed_fields self.assertEqual(['time'], log._changed_fields)

View File

@ -1,26 +1,64 @@
# -*- coding: utf-8 -*-
from decimal import Decimal from decimal import Decimal
import pytest from mongoengine import *
from mongoengine import DecimalField, Document, ValidationError
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
class TestDecimalField(MongoDBTestCase): class TestDecimalField(MongoDBTestCase):
def test_validation(self):
"""Ensure that invalid values cannot be assigned to decimal fields.
"""
class Person(Document):
height = DecimalField(min_value=Decimal('0.1'),
max_value=Decimal('3.5'))
Person.drop_collection()
Person(height=Decimal('1.89')).save()
person = Person.objects.first()
self.assertEqual(person.height, Decimal('1.89'))
person.height = '2.0'
person.save()
person.height = 0.01
self.assertRaises(ValidationError, person.validate)
person.height = Decimal('0.01')
self.assertRaises(ValidationError, person.validate)
person.height = Decimal('4.0')
self.assertRaises(ValidationError, person.validate)
person.height = 'something invalid'
self.assertRaises(ValidationError, person.validate)
person_2 = Person(height='something invalid')
self.assertRaises(ValidationError, person_2.validate)
def test_comparison(self):
class Person(Document):
money = DecimalField()
Person.drop_collection()
Person(money=6).save()
Person(money=7).save()
Person(money=8).save()
Person(money=10).save()
self.assertEqual(2, Person.objects(money__gt=Decimal("7")).count())
self.assertEqual(2, Person.objects(money__gt=7).count())
self.assertEqual(2, Person.objects(money__gt="7").count())
self.assertEqual(3, Person.objects(money__gte="7").count())
def test_storage(self): def test_storage(self):
class Person(Document): class Person(Document):
float_value = DecimalField(precision=4) float_value = DecimalField(precision=4)
string_value = DecimalField(precision=4, force_string=True) string_value = DecimalField(precision=4, force_string=True)
Person.drop_collection() Person.drop_collection()
values_to_store = [ values_to_store = [10, 10.1, 10.11, "10.111", Decimal("10.1111"), Decimal("10.11111")]
10,
10.1,
10.11,
"10.111",
Decimal("10.1111"),
Decimal("10.11111"),
]
for store_at_creation in [True, False]: for store_at_creation in [True, False]:
for value in values_to_store: for value in values_to_store:
# to_python is called explicitly if values were sent in the kwargs of __init__ # to_python is called explicitly if values were sent in the kwargs of __init__
@ -34,107 +72,20 @@ class TestDecimalField(MongoDBTestCase):
# How its stored # How its stored
expected = [ expected = [
{"float_value": 10.0, "string_value": "10.0000"}, {'float_value': 10.0, 'string_value': '10.0000'},
{"float_value": 10.1, "string_value": "10.1000"}, {'float_value': 10.1, 'string_value': '10.1000'},
{"float_value": 10.11, "string_value": "10.1100"}, {'float_value': 10.11, 'string_value': '10.1100'},
{"float_value": 10.111, "string_value": "10.1110"}, {'float_value': 10.111, 'string_value': '10.1110'},
{"float_value": 10.1111, "string_value": "10.1111"}, {'float_value': 10.1111, 'string_value': '10.1111'},
{"float_value": 10.1111, "string_value": "10.1111"}, {'float_value': 10.1111, 'string_value': '10.1111'}]
]
expected.extend(expected) expected.extend(expected)
actual = list(Person.objects.exclude("id").as_pymongo()) actual = list(Person.objects.exclude('id').as_pymongo())
assert expected == actual self.assertEqual(expected, actual)
# How it comes out locally # How it comes out locally
expected = [ expected = [Decimal('10.0000'), Decimal('10.1000'), Decimal('10.1100'),
Decimal("10.0000"), Decimal('10.1110'), Decimal('10.1111'), Decimal('10.1111')]
Decimal("10.1000"),
Decimal("10.1100"),
Decimal("10.1110"),
Decimal("10.1111"),
Decimal("10.1111"),
]
expected.extend(expected) expected.extend(expected)
for field_name in ["float_value", "string_value"]: for field_name in ['float_value', 'string_value']:
actual = list(Person.objects().scalar(field_name)) actual = list(Person.objects().scalar(field_name))
assert expected == actual self.assertEqual(expected, actual)
def test_save_none(self):
class Person(Document):
value = DecimalField()
Person.drop_collection()
person = Person(value=None)
assert person.value is None
person.save()
fetched_person = Person.objects.first()
fetched_person.value is None
def test_validation(self):
"""Ensure that invalid values cannot be assigned to decimal fields."""
class Person(Document):
height = DecimalField(min_value=Decimal("0.1"), max_value=Decimal("3.5"))
Person.drop_collection()
Person(height=Decimal("1.89")).save()
person = Person.objects.first()
assert person.height == Decimal("1.89")
person.height = "2.0"
person.save()
person.height = 0.01
with pytest.raises(ValidationError):
person.validate()
person.height = Decimal("0.01")
with pytest.raises(ValidationError):
person.validate()
person.height = Decimal("4.0")
with pytest.raises(ValidationError):
person.validate()
person.height = "something invalid"
with pytest.raises(ValidationError):
person.validate()
person_2 = Person(height="something invalid")
with pytest.raises(ValidationError):
person_2.validate()
def test_comparison(self):
class Person(Document):
money = DecimalField()
Person.drop_collection()
Person(money=6).save()
Person(money=7).save()
Person(money=8).save()
Person(money=10).save()
assert 2 == Person.objects(money__gt=Decimal("7")).count()
assert 2 == Person.objects(money__gt=7).count()
assert 2 == Person.objects(money__gt="7").count()
assert 3 == Person.objects(money__gte="7").count()
def test_precision_0(self):
"""prevent regression of a bug that was raising an exception when using precision=0"""
class TestDoc(Document):
d = DecimalField(precision=0)
TestDoc.drop_collection()
td = TestDoc(d=Decimal("12.00032678131263"))
assert td.d == Decimal("12")
def test_precision_negative_raise(self):
"""prevent regression of a bug that was raising an exception when using precision=0"""
with pytest.raises(
ValidationError, match="precision must be a positive integer"
):
class TestDoc(Document):
dneg = DecimalField(precision=-1)

View File

@ -1,124 +1,100 @@
import pytest # -*- coding: utf-8 -*-
from bson import InvalidDocument
from mongoengine import * from mongoengine import *
from mongoengine.base import BaseDict from mongoengine.base import BaseDict
from mongoengine.mongodb_support import (
MONGODB_36,
get_mongodb_version,
)
from tests.utils import MongoDBTestCase, get_as_pymongo from tests.utils import MongoDBTestCase, get_as_pymongo
class TestDictField(MongoDBTestCase): class TestDictField(MongoDBTestCase):
def test_storage(self): def test_storage(self):
class BlogPost(Document): class BlogPost(Document):
info = DictField() info = DictField()
BlogPost.drop_collection() BlogPost.drop_collection()
info = {"testkey": "testvalue"} info = {'testkey': 'testvalue'}
post = BlogPost(info=info).save() post = BlogPost(info=info).save()
assert get_as_pymongo(post) == {"_id": post.id, "info": info} self.assertEqual(
get_as_pymongo(post),
def test_validate_invalid_type(self): {
class BlogPost(Document): '_id': post.id,
info = DictField() 'info': info
}
BlogPost.drop_collection() )
invalid_infos = ["my post", ["test", "test"], {1: "test"}]
for invalid_info in invalid_infos:
with pytest.raises(ValidationError):
BlogPost(info=invalid_info).validate()
def test_keys_with_dots_or_dollars(self):
class BlogPost(Document):
info = DictField()
BlogPost.drop_collection()
post = BlogPost()
post.info = {"$title": "test"}
with pytest.raises(ValidationError):
post.validate()
post.info = {"nested": {"$title": "test"}}
with pytest.raises(ValidationError):
post.validate()
post.info = {"$title.test": "test"}
with pytest.raises(ValidationError):
post.validate()
post.info = {"nested": {"the.title": "test"}}
if get_mongodb_version() < MONGODB_36:
# MongoDB < 3.6 rejects dots
# To avoid checking the mongodb version from the DictField class
# we rely on MongoDB to reject the data during the save
post.validate()
with pytest.raises(InvalidDocument):
post.save()
else:
post.validate()
post.info = {"dollar_and_dot": {"te$st.test": "test"}}
if get_mongodb_version() < MONGODB_36:
post.validate()
with pytest.raises(InvalidDocument):
post.save()
else:
post.validate()
def test_general_things(self): def test_general_things(self):
"""Ensure that dict types work as expected.""" """Ensure that dict types work as expected."""
class BlogPost(Document): class BlogPost(Document):
info = DictField() info = DictField()
BlogPost.drop_collection() BlogPost.drop_collection()
post = BlogPost(info={"title": "test"}) post = BlogPost()
post.info = 'my post'
self.assertRaises(ValidationError, post.validate)
post.info = ['test', 'test']
self.assertRaises(ValidationError, post.validate)
post.info = {'$title': 'test'}
self.assertRaises(ValidationError, post.validate)
post.info = {'nested': {'$title': 'test'}}
self.assertRaises(ValidationError, post.validate)
post.info = {'the.title': 'test'}
self.assertRaises(ValidationError, post.validate)
post.info = {'nested': {'the.title': 'test'}}
self.assertRaises(ValidationError, post.validate)
post.info = {1: 'test'}
self.assertRaises(ValidationError, post.validate)
post.info = {'title': 'test'}
post.save() post.save()
post = BlogPost() post = BlogPost()
post.info = {"title": "dollar_sign", "details": {"te$t": "test"}} post.info = {'title': 'dollar_sign', 'details': {'te$t': 'test'}}
post.save() post.save()
post = BlogPost() post = BlogPost()
post.info = {"details": {"test": "test"}} post.info = {'details': {'test': 'test'}}
post.save() post.save()
post = BlogPost() post = BlogPost()
post.info = {"details": {"test": 3}} post.info = {'details': {'test': 3}}
post.save() post.save()
assert BlogPost.objects.count() == 4 self.assertEqual(BlogPost.objects.count(), 4)
assert BlogPost.objects.filter(info__title__exact="test").count() == 1 self.assertEqual(
assert BlogPost.objects.filter(info__details__test__exact="test").count() == 1 BlogPost.objects.filter(info__title__exact='test').count(), 1)
self.assertEqual(
BlogPost.objects.filter(info__details__test__exact='test').count(), 1)
post = BlogPost.objects.filter(info__title__exact="dollar_sign").first() post = BlogPost.objects.filter(info__title__exact='dollar_sign').first()
assert "te$t" in post["info"]["details"] self.assertIn('te$t', post['info']['details'])
# Confirm handles non strings or non existing keys # Confirm handles non strings or non existing keys
assert BlogPost.objects.filter(info__details__test__exact=5).count() == 0 self.assertEqual(
assert BlogPost.objects.filter(info__made_up__test__exact="test").count() == 0 BlogPost.objects.filter(info__details__test__exact=5).count(), 0)
self.assertEqual(
BlogPost.objects.filter(info__made_up__test__exact='test').count(), 0)
post = BlogPost.objects.create(info={"title": "original"}) post = BlogPost.objects.create(info={'title': 'original'})
post.info.update({"title": "updated"}) post.info.update({'title': 'updated'})
post.save() post.save()
post.reload() post.reload()
assert "updated" == post.info["title"] self.assertEqual('updated', post.info['title'])
post.info.setdefault("authors", []) post.info.setdefault('authors', [])
post.save() post.save()
post.reload() post.reload()
assert post.info["authors"] == [] self.assertEqual([], post.info['authors'])
def test_dictfield_dump_document(self): def test_dictfield_dump_document(self):
"""Ensure a DictField can handle another document's dump.""" """Ensure a DictField can handle another document's dump."""
class Doc(Document): class Doc(Document):
field = DictField() field = DictField()
@ -130,60 +106,51 @@ class TestDictField(MongoDBTestCase):
id = IntField(primary_key=True, default=1) id = IntField(primary_key=True, default=1)
recursive = DictField() recursive = DictField()
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
class ToEmbedChild(ToEmbedParent): class ToEmbedChild(ToEmbedParent):
pass pass
to_embed_recursive = ToEmbed(id=1).save() to_embed_recursive = ToEmbed(id=1).save()
to_embed = ToEmbed( to_embed = ToEmbed(
id=2, recursive=to_embed_recursive.to_mongo().to_dict() id=2, recursive=to_embed_recursive.to_mongo().to_dict()).save()
).save()
doc = Doc(field=to_embed.to_mongo().to_dict()) doc = Doc(field=to_embed.to_mongo().to_dict())
doc.save() doc.save()
assert isinstance(doc.field, dict) self.assertIsInstance(doc.field, dict)
assert doc.field == {"_id": 2, "recursive": {"_id": 1, "recursive": {}}} self.assertEqual(doc.field, {'_id': 2, 'recursive': {'_id': 1, 'recursive': {}}})
# Same thing with a Document with a _cls field # Same thing with a Document with a _cls field
to_embed_recursive = ToEmbedChild(id=1).save() to_embed_recursive = ToEmbedChild(id=1).save()
to_embed_child = ToEmbedChild( to_embed_child = ToEmbedChild(
id=2, recursive=to_embed_recursive.to_mongo().to_dict() id=2, recursive=to_embed_recursive.to_mongo().to_dict()).save()
).save()
doc = Doc(field=to_embed_child.to_mongo().to_dict()) doc = Doc(field=to_embed_child.to_mongo().to_dict())
doc.save() doc.save()
assert isinstance(doc.field, dict) self.assertIsInstance(doc.field, dict)
expected = { expected = {
"_id": 2, '_id': 2, '_cls': 'ToEmbedParent.ToEmbedChild',
"_cls": "ToEmbedParent.ToEmbedChild", 'recursive': {'_id': 1, '_cls': 'ToEmbedParent.ToEmbedChild', 'recursive': {}}
"recursive": {
"_id": 1,
"_cls": "ToEmbedParent.ToEmbedChild",
"recursive": {},
},
} }
assert doc.field == expected self.assertEqual(doc.field, expected)
def test_dictfield_strict(self): def test_dictfield_strict(self):
"""Ensure that dict field handles validation if provided a strict field type.""" """Ensure that dict field handles validation if provided a strict field type."""
class Simple(Document): class Simple(Document):
mapping = DictField(field=IntField()) mapping = DictField(field=IntField())
Simple.drop_collection() Simple.drop_collection()
e = Simple() e = Simple()
e.mapping["someint"] = 1 e.mapping['someint'] = 1
e.save() e.save()
# try creating an invalid mapping # try creating an invalid mapping
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
e.mapping["somestring"] = "abc" e.mapping['somestring'] = "abc"
e.save() e.save()
def test_dictfield_complex(self): def test_dictfield_complex(self):
"""Ensure that the dict field can handle the complex types.""" """Ensure that the dict field can handle the complex types."""
class SettingBase(EmbeddedDocument): class SettingBase(EmbeddedDocument):
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
class StringSetting(SettingBase): class StringSetting(SettingBase):
value = StringField() value = StringField()
@ -197,76 +164,73 @@ class TestDictField(MongoDBTestCase):
Simple.drop_collection() Simple.drop_collection()
e = Simple() e = Simple()
e.mapping["somestring"] = StringSetting(value="foo") e.mapping['somestring'] = StringSetting(value='foo')
e.mapping["someint"] = IntegerSetting(value=42) e.mapping['someint'] = IntegerSetting(value=42)
e.mapping["nested_dict"] = { e.mapping['nested_dict'] = {'number': 1, 'string': 'Hi!',
"number": 1, 'float': 1.001,
"string": "Hi!", 'complex': IntegerSetting(value=42),
"float": 1.001, 'list': [IntegerSetting(value=42),
"complex": IntegerSetting(value=42), StringSetting(value='foo')]}
"list": [IntegerSetting(value=42), StringSetting(value="foo")],
}
e.save() e.save()
e2 = Simple.objects.get(id=e.id) e2 = Simple.objects.get(id=e.id)
assert isinstance(e2.mapping["somestring"], StringSetting) self.assertIsInstance(e2.mapping['somestring'], StringSetting)
assert isinstance(e2.mapping["someint"], IntegerSetting) self.assertIsInstance(e2.mapping['someint'], IntegerSetting)
# Test querying # Test querying
assert Simple.objects.filter(mapping__someint__value=42).count() == 1 self.assertEqual(
assert Simple.objects.filter(mapping__nested_dict__number=1).count() == 1 Simple.objects.filter(mapping__someint__value=42).count(), 1)
assert ( self.assertEqual(
Simple.objects.filter(mapping__nested_dict__complex__value=42).count() == 1 Simple.objects.filter(mapping__nested_dict__number=1).count(), 1)
) self.assertEqual(
assert ( Simple.objects.filter(mapping__nested_dict__complex__value=42).count(), 1)
Simple.objects.filter(mapping__nested_dict__list__0__value=42).count() == 1 self.assertEqual(
) Simple.objects.filter(mapping__nested_dict__list__0__value=42).count(), 1)
assert ( self.assertEqual(
Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count() Simple.objects.filter(mapping__nested_dict__list__1__value='foo').count(), 1)
== 1
)
# Confirm can update # Confirm can update
Simple.objects().update(set__mapping={"someint": IntegerSetting(value=10)})
Simple.objects().update( Simple.objects().update(
set__mapping__nested_dict__list__1=StringSetting(value="Boo") set__mapping={"someint": IntegerSetting(value=10)})
) Simple.objects().update(
assert ( set__mapping__nested_dict__list__1=StringSetting(value='Boo'))
Simple.objects.filter(mapping__nested_dict__list__1__value="foo").count() self.assertEqual(
== 0 Simple.objects.filter(mapping__nested_dict__list__1__value='foo').count(), 0)
) self.assertEqual(
assert ( Simple.objects.filter(mapping__nested_dict__list__1__value='Boo').count(), 1)
Simple.objects.filter(mapping__nested_dict__list__1__value="Boo").count()
== 1
)
def test_push_dict(self): def test_push_dict(self):
class MyModel(Document): class MyModel(Document):
events = ListField(DictField()) events = ListField(DictField())
doc = MyModel(events=[{"a": 1}]).save() doc = MyModel(events=[{'a': 1}]).save()
raw_doc = get_as_pymongo(doc) raw_doc = get_as_pymongo(doc)
expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}]} expected_raw_doc = {
assert raw_doc == expected_raw_doc '_id': doc.id,
'events': [{'a': 1}]
}
self.assertEqual(raw_doc, expected_raw_doc)
MyModel.objects(id=doc.id).update(push__events={}) MyModel.objects(id=doc.id).update(push__events={})
raw_doc = get_as_pymongo(doc) raw_doc = get_as_pymongo(doc)
expected_raw_doc = {"_id": doc.id, "events": [{"a": 1}, {}]} expected_raw_doc = {
assert raw_doc == expected_raw_doc '_id': doc.id,
'events': [{'a': 1}, {}]
}
self.assertEqual(raw_doc, expected_raw_doc)
def test_ensure_unique_default_instances(self): def test_ensure_unique_default_instances(self):
"""Ensure that every field has it's own unique default instance.""" """Ensure that every field has it's own unique default instance."""
class D(Document): class D(Document):
data = DictField() data = DictField()
data2 = DictField(default=lambda: {}) data2 = DictField(default=lambda: {})
d1 = D() d1 = D()
d1.data["foo"] = "bar" d1.data['foo'] = 'bar'
d1.data2["foo"] = "bar" d1.data2['foo'] = 'bar'
d2 = D() d2 = D()
assert d2.data == {} self.assertEqual(d2.data, {})
assert d2.data2 == {} self.assertEqual(d2.data2, {})
def test_dict_field_invalid_dict_value(self): def test_dict_field_invalid_dict_value(self):
class DictFieldTest(Document): class DictFieldTest(Document):
@ -276,13 +240,11 @@ class TestDictField(MongoDBTestCase):
test = DictFieldTest(dictionary=None) test = DictFieldTest(dictionary=None)
test.dictionary # Just access to test getter test.dictionary # Just access to test getter
with pytest.raises(ValidationError): self.assertRaises(ValidationError, test.validate)
test.validate()
test = DictFieldTest(dictionary=False) test = DictFieldTest(dictionary=False)
test.dictionary # Just access to test getter test.dictionary # Just access to test getter
with pytest.raises(ValidationError): self.assertRaises(ValidationError, test.validate)
test.validate()
def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self): def test_dict_field_raises_validation_error_if_wrongly_assign_embedded_doc(self):
class DictFieldTest(Document): class DictFieldTest(Document):
@ -293,34 +255,31 @@ class TestDictField(MongoDBTestCase):
class Embedded(EmbeddedDocument): class Embedded(EmbeddedDocument):
name = StringField() name = StringField()
embed = Embedded(name="garbage") embed = Embedded(name='garbage')
doc = DictFieldTest(dictionary=embed) doc = DictFieldTest(dictionary=embed)
with pytest.raises(ValidationError) as exc_info: with self.assertRaises(ValidationError) as ctx_err:
doc.validate() doc.validate()
self.assertIn("'dictionary'", str(ctx_err.exception))
error_msg = str(exc_info.value) self.assertIn('Only dictionaries may be used in a DictField', str(ctx_err.exception))
assert "'dictionary'" in error_msg
assert "Only dictionaries may be used in a DictField" in error_msg
def test_atomic_update_dict_field(self): def test_atomic_update_dict_field(self):
"""Ensure that the entire DictField can be atomically updated.""" """Ensure that the entire DictField can be atomically updated."""
class Simple(Document): class Simple(Document):
mapping = DictField(field=ListField(IntField(required=True))) mapping = DictField(field=ListField(IntField(required=True)))
Simple.drop_collection() Simple.drop_collection()
e = Simple() e = Simple()
e.mapping["someints"] = [1, 2] e.mapping['someints'] = [1, 2]
e.save() e.save()
e.update(set__mapping={"ints": [3, 4]}) e.update(set__mapping={"ints": [3, 4]})
e.reload() e.reload()
assert isinstance(e.mapping, BaseDict) self.assertEqual(BaseDict, type(e.mapping))
assert {"ints": [3, 4]} == e.mapping self.assertEqual({"ints": [3, 4]}, e.mapping)
# try creating an invalid mapping # try creating an invalid mapping
with pytest.raises(ValueError): with self.assertRaises(ValueError):
e.update(set__mapping={"somestrings": ["foo", "bar"]}) e.update(set__mapping={"somestrings": ["foo", "bar", ]})
def test_dictfield_with_referencefield_complex_nesting_cases(self): def test_dictfield_with_referencefield_complex_nesting_cases(self):
"""Ensure complex nesting inside DictField handles dereferencing of ReferenceField(dbref=True | False)""" """Ensure complex nesting inside DictField handles dereferencing of ReferenceField(dbref=True | False)"""
@ -337,33 +296,29 @@ class TestDictField(MongoDBTestCase):
mapping5 = DictField(DictField(field=ReferenceField(Doc, dbref=False))) mapping5 = DictField(DictField(field=ReferenceField(Doc, dbref=False)))
mapping6 = DictField(ListField(DictField(ReferenceField(Doc, dbref=True)))) mapping6 = DictField(ListField(DictField(ReferenceField(Doc, dbref=True))))
mapping7 = DictField(ListField(DictField(ReferenceField(Doc, dbref=False)))) mapping7 = DictField(ListField(DictField(ReferenceField(Doc, dbref=False))))
mapping8 = DictField( mapping8 = DictField(ListField(DictField(ListField(ReferenceField(Doc, dbref=True)))))
ListField(DictField(ListField(ReferenceField(Doc, dbref=True)))) mapping9 = DictField(ListField(DictField(ListField(ReferenceField(Doc, dbref=False)))))
)
mapping9 = DictField(
ListField(DictField(ListField(ReferenceField(Doc, dbref=False))))
)
Doc.drop_collection() Doc.drop_collection()
Simple.drop_collection() Simple.drop_collection()
d = Doc(s="aa").save() d = Doc(s='aa').save()
e = Simple() e = Simple()
e.mapping0["someint"] = e.mapping1["someint"] = d e.mapping0['someint'] = e.mapping1['someint'] = d
e.mapping2["someint"] = e.mapping3["someint"] = [d] e.mapping2['someint'] = e.mapping3['someint'] = [d]
e.mapping4["someint"] = e.mapping5["someint"] = {"d": d} e.mapping4['someint'] = e.mapping5['someint'] = {'d': d}
e.mapping6["someint"] = e.mapping7["someint"] = [{"d": d}] e.mapping6['someint'] = e.mapping7['someint'] = [{'d': d}]
e.mapping8["someint"] = e.mapping9["someint"] = [{"d": [d]}] e.mapping8['someint'] = e.mapping9['someint'] = [{'d': [d]}]
e.save() e.save()
s = Simple.objects.first() s = Simple.objects.first()
assert isinstance(s.mapping0["someint"], Doc) self.assertIsInstance(s.mapping0['someint'], Doc)
assert isinstance(s.mapping1["someint"], Doc) self.assertIsInstance(s.mapping1['someint'], Doc)
assert isinstance(s.mapping2["someint"][0], Doc) self.assertIsInstance(s.mapping2['someint'][0], Doc)
assert isinstance(s.mapping3["someint"][0], Doc) self.assertIsInstance(s.mapping3['someint'][0], Doc)
assert isinstance(s.mapping4["someint"]["d"], Doc) self.assertIsInstance(s.mapping4['someint']['d'], Doc)
assert isinstance(s.mapping5["someint"]["d"], Doc) self.assertIsInstance(s.mapping5['someint']['d'], Doc)
assert isinstance(s.mapping6["someint"][0]["d"], Doc) self.assertIsInstance(s.mapping6['someint'][0]['d'], Doc)
assert isinstance(s.mapping7["someint"][0]["d"], Doc) self.assertIsInstance(s.mapping7['someint'][0]['d'], Doc)
assert isinstance(s.mapping8["someint"][0]["d"][0], Doc) self.assertIsInstance(s.mapping8['someint'][0]['d'][0], Doc)
assert isinstance(s.mapping9["someint"][0]["d"][0], Doc) self.assertIsInstance(s.mapping9['someint'][0]['d'][0], Doc)

View File

@ -1,6 +1,9 @@
import pytest # -*- coding: utf-8 -*-
import sys
from unittest import SkipTest
from mongoengine import *
from mongoengine import Document, EmailField, ValidationError
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -9,52 +12,52 @@ class TestEmailField(MongoDBTestCase):
class User(Document): class User(Document):
email = EmailField() email = EmailField()
user = User(email="ross@example.com") user = User(email='ross@example.com')
user.validate() user.validate()
user = User(email="ross@example.co.uk") user = User(email='ross@example.co.uk')
user.validate() user.validate()
user = User( user = User(email=('Kofq@rhom0e4klgauOhpbpNdogawnyIKvQS0wk2mjqrgGQ5S'
email=("Kofq@rhom0e4klgauOhpbpNdogawnyIKvQS0wk2mjqrgGQ5SaJIazqqWkm7.net") 'aJIazqqWkm7.net'))
)
user.validate() user.validate()
user = User(email="new-tld@example.technology") user = User(email='new-tld@example.technology')
user.validate() user.validate()
user = User(email="ross@example.com.") user = User(email='ross@example.com.')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# unicode domain # unicode domain
user = User(email="user@пример.рф") user = User(email=u'user@пример.рф')
user.validate() user.validate()
# invalid unicode domain # invalid unicode domain
user = User(email="user@пример") user = User(email=u'user@пример')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# invalid data type # invalid data type
user = User(email=123) user = User(email=123)
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
def test_email_field_unicode_user(self): def test_email_field_unicode_user(self):
# Don't run this test on pypy3, which doesn't support unicode regex:
# https://bitbucket.org/pypy/pypy/issues/1821/regular-expression-doesnt-find-unicode
if sys.version_info[:2] == (3, 2):
raise SkipTest('unicode email addresses are not supported on PyPy 3')
class User(Document): class User(Document):
email = EmailField() email = EmailField()
# unicode user shouldn't validate by default... # unicode user shouldn't validate by default...
user = User(email="Dörte@Sörensen.example.com") user = User(email=u'Dörte@Sörensen.example.com')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# ...but it should be fine with allow_utf8_user set to True # ...but it should be fine with allow_utf8_user set to True
class User(Document): class User(Document):
email = EmailField(allow_utf8_user=True) email = EmailField(allow_utf8_user=True)
user = User(email="Dörte@Sörensen.example.com") user = User(email=u'Dörte@Sörensen.example.com')
user.validate() user.validate()
def test_email_field_domain_whitelist(self): def test_email_field_domain_whitelist(self):
@ -62,48 +65,43 @@ class TestEmailField(MongoDBTestCase):
email = EmailField() email = EmailField()
# localhost domain shouldn't validate by default... # localhost domain shouldn't validate by default...
user = User(email="me@localhost") user = User(email='me@localhost')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# ...but it should be fine if it's whitelisted # ...but it should be fine if it's whitelisted
class User(Document): class User(Document):
email = EmailField(domain_whitelist=["localhost"]) email = EmailField(domain_whitelist=['localhost'])
user = User(email="me@localhost") user = User(email='me@localhost')
user.validate() user.validate()
def test_email_domain_validation_fails_if_invalid_idn(self): def test_email_domain_validation_fails_if_invalid_idn(self):
class User(Document): class User(Document):
email = EmailField() email = EmailField()
invalid_idn = ".google.com" invalid_idn = '.google.com'
user = User(email="me@%s" % invalid_idn) user = User(email='me@%s' % invalid_idn)
with self.assertRaises(ValidationError) as ctx_err:
with pytest.raises(ValidationError) as exc_info:
user.validate() user.validate()
assert "domain failed IDN encoding" in str(exc_info.value) self.assertIn("domain failed IDN encoding", str(ctx_err.exception))
def test_email_field_ip_domain(self): def test_email_field_ip_domain(self):
class User(Document): class User(Document):
email = EmailField() email = EmailField()
valid_ipv4 = "email@[127.0.0.1]" valid_ipv4 = 'email@[127.0.0.1]'
valid_ipv6 = "email@[2001:dB8::1]" valid_ipv6 = 'email@[2001:dB8::1]'
invalid_ip = "email@[324.0.0.1]" invalid_ip = 'email@[324.0.0.1]'
# IP address as a domain shouldn't validate by default... # IP address as a domain shouldn't validate by default...
user = User(email=valid_ipv4) user = User(email=valid_ipv4)
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
user = User(email=valid_ipv6) user = User(email=valid_ipv6)
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
user = User(email=invalid_ip) user = User(email=invalid_ip)
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# ...but it should be fine with allow_ip_domain set to True # ...but it should be fine with allow_ip_domain set to True
class User(Document): class User(Document):
@ -117,18 +115,16 @@ class TestEmailField(MongoDBTestCase):
# invalid IP should still fail validation # invalid IP should still fail validation
user = User(email=invalid_ip) user = User(email=invalid_ip)
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
def test_email_field_honors_regex(self): def test_email_field_honors_regex(self):
class User(Document): class User(Document):
email = EmailField(regex=r"\w+@example.com") email = EmailField(regex=r'\w+@example.com')
# Fails regex validation # Fails regex validation
user = User(email="me@foo.com") user = User(email='me@foo.com')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, user.validate)
user.validate()
# Passes regex validation # Passes regex validation
user = User(email="me@example.com") user = User(email='me@example.com')
assert user.validate() is None self.assertIsNone(user.validate())

View File

@ -1,21 +1,8 @@
from copy import deepcopy # -*- coding: utf-8 -*-
from mongoengine import Document, StringField, ValidationError, EmbeddedDocument, EmbeddedDocumentField, \
InvalidQueryError, LookUpError, IntField, GenericEmbeddedDocumentField, ListField, EmbeddedDocumentListField, \
ReferenceField
import pytest
from bson import ObjectId
from mongoengine import (
Document,
EmbeddedDocument,
EmbeddedDocumentField,
GenericEmbeddedDocumentField,
IntField,
InvalidQueryError,
ListField,
LookUpError,
MapField,
StringField,
ValidationError,
)
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -25,41 +12,37 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
name = StringField() name = StringField()
field = EmbeddedDocumentField(MyDoc) field = EmbeddedDocumentField(MyDoc)
assert field.document_type_obj == MyDoc self.assertEqual(field.document_type_obj, MyDoc)
field2 = EmbeddedDocumentField("MyDoc") field2 = EmbeddedDocumentField('MyDoc')
assert field2.document_type_obj == "MyDoc" self.assertEqual(field2.document_type_obj, 'MyDoc')
def test___init___throw_error_if_document_type_is_not_EmbeddedDocument(self): def test___init___throw_error_if_document_type_is_not_EmbeddedDocument(self):
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
EmbeddedDocumentField(dict) EmbeddedDocumentField(dict)
def test_document_type_throw_error_if_not_EmbeddedDocument_subclass(self): def test_document_type_throw_error_if_not_EmbeddedDocument_subclass(self):
class MyDoc(Document): class MyDoc(Document):
name = StringField() name = StringField()
emb = EmbeddedDocumentField("MyDoc") emb = EmbeddedDocumentField('MyDoc')
with pytest.raises(ValidationError) as exc_info: with self.assertRaises(ValidationError) as ctx:
emb.document_type emb.document_type
assert ( self.assertIn('Invalid embedded document class provided to an EmbeddedDocumentField', str(ctx.exception))
"Invalid embedded document class provided to an EmbeddedDocumentField"
in str(exc_info.value)
)
def test_embedded_document_field_only_allow_subclasses_of_embedded_document(self): def test_embedded_document_field_only_allow_subclasses_of_embedded_document(self):
# Relates to #1661 # Relates to #1661
class MyDoc(Document): class MyDoc(Document):
name = StringField() name = StringField()
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
class MyFailingDoc(Document): class MyFailingDoc(Document):
emb = EmbeddedDocumentField(MyDoc) emb = EmbeddedDocumentField(MyDoc)
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
class MyFailingdoc2(Document): class MyFailingdoc2(Document):
emb = EmbeddedDocumentField("MyDoc") emb = EmbeddedDocumentField('MyDoc')
def test_query_embedded_document_attribute(self): def test_query_embedded_document_attribute(self):
class AdminSettings(EmbeddedDocument): class AdminSettings(EmbeddedDocument):
@ -72,31 +55,34 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
p = Person(settings=AdminSettings(foo1="bar1", foo2="bar2"), name="John").save() p = Person(
settings=AdminSettings(foo1='bar1', foo2='bar2'),
name='John',
).save()
# Test non exiting attribute # Test non exiting attribute
with pytest.raises(InvalidQueryError) as exc_info: with self.assertRaises(InvalidQueryError) as ctx_err:
Person.objects(settings__notexist="bar").first() Person.objects(settings__notexist='bar').first()
assert str(exc_info.value) == 'Cannot resolve field "notexist"' self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
with pytest.raises(LookUpError): with self.assertRaises(LookUpError):
Person.objects.only("settings.notexist") Person.objects.only('settings.notexist')
# Test existing attribute # Test existing attribute
assert Person.objects(settings__foo1="bar1").first().id == p.id self.assertEqual(Person.objects(settings__foo1='bar1').first().id, p.id)
only_p = Person.objects.only("settings.foo1").first() only_p = Person.objects.only('settings.foo1').first()
assert only_p.settings.foo1 == p.settings.foo1 self.assertEqual(only_p.settings.foo1, p.settings.foo1)
assert only_p.settings.foo2 is None self.assertIsNone(only_p.settings.foo2)
assert only_p.name is None self.assertIsNone(only_p.name)
exclude_p = Person.objects.exclude("settings.foo1").first() exclude_p = Person.objects.exclude('settings.foo1').first()
assert exclude_p.settings.foo1 is None self.assertIsNone(exclude_p.settings.foo1)
assert exclude_p.settings.foo2 == p.settings.foo2 self.assertEqual(exclude_p.settings.foo2, p.settings.foo2)
assert exclude_p.name == p.name self.assertEqual(exclude_p.name, p.name)
def test_query_embedded_document_attribute_with_inheritance(self): def test_query_embedded_document_attribute_with_inheritance(self):
class BaseSettings(EmbeddedDocument): class BaseSettings(EmbeddedDocument):
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
base_foo = StringField() base_foo = StringField()
class AdminSettings(BaseSettings): class AdminSettings(BaseSettings):
@ -107,26 +93,26 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
p = Person(settings=AdminSettings(base_foo="basefoo", sub_foo="subfoo")) p = Person(settings=AdminSettings(base_foo='basefoo', sub_foo='subfoo'))
p.save() p.save()
# Test non exiting attribute # Test non exiting attribute
with pytest.raises(InvalidQueryError) as exc_info: with self.assertRaises(InvalidQueryError) as ctx_err:
assert Person.objects(settings__notexist="bar").first().id == p.id self.assertEqual(Person.objects(settings__notexist='bar').first().id, p.id)
assert str(exc_info.value) == 'Cannot resolve field "notexist"' self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
# Test existing attribute # Test existing attribute
assert Person.objects(settings__base_foo="basefoo").first().id == p.id self.assertEqual(Person.objects(settings__base_foo='basefoo').first().id, p.id)
assert Person.objects(settings__sub_foo="subfoo").first().id == p.id self.assertEqual(Person.objects(settings__sub_foo='subfoo').first().id, p.id)
only_p = Person.objects.only("settings.base_foo", "settings._cls").first() only_p = Person.objects.only('settings.base_foo', 'settings._cls').first()
assert only_p.settings.base_foo == "basefoo" self.assertEqual(only_p.settings.base_foo, 'basefoo')
assert only_p.settings.sub_foo is None self.assertIsNone(only_p.settings.sub_foo)
def test_query_list_embedded_document_with_inheritance(self): def test_query_list_embedded_document_with_inheritance(self):
class Post(EmbeddedDocument): class Post(EmbeddedDocument):
title = StringField(max_length=120, required=True) title = StringField(max_length=120, required=True)
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
class TextPost(Post): class TextPost(Post):
content = StringField() content = StringField()
@ -137,21 +123,22 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
class Record(Document): class Record(Document):
posts = ListField(EmbeddedDocumentField(Post)) posts = ListField(EmbeddedDocumentField(Post))
record_movie = Record(posts=[MoviePost(author="John", title="foo")]).save() record_movie = Record(posts=[MoviePost(author='John', title='foo')]).save()
record_text = Record(posts=[TextPost(content="a", title="foo")]).save() record_text = Record(posts=[TextPost(content='a', title='foo')]).save()
records = list(Record.objects(posts__author=record_movie.posts[0].author)) records = list(Record.objects(posts__author=record_movie.posts[0].author))
assert len(records) == 1 self.assertEqual(len(records), 1)
assert records[0].id == record_movie.id self.assertEqual(records[0].id, record_movie.id)
records = list(Record.objects(posts__content=record_text.posts[0].content)) records = list(Record.objects(posts__content=record_text.posts[0].content))
assert len(records) == 1 self.assertEqual(len(records), 1)
assert records[0].id == record_text.id self.assertEqual(records[0].id, record_text.id)
assert Record.objects(posts__title="foo").count() == 2 self.assertEqual(Record.objects(posts__title='foo').count(), 2)
class TestGenericEmbeddedDocumentField(MongoDBTestCase): class TestGenericEmbeddedDocumentField(MongoDBTestCase):
def test_generic_embedded_document(self): def test_generic_embedded_document(self):
class Car(EmbeddedDocument): class Car(EmbeddedDocument):
name = StringField() name = StringField()
@ -166,22 +153,21 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
person = Person(name="Test User") person = Person(name='Test User')
person.like = Car(name="Fiat") person.like = Car(name='Fiat')
person.save() person.save()
person = Person.objects.first() person = Person.objects.first()
assert isinstance(person.like, Car) self.assertIsInstance(person.like, Car)
person.like = Dish(food="arroz", number=15) person.like = Dish(food="arroz", number=15)
person.save() person.save()
person = Person.objects.first() person = Person.objects.first()
assert isinstance(person.like, Dish) self.assertIsInstance(person.like, Dish)
def test_generic_embedded_document_choices(self): def test_generic_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices.""" """Ensure you can limit GenericEmbeddedDocument choices."""
class Car(EmbeddedDocument): class Car(EmbeddedDocument):
name = StringField() name = StringField()
@ -195,22 +181,20 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
person = Person(name="Test User") person = Person(name='Test User')
person.like = Car(name="Fiat") person.like = Car(name='Fiat')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person.like = Dish(food="arroz", number=15) person.like = Dish(food="arroz", number=15)
person.save() person.save()
person = Person.objects.first() person = Person.objects.first()
assert isinstance(person.like, Dish) self.assertIsInstance(person.like, Dish)
def test_generic_list_embedded_document_choices(self): def test_generic_list_embedded_document_choices(self):
"""Ensure you can limit GenericEmbeddedDocument choices inside """Ensure you can limit GenericEmbeddedDocument choices inside
a list field. a list field.
""" """
class Car(EmbeddedDocument): class Car(EmbeddedDocument):
name = StringField() name = StringField()
@ -224,38 +208,39 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
person = Person(name="Test User") person = Person(name='Test User')
person.likes = [Car(name="Fiat")] person.likes = [Car(name='Fiat')]
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person.likes = [Dish(food="arroz", number=15)] person.likes = [Dish(food="arroz", number=15)]
person.save() person.save()
person = Person.objects.first() person = Person.objects.first()
assert isinstance(person.likes[0], Dish) self.assertIsInstance(person.likes[0], Dish)
def test_choices_validation_documents(self): def test_choices_validation_documents(self):
""" """
Ensure fields with document choices validate given a valid choice. Ensure fields with document choices validate given a valid choice.
""" """
class UserComments(EmbeddedDocument): class UserComments(EmbeddedDocument):
author = StringField() author = StringField()
message = StringField() message = StringField()
class BlogPost(Document): class BlogPost(Document):
comments = ListField(GenericEmbeddedDocumentField(choices=(UserComments,))) comments = ListField(
GenericEmbeddedDocumentField(choices=(UserComments,))
)
# Ensure Validation Passes # Ensure Validation Passes
BlogPost(comments=[UserComments(author="user2", message="message2")]).save() BlogPost(comments=[
UserComments(author='user2', message='message2'),
]).save()
def test_choices_validation_documents_invalid(self): def test_choices_validation_documents_invalid(self):
""" """
Ensure fields with document choices validate given an invalid choice. Ensure fields with document choices validate given an invalid choice.
This should throw a ValidationError exception. This should throw a ValidationError exception.
""" """
class UserComments(EmbeddedDocument): class UserComments(EmbeddedDocument):
author = StringField() author = StringField()
message = StringField() message = StringField()
@ -265,30 +250,31 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
message = StringField() message = StringField()
class BlogPost(Document): class BlogPost(Document):
comments = ListField(GenericEmbeddedDocumentField(choices=(UserComments,))) comments = ListField(
GenericEmbeddedDocumentField(choices=(UserComments,))
)
# Single Entry Failure # Single Entry Failure
post = BlogPost(comments=[ModeratorComments(author="mod1", message="message1")]) post = BlogPost(comments=[
with pytest.raises(ValidationError): ModeratorComments(author='mod1', message='message1'),
post.save() ])
self.assertRaises(ValidationError, post.save)
# Mixed Entry Failure # Mixed Entry Failure
post = BlogPost( post = BlogPost(comments=[
comments=[ ModeratorComments(author='mod1', message='message1'),
ModeratorComments(author="mod1", message="message1"), UserComments(author='user2', message='message2'),
UserComments(author="user2", message="message2"), ])
] self.assertRaises(ValidationError, post.save)
)
with pytest.raises(ValidationError):
post.save()
def test_choices_validation_documents_inheritance(self): def test_choices_validation_documents_inheritance(self):
""" """
Ensure fields with document choices validate given subclass of choice. Ensure fields with document choices validate given subclass of choice.
""" """
class Comments(EmbeddedDocument): class Comments(EmbeddedDocument):
meta = {"abstract": True} meta = {
'abstract': True
}
author = StringField() author = StringField()
message = StringField() message = StringField()
@ -296,10 +282,14 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
pass pass
class BlogPost(Document): class BlogPost(Document):
comments = ListField(GenericEmbeddedDocumentField(choices=(Comments,))) comments = ListField(
GenericEmbeddedDocumentField(choices=(Comments,))
)
# Save Valid EmbeddedDocument Type # Save Valid EmbeddedDocument Type
BlogPost(comments=[UserComments(author="user2", message="message2")]).save() BlogPost(comments=[
UserComments(author='user2', message='message2'),
]).save()
def test_query_generic_embedded_document_attribute(self): def test_query_generic_embedded_document_attribute(self):
class AdminSettings(EmbeddedDocument): class AdminSettings(EmbeddedDocument):
@ -309,30 +299,28 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
foo2 = StringField() foo2 = StringField()
class Person(Document): class Person(Document):
settings = GenericEmbeddedDocumentField( settings = GenericEmbeddedDocumentField(choices=(AdminSettings, NonAdminSettings))
choices=(AdminSettings, NonAdminSettings)
)
Person.drop_collection() Person.drop_collection()
p1 = Person(settings=AdminSettings(foo1="bar1")).save() p1 = Person(settings=AdminSettings(foo1='bar1')).save()
p2 = Person(settings=NonAdminSettings(foo2="bar2")).save() p2 = Person(settings=NonAdminSettings(foo2='bar2')).save()
# Test non exiting attribute # Test non exiting attribute
with pytest.raises(InvalidQueryError) as exc_info: with self.assertRaises(InvalidQueryError) as ctx_err:
Person.objects(settings__notexist="bar").first() Person.objects(settings__notexist='bar').first()
assert str(exc_info.value) == 'Cannot resolve field "notexist"' self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
with pytest.raises(LookUpError): with self.assertRaises(LookUpError):
Person.objects.only("settings.notexist") Person.objects.only('settings.notexist')
# Test existing attribute # Test existing attribute
assert Person.objects(settings__foo1="bar1").first().id == p1.id self.assertEqual(Person.objects(settings__foo1='bar1').first().id, p1.id)
assert Person.objects(settings__foo2="bar2").first().id == p2.id self.assertEqual(Person.objects(settings__foo2='bar2').first().id, p2.id)
def test_query_generic_embedded_document_attribute_with_inheritance(self): def test_query_generic_embedded_document_attribute_with_inheritance(self):
class BaseSettings(EmbeddedDocument): class BaseSettings(EmbeddedDocument):
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
base_foo = StringField() base_foo = StringField()
class AdminSettings(BaseSettings): class AdminSettings(BaseSettings):
@ -343,41 +331,14 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
Person.drop_collection() Person.drop_collection()
p = Person(settings=AdminSettings(base_foo="basefoo", sub_foo="subfoo")) p = Person(settings=AdminSettings(base_foo='basefoo', sub_foo='subfoo'))
p.save() p.save()
# Test non exiting attribute # Test non exiting attribute
with pytest.raises(InvalidQueryError) as exc_info: with self.assertRaises(InvalidQueryError) as ctx_err:
assert Person.objects(settings__notexist="bar").first().id == p.id self.assertEqual(Person.objects(settings__notexist='bar').first().id, p.id)
assert str(exc_info.value) == 'Cannot resolve field "notexist"' self.assertEqual(unicode(ctx_err.exception), u'Cannot resolve field "notexist"')
# Test existing attribute # Test existing attribute
assert Person.objects(settings__base_foo="basefoo").first().id == p.id self.assertEqual(Person.objects(settings__base_foo='basefoo').first().id, p.id)
assert Person.objects(settings__sub_foo="subfoo").first().id == p.id self.assertEqual(Person.objects(settings__sub_foo='subfoo').first().id, p.id)
def test_deepcopy_set__instance(self):
"""Ensure that the _instance attribute on EmbeddedDocument exists after a deepcopy"""
class Wallet(EmbeddedDocument):
money = IntField()
class Person(Document):
wallet = EmbeddedDocumentField(Wallet)
wallet_map = MapField(EmbeddedDocumentField(Wallet))
# Test on fresh EmbeddedDoc
emb_doc = Wallet(money=1)
assert emb_doc._instance is None
copied_emb_doc = deepcopy(emb_doc)
assert copied_emb_doc._instance is None
# Test on attached EmbeddedDoc
doc = Person(
id=ObjectId(), wallet=Wallet(money=2), wallet_map={"test": Wallet(money=2)}
)
assert doc.wallet._instance == doc
copied_emb_doc = deepcopy(doc.wallet)
assert copied_emb_doc._instance is None
copied_map_emb_doc = deepcopy(doc.wallet_map)
assert copied_map_emb_doc["test"]._instance is None

View File

@ -1,145 +0,0 @@
from enum import Enum
import pytest
from bson import InvalidDocument
from mongoengine import Document, EnumField, ValidationError
from tests.utils import MongoDBTestCase, get_as_pymongo
class Status(Enum):
NEW = "new"
DONE = "done"
class Color(Enum):
RED = 1
BLUE = 2
class ModelWithEnum(Document):
status = EnumField(Status)
class TestStringEnumField(MongoDBTestCase):
def test_storage(self):
model = ModelWithEnum(status=Status.NEW).save()
assert get_as_pymongo(model) == {"_id": model.id, "status": "new"}
def test_set_enum(self):
ModelWithEnum.drop_collection()
ModelWithEnum(status=Status.NEW).save()
assert ModelWithEnum.objects(status=Status.NEW).count() == 1
assert ModelWithEnum.objects.first().status == Status.NEW
def test_set_by_value(self):
ModelWithEnum.drop_collection()
ModelWithEnum(status="new").save()
assert ModelWithEnum.objects.first().status == Status.NEW
def test_filter(self):
ModelWithEnum.drop_collection()
ModelWithEnum(status="new").save()
assert ModelWithEnum.objects(status="new").count() == 1
assert ModelWithEnum.objects(status=Status.NEW).count() == 1
assert ModelWithEnum.objects(status=Status.DONE).count() == 0
def test_change_value(self):
m = ModelWithEnum(status="new")
m.status = Status.DONE
m.save()
assert m.status == Status.DONE
m.status = "wrong"
assert m.status == "wrong"
with pytest.raises(ValidationError):
m.validate()
def test_set_default(self):
class ModelWithDefault(Document):
status = EnumField(Status, default=Status.DONE)
m = ModelWithDefault().save()
assert m.status == Status.DONE
def test_enum_field_can_be_empty(self):
ModelWithEnum.drop_collection()
m = ModelWithEnum().save()
assert m.status is None
assert ModelWithEnum.objects()[0].status is None
assert ModelWithEnum.objects(status=None).count() == 1
def test_set_none_explicitly(self):
ModelWithEnum.drop_collection()
ModelWithEnum(status=None).save()
assert ModelWithEnum.objects.first().status is None
def test_cannot_create_model_with_wrong_enum_value(self):
m = ModelWithEnum(status="wrong_one")
with pytest.raises(ValidationError):
m.validate()
def test_partial_choices(self):
partial = [Status.DONE]
enum_field = EnumField(Status, choices=partial)
assert enum_field.choices == partial
class FancyDoc(Document):
z = enum_field
FancyDoc(z=Status.DONE).validate()
with pytest.raises(
ValidationError, match=r"Value must be one of .*Status.DONE"
):
FancyDoc(z=Status.NEW).validate()
def test_wrong_choices(self):
with pytest.raises(ValueError, match="Invalid choices"):
EnumField(Status, choices=["my", "custom", "options"])
with pytest.raises(ValueError, match="Invalid choices"):
EnumField(Status, choices=[Color.RED])
with pytest.raises(ValueError, match="Invalid choices"):
EnumField(Status, choices=[Status.DONE, Color.RED])
class ModelWithColor(Document):
color = EnumField(Color, default=Color.RED)
class TestIntEnumField(MongoDBTestCase):
def test_enum_with_int(self):
ModelWithColor.drop_collection()
m = ModelWithColor().save()
assert m.color == Color.RED
assert ModelWithColor.objects(color=Color.RED).count() == 1
assert ModelWithColor.objects(color=1).count() == 1
assert ModelWithColor.objects(color=2).count() == 0
def test_create_int_enum_by_value(self):
model = ModelWithColor(color=2).save()
assert model.color == Color.BLUE
def test_storage_enum_with_int(self):
model = ModelWithColor(color=Color.BLUE).save()
assert get_as_pymongo(model) == {"_id": model.id, "color": 2}
def test_validate_model(self):
with pytest.raises(ValidationError, match="Value must be one of"):
ModelWithColor(color=3).validate()
with pytest.raises(ValidationError, match="Value must be one of"):
ModelWithColor(color="wrong_type").validate()
class TestFunkyEnumField(MongoDBTestCase):
def test_enum_incompatible_bson_type_fails_during_save(self):
class FunkyColor(Enum):
YELLOW = object()
class ModelWithFunkyColor(Document):
color = EnumField(FunkyColor)
m = ModelWithFunkyColor(color=FunkyColor.YELLOW)
with pytest.raises(InvalidDocument, match="[cC]annot encode object"):
m.save()

View File

@ -1,10 +1,13 @@
import pytest # -*- coding: utf-8 -*-
import six
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
class TestFloatField(MongoDBTestCase): class TestFloatField(MongoDBTestCase):
def test_float_ne_operator(self): def test_float_ne_operator(self):
class TestDocument(Document): class TestDocument(Document):
float_fld = FloatField() float_fld = FloatField()
@ -14,12 +17,12 @@ class TestFloatField(MongoDBTestCase):
TestDocument(float_fld=None).save() TestDocument(float_fld=None).save()
TestDocument(float_fld=1).save() TestDocument(float_fld=1).save()
assert 1 == TestDocument.objects(float_fld__ne=None).count() self.assertEqual(1, TestDocument.objects(float_fld__ne=None).count())
assert 1 == TestDocument.objects(float_fld__ne=1).count() self.assertEqual(1, TestDocument.objects(float_fld__ne=1).count())
def test_validation(self): def test_validation(self):
"""Ensure that invalid values cannot be assigned to float fields.""" """Ensure that invalid values cannot be assigned to float fields.
"""
class Person(Document): class Person(Document):
height = FloatField(min_value=0.1, max_value=3.5) height = FloatField(min_value=0.1, max_value=3.5)
@ -30,30 +33,26 @@ class TestFloatField(MongoDBTestCase):
person.height = 1.89 person.height = 1.89
person.validate() person.validate()
person.height = "2.0" person.height = '2.0'
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person.height = 0.01 person.height = 0.01
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person.height = 4.0 person.height = 4.0
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person_2 = Person(height="something invalid") person_2 = Person(height='something invalid')
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person_2.validate)
person_2.validate()
big_person = BigPerson() big_person = BigPerson()
big_person.height = int(0) for value, value_type in enumerate(six.integer_types):
big_person.height = value_type(value)
big_person.validate() big_person.validate()
big_person.height = 2 ** 500 big_person.height = 2 ** 500
big_person.validate() big_person.validate()
big_person.height = 2 ** 100000 # Too big for a float value big_person.height = 2 ** 100000 # Too big for a float value
with pytest.raises(ValidationError): self.assertRaises(ValidationError, big_person.validate)
big_person.validate()

View File

@ -1,13 +1,14 @@
import pytest # -*- coding: utf-8 -*-
from mongoengine import * from mongoengine import *
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
class TestIntField(MongoDBTestCase): class TestIntField(MongoDBTestCase):
def test_int_validation(self):
"""Ensure that invalid values cannot be assigned to int fields."""
def test_int_validation(self):
"""Ensure that invalid values cannot be assigned to int fields.
"""
class Person(Document): class Person(Document):
age = IntField(min_value=0, max_value=110) age = IntField(min_value=0, max_value=110)
@ -22,14 +23,11 @@ class TestIntField(MongoDBTestCase):
person.validate() person.validate()
person.age = -1 person.age = -1
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate()
person.age = 120 person.age = 120
with pytest.raises(ValidationError): self.assertRaises(ValidationError, person.validate)
person.validate() person.age = 'ten'
person.age = "ten" self.assertRaises(ValidationError, person.validate)
with pytest.raises(ValidationError):
person.validate()
def test_ne_operator(self): def test_ne_operator(self):
class TestDocument(Document): class TestDocument(Document):
@ -40,5 +38,5 @@ class TestIntField(MongoDBTestCase):
TestDocument(int_fld=None).save() TestDocument(int_fld=None).save()
TestDocument(int_fld=1).save() TestDocument(int_fld=1).save()
assert 1 == TestDocument.objects(int_fld__ne=None).count() self.assertEqual(1, TestDocument.objects(int_fld__ne=None).count())
assert 1 == TestDocument.objects(int_fld__ne=1).count() self.assertEqual(1, TestDocument.objects(int_fld__ne=1).count())

View File

@ -1,9 +1,9 @@
import pytest # -*- coding: utf-8 -*-
from bson import DBRef, ObjectId from bson import DBRef, ObjectId
from mongoengine import * from mongoengine import *
from mongoengine.base import LazyReference from mongoengine.base import LazyReference
from mongoengine.context_managers import query_counter
from tests.utils import MongoDBTestCase from tests.utils import MongoDBTestCase
@ -11,8 +11,7 @@ class TestLazyReferenceField(MongoDBTestCase):
def test_lazy_reference_config(self): def test_lazy_reference_config(self):
# Make sure ReferenceField only accepts a document class or a string # Make sure ReferenceField only accepts a document class or a string
# with a document class name. # with a document class name.
with pytest.raises(ValidationError): self.assertRaises(ValidationError, LazyReferenceField, EmbeddedDocument)
LazyReferenceField(EmbeddedDocument)
def test___repr__(self): def test___repr__(self):
class Animal(Document): class Animal(Document):
@ -26,7 +25,7 @@ class TestLazyReferenceField(MongoDBTestCase):
animal = Animal() animal = Animal()
oc = Ocurrence(animal=animal) oc = Ocurrence(animal=animal)
assert "LazyReference" in repr(oc.animal) self.assertIn('LazyReference', repr(oc.animal))
def test___getattr___unknown_attr_raises_attribute_error(self): def test___getattr___unknown_attr_raises_attribute_error(self):
class Animal(Document): class Animal(Document):
@ -40,7 +39,7 @@ class TestLazyReferenceField(MongoDBTestCase):
animal = Animal().save() animal = Animal().save()
oc = Ocurrence(animal=animal) oc = Ocurrence(animal=animal)
with pytest.raises(AttributeError): with self.assertRaises(AttributeError):
oc.animal.not_exist oc.animal.not_exist
def test_lazy_reference_simple(self): def test_lazy_reference_simple(self):
@ -58,19 +57,19 @@ class TestLazyReferenceField(MongoDBTestCase):
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
Ocurrence(person="test", animal=animal).save() Ocurrence(person="test", animal=animal).save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert isinstance(p.animal, LazyReference) self.assertIsInstance(p.animal, LazyReference)
fetched_animal = p.animal.fetch() fetched_animal = p.animal.fetch()
assert fetched_animal == animal self.assertEqual(fetched_animal, animal)
# `fetch` keep cache on referenced document by default... # `fetch` keep cache on referenced document by default...
animal.tag = "not so heavy" animal.tag = "not so heavy"
animal.save() animal.save()
double_fetch = p.animal.fetch() double_fetch = p.animal.fetch()
assert fetched_animal is double_fetch self.assertIs(fetched_animal, double_fetch)
assert double_fetch.tag == "heavy" self.assertEqual(double_fetch.tag, "heavy")
# ...unless specified otherwise # ...unless specified otherwise
fetch_force = p.animal.fetch(force=True) fetch_force = p.animal.fetch(force=True)
assert fetch_force is not fetched_animal self.assertIsNot(fetch_force, fetched_animal)
assert fetch_force.tag == "not so heavy" self.assertEqual(fetch_force.tag, "not so heavy")
def test_lazy_reference_fetch_invalid_ref(self): def test_lazy_reference_fetch_invalid_ref(self):
class Animal(Document): class Animal(Document):
@ -88,13 +87,13 @@ class TestLazyReferenceField(MongoDBTestCase):
Ocurrence(person="test", animal=animal).save() Ocurrence(person="test", animal=animal).save()
animal.delete() animal.delete()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert isinstance(p.animal, LazyReference) self.assertIsInstance(p.animal, LazyReference)
with pytest.raises(DoesNotExist): with self.assertRaises(DoesNotExist):
p.animal.fetch() p.animal.fetch()
def test_lazy_reference_set(self): def test_lazy_reference_set(self):
class Animal(Document): class Animal(Document):
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
name = StringField() name = StringField()
tag = StringField() tag = StringField()
@ -110,12 +109,13 @@ class TestLazyReferenceField(MongoDBTestCase):
nick = StringField() nick = StringField()
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
sub_animal = SubAnimal(nick="doggo", name="dog").save() sub_animal = SubAnimal(nick='doggo', name='dog').save()
for ref in ( for ref in (
animal, animal,
animal.pk, animal.pk,
DBRef(animal._get_collection_name(), animal.pk), DBRef(animal._get_collection_name(), animal.pk),
LazyReference(Animal, animal.pk), LazyReference(Animal, animal.pk),
sub_animal, sub_animal,
sub_animal.pk, sub_animal.pk,
DBRef(sub_animal._get_collection_name(), sub_animal.pk), DBRef(sub_animal._get_collection_name(), sub_animal.pk),
@ -123,7 +123,7 @@ class TestLazyReferenceField(MongoDBTestCase):
): ):
p = Ocurrence(person="test", animal=ref).save() p = Ocurrence(person="test", animal=ref).save()
p.reload() p.reload()
assert isinstance(p.animal, LazyReference) self.assertIsInstance(p.animal, LazyReference)
p.animal.fetch() p.animal.fetch()
def test_lazy_reference_bad_set(self): def test_lazy_reference_bad_set(self):
@ -145,19 +145,18 @@ class TestLazyReferenceField(MongoDBTestCase):
baddoc = BadDoc().save() baddoc = BadDoc().save()
for bad in ( for bad in (
42, 42,
"foo", 'foo',
baddoc, baddoc,
DBRef(baddoc._get_collection_name(), animal.pk), DBRef(baddoc._get_collection_name(), animal.pk),
LazyReference(BadDoc, animal.pk), LazyReference(BadDoc, animal.pk)
): ):
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
Ocurrence(person="test", animal=bad).save() p = Ocurrence(person="test", animal=bad).save()
def test_lazy_reference_query_conversion(self): def test_lazy_reference_query_conversion(self):
"""Ensure that LazyReferenceFields can be queried using objects and values """Ensure that LazyReferenceFields can be queried using objects and values
of the type of the primary key of the referenced object. of the type of the primary key of the referenced object.
""" """
class Member(Document): class Member(Document):
user_num = IntField(primary_key=True) user_num = IntField(primary_key=True)
@ -173,27 +172,26 @@ class TestLazyReferenceField(MongoDBTestCase):
m2 = Member(user_num=2) m2 = Member(user_num=2)
m2.save() m2.save()
post1 = BlogPost(title="post 1", author=m1) post1 = BlogPost(title='post 1', author=m1)
post1.save() post1.save()
post2 = BlogPost(title="post 2", author=m2) post2 = BlogPost(title='post 2', author=m2)
post2.save() post2.save()
post = BlogPost.objects(author=m1).first() post = BlogPost.objects(author=m1).first()
assert post.id == post1.id self.assertEqual(post.id, post1.id)
post = BlogPost.objects(author=m2).first() post = BlogPost.objects(author=m2).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
# Same thing by passing a LazyReference instance # Same thing by passing a LazyReference instance
post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
def test_lazy_reference_query_conversion_dbref(self): def test_lazy_reference_query_conversion_dbref(self):
"""Ensure that LazyReferenceFields can be queried using objects and values """Ensure that LazyReferenceFields can be queried using objects and values
of the type of the primary key of the referenced object. of the type of the primary key of the referenced object.
""" """
class Member(Document): class Member(Document):
user_num = IntField(primary_key=True) user_num = IntField(primary_key=True)
@ -209,21 +207,21 @@ class TestLazyReferenceField(MongoDBTestCase):
m2 = Member(user_num=2) m2 = Member(user_num=2)
m2.save() m2.save()
post1 = BlogPost(title="post 1", author=m1) post1 = BlogPost(title='post 1', author=m1)
post1.save() post1.save()
post2 = BlogPost(title="post 2", author=m2) post2 = BlogPost(title='post 2', author=m2)
post2.save() post2.save()
post = BlogPost.objects(author=m1).first() post = BlogPost.objects(author=m1).first()
assert post.id == post1.id self.assertEqual(post.id, post1.id)
post = BlogPost.objects(author=m2).first() post = BlogPost.objects(author=m2).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
# Same thing by passing a LazyReference instance # Same thing by passing a LazyReference instance
post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
def test_lazy_reference_passthrough(self): def test_lazy_reference_passthrough(self):
class Animal(Document): class Animal(Document):
@ -240,21 +238,21 @@ class TestLazyReferenceField(MongoDBTestCase):
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
Ocurrence(animal=animal, animal_passthrough=animal).save() Ocurrence(animal=animal, animal_passthrough=animal).save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert isinstance(p.animal, LazyReference) self.assertIsInstance(p.animal, LazyReference)
with pytest.raises(KeyError): with self.assertRaises(KeyError):
p.animal["name"] p.animal['name']
with pytest.raises(AttributeError): with self.assertRaises(AttributeError):
p.animal.name p.animal.name
assert p.animal.pk == animal.pk self.assertEqual(p.animal.pk, animal.pk)
assert p.animal_passthrough.name == "Leopard" self.assertEqual(p.animal_passthrough.name, "Leopard")
assert p.animal_passthrough["name"] == "Leopard" self.assertEqual(p.animal_passthrough['name'], "Leopard")
# Should not be able to access referenced document's methods # Should not be able to access referenced document's methods
with pytest.raises(AttributeError): with self.assertRaises(AttributeError):
p.animal.save p.animal.save
with pytest.raises(KeyError): with self.assertRaises(KeyError):
p.animal["save"] p.animal['save']
def test_lazy_reference_not_set(self): def test_lazy_reference_not_set(self):
class Animal(Document): class Animal(Document):
@ -268,9 +266,9 @@ class TestLazyReferenceField(MongoDBTestCase):
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
Ocurrence(person="foo").save() Ocurrence(person='foo').save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert p.animal is None self.assertIs(p.animal, None)
def test_lazy_reference_equality(self): def test_lazy_reference_equality(self):
class Animal(Document): class Animal(Document):
@ -281,12 +279,12 @@ class TestLazyReferenceField(MongoDBTestCase):
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
animalref = LazyReference(Animal, animal.pk) animalref = LazyReference(Animal, animal.pk)
assert animal == animalref self.assertEqual(animal, animalref)
assert animalref == animal self.assertEqual(animalref, animal)
other_animalref = LazyReference(Animal, ObjectId("54495ad94c934721ede76f90")) other_animalref = LazyReference(Animal, ObjectId("54495ad94c934721ede76f90"))
assert animal != other_animalref self.assertNotEqual(animal, other_animalref)
assert other_animalref != animal self.assertNotEqual(other_animalref, animal)
def test_lazy_reference_embedded(self): def test_lazy_reference_embedded(self):
class Animal(Document): class Animal(Document):
@ -305,21 +303,21 @@ class TestLazyReferenceField(MongoDBTestCase):
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
animal1 = Animal(name="doggo").save() animal1 = Animal('doggo').save()
animal2 = Animal(name="cheeta").save() animal2 = Animal('cheeta').save()
def check_fields_type(occ): def check_fields_type(occ):
assert isinstance(occ.direct, LazyReference) self.assertIsInstance(occ.direct, LazyReference)
for elem in occ.in_list: for elem in occ.in_list:
assert isinstance(elem, LazyReference) self.assertIsInstance(elem, LazyReference)
assert isinstance(occ.in_embedded.direct, LazyReference) self.assertIsInstance(occ.in_embedded.direct, LazyReference)
for elem in occ.in_embedded.in_list: for elem in occ.in_embedded.in_list:
assert isinstance(elem, LazyReference) self.assertIsInstance(elem, LazyReference)
occ = Ocurrence( occ = Ocurrence(
in_list=[animal1, animal2], in_list=[animal1, animal2],
in_embedded={"in_list": [animal1, animal2], "direct": animal1}, in_embedded={'in_list': [animal1, animal2], 'direct': animal1},
direct=animal1, direct=animal1
).save() ).save()
check_fields_type(occ) check_fields_type(occ)
occ.reload() occ.reload()
@ -330,70 +328,6 @@ class TestLazyReferenceField(MongoDBTestCase):
occ.in_embedded.in_list = [animal1.id, animal2.id] occ.in_embedded.in_list = [animal1.id, animal2.id]
check_fields_type(occ) check_fields_type(occ)
def test_lazy_reference_embedded_dereferencing(self):
# Test case for #2375
# -- Test documents
class Author(Document):
name = StringField()
class AuthorReference(EmbeddedDocument):
author = LazyReferenceField(Author)
class Book(Document):
authors = EmbeddedDocumentListField(AuthorReference)
# -- Cleanup
Author.drop_collection()
Book.drop_collection()
# -- Create test data
author_1 = Author(name="A1").save()
author_2 = Author(name="A2").save()
author_3 = Author(name="A3").save()
book = Book(
authors=[
AuthorReference(author=author_1),
AuthorReference(author=author_2),
AuthorReference(author=author_3),
]
).save()
with query_counter() as qc:
book = Book.objects.first()
# Accessing the list must not trigger dereferencing.
book.authors
assert qc == 1
for ref in book.authors:
with pytest.raises(AttributeError):
ref["author"].name
assert isinstance(ref.author, LazyReference)
assert isinstance(ref.author.id, ObjectId)
def test_lazy_reference_in_list_with_changed_element(self):
class Animal(Document):
name = StringField()
tag = StringField()
class Ocurrence(Document):
in_list = ListField(LazyReferenceField(Animal))
Animal.drop_collection()
Ocurrence.drop_collection()
animal1 = Animal(name="doggo").save()
animal1.tag = "blue"
occ = Ocurrence(in_list=[animal1]).save()
animal1.save()
assert isinstance(occ.in_list[0], LazyReference)
assert occ.in_list[0].pk == animal1.pk
class TestGenericLazyReferenceField(MongoDBTestCase): class TestGenericLazyReferenceField(MongoDBTestCase):
def test_generic_lazy_reference_simple(self): def test_generic_lazy_reference_simple(self):
@ -411,19 +345,19 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
Ocurrence(person="test", animal=animal).save() Ocurrence(person="test", animal=animal).save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert isinstance(p.animal, LazyReference) self.assertIsInstance(p.animal, LazyReference)
fetched_animal = p.animal.fetch() fetched_animal = p.animal.fetch()
assert fetched_animal == animal self.assertEqual(fetched_animal, animal)
# `fetch` keep cache on referenced document by default... # `fetch` keep cache on referenced document by default...
animal.tag = "not so heavy" animal.tag = "not so heavy"
animal.save() animal.save()
double_fetch = p.animal.fetch() double_fetch = p.animal.fetch()
assert fetched_animal is double_fetch self.assertIs(fetched_animal, double_fetch)
assert double_fetch.tag == "heavy" self.assertEqual(double_fetch.tag, "heavy")
# ...unless specified otherwise # ...unless specified otherwise
fetch_force = p.animal.fetch(force=True) fetch_force = p.animal.fetch(force=True)
assert fetch_force is not fetched_animal self.assertIsNot(fetch_force, fetched_animal)
assert fetch_force.tag == "not so heavy" self.assertEqual(fetch_force.tag, "not so heavy")
def test_generic_lazy_reference_choices(self): def test_generic_lazy_reference_choices(self):
class Animal(Document): class Animal(Document):
@ -449,14 +383,14 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
mineral = Mineral(name="Granite").save() mineral = Mineral(name="Granite").save()
occ_animal = Ocurrence(living_thing=animal, thing=animal).save() occ_animal = Ocurrence(living_thing=animal, thing=animal).save()
_ = Ocurrence(living_thing=vegetal, thing=vegetal).save() occ_vegetal = Ocurrence(living_thing=vegetal, thing=vegetal).save()
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
Ocurrence(living_thing=mineral).save() Ocurrence(living_thing=mineral).save()
occ = Ocurrence.objects.get(living_thing=animal) occ = Ocurrence.objects.get(living_thing=animal)
assert occ == occ_animal self.assertEqual(occ, occ_animal)
assert isinstance(occ.thing, LazyReference) self.assertIsInstance(occ.thing, LazyReference)
assert isinstance(occ.living_thing, LazyReference) self.assertIsInstance(occ.living_thing, LazyReference)
occ.thing = vegetal occ.thing = vegetal
occ.living_thing = vegetal occ.living_thing = vegetal
@ -464,12 +398,12 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
occ.thing = mineral occ.thing = mineral
occ.living_thing = mineral occ.living_thing = mineral
with pytest.raises(ValidationError): with self.assertRaises(ValidationError):
occ.save() occ.save()
def test_generic_lazy_reference_set(self): def test_generic_lazy_reference_set(self):
class Animal(Document): class Animal(Document):
meta = {"allow_inheritance": True} meta = {'allow_inheritance': True}
name = StringField() name = StringField()
tag = StringField() tag = StringField()
@ -485,21 +419,19 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
nick = StringField() nick = StringField()
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
sub_animal = SubAnimal(nick="doggo", name="dog").save() sub_animal = SubAnimal(nick='doggo', name='dog').save()
for ref in ( for ref in (
animal, animal,
LazyReference(Animal, animal.pk), LazyReference(Animal, animal.pk),
{"_cls": "Animal", "_ref": DBRef(animal._get_collection_name(), animal.pk)}, {'_cls': 'Animal', '_ref': DBRef(animal._get_collection_name(), animal.pk)},
sub_animal, sub_animal,
LazyReference(SubAnimal, sub_animal.pk), LazyReference(SubAnimal, sub_animal.pk),
{ {'_cls': 'SubAnimal', '_ref': DBRef(sub_animal._get_collection_name(), sub_animal.pk)},
"_cls": "SubAnimal",
"_ref": DBRef(sub_animal._get_collection_name(), sub_animal.pk),
},
): ):
p = Ocurrence(person="test", animal=ref).save() p = Ocurrence(person="test", animal=ref).save()
p.reload() p.reload()
assert isinstance(p.animal, (LazyReference, Document)) self.assertIsInstance(p.animal, (LazyReference, Document))
p.animal.fetch() p.animal.fetch()
def test_generic_lazy_reference_bad_set(self): def test_generic_lazy_reference_bad_set(self):
@ -509,7 +441,7 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
class Ocurrence(Document): class Ocurrence(Document):
person = StringField() person = StringField()
animal = GenericLazyReferenceField(choices=["Animal"]) animal = GenericLazyReferenceField(choices=['Animal'])
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
@ -519,9 +451,14 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
animal = Animal(name="Leopard", tag="heavy").save() animal = Animal(name="Leopard", tag="heavy").save()
baddoc = BadDoc().save() baddoc = BadDoc().save()
for bad in (42, "foo", baddoc, LazyReference(BadDoc, animal.pk)): for bad in (
with pytest.raises(ValidationError): 42,
Ocurrence(person="test", animal=bad).save() 'foo',
baddoc,
LazyReference(BadDoc, animal.pk)
):
with self.assertRaises(ValidationError):
p = Ocurrence(person="test", animal=bad).save()
def test_generic_lazy_reference_query_conversion(self): def test_generic_lazy_reference_query_conversion(self):
class Member(Document): class Member(Document):
@ -539,21 +476,21 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
m2 = Member(user_num=2) m2 = Member(user_num=2)
m2.save() m2.save()
post1 = BlogPost(title="post 1", author=m1) post1 = BlogPost(title='post 1', author=m1)
post1.save() post1.save()
post2 = BlogPost(title="post 2", author=m2) post2 = BlogPost(title='post 2', author=m2)
post2.save() post2.save()
post = BlogPost.objects(author=m1).first() post = BlogPost.objects(author=m1).first()
assert post.id == post1.id self.assertEqual(post.id, post1.id)
post = BlogPost.objects(author=m2).first() post = BlogPost.objects(author=m2).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
# Same thing by passing a LazyReference instance # Same thing by passing a LazyReference instance
post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first() post = BlogPost.objects(author=LazyReference(Member, m2.pk)).first()
assert post.id == post2.id self.assertEqual(post.id, post2.id)
def test_generic_lazy_reference_not_set(self): def test_generic_lazy_reference_not_set(self):
class Animal(Document): class Animal(Document):
@ -567,9 +504,9 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
Ocurrence(person="foo").save() Ocurrence(person='foo').save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert p.animal is None self.assertIs(p.animal, None)
def test_generic_lazy_reference_accepts_string_instead_of_class(self): def test_generic_lazy_reference_accepts_string_instead_of_class(self):
class Animal(Document): class Animal(Document):
@ -578,7 +515,7 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
class Ocurrence(Document): class Ocurrence(Document):
person = StringField() person = StringField()
animal = GenericLazyReferenceField("Animal") animal = GenericLazyReferenceField('Animal')
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
@ -586,7 +523,7 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
animal = Animal().save() animal = Animal().save()
Ocurrence(animal=animal).save() Ocurrence(animal=animal).save()
p = Ocurrence.objects.get() p = Ocurrence.objects.get()
assert p.animal == animal self.assertEqual(p.animal, animal)
def test_generic_lazy_reference_embedded(self): def test_generic_lazy_reference_embedded(self):
class Animal(Document): class Animal(Document):
@ -605,33 +542,27 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
Animal.drop_collection() Animal.drop_collection()
Ocurrence.drop_collection() Ocurrence.drop_collection()
animal1 = Animal(name="doggo").save() animal1 = Animal('doggo').save()
animal2 = Animal(name="cheeta").save() animal2 = Animal('cheeta').save()
def check_fields_type(occ): def check_fields_type(occ):
assert isinstance(occ.direct, LazyReference) self.assertIsInstance(occ.direct, LazyReference)
for elem in occ.in_list: for elem in occ.in_list:
assert isinstance(elem, LazyReference) self.assertIsInstance(elem, LazyReference)
assert isinstance(occ.in_embedded.direct, LazyReference) self.assertIsInstance(occ.in_embedded.direct, LazyReference)
for elem in occ.in_embedded.in_list: for elem in occ.in_embedded.in_list:
assert isinstance(elem, LazyReference) self.assertIsInstance(elem, LazyReference)
occ = Ocurrence( occ = Ocurrence(
in_list=[animal1, animal2], in_list=[animal1, animal2],
in_embedded={"in_list": [animal1, animal2], "direct": animal1}, in_embedded={'in_list': [animal1, animal2], 'direct': animal1},
direct=animal1, direct=animal1
).save() ).save()
check_fields_type(occ) check_fields_type(occ)
occ.reload() occ.reload()
check_fields_type(occ) check_fields_type(occ)
animal1_ref = { animal1_ref = {'_cls': 'Animal', '_ref': DBRef(animal1._get_collection_name(), animal1.pk)}
"_cls": "Animal", animal2_ref = {'_cls': 'Animal', '_ref': DBRef(animal2._get_collection_name(), animal2.pk)}
"_ref": DBRef(animal1._get_collection_name(), animal1.pk),
}
animal2_ref = {
"_cls": "Animal",
"_ref": DBRef(animal2._get_collection_name(), animal2.pk),
}
occ.direct = animal1_ref occ.direct = animal1_ref
occ.in_list = [animal1_ref, animal2_ref] occ.in_list = [animal1_ref, animal2_ref]
occ.in_embedded.direct = animal1_ref occ.in_embedded.direct = animal1_ref

View File

@ -1,60 +1,48 @@
import pytest # -*- coding: utf-8 -*-
from bson.int64 import Int64 import six
try:
from bson.int64 import Int64
except ImportError:
Int64 = long
from mongoengine import * from mongoengine import *
from mongoengine.connection import get_db from mongoengine.connection import get_db
from tests.utils import MongoDBTestCase, get_as_pymongo
from tests.utils import MongoDBTestCase
class TestLongField(MongoDBTestCase): class TestLongField(MongoDBTestCase):
def test_storage(self):
class Person(Document):
value = LongField()
Person.drop_collection()
person = Person(value=5000)
person.save()
assert get_as_pymongo(person) == {"_id": person.id, "value": 5000}
def test_construction_does_not_fail_with_invalid_value(self):
class Person(Document):
value = LongField()
person = Person(value="not_an_int")
assert person.value == "not_an_int"
def test_long_field_is_considered_as_int64(self): def test_long_field_is_considered_as_int64(self):
""" """
Tests that long fields are stored as long in mongo, even if long Tests that long fields are stored as long in mongo, even if long
value is small enough to be an int. value is small enough to be an int.
""" """
class TestLongFieldConsideredAsInt64(Document): class TestLongFieldConsideredAsInt64(Document):
some_long = LongField() some_long = LongField()
doc = TestLongFieldConsideredAsInt64(some_long=42).save() doc = TestLongFieldConsideredAsInt64(some_long=42).save()
db = get_db() db = get_db()
assert isinstance( self.assertIsInstance(db.test_long_field_considered_as_int64.find()[0]['some_long'], Int64)
db.test_long_field_considered_as_int64.find()[0]["some_long"], Int64 self.assertIsInstance(doc.some_long, six.integer_types)
)
assert isinstance(doc.some_long, int)
def test_long_validation(self): def test_long_validation(self):
"""Ensure that invalid values cannot be assigned to long fields.""" """Ensure that invalid values cannot be assigned to long fields.
"""
class TestDocument(Document): class TestDocument(Document):
value = LongField(min_value=0, max_value=110) value = LongField(min_value=0, max_value=110)
TestDocument(value=50).validate() doc = TestDocument()
doc.value = 50
doc.validate()
with pytest.raises(ValidationError): doc.value = -1
TestDocument(value=-1).validate() self.assertRaises(ValidationError, doc.validate)
doc.value = 120
with pytest.raises(ValidationError): self.assertRaises(ValidationError, doc.validate)
TestDocument(value=120).validate() doc.value = 'ten'
self.assertRaises(ValidationError, doc.validate)
with pytest.raises(ValidationError):
TestDocument(value="ten").validate()
def test_long_ne_operator(self): def test_long_ne_operator(self):
class TestDocument(Document): class TestDocument(Document):
@ -65,5 +53,4 @@ class TestLongField(MongoDBTestCase):
TestDocument(long_fld=None).save() TestDocument(long_fld=None).save()
TestDocument(long_fld=1).save() TestDocument(long_fld=1).save()
assert TestDocument.objects(long_fld__ne=None).count() == 1 self.assertEqual(1, TestDocument.objects(long_fld__ne=None).count())
assert TestDocument.objects(long_fld__ne=1).count() == 1

Some files were not shown because too many files have changed in this diff Show More