Compare commits
199 Commits
v0.19.1
...
bagerard-p
Author | SHA1 | Date | |
---|---|---|---|
|
d73ca6f90d | ||
|
e6c2169f76 | ||
|
1d17dc4663 | ||
|
eeac3bd2e6 | ||
|
3f5a15d236 | ||
|
338c40b5d5 | ||
|
fc3ccf9606 | ||
|
746faceb5c | ||
|
eb56fb9bda | ||
|
161493c0d2 | ||
|
cb9f329d11 | ||
|
03af784ebe | ||
|
e5f6e4584a | ||
|
79f9f223d0 | ||
|
0bc18cd6e1 | ||
|
30a3c6a5b7 | ||
|
90c5d83f84 | ||
|
d8b8ff6851 | ||
|
ee664f0c90 | ||
|
94a7e813b1 | ||
|
8ef7213426 | ||
|
2f4464ead5 | ||
|
89b93461ac | ||
|
9e40f3ae83 | ||
|
f4962fbc40 | ||
|
c9d53ca5d5 | ||
|
65f50fd713 | ||
|
bf1d04e399 | ||
|
5a8e5e5a40 | ||
|
f3919dd839 | ||
|
9f82a02ddf | ||
|
015a36c85f | ||
|
fbd3388a59 | ||
|
d8a52d68c5 | ||
|
4286708e2e | ||
|
e362d089e1 | ||
|
6b657886a5 | ||
|
eb16945147 | ||
|
38047ca992 | ||
|
c801e79d4b | ||
|
3fca3739de | ||
|
c218c8bb6c | ||
|
0bbc05995a | ||
|
3adb67901b | ||
|
d4350e7da4 | ||
|
4665658145 | ||
|
0d289fd5a1 | ||
|
aabc18755c | ||
|
1f2a5db016 | ||
|
ff40f66291 | ||
|
7f77084e0e | ||
|
aca4de728e | ||
|
9e7ca43cad | ||
|
7116dec74a | ||
|
a5302b870b | ||
|
604e9974b6 | ||
|
3e1c83f8fa | ||
|
e431e27cb2 | ||
|
4f188655d0 | ||
|
194b0cac88 | ||
|
7b4175fc5c | ||
|
adb5f74ddb | ||
|
107a1c34c8 | ||
|
dc7da5204f | ||
|
0301bca176 | ||
|
49f9bca23b | ||
|
31498bd7dd | ||
|
1698f398eb | ||
|
4275c2d7b7 | ||
|
22bff8566d | ||
|
d8657be320 | ||
|
3db9d58dac | ||
|
3fbe9c3cdd | ||
|
130e9c519c | ||
|
78c9e9745d | ||
|
38ebb5abf4 | ||
|
9b73be26ab | ||
|
fd0095b73f | ||
|
226049f66a | ||
|
dc1cf88ca6 | ||
|
f5f8b730b5 | ||
|
e8f6b42316 | ||
|
49b0d73654 | ||
|
394da67cf1 | ||
|
ef7da36ac6 | ||
|
1312100bc7 | ||
|
4085bc2152 | ||
|
f4d7e72426 | ||
|
ece63ad071 | ||
|
a9550b8243 | ||
|
43724e40b2 | ||
|
1bfa40e926 | ||
|
d493f71c4e | ||
|
87f4d1a323 | ||
|
0a0e6114f5 | ||
|
41d36fa3bf | ||
|
707923e3f5 | ||
|
d9b9581df2 | ||
|
463e7c66af | ||
|
2be28a22a7 | ||
|
d73f0bb1af | ||
|
ce74978b1e | ||
|
2b0157aecd | ||
|
f49baf5d90 | ||
|
7cc964c7d8 | ||
|
bc77322c2f | ||
|
8913a74a86 | ||
|
af35b25d15 | ||
|
476b07af6e | ||
|
e2b9a02531 | ||
|
6cc6229066 | ||
|
4c62a060f0 | ||
|
3d80637fa4 | ||
|
68be9fe979 | ||
|
547cd4a3ae | ||
|
ee2d50b2d1 | ||
|
15c3ddece8 | ||
|
beaa9744b7 | ||
|
8eb51790b5 | ||
|
aadc6262ed | ||
|
00ae6298d4 | ||
|
ad0669a326 | ||
|
85df76c623 | ||
|
87512246cb | ||
|
a3f9016ae9 | ||
|
4e58e9f8d1 | ||
|
7c533394fd | ||
|
333e014f13 | ||
|
c0c0efce18 | ||
|
beabaee345 | ||
|
c937af3919 | ||
|
aa4a6ae023 | ||
|
b57946ec98 | ||
|
1e110a2c41 | ||
|
b234aa48e4 | ||
|
8086576677 | ||
|
03e34299f0 | ||
|
421e3f324f | ||
|
a0b803959c | ||
|
ff4d57032a | ||
|
ba34589065 | ||
|
a4d11eef46 | ||
|
fda2e2b47a | ||
|
d287f480e5 | ||
|
d85f0e6226 | ||
|
cfb4943986 | ||
|
b453a96211 | ||
|
81f9b351b3 | ||
|
4bca3de42f | ||
|
235b1a3679 | ||
|
450658d7ac | ||
|
8e17e42e26 | ||
|
2d6a4c4b90 | ||
|
38703acc29 | ||
|
095217e797 | ||
|
86e965f854 | ||
|
57db68dc04 | ||
|
72de6d67c7 | ||
|
b2c3acd025 | ||
|
605de59bd0 | ||
|
e0565ddac5 | ||
|
18b68f1b80 | ||
|
ea88806630 | ||
|
412bed0f6d | ||
|
53cf26b9af | ||
|
d738462139 | ||
|
2fa48cd9e5 | ||
|
e64a7a9448 | ||
|
9490ad2bf7 | ||
|
84f3dce492 | ||
|
60c42dddd5 | ||
|
f93f9406ee | ||
|
705c55ce24 | ||
|
928770c43a | ||
|
59fbd505a0 | ||
|
1cc20c9770 | ||
|
f8f267a880 | ||
|
d8c0631dab | ||
|
9166ba91d7 | ||
|
6bc4e602bb | ||
|
45a7520fc3 | ||
|
64c0cace85 | ||
|
82af5e4a19 | ||
|
7e0ba1b335 | ||
|
44b7f792fe | ||
|
a3e432eb68 | ||
|
009f9a2b14 | ||
|
2ca905b6e5 | ||
|
90fecc56dd | ||
|
d3d7f0e670 | ||
|
37ffeafeff | ||
|
abc159b7b9 | ||
|
648b28876d | ||
|
5b9f2bac87 | ||
|
17151f67c2 | ||
|
b61c8cd104 | ||
|
8f288fe458 | ||
|
d37a30e083 | ||
|
c9ed930606 |
33
.github/workflows/main.yml
vendored
Normal file
33
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: CI
|
||||
|
||||
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||
# events but only for the master branch
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: echo Hello, world!
|
||||
|
||||
# Runs a set of commands using the runners shell
|
||||
- name: Run a multi-line script
|
||||
run: |
|
||||
echo Add other actions to build,
|
||||
echo test, and deploy your project.
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,8 +1,15 @@
|
||||
.*
|
||||
!.gitignore
|
||||
*~
|
||||
*.py[co]
|
||||
.*.sw[po]
|
||||
.cache/
|
||||
.coverage
|
||||
.coveragerc
|
||||
.env
|
||||
.idea/
|
||||
.pytest_cache/
|
||||
.tox/
|
||||
.eggs/
|
||||
*.egg
|
||||
docs/.build
|
||||
docs/_build
|
||||
@@ -13,8 +20,6 @@ env/
|
||||
.settings
|
||||
.project
|
||||
.pydevproject
|
||||
tests/test_bugfix.py
|
||||
htmlcov/
|
||||
venv
|
||||
venv3
|
||||
scratchpad
|
||||
|
@@ -5,17 +5,12 @@ pylint:
|
||||
|
||||
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
|
||||
# add long as valid built-ins.
|
||||
- long
|
||||
|
||||
pyflakes:
|
||||
disable:
|
||||
# undefined variables are already covered by pylint (and exclude
|
||||
# xrange & long)
|
||||
# undefined variables are already covered by pylint (and exclude long)
|
||||
- F821
|
||||
|
||||
ignore-paths:
|
||||
|
12
.pre-commit-config.yaml
Normal file
12
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
fail_fast: false
|
||||
repos:
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 19.10b0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.0a2
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies:
|
||||
- flake8-import-order
|
20
.readthedocs.yml
Normal file
20
.readthedocs.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# .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: .
|
63
.travis.yml
63
.travis.yml
@@ -1,13 +1,10 @@
|
||||
# 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, v3.7, v3.8 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.
|
||||
#
|
||||
# combinations.
|
||||
# * Python3.7, MongoDB v3.4 & the latest PyMongo v3.x is currently the "main" setup,
|
||||
# Other combinations are tested. See below for the details or check the travis jobs
|
||||
|
||||
# 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:
|
||||
@@ -16,41 +13,43 @@
|
||||
#
|
||||
# Reminder: Update README.rst if you change MongoDB versions we test.
|
||||
|
||||
|
||||
language: python
|
||||
dist: xenial
|
||||
python:
|
||||
- 2.7
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8
|
||||
- pypy
|
||||
- 3.9
|
||||
- pypy3
|
||||
|
||||
dist: xenial
|
||||
|
||||
env:
|
||||
global:
|
||||
- MONGODB_3_4=3.4.17
|
||||
- MONGODB_3_6=3.6.12
|
||||
- PYMONGO_3_9=3.9
|
||||
- PYMONGO_3_6=3.6
|
||||
- MONGODB_3_4=3.4.19
|
||||
- MONGODB_3_6=3.6.13
|
||||
- MONGODB_4_0=4.0.13
|
||||
|
||||
- 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
|
||||
matrix:
|
||||
- MONGODB=${MONGODB_3_4} PYMONGO=${PYMONGO_3_9}
|
||||
- MONGODB=${MONGODB_3_4} PYMONGO=${PYMONGO_3_11}
|
||||
|
||||
matrix:
|
||||
|
||||
# Finish the build as soon as one job fails
|
||||
fast_finish: true
|
||||
|
||||
include:
|
||||
- python: 2.7
|
||||
env: MONGODB=${MONGODB_3_4} PYMONGO=${PYMONGO_3_4}
|
||||
- python: 3.7
|
||||
env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_6}
|
||||
- python: 3.7
|
||||
env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_9}
|
||||
- python: 3.7
|
||||
env: MONGODB=${MONGODB_3_6} PYMONGO=${PYMONGO_3_11}
|
||||
- python: 3.8
|
||||
env: MONGODB=${MONGODB_4_0} PYMONGO=${PYMONGO_3_11}
|
||||
|
||||
install:
|
||||
# Install Mongo
|
||||
@@ -60,29 +59,23 @@ install:
|
||||
# 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)
|
||||
- pip install pre-commit
|
||||
- pip install tox
|
||||
# tox dryrun to setup the tox venv (we run a mock test).
|
||||
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -a "-k=test_ci_placeholder"
|
||||
# Install black for Python v3.7 only.
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then pip install black; fi
|
||||
|
||||
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 Python 2.7 only
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == '3.7' ]]; then black --check .; else echo "black only runs on py37"; fi # Run black for Python 3.7 only
|
||||
# Run pre-commit hooks (black, flake8, etc) on entire codebase
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == $MAIN_PYTHON_VERSION ]]; then pre-commit run -a; else echo "pre-commit checks only runs on py37"; fi
|
||||
- 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/') -- -a "--cov=mongoengine"
|
||||
|
||||
# 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; else echo "coveralls only sent for py27"; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == $MAIN_PYTHON_VERSION ]]; then coveralls --verbose; else echo "coveralls only sent for py37"; fi
|
||||
|
||||
notifications:
|
||||
irc: irc.freenode.org#mongoengine
|
||||
@@ -104,11 +97,11 @@ deploy:
|
||||
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.
|
||||
# repo's builds running Python v3.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 = ${PYMONGO_3_9}) && ($MONGODB = ${MONGODB_3_4})
|
||||
python: 2.7
|
||||
condition: ($PYMONGO = ${PYMONGO_3_11}) && ($MONGODB = ${MONGODB_3_4})
|
||||
python: 3.7
|
||||
|
4
AUTHORS
4
AUTHORS
@@ -255,3 +255,7 @@ that much better:
|
||||
* 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)
|
||||
|
@@ -20,19 +20,43 @@ post to the `user group <http://groups.google.com/group/mongoengine-users>`
|
||||
Supported Interpreters
|
||||
----------------------
|
||||
|
||||
MongoEngine supports CPython 2.7 and newer. Language
|
||||
features not supported by all interpreters can not be used.
|
||||
The codebase is written in python 2 so you must be using python 2
|
||||
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
|
||||
`2to3 <http://docs.python.org/library/2to3.html>`_.
|
||||
MongoEngine supports CPython 3.5 and newer as well as Pypy3.
|
||||
Language features not supported by all interpreters can not be used.
|
||||
|
||||
Python3 codebase
|
||||
----------------------
|
||||
|
||||
Since 0.20, the codebase is exclusively Python 3.
|
||||
|
||||
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
|
||||
-----------
|
||||
|
||||
MongoEngine uses `black <https://github.com/python/black>`_ for code
|
||||
formatting.
|
||||
MongoEngine's codebase is formatted with `black <https://github.com/python/black>`_, 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.
|
||||
|
||||
To install all development tools, simply run the following commands:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ 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.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
20
README.rst
20
README.rst
@@ -16,6 +16,9 @@ MongoEngine
|
||||
:target: https://landscape.io/github/MongoEngine/mongoengine/master
|
||||
:alt: Code Health
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
About
|
||||
=====
|
||||
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
|
||||
@@ -26,15 +29,15 @@ an `API reference <https://mongoengine-odm.readthedocs.io/apireference.html>`_.
|
||||
|
||||
Supported MongoDB Versions
|
||||
==========================
|
||||
MongoEngine is currently tested against MongoDB v3.4 and v3.6. Future versions
|
||||
MongoEngine is currently tested against MongoDB v3.4, v3.6 and v4.0. Future versions
|
||||
should be supported as well, but aren't actively tested at the moment. Make
|
||||
sure to open an issue or submit a pull request if you experience any problems
|
||||
with MongoDB version > 3.6.
|
||||
with MongoDB version > 4.0.
|
||||
|
||||
Installation
|
||||
============
|
||||
We recommend the use of `virtualenv <https://virtualenv.pypa.io/>`_ and of
|
||||
`pip <https://pip.pypa.io/>`_. You can then use ``pip install -U mongoengine``.
|
||||
`pip <https://pip.pypa.io/>`_. You can then use ``python -m pip install -U mongoengine``.
|
||||
You may also have `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
|
||||
and thus you can use ``easy_install -U mongoengine``. Another option is
|
||||
`pipenv <https://docs.pipenv.org/>`_. You can then use ``pipenv install mongoengine``
|
||||
@@ -42,13 +45,14 @@ to both create the virtual environment and install the package. Otherwise, you c
|
||||
download the source from `GitHub <http://github.com/MongoEngine/mongoengine>`_ and
|
||||
run ``python setup.py install``.
|
||||
|
||||
The support for Python2 was dropped with MongoEngine 0.20.0
|
||||
|
||||
Dependencies
|
||||
============
|
||||
All of the dependencies can easily be installed via `pip <https://pip.pypa.io/>`_.
|
||||
All of the dependencies can easily be installed via `python -m pip <https://pip.pypa.io/>`_.
|
||||
At the very least, you'll need these two packages to use MongoEngine:
|
||||
|
||||
- pymongo>=3.4
|
||||
- six>=1.10.0
|
||||
|
||||
If you utilize a ``DateTimeField``, you might also use a more flexible date parser:
|
||||
|
||||
@@ -58,6 +62,10 @@ If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
|
||||
|
||||
- Pillow>=2.0.0
|
||||
|
||||
If you need to use signals:
|
||||
|
||||
- blinker>=1.3
|
||||
|
||||
Examples
|
||||
========
|
||||
Some simple examples of what MongoEngine code looks like:
|
||||
@@ -125,7 +133,7 @@ installed in your environment and then:
|
||||
.. code-block:: shell
|
||||
|
||||
# Install tox
|
||||
$ pip install tox
|
||||
$ python -m pip install tox
|
||||
# Run the test suites
|
||||
$ tox
|
||||
|
||||
|
@@ -4,12 +4,14 @@ import timeit
|
||||
def main():
|
||||
setup = """
|
||||
from pymongo import MongoClient
|
||||
|
||||
connection = MongoClient()
|
||||
connection.drop_database('mongoengine_benchmark_test')
|
||||
"""
|
||||
|
||||
stmt = """
|
||||
from pymongo import MongoClient
|
||||
|
||||
connection = MongoClient()
|
||||
|
||||
db = connection.mongoengine_benchmark_test
|
||||
@@ -56,6 +58,7 @@ myNoddys = noddy.find()
|
||||
|
||||
setup = """
|
||||
from pymongo import MongoClient
|
||||
|
||||
connection = MongoClient()
|
||||
connection.drop_database('mongoengine_benchmark_test')
|
||||
connection.close()
|
||||
|
@@ -6,10 +6,40 @@ Changelog
|
||||
Development
|
||||
===========
|
||||
- (Fill this out as you fix issues and develop your features).
|
||||
- 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 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 it's usually done) #2392
|
||||
- Add EnumField: ``mongoengine.fields.EnumField``
|
||||
- Refactoring - Remove useless code related to Document.__only_fields and Queryset.only_fields
|
||||
|
||||
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
|
||||
=================
|
||||
- Requires Pillow < 7.0.0 as it dropped Python2 support
|
||||
- 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
|
||||
|
||||
@@ -437,9 +467,6 @@ Changes in 0.8.3
|
||||
- Document.select_related() now respects ``db_alias`` (#377)
|
||||
- Reload uses shard_key if applicable (#384)
|
||||
- Dynamic fields are ordered based on creation and stored in _fields_ordered (#396)
|
||||
|
||||
**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)
|
||||
- Added Django 1.5 PY3 support (#392)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# MongoEngine documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Nov 22 18:14:13 2009.
|
||||
|
@@ -13,7 +13,7 @@ Help Wanted!
|
||||
|
||||
The MongoEngine team is looking for help contributing and maintaining a new
|
||||
Django extension for MongoEngine! If you have Django experience and would like
|
||||
to help contribute to the project, please get in touch on the
|
||||
`mailing list <http://groups.google.com/group/mongoengine-users>`_ or by
|
||||
to help contribute to the project, please get in touch on the
|
||||
`mailing list <http://groups.google.com/group/mongoengine-users>`_ or by
|
||||
simply contributing on
|
||||
`GitHub <https://github.com/MongoEngine/django-mongoengine>`_.
|
||||
|
@@ -10,4 +10,3 @@ If this is a requirement for your project, check the alternative: `uMongo`_ and
|
||||
|
||||
.. _uMongo: https://umongo.readthedocs.io/
|
||||
.. _MotorEngine: https://motorengine.readthedocs.io/
|
||||
|
||||
|
@@ -31,6 +31,8 @@ the :attr:`host` to
|
||||
|
||||
connect('project1', host='mongodb://localhost/database_name')
|
||||
|
||||
.. note:: URI containing SRV records (e.g mongodb+srv://server.example.com/) can be used as well as the :attr:`host`
|
||||
|
||||
.. note:: Database, username and password from URI string overrides
|
||||
corresponding parameters in :func:`~mongoengine.connect`: ::
|
||||
|
||||
@@ -86,7 +88,7 @@ using 3 different databases to store data::
|
||||
connect(alias='user-db-alias', db='user-db')
|
||||
connect(alias='book-db-alias', db='book-db')
|
||||
connect(alias='users-books-db-alias', db='users-books-db')
|
||||
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
|
@@ -76,6 +76,7 @@ are as follows:
|
||||
* :class:`~mongoengine.fields.EmailField`
|
||||
* :class:`~mongoengine.fields.EmbeddedDocumentField`
|
||||
* :class:`~mongoengine.fields.EmbeddedDocumentListField`
|
||||
* :class:`~mongoengine.fields.EnumField`
|
||||
* :class:`~mongoengine.fields.FileField`
|
||||
* :class:`~mongoengine.fields.FloatField`
|
||||
* :class:`~mongoengine.fields.GenericEmbeddedDocumentField`
|
||||
@@ -426,19 +427,6 @@ either a single field name, or a list or tuple of field names::
|
||||
first_name = StringField()
|
||||
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
|
||||
====================
|
||||
@@ -555,7 +543,6 @@ There are a few top level defaults for all indexes that can be set::
|
||||
'index_background': True,
|
||||
'index_cls': False,
|
||||
'auto_create_index': True,
|
||||
'index_drop_dups': True,
|
||||
}
|
||||
|
||||
|
||||
@@ -574,11 +561,6 @@ 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
|
||||
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
|
||||
-------------------------------------------
|
||||
|
@@ -41,35 +41,6 @@ already exist, then any changes will be updated atomically. For example::
|
||||
.. seealso::
|
||||
: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
|
||||
---------------
|
||||
If your document contains :class:`~mongoengine.fields.ReferenceField` or
|
||||
|
@@ -10,6 +10,7 @@ User Guide
|
||||
defining-documents
|
||||
document-instances
|
||||
querying
|
||||
validation
|
||||
gridfs
|
||||
signals
|
||||
text-indexes
|
||||
|
@@ -12,7 +12,7 @@ MongoEngine is available on PyPI, so you can use :program:`pip`:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install mongoengine
|
||||
$ python -m pip install mongoengine
|
||||
|
||||
Alternatively, if you don't have setuptools installed, `download it from PyPi
|
||||
<http://pypi.python.org/pypi/mongoengine/>`_ and run
|
||||
|
@@ -609,7 +609,7 @@ to push values with index::
|
||||
.. note::
|
||||
Currently only top level lists are handled, future versions of mongodb /
|
||||
pymongo plan to support nested positional operators. See `The $ positional
|
||||
operator <http://www.mongodb.org/display/DOCS/Updating#Updating-The%24positionaloperator>`_.
|
||||
operator <https://docs.mongodb.com/manual/tutorial/update-documents/#Updating-The%24positionaloperator>`_.
|
||||
|
||||
Server-side javascript execution
|
||||
================================
|
||||
|
@@ -44,8 +44,8 @@ Available signals include:
|
||||
|
||||
`post_save`
|
||||
Called within :meth:`~mongoengine.Document.save` after most actions
|
||||
(validation, insert/update, and cascades, but not clearing dirty flags) have
|
||||
completed successfully. Passed the additional boolean keyword argument
|
||||
(validation, insert/update, and cascades, but not clearing dirty flags) have
|
||||
completed successfully. Passed the additional boolean keyword argument
|
||||
`created` to indicate if the save was an insert or an update.
|
||||
|
||||
`pre_delete`
|
||||
|
@@ -8,7 +8,7 @@ After MongoDB 2.4 version, supports search documents by text indexes.
|
||||
Defining a Document with text index
|
||||
===================================
|
||||
Use the *$* prefix to set a text index, Look the declaration::
|
||||
|
||||
|
||||
class News(Document):
|
||||
title = StringField()
|
||||
content = StringField()
|
||||
@@ -35,10 +35,10 @@ Saving a document::
|
||||
content="Various improvements").save()
|
||||
|
||||
Next, start a text search using :attr:`QuerySet.search_text` method::
|
||||
|
||||
|
||||
document = News.objects.search_text('testing').first()
|
||||
document.title # may be: "Using mongodb text search"
|
||||
|
||||
|
||||
document = News.objects.search_text('released').first()
|
||||
document.title # may be: "MongoEngine 0.9 released"
|
||||
|
||||
|
123
docs/guide/validation.rst
Normal file
123
docs/guide/validation.rst
Normal file
@@ -0,0 +1,123 @@
|
||||
====================
|
||||
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
|
||||
|
@@ -7,7 +7,7 @@ MongoDB. To install it, simply run
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ pip install -U mongoengine
|
||||
$ python -m pip install -U mongoengine
|
||||
|
||||
:doc:`tutorial`
|
||||
A quick tutorial building a tumblelog to get you up and running with
|
||||
@@ -91,4 +91,3 @@ Indices and tables
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
3
docs/requirements.txt
Normal file
3
docs/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
pymongo>=3.11
|
||||
Sphinx==3.2.1
|
||||
sphinx-rtd-theme==0.5.0
|
@@ -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,
|
||||
simply use pip to install it like so::
|
||||
|
||||
$ pip install mongoengine
|
||||
$ python -m pip install mongoengine
|
||||
|
||||
Before we can start using MongoEngine, we need to tell it how to connect to our
|
||||
instance of :program:`mongod`. For this we use the :func:`~mongoengine.connect`
|
||||
|
@@ -52,7 +52,7 @@ rename its occurrences.
|
||||
This release includes a major rehaul of MongoEngine's code quality and
|
||||
introduces a few breaking changes. It also touches many different parts of
|
||||
the package and although all the changes have been tested and scrutinized,
|
||||
you're encouraged to thorougly test the upgrade.
|
||||
you're encouraged to thoroughly test the upgrade.
|
||||
|
||||
First breaking change involves renaming `ConnectionError` to `MongoEngineConnectionError`.
|
||||
If you import or catch this exception, you'll need to rename it in your code.
|
||||
@@ -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: ::
|
||||
|
||||
pip uninstall pymongo
|
||||
pip uninstall mongoengine
|
||||
pip install pymongo==2.8
|
||||
pip install mongoengine
|
||||
python -m pip uninstall pymongo
|
||||
python -m pip uninstall mongoengine
|
||||
python -m pip install pymongo==2.8
|
||||
python -m pip install mongoengine
|
||||
|
||||
0.8.7
|
||||
*****
|
||||
@@ -153,7 +153,7 @@ inherited classes like so: ::
|
||||
|
||||
# 4. Remove indexes
|
||||
info = collection.index_information()
|
||||
indexes_to_drop = [key for key, value in info.iteritems()
|
||||
indexes_to_drop = [key for key, value in info.items()
|
||||
if '_types' in dict(value['key'])]
|
||||
for index in indexes_to_drop:
|
||||
collection.drop_index(index)
|
||||
|
@@ -28,7 +28,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
VERSION = (0, 19, 1)
|
||||
VERSION = (0, 20, 0)
|
||||
|
||||
|
||||
def get_version():
|
||||
|
@@ -1,8 +1,6 @@
|
||||
import weakref
|
||||
|
||||
from bson import DBRef
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import DoesNotExist, MultipleObjectsReturned
|
||||
@@ -53,7 +51,7 @@ class BaseDict(dict):
|
||||
if isinstance(instance, BaseDocument):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
super(BaseDict, self).__init__(dict_items)
|
||||
super().__init__(dict_items)
|
||||
|
||||
def get(self, key, default=None):
|
||||
# get does not use __getitem__ by default so we must override it as well
|
||||
@@ -63,18 +61,18 @@ class BaseDict(dict):
|
||||
return default
|
||||
|
||||
def __getitem__(self, key):
|
||||
value = super(BaseDict, self).__getitem__(key)
|
||||
value = super().__getitem__(key)
|
||||
|
||||
EmbeddedDocument = _import_class("EmbeddedDocument")
|
||||
if isinstance(value, EmbeddedDocument) and value._instance is None:
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
value = BaseDict(value, None, "%s.%s" % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value = BaseDict(value, None, "{}.{}".format(self._name, key))
|
||||
super().__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, list) and not isinstance(value, BaseList):
|
||||
value = BaseList(value, None, "%s.%s" % (self._name, key))
|
||||
super(BaseDict, self).__setitem__(key, value)
|
||||
value = BaseList(value, None, "{}.{}".format(self._name, key))
|
||||
super().__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
@@ -99,7 +97,7 @@ class BaseDict(dict):
|
||||
def _mark_as_changed(self, key=None):
|
||||
if hasattr(self._instance, "_mark_as_changed"):
|
||||
if key:
|
||||
self._instance._mark_as_changed("%s.%s" % (self._name, key))
|
||||
self._instance._mark_as_changed("{}.{}".format(self._name, key))
|
||||
else:
|
||||
self._instance._mark_as_changed(self._name)
|
||||
|
||||
@@ -117,13 +115,13 @@ class BaseList(list):
|
||||
if isinstance(instance, BaseDocument):
|
||||
self._instance = weakref.proxy(instance)
|
||||
self._name = name
|
||||
super(BaseList, self).__init__(list_items)
|
||||
super().__init__(list_items)
|
||||
|
||||
def __getitem__(self, key):
|
||||
# change index to positive value because MongoDB does not support negative one
|
||||
if isinstance(key, int) and key < 0:
|
||||
key = len(self) + key
|
||||
value = super(BaseList, self).__getitem__(key)
|
||||
value = super().__getitem__(key)
|
||||
|
||||
if isinstance(key, slice):
|
||||
# When receiving a slice operator, we don't convert the structure and bind
|
||||
@@ -135,19 +133,18 @@ class BaseList(list):
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, dict) and not isinstance(value, BaseDict):
|
||||
# Replace dict by BaseDict
|
||||
value = BaseDict(value, None, "%s.%s" % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value = BaseDict(value, None, "{}.{}".format(self._name, key))
|
||||
super().__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
elif isinstance(value, list) and not isinstance(value, BaseList):
|
||||
# Replace list by BaseList
|
||||
value = BaseList(value, None, "%s.%s" % (self._name, key))
|
||||
super(BaseList, self).__setitem__(key, value)
|
||||
value = BaseList(value, None, "{}.{}".format(self._name, key))
|
||||
super().__setitem__(key, value)
|
||||
value._instance = self._instance
|
||||
return value
|
||||
|
||||
def __iter__(self):
|
||||
for v in super(BaseList, self).__iter__():
|
||||
yield v
|
||||
yield from super().__iter__()
|
||||
|
||||
def __getstate__(self):
|
||||
self.instance = None
|
||||
@@ -165,7 +162,7 @@ class BaseList(list):
|
||||
# instead, we simply marks the whole list as changed
|
||||
changed_key = None
|
||||
|
||||
result = super(BaseList, self).__setitem__(key, value)
|
||||
result = super().__setitem__(key, value)
|
||||
self._mark_as_changed(changed_key)
|
||||
return result
|
||||
|
||||
@@ -180,30 +177,19 @@ class BaseList(list):
|
||||
__iadd__ = mark_as_changed_wrapper(list.__iadd__)
|
||||
__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):
|
||||
if hasattr(self._instance, "_mark_as_changed"):
|
||||
if key:
|
||||
self._instance._mark_as_changed("%s.%s" % (self._name, key % len(self)))
|
||||
if key is not None:
|
||||
self._instance._mark_as_changed(
|
||||
"{}.{}".format(self._name, key % len(self))
|
||||
)
|
||||
else:
|
||||
self._instance._mark_as_changed(self._name)
|
||||
|
||||
|
||||
class EmbeddedDocumentList(BaseList):
|
||||
def __init__(self, list_items, instance, name):
|
||||
super(EmbeddedDocumentList, self).__init__(list_items, instance, name)
|
||||
super().__init__(list_items, instance, name)
|
||||
self._instance = instance
|
||||
|
||||
@classmethod
|
||||
@@ -213,7 +199,7 @@ class EmbeddedDocumentList(BaseList):
|
||||
"""
|
||||
for key, expected_value in kwargs.items():
|
||||
doc_val = getattr(embedded_doc, key)
|
||||
if doc_val != expected_value and six.text_type(doc_val) != expected_value:
|
||||
if doc_val != expected_value and str(doc_val) != expected_value:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -229,7 +215,7 @@ class EmbeddedDocumentList(BaseList):
|
||||
Filters the list by only including embedded documents with the
|
||||
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
|
||||
|
||||
:param kwargs: The keyword arguments corresponding to the fields to
|
||||
@@ -303,11 +289,11 @@ class EmbeddedDocumentList(BaseList):
|
||||
|
||||
def create(self, **values):
|
||||
"""
|
||||
Creates a new embedded document and saves it to the database.
|
||||
Creates a new instance of the EmbeddedDocument and appends it to this EmbeddedDocumentList.
|
||||
|
||||
.. note::
|
||||
The embedded document changes are not automatically saved
|
||||
to the database after calling this method.
|
||||
the instance of the EmbeddedDocument is not automatically saved to the database.
|
||||
You still need to call .save() on the parent Document.
|
||||
|
||||
:param values: A dictionary of values for the embedded document.
|
||||
:return: The new embedded document instance.
|
||||
@@ -368,13 +354,13 @@ class EmbeddedDocumentList(BaseList):
|
||||
return len(values)
|
||||
|
||||
|
||||
class StrictDict(object):
|
||||
class StrictDict:
|
||||
__slots__ = ()
|
||||
_special_fields = {"get", "pop", "iteritems", "items", "keys", "create"}
|
||||
_classes = {}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in iteritems(kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getitem__(self, key):
|
||||
@@ -422,13 +408,13 @@ class StrictDict(object):
|
||||
return (key for key in self.__slots__ if hasattr(self, key))
|
||||
|
||||
def __len__(self):
|
||||
return len(list(iteritems(self)))
|
||||
return len(list(self.items()))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.items() == other.items()
|
||||
return list(self.items()) == list(other.items())
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.items() != other.items()
|
||||
return not (self == other)
|
||||
|
||||
@classmethod
|
||||
def create(cls, allowed_keys):
|
||||
@@ -443,7 +429,7 @@ class StrictDict(object):
|
||||
|
||||
def __repr__(self):
|
||||
return "{%s}" % ", ".join(
|
||||
'"{0!s}": {1!r}'.format(k, v) for k, v in self.items()
|
||||
'"{!s}": {!r}'.format(k, v) for k, v in self.items()
|
||||
)
|
||||
|
||||
cls._classes[allowed_keys] = SpecificStrictDict
|
||||
@@ -468,9 +454,7 @@ class LazyReference(DBRef):
|
||||
self.document_type = document_type
|
||||
self._cached_doc = cached_doc
|
||||
self.passthrough = passthrough
|
||||
super(LazyReference, self).__init__(
|
||||
self.document_type._get_collection_name(), pk
|
||||
)
|
||||
super().__init__(self.document_type._get_collection_name(), pk)
|
||||
|
||||
def __getitem__(self, name):
|
||||
if not self.passthrough:
|
||||
@@ -488,4 +472,4 @@ class LazyReference(DBRef):
|
||||
raise AttributeError()
|
||||
|
||||
def __repr__(self):
|
||||
return "<LazyReference(%s, %r)>" % (self.document_type, self.pk)
|
||||
return "<LazyReference({}, {!r})>".format(self.document_type, self.pk)
|
||||
|
@@ -1,11 +1,10 @@
|
||||
import copy
|
||||
|
||||
import numbers
|
||||
from functools import partial
|
||||
|
||||
from bson import DBRef, ObjectId, SON, json_util
|
||||
import pymongo
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import signals
|
||||
from mongoengine.base.common import get_document
|
||||
@@ -25,14 +24,13 @@ from mongoengine.errors import (
|
||||
OperationError,
|
||||
ValidationError,
|
||||
)
|
||||
from mongoengine.python_support import Hashable
|
||||
|
||||
__all__ = ("BaseDocument", "NON_FIELD_ERRORS")
|
||||
|
||||
NON_FIELD_ERRORS = "__all__"
|
||||
|
||||
|
||||
class BaseDocument(object):
|
||||
class BaseDocument:
|
||||
# TODO simplify how `_changed_fields` is used.
|
||||
# Currently, handling of `_changed_fields` seems unnecessarily convoluted:
|
||||
# 1. `BaseDocument` defines `_changed_fields` in its `__slots__`, yet it's
|
||||
@@ -66,8 +64,6 @@ class BaseDocument(object):
|
||||
It may contain additional reserved keywords, e.g. "__auto_convert".
|
||||
:param __auto_convert: If True, supplied values will be converted
|
||||
to Python-type values via each field's `to_python` method.
|
||||
:param __only_fields: A set of fields that have been loaded for
|
||||
this document. Empty if all fields have been loaded.
|
||||
:param _created: Indicates whether this is a brand new document
|
||||
or whether it's already been persisted before. Defaults to true.
|
||||
"""
|
||||
@@ -82,8 +78,6 @@ class BaseDocument(object):
|
||||
|
||||
__auto_convert = values.pop("__auto_convert", True)
|
||||
|
||||
__only_fields = set(values.pop("__only_fields", values))
|
||||
|
||||
_created = values.pop("_created", True)
|
||||
|
||||
signals.pre_init.send(self.__class__, document=self, values=values)
|
||||
@@ -92,10 +86,10 @@ class BaseDocument(object):
|
||||
# if so raise an Exception.
|
||||
if not self._dynamic and (self._meta.get("strict", True) or _created):
|
||||
_undefined_fields = set(values.keys()) - set(
|
||||
self._fields.keys() + ["id", "pk", "_cls", "_text_score"]
|
||||
list(self._fields.keys()) + ["id", "pk", "_cls", "_text_score"]
|
||||
)
|
||||
if _undefined_fields:
|
||||
msg = ('The fields "{0}" do not exist on the document "{1}"').format(
|
||||
msg = ('The fields "{}" do not exist on the document "{}"').format(
|
||||
_undefined_fields, self._class_name
|
||||
)
|
||||
raise FieldDoesNotExist(msg)
|
||||
@@ -108,10 +102,8 @@ class BaseDocument(object):
|
||||
self._dynamic_fields = SON()
|
||||
|
||||
# Assign default values to the instance.
|
||||
# We set default values only for fields loaded from DB. See
|
||||
# https://github.com/mongoengine/mongoengine/issues/399 for more info.
|
||||
for key, field in iteritems(self._fields):
|
||||
if self._db_field_map.get(key, key) in __only_fields:
|
||||
for key, field in self._fields.items():
|
||||
if self._db_field_map.get(key, key) in values:
|
||||
continue
|
||||
value = getattr(self, key, None)
|
||||
setattr(self, key, value)
|
||||
@@ -119,25 +111,22 @@ class BaseDocument(object):
|
||||
if "_cls" not in values:
|
||||
self._cls = self._class_name
|
||||
|
||||
# Set passed values after initialisation
|
||||
if self._dynamic:
|
||||
dynamic_data = {}
|
||||
for key, value in iteritems(values):
|
||||
if key in self._fields or key == "_id":
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
# Set actual values
|
||||
dynamic_data = {}
|
||||
FileField = _import_class("FileField")
|
||||
for key, value in values.items():
|
||||
key = self._reverse_db_field_map.get(key, key)
|
||||
field = self._fields.get(key)
|
||||
if field or key in ("id", "pk", "_cls"):
|
||||
if __auto_convert and value is not None:
|
||||
if field and not isinstance(field, FileField):
|
||||
value = field.to_python(value)
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
if self._dynamic:
|
||||
dynamic_data[key] = value
|
||||
else:
|
||||
FileField = _import_class("FileField")
|
||||
for key, value in iteritems(values):
|
||||
key = self._reverse_db_field_map.get(key, key)
|
||||
if key in self._fields or key in ("id", "pk", "_cls"):
|
||||
if __auto_convert and value is not None:
|
||||
field = self._fields.get(key)
|
||||
if field and not isinstance(field, FileField):
|
||||
value = field.to_python(value)
|
||||
setattr(self, key, value)
|
||||
else:
|
||||
# For strict Document
|
||||
self._data[key] = value
|
||||
|
||||
# Set any get_<field>_display methods
|
||||
@@ -145,7 +134,7 @@ class BaseDocument(object):
|
||||
|
||||
if self._dynamic:
|
||||
self._dynamic_lock = False
|
||||
for key, value in iteritems(dynamic_data):
|
||||
for key, value in dynamic_data.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
# Flag initialised
|
||||
@@ -163,7 +152,7 @@ class BaseDocument(object):
|
||||
default = default()
|
||||
setattr(self, field_name, default)
|
||||
else:
|
||||
super(BaseDocument, self).__delattr__(*args, **kwargs)
|
||||
super().__delattr__(*args, **kwargs)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# Handle dynamic data only if an initialised dynamic document
|
||||
@@ -210,9 +199,9 @@ class BaseDocument(object):
|
||||
and self__created
|
||||
and name == self._meta.get("id_field")
|
||||
):
|
||||
super(BaseDocument, self).__setattr__("_created", False)
|
||||
super().__setattr__("_created", False)
|
||||
|
||||
super(BaseDocument, self).__setattr__(name, value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __getstate__(self):
|
||||
data = {}
|
||||
@@ -288,16 +277,13 @@ class BaseDocument(object):
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
u = "[Bad Unicode data]"
|
||||
repr_type = str if u is None else type(u)
|
||||
return repr_type("<%s: %s>" % (self.__class__.__name__, u))
|
||||
return repr_type("<{}: {}>".format(self.__class__.__name__, u))
|
||||
|
||||
def __str__(self):
|
||||
# TODO this could be simpler?
|
||||
if hasattr(self, "__unicode__"):
|
||||
if six.PY3:
|
||||
return self.__unicode__()
|
||||
else:
|
||||
return six.text_type(self).encode("utf-8")
|
||||
return six.text_type("%s object" % self.__class__.__name__)
|
||||
return self.__unicode__()
|
||||
return "%s object" % self.__class__.__name__
|
||||
|
||||
def __eq__(self, other):
|
||||
if (
|
||||
@@ -319,7 +305,8 @@ class BaseDocument(object):
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Hook for doing document level data cleaning before validation is run.
|
||||
Hook for doing document level data cleaning (usually validation or assignment)
|
||||
before validation is run.
|
||||
|
||||
Any ValidationError raised by this method will not be associated with
|
||||
a particular field; it will have a special-case association with the
|
||||
@@ -446,7 +433,7 @@ class BaseDocument(object):
|
||||
pk = self.pk
|
||||
elif self._instance and hasattr(self._instance, "pk"):
|
||||
pk = self._instance.pk
|
||||
message = "ValidationError (%s:%s) " % (self._class_name, pk)
|
||||
message = "ValidationError ({}:{}) ".format(self._class_name, pk)
|
||||
raise ValidationError(message, errors=errors)
|
||||
|
||||
def to_json(self, *args, **kwargs):
|
||||
@@ -519,7 +506,7 @@ class BaseDocument(object):
|
||||
if "." in key:
|
||||
key, rest = key.split(".", 1)
|
||||
key = self._db_field_map.get(key, key)
|
||||
key = "%s.%s" % (key, rest)
|
||||
key = "{}.{}".format(key, rest)
|
||||
else:
|
||||
key = self._db_field_map.get(key, key)
|
||||
|
||||
@@ -542,6 +529,9 @@ class BaseDocument(object):
|
||||
"""Using _get_changed_fields iterate and remove any fields that
|
||||
are marked as changed.
|
||||
"""
|
||||
ReferenceField = _import_class("ReferenceField")
|
||||
GenericReferenceField = _import_class("GenericReferenceField")
|
||||
|
||||
for changed in self._get_changed_fields():
|
||||
parts = changed.split(".")
|
||||
data = self
|
||||
@@ -554,7 +544,8 @@ class BaseDocument(object):
|
||||
elif isinstance(data, dict):
|
||||
data = data.get(part, None)
|
||||
else:
|
||||
data = getattr(data, part, None)
|
||||
field_name = data._reverse_db_field_map.get(part, part)
|
||||
data = getattr(data, field_name, None)
|
||||
|
||||
if not isinstance(data, LazyReference) and hasattr(
|
||||
data, "_changed_fields"
|
||||
@@ -563,10 +554,40 @@ class BaseDocument(object):
|
||||
continue
|
||||
|
||||
data._changed_fields = []
|
||||
elif isinstance(data, (list, tuple, dict)):
|
||||
if hasattr(data, "field") and isinstance(
|
||||
data.field, (ReferenceField, GenericReferenceField)
|
||||
):
|
||||
continue
|
||||
BaseDocument._nestable_types_clear_changed_fields(data)
|
||||
|
||||
self._changed_fields = []
|
||||
|
||||
def _nestable_types_changed_fields(self, changed_fields, base_key, data):
|
||||
@staticmethod
|
||||
def _nestable_types_clear_changed_fields(data):
|
||||
"""Inspect nested data for changed fields
|
||||
|
||||
:param data: data to inspect for changes
|
||||
"""
|
||||
Document = _import_class("Document")
|
||||
|
||||
# Loop list / dict fields as they contain documents
|
||||
# Determine the iterator to use
|
||||
if not hasattr(data, "items"):
|
||||
iterator = enumerate(data)
|
||||
else:
|
||||
iterator = data.items()
|
||||
|
||||
for index_or_key, value in iterator:
|
||||
if hasattr(value, "_get_changed_fields") and not isinstance(
|
||||
value, Document
|
||||
): # don't follow references
|
||||
value._clear_changed_fields()
|
||||
elif isinstance(value, (list, tuple, dict)):
|
||||
BaseDocument._nestable_types_clear_changed_fields(value)
|
||||
|
||||
@staticmethod
|
||||
def _nestable_types_changed_fields(changed_fields, base_key, data):
|
||||
"""Inspect nested data for changed fields
|
||||
|
||||
:param changed_fields: Previously collected changed fields
|
||||
@@ -578,10 +599,10 @@ class BaseDocument(object):
|
||||
if not hasattr(data, "items"):
|
||||
iterator = enumerate(data)
|
||||
else:
|
||||
iterator = iteritems(data)
|
||||
iterator = data.items()
|
||||
|
||||
for index_or_key, value in iterator:
|
||||
item_key = "%s%s." % (base_key, index_or_key)
|
||||
item_key = "{}{}.".format(base_key, index_or_key)
|
||||
# don't check anything lower if this key is already marked
|
||||
# as changed.
|
||||
if item_key[:-1] in changed_fields:
|
||||
@@ -589,9 +610,11 @@ class BaseDocument(object):
|
||||
|
||||
if hasattr(value, "_get_changed_fields"):
|
||||
changed = value._get_changed_fields()
|
||||
changed_fields += ["%s%s" % (item_key, k) for k in changed if k]
|
||||
changed_fields += ["{}{}".format(item_key, k) for k in changed if k]
|
||||
elif isinstance(value, (list, tuple, dict)):
|
||||
self._nestable_types_changed_fields(changed_fields, item_key, value)
|
||||
BaseDocument._nestable_types_changed_fields(
|
||||
changed_fields, item_key, value
|
||||
)
|
||||
|
||||
def _get_changed_fields(self):
|
||||
"""Return a list of all fields that have explicitly been changed.
|
||||
@@ -620,7 +643,7 @@ class BaseDocument(object):
|
||||
if isinstance(data, EmbeddedDocument):
|
||||
# Find all embedded fields that have been changed
|
||||
changed = data._get_changed_fields()
|
||||
changed_fields += ["%s%s" % (key, k) for k in changed if k]
|
||||
changed_fields += ["{}{}".format(key, k) for k in changed if k]
|
||||
elif isinstance(data, (list, tuple, dict)):
|
||||
if hasattr(field, "field") and isinstance(
|
||||
field.field, (ReferenceField, GenericReferenceField)
|
||||
@@ -670,7 +693,7 @@ class BaseDocument(object):
|
||||
del set_data["_id"]
|
||||
|
||||
# Determine if any changed items were actually unset.
|
||||
for path, value in set_data.items():
|
||||
for path, value in list(set_data.items()):
|
||||
if value or isinstance(
|
||||
value, (numbers.Number, bool)
|
||||
): # Account for 0 and True that are truthy
|
||||
@@ -726,11 +749,8 @@ class BaseDocument(object):
|
||||
return cls._meta.get("collection", None)
|
||||
|
||||
@classmethod
|
||||
def _from_son(cls, son, _auto_dereference=True, only_fields=None, created=False):
|
||||
def _from_son(cls, son, _auto_dereference=True, created=False):
|
||||
"""Create an instance of a Document (subclass) from a PyMongo SON."""
|
||||
if not only_fields:
|
||||
only_fields = []
|
||||
|
||||
if son and not isinstance(son, dict):
|
||||
raise ValueError(
|
||||
"The source SON object needs to be of type 'dict' but a '%s' was found"
|
||||
@@ -744,7 +764,7 @@ class BaseDocument(object):
|
||||
# Convert SON to a data dict, making sure each key is a string and
|
||||
# corresponds to the right db field.
|
||||
data = {}
|
||||
for key, value in iteritems(son):
|
||||
for key, value in son.items():
|
||||
key = str(key)
|
||||
key = cls._db_field_map.get(key, key)
|
||||
data[key] = value
|
||||
@@ -759,7 +779,7 @@ class BaseDocument(object):
|
||||
if not _auto_dereference:
|
||||
fields = copy.deepcopy(fields)
|
||||
|
||||
for field_name, field in iteritems(fields):
|
||||
for field_name, field in fields.items():
|
||||
field._auto_dereference = _auto_dereference
|
||||
if field.db_field in data:
|
||||
value = data[field.db_field]
|
||||
@@ -774,21 +794,18 @@ class BaseDocument(object):
|
||||
|
||||
if errors_dict:
|
||||
errors = "\n".join(
|
||||
["Field '%s' - %s" % (k, v) for k, v in errors_dict.items()]
|
||||
["Field '{}' - {}".format(k, v) for k, v in errors_dict.items()]
|
||||
)
|
||||
msg = "Invalid data to create a `%s` instance.\n%s" % (
|
||||
cls._class_name,
|
||||
errors,
|
||||
msg = "Invalid data to create a `{}` instance.\n{}".format(
|
||||
cls._class_name, errors,
|
||||
)
|
||||
raise InvalidDocumentError(msg)
|
||||
|
||||
# In STRICT documents, remove any keys that aren't in cls._fields
|
||||
if cls.STRICT:
|
||||
data = {k: v for k, v in iteritems(data) if k in cls._fields}
|
||||
data = {k: v for k, v in data.items() if k in cls._fields}
|
||||
|
||||
obj = cls(
|
||||
__auto_convert=False, _created=created, __only_fields=only_fields, **data
|
||||
)
|
||||
obj = cls(__auto_convert=False, _created=created, **data)
|
||||
obj._changed_fields = []
|
||||
if not _auto_dereference:
|
||||
obj._fields = fields
|
||||
@@ -831,7 +848,7 @@ class BaseDocument(object):
|
||||
@classmethod
|
||||
def _build_index_spec(cls, spec):
|
||||
"""Build a PyMongo index spec from a MongoEngine index spec."""
|
||||
if isinstance(spec, six.string_types):
|
||||
if isinstance(spec, str):
|
||||
spec = {"fields": [spec]}
|
||||
elif isinstance(spec, (list, tuple)):
|
||||
spec = {"fields": list(spec)}
|
||||
@@ -928,7 +945,7 @@ class BaseDocument(object):
|
||||
|
||||
# Add any unique_with fields to the back of the index spec
|
||||
if field.unique_with:
|
||||
if isinstance(field.unique_with, six.string_types):
|
||||
if isinstance(field.unique_with, str):
|
||||
field.unique_with = [field.unique_with]
|
||||
|
||||
# Convert unique_with field names to real field names
|
||||
@@ -949,7 +966,8 @@ class BaseDocument(object):
|
||||
|
||||
# Add the new index to the list
|
||||
fields = [
|
||||
("%s%s" % (namespace, f), pymongo.ASCENDING) for f in unique_fields
|
||||
("{}{}".format(namespace, f), pymongo.ASCENDING)
|
||||
for f in unique_fields
|
||||
]
|
||||
index = {"fields": fields, "unique": True, "sparse": sparse}
|
||||
unique_indexes.append(index)
|
||||
@@ -1006,7 +1024,7 @@ class BaseDocument(object):
|
||||
elif field._geo_index:
|
||||
field_name = field.db_field
|
||||
if parent_field:
|
||||
field_name = "%s.%s" % (parent_field, field_name)
|
||||
field_name = "{}.{}".format(parent_field, field_name)
|
||||
geo_indices.append({"fields": [(field_name, field._geo_index)]})
|
||||
|
||||
return geo_indices
|
||||
@@ -1175,9 +1193,6 @@ class BaseDocument(object):
|
||||
else [value]
|
||||
)
|
||||
return sep.join(
|
||||
[
|
||||
six.text_type(dict(field.choices).get(val, val))
|
||||
for val in values or []
|
||||
]
|
||||
[str(dict(field.choices).get(val, val)) for val in values or []]
|
||||
)
|
||||
return value
|
||||
|
@@ -4,8 +4,6 @@ import weakref
|
||||
|
||||
from bson import DBRef, ObjectId, SON
|
||||
import pymongo
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine.base.common import UPDATE_OPERATORS
|
||||
from mongoengine.base.datastructures import BaseDict, BaseList, EmbeddedDocumentList
|
||||
@@ -15,7 +13,7 @@ from mongoengine.errors import DeprecatedError, ValidationError
|
||||
__all__ = ("BaseField", "ComplexBaseField", "ObjectIdField", "GeoJsonBaseField")
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
class BaseField:
|
||||
"""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.
|
||||
|
||||
@@ -36,7 +34,6 @@ class BaseField(object):
|
||||
def __init__(
|
||||
self,
|
||||
db_field=None,
|
||||
name=None,
|
||||
required=False,
|
||||
default=None,
|
||||
unique=False,
|
||||
@@ -51,7 +48,6 @@ class BaseField(object):
|
||||
"""
|
||||
:param db_field: The database field to store this field in
|
||||
(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
|
||||
value or not. Defaults to False.
|
||||
:param default: (optional) The default value for this field if no value
|
||||
@@ -75,11 +71,8 @@ class BaseField(object):
|
||||
existing attributes. Common metadata includes `verbose_name` and
|
||||
`help_text`.
|
||||
"""
|
||||
self.db_field = (db_field or name) if not primary_key else "_id"
|
||||
self.db_field = db_field 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.default = default
|
||||
self.unique = bool(unique or unique_with)
|
||||
@@ -92,13 +85,11 @@ class BaseField(object):
|
||||
self._owner_document = None
|
||||
|
||||
# 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, six.string_types
|
||||
):
|
||||
if self.db_field is not None and not isinstance(self.db_field, str):
|
||||
raise TypeError("db_field should be a string.")
|
||||
|
||||
# Make sure db_field doesn't contain any forbidden characters.
|
||||
if isinstance(self.db_field, six.string_types) and (
|
||||
if isinstance(self.db_field, str) and (
|
||||
"." in self.db_field
|
||||
or "\0" in self.db_field
|
||||
or self.db_field.startswith("$")
|
||||
@@ -221,14 +212,12 @@ class BaseField(object):
|
||||
# Choices which are other types of Documents
|
||||
if isinstance(value, (Document, EmbeddedDocument)):
|
||||
if not any(isinstance(value, c) for c in choice_list):
|
||||
self.error(
|
||||
"Value must be an instance of %s" % (six.text_type(choice_list))
|
||||
)
|
||||
self.error("Value must be an instance of %s" % (choice_list))
|
||||
# Choices which are types other than Documents
|
||||
else:
|
||||
values = value if isinstance(value, (list, tuple)) else [value]
|
||||
if len(set(values) - set(choice_list)):
|
||||
self.error("Value must be one of %s" % six.text_type(choice_list))
|
||||
self.error("Value must be one of %s" % str(choice_list))
|
||||
|
||||
def _validate(self, value, **kwargs):
|
||||
# Check the Choices Constraint
|
||||
@@ -316,7 +305,7 @@ class ComplexBaseField(BaseField):
|
||||
if hasattr(instance._data[self.name], "_dereferenced"):
|
||||
instance._data[self.name]._dereferenced = True
|
||||
|
||||
value = super(ComplexBaseField, self).__get__(instance, owner)
|
||||
value = super().__get__(instance, owner)
|
||||
|
||||
# Convert lists / values so we can watch for any changes on them
|
||||
if isinstance(value, (list, tuple)):
|
||||
@@ -345,7 +334,7 @@ class ComplexBaseField(BaseField):
|
||||
|
||||
def to_python(self, value):
|
||||
"""Convert a MongoDB-compatible type to a Python type."""
|
||||
if isinstance(value, six.string_types):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
if hasattr(value, "to_python"):
|
||||
@@ -399,7 +388,7 @@ class ComplexBaseField(BaseField):
|
||||
EmbeddedDocument = _import_class("EmbeddedDocument")
|
||||
GenericReferenceField = _import_class("GenericReferenceField")
|
||||
|
||||
if isinstance(value, six.string_types):
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
if hasattr(value, "to_mongo"):
|
||||
@@ -423,11 +412,11 @@ class ComplexBaseField(BaseField):
|
||||
if self.field:
|
||||
value_dict = {
|
||||
key: self.field._to_mongo_safe_call(item, use_db_field, fields)
|
||||
for key, item in iteritems(value)
|
||||
for key, item in value.items()
|
||||
}
|
||||
else:
|
||||
value_dict = {}
|
||||
for k, v in iteritems(value):
|
||||
for k, v in value.items():
|
||||
if isinstance(v, Document):
|
||||
# We need the id from the saved object to create the DBRef
|
||||
if v.pk is None:
|
||||
@@ -466,8 +455,8 @@ class ComplexBaseField(BaseField):
|
||||
"""If field is provided ensure the value is valid."""
|
||||
errors = {}
|
||||
if self.field:
|
||||
if hasattr(value, "iteritems") or hasattr(value, "items"):
|
||||
sequence = iteritems(value)
|
||||
if hasattr(value, "items"):
|
||||
sequence = value.items()
|
||||
else:
|
||||
sequence = enumerate(value)
|
||||
for k, v in sequence:
|
||||
@@ -480,7 +469,9 @@ class ComplexBaseField(BaseField):
|
||||
|
||||
if errors:
|
||||
field_class = self.field.__class__.__name__
|
||||
self.error("Invalid %s item (%s)" % (field_class, value), errors=errors)
|
||||
self.error(
|
||||
"Invalid {} item ({})".format(field_class, value), errors=errors
|
||||
)
|
||||
# Don't allow empty values if required
|
||||
if self.required and not value:
|
||||
self.error("Field is required and cannot be empty")
|
||||
@@ -513,10 +504,9 @@ class ObjectIdField(BaseField):
|
||||
def to_mongo(self, value):
|
||||
if not isinstance(value, ObjectId):
|
||||
try:
|
||||
return ObjectId(six.text_type(value))
|
||||
return ObjectId(str(value))
|
||||
except Exception as e:
|
||||
# e.message attribute has been deprecated since Python 2.6
|
||||
self.error(six.text_type(e))
|
||||
self.error(str(e))
|
||||
return value
|
||||
|
||||
def prepare_query_value(self, op, value):
|
||||
@@ -524,9 +514,9 @@ class ObjectIdField(BaseField):
|
||||
|
||||
def validate(self, value):
|
||||
try:
|
||||
ObjectId(six.text_type(value))
|
||||
ObjectId(str(value))
|
||||
except Exception:
|
||||
self.error("Invalid Object ID")
|
||||
self.error("Invalid ObjectID")
|
||||
|
||||
|
||||
class GeoJsonBaseField(BaseField):
|
||||
@@ -546,14 +536,14 @@ class GeoJsonBaseField(BaseField):
|
||||
self._name = "%sField" % self._type
|
||||
if not auto_index:
|
||||
self._geo_index = False
|
||||
super(GeoJsonBaseField, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, value):
|
||||
"""Validate the GeoJson object based on its type."""
|
||||
if isinstance(value, dict):
|
||||
if set(value.keys()) == {"type", "coordinates"}:
|
||||
if value["type"] != self._type:
|
||||
self.error('%s type must be "%s"' % (self._name, self._type))
|
||||
self.error('{} type must be "{}"'.format(self._name, self._type))
|
||||
return self.validate(value["coordinates"])
|
||||
else:
|
||||
self.error(
|
||||
|
@@ -1,9 +1,6 @@
|
||||
import itertools
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from six import iteritems, itervalues
|
||||
|
||||
from mongoengine.base.common import _document_registry
|
||||
from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
|
||||
from mongoengine.common import _import_class
|
||||
@@ -25,7 +22,7 @@ class DocumentMetaclass(type):
|
||||
# TODO lower complexity of this method
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
flattened_bases = mcs._get_bases(bases)
|
||||
super_new = super(DocumentMetaclass, mcs).__new__
|
||||
super_new = super().__new__
|
||||
|
||||
# If a base class just call super
|
||||
metaclass = attrs.get("my_metaclass")
|
||||
@@ -69,7 +66,7 @@ class DocumentMetaclass(type):
|
||||
# Standard object mixin - merge in any Fields
|
||||
if not hasattr(base, "_meta"):
|
||||
base_fields = {}
|
||||
for attr_name, attr_value in iteritems(base.__dict__):
|
||||
for attr_name, attr_value in base.__dict__.items():
|
||||
if not isinstance(attr_value, BaseField):
|
||||
continue
|
||||
attr_value.name = attr_name
|
||||
@@ -81,7 +78,7 @@ class DocumentMetaclass(type):
|
||||
|
||||
# Discover any document fields
|
||||
field_names = {}
|
||||
for attr_name, attr_value in iteritems(attrs):
|
||||
for attr_name, attr_value in attrs.items():
|
||||
if not isinstance(attr_value, BaseField):
|
||||
continue
|
||||
attr_value.name = attr_name
|
||||
@@ -111,9 +108,7 @@ class DocumentMetaclass(type):
|
||||
|
||||
attrs["_fields_ordered"] = tuple(
|
||||
i[1]
|
||||
for i in sorted(
|
||||
(v.creation_counter, v.name) for v in itervalues(doc_fields)
|
||||
)
|
||||
for i in sorted((v.creation_counter, v.name) for v in doc_fields.values())
|
||||
)
|
||||
|
||||
#
|
||||
@@ -173,24 +168,8 @@ class DocumentMetaclass(type):
|
||||
# Add class to the _document_registry
|
||||
_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
|
||||
for field in itervalues(new_class._fields):
|
||||
for field in new_class._fields.values():
|
||||
f = field
|
||||
if f.owner_document is None:
|
||||
f.owner_document = new_class
|
||||
@@ -252,8 +231,7 @@ class DocumentMetaclass(type):
|
||||
if base is object:
|
||||
continue
|
||||
yield base
|
||||
for child_base in mcs.__get_bases(base.__bases__):
|
||||
yield child_base
|
||||
yield from mcs.__get_bases(base.__bases__)
|
||||
|
||||
@classmethod
|
||||
def _import_classes(mcs):
|
||||
@@ -271,7 +249,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
flattened_bases = mcs._get_bases(bases)
|
||||
super_new = super(TopLevelDocumentMetaclass, mcs).__new__
|
||||
super_new = super().__new__
|
||||
|
||||
# Set default _meta data if base class, otherwise get user defined meta
|
||||
if attrs.get("my_metaclass") == TopLevelDocumentMetaclass:
|
||||
@@ -284,7 +262,6 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
"indexes": [], # indexes to be ensured at runtime
|
||||
"id_field": None,
|
||||
"index_background": False,
|
||||
"index_drop_dups": False,
|
||||
"index_opts": None,
|
||||
"delete_rules": None,
|
||||
# allow_inheritance can be True, False, and None. True means
|
||||
@@ -399,7 +376,7 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
new_class.objects = QuerySetManager()
|
||||
|
||||
# Validate the fields and set primary key if needed
|
||||
for field_name, field in iteritems(new_class._fields):
|
||||
for field_name, field in new_class._fields.items():
|
||||
if field.primary_key:
|
||||
# Ensure only one primary key is set
|
||||
current_pk = new_class._meta.get("id_field")
|
||||
@@ -462,8 +439,8 @@ class TopLevelDocumentMetaclass(DocumentMetaclass):
|
||||
|
||||
id_basename, id_db_basename, i = ("auto_id", "_auto_id", 0)
|
||||
for i in itertools.count():
|
||||
id_name = "{0}_{1}".format(id_basename, i)
|
||||
id_db_name = "{0}_{1}".format(id_db_basename, i)
|
||||
id_name = "{}_{}".format(id_basename, i)
|
||||
id_db_name = "{}_{}".format(id_db_basename, i)
|
||||
if id_name not in existing_fields and id_db_name not in existing_db_fields:
|
||||
return id_name, id_db_name
|
||||
|
||||
@@ -476,7 +453,7 @@ class MetaDict(dict):
|
||||
_merge_options = ("indexes",)
|
||||
|
||||
def merge(self, new_options):
|
||||
for k, v in iteritems(new_options):
|
||||
for k, v in new_options.items():
|
||||
if k in self._merge_options:
|
||||
self[k] = self.get(k, []) + v
|
||||
else:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import re
|
||||
|
||||
|
||||
class LazyRegexCompiler(object):
|
||||
class LazyRegexCompiler:
|
||||
"""Descriptor to allow lazy compilation of regex"""
|
||||
|
||||
def __init__(self, pattern, flags=0):
|
||||
|
@@ -1,6 +1,5 @@
|
||||
from pymongo import MongoClient, ReadPreference, uri_parser
|
||||
from pymongo.database import _check_name
|
||||
import six
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_CONNECTION_NAME",
|
||||
@@ -39,8 +38,8 @@ def _check_db_name(name):
|
||||
"""Check if a database name is valid.
|
||||
This functionality is copied from pymongo Database class constructor.
|
||||
"""
|
||||
if not isinstance(name, six.string_types):
|
||||
raise TypeError("name must be an instance of %s" % six.string_types)
|
||||
if not isinstance(name, str):
|
||||
raise TypeError("name must be an instance of %s" % str)
|
||||
elif name != "$external":
|
||||
_check_name(name)
|
||||
|
||||
@@ -93,7 +92,7 @@ def _get_connection_settings(
|
||||
conn_host = conn_settings["host"]
|
||||
|
||||
# Host can be a list or a string, so if string, force to a list.
|
||||
if isinstance(conn_host, six.string_types):
|
||||
if isinstance(conn_host, str):
|
||||
conn_host = [conn_host]
|
||||
|
||||
resolved_hosts = []
|
||||
@@ -148,7 +147,7 @@ def _get_connection_settings(
|
||||
# TODO simplify the code below once we drop support for
|
||||
# PyMongo v3.4.
|
||||
read_pf_mode = uri_options["readpreference"]
|
||||
if isinstance(read_pf_mode, six.string_types):
|
||||
if isinstance(read_pf_mode, str):
|
||||
read_pf_mode = read_pf_mode.lower()
|
||||
for preference in read_preferences:
|
||||
if (
|
||||
@@ -318,7 +317,7 @@ def _create_connection(alias, connection_class, **connection_settings):
|
||||
try:
|
||||
return connection_class(**connection_settings)
|
||||
except Exception as e:
|
||||
raise ConnectionFailure("Cannot connect to database %s :\n%s" % (alias, e))
|
||||
raise ConnectionFailure("Cannot connect to database {} :\n{}".format(alias, e))
|
||||
|
||||
|
||||
def _find_existing_connection(connection_settings):
|
||||
@@ -396,8 +395,8 @@ def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
|
||||
|
||||
if new_conn_settings != prev_conn_setting:
|
||||
err_msg = (
|
||||
u"A different connection with alias `{}` was already "
|
||||
u"registered. Use disconnect() first"
|
||||
"A different connection with alias `{}` was already "
|
||||
"registered. Use disconnect() first"
|
||||
).format(alias)
|
||||
raise ConnectionFailure(err_msg)
|
||||
else:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
from pymongo.read_concern import ReadConcern
|
||||
from pymongo.write_concern import WriteConcern
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.connection import DEFAULT_CONNECTION_NAME, get_db
|
||||
@@ -14,10 +14,11 @@ __all__ = (
|
||||
"no_sub_classes",
|
||||
"query_counter",
|
||||
"set_write_concern",
|
||||
"set_read_write_concern",
|
||||
)
|
||||
|
||||
|
||||
class switch_db(object):
|
||||
class switch_db:
|
||||
"""switch_db alias context manager.
|
||||
|
||||
Example ::
|
||||
@@ -58,7 +59,7 @@ class switch_db(object):
|
||||
self.cls._collection = self.collection
|
||||
|
||||
|
||||
class switch_collection(object):
|
||||
class switch_collection:
|
||||
"""switch_collection alias context manager.
|
||||
|
||||
Example ::
|
||||
@@ -100,7 +101,7 @@ class switch_collection(object):
|
||||
self.cls._get_collection_name = self.ori_get_collection_name
|
||||
|
||||
|
||||
class no_dereference(object):
|
||||
class no_dereference:
|
||||
"""no_dereference context manager.
|
||||
|
||||
Turns off all dereferencing in Documents for the duration of the context
|
||||
@@ -123,7 +124,7 @@ class no_dereference(object):
|
||||
|
||||
self.deref_fields = [
|
||||
k
|
||||
for k, v in iteritems(self.cls._fields)
|
||||
for k, v in self.cls._fields.items()
|
||||
if isinstance(v, (ReferenceField, GenericReferenceField, ComplexBaseField))
|
||||
]
|
||||
|
||||
@@ -140,7 +141,7 @@ class no_dereference(object):
|
||||
return self.cls
|
||||
|
||||
|
||||
class no_sub_classes(object):
|
||||
class no_sub_classes:
|
||||
"""no_sub_classes context manager.
|
||||
|
||||
Only returns instances of this class and no sub (inherited) classes::
|
||||
@@ -168,7 +169,7 @@ class no_sub_classes(object):
|
||||
self.cls._subclasses = self.cls_initial_subclasses
|
||||
|
||||
|
||||
class query_counter(object):
|
||||
class query_counter:
|
||||
"""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,
|
||||
resetting the db.system.profile collection at the beginning of the context and counting the new entries.
|
||||
@@ -235,7 +236,7 @@ class query_counter(object):
|
||||
|
||||
def __repr__(self):
|
||||
"""repr query_counter as the number of queries."""
|
||||
return u"%s" % self._get_count()
|
||||
return "%s" % self._get_count()
|
||||
|
||||
def _get_count(self):
|
||||
"""Get the number of queries by counting the current number of entries in db.system.profile
|
||||
@@ -257,3 +258,21 @@ def set_write_concern(collection, write_concerns):
|
||||
combined_concerns = dict(collection.write_concern.document.items())
|
||||
combined_concerns.update(write_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),
|
||||
)
|
||||
|
@@ -1,6 +1,4 @@
|
||||
from bson import DBRef, SON
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine.base import (
|
||||
BaseDict,
|
||||
@@ -16,7 +14,7 @@ from mongoengine.fields import DictField, ListField, MapField, ReferenceField
|
||||
from mongoengine.queryset import QuerySet
|
||||
|
||||
|
||||
class DeReference(object):
|
||||
class DeReference:
|
||||
def __call__(self, items, max_depth=1, instance=None, name=None):
|
||||
"""
|
||||
Cheaply dereferences the items to a set depth.
|
||||
@@ -30,7 +28,7 @@ class DeReference(object):
|
||||
:class:`~mongoengine.base.ComplexBaseField`
|
||||
:param get: A boolean determining if being called by __get__
|
||||
"""
|
||||
if items is None or isinstance(items, six.string_types):
|
||||
if items is None or isinstance(items, str):
|
||||
return items
|
||||
|
||||
# cheapest way to convert a queryset to a list
|
||||
@@ -79,7 +77,7 @@ class DeReference(object):
|
||||
|
||||
def _get_items_from_dict(items):
|
||||
new_items = {}
|
||||
for k, v in iteritems(items):
|
||||
for k, v in items.items():
|
||||
value = v
|
||||
if isinstance(v, list):
|
||||
value = _get_items_from_list(v)
|
||||
@@ -120,7 +118,7 @@ class DeReference(object):
|
||||
depth += 1
|
||||
for item in iterator:
|
||||
if isinstance(item, (Document, EmbeddedDocument)):
|
||||
for field_name, field in iteritems(item._fields):
|
||||
for field_name, field in item._fields.items():
|
||||
v = item._data.get(field_name, None)
|
||||
if isinstance(v, LazyReference):
|
||||
# LazyReference inherits DBRef but should not be dereferenced here !
|
||||
@@ -136,7 +134,7 @@ class DeReference(object):
|
||||
getattr(field, "field", None), "document_type", None
|
||||
)
|
||||
references = self._find_references(v, depth)
|
||||
for key, refs in iteritems(references):
|
||||
for key, refs in references.items():
|
||||
if isinstance(
|
||||
field_cls, (Document, TopLevelDocumentMetaclass)
|
||||
):
|
||||
@@ -153,7 +151,7 @@ class DeReference(object):
|
||||
)
|
||||
elif isinstance(item, (dict, list, tuple)) and depth - 1 <= self.max_depth:
|
||||
references = self._find_references(item, depth - 1)
|
||||
for key, refs in iteritems(references):
|
||||
for key, refs in references.items():
|
||||
reference_map.setdefault(key, set()).update(refs)
|
||||
|
||||
return reference_map
|
||||
@@ -162,7 +160,7 @@ class DeReference(object):
|
||||
"""Fetch all references and convert to their document objects
|
||||
"""
|
||||
object_map = {}
|
||||
for collection, dbrefs in iteritems(self.reference_map):
|
||||
for collection, dbrefs in self.reference_map.items():
|
||||
|
||||
# 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))
|
||||
@@ -174,7 +172,7 @@ class DeReference(object):
|
||||
dbref for dbref in dbrefs if (col_name, dbref) not in object_map
|
||||
]
|
||||
references = collection.objects.in_bulk(refs)
|
||||
for key, doc in iteritems(references):
|
||||
for key, doc in references.items():
|
||||
object_map[(col_name, key)] = doc
|
||||
else: # Generic reference: use the refs data to convert to document
|
||||
if isinstance(doc_type, (ListField, DictField, MapField)):
|
||||
@@ -250,7 +248,7 @@ class DeReference(object):
|
||||
data = []
|
||||
else:
|
||||
is_list = False
|
||||
iterator = iteritems(items)
|
||||
iterator = items.items()
|
||||
data = {}
|
||||
|
||||
depth += 1
|
||||
@@ -274,14 +272,12 @@ class DeReference(object):
|
||||
(v["_ref"].collection, v["_ref"].id), v
|
||||
)
|
||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||
item_name = six.text_type("{0}.{1}.{2}").format(
|
||||
name, k, field_name
|
||||
)
|
||||
item_name = "{}.{}.{}".format(name, k, field_name)
|
||||
data[k]._data[field_name] = self._attach_objects(
|
||||
v, depth, instance=instance, name=item_name
|
||||
)
|
||||
elif isinstance(v, (dict, list, tuple)) and depth <= self.max_depth:
|
||||
item_name = "%s.%s" % (name, k) if name else name
|
||||
item_name = "{}.{}".format(name, k) if name else name
|
||||
data[k] = self._attach_objects(
|
||||
v, depth - 1, instance=instance, name=item_name
|
||||
)
|
||||
|
@@ -4,8 +4,6 @@ import warnings
|
||||
from bson.dbref import DBRef
|
||||
import pymongo
|
||||
from pymongo.read_preferences import ReadPreference
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import signals
|
||||
from mongoengine.base import (
|
||||
@@ -44,7 +42,7 @@ def includes_cls(fields):
|
||||
"""Helper function used for ensuring and comparing indexes."""
|
||||
first_field = None
|
||||
if len(fields):
|
||||
if isinstance(fields[0], six.string_types):
|
||||
if isinstance(fields[0], str):
|
||||
first_field = fields[0]
|
||||
elif isinstance(fields[0], (list, tuple)) and len(fields[0]):
|
||||
first_field = fields[0][0]
|
||||
@@ -55,8 +53,8 @@ class InvalidCollectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
|
||||
"""A :class:`~mongoengine.Document` that isn't stored in its own
|
||||
class EmbeddedDocument(BaseDocument, metaclass=DocumentMetaclass):
|
||||
r"""A :class:`~mongoengine.Document` that isn't stored in its own
|
||||
collection. :class:`~mongoengine.EmbeddedDocument`\ s should be used as
|
||||
fields on :class:`~mongoengine.Document`\ s through the
|
||||
:class:`~mongoengine.EmbeddedDocumentField` field type.
|
||||
@@ -71,7 +69,6 @@ class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
|
||||
|
||||
__slots__ = ("_instance",)
|
||||
|
||||
# The __metaclass__ attribute is removed by 2to3 when running with Python3
|
||||
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
|
||||
my_metaclass = DocumentMetaclass
|
||||
|
||||
@@ -82,7 +79,7 @@ class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
|
||||
__hash__ = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EmbeddedDocument, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._instance = None
|
||||
self._changed_fields = []
|
||||
|
||||
@@ -95,7 +92,7 @@ class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def to_mongo(self, *args, **kwargs):
|
||||
data = super(EmbeddedDocument, self).to_mongo(*args, **kwargs)
|
||||
data = super().to_mongo(*args, **kwargs)
|
||||
|
||||
# remove _id from the SON if it's in it and it's None
|
||||
if "_id" in data and data["_id"] is None:
|
||||
@@ -104,7 +101,7 @@ class EmbeddedDocument(six.with_metaclass(DocumentMetaclass, BaseDocument)):
|
||||
return data
|
||||
|
||||
|
||||
class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
class Document(BaseDocument, metaclass=TopLevelDocumentMetaclass):
|
||||
"""The base class used for defining the structure and properties of
|
||||
collections of documents stored in MongoDB. Inherit from this class, and
|
||||
add fields as class attributes to define a document's structure.
|
||||
@@ -156,7 +153,6 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
in the :attr:`meta` dictionary.
|
||||
"""
|
||||
|
||||
# The __metaclass__ attribute is removed by 2to3 when running with Python3
|
||||
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
|
||||
my_metaclass = TopLevelDocumentMetaclass
|
||||
|
||||
@@ -260,7 +256,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
return db.create_collection(collection_name, **opts)
|
||||
|
||||
def to_mongo(self, *args, **kwargs):
|
||||
data = super(Document, self).to_mongo(*args, **kwargs)
|
||||
data = super().to_mongo(*args, **kwargs)
|
||||
|
||||
# If '_id' is None, try and set it from self._data. If that
|
||||
# doesn't exist either, remove '_id' from the SON completely.
|
||||
@@ -332,7 +328,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
):
|
||||
"""Save the :class:`~mongoengine.Document` to the database. If the
|
||||
document already exists, it will be updated, otherwise it will be
|
||||
created.
|
||||
created. Returns the saved object instance.
|
||||
|
||||
:param force_insert: only try to create a new document, don't allow
|
||||
updates of existing documents.
|
||||
@@ -431,16 +427,16 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
self.cascade_save(**kwargs)
|
||||
|
||||
except pymongo.errors.DuplicateKeyError as err:
|
||||
message = u"Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % six.text_type(err))
|
||||
message = "Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % err)
|
||||
except pymongo.errors.OperationFailure as err:
|
||||
message = "Could not save document (%s)"
|
||||
if re.match("^E1100[01] duplicate key", six.text_type(err)):
|
||||
if re.match("^E1100[01] duplicate key", str(err)):
|
||||
# E11000 - duplicate key error index
|
||||
# E11001 - duplicate key on update
|
||||
message = u"Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % six.text_type(err))
|
||||
raise OperationError(message % six.text_type(err))
|
||||
message = "Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % err)
|
||||
raise OperationError(message % err)
|
||||
|
||||
# Make sure we store the PK on this document now that it's saved
|
||||
id_field = self._meta["id_field"]
|
||||
@@ -468,9 +464,9 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
# insert_one will provoke UniqueError alongside save does not
|
||||
# therefore, it need to catch and call replace_one.
|
||||
if "_id" in doc:
|
||||
raw_object = wc_collection.find_one_and_replace(
|
||||
{"_id": doc["_id"]}, doc
|
||||
)
|
||||
select_dict = {"_id": doc["_id"]}
|
||||
select_dict = self._integrate_shard_key(doc, select_dict)
|
||||
raw_object = wc_collection.find_one_and_replace(select_dict, doc)
|
||||
if raw_object:
|
||||
return doc["_id"]
|
||||
|
||||
@@ -493,6 +489,23 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
|
||||
return update_doc
|
||||
|
||||
def _integrate_shard_key(self, doc, select_dict):
|
||||
"""Integrates the collection's shard key to the `select_dict`, which will be used for the query.
|
||||
The value from the shard key is taken from the `doc` and finally the select_dict is returned.
|
||||
"""
|
||||
|
||||
# Need to add shard key to query, or you get an error
|
||||
shard_key = self._meta.get("shard_key", tuple())
|
||||
for k in shard_key:
|
||||
path = self._lookup_field(k.split("."))
|
||||
actual_key = [p.db_field for p in path]
|
||||
val = doc
|
||||
for ak in actual_key:
|
||||
val = val[ak]
|
||||
select_dict[".".join(actual_key)] = val
|
||||
|
||||
return select_dict
|
||||
|
||||
def _save_update(self, doc, save_condition, write_concern):
|
||||
"""Update an existing document.
|
||||
|
||||
@@ -508,15 +521,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
|
||||
select_dict["_id"] = object_id
|
||||
|
||||
# Need to add shard key to query, or you get an error
|
||||
shard_key = self._meta.get("shard_key", tuple())
|
||||
for k in shard_key:
|
||||
path = self._lookup_field(k.split("."))
|
||||
actual_key = [p.db_field for p in path]
|
||||
val = doc
|
||||
for ak in actual_key:
|
||||
val = val[ak]
|
||||
select_dict[".".join(actual_key)] = val
|
||||
select_dict = self._integrate_shard_key(doc, select_dict)
|
||||
|
||||
update_doc = self._get_update_doc()
|
||||
if update_doc:
|
||||
@@ -559,7 +564,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
if not getattr(ref, "_changed_fields", True):
|
||||
continue
|
||||
|
||||
ref_id = "%s,%s" % (ref.__class__.__name__, str(ref._data))
|
||||
ref_id = "{},{}".format(ref.__class__.__name__, str(ref._data))
|
||||
if ref and ref_id not in _refs:
|
||||
_refs.append(ref_id)
|
||||
kwargs["_refs"] = _refs
|
||||
@@ -634,7 +639,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
|
||||
# Delete FileFields separately
|
||||
FileField = _import_class("FileField")
|
||||
for name, field in iteritems(self._fields):
|
||||
for name, field in self._fields.items():
|
||||
if isinstance(field, FileField):
|
||||
getattr(self, name).delete()
|
||||
|
||||
@@ -643,7 +648,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
write_concern=write_concern, _from_doc_delete=True
|
||||
)
|
||||
except pymongo.errors.OperationFailure as err:
|
||||
message = u"Could not delete document (%s)" % err.message
|
||||
message = "Could not delete document (%s)" % err.args
|
||||
raise OperationError(message)
|
||||
signals.post_delete.send(self.__class__, document=self, **signal_kwargs)
|
||||
|
||||
@@ -851,17 +856,13 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
index_spec = cls._build_index_spec(keys)
|
||||
index_spec = index_spec.copy()
|
||||
fields = index_spec.pop("fields")
|
||||
drop_dups = kwargs.get("drop_dups", False)
|
||||
if drop_dups:
|
||||
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
index_spec["background"] = background
|
||||
index_spec.update(kwargs)
|
||||
|
||||
return cls._get_collection().create_index(fields, **index_spec)
|
||||
|
||||
@classmethod
|
||||
def ensure_index(cls, key_or_list, drop_dups=False, background=False, **kwargs):
|
||||
def ensure_index(cls, key_or_list, background=False, **kwargs):
|
||||
"""Ensure that the given indexes are in place. Deprecated in favour
|
||||
of create_index.
|
||||
|
||||
@@ -869,12 +870,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
construct a multi-field index); keys may be prefixed with a **+**
|
||||
or a **-** to determine the index ordering
|
||||
:param background: Allows index creation in the background
|
||||
:param drop_dups: Was removed/ignored with MongoDB >2.7.5. The value
|
||||
will be removed if PyMongo3+ is used
|
||||
"""
|
||||
if drop_dups:
|
||||
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return cls.create_index(key_or_list, background=background, **kwargs)
|
||||
|
||||
@classmethod
|
||||
@@ -887,12 +883,8 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
`auto_create_index` to False in the documents meta data
|
||||
"""
|
||||
background = cls._meta.get("index_background", False)
|
||||
drop_dups = cls._meta.get("index_drop_dups", False)
|
||||
index_opts = cls._meta.get("index_opts") or {}
|
||||
index_cls = cls._meta.get("index_cls", True)
|
||||
if drop_dups:
|
||||
msg = "drop_dups is deprecated and is removed when using PyMongo 3+."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
|
||||
collection = cls._get_collection()
|
||||
# 746: when connection is via mongos, the read preference is not necessarily an indication that
|
||||
@@ -936,7 +928,7 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
|
||||
@classmethod
|
||||
def list_indexes(cls):
|
||||
""" Lists all of the indexes that should be created for given
|
||||
"""Lists all of the indexes that should be created for given
|
||||
collection. It includes all the indexes from super- and sub-classes.
|
||||
"""
|
||||
if cls._meta.get("abstract"):
|
||||
@@ -992,16 +984,16 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
indexes.append(index)
|
||||
|
||||
# finish up by appending { '_id': 1 } and { '_cls': 1 }, if needed
|
||||
if [(u"_id", 1)] not in indexes:
|
||||
indexes.append([(u"_id", 1)])
|
||||
if [("_id", 1)] not in indexes:
|
||||
indexes.append([("_id", 1)])
|
||||
if cls._meta.get("index_cls", True) and cls._meta.get("allow_inheritance"):
|
||||
indexes.append([(u"_cls", 1)])
|
||||
indexes.append([("_cls", 1)])
|
||||
|
||||
return indexes
|
||||
|
||||
@classmethod
|
||||
def compare_indexes(cls):
|
||||
""" Compares the indexes defined in MongoEngine with the ones
|
||||
"""Compares the indexes defined in MongoEngine with the ones
|
||||
existing in the database. Returns any missing/extra indexes.
|
||||
"""
|
||||
|
||||
@@ -1019,19 +1011,19 @@ class Document(six.with_metaclass(TopLevelDocumentMetaclass, BaseDocument)):
|
||||
extra = [index for index in existing if index not in required]
|
||||
|
||||
# if { _cls: 1 } is missing, make sure it's *really* necessary
|
||||
if [(u"_cls", 1)] in missing:
|
||||
if [("_cls", 1)] in missing:
|
||||
cls_obsolete = False
|
||||
for index in existing:
|
||||
if includes_cls(index) and index not in extra:
|
||||
cls_obsolete = True
|
||||
break
|
||||
if cls_obsolete:
|
||||
missing.remove([(u"_cls", 1)])
|
||||
missing.remove([("_cls", 1)])
|
||||
|
||||
return {"missing": missing, "extra": extra}
|
||||
|
||||
|
||||
class DynamicDocument(six.with_metaclass(TopLevelDocumentMetaclass, Document)):
|
||||
class DynamicDocument(Document, metaclass=TopLevelDocumentMetaclass):
|
||||
"""A Dynamic Document class allowing flexible, expandable and uncontrolled
|
||||
schemas. As a :class:`~mongoengine.Document` subclass, acts in the same
|
||||
way as an ordinary document but has expanded style properties. Any data
|
||||
@@ -1045,7 +1037,6 @@ class DynamicDocument(six.with_metaclass(TopLevelDocumentMetaclass, Document)):
|
||||
There is one caveat on Dynamic Documents: undeclared fields cannot start with `_`
|
||||
"""
|
||||
|
||||
# The __metaclass__ attribute is removed by 2to3 when running with Python3
|
||||
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
|
||||
my_metaclass = TopLevelDocumentMetaclass
|
||||
|
||||
@@ -1060,16 +1051,15 @@ class DynamicDocument(six.with_metaclass(TopLevelDocumentMetaclass, Document)):
|
||||
setattr(self, field_name, None)
|
||||
self._dynamic_fields[field_name].null = False
|
||||
else:
|
||||
super(DynamicDocument, self).__delattr__(*args, **kwargs)
|
||||
super().__delattr__(*args, **kwargs)
|
||||
|
||||
|
||||
class DynamicEmbeddedDocument(six.with_metaclass(DocumentMetaclass, EmbeddedDocument)):
|
||||
class DynamicEmbeddedDocument(EmbeddedDocument, metaclass=DocumentMetaclass):
|
||||
"""A Dynamic Embedded Document class allowing flexible, expandable and
|
||||
uncontrolled schemas. See :class:`~mongoengine.DynamicDocument` for more
|
||||
information about dynamic documents.
|
||||
"""
|
||||
|
||||
# The __metaclass__ attribute is removed by 2to3 when running with Python3
|
||||
# my_metaclass is defined so that metaclass can be queried in Python 2 & 3
|
||||
my_metaclass = DocumentMetaclass
|
||||
|
||||
@@ -1089,7 +1079,7 @@ class DynamicEmbeddedDocument(six.with_metaclass(DocumentMetaclass, EmbeddedDocu
|
||||
setattr(self, field_name, None)
|
||||
|
||||
|
||||
class MapReduceDocument(object):
|
||||
class MapReduceDocument:
|
||||
"""A document returned from a map/reduce query.
|
||||
|
||||
:param collection: An instance of :class:`~pymongo.Collection`
|
||||
|
@@ -1,7 +1,5 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
__all__ = (
|
||||
"NotRegistered",
|
||||
@@ -87,24 +85,24 @@ class ValidationError(AssertionError):
|
||||
_message = None
|
||||
|
||||
def __init__(self, message="", **kwargs):
|
||||
super(ValidationError, self).__init__(message)
|
||||
super().__init__(message)
|
||||
self.errors = kwargs.get("errors", {})
|
||||
self.field_name = kwargs.get("field_name")
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return six.text_type(self.message)
|
||||
return str(self.message)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s,)" % (self.__class__.__name__, self.message)
|
||||
return "{}({},)".format(self.__class__.__name__, self.message)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
message = super(ValidationError, self).__getattribute__(name)
|
||||
message = super().__getattribute__(name)
|
||||
if name == "message":
|
||||
if self.field_name:
|
||||
message = "%s" % message
|
||||
if self.errors:
|
||||
message = "%s(%s)" % (message, self._format_errors())
|
||||
message = "{}({})".format(message, self._format_errors())
|
||||
return message
|
||||
|
||||
def _get_message(self):
|
||||
@@ -126,12 +124,12 @@ class ValidationError(AssertionError):
|
||||
def build_dict(source):
|
||||
errors_dict = {}
|
||||
if isinstance(source, dict):
|
||||
for field_name, error in iteritems(source):
|
||||
for field_name, error in source.items():
|
||||
errors_dict[field_name] = build_dict(error)
|
||||
elif isinstance(source, ValidationError) and source.errors:
|
||||
return build_dict(source.errors)
|
||||
else:
|
||||
return six.text_type(source)
|
||||
return str(source)
|
||||
|
||||
return errors_dict
|
||||
|
||||
@@ -147,15 +145,15 @@ class ValidationError(AssertionError):
|
||||
if isinstance(value, list):
|
||||
value = " ".join([generate_key(k) for k in value])
|
||||
elif isinstance(value, dict):
|
||||
value = " ".join([generate_key(v, k) for k, v in iteritems(value)])
|
||||
value = " ".join([generate_key(v, k) for k, v in value.items()])
|
||||
|
||||
results = "%s.%s" % (prefix, value) if prefix else value
|
||||
results = "{}.{}".format(prefix, value) if prefix else value
|
||||
return results
|
||||
|
||||
error_dict = defaultdict(list)
|
||||
for k, v in iteritems(self.to_dict()):
|
||||
for k, v in self.to_dict().items():
|
||||
error_dict[generate_key(v)].append(k)
|
||||
return " ".join(["%s: %s" % (k, v) for k, v in iteritems(error_dict)])
|
||||
return " ".join(["{}: {}".format(k, v) for k, v in error_dict.items()])
|
||||
|
||||
|
||||
class DeprecatedError(Exception):
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ MONGODB_36 = (3, 6)
|
||||
|
||||
|
||||
def get_mongodb_version():
|
||||
"""Return the version of the connected mongoDB (first 2 digits)
|
||||
"""Return the version of the default connected mongoDB (first 2 digits)
|
||||
|
||||
:return: tuple(int, int)
|
||||
"""
|
||||
|
@@ -2,6 +2,7 @@
|
||||
Helper functions, constants, and types to aid with PyMongo v2.7 - v3.x support.
|
||||
"""
|
||||
import pymongo
|
||||
from pymongo.errors import OperationFailure
|
||||
|
||||
_PYMONGO_37 = (3, 7)
|
||||
|
||||
@@ -10,13 +11,41 @@ PYMONGO_VERSION = tuple(pymongo.version_tuple[:2])
|
||||
IS_PYMONGO_GTE_37 = PYMONGO_VERSION >= _PYMONGO_37
|
||||
|
||||
|
||||
def count_documents(collection, filter):
|
||||
"""Pymongo>3.7 deprecates count in favour of count_documents"""
|
||||
def count_documents(
|
||||
collection, filter, skip=None, limit=None, hint=None, collation=None
|
||||
):
|
||||
"""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:
|
||||
return collection.count_documents(filter)
|
||||
else:
|
||||
count = collection.find(filter).count()
|
||||
return count
|
||||
try:
|
||||
return collection.count_documents(filter=filter, **kwargs)
|
||||
except OperationFailure:
|
||||
# OperationFailure - accounts for some operators that used to work
|
||||
# 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):
|
||||
|
@@ -1,23 +0,0 @@
|
||||
"""
|
||||
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
|
@@ -1,24 +1,27 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import copy
|
||||
import itertools
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from bson import SON, json_util
|
||||
from bson.code import Code
|
||||
import pymongo
|
||||
import pymongo.errors
|
||||
from pymongo.collection import ReturnDocument
|
||||
from pymongo.common import validate_read_preference
|
||||
import six
|
||||
from six import iteritems
|
||||
from pymongo.read_concern import ReadConcern
|
||||
|
||||
from mongoengine import signals
|
||||
from mongoengine.base import get_document
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.context_managers import set_write_concern, switch_db
|
||||
from mongoengine.context_managers import (
|
||||
set_read_write_concern,
|
||||
set_write_concern,
|
||||
switch_db,
|
||||
)
|
||||
from mongoengine.errors import (
|
||||
BulkWriteError,
|
||||
InvalidQueryError,
|
||||
@@ -26,6 +29,7 @@ from mongoengine.errors import (
|
||||
NotUniqueError,
|
||||
OperationError,
|
||||
)
|
||||
from mongoengine.pymongo_support import count_documents
|
||||
from mongoengine.queryset import transform
|
||||
from mongoengine.queryset.field_list import QueryFieldList
|
||||
from mongoengine.queryset.visitor import Q, QNode
|
||||
@@ -41,7 +45,7 @@ DENY = 3
|
||||
PULL = 4
|
||||
|
||||
|
||||
class BaseQuerySet(object):
|
||||
class BaseQuerySet:
|
||||
"""A set of results returned from a query. Wraps a MongoDB cursor,
|
||||
providing :class:`~mongoengine.Document` objects as the results.
|
||||
"""
|
||||
@@ -60,8 +64,8 @@ class BaseQuerySet(object):
|
||||
self._ordering = None
|
||||
self._snapshot = False
|
||||
self._timeout = True
|
||||
self._slave_okay = False
|
||||
self._read_preference = None
|
||||
self._read_concern = None
|
||||
self._iter = False
|
||||
self._scalar = []
|
||||
self._none = False
|
||||
@@ -80,13 +84,20 @@ class BaseQuerySet(object):
|
||||
self._cursor_obj = None
|
||||
self._limit = None
|
||||
self._skip = None
|
||||
|
||||
self._hint = -1 # Using -1 as None is a valid value for hint
|
||||
self._collation = None
|
||||
self._batch_size = None
|
||||
self.only_fields = []
|
||||
self._max_time_ms = None
|
||||
self._comment = None
|
||||
|
||||
# Hack - As people expect cursor[5:5] to return
|
||||
# an empty result set. It's hard to do that right, though, because the
|
||||
# server uses limit(0) to mean 'no limit'. So we set _empty
|
||||
# in that case and check for it when iterating. We also unset
|
||||
# it anytime we change _limit. Inspired by how it is done in pymongo.Cursor
|
||||
self._empty = False
|
||||
|
||||
def __call__(self, q_obj=None, **query):
|
||||
"""Filter the selected documents by calling the
|
||||
:class:`~mongoengine.queryset.QuerySet` with a query.
|
||||
@@ -159,6 +170,7 @@ class BaseQuerySet(object):
|
||||
[<User: User object>, <User: User object>]
|
||||
"""
|
||||
queryset = self.clone()
|
||||
queryset._empty = False
|
||||
|
||||
# Handle a slice
|
||||
if isinstance(key, slice):
|
||||
@@ -166,6 +178,8 @@ class BaseQuerySet(object):
|
||||
queryset._skip, queryset._limit = key.start, key.stop
|
||||
if key.start and key.stop:
|
||||
queryset._limit = key.stop - key.start
|
||||
if queryset._limit == 0:
|
||||
queryset._empty = True
|
||||
|
||||
# Allow further QuerySet modifications to be performed
|
||||
return queryset
|
||||
@@ -175,9 +189,7 @@ class BaseQuerySet(object):
|
||||
if queryset._scalar:
|
||||
return queryset._get_scalar(
|
||||
queryset._document._from_son(
|
||||
queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields,
|
||||
queryset._cursor[key], _auto_dereference=self._auto_dereference,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -185,9 +197,7 @@ class BaseQuerySet(object):
|
||||
return queryset._cursor[key]
|
||||
|
||||
return queryset._document._from_son(
|
||||
queryset._cursor[key],
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields,
|
||||
queryset._cursor[key], _auto_dereference=self._auto_dereference,
|
||||
)
|
||||
|
||||
raise TypeError("Provide a slice or an integer index")
|
||||
@@ -204,8 +214,6 @@ class BaseQuerySet(object):
|
||||
"""Avoid to open all records in an if stmt in Py3."""
|
||||
return self._has_data()
|
||||
|
||||
__nonzero__ = __bool__ # For Py2 support
|
||||
|
||||
# Core functions
|
||||
|
||||
def all(self):
|
||||
@@ -256,20 +264,21 @@ class BaseQuerySet(object):
|
||||
queryset = queryset.filter(*q_objs, **query)
|
||||
|
||||
try:
|
||||
result = six.next(queryset)
|
||||
result = next(queryset)
|
||||
except StopIteration:
|
||||
msg = "%s matching query does not exist." % queryset._document._class_name
|
||||
raise queryset._document.DoesNotExist(msg)
|
||||
|
||||
try:
|
||||
six.next(queryset)
|
||||
# Check if there is another match
|
||||
next(queryset)
|
||||
except StopIteration:
|
||||
return result
|
||||
|
||||
# If we were able to retrieve the 2nd doc, rewind the cursor and
|
||||
# raise the MultipleObjectsReturned exception.
|
||||
queryset.rewind()
|
||||
message = u"%d items returned, instead of 1" % queryset.count()
|
||||
raise queryset._document.MultipleObjectsReturned(message)
|
||||
# If we were able to retrieve the 2nd doc, raise the MultipleObjectsReturned exception.
|
||||
raise queryset._document.MultipleObjectsReturned(
|
||||
"2 or more items returned, instead of 1"
|
||||
)
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""Create new object. Returns the saved object instance.
|
||||
@@ -353,20 +362,20 @@ class BaseQuerySet(object):
|
||||
)
|
||||
except pymongo.errors.DuplicateKeyError as err:
|
||||
message = "Could not save document (%s)"
|
||||
raise NotUniqueError(message % six.text_type(err))
|
||||
raise NotUniqueError(message % err)
|
||||
except pymongo.errors.BulkWriteError as err:
|
||||
# inserting documents that already have an _id field will
|
||||
# give huge performance debt or raise
|
||||
message = u"Bulk write error: (%s)"
|
||||
raise BulkWriteError(message % six.text_type(err.details))
|
||||
message = "Bulk write error: (%s)"
|
||||
raise BulkWriteError(message % err.details)
|
||||
except pymongo.errors.OperationFailure as err:
|
||||
message = "Could not save document (%s)"
|
||||
if re.match("^E1100[01] duplicate key", six.text_type(err)):
|
||||
if re.match("^E1100[01] duplicate key", str(err)):
|
||||
# E11000 - duplicate key error index
|
||||
# E11001 - duplicate key on update
|
||||
message = u"Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % six.text_type(err))
|
||||
raise OperationError(message % six.text_type(err))
|
||||
message = "Tried to save duplicate unique keys (%s)"
|
||||
raise NotUniqueError(message % err)
|
||||
raise OperationError(message % err)
|
||||
|
||||
# Apply inserted_ids to documents
|
||||
for doc, doc_id in zip(docs, ids):
|
||||
@@ -392,9 +401,36 @@ class BaseQuerySet(object):
|
||||
:meth:`skip` that has been applied to this cursor into account when
|
||||
getting the count
|
||||
"""
|
||||
if self._limit == 0 and with_limit_and_skip is False or self._none:
|
||||
# mimic the fact that setting .limit(0) in pymongo sets no limit
|
||||
# https://docs.mongodb.com/manual/reference/method/cursor.limit/#zero-value
|
||||
if (
|
||||
self._limit == 0
|
||||
and with_limit_and_skip is False
|
||||
or self._none
|
||||
or self._empty
|
||||
):
|
||||
return 0
|
||||
count = self._cursor.count(with_limit_and_skip=with_limit_and_skip)
|
||||
|
||||
kwargs = (
|
||||
{"limit": self._limit, "skip": self._skip} if with_limit_and_skip else {}
|
||||
)
|
||||
|
||||
if self._limit == 0:
|
||||
# mimic the fact that historically .limit(0) sets no limit
|
||||
kwargs.pop("limit", None)
|
||||
|
||||
if self._hint not in (-1, None):
|
||||
kwargs["hint"] = self._hint
|
||||
|
||||
if self._collation:
|
||||
kwargs["collation"] = self._collation
|
||||
|
||||
count = count_documents(
|
||||
collection=self._cursor.collection,
|
||||
filter=self._cursor._Cursor__spec,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
self._cursor_obj = None
|
||||
return count
|
||||
|
||||
@@ -490,7 +526,13 @@ class BaseQuerySet(object):
|
||||
return result.deleted_count
|
||||
|
||||
def update(
|
||||
self, upsert=False, multi=True, write_concern=None, full_result=False, **update
|
||||
self,
|
||||
upsert=False,
|
||||
multi=True,
|
||||
write_concern=None,
|
||||
read_concern=None,
|
||||
full_result=False,
|
||||
**update
|
||||
):
|
||||
"""Perform an atomic update on the fields matched by the query.
|
||||
|
||||
@@ -502,6 +544,7 @@ class BaseQuerySet(object):
|
||||
``save(..., write_concern={w: 2, fsync: True}, ...)`` will
|
||||
wait until at least two servers have recorded the write and
|
||||
will force an fsync on the primary server.
|
||||
:param read_concern: Override the read concern for the operation
|
||||
:param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number
|
||||
updated items
|
||||
:param update: Django-style update keyword arguments
|
||||
@@ -528,7 +571,9 @@ class BaseQuerySet(object):
|
||||
else:
|
||||
update["$set"] = {"_cls": queryset._document._class_name}
|
||||
try:
|
||||
with set_write_concern(queryset._collection, write_concern) as collection:
|
||||
with set_read_write_concern(
|
||||
queryset._collection, write_concern, read_concern
|
||||
) as collection:
|
||||
update_func = collection.update_one
|
||||
if multi:
|
||||
update_func = collection.update_many
|
||||
@@ -538,14 +583,14 @@ class BaseQuerySet(object):
|
||||
elif result.raw_result:
|
||||
return result.raw_result["n"]
|
||||
except pymongo.errors.DuplicateKeyError as err:
|
||||
raise NotUniqueError(u"Update failed (%s)" % six.text_type(err))
|
||||
raise NotUniqueError("Update failed (%s)" % err)
|
||||
except pymongo.errors.OperationFailure as err:
|
||||
if six.text_type(err) == u"multi not coded yet":
|
||||
message = u"update() method requires MongoDB 1.1.3+"
|
||||
if str(err) == "multi not coded yet":
|
||||
message = "update() method requires MongoDB 1.1.3+"
|
||||
raise OperationError(message)
|
||||
raise OperationError(u"Update failed (%s)" % six.text_type(err))
|
||||
raise OperationError("Update failed (%s)" % err)
|
||||
|
||||
def upsert_one(self, write_concern=None, **update):
|
||||
def upsert_one(self, write_concern=None, read_concern=None, **update):
|
||||
"""Overwrite or add the first document matched by the query.
|
||||
|
||||
:param write_concern: Extra keyword arguments are passed down which
|
||||
@@ -554,6 +599,7 @@ class BaseQuerySet(object):
|
||||
``save(..., write_concern={w: 2, fsync: True}, ...)`` will
|
||||
wait until at least two servers have recorded the write and
|
||||
will force an fsync on the primary server.
|
||||
:param read_concern: Override the read concern for the operation
|
||||
:param update: Django-style update keyword arguments
|
||||
|
||||
:returns the new or overwritten document
|
||||
@@ -565,6 +611,7 @@ class BaseQuerySet(object):
|
||||
multi=False,
|
||||
upsert=True,
|
||||
write_concern=write_concern,
|
||||
read_concern=read_concern,
|
||||
full_result=True,
|
||||
**update
|
||||
)
|
||||
@@ -661,18 +708,16 @@ class BaseQuerySet(object):
|
||||
**self._cursor_args
|
||||
)
|
||||
except pymongo.errors.DuplicateKeyError as err:
|
||||
raise NotUniqueError(u"Update failed (%s)" % err)
|
||||
raise NotUniqueError("Update failed (%s)" % err)
|
||||
except pymongo.errors.OperationFailure as err:
|
||||
raise OperationError(u"Update failed (%s)" % err)
|
||||
raise OperationError("Update failed (%s)" % err)
|
||||
|
||||
if full_response:
|
||||
if result["value"] is not None:
|
||||
result["value"] = self._document._from_son(
|
||||
result["value"], only_fields=self.only_fields
|
||||
)
|
||||
result["value"] = self._document._from_son(result["value"])
|
||||
else:
|
||||
if result is not None:
|
||||
result = self._document._from_son(result, only_fields=self.only_fields)
|
||||
result = self._document._from_son(result)
|
||||
|
||||
return result
|
||||
|
||||
@@ -692,10 +737,10 @@ class BaseQuerySet(object):
|
||||
return queryset.filter(pk=object_id).first()
|
||||
|
||||
def in_bulk(self, object_ids):
|
||||
"""Retrieve a set of documents by their ids.
|
||||
""""Retrieve a set of documents by their ids.
|
||||
|
||||
:param object_ids: a list or tuple of ``ObjectId``\ s
|
||||
:rtype: dict of ObjectIds as keys and collection-specific
|
||||
:param object_ids: a list or tuple of ObjectId's
|
||||
:rtype: dict of ObjectId's as keys and collection-specific
|
||||
Document subclasses as values.
|
||||
|
||||
.. versionadded:: 0.3
|
||||
@@ -705,24 +750,22 @@ class BaseQuerySet(object):
|
||||
docs = self._collection.find({"_id": {"$in": object_ids}}, **self._cursor_args)
|
||||
if self._scalar:
|
||||
for doc in docs:
|
||||
doc_map[doc["_id"]] = self._get_scalar(
|
||||
self._document._from_son(doc, only_fields=self.only_fields)
|
||||
)
|
||||
doc_map[doc["_id"]] = self._get_scalar(self._document._from_son(doc))
|
||||
elif self._as_pymongo:
|
||||
for doc in docs:
|
||||
doc_map[doc["_id"]] = doc
|
||||
else:
|
||||
for doc in docs:
|
||||
doc_map[doc["_id"]] = self._document._from_son(
|
||||
doc,
|
||||
only_fields=self.only_fields,
|
||||
_auto_dereference=self._auto_dereference,
|
||||
doc, _auto_dereference=self._auto_dereference,
|
||||
)
|
||||
|
||||
return doc_map
|
||||
|
||||
def none(self):
|
||||
"""Helper that just returns a list"""
|
||||
"""Returns a queryset that never returns any objects and no query will be executed when accessing the results
|
||||
inspired by django none() https://docs.djangoproject.com/en/dev/ref/models/querysets/#none
|
||||
"""
|
||||
queryset = self.clone()
|
||||
queryset._none = True
|
||||
return queryset
|
||||
@@ -775,18 +818,18 @@ class BaseQuerySet(object):
|
||||
"_ordering",
|
||||
"_snapshot",
|
||||
"_timeout",
|
||||
"_slave_okay",
|
||||
"_read_preference",
|
||||
"_read_concern",
|
||||
"_iter",
|
||||
"_scalar",
|
||||
"_as_pymongo",
|
||||
"_limit",
|
||||
"_skip",
|
||||
"_empty",
|
||||
"_hint",
|
||||
"_collation",
|
||||
"_auto_dereference",
|
||||
"_search_text",
|
||||
"only_fields",
|
||||
"_max_time_ms",
|
||||
"_comment",
|
||||
"_batch_size",
|
||||
@@ -822,6 +865,7 @@ class BaseQuerySet(object):
|
||||
"""
|
||||
queryset = self.clone()
|
||||
queryset._limit = n
|
||||
queryset._empty = False # cancels the effect of empty
|
||||
|
||||
# If a cursor object has already been created, apply the limit to it.
|
||||
if queryset._cursor_obj:
|
||||
@@ -989,7 +1033,6 @@ class BaseQuerySet(object):
|
||||
.. versionchanged:: 0.5 - Added subfield support
|
||||
"""
|
||||
fields = {f: QueryFieldList.ONLY for f in fields}
|
||||
self.only_fields = fields.keys()
|
||||
return self.fields(True, **fields)
|
||||
|
||||
def exclude(self, *fields):
|
||||
@@ -1026,9 +1069,11 @@ class BaseQuerySet(object):
|
||||
|
||||
posts = BlogPost.objects(...).fields(comments=0)
|
||||
|
||||
To retrieve a subrange of array elements:
|
||||
To retrieve a subrange or sublist of array elements,
|
||||
support exist for both the `slice` and `elemMatch` projection operator:
|
||||
|
||||
posts = BlogPost.objects(...).fields(slice__comments=5)
|
||||
posts = BlogPost.objects(...).fields(elemMatch__comments="test")
|
||||
|
||||
:param kwargs: A set of keyword arguments identifying what to
|
||||
include, exclude, or slice.
|
||||
@@ -1037,7 +1082,7 @@ class BaseQuerySet(object):
|
||||
"""
|
||||
|
||||
# Check for an operator and transform to mongo-style if there is
|
||||
operators = ["slice"]
|
||||
operators = ["slice", "elemMatch"]
|
||||
cleaned_fields = []
|
||||
for key, value in kwargs.items():
|
||||
parts = key.split("__")
|
||||
@@ -1140,7 +1185,7 @@ class BaseQuerySet(object):
|
||||
|
||||
def explain(self):
|
||||
"""Return an explain plan record for the
|
||||
:class:`~mongoengine.queryset.QuerySet`\ 's cursor.
|
||||
:class:`~mongoengine.queryset.QuerySet` cursor.
|
||||
"""
|
||||
return self._cursor.explain()
|
||||
|
||||
@@ -1170,20 +1215,6 @@ class BaseQuerySet(object):
|
||||
queryset._timeout = enabled
|
||||
return queryset
|
||||
|
||||
# DEPRECATED. Has no more impact on PyMongo 3+
|
||||
def slave_okay(self, enabled):
|
||||
"""Enable or disable the slave_okay when querying.
|
||||
|
||||
:param enabled: whether or not the slave_okay is enabled
|
||||
|
||||
.. deprecated:: Ignored with PyMongo 3+
|
||||
"""
|
||||
msg = "slave_okay is deprecated as it has no impact when using PyMongo 3+."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
queryset = self.clone()
|
||||
queryset._slave_okay = enabled
|
||||
return queryset
|
||||
|
||||
def read_preference(self, read_preference):
|
||||
"""Change the read_preference when querying.
|
||||
|
||||
@@ -1196,6 +1227,22 @@ class BaseQuerySet(object):
|
||||
queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_preference
|
||||
return queryset
|
||||
|
||||
def read_concern(self, read_concern):
|
||||
"""Change the read_concern when querying.
|
||||
|
||||
:param read_concern: override ReplicaSetConnection-level
|
||||
preference.
|
||||
"""
|
||||
if read_concern is not None and not isinstance(read_concern, Mapping):
|
||||
raise TypeError("%r is not a valid read concern." % (read_concern,))
|
||||
|
||||
queryset = self.clone()
|
||||
queryset._read_concern = (
|
||||
ReadConcern(**read_concern) if read_concern is not None else None
|
||||
)
|
||||
queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_concern
|
||||
return queryset
|
||||
|
||||
def scalar(self, *fields):
|
||||
"""Instead of returning Document instances, return either a specific
|
||||
value or a tuple of values in order.
|
||||
@@ -1250,10 +1297,7 @@ class BaseQuerySet(object):
|
||||
def from_json(self, json_data):
|
||||
"""Converts json data to unsaved objects"""
|
||||
son_data = json_util.loads(json_data)
|
||||
return [
|
||||
self._document._from_son(data, only_fields=self.only_fields)
|
||||
for data in son_data
|
||||
]
|
||||
return [self._document._from_son(data) for data in son_data]
|
||||
|
||||
def aggregate(self, pipeline, *suppl_pipeline, **kwargs):
|
||||
"""Perform a aggregate function based in your queryset params
|
||||
@@ -1295,10 +1339,11 @@ class BaseQuerySet(object):
|
||||
final_pipeline = initial_pipeline + user_pipeline
|
||||
|
||||
collection = self._collection
|
||||
if self._read_preference is not None:
|
||||
if self._read_preference is not None or self._read_concern is not None:
|
||||
collection = self._collection.with_options(
|
||||
read_preference=self._read_preference
|
||||
read_preference=self._read_preference, read_concern=self._read_concern
|
||||
)
|
||||
|
||||
return collection.aggregate(final_pipeline, cursor={}, **kwargs)
|
||||
|
||||
# JS functionality
|
||||
@@ -1352,13 +1397,13 @@ class BaseQuerySet(object):
|
||||
map_f_scope = {}
|
||||
if isinstance(map_f, Code):
|
||||
map_f_scope = map_f.scope
|
||||
map_f = six.text_type(map_f)
|
||||
map_f = str(map_f)
|
||||
map_f = Code(queryset._sub_js_fields(map_f), map_f_scope)
|
||||
|
||||
reduce_f_scope = {}
|
||||
if isinstance(reduce_f, Code):
|
||||
reduce_f_scope = reduce_f.scope
|
||||
reduce_f = six.text_type(reduce_f)
|
||||
reduce_f = str(reduce_f)
|
||||
reduce_f_code = queryset._sub_js_fields(reduce_f)
|
||||
reduce_f = Code(reduce_f_code, reduce_f_scope)
|
||||
|
||||
@@ -1368,7 +1413,7 @@ class BaseQuerySet(object):
|
||||
finalize_f_scope = {}
|
||||
if isinstance(finalize_f, Code):
|
||||
finalize_f_scope = finalize_f.scope
|
||||
finalize_f = six.text_type(finalize_f)
|
||||
finalize_f = str(finalize_f)
|
||||
finalize_f_code = queryset._sub_js_fields(finalize_f)
|
||||
finalize_f = Code(finalize_f_code, finalize_f_scope)
|
||||
mr_args["finalize"] = finalize_f
|
||||
@@ -1384,7 +1429,7 @@ class BaseQuerySet(object):
|
||||
else:
|
||||
map_reduce_function = "map_reduce"
|
||||
|
||||
if isinstance(output, six.string_types):
|
||||
if isinstance(output, str):
|
||||
mr_args["out"] = output
|
||||
|
||||
elif isinstance(output, dict):
|
||||
@@ -1568,18 +1613,16 @@ class BaseQuerySet(object):
|
||||
def __next__(self):
|
||||
"""Wrap the result in a :class:`~mongoengine.Document` object.
|
||||
"""
|
||||
if self._limit == 0 or self._none:
|
||||
if self._none or self._empty:
|
||||
raise StopIteration
|
||||
|
||||
raw_doc = six.next(self._cursor)
|
||||
raw_doc = next(self._cursor)
|
||||
|
||||
if self._as_pymongo:
|
||||
return raw_doc
|
||||
|
||||
doc = self._document._from_son(
|
||||
raw_doc,
|
||||
_auto_dereference=self._auto_dereference,
|
||||
only_fields=self.only_fields,
|
||||
raw_doc, _auto_dereference=self._auto_dereference,
|
||||
)
|
||||
|
||||
if self._scalar:
|
||||
@@ -1587,8 +1630,6 @@ class BaseQuerySet(object):
|
||||
|
||||
return doc
|
||||
|
||||
next = __next__ # For Python2 support
|
||||
|
||||
def rewind(self):
|
||||
"""Rewind the cursor to its unevaluated state.
|
||||
|
||||
@@ -1642,9 +1683,9 @@ class BaseQuerySet(object):
|
||||
# XXX In PyMongo 3+, we define the read preference on a collection
|
||||
# level, not a cursor level. Thus, we need to get a cloned collection
|
||||
# object using `with_options` first.
|
||||
if self._read_preference is not None:
|
||||
if self._read_preference is not None or self._read_concern is not None:
|
||||
self._cursor_obj = self._collection.with_options(
|
||||
read_preference=self._read_preference
|
||||
read_preference=self._read_preference, read_concern=self._read_concern
|
||||
).find(self._query, **self._cursor_args)
|
||||
else:
|
||||
self._cursor_obj = self._collection.find(self._query, **self._cursor_args)
|
||||
@@ -1816,13 +1857,13 @@ class BaseQuerySet(object):
|
||||
}
|
||||
"""
|
||||
total, data, types = self.exec_js(freq_func, field)
|
||||
values = {types.get(k): int(v) for k, v in iteritems(data)}
|
||||
values = {types.get(k): int(v) for k, v in data.items()}
|
||||
|
||||
if normalize:
|
||||
values = {k: float(v) / total for k, v in values.items()}
|
||||
|
||||
frequencies = {}
|
||||
for k, v in iteritems(values):
|
||||
for k, v in values.items():
|
||||
if isinstance(k, float):
|
||||
if int(k) == k:
|
||||
k = int(k)
|
||||
@@ -1842,7 +1883,7 @@ class BaseQuerySet(object):
|
||||
field_parts = field.split(".")
|
||||
try:
|
||||
field = ".".join(
|
||||
f if isinstance(f, six.string_types) else f.db_field
|
||||
f if isinstance(f, str) else f.db_field
|
||||
for f in self._document._lookup_field(field_parts)
|
||||
)
|
||||
db_field_paths.append(field)
|
||||
@@ -1854,7 +1895,7 @@ class BaseQuerySet(object):
|
||||
for subdoc in subclasses:
|
||||
try:
|
||||
subfield = ".".join(
|
||||
f if isinstance(f, six.string_types) else f.db_field
|
||||
f if isinstance(f, str) else f.db_field
|
||||
for f in subdoc._lookup_field(field_parts)
|
||||
)
|
||||
db_field_paths.append(subfield)
|
||||
@@ -1928,7 +1969,7 @@ class BaseQuerySet(object):
|
||||
field_name = match.group(1).split(".")
|
||||
fields = self._document._lookup_field(field_name)
|
||||
# Substitute the correct name for the field into the javascript
|
||||
return u'["%s"]' % fields[-1].db_field
|
||||
return '["%s"]' % fields[-1].db_field
|
||||
|
||||
def field_path_sub(match):
|
||||
# Extract just the field name, and look up the field objects
|
||||
@@ -1958,23 +1999,3 @@ class BaseQuerySet(object):
|
||||
setattr(queryset, "_" + method_name, val)
|
||||
|
||||
return queryset
|
||||
|
||||
# Deprecated
|
||||
def ensure_index(self, **kwargs):
|
||||
"""Deprecated use :func:`Document.ensure_index`"""
|
||||
msg = (
|
||||
"Doc.objects()._ensure_index() is deprecated. "
|
||||
"Use Doc.ensure_index() instead."
|
||||
)
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
self._document.__class__.ensure_index(**kwargs)
|
||||
return self
|
||||
|
||||
def _ensure_indexes(self):
|
||||
"""Deprecated use :func:`~Document.ensure_indexes`"""
|
||||
msg = (
|
||||
"Doc.objects()._ensure_indexes() is deprecated. "
|
||||
"Use Doc.ensure_indexes() instead."
|
||||
)
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
self._document.__class__.ensure_indexes()
|
||||
|
@@ -1,7 +1,7 @@
|
||||
__all__ = ("QueryFieldList",)
|
||||
|
||||
|
||||
class QueryFieldList(object):
|
||||
class QueryFieldList:
|
||||
"""Object that handles combinations of .only() and .exclude() calls"""
|
||||
|
||||
ONLY = 1
|
||||
@@ -69,8 +69,6 @@ class QueryFieldList(object):
|
||||
def __bool__(self):
|
||||
return bool(self.fields)
|
||||
|
||||
__nonzero__ = __bool__ # For Py2 support
|
||||
|
||||
def as_dict(self):
|
||||
field_list = {field: self.value for field in self.fields}
|
||||
if self.slice:
|
||||
@@ -80,7 +78,7 @@ class QueryFieldList(object):
|
||||
return field_list
|
||||
|
||||
def reset(self):
|
||||
self.fields = set([])
|
||||
self.fields = set()
|
||||
self.slice = {}
|
||||
self.value = self.ONLY
|
||||
|
||||
|
@@ -4,7 +4,7 @@ from mongoengine.queryset.queryset import QuerySet
|
||||
__all__ = ("queryset_manager", "QuerySetManager")
|
||||
|
||||
|
||||
class QuerySetManager(object):
|
||||
class QuerySetManager:
|
||||
"""
|
||||
The default QuerySet Manager.
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import six
|
||||
|
||||
from mongoengine.errors import OperationError
|
||||
from mongoengine.queryset.base import (
|
||||
BaseQuerySet,
|
||||
@@ -127,8 +125,8 @@ class QuerySet(BaseQuerySet):
|
||||
# Pull in ITER_CHUNK_SIZE docs from the database and store them in
|
||||
# the result cache.
|
||||
try:
|
||||
for _ in six.moves.range(ITER_CHUNK_SIZE):
|
||||
self._result_cache.append(six.next(self))
|
||||
for _ in range(ITER_CHUNK_SIZE):
|
||||
self._result_cache.append(next(self))
|
||||
except StopIteration:
|
||||
# Getting this exception means there are no more docs in the
|
||||
# db cursor. Set _has_more to False so that we can use that
|
||||
@@ -143,10 +141,11 @@ class QuerySet(BaseQuerySet):
|
||||
getting the count
|
||||
"""
|
||||
if with_limit_and_skip is False:
|
||||
return super(QuerySet, self).count(with_limit_and_skip)
|
||||
return super().count(with_limit_and_skip)
|
||||
|
||||
if self._len is None:
|
||||
self._len = super(QuerySet, self).count(with_limit_and_skip)
|
||||
# cache the length
|
||||
self._len = super().count(with_limit_and_skip)
|
||||
|
||||
return self._len
|
||||
|
||||
@@ -180,9 +179,9 @@ class QuerySetNoCache(BaseQuerySet):
|
||||
return ".. queryset mid-iteration .."
|
||||
|
||||
data = []
|
||||
for _ in six.moves.range(REPR_OUTPUT_SIZE + 1):
|
||||
for _ in range(REPR_OUTPUT_SIZE + 1):
|
||||
try:
|
||||
data.append(six.next(self))
|
||||
data.append(next(self))
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
|
@@ -3,14 +3,12 @@ from collections import defaultdict
|
||||
from bson import ObjectId, SON
|
||||
from bson.dbref import DBRef
|
||||
import pymongo
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine.base import UPDATE_OPERATORS
|
||||
from mongoengine.common import _import_class
|
||||
from mongoengine.errors import InvalidQueryError
|
||||
|
||||
__all__ = ("query", "update")
|
||||
__all__ = ("query", "update", "STRING_OPERATORS")
|
||||
|
||||
COMPARISON_OPERATORS = (
|
||||
"ne",
|
||||
@@ -101,7 +99,7 @@ def query(_doc_cls=None, **kwargs):
|
||||
cleaned_fields = []
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, six.string_types):
|
||||
if isinstance(field, str):
|
||||
parts.append(field)
|
||||
append_field = False
|
||||
# is last and CachedReferenceField
|
||||
@@ -169,9 +167,9 @@ def query(_doc_cls=None, **kwargs):
|
||||
|
||||
key = ".".join(parts)
|
||||
|
||||
if op is None or key not in mongo_query:
|
||||
if key not in mongo_query:
|
||||
mongo_query[key] = value
|
||||
elif key in mongo_query:
|
||||
else:
|
||||
if isinstance(mongo_query[key], dict) and isinstance(value, dict):
|
||||
mongo_query[key].update(value)
|
||||
# $max/minDistance needs to come last - convert to SON
|
||||
@@ -180,7 +178,7 @@ def query(_doc_cls=None, **kwargs):
|
||||
"$near" in value_dict or "$nearSphere" in value_dict
|
||||
):
|
||||
value_son = SON()
|
||||
for k, v in iteritems(value_dict):
|
||||
for k, v in value_dict.items():
|
||||
if k == "$maxDistance" or k == "$minDistance":
|
||||
continue
|
||||
value_son[k] = v
|
||||
@@ -281,7 +279,7 @@ def update(_doc_cls=None, **update):
|
||||
appended_sub_field = False
|
||||
for field in fields:
|
||||
append_field = True
|
||||
if isinstance(field, six.string_types):
|
||||
if isinstance(field, str):
|
||||
# Convert the S operator to $
|
||||
if field == "S":
|
||||
field = "$"
|
||||
@@ -435,7 +433,9 @@ def _geo_operator(field, op, value):
|
||||
value = {"$near": _infer_geometry(value)}
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'Geo method "%s" has not been implemented for a %s ' % (op, field._name)
|
||||
'Geo method "{}" has not been implemented for a {} '.format(
|
||||
op, field._name
|
||||
)
|
||||
)
|
||||
return value
|
||||
|
||||
|
@@ -7,7 +7,12 @@ from mongoengine.queryset import transform
|
||||
__all__ = ("Q", "QNode")
|
||||
|
||||
|
||||
class QNodeVisitor(object):
|
||||
def warn_empty_is_deprecated():
|
||||
msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
|
||||
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
||||
|
||||
|
||||
class QNodeVisitor:
|
||||
"""Base visitor class for visiting Q-object nodes in a query tree.
|
||||
"""
|
||||
|
||||
@@ -79,7 +84,7 @@ class QueryCompilerVisitor(QNodeVisitor):
|
||||
return transform.query(self.document, **query.query)
|
||||
|
||||
|
||||
class QNode(object):
|
||||
class QNode:
|
||||
"""Base class for nodes in query trees."""
|
||||
|
||||
AND = 0
|
||||
@@ -98,19 +103,18 @@ class QNode(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
|
||||
|
||||
# Or if this Q is empty, ignore it and just use `other`.
|
||||
if self.empty:
|
||||
if not bool(self):
|
||||
return other
|
||||
|
||||
return QCombination(operation, [self, other])
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
warn_empty_is_deprecated()
|
||||
return False
|
||||
|
||||
def __or__(self, other):
|
||||
@@ -143,8 +147,6 @@ class QCombination(QNode):
|
||||
def __bool__(self):
|
||||
return bool(self.children)
|
||||
|
||||
__nonzero__ = __bool__ # For Py2 support
|
||||
|
||||
def accept(self, visitor):
|
||||
for i in range(len(self.children)):
|
||||
if isinstance(self.children[i], QNode):
|
||||
@@ -154,8 +156,7 @@ class QCombination(QNode):
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
msg = "'empty' property is deprecated in favour of using 'not bool(filter)'"
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
warn_empty_is_deprecated()
|
||||
return not bool(self.children)
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -180,8 +181,6 @@ class Q(QNode):
|
||||
def __bool__(self):
|
||||
return bool(self.query)
|
||||
|
||||
__nonzero__ = __bool__ # For Py2 support
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__ and self.query == other.query
|
||||
|
||||
@@ -190,4 +189,5 @@ class Q(QNode):
|
||||
|
||||
@property
|
||||
def empty(self):
|
||||
warn_empty_is_deprecated()
|
||||
return not bool(self.query)
|
||||
|
@@ -15,11 +15,11 @@ try:
|
||||
signals_available = True
|
||||
except ImportError:
|
||||
|
||||
class Namespace(object):
|
||||
class Namespace:
|
||||
def signal(self, name, doc=None):
|
||||
return _FakeSignal(name, doc)
|
||||
|
||||
class _FakeSignal(object):
|
||||
class _FakeSignal:
|
||||
"""If blinker is unavailable, create a fake class with the same
|
||||
interface that allows sending of signals but will fail with an
|
||||
error on anything else. Instead of doing anything on send, it
|
||||
|
@@ -51,4 +51,4 @@ rm -rf $RPM_BUILD_ROOT
|
||||
# %{python_sitearch}/*
|
||||
|
||||
%changelog
|
||||
* See: http://docs.mongoengine.org/en/latest/changelog.html
|
||||
* See: http://docs.mongoengine.org/en/latest/changelog.html
|
||||
|
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
black
|
||||
flake8
|
||||
flake8-import-order
|
||||
pre-commit
|
||||
pytest
|
||||
ipdb
|
||||
ipython
|
||||
tox
|
@@ -1,3 +0,0 @@
|
||||
black
|
||||
flake8
|
||||
flake8-import-order
|
@@ -1,4 +0,0 @@
|
||||
pymongo>=3.4
|
||||
six==1.10.0
|
||||
Sphinx==1.5.5
|
||||
sphinx-rtd-theme==0.2.4
|
24
setup.py
24
setup.py
@@ -92,16 +92,16 @@ version_line = list(filter(lambda l: l.startswith("VERSION"), open(init)))[0]
|
||||
VERSION = get_version(eval(version_line.split("=")[-1]))
|
||||
|
||||
CLASSIFIERS = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.5",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Topic :: Database",
|
||||
@@ -118,15 +118,12 @@ extra_opts = {
|
||||
"Pillow>=2.0.0, <7.0.0", # 7.0.0 dropped Python2 support
|
||||
],
|
||||
}
|
||||
if sys.version_info[0] == 3:
|
||||
extra_opts["use_2to3"] = True
|
||||
if "test" in sys.argv:
|
||||
extra_opts["packages"] = find_packages()
|
||||
extra_opts["package_data"] = {
|
||||
"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]
|
||||
}
|
||||
else:
|
||||
extra_opts["tests_require"] += ["python-dateutil"]
|
||||
|
||||
if "test" in sys.argv:
|
||||
extra_opts["packages"] = find_packages()
|
||||
extra_opts["package_data"] = {
|
||||
"tests": ["fields/mongoengine.png", "fields/mongodb_leaf.png"]
|
||||
}
|
||||
|
||||
setup(
|
||||
name="mongoengine",
|
||||
@@ -143,7 +140,8 @@ setup(
|
||||
long_description=LONG_DESCRIPTION,
|
||||
platforms=["any"],
|
||||
classifiers=CLASSIFIERS,
|
||||
install_requires=["pymongo>=3.4", "six>=1.10.0"],
|
||||
python_requires=">=3.5",
|
||||
install_requires=["pymongo>=3.4, <4.0"],
|
||||
cmdclass={"test": PyTest},
|
||||
**extra_opts
|
||||
)
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from bson import SON
|
||||
@@ -29,7 +28,8 @@ class TestDelta(MongoDBTestCase):
|
||||
self.delta(Document)
|
||||
self.delta(DynamicDocument)
|
||||
|
||||
def delta(self, DocClass):
|
||||
@staticmethod
|
||||
def delta(DocClass):
|
||||
class Doc(DocClass):
|
||||
string_field = StringField()
|
||||
int_field = IntField()
|
||||
@@ -428,13 +428,20 @@ class TestDelta(MongoDBTestCase):
|
||||
assert doc.dict_field == {"hello": "world"}
|
||||
assert doc.list_field == ["1", 2, {"hello": "world"}]
|
||||
|
||||
def test_delta_recursive_db_field(self):
|
||||
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)
|
||||
|
||||
def delta_recursive_db_field(self, DocClass, EmbeddedClass):
|
||||
@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")
|
||||
@@ -487,6 +494,7 @@ class TestDelta(MongoDBTestCase):
|
||||
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})
|
||||
@@ -537,6 +545,7 @@ class TestDelta(MongoDBTestCase):
|
||||
{},
|
||||
)
|
||||
doc.save()
|
||||
assert doc._get_changed_fields() == []
|
||||
doc = doc.reload(10)
|
||||
|
||||
assert doc.embedded_field.list_field[0] == "1"
|
||||
@@ -634,6 +643,7 @@ class TestDelta(MongoDBTestCase):
|
||||
doc.save()
|
||||
doc = doc.reload(10)
|
||||
|
||||
assert doc._delta() == ({}, {},)
|
||||
del doc.embedded_field.list_field[2].list_field
|
||||
assert doc._delta() == (
|
||||
{},
|
||||
@@ -732,12 +742,12 @@ class TestDelta(MongoDBTestCase):
|
||||
assert organization._get_changed_fields() == []
|
||||
|
||||
updates, removals = organization._delta()
|
||||
assert {} == removals
|
||||
assert {} == updates
|
||||
assert removals == {}
|
||||
assert updates == {}
|
||||
|
||||
organization.employees.append(person)
|
||||
updates, removals = organization._delta()
|
||||
assert {} == removals
|
||||
assert removals == {}
|
||||
assert "employees" in updates
|
||||
|
||||
def test_delta_with_dbref_false(self):
|
||||
@@ -749,12 +759,12 @@ class TestDelta(MongoDBTestCase):
|
||||
assert organization._get_changed_fields() == []
|
||||
|
||||
updates, removals = organization._delta()
|
||||
assert {} == removals
|
||||
assert {} == updates
|
||||
assert removals == {}
|
||||
assert updates == {}
|
||||
|
||||
organization.employees.append(person)
|
||||
updates, removals = organization._delta()
|
||||
assert {} == removals
|
||||
assert removals == {}
|
||||
assert "employees" in updates
|
||||
|
||||
def test_nested_nested_fields_mark_as_changed(self):
|
||||
@@ -767,19 +777,46 @@ class TestDelta(MongoDBTestCase):
|
||||
|
||||
MyDoc.drop_collection()
|
||||
|
||||
mydoc = MyDoc(
|
||||
name="testcase1", subs={"a": {"b": EmbeddedDoc(name="foo")}}
|
||||
).save()
|
||||
MyDoc(name="testcase1", subs={"a": {"b": EmbeddedDoc(name="foo")}}).save()
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
subdoc = mydoc.subs["a"]["b"]
|
||||
subdoc.name = "bar"
|
||||
|
||||
assert ["name"] == subdoc._get_changed_fields()
|
||||
assert ["subs.a.b.name"] == mydoc._get_changed_fields()
|
||||
assert subdoc._get_changed_fields() == ["name"]
|
||||
assert mydoc._get_changed_fields() == ["subs.a.b.name"]
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
assert [] == mydoc._get_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):
|
||||
@@ -794,17 +831,17 @@ class TestDelta(MongoDBTestCase):
|
||||
|
||||
mydoc = MyDoc.objects.first()
|
||||
mydoc.subs["a"] = EmbeddedDoc()
|
||||
assert ["subs.a"] == mydoc._get_changed_fields()
|
||||
assert mydoc._get_changed_fields() == ["subs.a"]
|
||||
|
||||
subdoc = mydoc.subs["a"]
|
||||
subdoc.name = "bar"
|
||||
|
||||
assert ["name"] == subdoc._get_changed_fields()
|
||||
assert ["subs.a"] == mydoc._get_changed_fields()
|
||||
assert subdoc._get_changed_fields() == ["name"]
|
||||
assert mydoc._get_changed_fields() == ["subs.a"]
|
||||
mydoc.save()
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
assert [] == mydoc._get_changed_fields()
|
||||
assert mydoc._get_changed_fields() == []
|
||||
|
||||
def test_upper_level_mark_as_changed(self):
|
||||
class EmbeddedDoc(EmbeddedDocument):
|
||||
@@ -821,15 +858,15 @@ class TestDelta(MongoDBTestCase):
|
||||
subdoc = mydoc.subs["a"]
|
||||
subdoc.name = "bar"
|
||||
|
||||
assert ["name"] == subdoc._get_changed_fields()
|
||||
assert ["subs.a.name"] == mydoc._get_changed_fields()
|
||||
assert subdoc._get_changed_fields() == ["name"]
|
||||
assert mydoc._get_changed_fields() == ["subs.a.name"]
|
||||
|
||||
mydoc.subs["a"] = EmbeddedDoc()
|
||||
assert ["subs.a"] == mydoc._get_changed_fields()
|
||||
assert mydoc._get_changed_fields() == ["subs.a"]
|
||||
mydoc.save()
|
||||
|
||||
mydoc._clear_changed_fields()
|
||||
assert [] == mydoc._get_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"""
|
||||
|
@@ -37,6 +37,19 @@ class TestDynamicDocument(MongoDBTestCase):
|
||||
# Confirm no changes to self.Person
|
||||
assert not 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):
|
||||
"""Test changing the scope of a dynamic field has no adverse effects"""
|
||||
p = self.Person()
|
||||
|
@@ -1,11 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
from pymongo.collation import Collation
|
||||
from pymongo.errors import OperationFailure
|
||||
import pytest
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
@@ -59,7 +57,7 @@ class TestIndexes(unittest.TestCase):
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
# _id, '-date', 'tags', ('cat', 'date')
|
||||
assert len(info) == 4
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
for expected in expected_specs:
|
||||
assert expected["fields"] in info
|
||||
|
||||
@@ -87,7 +85,7 @@ class TestIndexes(unittest.TestCase):
|
||||
# the indices on -date and tags will both contain
|
||||
# _cls as first element in the key
|
||||
assert len(info) == 4
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
for expected in expected_specs:
|
||||
assert expected["fields"] in info
|
||||
|
||||
@@ -102,7 +100,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
ExtendedBlogPost.ensure_indexes()
|
||||
info = ExtendedBlogPost.objects._collection.index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
for expected in expected_specs:
|
||||
assert expected["fields"] in info
|
||||
|
||||
@@ -192,7 +190,7 @@ class TestIndexes(unittest.TestCase):
|
||||
# Indexes are lazy so use list() to perform query
|
||||
list(Person.objects)
|
||||
info = Person.objects._collection.index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("rank.title", 1)] in info
|
||||
|
||||
def test_explicit_geo2d_index(self):
|
||||
@@ -207,7 +205,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("location.point", "2d")] in info
|
||||
|
||||
def test_explicit_geo2d_index_embedded(self):
|
||||
@@ -227,7 +225,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("current.location.point", "2d")] in info
|
||||
|
||||
def test_explicit_geosphere_index(self):
|
||||
@@ -244,7 +242,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("location.point", "2dsphere")] in info
|
||||
|
||||
def test_explicit_geohaystack_index(self):
|
||||
@@ -266,7 +264,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
Place.ensure_indexes()
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("location.point", "geoHaystack")] in info
|
||||
|
||||
def test_create_geohaystack_index(self):
|
||||
@@ -279,7 +277,7 @@ class TestIndexes(unittest.TestCase):
|
||||
|
||||
Place.create_index({"fields": (")location.point", "name")}, bucketSize=10)
|
||||
info = Place._get_collection().index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("location.point", "geoHaystack"), ("name", 1)] in info
|
||||
|
||||
def test_dictionary_indexes(self):
|
||||
@@ -308,7 +306,7 @@ class TestIndexes(unittest.TestCase):
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
info = [
|
||||
(value["key"], value.get("unique", False), value.get("sparse", False))
|
||||
for key, value in iteritems(info)
|
||||
for key, value in info.items()
|
||||
]
|
||||
assert ([("addDate", -1)], True, True) in info
|
||||
|
||||
@@ -552,8 +550,9 @@ class TestIndexes(unittest.TestCase):
|
||||
assert 5 == query_result.count()
|
||||
|
||||
incorrect_collation = {"arndom": "wrdo"}
|
||||
with pytest.raises(OperationFailure):
|
||||
with pytest.raises(OperationFailure) as exc_info:
|
||||
BlogPost.objects.collation(incorrect_collation).count()
|
||||
assert "Missing expected field" in str(exc_info.value)
|
||||
|
||||
query_result = BlogPost.objects.collation({}).order_by("name")
|
||||
assert [x.name for x in query_result] == sorted(names)
|
||||
@@ -806,18 +805,6 @@ class TestIndexes(unittest.TestCase):
|
||||
info = Log.objects._collection.index_information()
|
||||
assert 3600 == info["created_1"]["expireAfterSeconds"]
|
||||
|
||||
def test_index_drop_dups_silently_ignored(self):
|
||||
class Customer(Document):
|
||||
cust_id = IntField(unique=True, required=True)
|
||||
meta = {
|
||||
"indexes": ["cust_id"],
|
||||
"index_drop_dups": True,
|
||||
"allow_inheritance": False,
|
||||
}
|
||||
|
||||
Customer.drop_collection()
|
||||
Customer.objects.first()
|
||||
|
||||
def test_unique_and_indexes(self):
|
||||
"""Ensure that 'unique' constraints aren't overridden by
|
||||
meta.indexes.
|
||||
@@ -901,7 +888,7 @@ class TestIndexes(unittest.TestCase):
|
||||
self.fail("Unbound local error at index + pk definition")
|
||||
|
||||
info = BlogPost.objects._collection.index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
index_item = [("_id", 1), ("comments.comment_id", 1)]
|
||||
assert index_item in info
|
||||
|
||||
@@ -942,7 +929,7 @@ class TestIndexes(unittest.TestCase):
|
||||
meta = {"indexes": ["provider_ids.foo", "provider_ids.bar"]}
|
||||
|
||||
info = MyDoc.objects._collection.index_information()
|
||||
info = [value["key"] for key, value in iteritems(info)]
|
||||
info = [value["key"] for key, value in info.items()]
|
||||
assert [("provider_ids.foo", 1)] in info
|
||||
assert [("provider_ids.bar", 1)] in info
|
||||
|
||||
@@ -1058,10 +1045,6 @@ class TestIndexes(unittest.TestCase):
|
||||
del index_info[key][
|
||||
"ns"
|
||||
] # drop the index namespace - we don't care about that here, MongoDB 3+
|
||||
if "dropDups" in index_info[key]:
|
||||
del index_info[key][
|
||||
"dropDups"
|
||||
] # drop the index dropDups - it is deprecated in MongoDB 3+
|
||||
|
||||
assert index_info == {
|
||||
"txt_1": {"key": [("txt", 1)], "background": False},
|
||||
|
@@ -1,9 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import (
|
||||
BooleanField,
|
||||
@@ -523,7 +521,6 @@ class TestInheritance(MongoDBTestCase):
|
||||
|
||||
defaults = {
|
||||
"index_background": True,
|
||||
"index_drop_dups": True,
|
||||
"index_opts": {"hello": "world"},
|
||||
"allow_inheritance": True,
|
||||
"queryset_class": "QuerySet",
|
||||
@@ -550,7 +547,7 @@ class TestInheritance(MongoDBTestCase):
|
||||
class Human(Mammal):
|
||||
pass
|
||||
|
||||
for k, v in iteritems(defaults):
|
||||
for k, v in defaults.items():
|
||||
for cls in [Animal, Fish, Guppy]:
|
||||
assert cls._meta[k] == v
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import pickle
|
||||
import unittest
|
||||
@@ -10,7 +9,6 @@ import bson
|
||||
from bson import DBRef, ObjectId
|
||||
from pymongo.errors import DuplicateKeyError
|
||||
import pytest
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine import signals
|
||||
@@ -189,7 +187,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
def test_queryset_resurrects_dropped_collection(self):
|
||||
self.Person.drop_collection()
|
||||
assert [] == list(self.Person.objects())
|
||||
assert list(self.Person.objects()) == []
|
||||
|
||||
# Ensure works correctly with inhertited classes
|
||||
class Actor(self.Person):
|
||||
@@ -197,7 +195,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
Actor.objects()
|
||||
self.Person.drop_collection()
|
||||
assert [] == list(Actor.objects())
|
||||
assert list(Actor.objects()) == []
|
||||
|
||||
def test_polymorphic_references(self):
|
||||
"""Ensure that the correct subclasses are returned from a query
|
||||
@@ -502,7 +500,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
doc.reload()
|
||||
Animal.drop_collection()
|
||||
|
||||
def test_update_shard_key_routing(self):
|
||||
def test_save_update_shard_key_routing(self):
|
||||
"""Ensures updating a doc with a specified shard_key includes it in
|
||||
the query.
|
||||
"""
|
||||
@@ -530,6 +528,29 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
Animal.drop_collection()
|
||||
|
||||
def test_save_create_shard_key_routing(self):
|
||||
"""Ensures inserting a doc with a specified shard_key includes it in
|
||||
the query.
|
||||
"""
|
||||
|
||||
class Animal(Document):
|
||||
_id = UUIDField(binary=False, primary_key=True, default=uuid.uuid4)
|
||||
is_mammal = BooleanField()
|
||||
name = StringField()
|
||||
meta = {"shard_key": ("is_mammal",)}
|
||||
|
||||
Animal.drop_collection()
|
||||
doc = Animal(is_mammal=True, name="Dog")
|
||||
|
||||
with query_counter() as q:
|
||||
doc.save()
|
||||
query_op = q.db.system.profile.find({"ns": "mongoenginetest.animal"})[0]
|
||||
assert query_op["op"] == "command"
|
||||
assert query_op["command"]["findAndModify"] == "animal"
|
||||
assert set(query_op["command"]["query"].keys()) == set(["_id", "is_mammal"])
|
||||
|
||||
Animal.drop_collection()
|
||||
|
||||
def test_reload_with_changed_fields(self):
|
||||
"""Ensures reloading will not affect changed fields"""
|
||||
|
||||
@@ -579,7 +600,8 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
doc.embedded_field.list_field.append(1)
|
||||
doc.embedded_field.dict_field["woot"] = "woot"
|
||||
|
||||
assert doc._get_changed_fields() == [
|
||||
changed = doc._get_changed_fields()
|
||||
assert changed == [
|
||||
"list_field",
|
||||
"dict_field.woot",
|
||||
"embedded_field.list_field",
|
||||
@@ -1415,7 +1437,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
assert raw_doc["first_name"] == "John"
|
||||
|
||||
def test_inserts_if_you_set_the_pk(self):
|
||||
p1 = self.Person(name="p1", id=bson.ObjectId()).save()
|
||||
_ = self.Person(name="p1", id=bson.ObjectId()).save()
|
||||
p2 = self.Person(name="p2")
|
||||
p2.id = bson.ObjectId()
|
||||
p2.save()
|
||||
@@ -2196,7 +2218,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
user = User(name="Mike").save()
|
||||
reviewer = User(name="John").save()
|
||||
book = Book(author=user, reviewer=reviewer).save()
|
||||
_ = Book(author=user, reviewer=reviewer).save()
|
||||
|
||||
reviewer.delete()
|
||||
assert Book.objects.count() == 1
|
||||
@@ -2222,7 +2244,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
user_1 = User(id=1).save()
|
||||
user_2 = User(id=2).save()
|
||||
book_1 = Book(id=1, author=user_2).save()
|
||||
_ = Book(id=1, author=user_2).save()
|
||||
book_2 = Book(id=2, author=user_1).save()
|
||||
|
||||
user_2.delete()
|
||||
@@ -2231,7 +2253,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
assert Book.objects.get() == book_2
|
||||
|
||||
user_3 = User(id=3).save()
|
||||
book_3 = Book(id=3, author=user_3).save()
|
||||
_ = Book(id=3, author=user_3).save()
|
||||
|
||||
user_3.delete()
|
||||
# Deleting user_3 should also delete book_3
|
||||
@@ -3205,7 +3227,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
def test_positional_creation(self):
|
||||
"""Document cannot be instantiated using positional arguments."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
person = self.Person("Test User", 42)
|
||||
self.Person("Test User", 42)
|
||||
|
||||
expected_msg = (
|
||||
"Instantiating a document with positional arguments is not "
|
||||
@@ -3274,7 +3296,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
|
||||
def expand(self):
|
||||
self.flattened_parameter = {}
|
||||
for parameter_name, parameter in iteritems(self.parameters):
|
||||
for parameter_name, parameter in self.parameters.items():
|
||||
parameter.expand()
|
||||
|
||||
class NodesSystem(Document):
|
||||
@@ -3282,7 +3304,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
nodes = MapField(ReferenceField(Node, dbref=False))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
for node_name, node in iteritems(self.nodes):
|
||||
for node_name, node in self.nodes.items():
|
||||
node.expand()
|
||||
node.save(*args, **kwargs)
|
||||
super(NodesSystem, self).save(*args, **kwargs)
|
||||
@@ -3412,7 +3434,7 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
assert obj3 != dbref2
|
||||
assert dbref2 != obj3
|
||||
|
||||
def test_default_values(self):
|
||||
def test_default_values_dont_get_override_upon_save_when_only_is_used(self):
|
||||
class Person(Document):
|
||||
created_on = DateTimeField(default=lambda: datetime.utcnow())
|
||||
name = StringField()
|
||||
@@ -3607,13 +3629,13 @@ class TestDocumentInstance(MongoDBTestCase):
|
||||
v = StringField()
|
||||
|
||||
class A(Document):
|
||||
l = ListField(EmbeddedDocumentField(B))
|
||||
array = ListField(EmbeddedDocumentField(B))
|
||||
|
||||
A.objects.delete()
|
||||
A(l=[B(v="1"), B(v="2"), B(v="3")]).save()
|
||||
A(array=[B(v="1"), B(v="2"), B(v="3")]).save()
|
||||
a = A.objects.get()
|
||||
assert a.l._instance == a
|
||||
for idx, b in enumerate(a.l):
|
||||
assert a.array._instance == a
|
||||
for idx, b in enumerate(a.array):
|
||||
assert b._instance == a
|
||||
assert idx == 2
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
|
@@ -1,15 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import uuid
|
||||
|
||||
from bson import Binary
|
||||
import pytest
|
||||
import six
|
||||
|
||||
from mongoengine import *
|
||||
from tests.utils import MongoDBTestCase
|
||||
|
||||
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"
|
||||
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(
|
||||
"latin-1"
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +20,7 @@ class TestBinaryField(MongoDBTestCase):
|
||||
content_type = StringField()
|
||||
blob = BinaryField()
|
||||
|
||||
BLOB = six.b("\xe6\x00\xc4\xff\x07")
|
||||
BLOB = "\xe6\x00\xc4\xff\x07".encode("latin-1")
|
||||
MIME_TYPE = "application/octet-stream"
|
||||
|
||||
Attachment.drop_collection()
|
||||
@@ -32,7 +30,7 @@ class TestBinaryField(MongoDBTestCase):
|
||||
|
||||
attachment_1 = Attachment.objects().first()
|
||||
assert MIME_TYPE == attachment_1.content_type
|
||||
assert BLOB == six.binary_type(attachment_1.blob)
|
||||
assert BLOB == bytes(attachment_1.blob)
|
||||
|
||||
def test_validation_succeeds(self):
|
||||
"""Ensure that valid values can be assigned to binary fields.
|
||||
@@ -47,11 +45,11 @@ class TestBinaryField(MongoDBTestCase):
|
||||
attachment_required = AttachmentRequired()
|
||||
with pytest.raises(ValidationError):
|
||||
attachment_required.validate()
|
||||
attachment_required.blob = Binary(six.b("\xe6\x00\xc4\xff\x07"))
|
||||
attachment_required.blob = Binary("\xe6\x00\xc4\xff\x07".encode("latin-1"))
|
||||
attachment_required.validate()
|
||||
|
||||
_5_BYTES = six.b("\xe6\x00\xc4\xff\x07")
|
||||
_4_BYTES = six.b("\xe6\x00\xc4\xff")
|
||||
_5_BYTES = "\xe6\x00\xc4\xff\x07".encode("latin-1")
|
||||
_4_BYTES = "\xe6\x00\xc4\xff".encode("latin-1")
|
||||
with pytest.raises(ValidationError):
|
||||
AttachmentSizeLimit(blob=_5_BYTES).validate()
|
||||
AttachmentSizeLimit(blob=_4_BYTES).validate()
|
||||
@@ -123,10 +121,7 @@ class TestBinaryField(MongoDBTestCase):
|
||||
upsert=True, new=True, set__bin_field=BIN_VALUE
|
||||
)
|
||||
assert doc.some_field == "test"
|
||||
if six.PY3:
|
||||
assert doc.bin_field == BIN_VALUE
|
||||
else:
|
||||
assert doc.bin_field == Binary(BIN_VALUE)
|
||||
assert doc.bin_field == BIN_VALUE
|
||||
|
||||
def test_update_one(self):
|
||||
"""Ensures no regression of bug #1127"""
|
||||
@@ -136,7 +131,7 @@ class TestBinaryField(MongoDBTestCase):
|
||||
|
||||
MyDocument.drop_collection()
|
||||
|
||||
bin_data = six.b("\xe6\x00\xc4\xff\x07")
|
||||
bin_data = "\xe6\x00\xc4\xff\x07".encode("latin-1")
|
||||
doc = MyDocument(bin_field=bin_data).save()
|
||||
|
||||
n_updated = MyDocument.objects(bin_field=bin_data).update_one(
|
||||
@@ -144,7 +139,4 @@ class TestBinaryField(MongoDBTestCase):
|
||||
)
|
||||
assert n_updated == 1
|
||||
fetched = MyDocument.objects.with_id(doc.id)
|
||||
if six.PY3:
|
||||
assert fetched.bin_field == BIN_VALUE
|
||||
else:
|
||||
assert fetched.bin_field == Binary(BIN_VALUE)
|
||||
assert fetched.bin_field == BIN_VALUE
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import itertools
|
||||
import math
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from tests.utils import MongoDBTestCase
|
||||
@@ -65,7 +66,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
|
||||
for values in itertools.product([2014], mm, dd, hh, ii, ss, microsecond):
|
||||
stored = LogEntry(date=datetime.datetime(*values)).to_mongo()["date"]
|
||||
assert (
|
||||
re.match("^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored)
|
||||
re.match(r"^\d{4},\d{2},\d{2},\d{2},\d{2},\d{2},\d{6}$", stored)
|
||||
is not None
|
||||
)
|
||||
|
||||
@@ -74,7 +75,7 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
|
||||
"date_with_dots"
|
||||
]
|
||||
assert (
|
||||
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
|
||||
)
|
||||
|
||||
def test_complexdatetime_usage(self):
|
||||
@@ -191,3 +192,18 @@ class ComplexDateTimeFieldTest(MongoDBTestCase):
|
||||
|
||||
fetched_log = Log.objects.with_id(log.id)
|
||||
assert 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()
|
||||
|
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
import six
|
||||
|
||||
try:
|
||||
import dateutil
|
||||
@@ -89,17 +87,6 @@ class TestDateField(MongoDBTestCase):
|
||||
assert log.date == d1.date()
|
||||
assert 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()
|
||||
assert log.date == d1.date()
|
||||
assert log.date == d2.date()
|
||||
|
||||
def test_regular_usage(self):
|
||||
"""Tests for regular datetime fields"""
|
||||
|
||||
|
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime as dt
|
||||
|
||||
import pytest
|
||||
import six
|
||||
|
||||
try:
|
||||
import dateutil
|
||||
@@ -98,17 +96,6 @@ class TestDateTimeField(MongoDBTestCase):
|
||||
assert log.date != d1
|
||||
assert 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()
|
||||
assert log.date != d1
|
||||
assert log.date == d2
|
||||
|
||||
def test_regular_usage(self):
|
||||
"""Tests for regular datetime fields"""
|
||||
|
||||
@@ -213,7 +200,7 @@ class TestDateTimeField(MongoDBTestCase):
|
||||
# make sure that passing a parsable datetime works
|
||||
dtd = DTDoc()
|
||||
dtd.date = date_str
|
||||
assert isinstance(dtd.date, six.string_types)
|
||||
assert isinstance(dtd.date, str)
|
||||
dtd.save()
|
||||
dtd.reload()
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from bson import InvalidDocument
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
@@ -19,22 +19,24 @@ class TestDictField(MongoDBTestCase):
|
||||
post = BlogPost(info=info).save()
|
||||
assert get_as_pymongo(post) == {"_id": post.id, "info": info}
|
||||
|
||||
def test_general_things(self):
|
||||
"""Ensure that dict types work as expected."""
|
||||
def test_validate_invalid_type(self):
|
||||
class BlogPost(Document):
|
||||
info = DictField()
|
||||
|
||||
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 = "my post"
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
|
||||
post.info = ["test", "test"]
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
|
||||
post.info = {"$title": "test"}
|
||||
with pytest.raises(ValidationError):
|
||||
@@ -48,25 +50,34 @@ class TestDictField(MongoDBTestCase):
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
|
||||
post.info = {1: "test"}
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
|
||||
post.info = {"nested": {"the.title": "test"}}
|
||||
if get_mongodb_version() < MONGODB_36:
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
# 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:
|
||||
with pytest.raises(ValidationError):
|
||||
post.validate()
|
||||
post.validate()
|
||||
with pytest.raises(InvalidDocument):
|
||||
post.save()
|
||||
else:
|
||||
post.validate()
|
||||
|
||||
post.info = {"title": "test"}
|
||||
def test_general_things(self):
|
||||
"""Ensure that dict types work as expected."""
|
||||
|
||||
class BlogPost(Document):
|
||||
info = DictField()
|
||||
|
||||
BlogPost.drop_collection()
|
||||
|
||||
post = BlogPost(info={"title": "test"})
|
||||
post.save()
|
||||
|
||||
post = BlogPost()
|
||||
@@ -101,7 +112,7 @@ class TestDictField(MongoDBTestCase):
|
||||
post.info.setdefault("authors", [])
|
||||
post.save()
|
||||
post.reload()
|
||||
assert [] == post.info["authors"]
|
||||
assert post.info["authors"] == []
|
||||
|
||||
def test_dictfield_dump_document(self):
|
||||
"""Ensure a DictField can handle another document's dump."""
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from mongoengine import (
|
||||
@@ -75,7 +74,7 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
|
||||
# Test non exiting attribute
|
||||
with pytest.raises(InvalidQueryError) as exc_info:
|
||||
Person.objects(settings__notexist="bar").first()
|
||||
assert unicode(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
assert str(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
|
||||
with pytest.raises(LookUpError):
|
||||
Person.objects.only("settings.notexist")
|
||||
@@ -111,7 +110,7 @@ class TestEmbeddedDocumentField(MongoDBTestCase):
|
||||
# Test non exiting attribute
|
||||
with pytest.raises(InvalidQueryError) as exc_info:
|
||||
assert Person.objects(settings__notexist="bar").first().id == p.id
|
||||
assert unicode(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
assert str(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
|
||||
# Test existing attribute
|
||||
assert Person.objects(settings__base_foo="basefoo").first().id == p.id
|
||||
@@ -319,7 +318,7 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
|
||||
# Test non exiting attribute
|
||||
with pytest.raises(InvalidQueryError) as exc_info:
|
||||
Person.objects(settings__notexist="bar").first()
|
||||
assert unicode(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
assert str(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
|
||||
with pytest.raises(LookUpError):
|
||||
Person.objects.only("settings.notexist")
|
||||
@@ -347,7 +346,7 @@ class TestGenericEmbeddedDocumentField(MongoDBTestCase):
|
||||
# Test non exiting attribute
|
||||
with pytest.raises(InvalidQueryError) as exc_info:
|
||||
assert Person.objects(settings__notexist="bar").first().id == p.id
|
||||
assert unicode(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
assert str(exc_info.value) == u'Cannot resolve field "notexist"'
|
||||
|
||||
# Test existing attribute
|
||||
assert Person.objects(settings__base_foo="basefoo").first().id == p.id
|
||||
|
122
tests/fields/test_enum_field.py
Normal file
122
tests/fields/test_enum_field.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from enum import Enum
|
||||
|
||||
from bson import InvalidDocument
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
from tests.utils import MongoDBTestCase, get_as_pymongo
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
NEW = "new"
|
||||
DONE = "done"
|
||||
|
||||
|
||||
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
|
||||
|
||||
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_user_is_informed_when_tries_to_set_choices(self):
|
||||
with pytest.raises(ValueError, match="'choices' can't be set on EnumField"):
|
||||
EnumField(Status, choices=["my", "custom", "options"])
|
||||
|
||||
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
BLUE = 2
|
||||
|
||||
|
||||
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()
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
@@ -336,7 +335,7 @@ class TestField(MongoDBTestCase):
|
||||
doc.save()
|
||||
|
||||
# Unset all the fields
|
||||
HandleNoneFields._get_collection().update(
|
||||
HandleNoneFields._get_collection().update_one(
|
||||
{"_id": doc.id},
|
||||
{"$unset": {"str_fld": 1, "int_fld": 1, "flt_fld": 1, "comp_dt_fld": 1}},
|
||||
)
|
||||
@@ -1084,7 +1083,7 @@ class TestField(MongoDBTestCase):
|
||||
|
||||
e = Simple().save()
|
||||
e.mapping = []
|
||||
assert [] == e._changed_fields
|
||||
assert e._changed_fields == []
|
||||
|
||||
class Simple(Document):
|
||||
mapping = DictField()
|
||||
@@ -1093,7 +1092,7 @@ class TestField(MongoDBTestCase):
|
||||
|
||||
e = Simple().save()
|
||||
e.mapping = {}
|
||||
assert [] == e._changed_fields
|
||||
assert e._changed_fields == []
|
||||
|
||||
def test_slice_marks_field_as_changed(self):
|
||||
class Simple(Document):
|
||||
|
@@ -1,16 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import copy
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from io import BytesIO
|
||||
|
||||
import gridfs
|
||||
import pytest
|
||||
import six
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.python_support import StringIO
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
@@ -30,7 +28,7 @@ TEST_IMAGE2_PATH = os.path.join(os.path.dirname(__file__), "mongodb_leaf.png")
|
||||
def get_file(path):
|
||||
"""Use a BytesIO instead of a file to allow
|
||||
to have a one-liner and avoid that the file remains opened"""
|
||||
bytes_io = StringIO()
|
||||
bytes_io = BytesIO()
|
||||
with open(path, "rb") as f:
|
||||
bytes_io.write(f.read())
|
||||
bytes_io.seek(0)
|
||||
@@ -58,7 +56,7 @@ class TestFileField(MongoDBTestCase):
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
content_type = "text/plain"
|
||||
|
||||
putfile = PutFile()
|
||||
@@ -80,7 +78,7 @@ class TestFileField(MongoDBTestCase):
|
||||
PutFile.drop_collection()
|
||||
|
||||
putfile = PutFile()
|
||||
putstring = StringIO()
|
||||
putstring = BytesIO()
|
||||
putstring.write(text)
|
||||
putstring.seek(0)
|
||||
putfile.the_file.put(putstring, content_type=content_type)
|
||||
@@ -101,8 +99,8 @@ class TestFileField(MongoDBTestCase):
|
||||
|
||||
StreamFile.drop_collection()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
more_text = six.b("Foo Bar")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
more_text = "Foo Bar".encode("latin-1")
|
||||
content_type = "text/plain"
|
||||
|
||||
streamfile = StreamFile()
|
||||
@@ -137,8 +135,8 @@ class TestFileField(MongoDBTestCase):
|
||||
|
||||
StreamFile.drop_collection()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
more_text = six.b("Foo Bar")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
more_text = "Foo Bar".encode("latin-1")
|
||||
|
||||
streamfile = StreamFile()
|
||||
streamfile.save()
|
||||
@@ -167,8 +165,8 @@ class TestFileField(MongoDBTestCase):
|
||||
class SetFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
more_text = six.b("Foo Bar")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
more_text = "Foo Bar".encode("latin-1")
|
||||
|
||||
SetFile.drop_collection()
|
||||
|
||||
@@ -196,7 +194,7 @@ class TestFileField(MongoDBTestCase):
|
||||
GridDocument.drop_collection()
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(six.b("Hello World!"))
|
||||
f.write("Hello World!".encode("latin-1"))
|
||||
f.flush()
|
||||
|
||||
# Test without default
|
||||
@@ -213,7 +211,7 @@ class TestFileField(MongoDBTestCase):
|
||||
assert doc_b.the_file.grid_id == doc_c.the_file.grid_id
|
||||
|
||||
# Test with default
|
||||
doc_d = GridDocument(the_file=six.b(""))
|
||||
doc_d = GridDocument(the_file="".encode("latin-1"))
|
||||
doc_d.save()
|
||||
|
||||
doc_e = GridDocument.objects.with_id(doc_d.id)
|
||||
@@ -240,7 +238,7 @@ class TestFileField(MongoDBTestCase):
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "Hello, World!"
|
||||
test_file.the_file.put(six.b("Hello, World!"))
|
||||
test_file.the_file.put("Hello, World!".encode("latin-1"))
|
||||
test_file.save()
|
||||
|
||||
# Second instance
|
||||
@@ -297,7 +295,9 @@ class TestFileField(MongoDBTestCase):
|
||||
|
||||
test_file = TestFile()
|
||||
assert not bool(test_file.the_file)
|
||||
test_file.the_file.put(six.b("Hello, World!"), content_type="text/plain")
|
||||
test_file.the_file.put(
|
||||
"Hello, World!".encode("latin-1"), content_type="text/plain"
|
||||
)
|
||||
test_file.save()
|
||||
assert bool(test_file.the_file)
|
||||
|
||||
@@ -319,7 +319,7 @@ class TestFileField(MongoDBTestCase):
|
||||
class TestFile(Document):
|
||||
the_file = FileField()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
content_type = "text/plain"
|
||||
|
||||
testfile = TestFile()
|
||||
@@ -363,7 +363,7 @@ class TestFileField(MongoDBTestCase):
|
||||
testfile.the_file.put(text, content_type=content_type, filename="hello")
|
||||
testfile.save()
|
||||
|
||||
text = six.b("Bonjour, World!")
|
||||
text = "Bonjour, World!".encode("latin-1")
|
||||
testfile.the_file.replace(text, content_type=content_type, filename="hello")
|
||||
testfile.save()
|
||||
|
||||
@@ -387,7 +387,7 @@ class TestFileField(MongoDBTestCase):
|
||||
TestImage.drop_collection()
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
f.write(six.b("Hello World!"))
|
||||
f.write("Hello World!".encode("latin-1"))
|
||||
f.flush()
|
||||
|
||||
t = TestImage()
|
||||
@@ -503,21 +503,21 @@ class TestFileField(MongoDBTestCase):
|
||||
# First instance
|
||||
test_file = TestFile()
|
||||
test_file.name = "Hello, World!"
|
||||
test_file.the_file.put(six.b("Hello, World!"), name="hello.txt")
|
||||
test_file.the_file.put("Hello, World!".encode("latin-1"), name="hello.txt")
|
||||
test_file.save()
|
||||
|
||||
data = get_db("test_files").macumba.files.find_one()
|
||||
assert data.get("name") == "hello.txt"
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
assert test_file.the_file.read() == six.b("Hello, World!")
|
||||
assert test_file.the_file.read() == "Hello, World!".encode("latin-1")
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
test_file.the_file = six.b("HELLO, WORLD!")
|
||||
test_file.the_file = "Hello, World!".encode("latin-1")
|
||||
test_file.save()
|
||||
|
||||
test_file = TestFile.objects.first()
|
||||
assert test_file.the_file.read() == six.b("HELLO, WORLD!")
|
||||
assert test_file.the_file.read() == "Hello, World!".encode("latin-1")
|
||||
|
||||
def test_copyable(self):
|
||||
class PutFile(Document):
|
||||
@@ -525,7 +525,7 @@ class TestFileField(MongoDBTestCase):
|
||||
|
||||
PutFile.drop_collection()
|
||||
|
||||
text = six.b("Hello, World!")
|
||||
text = "Hello, World!".encode("latin-1")
|
||||
content_type = "text/plain"
|
||||
|
||||
putfile = PutFile()
|
||||
|
@@ -1,6 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
import six
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
@@ -52,9 +50,8 @@ class TestFloatField(MongoDBTestCase):
|
||||
|
||||
big_person = BigPerson()
|
||||
|
||||
for value, value_type in enumerate(six.integer_types):
|
||||
big_person.height = value_type(value)
|
||||
big_person.validate()
|
||||
big_person.height = int(0)
|
||||
big_person.validate()
|
||||
|
||||
big_person.height = 2 ** 500
|
||||
big_person.validate()
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
@@ -381,7 +380,7 @@ class TestGeoField(MongoDBTestCase):
|
||||
|
||||
meta = {"indexes": [[("location", "2dsphere"), ("datetime", 1)]]}
|
||||
|
||||
assert [] == Log._geo_indices()
|
||||
assert Log._geo_indices() == []
|
||||
|
||||
Log.drop_collection()
|
||||
Log.ensure_indexes()
|
||||
@@ -401,7 +400,7 @@ class TestGeoField(MongoDBTestCase):
|
||||
"indexes": [{"fields": [("location", "2dsphere"), ("datetime", 1)]}]
|
||||
}
|
||||
|
||||
assert [] == Log._geo_indices()
|
||||
assert Log._geo_indices() == []
|
||||
|
||||
Log.drop_collection()
|
||||
Log.ensure_indexes()
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from bson import DBRef, ObjectId
|
||||
import pytest
|
||||
|
||||
@@ -152,7 +151,7 @@ class TestLazyReferenceField(MongoDBTestCase):
|
||||
LazyReference(BadDoc, animal.pk),
|
||||
):
|
||||
with pytest.raises(ValidationError):
|
||||
p = Ocurrence(person="test", animal=bad).save()
|
||||
Ocurrence(person="test", animal=bad).save()
|
||||
|
||||
def test_lazy_reference_query_conversion(self):
|
||||
"""Ensure that LazyReferenceFields can be queried using objects and values
|
||||
@@ -386,7 +385,7 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
|
||||
mineral = Mineral(name="Granite").save()
|
||||
|
||||
occ_animal = Ocurrence(living_thing=animal, thing=animal).save()
|
||||
occ_vegetal = Ocurrence(living_thing=vegetal, thing=vegetal).save()
|
||||
_ = Ocurrence(living_thing=vegetal, thing=vegetal).save()
|
||||
with pytest.raises(ValidationError):
|
||||
Ocurrence(living_thing=mineral).save()
|
||||
|
||||
@@ -458,7 +457,7 @@ class TestGenericLazyReferenceField(MongoDBTestCase):
|
||||
baddoc = BadDoc().save()
|
||||
for bad in (42, "foo", baddoc, LazyReference(BadDoc, animal.pk)):
|
||||
with pytest.raises(ValidationError):
|
||||
p = Ocurrence(person="test", animal=bad).save()
|
||||
Ocurrence(person="test", animal=bad).save()
|
||||
|
||||
def test_generic_lazy_reference_query_conversion(self):
|
||||
class Member(Document):
|
||||
|
@@ -1,11 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from bson.int64 import Int64
|
||||
import pytest
|
||||
import six
|
||||
|
||||
try:
|
||||
from bson.int64 import Int64
|
||||
except ImportError:
|
||||
Int64 = long
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
@@ -28,7 +22,7 @@ class TestLongField(MongoDBTestCase):
|
||||
assert isinstance(
|
||||
db.test_long_field_considered_as_int64.find()[0]["some_long"], Int64
|
||||
)
|
||||
assert isinstance(doc.some_long, six.integer_types)
|
||||
assert isinstance(doc.some_long, int)
|
||||
|
||||
def test_long_validation(self):
|
||||
"""Ensure that invalid values cannot be assigned to long fields.
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from bson import DBRef, SON
|
||||
import pytest
|
||||
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from tests.utils import MongoDBTestCase
|
||||
@@ -21,7 +19,7 @@ class TestSequenceField(MongoDBTestCase):
|
||||
assert c["next"] == 10
|
||||
|
||||
ids = [i.id for i in Person.objects]
|
||||
assert ids == range(1, 11)
|
||||
assert ids == list(range(1, 11))
|
||||
|
||||
c = self.db["mongoengine.counters"].find_one({"_id": "person.id"})
|
||||
assert c["next"] == 10
|
||||
@@ -76,7 +74,7 @@ class TestSequenceField(MongoDBTestCase):
|
||||
assert c["next"] == 10
|
||||
|
||||
ids = [i.id for i in Person.objects]
|
||||
assert ids == range(1, 11)
|
||||
assert ids == list(range(1, 11))
|
||||
|
||||
c = self.db["mongoengine.counters"].find_one({"_id": "jelly.id"})
|
||||
assert c["next"] == 10
|
||||
@@ -101,10 +99,10 @@ class TestSequenceField(MongoDBTestCase):
|
||||
assert c["next"] == 10
|
||||
|
||||
ids = [i.id for i in Person.objects]
|
||||
assert ids == range(1, 11)
|
||||
assert ids == list(range(1, 11))
|
||||
|
||||
counters = [i.counter for i in Person.objects]
|
||||
assert counters == range(1, 11)
|
||||
assert counters == list(range(1, 11))
|
||||
|
||||
c = self.db["mongoengine.counters"].find_one({"_id": "person.id"})
|
||||
assert c["next"] == 10
|
||||
@@ -166,10 +164,10 @@ class TestSequenceField(MongoDBTestCase):
|
||||
assert c["next"] == 10
|
||||
|
||||
ids = [i.id for i in Person.objects]
|
||||
assert ids == range(1, 11)
|
||||
assert ids == list(range(1, 11))
|
||||
|
||||
id = [i.id for i in Animal.objects]
|
||||
assert id == range(1, 11)
|
||||
assert id == list(range(1, 11))
|
||||
|
||||
c = self.db["mongoengine.counters"].find_one({"_id": "person.id"})
|
||||
assert c["next"] == 10
|
||||
@@ -193,7 +191,7 @@ class TestSequenceField(MongoDBTestCase):
|
||||
assert c["next"] == 10
|
||||
|
||||
ids = [i.id for i in Person.objects]
|
||||
assert ids == map(str, range(1, 11))
|
||||
assert ids == [str(i) for i in range(1, 11)]
|
||||
|
||||
c = self.db["mongoengine.counters"].find_one({"_id": "person.id"})
|
||||
assert c["next"] == 10
|
||||
@@ -267,12 +265,12 @@ class TestSequenceField(MongoDBTestCase):
|
||||
foo = Foo(name="Foo")
|
||||
foo.save()
|
||||
|
||||
assert not (
|
||||
"base.counter" in self.db["mongoengine.counters"].find().distinct("_id")
|
||||
assert "base.counter" not in self.db["mongoengine.counters"].find().distinct(
|
||||
"_id"
|
||||
)
|
||||
assert ("foo.counter" and "bar.counter") in self.db[
|
||||
"mongoengine.counters"
|
||||
].find().distinct("_id")
|
||||
existing_counters = self.db["mongoengine.counters"].find().distinct("_id")
|
||||
assert "foo.counter" in existing_counters
|
||||
assert "bar.counter" in existing_counters
|
||||
assert foo.counter == bar.counter
|
||||
assert foo._fields["counter"].owner_document == Foo
|
||||
assert bar._fields["counter"].owner_document == Bar
|
||||
|
@@ -1,8 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from mongoengine import *
|
||||
|
||||
from tests.utils import MongoDBTestCase
|
||||
|
||||
|
||||
@@ -35,7 +33,7 @@ class TestURLField(MongoDBTestCase):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
link.validate()
|
||||
assert (
|
||||
unicode(exc_info.value)
|
||||
str(exc_info.value)
|
||||
== u"ValidationError (Link:None) (Invalid URL: http://\u043f\u0440\u0438\u0432\u0435\u0442.com: ['url'])"
|
||||
)
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import datetime
|
||||
import unittest
|
||||
import uuid
|
||||
@@ -7,11 +5,10 @@ from decimal import Decimal
|
||||
|
||||
from bson import DBRef, ObjectId
|
||||
import pymongo
|
||||
from pymongo.read_concern import ReadConcern
|
||||
from pymongo.read_preferences import ReadPreference
|
||||
from pymongo.results import UpdateResult
|
||||
import pytest
|
||||
import six
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
@@ -110,15 +107,47 @@ class TestQueryset(unittest.TestCase):
|
||||
# Filter people by age
|
||||
people = self.Person.objects(age=20)
|
||||
assert people.count() == 1
|
||||
person = people.next()
|
||||
person = next(people)
|
||||
assert person == user_a
|
||||
assert person.name == "User A"
|
||||
assert person.age == 20
|
||||
|
||||
def test_slicing_sets_empty_limit_skip(self):
|
||||
self.Person.objects.insert(
|
||||
[self.Person(name="User {}".format(i), age=i) for i in range(5)],
|
||||
load_bulk=False,
|
||||
)
|
||||
|
||||
self.Person.objects.create(name="User B", age=30)
|
||||
self.Person.objects.create(name="User C", age=40)
|
||||
|
||||
qs = self.Person.objects()[1:2]
|
||||
assert (qs._empty, qs._skip, qs._limit) == (False, 1, 1)
|
||||
assert len(list(qs)) == 1
|
||||
|
||||
# Test edge case of [1:1] which should return nothing
|
||||
# and require a hack so that it doesn't clash with limit(0)
|
||||
qs = self.Person.objects()[1:1]
|
||||
assert (qs._empty, qs._skip, qs._limit) == (True, 1, 0)
|
||||
assert len(list(qs)) == 0
|
||||
|
||||
qs2 = qs[1:5] # Make sure that further slicing resets _empty
|
||||
assert (qs2._empty, qs2._skip, qs2._limit) == (False, 1, 4)
|
||||
assert len(list(qs2)) == 4
|
||||
|
||||
def test_limit_0_returns_all_documents(self):
|
||||
self.Person.objects.create(name="User A", age=20)
|
||||
self.Person.objects.create(name="User B", age=30)
|
||||
|
||||
n_docs = self.Person.objects().count()
|
||||
|
||||
persons = list(self.Person.objects().limit(0))
|
||||
assert len(persons) == 2 == n_docs
|
||||
|
||||
def test_limit(self):
|
||||
"""Ensure that QuerySet.limit works as expected."""
|
||||
user_a = self.Person.objects.create(name="User A", age=20)
|
||||
user_b = self.Person.objects.create(name="User B", age=30)
|
||||
_ = self.Person.objects.create(name="User B", age=30)
|
||||
|
||||
# Test limit on a new queryset
|
||||
people = list(self.Person.objects.limit(1))
|
||||
@@ -150,6 +179,11 @@ class TestQueryset(unittest.TestCase):
|
||||
user_b = self.Person.objects.create(name="User B", age=30)
|
||||
|
||||
# Test skip on a new queryset
|
||||
people = list(self.Person.objects.skip(0))
|
||||
assert len(people) == 2
|
||||
assert people[0] == user_a
|
||||
assert people[1] == user_b
|
||||
|
||||
people = list(self.Person.objects.skip(1))
|
||||
assert len(people) == 1
|
||||
assert people[0] == user_b
|
||||
@@ -274,32 +308,47 @@ class TestQueryset(unittest.TestCase):
|
||||
with pytest.raises(InvalidQueryError):
|
||||
self.Person.objects(name="User A").with_id(person1.id)
|
||||
|
||||
def test_find_only_one(self):
|
||||
"""Ensure that a query using ``get`` returns at most one result.
|
||||
"""
|
||||
def test_get_no_document_exists_raises_doesnotexist(self):
|
||||
assert self.Person.objects.count() == 0
|
||||
# Try retrieving when no objects exists
|
||||
with pytest.raises(DoesNotExist):
|
||||
self.Person.objects.get()
|
||||
with pytest.raises(self.Person.DoesNotExist):
|
||||
self.Person.objects.get()
|
||||
|
||||
def test_get_multiple_match_raises_multipleobjectsreturned(self):
|
||||
"""Ensure that a query using ``get`` returns at most one result.
|
||||
"""
|
||||
assert self.Person.objects().count() == 0
|
||||
|
||||
person1 = self.Person(name="User A", age=20)
|
||||
person1.save()
|
||||
person2 = self.Person(name="User B", age=30)
|
||||
|
||||
p = self.Person.objects.get()
|
||||
assert p == person1
|
||||
|
||||
person2 = self.Person(name="User B", age=20)
|
||||
person2.save()
|
||||
|
||||
# Retrieve the first person from the database
|
||||
person3 = self.Person(name="User C", age=30)
|
||||
person3.save()
|
||||
|
||||
# .get called without argument
|
||||
with pytest.raises(MultipleObjectsReturned):
|
||||
self.Person.objects.get()
|
||||
with pytest.raises(self.Person.MultipleObjectsReturned):
|
||||
self.Person.objects.get()
|
||||
|
||||
# check filtering
|
||||
with pytest.raises(MultipleObjectsReturned):
|
||||
self.Person.objects.get(age__lt=30)
|
||||
with pytest.raises(MultipleObjectsReturned) as exc_info:
|
||||
self.Person.objects(age__lt=30).get()
|
||||
assert "2 or more items returned, instead of 1" == str(exc_info.value)
|
||||
|
||||
# Use a query to filter the people found to just person2
|
||||
person = self.Person.objects.get(age=30)
|
||||
assert person.name == "User B"
|
||||
|
||||
person = self.Person.objects.get(age__lt=30)
|
||||
assert person.name == "User A"
|
||||
assert person == person3
|
||||
|
||||
def test_find_array_position(self):
|
||||
"""Ensure that query by array position works.
|
||||
@@ -358,6 +407,9 @@ class TestQueryset(unittest.TestCase):
|
||||
|
||||
assert list(A.objects.none()) == []
|
||||
assert list(A.objects.none().all()) == []
|
||||
assert list(A.objects.none().limit(1)) == []
|
||||
assert list(A.objects.none().skip(1)) == []
|
||||
assert list(A.objects.none()[:5]) == []
|
||||
|
||||
def test_chaining(self):
|
||||
class A(Document):
|
||||
@@ -2573,13 +2625,8 @@ class TestQueryset(unittest.TestCase):
|
||||
age = IntField()
|
||||
|
||||
with db_ops_tracker() as q:
|
||||
adult1 = (
|
||||
User.objects.filter(age__gte=18).comment("looking for an adult").first()
|
||||
)
|
||||
|
||||
adult2 = (
|
||||
User.objects.comment("looking for an adult").filter(age__gte=18).first()
|
||||
)
|
||||
User.objects.filter(age__gte=18).comment("looking for an adult").first()
|
||||
User.objects.comment("looking for an adult").filter(age__gte=18).first()
|
||||
|
||||
ops = q.get_ops()
|
||||
assert len(ops) == 2
|
||||
@@ -2768,7 +2815,7 @@ class TestQueryset(unittest.TestCase):
|
||||
)
|
||||
|
||||
# start a map/reduce
|
||||
cursor.next()
|
||||
next(cursor)
|
||||
|
||||
results = Person.objects.map_reduce(
|
||||
map_f=map_person,
|
||||
@@ -4007,6 +4054,32 @@ class TestQueryset(unittest.TestCase):
|
||||
|
||||
Number.drop_collection()
|
||||
|
||||
def test_clone_retains_settings(self):
|
||||
"""Ensure that cloning retains the read_preference and read_concern
|
||||
"""
|
||||
|
||||
class Number(Document):
|
||||
n = IntField()
|
||||
|
||||
Number.drop_collection()
|
||||
|
||||
qs = Number.objects
|
||||
qs_clone = qs.clone()
|
||||
assert qs._read_preference == qs_clone._read_preference
|
||||
assert qs._read_concern == qs_clone._read_concern
|
||||
|
||||
qs = Number.objects.read_preference(ReadPreference.PRIMARY_PREFERRED)
|
||||
qs_clone = qs.clone()
|
||||
assert qs._read_preference == ReadPreference.PRIMARY_PREFERRED
|
||||
assert qs._read_preference == qs_clone._read_preference
|
||||
|
||||
qs = Number.objects.read_concern({"level": "majority"})
|
||||
qs_clone = qs.clone()
|
||||
assert qs._read_concern.document == {"level": "majority"}
|
||||
assert qs._read_concern == qs_clone._read_concern
|
||||
|
||||
Number.drop_collection()
|
||||
|
||||
def test_using(self):
|
||||
"""Ensure that switching databases for a queryset is possible
|
||||
"""
|
||||
@@ -4093,7 +4166,7 @@ class TestQueryset(unittest.TestCase):
|
||||
info = Comment.objects._collection.index_information()
|
||||
info = [
|
||||
(value["key"], value.get("unique", False), value.get("sparse", False))
|
||||
for key, value in iteritems(info)
|
||||
for key, value in info.items()
|
||||
]
|
||||
assert ([("_cls", 1), ("message", 1)], False, False) in info
|
||||
|
||||
@@ -4395,7 +4468,7 @@ class TestQueryset(unittest.TestCase):
|
||||
# Use a query to filter the people found to just person1
|
||||
people = self.Person.objects(age=20).scalar("name")
|
||||
assert people.count() == 1
|
||||
person = people.next()
|
||||
person = next(people)
|
||||
assert person == "User A"
|
||||
|
||||
# Test limit
|
||||
@@ -4428,7 +4501,9 @@ class TestQueryset(unittest.TestCase):
|
||||
assert len(people) == 1
|
||||
assert people[0] == "User B"
|
||||
|
||||
people = list(self.Person.objects[1:1].scalar("name"))
|
||||
# people = list(self.Person.objects[1:1].scalar("name"))
|
||||
people = self.Person.objects[1:1]
|
||||
people = people.scalar("name")
|
||||
assert len(people) == 0
|
||||
|
||||
# Test slice out of range
|
||||
@@ -4445,24 +4520,14 @@ class TestQueryset(unittest.TestCase):
|
||||
"A0" == "%s" % self.Person.objects.order_by("name").scalar("name").first()
|
||||
)
|
||||
assert "A0" == "%s" % self.Person.objects.scalar("name").order_by("name")[0]
|
||||
if six.PY3:
|
||||
assert (
|
||||
"['A1', 'A2']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[1:3]
|
||||
)
|
||||
assert (
|
||||
"['A51', 'A52']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[51:53]
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
"[u'A1', u'A2']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[1:3]
|
||||
)
|
||||
assert (
|
||||
"[u'A51', u'A52']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[51:53]
|
||||
)
|
||||
assert (
|
||||
"['A1', 'A2']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[1:3]
|
||||
)
|
||||
assert (
|
||||
"['A51', 'A52']"
|
||||
== "%s" % self.Person.objects.order_by("age").scalar("name")[51:53]
|
||||
)
|
||||
|
||||
# with_id and in_bulk
|
||||
person = self.Person.objects.order_by("name").first()
|
||||
@@ -4470,12 +4535,77 @@ class TestQueryset(unittest.TestCase):
|
||||
|
||||
pks = self.Person.objects.order_by("age").scalar("pk")[1:3]
|
||||
names = self.Person.objects.scalar("name").in_bulk(list(pks)).values()
|
||||
if six.PY3:
|
||||
expected = "['A1', 'A2']"
|
||||
else:
|
||||
expected = "[u'A1', u'A2']"
|
||||
expected = "['A1', 'A2']"
|
||||
assert expected == "%s" % sorted(names)
|
||||
|
||||
def test_fields(self):
|
||||
class Bar(EmbeddedDocument):
|
||||
v = StringField()
|
||||
z = StringField()
|
||||
|
||||
class Foo(Document):
|
||||
x = StringField()
|
||||
y = IntField()
|
||||
items = EmbeddedDocumentListField(Bar)
|
||||
|
||||
Foo.drop_collection()
|
||||
|
||||
Foo(x="foo1", y=1).save()
|
||||
Foo(x="foo2", y=2, items=[]).save()
|
||||
Foo(x="foo3", y=3, items=[Bar(z="a", v="V")]).save()
|
||||
Foo(
|
||||
x="foo4",
|
||||
y=4,
|
||||
items=[
|
||||
Bar(z="a", v="V"),
|
||||
Bar(z="b", v="W"),
|
||||
Bar(z="b", v="X"),
|
||||
Bar(z="c", v="V"),
|
||||
],
|
||||
).save()
|
||||
Foo(
|
||||
x="foo5",
|
||||
y=5,
|
||||
items=[
|
||||
Bar(z="b", v="X"),
|
||||
Bar(z="c", v="V"),
|
||||
Bar(z="d", v="V"),
|
||||
Bar(z="e", v="V"),
|
||||
],
|
||||
).save()
|
||||
|
||||
foos_with_x = list(Foo.objects.order_by("y").fields(x=1))
|
||||
|
||||
assert all(o.x is not None for o in foos_with_x)
|
||||
|
||||
foos_without_y = list(Foo.objects.order_by("y").fields(y=0))
|
||||
|
||||
assert all(o.y is None for o in foos_without_y)
|
||||
|
||||
foos_with_sliced_items = list(Foo.objects.order_by("y").fields(slice__items=1))
|
||||
|
||||
assert foos_with_sliced_items[0].items == []
|
||||
assert foos_with_sliced_items[1].items == []
|
||||
assert len(foos_with_sliced_items[2].items) == 1
|
||||
assert foos_with_sliced_items[2].items[0].z == "a"
|
||||
assert len(foos_with_sliced_items[3].items) == 1
|
||||
assert foos_with_sliced_items[3].items[0].z == "a"
|
||||
assert len(foos_with_sliced_items[4].items) == 1
|
||||
assert foos_with_sliced_items[4].items[0].z == "b"
|
||||
|
||||
foos_with_elem_match_items = list(
|
||||
Foo.objects.order_by("y").fields(elemMatch__items={"z": "b"})
|
||||
)
|
||||
|
||||
assert foos_with_elem_match_items[0].items == []
|
||||
assert foos_with_elem_match_items[1].items == []
|
||||
assert foos_with_elem_match_items[2].items == []
|
||||
assert len(foos_with_elem_match_items[3].items) == 1
|
||||
assert foos_with_elem_match_items[3].items[0].z == "b"
|
||||
assert foos_with_elem_match_items[3].items[0].v == "W"
|
||||
assert len(foos_with_elem_match_items[4].items) == 1
|
||||
assert foos_with_elem_match_items[4].items[0].z == "b"
|
||||
|
||||
def test_elem_match(self):
|
||||
class Foo(EmbeddedDocument):
|
||||
shape = StringField()
|
||||
@@ -4658,6 +4788,46 @@ class TestQueryset(unittest.TestCase):
|
||||
)
|
||||
assert_read_pref(bars, ReadPreference.SECONDARY_PREFERRED)
|
||||
|
||||
def test_read_concern(self):
|
||||
class Bar(Document):
|
||||
txt = StringField()
|
||||
|
||||
meta = {"indexes": ["txt"]}
|
||||
|
||||
Bar.drop_collection()
|
||||
bar = Bar.objects.create(txt="xyz")
|
||||
|
||||
bars = list(Bar.objects.read_concern(None))
|
||||
assert bars == [bar]
|
||||
|
||||
bars = Bar.objects.read_concern({"level": "local"})
|
||||
assert bars._read_concern.document == {"level": "local"}
|
||||
assert bars._cursor.collection.read_concern.document == {"level": "local"}
|
||||
|
||||
# Make sure that `.read_concern(...)` does not accept string values.
|
||||
with pytest.raises(TypeError):
|
||||
Bar.objects.read_concern("local")
|
||||
|
||||
def assert_read_concern(qs, expected_read_concern):
|
||||
assert qs._read_concern.document == expected_read_concern
|
||||
assert qs._cursor.collection.read_concern.document == expected_read_concern
|
||||
|
||||
# Make sure read concern is respected after a `.skip(...)`.
|
||||
bars = Bar.objects.skip(1).read_concern({"level": "local"})
|
||||
assert_read_concern(bars, {"level": "local"})
|
||||
|
||||
# Make sure read concern is respected after a `.limit(...)`.
|
||||
bars = Bar.objects.limit(1).read_concern({"level": "local"})
|
||||
assert_read_concern(bars, {"level": "local"})
|
||||
|
||||
# Make sure read concern is respected after an `.order_by(...)`.
|
||||
bars = Bar.objects.order_by("txt").read_concern({"level": "local"})
|
||||
assert_read_concern(bars, {"level": "local"})
|
||||
|
||||
# Make sure read concern is respected after a `.hint(...)`.
|
||||
bars = Bar.objects.hint([("txt", 1)]).read_concern({"level": "majority"})
|
||||
assert_read_concern(bars, {"level": "majority"})
|
||||
|
||||
def test_json_simple(self):
|
||||
class Embedded(EmbeddedDocument):
|
||||
string = StringField()
|
||||
@@ -5294,7 +5464,7 @@ class TestQueryset(unittest.TestCase):
|
||||
if not test:
|
||||
raise AssertionError("Cursor has data and returned False")
|
||||
|
||||
queryset.next()
|
||||
next(queryset)
|
||||
if not queryset:
|
||||
raise AssertionError(
|
||||
"Cursor has data and it must returns True, even in the last item."
|
||||
@@ -5527,7 +5697,7 @@ class TestQueryset(unittest.TestCase):
|
||||
self.Person.objects.create(name="Baz")
|
||||
assert self.Person.objects.count(with_limit_and_skip=True) == 3
|
||||
|
||||
newPerson = self.Person.objects.create(name="Foo_1")
|
||||
self.Person.objects.create(name="Foo_1")
|
||||
assert self.Person.objects.count(with_limit_and_skip=True) == 4
|
||||
|
||||
def test_no_cursor_timeout(self):
|
||||
|
@@ -1,5 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
import warnings
|
||||
|
||||
|
@@ -24,6 +24,12 @@ class TestTransform(unittest.TestCase):
|
||||
}
|
||||
assert transform.query(friend__age__gte=30) == {"friend.age": {"$gte": 30}}
|
||||
assert transform.query(name__exists=True) == {"name": {"$exists": True}}
|
||||
assert transform.query(name=["Mark"], __raw__={"name": {"$in": "Tom"}}) == {
|
||||
"$and": [{"name": ["Mark"]}, {"name": {"$in": "Tom"}}]
|
||||
}
|
||||
assert transform.query(name__in=["Tom"], __raw__={"name": "Mark"}) == {
|
||||
"$and": [{"name": {"$in": ["Tom"]}}, {"name": "Mark"}]
|
||||
}
|
||||
|
||||
def test_transform_update(self):
|
||||
class LisDoc(Document):
|
||||
@@ -338,6 +344,31 @@ class TestTransform(unittest.TestCase):
|
||||
)
|
||||
assert update == {"$pull": {"content.text": {"word": {"$nin": ["foo", "bar"]}}}}
|
||||
|
||||
def test_transform_embedded_document_list_fields(self):
|
||||
"""
|
||||
Test added to check filtering
|
||||
EmbeddedDocumentListField which is inside a EmbeddedDocumentField
|
||||
"""
|
||||
|
||||
class Drink(EmbeddedDocument):
|
||||
id = StringField()
|
||||
meta = {"strict": False}
|
||||
|
||||
class Shop(Document):
|
||||
drinks = EmbeddedDocumentListField(Drink)
|
||||
|
||||
Shop.drop_collection()
|
||||
drinks = [Drink(id="drink_1"), Drink(id="drink_2")]
|
||||
Shop.objects.create(drinks=drinks)
|
||||
q_obj = transform.query(
|
||||
Shop, drinks__all=[{"$elemMatch": {"_id": x.id}} for x in drinks]
|
||||
)
|
||||
assert q_obj == {
|
||||
"drinks": {"$all": [{"$elemMatch": {"_id": x.id}} for x in drinks]}
|
||||
}
|
||||
|
||||
Shop.drop_collection()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@@ -282,7 +282,7 @@ class ConnectionTest(unittest.TestCase):
|
||||
# database won't exist until we save a document
|
||||
some_document.save()
|
||||
assert conn.get_default_database().name == "mongoenginetest"
|
||||
assert conn.database_names()[0] == "mongoenginetest"
|
||||
assert conn.list_database_names()[0] == "mongoenginetest"
|
||||
|
||||
@require_mongomock
|
||||
def test_connect_with_host_list(self):
|
||||
@@ -348,7 +348,7 @@ class ConnectionTest(unittest.TestCase):
|
||||
|
||||
def test_disconnect_cleans_cached_collection_attribute_in_document(self):
|
||||
"""Ensure that the disconnect() method works properly"""
|
||||
conn1 = connect("mongoenginetest")
|
||||
connect("mongoenginetest")
|
||||
|
||||
class History(Document):
|
||||
pass
|
||||
@@ -518,7 +518,7 @@ class ConnectionTest(unittest.TestCase):
|
||||
"""Ensure connect() uses the username & password params if the URI
|
||||
doesn't explicitly specify them.
|
||||
"""
|
||||
c = connect(
|
||||
connect(
|
||||
host="mongodb://localhost/mongoenginetest", username="user", password="pass"
|
||||
)
|
||||
|
||||
@@ -632,7 +632,7 @@ class ConnectionTest(unittest.TestCase):
|
||||
"""Ensure connect() works when specifying a replicaSet via the
|
||||
MongoDB URI.
|
||||
"""
|
||||
c = connect(host="mongodb://localhost/test?replicaSet=local-rs")
|
||||
connect(host="mongodb://localhost/test?replicaSet=local-rs")
|
||||
db = get_db()
|
||||
assert isinstance(db, pymongo.database.Database)
|
||||
assert db.name == "test"
|
||||
|
@@ -8,6 +8,8 @@ from mongoengine.context_managers import (
|
||||
no_dereference,
|
||||
no_sub_classes,
|
||||
query_counter,
|
||||
set_read_write_concern,
|
||||
set_write_concern,
|
||||
switch_collection,
|
||||
switch_db,
|
||||
)
|
||||
@@ -15,6 +17,52 @@ from mongoengine.pymongo_support import count_documents
|
||||
|
||||
|
||||
class TestContextManagers:
|
||||
def test_set_write_concern(self):
|
||||
connect("mongoenginetest")
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
collection = User._get_collection()
|
||||
original_write_concern = collection.write_concern
|
||||
|
||||
with set_write_concern(
|
||||
collection, {"w": "majority", "j": True, "wtimeout": 1234}
|
||||
) as updated_collection:
|
||||
assert updated_collection.write_concern.document == {
|
||||
"w": "majority",
|
||||
"j": True,
|
||||
"wtimeout": 1234,
|
||||
}
|
||||
|
||||
assert original_write_concern.document == collection.write_concern.document
|
||||
|
||||
def test_set_read_write_concern(self):
|
||||
connect("mongoenginetest")
|
||||
|
||||
class User(Document):
|
||||
name = StringField()
|
||||
|
||||
collection = User._get_collection()
|
||||
|
||||
original_read_concern = collection.read_concern
|
||||
original_write_concern = collection.write_concern
|
||||
|
||||
with set_read_write_concern(
|
||||
collection,
|
||||
{"w": "majority", "j": True, "wtimeout": 1234},
|
||||
{"level": "local"},
|
||||
) as update_collection:
|
||||
assert update_collection.read_concern.document == {"level": "local"}
|
||||
assert update_collection.write_concern.document == {
|
||||
"w": "majority",
|
||||
"j": True,
|
||||
"wtimeout": 1234,
|
||||
}
|
||||
|
||||
assert original_read_concern.document == collection.read_concern.document
|
||||
assert original_write_concern.document == collection.write_concern.document
|
||||
|
||||
def test_switch_db_context_manager(self):
|
||||
connect("mongoenginetest")
|
||||
register_connection("testdb-1", "mongoenginetest2")
|
||||
@@ -216,7 +264,7 @@ class TestContextManagers:
|
||||
|
||||
def test_query_counter_does_not_swallow_exception(self):
|
||||
with pytest.raises(TypeError):
|
||||
with query_counter() as q:
|
||||
with query_counter():
|
||||
raise TypeError()
|
||||
|
||||
def test_query_counter_temporarily_modifies_profiling_level(self):
|
||||
@@ -226,12 +274,12 @@ class TestContextManagers:
|
||||
initial_profiling_level = db.profiling_level()
|
||||
|
||||
try:
|
||||
NEW_LEVEL = 1
|
||||
db.set_profiling_level(NEW_LEVEL)
|
||||
assert db.profiling_level() == NEW_LEVEL
|
||||
with query_counter() as q:
|
||||
new_level = 1
|
||||
db.set_profiling_level(new_level)
|
||||
assert db.profiling_level() == new_level
|
||||
with query_counter():
|
||||
assert db.profiling_level() == 2
|
||||
assert db.profiling_level() == NEW_LEVEL
|
||||
assert db.profiling_level() == new_level
|
||||
except Exception:
|
||||
db.set_profiling_level(
|
||||
initial_profiling_level
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
from six import iterkeys
|
||||
|
||||
from mongoengine import Document
|
||||
from mongoengine.base.datastructures import BaseDict, BaseList, StrictDict
|
||||
@@ -10,10 +9,14 @@ from mongoengine.base.datastructures import BaseDict, BaseList, StrictDict
|
||||
class DocumentStub(object):
|
||||
def __init__(self):
|
||||
self._changed_fields = []
|
||||
self._unset_fields = []
|
||||
|
||||
def _mark_as_changed(self, key):
|
||||
self._changed_fields.append(key)
|
||||
|
||||
def _mark_as_unset(self, key):
|
||||
self._unset_fields.append(key)
|
||||
|
||||
|
||||
class TestBaseDict:
|
||||
@staticmethod
|
||||
@@ -287,7 +290,7 @@ class TestBaseList:
|
||||
base_list[:] = [
|
||||
0,
|
||||
1,
|
||||
] # Will use __setslice__ under py2 and __setitem__ under py3
|
||||
]
|
||||
assert base_list._instance._changed_fields == ["my_name"]
|
||||
assert base_list == [0, 1]
|
||||
|
||||
@@ -296,13 +299,13 @@ class TestBaseList:
|
||||
base_list[0:2] = [
|
||||
1,
|
||||
0,
|
||||
] # Will use __setslice__ under py2 and __setitem__ under py3
|
||||
]
|
||||
assert base_list._instance._changed_fields == ["my_name"]
|
||||
assert base_list == [1, 0, 2]
|
||||
|
||||
def test___setitem___calls_with_step_slice_mark_as_changed(self):
|
||||
base_list = self._get_baselist([0, 1, 2])
|
||||
base_list[0:3:2] = [-1, -2] # uses __setitem__ in both py2 & 3
|
||||
base_list[0:3:2] = [-1, -2] # uses __setitem__
|
||||
assert base_list._instance._changed_fields == ["my_name"]
|
||||
assert base_list == [-1, 1, -2]
|
||||
|
||||
@@ -315,7 +318,7 @@ class TestBaseList:
|
||||
def test___setitem___item_0_calls_mark_as_changed(self):
|
||||
base_list = self._get_baselist([True])
|
||||
base_list[0] = False
|
||||
assert base_list._instance._changed_fields == ["my_name"]
|
||||
assert base_list._instance._changed_fields == ["my_name.0"]
|
||||
assert base_list == [False]
|
||||
|
||||
def test___setitem___item_1_calls_mark_as_changed(self):
|
||||
@@ -372,7 +375,7 @@ class TestStrictDict(unittest.TestCase):
|
||||
|
||||
def test_iterkeys(self):
|
||||
d = self.dtype(a=1)
|
||||
assert list(iterkeys(d)) == ["a"]
|
||||
assert list(d.keys()) == ["a"]
|
||||
|
||||
def test_len(self):
|
||||
d = self.dtype(a=1)
|
||||
|
@@ -1,11 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from bson import DBRef, ObjectId
|
||||
from six import iteritems
|
||||
|
||||
from mongoengine import *
|
||||
from mongoengine.connection import get_db
|
||||
from mongoengine.context_managers import query_counter
|
||||
|
||||
|
||||
@@ -372,8 +369,7 @@ class FieldTest(unittest.TestCase):
|
||||
assert Post.objects.all()[0].user_lists == [[u1, u2], [u3]]
|
||||
|
||||
def test_circular_reference(self):
|
||||
"""Ensure you can handle circular references
|
||||
"""
|
||||
"""Ensure you can handle circular references"""
|
||||
|
||||
class Relation(EmbeddedDocument):
|
||||
name = StringField()
|
||||
@@ -428,6 +424,7 @@ class FieldTest(unittest.TestCase):
|
||||
|
||||
daughter.relations.append(mother)
|
||||
daughter.relations.append(daughter)
|
||||
assert daughter._get_changed_fields() == ["relations"]
|
||||
daughter.save()
|
||||
|
||||
assert "[<Person: Mother>, <Person: Daughter>]" == "%s" % Person.objects()
|
||||
@@ -739,7 +736,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, User)
|
||||
|
||||
# Document select_related
|
||||
@@ -752,7 +749,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, User)
|
||||
|
||||
# Queryset select_related
|
||||
@@ -766,7 +763,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, User)
|
||||
|
||||
User.drop_collection()
|
||||
@@ -820,7 +817,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
# Document select_related
|
||||
@@ -836,7 +833,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
# Queryset select_related
|
||||
@@ -853,7 +850,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
Group.objects.delete()
|
||||
@@ -910,7 +907,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, UserA)
|
||||
|
||||
# Document select_related
|
||||
@@ -926,7 +923,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, UserA)
|
||||
|
||||
# Queryset select_related
|
||||
@@ -943,7 +940,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 2
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert isinstance(m, UserA)
|
||||
|
||||
UserA.drop_collection()
|
||||
@@ -997,7 +994,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
# Document select_related
|
||||
@@ -1013,7 +1010,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
# Queryset select_related
|
||||
@@ -1030,7 +1027,7 @@ class FieldTest(unittest.TestCase):
|
||||
[m for m in group_obj.members]
|
||||
assert q == 4
|
||||
|
||||
for k, m in iteritems(group_obj.members):
|
||||
for k, m in group_obj.members.items():
|
||||
assert "User" in m.__class__.__name__
|
||||
|
||||
Group.objects.delete()
|
||||
|
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import unittest
|
||||
|
||||
from mongoengine import *
|
||||
@@ -58,7 +57,9 @@ class TestSignal(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def post_save(cls, sender, document, **kwargs):
|
||||
dirty_keys = document._delta()[0].keys() + document._delta()[1].keys()
|
||||
dirty_keys = list(document._delta()[0].keys()) + list(
|
||||
document._delta()[1].keys()
|
||||
)
|
||||
signal_output.append("post_save signal, %s" % document)
|
||||
signal_output.append("post_save dirty keys, %s" % dirty_keys)
|
||||
if kwargs.pop("created", False):
|
||||
@@ -265,7 +266,7 @@ class TestSignal(unittest.TestCase):
|
||||
a = self.Author(name="Bill Shakespeare")
|
||||
a.save()
|
||||
self.get_signal_output(lambda: None) # eliminate signal output
|
||||
a1 = self.Author.objects(name="Bill Shakespeare")[0]
|
||||
_ = self.Author.objects(name="Bill Shakespeare")[0]
|
||||
|
||||
assert self.get_signal_output(create_author) == [
|
||||
"pre_init signal, Author",
|
||||
|
@@ -50,7 +50,7 @@ def _decorated_with_ver_requirement(func, mongo_version_req, oper):
|
||||
ran against MongoDB < v3.6.
|
||||
|
||||
:param mongo_version_req: The mongodb version requirement (tuple(int, int))
|
||||
:param oper: The operator to apply (e.g: operator.ge)
|
||||
:param oper: The operator to apply (e.g. operator.ge)
|
||||
"""
|
||||
|
||||
def _inner(*args, **kwargs):
|
||||
|
5
tox.ini
5
tox.ini
@@ -1,5 +1,5 @@
|
||||
[tox]
|
||||
envlist = {py27,py35,pypy,pypy3}-{mg34,mg36}
|
||||
envlist = {py35,pypy3}-{mg34,mg36,mg39,mg311}
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
@@ -7,6 +7,7 @@ commands =
|
||||
deps =
|
||||
mg34: pymongo>=3.4,<3.5
|
||||
mg36: pymongo>=3.6,<3.7
|
||||
mg39: pymongo>=3.9,<4.0
|
||||
mg39: pymongo>=3.9,<3.10
|
||||
mg311: pymongo>=3.11,<3.12
|
||||
setenv =
|
||||
PYTHON_EGG_CACHE = {envdir}/python-eggs
|
||||
|
Reference in New Issue
Block a user