diff --git a/.install_mongodb_on_travis.sh b/.install_mongodb_on_travis.sh
new file mode 100644
index 00000000..8563ae74
--- /dev/null
+++ b/.install_mongodb_on_travis.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
+
+if [ "$MONGODB" = "2.4" ]; then
+ echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
+ sudo apt-get update
+ sudo apt-get install mongodb-10gen=2.4.14
+ sudo service mongodb start
+elif [ "$MONGODB" = "2.6" ]; then
+ echo "deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen" | sudo tee /etc/apt/sources.list.d/mongodb.list
+ sudo apt-get update
+ sudo apt-get install mongodb-org-server=2.6.12
+ # service should be started automatically
+elif [ "$MONGODB" = "3.0" ]; then
+ echo "deb http://repo.mongodb.org/apt/ubuntu precise/mongodb-org/3.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb.list
+ sudo apt-get update
+ sudo apt-get install mongodb-org-server=3.0.14
+ # service should be started automatically
+else
+ echo "Invalid MongoDB version, expected 2.4, 2.6, or 3.0."
+ exit 1
+fi;
diff --git a/.travis.yml b/.travis.yml
index cb6c97e6..47448950 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,28 +1,48 @@
+# For full coverage, we'd have to test all supported Python, MongoDB, and
+# PyMongo combinations. However, that would result in an overly long build
+# with a very large number of jobs, hence we only test a subset of all the
+# combinations:
+# * MongoDB v2.4 & v3.0 are only tested against Python v2.7 & v3.5.
+# * MongoDB v2.4 is tested against PyMongo v2.7 & v3.x.
+# * MongoDB v3.0 is tested against PyMongo v3.x.
+# * MongoDB v2.6 is currently the "main" version tested against Python v2.7,
+# v3.5, PyPy & PyPy3, and PyMongo v2.7, v2.8 & v3.x.
+#
+# Reminder: Update README.rst if you change MongoDB versions we test.
+
language: python
python:
-- '2.7'
-- '3.3'
-- '3.4'
-- '3.5'
+- 2.7
+- 3.5
- pypy
- pypy3
env:
-- PYMONGO=2.7
-- PYMONGO=2.8
-- PYMONGO=3.0
-- PYMONGO=dev
+- MONGODB=2.6 PYMONGO=2.7
+- MONGODB=2.6 PYMONGO=2.8
+- MONGODB=2.6 PYMONGO=3.0
matrix:
+ # Finish the build as soon as one job fails
fast_finish: true
+ include:
+ - python: 2.7
+ env: MONGODB=2.4 PYMONGO=2.7
+ - python: 2.7
+ env: MONGODB=2.4 PYMONGO=3.0
+ - python: 2.7
+ env: MONGODB=3.0 PYMONGO=3.0
+ - python: 3.5
+ env: MONGODB=2.4 PYMONGO=2.7
+ - python: 3.5
+ env: MONGODB=2.4 PYMONGO=3.0
+ - python: 3.5
+ env: MONGODB=3.0 PYMONGO=3.0
+
before_install:
-- travis_retry sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
-- echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' |
- sudo tee /etc/apt/sources.list.d/mongodb.list
-- travis_retry sudo apt-get update
-- travis_retry sudo apt-get install mongodb-org-server
+- bash .install_mongodb_on_travis.sh
install:
- sudo apt-get install python-dev python3-dev libopenjpeg-dev zlib1g-dev libjpeg-turbo8-dev
@@ -30,14 +50,17 @@ install:
python-tk
- travis_retry pip install --upgrade pip
- travis_retry pip install coveralls
-- travis_retry pip install flake8
+- travis_retry pip install flake8 flake8-import-order
- travis_retry pip install tox>=1.9
- travis_retry pip install "virtualenv<14.0.0" # virtualenv>=14.0.0 has dropped Python 3.2 support (and pypy3 is based on py32)
- travis_retry tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- -e test
+# Cache dependencies installed via pip
+cache: pip
+
# Run flake8 for py27
before_script:
-- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then tox -e flake8; fi
+- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then flake8 .; else echo "flake8 only runs on py27"; fi
script:
- tox -e $(echo py$TRAVIS_PYTHON_VERSION-mg$PYMONGO | tr -d . | sed -e 's/pypypy/pypy/') -- --with-coverage
@@ -45,22 +68,34 @@ script:
# 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_script:
+after_success:
- if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then coveralls --verbose; fi
notifications:
irc: irc.freenode.org#mongoengine
+# Only run builds on the master branch and GitHub releases (tagged as vX.Y.Z)
branches:
only:
- master
- /^v.*$/
+# Whenever a new release is created via GitHub, publish it on PyPI.
deploy:
provider: pypi
user: the_drow
password:
secure: QMyatmWBnC6ZN3XLW2+fTBDU4LQcp1m/LjR2/0uamyeUzWKdlOoh/Wx5elOgLwt/8N9ppdPeG83ose1jOz69l5G0MUMjv8n/RIcMFSpCT59tGYqn3kh55b0cIZXFT9ar+5cxlif6a5rS72IHm5li7QQyxexJIII6Uxp0kpvUmek=
+
+ # create a source distribution and a pure python wheel for faster installs
+ distributions: "sdist bdist_wheel"
+
+ # only deploy on tagged commits (aka GitHub releases) and only for the
+ # parent repo's builds running Python 2.7 along with dev PyMongo (we run
+ # Travis against many different Python and PyMongo versions and we don't
+ # want the deploy to occur multiple times).
on:
tags: true
repo: MongoEngine/mongoengine
+ condition: "$PYMONGO = 3.0"
+ python: 2.7
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index 2668499c..5707886b 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -29,19 +29,20 @@ Style Guide
-----------
MongoEngine aims to follow `PEP8 `_
-including 4 space indents. When possible we try to stick to 79 character line limits.
-However, screens got bigger and an ORM has a strong focus on readability and
-if it can help, we accept 119 as maximum line length, in a similar way as
-`django does `_
+including 4 space indents. When possible we try to stick to 79 character line
+limits. However, screens got bigger and an ORM has a strong focus on
+readability and if it can help, we accept 119 as maximum line length, in a
+similar way as `django does
+`_
Testing
-------
All tests are run on `Travis `_
-and any pull requests are automatically tested by Travis. Any pull requests
-without tests will take longer to be integrated and might be refused.
+and any pull requests are automatically tested. Any pull requests without
+tests will take longer to be integrated and might be refused.
-You may also submit a simple failing test as a PullRequest if you don't know
+You may also submit a simple failing test as a pull request if you don't know
how to fix it, it will be easier for other people to work on it and it may get
fixed faster.
@@ -49,13 +50,18 @@ General Guidelines
------------------
- Avoid backward breaking changes if at all possible.
+- If you *have* to introduce a breaking change, make it very clear in your
+ pull request's description. Also, describe how users of this package
+ should adapt to the breaking change in docs/upgrade.rst.
- Write inline documentation for new classes and methods.
- Write tests and make sure they pass (make sure you have a mongod
running on the default port, then execute ``python setup.py nosetests``
from the cmd line to run the test suite).
-- Ensure tests pass on every Python and PyMongo versions.
- You can test on these versions locally by executing ``tox``
-- Add enhancements or problematic bug fixes to docs/changelog.rst
+- Ensure tests pass on all supported Python, PyMongo, and MongoDB versions.
+ You can test various Python and PyMongo versions locally by executing
+ ``tox``. For different MongoDB versions, you can rely on our automated
+ Travis tests.
+- Add enhancements or problematic bug fixes to docs/changelog.rst.
- Add yourself to AUTHORS :)
Documentation
@@ -69,3 +75,6 @@ just make your changes to the inline documentation of the appropriate
branch and submit a `pull request `_.
You might also use the github `Edit `_
button.
+
+If you want to test your documentation changes locally, you need to install
+the ``sphinx`` package.
diff --git a/README.rst b/README.rst
index adfa0c71..e1e2aef6 100644
--- a/README.rst
+++ b/README.rst
@@ -19,23 +19,31 @@ MongoEngine
About
=====
MongoEngine is a Python Object-Document Mapper for working with MongoDB.
-Documentation available at https://mongoengine-odm.readthedocs.io - there is currently
-a `tutorial `_, a `user guide
-`_ and an `API reference
-`_.
+Documentation is available at https://mongoengine-odm.readthedocs.io - there
+is currently a `tutorial `_,
+a `user guide `_, and
+an `API reference `_.
+
+Supported MongoDB Versions
+==========================
+MongoEngine is currently tested against MongoDB v2.4, v2.6, and v3.0. Future
+versions should be supported as well, but aren't actively tested at the moment.
+Make sure to open an issue or submit a pull request if you experience any
+problems with MongoDB v3.2+.
Installation
============
We recommend the use of `virtualenv `_ and of
`pip `_. You can then use ``pip install -U mongoengine``.
-You may also have `setuptools `_ and thus
-you can use ``easy_install -U mongoengine``. Otherwise, you can download the
+You may also have `setuptools `_
+and thus you can use ``easy_install -U mongoengine``. Otherwise, you can download the
source from `GitHub `_ and run ``python
setup.py install``.
Dependencies
============
-All of the dependencies can easily be installed via `pip `_. At the very least, you'll need these two packages to use MongoEngine:
+All of the dependencies can easily be installed via `pip `_.
+At the very least, you'll need these two packages to use MongoEngine:
- pymongo>=2.7.1
- six>=1.10.0
@@ -48,10 +56,6 @@ If you need to use an ``ImageField`` or ``ImageGridFsProxy``:
- Pillow>=2.0.0
-If you want to generate the documentation (e.g. to contribute to it):
-
-- sphinx
-
Examples
========
Some simple examples of what MongoEngine code looks like:
@@ -110,11 +114,11 @@ Some simple examples of what MongoEngine code looks like:
Tests
=====
To run the test suite, ensure you are running a local instance of MongoDB on
-the standard port and have ``nose`` installed. Then, run: ``python setup.py nosetests``.
+the standard port and have ``nose`` installed. Then, run ``python setup.py nosetests``.
-To run the test suite on every supported Python version and every supported PyMongo version,
-you can use ``tox``.
-tox and each supported Python version should be installed in your environment:
+To run the test suite on every supported Python and PyMongo version, you can
+use ``tox``. You'll need to make sure you have each supported Python version
+installed in your environment and then:
.. code-block:: shell
@@ -123,13 +127,16 @@ tox and each supported Python version should be installed in your environment:
# Run the test suites
$ tox
-If you wish to run one single or selected tests, use the nosetest convention. It will find the folder,
-eventually the file, go to the TestClass specified after the colon and eventually right to the single test.
-Also use the -s argument if you want to print out whatever or access pdb while testing.
+If you wish to run a subset of tests, use the nosetests convention:
.. code-block:: shell
- $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest.test_cls_field -s
+ # Run all the tests in a particular test file
+ $ python setup.py nosetests --tests tests/fields/fields.py
+ # Run only particular test class in that file
+ $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest
+ # Use the -s option if you want to print some debug statements or use pdb
+ $ python setup.py nosetests --tests tests/fields/fields.py:FieldTest -s
Community
=========
diff --git a/tests/document/indexes.py b/tests/document/indexes.py
index af93e7db..5d219eda 100644
--- a/tests/document/indexes.py
+++ b/tests/document/indexes.py
@@ -2,14 +2,14 @@
import unittest
import sys
-
-import pymongo
-
from nose.plugins.skip import SkipTest
from datetime import datetime
+import pymongo
from mongoengine import *
-from mongoengine.connection import get_db, get_connection
+from mongoengine.connection import get_db
+
+from tests.utils import get_mongodb_version, needs_mongodb_v26
__all__ = ("IndexesTest", )
@@ -494,8 +494,7 @@ class IndexesTest(unittest.TestCase):
obj = Test(a=1)
obj.save()
- connection = get_connection()
- IS_MONGODB_3 = connection.server_info()['versionArray'][0] >= 3
+ IS_MONGODB_3 = get_mongodb_version()[0] >= 3
# Need to be explicit about covered indexes as mongoDB doesn't know if
# the documents returned might have more keys in that here.
@@ -733,14 +732,6 @@ class IndexesTest(unittest.TestCase):
Log.drop_collection()
- if pymongo.version_tuple[0] < 2 and pymongo.version_tuple[1] < 3:
- raise SkipTest('pymongo needs to be 2.3 or higher for this test')
-
- connection = get_connection()
- version_array = connection.server_info()['versionArray']
- if version_array[0] < 2 and version_array[1] < 2:
- raise SkipTest('MongoDB needs to be 2.2 or higher for this test')
-
# Indexes are lazy so use list() to perform query
list(Log.objects)
info = Log.objects._collection.index_information()
@@ -874,8 +865,8 @@ class IndexesTest(unittest.TestCase):
info['provider_ids.foo_1_provider_ids.bar_1']['key'])
self.assertTrue(info['provider_ids.foo_1_provider_ids.bar_1']['sparse'])
+ @needs_mongodb_v26
def test_text_indexes(self):
-
class Book(Document):
title = DictField()
meta = {
diff --git a/tests/queryset/geo.py b/tests/queryset/geo.py
index d10c51cd..51a32382 100644
--- a/tests/queryset/geo.py
+++ b/tests/queryset/geo.py
@@ -1,105 +1,139 @@
-from datetime import datetime, timedelta
+import datetime
import unittest
-from pymongo.errors import OperationFailure
from mongoengine import *
-from mongoengine.connection import get_connection
-from nose.plugins.skip import SkipTest
+
+from tests.utils import MongoDBTestCase, needs_mongodb_v3
__all__ = ("GeoQueriesTest",)
-class GeoQueriesTest(unittest.TestCase):
+class GeoQueriesTest(MongoDBTestCase):
- def setUp(self):
- connect(db='mongoenginetest')
-
- def test_geospatial_operators(self):
- """Ensure that geospatial queries are working.
- """
+ def _create_event_data(self, point_field_class=GeoPointField):
+ """Create some sample data re-used in many of the tests below."""
class Event(Document):
title = StringField()
date = DateTimeField()
- location = GeoPointField()
+ location = point_field_class()
def __unicode__(self):
return self.title
+ self.Event = Event
+
Event.drop_collection()
- event1 = Event(title="Coltrane Motion @ Double Door",
- date=datetime.now() - timedelta(days=1),
- location=[-87.677137, 41.909889]).save()
- event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
- date=datetime.now() - timedelta(days=10),
- location=[-122.4194155, 37.7749295]).save()
- event3 = Event(title="Coltrane Motion @ Empty Bottle",
- date=datetime.now(),
- location=[-87.686638, 41.900474]).save()
+ event1 = Event.objects.create(
+ title="Coltrane Motion @ Double Door",
+ date=datetime.datetime.now() - datetime.timedelta(days=1),
+ location=[-87.677137, 41.909889])
+ event2 = Event.objects.create(
+ title="Coltrane Motion @ Bottom of the Hill",
+ date=datetime.datetime.now() - datetime.timedelta(days=10),
+ location=[-122.4194155, 37.7749295])
+ event3 = Event.objects.create(
+ title="Coltrane Motion @ Empty Bottle",
+ date=datetime.datetime.now(),
+ location=[-87.686638, 41.900474])
+
+ return event1, event2, event3
+
+ def test_near(self):
+ """Make sure the "near" operator works."""
+ event1, event2, event3 = self._create_event_data()
# find all events "near" pitchfork office, chicago.
# note that "near" will show the san francisco event, too,
# although it sorts to last.
- events = Event.objects(location__near=[-87.67892, 41.9120459])
+ events = self.Event.objects(location__near=[-87.67892, 41.9120459])
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
+ # ensure ordering is respected by "near"
+ events = self.Event.objects(location__near=[-87.67892, 41.9120459])
+ events = events.order_by("-date")
+ self.assertEqual(events.count(), 3)
+ self.assertEqual(list(events), [event3, event1, event2])
+
+ def test_near_and_max_distance(self):
+ """Ensure the "max_distance" operator works alongside the "near"
+ operator.
+ """
+ event1, event2, event3 = self._create_event_data()
+
+ # find events within 10 degrees of san francisco
+ point = [-122.415579, 37.7566023]
+ events = self.Event.objects(location__near=point,
+ location__max_distance=10)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0], event2)
+
+ # $minDistance was added in MongoDB v2.6, but continued being buggy
+ # until v3.0; skip for older versions
+ @needs_mongodb_v3
+ def test_near_and_min_distance(self):
+ """Ensure the "min_distance" operator works alongside the "near"
+ operator.
+ """
+ event1, event2, event3 = self._create_event_data()
+
+ # find events at least 10 degrees away of san francisco
+ point = [-122.415579, 37.7566023]
+ events = self.Event.objects(location__near=point,
+ location__min_distance=10)
+ self.assertEqual(events.count(), 2)
+
+ def test_within_distance(self):
+ """Make sure the "within_distance" operator works."""
+ event1, event2, event3 = self._create_event_data()
+
# find events within 5 degrees of pitchfork office, chicago
point_and_distance = [[-87.67892, 41.9120459], 5]
- events = Event.objects(location__within_distance=point_and_distance)
+ events = self.Event.objects(
+ location__within_distance=point_and_distance)
self.assertEqual(events.count(), 2)
events = list(events)
self.assertTrue(event2 not in events)
self.assertTrue(event1 in events)
self.assertTrue(event3 in events)
- # ensure ordering is respected by "near"
- events = Event.objects(location__near=[-87.67892, 41.9120459])
- events = events.order_by("-date")
- self.assertEqual(events.count(), 3)
- self.assertEqual(list(events), [event3, event1, event2])
-
- # find events within 10 degrees of san francisco
- point = [-122.415579, 37.7566023]
- events = Event.objects(location__near=point, location__max_distance=10)
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0], event2)
-
- # find events at least 10 degrees away of san francisco
- point = [-122.415579, 37.7566023]
- events = Event.objects(location__near=point, location__min_distance=10)
- # The following real test passes on MongoDB 3 but minDistance seems
- # buggy on older MongoDB versions
- if get_connection().server_info()['versionArray'][0] > 2:
- self.assertEqual(events.count(), 2)
- else:
- self.assertTrue(events.count() >= 2)
-
# find events within 10 degrees of san francisco
point_and_distance = [[-122.415579, 37.7566023], 10]
- events = Event.objects(location__within_distance=point_and_distance)
+ events = self.Event.objects(
+ location__within_distance=point_and_distance)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0], event2)
# find events within 1 degree of greenpoint, broolyn, nyc, ny
point_and_distance = [[-73.9509714, 40.7237134], 1]
- events = Event.objects(location__within_distance=point_and_distance)
+ events = self.Event.objects(
+ location__within_distance=point_and_distance)
self.assertEqual(events.count(), 0)
# ensure ordering is respected by "within_distance"
point_and_distance = [[-87.67892, 41.9120459], 10]
- events = Event.objects(location__within_distance=point_and_distance)
+ events = self.Event.objects(
+ location__within_distance=point_and_distance)
events = events.order_by("-date")
self.assertEqual(events.count(), 2)
self.assertEqual(events[0], event3)
+ def test_within_box(self):
+ """Ensure the "within_box" operator works."""
+ event1, event2, event3 = self._create_event_data()
+
# check that within_box works
box = [(-125.0, 35.0), (-100.0, 40.0)]
- events = Event.objects(location__within_box=box)
+ events = self.Event.objects(location__within_box=box)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event2.id)
+ def test_within_polygon(self):
+ """Ensure the "within_polygon" operator works."""
+ event1, event2, event3 = self._create_event_data()
+
polygon = [
(-87.694445, 41.912114),
(-87.69084, 41.919395),
@@ -107,7 +141,7 @@ class GeoQueriesTest(unittest.TestCase):
(-87.654276, 41.911731),
(-87.656164, 41.898061),
]
- events = Event.objects(location__within_polygon=polygon)
+ events = self.Event.objects(location__within_polygon=polygon)
self.assertEqual(events.count(), 1)
self.assertEqual(events[0].id, event1.id)
@@ -116,13 +150,151 @@ class GeoQueriesTest(unittest.TestCase):
(-1.225891, 52.792797),
(-4.40094, 53.389881)
]
- events = Event.objects(location__within_polygon=polygon2)
+ events = self.Event.objects(location__within_polygon=polygon2)
self.assertEqual(events.count(), 0)
- def test_geo_spatial_embedded(self):
+ def test_2dsphere_near(self):
+ """Make sure the "near" operator works with a PointField, which
+ corresponds to a 2dsphere index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+ # find all events "near" pitchfork office, chicago.
+ # note that "near" will show the san francisco event, too,
+ # although it sorts to last.
+ events = self.Event.objects(location__near=[-87.67892, 41.9120459])
+ self.assertEqual(events.count(), 3)
+ self.assertEqual(list(events), [event1, event3, event2])
+
+ # ensure ordering is respected by "near"
+ events = self.Event.objects(location__near=[-87.67892, 41.9120459])
+ events = events.order_by("-date")
+ self.assertEqual(events.count(), 3)
+ self.assertEqual(list(events), [event3, event1, event2])
+
+ def test_2dsphere_near_and_max_distance(self):
+ """Ensure the "max_distance" operator works alongside the "near"
+ operator with a 2dsphere index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+
+ # find events within 10km of san francisco
+ point = [-122.415579, 37.7566023]
+ events = self.Event.objects(location__near=point,
+ location__max_distance=10000)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0], event2)
+
+ # find events within 1km of greenpoint, broolyn, nyc, ny
+ events = self.Event.objects(location__near=[-73.9509714, 40.7237134],
+ location__max_distance=1000)
+ self.assertEqual(events.count(), 0)
+
+ # ensure ordering is respected by "near"
+ events = self.Event.objects(
+ location__near=[-87.67892, 41.9120459],
+ location__max_distance=10000
+ ).order_by("-date")
+ self.assertEqual(events.count(), 2)
+ self.assertEqual(events[0], event3)
+
+ def test_2dsphere_geo_within_box(self):
+ """Ensure the "geo_within_box" operator works with a 2dsphere
+ index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+
+ # check that within_box works
+ box = [(-125.0, 35.0), (-100.0, 40.0)]
+ events = self.Event.objects(location__geo_within_box=box)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0].id, event2.id)
+
+ def test_2dsphere_geo_within_polygon(self):
+ """Ensure the "geo_within_polygon" operator works with a
+ 2dsphere index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+
+ polygon = [
+ (-87.694445, 41.912114),
+ (-87.69084, 41.919395),
+ (-87.681742, 41.927186),
+ (-87.654276, 41.911731),
+ (-87.656164, 41.898061),
+ ]
+ events = self.Event.objects(location__geo_within_polygon=polygon)
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0].id, event1.id)
+
+ polygon2 = [
+ (-1.742249, 54.033586),
+ (-1.225891, 52.792797),
+ (-4.40094, 53.389881)
+ ]
+ events = self.Event.objects(location__geo_within_polygon=polygon2)
+ self.assertEqual(events.count(), 0)
+
+ # $minDistance was added in MongoDB v2.6, but continued being buggy
+ # until v3.0; skip for older versions
+ @needs_mongodb_v3
+ def test_2dsphere_near_and_min_max_distance(self):
+ """Ensure "min_distace" and "max_distance" operators work well
+ together with the "near" operator in a 2dsphere index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+
+ # ensure min_distance and max_distance combine well
+ events = self.Event.objects(
+ location__near=[-87.67892, 41.9120459],
+ location__min_distance=1000,
+ location__max_distance=10000
+ ).order_by("-date")
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0], event3)
+
+ # ensure ordering is respected by "near" with "min_distance"
+ events = self.Event.objects(
+ location__near=[-87.67892, 41.9120459],
+ location__min_distance=10000
+ ).order_by("-date")
+ self.assertEqual(events.count(), 1)
+ self.assertEqual(events[0], event2)
+
+ def test_2dsphere_geo_within_center(self):
+ """Make sure the "geo_within_center" operator works with a
+ 2dsphere index.
+ """
+ event1, event2, event3 = self._create_event_data(
+ point_field_class=PointField
+ )
+
+ # find events within 5 degrees of pitchfork office, chicago
+ point_and_distance = [[-87.67892, 41.9120459], 2]
+ events = self.Event.objects(
+ location__geo_within_center=point_and_distance)
+ self.assertEqual(events.count(), 2)
+ events = list(events)
+ self.assertTrue(event2 not in events)
+ self.assertTrue(event1 in events)
+ self.assertTrue(event3 in events)
+
+ def _test_embedded(self, point_field_class):
+ """Helper test method ensuring given point field class works
+ well in an embedded document.
+ """
class Venue(EmbeddedDocument):
- location = GeoPointField()
+ location = point_field_class()
name = StringField()
class Event(Document):
@@ -148,16 +320,18 @@ class GeoQueriesTest(unittest.TestCase):
self.assertEqual(events.count(), 3)
self.assertEqual(list(events), [event1, event3, event2])
- def test_spherical_geospatial_operators(self):
- """Ensure that spherical geospatial queries are working
- """
- # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
- connection = get_connection()
- info = connection.test.command('buildInfo')
- mongodb_version = tuple([int(i) for i in info['version'].split('.')])
- if mongodb_version < (2, 6, 4):
- raise SkipTest("Need MongoDB version 2.6.4+")
+ def test_geo_spatial_embedded(self):
+ """Make sure GeoPointField works properly in an embedded document."""
+ self._test_embedded(point_field_class=GeoPointField)
+ def test_2dsphere_point_embedded(self):
+ """Make sure PointField works properly in an embedded document."""
+ self._test_embedded(point_field_class=PointField)
+
+ # Needs MongoDB > 2.6.4 https://jira.mongodb.org/browse/SERVER-14039
+ @needs_mongodb_v3
+ def test_spherical_geospatial_operators(self):
+ """Ensure that spherical geospatial queries are working."""
class Point(Document):
location = GeoPointField()
@@ -177,7 +351,10 @@ class GeoQueriesTest(unittest.TestCase):
# Same behavior for _within_spherical_distance
points = Point.objects(
- location__within_spherical_distance=[[-122, 37.5], 60 / earth_radius]
+ location__within_spherical_distance=[
+ [-122, 37.5],
+ 60 / earth_radius
+ ]
)
self.assertEqual(points.count(), 2)
@@ -194,14 +371,9 @@ class GeoQueriesTest(unittest.TestCase):
# Test query works with min_distance, being farer from one point
points = Point.objects(location__near_sphere=[-122, 37.8],
location__min_distance=60 / earth_radius)
- # The following real test passes on MongoDB 3 but minDistance seems
- # buggy on older MongoDB versions
- if get_connection().server_info()['versionArray'][0] > 2:
- self.assertEqual(points.count(), 1)
- far_point = points.first()
- self.assertNotEqual(close_point, far_point)
- else:
- self.assertTrue(points.count() >= 1)
+ self.assertEqual(points.count(), 1)
+ far_point = points.first()
+ self.assertNotEqual(close_point, far_point)
# Finds both points, but orders the north point first because it's
# closer to the reference point to the north.
@@ -220,141 +392,15 @@ class GeoQueriesTest(unittest.TestCase):
# Finds only one point because only the first point is within 60km of
# the reference point to the south.
points = Point.objects(
- location__within_spherical_distance=[[-122, 36.5], 60/earth_radius])
+ location__within_spherical_distance=[
+ [-122, 36.5],
+ 60 / earth_radius
+ ]
+ )
self.assertEqual(points.count(), 1)
self.assertEqual(points[0].id, south_point.id)
- def test_2dsphere_point(self):
-
- class Event(Document):
- title = StringField()
- date = DateTimeField()
- location = PointField()
-
- def __unicode__(self):
- return self.title
-
- Event.drop_collection()
-
- event1 = Event(title="Coltrane Motion @ Double Door",
- date=datetime.now() - timedelta(days=1),
- location=[-87.677137, 41.909889])
- event1.save()
- event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
- date=datetime.now() - timedelta(days=10),
- location=[-122.4194155, 37.7749295]).save()
- event3 = Event(title="Coltrane Motion @ Empty Bottle",
- date=datetime.now(),
- location=[-87.686638, 41.900474]).save()
-
- # find all events "near" pitchfork office, chicago.
- # note that "near" will show the san francisco event, too,
- # although it sorts to last.
- events = Event.objects(location__near=[-87.67892, 41.9120459])
- self.assertEqual(events.count(), 3)
- self.assertEqual(list(events), [event1, event3, event2])
-
- # find events within 5 degrees of pitchfork office, chicago
- point_and_distance = [[-87.67892, 41.9120459], 2]
- events = Event.objects(location__geo_within_center=point_and_distance)
- self.assertEqual(events.count(), 2)
- events = list(events)
- self.assertTrue(event2 not in events)
- self.assertTrue(event1 in events)
- self.assertTrue(event3 in events)
-
- # ensure ordering is respected by "near"
- events = Event.objects(location__near=[-87.67892, 41.9120459])
- events = events.order_by("-date")
- self.assertEqual(events.count(), 3)
- self.assertEqual(list(events), [event3, event1, event2])
-
- # find events within 10km of san francisco
- point = [-122.415579, 37.7566023]
- events = Event.objects(location__near=point, location__max_distance=10000)
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0], event2)
-
- # find events within 1km of greenpoint, broolyn, nyc, ny
- events = Event.objects(location__near=[-73.9509714, 40.7237134], location__max_distance=1000)
- self.assertEqual(events.count(), 0)
-
- # ensure ordering is respected by "near"
- events = Event.objects(location__near=[-87.67892, 41.9120459],
- location__max_distance=10000).order_by("-date")
- self.assertEqual(events.count(), 2)
- self.assertEqual(events[0], event3)
-
- # ensure min_distance and max_distance combine well
- events = Event.objects(location__near=[-87.67892, 41.9120459],
- location__min_distance=1000,
- location__max_distance=10000).order_by("-date")
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0], event3)
-
- # ensure ordering is respected by "near"
- events = Event.objects(location__near=[-87.67892, 41.9120459],
- # location__min_distance=10000
- location__min_distance=10000).order_by("-date")
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0], event2)
-
- # check that within_box works
- box = [(-125.0, 35.0), (-100.0, 40.0)]
- events = Event.objects(location__geo_within_box=box)
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0].id, event2.id)
-
- polygon = [
- (-87.694445, 41.912114),
- (-87.69084, 41.919395),
- (-87.681742, 41.927186),
- (-87.654276, 41.911731),
- (-87.656164, 41.898061),
- ]
- events = Event.objects(location__geo_within_polygon=polygon)
- self.assertEqual(events.count(), 1)
- self.assertEqual(events[0].id, event1.id)
-
- polygon2 = [
- (-1.742249, 54.033586),
- (-1.225891, 52.792797),
- (-4.40094, 53.389881)
- ]
- events = Event.objects(location__geo_within_polygon=polygon2)
- self.assertEqual(events.count(), 0)
-
- def test_2dsphere_point_embedded(self):
-
- class Venue(EmbeddedDocument):
- location = GeoPointField()
- name = StringField()
-
- class Event(Document):
- title = StringField()
- venue = EmbeddedDocumentField(Venue)
-
- Event.drop_collection()
-
- venue1 = Venue(name="The Rock", location=[-87.677137, 41.909889])
- venue2 = Venue(name="The Bridge", location=[-122.4194155, 37.7749295])
-
- event1 = Event(title="Coltrane Motion @ Double Door",
- venue=venue1).save()
- event2 = Event(title="Coltrane Motion @ Bottom of the Hill",
- venue=venue2).save()
- event3 = Event(title="Coltrane Motion @ Empty Bottle",
- venue=venue1).save()
-
- # find all events "near" pitchfork office, chicago.
- # note that "near" will show the san francisco event, too,
- # although it sorts to last.
- events = Event.objects(venue__location__near=[-87.67892, 41.9120459])
- self.assertEqual(events.count(), 3)
- self.assertEqual(list(events), [event1, event3, event2])
-
def test_linestring(self):
-
class Road(Document):
name = StringField()
line = LineStringField()
@@ -410,7 +456,6 @@ class GeoQueriesTest(unittest.TestCase):
self.assertEqual(1, roads)
def test_polygon(self):
-
class Road(Document):
name = StringField()
poly = PolygonField()
@@ -507,5 +552,6 @@ class GeoQueriesTest(unittest.TestCase):
loc = Location.objects.as_pymongo()[0]
self.assertEqual(loc["poly"], {"type": "Polygon", "coordinates": [[[40, 4], [40, 6], [41, 6], [40, 4]]]})
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/queryset/queryset.py b/tests/queryset/queryset.py
index c54fa13d..b496e04a 100644
--- a/tests/queryset/queryset.py
+++ b/tests/queryset/queryset.py
@@ -19,6 +19,9 @@ from mongoengine.python_support import IS_PYMONGO_3
from mongoengine.queryset import (DoesNotExist, MultipleObjectsReturned,
QuerySet, QuerySetManager, queryset_manager)
+from tests.utils import needs_mongodb_v26, skip_pymongo3
+
+
__all__ = ("QuerySetTest",)
@@ -32,37 +35,6 @@ class db_ops_tracker(query_counter):
return list(self.db.system.profile.find(ignore_query))
-def skip_older_mongodb(f):
- def _inner(*args, **kwargs):
- connection = get_connection()
- info = connection.test.command('buildInfo')
- mongodb_version = tuple([int(i) for i in info['version'].split('.')])
-
- if mongodb_version < (2, 6):
- raise SkipTest("Need MongoDB version 2.6+")
-
- return f(*args, **kwargs)
-
- _inner.__name__ = f.__name__
- _inner.__doc__ = f.__doc__
-
- return _inner
-
-
-def skip_pymongo3(f):
- def _inner(*args, **kwargs):
-
- if IS_PYMONGO_3:
- raise SkipTest("Useless with PyMongo 3+")
-
- return f(*args, **kwargs)
-
- _inner.__name__ = f.__name__
- _inner.__doc__ = f.__doc__
-
- return _inner
-
-
class QuerySetTest(unittest.TestCase):
def setUp(self):
@@ -599,16 +571,23 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(post.comments[0].by, 'joe')
self.assertEqual(post.comments[0].votes.score, 4)
+ @needs_mongodb_v26
def test_update_min_max(self):
class Scores(Document):
high_score = IntField()
low_score = IntField()
- scores = Scores(high_score=800, low_score=200)
- scores.save()
+
+ scores = Scores.objects.create(high_score=800, low_score=200)
+
Scores.objects(id=scores.id).update(min__low_score=150)
- self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
+ self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150)
Scores.objects(id=scores.id).update(min__low_score=250)
- self.assertEqual(Scores.objects(id=scores.id).get().low_score, 150)
+ self.assertEqual(Scores.objects.get(id=scores.id).low_score, 150)
+
+ Scores.objects(id=scores.id).update(max__high_score=1000)
+ self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000)
+ Scores.objects(id=scores.id).update(max__high_score=500)
+ self.assertEqual(Scores.objects.get(id=scores.id).high_score, 1000)
def test_updates_can_have_match_operators(self):
@@ -1012,7 +991,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(person.name, "User A")
self.assertEqual(person.age, 20)
- @skip_older_mongodb
+ @needs_mongodb_v26
@skip_pymongo3
def test_cursor_args(self):
"""Ensures the cursor args can be set as expected
@@ -3129,7 +3108,7 @@ class QuerySetTest(unittest.TestCase):
self.assertEqual(Foo.objects.distinct("bar"), [bar])
- @skip_older_mongodb
+ @needs_mongodb_v26
def test_text_indexes(self):
class News(Document):
title = StringField()
@@ -3216,7 +3195,7 @@ class QuerySetTest(unittest.TestCase):
'brasil').order_by('$text_score').first()
self.assertEqual(item.get_text_score(), max_text_score)
- @skip_older_mongodb
+ @needs_mongodb_v26
def test_distinct_handles_references_to_alias(self):
register_connection('testdb', 'mongoenginetest2')
@@ -4891,6 +4870,7 @@ class QuerySetTest(unittest.TestCase):
self.assertTrue(Person.objects._has_data(),
'Cursor has data and returned False')
+ @needs_mongodb_v26
def test_queryset_aggregation_framework(self):
class Person(Document):
name = StringField()
@@ -4925,17 +4905,13 @@ class QuerySetTest(unittest.TestCase):
{'_id': p1.pk, 'name': "ISABELLA LUANNA"}
])
- data = Person.objects(
- age__gte=17, age__lte=40).order_by('-age').aggregate(
- {'$group': {
- '_id': None,
- 'total': {'$sum': 1},
- 'avg': {'$avg': '$age'}
- }
- }
-
- )
-
+ data = Person.objects(age__gte=17, age__lte=40).order_by('-age').aggregate({
+ '$group': {
+ '_id': None,
+ 'total': {'$sum': 1},
+ 'avg': {'$avg': '$age'}
+ }
+ })
self.assertEqual(list(data), [
{'_id': None, 'avg': 29, 'total': 2}
])
@@ -4976,11 +4952,13 @@ class QuerySetTest(unittest.TestCase):
self.assertEquals(Animal.objects(folded_ears=True).count(), 1)
self.assertEquals(Animal.objects(whiskers_length=5.1).count(), 1)
- def test_loop_via_invalid_id_does_not_crash(self):
+ def test_loop_over_invalid_id_does_not_crash(self):
class Person(Document):
name = StringField()
- Person.objects.delete()
- Person._get_collection().update({"name": "a"}, {"$set": {"_id": ""}}, upsert=True)
+
+ Person.drop_collection()
+
+ Person._get_collection().insert({'name': 'a', 'id': ''})
for p in Person.objects():
self.assertEqual(p.name, 'a')
diff --git a/tests/test_connection.py b/tests/test_connection.py
index a1d3bfb6..d74f68c7 100644
--- a/tests/test_connection.py
+++ b/tests/test_connection.py
@@ -35,8 +35,7 @@ class ConnectionTest(unittest.TestCase):
mongoengine.connection._dbs = {}
def test_connect(self):
- """Ensure that the connect() method works properly.
- """
+ """Ensure that the connect() method works properly."""
connect('mongoenginetest')
conn = get_connection()
@@ -146,8 +145,7 @@ class ConnectionTest(unittest.TestCase):
self.assertEqual(expected_connection, actual_connection)
def test_connect_uri(self):
- """Ensure that the connect() method works properly with uri's
- """
+ """Ensure that the connect() method works properly with URIs."""
c = connect(db='mongoenginetest', alias='admin')
c.admin.system.users.remove({})
c.mongoenginetest.system.users.remove({})
@@ -227,9 +225,8 @@ class ConnectionTest(unittest.TestCase):
self.assertRaises(OperationFailure, get_db)
def test_connect_uri_with_authsource(self):
- """Ensure that the connect() method works well with
- the option `authSource` in URI.
- This feature was introduced in MongoDB 2.4 and removed in 2.6
+ """Ensure that the connect() method works well with `authSource`
+ option in the URI.
"""
# Create users
c = connect('mongoenginetest')
@@ -238,30 +235,31 @@ class ConnectionTest(unittest.TestCase):
# Authentication fails without "authSource"
if IS_PYMONGO_3:
- test_conn = connect('mongoenginetest', alias='test1',
- host='mongodb://username2:password@localhost/mongoenginetest')
+ test_conn = connect(
+ 'mongoenginetest', alias='test1',
+ host='mongodb://username2:password@localhost/mongoenginetest'
+ )
self.assertRaises(OperationFailure, test_conn.server_info)
else:
self.assertRaises(
- MongoEngineConnectionError, connect, 'mongoenginetest',
- alias='test1',
+ MongoEngineConnectionError,
+ connect, 'mongoenginetest', alias='test1',
host='mongodb://username2:password@localhost/mongoenginetest'
)
self.assertRaises(MongoEngineConnectionError, get_db, 'test1')
# Authentication succeeds with "authSource"
- connect(
+ authd_conn = connect(
'mongoenginetest', alias='test2',
host=('mongodb://username2:password@localhost/'
'mongoenginetest?authSource=admin')
)
- # This will fail starting from MongoDB 2.6+
db = get_db('test2')
self.assertTrue(isinstance(db, pymongo.database.Database))
self.assertEqual(db.name, 'mongoenginetest')
# Clear all users
- c.admin.system.users.remove({})
+ authd_conn.admin.system.users.remove({})
def test_register_connection(self):
"""Ensure that connections with different aliases may be registered.
diff --git a/tests/utils.py b/tests/utils.py
index 128bbff0..4566d864 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,7 +1,11 @@
import unittest
+from nose.plugins.skip import SkipTest
+
from mongoengine import connect
-from mongoengine.connection import get_db
+from mongoengine.connection import get_db, get_connection
+from mongoengine.python_support import IS_PYMONGO_3
+
MONGO_TEST_DB = 'mongoenginetest'
@@ -20,3 +24,55 @@ class MongoDBTestCase(unittest.TestCase):
@classmethod
def tearDownClass(cls):
cls._connection.drop_database(MONGO_TEST_DB)
+
+
+def get_mongodb_version():
+ """Return the version tuple of the MongoDB server that the default
+ connection is connected to.
+ """
+ return tuple(get_connection().server_info()['versionArray'])
+
+def _decorated_with_ver_requirement(func, ver_tuple):
+ """Return a given function decorated with the version requirement
+ for a particular MongoDB version tuple.
+ """
+ def _inner(*args, **kwargs):
+ mongodb_ver = get_mongodb_version()
+ if mongodb_ver >= ver_tuple:
+ return func(*args, **kwargs)
+
+ raise SkipTest('Needs MongoDB v{}+'.format(
+ '.'.join([str(v) for v in ver_tuple])
+ ))
+
+ _inner.__name__ = func.__name__
+ _inner.__doc__ = func.__doc__
+
+ return _inner
+
+def needs_mongodb_v26(func):
+ """Raise a SkipTest exception if we're working with MongoDB version
+ lower than v2.6.
+ """
+ return _decorated_with_ver_requirement(func, (2, 6))
+
+def needs_mongodb_v3(func):
+ """Raise a SkipTest exception if we're working with MongoDB version
+ lower than v3.0.
+ """
+ return _decorated_with_ver_requirement(func, (3, 0))
+
+def skip_pymongo3(f):
+ """Raise a SkipTest exception if we're running a test against
+ PyMongo v3.x.
+ """
+ def _inner(*args, **kwargs):
+ if IS_PYMONGO_3:
+ raise SkipTest("Useless with PyMongo 3+")
+ return f(*args, **kwargs)
+
+ _inner.__name__ = f.__name__
+ _inner.__doc__ = f.__doc__
+
+ return _inner
+
diff --git a/tox.ini b/tox.ini
index d6052edf..7f0d36e4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = {py26,py27,py33,py34,py35,pypy,pypy3}-{mg27,mg28},flake8
+envlist = {py27,py35,pypy,pypy3}-{mg27,mg28,mg30}
[testenv]
commands =
@@ -7,16 +7,7 @@ commands =
deps =
nose
mg27: PyMongo<2.8
- mg28: PyMongo>=2.8,<3.0
+ mg28: PyMongo>=2.8,<2.9
mg30: PyMongo>=3.0
- mgdev: https://github.com/mongodb/mongo-python-driver/tarball/master
setenv =
PYTHON_EGG_CACHE = {envdir}/python-eggs
-passenv = windir
-
-[testenv:flake8]
-deps =
- flake8
- flake8-import-order
-commands =
- flake8