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