Permalink
Browse files

Initial import from my fork of PyMongo

  • Loading branch information...
1 parent b91d3cc commit 3d6089eeeea36ac808c6c409c36b8c13f1901bba @ajdavis ajdavis committed Jan 15, 2013
Showing with 10,482 additions and 7 deletions.
  1. +2 −0 .gitignore
  2. +22 −0 CONTRIBUTING.rst
  3. +202 −0 LICENSE
  4. +5 −0 MANIFEST.in
  5. +10 −7 README.rst
  6. +89 −0 doc/Makefile
  7. +1 −0 doc/__init__.py
  8. +81 −0 doc/api/generator_interface.rst
  9. +73 −0 doc/api/gridfs.rst
  10. +17 −0 doc/api/index.rst
  11. +59 −0 doc/api/motor_client.rst
  12. +53 −0 doc/api/motor_collection.rst
  13. +36 −0 doc/api/motor_cursor.rst
  14. +46 −0 doc/api/motor_database.rst
  15. +53 −0 doc/api/motor_replica_set_client.rst
  16. +12 −0 doc/api/web.rst
  17. +2 −0 doc/changelog.rst
  18. +155 −0 doc/conf.py
  19. +7 −0 doc/contributors.rst
  20. +415 −0 doc/differences.rst
  21. +55 −0 doc/examples/authentication.rst
  22. +190 −0 doc/examples/callbacks-and-generators.rst
  23. +129 −0 doc/examples/gridfs.rst
  24. +13 −0 doc/examples/index.rst
  25. +38 −0 doc/examples/tailable-cursors.rst
  26. +72 −0 doc/features.rst
  27. +25 −0 doc/index.rst
  28. +113 −0 doc/make.bat
  29. +104 −0 doc/mongo_extensions.py
  30. +237 −0 doc/motor_extensions.py
  31. +8 −0 doc/prerequisites.rst
  32. +585 −0 doc/tutorial.rst
  33. +1,865 −0 motor/__init__.py
  34. +153 −0 motor/web.py
  35. +3 −0 requirements.txt
  36. +57 −0 setup.py
  37. +304 −0 test/__init__.py
  38. 0 test/high_availability/__init__.py
  39. +1,044 −0 test/high_availability/test_motor_ha.py
  40. +118 −0 test/high_availability/test_motor_ha_utils.py
  41. +586 −0 test/synchro/__init__.py
  42. +187 −0 test/synchro/synchrotest.py
  43. +386 −0 test/test_motor_client.py
  44. +624 −0 test/test_motor_collection.py
  45. +546 −0 test/test_motor_cursor.py
  46. +170 −0 test/test_motor_database.py
  47. +111 −0 test/test_motor_gen.py
  48. +378 −0 test/test_motor_grid_file.py
  49. +267 −0 test/test_motor_gridfs.py
  50. +60 −0 test/test_motor_ipv6.py
  51. +208 −0 test/test_motor_replica_set.py
  52. +63 −0 test/test_motor_ssl.py
  53. +210 −0 test/test_motor_tail.py
  54. +197 −0 test/test_motor_web.py
  55. +36 −0 test/utils.py
View
2 .gitignore
@@ -1,2 +1,4 @@
/setup.cfg
*.pyc
+/nosetests.xml
+/motor.egg-info/
View
22 CONTRIBUTING.rst
@@ -0,0 +1,22 @@
+Contributing to Motor
+=====================
+
+Contributions are encouraged. Please read these guidelines before sending a
+pull request.
+
+Bugfixes and New Features
+-------------------------
+
+Before starting to write code, look for existing tickets or create one in `Jira
+<https://jira.mongodb.org/browse/MOTOR>`_ for your specific issue or feature
+request.
+
+General Guidelines
+------------------
+
+- Avoid backward breaking changes if at all possible.
+- 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 command line to run the test suite).
+- Add yourself to doc/contributors.rst :)
View
202 LICENSE
@@ -0,0 +1,202 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
View
5 MANIFEST.in
@@ -0,0 +1,5 @@
+include README.rst
+include LICENSE
+recursive-include doc *.rst
+recursive-include doc *.py
+recursive-include test *.py
View
17 README.rst
@@ -15,7 +15,7 @@ docs are on ReadTheDocs_.
Issues / Questions / Feedback
=============================
-Any issues with, questions about, or feedback for PyMongo should be sent to the
+Any issues with, questions about, or feedback for Motor should be sent to the
mongodb-user list on Google Groups. For confirmed issues or feature requests,
open a case on `jira <http://jira.mongodb.org>`_ in the "MOTOR" project. Please
do not e-mail the author directly with issues or questions - you're more likely
@@ -33,6 +33,7 @@ Motor works in all the environments officially supported by Tornado_. It
requires:
* Unix, including Mac OS X. Microsoft Windows is not officially supported.
+* PyMongo_
* Tornado_
* `Greenlet <http://pypi.python.org/pypi/greenlet>`_
* CPython 2.5, 2.6, 2.7, 3.2, or 3.3
@@ -49,16 +50,18 @@ Documentation
=============
You will need sphinx_ installed to generate the documentation. Documentation
-can be generated by running **python setup.py doc**. Generated documentation
-can be found in the *doc/build/html/* directory. You can read the current
-docs at
+can be generated by running ``python setup.py doc``. Generated documentation
+can be found in ``doc/build/html/``. You can read the current docs
+at ReadTheDocs_.
Testing
=======
-The easiest way to run the tests is to install nose_ and run **nosetests**
-or **python setup.py test** in the root of the distribution. Tests are located
-in the *test/* directory.
+The easiest way to run the tests is to install nose_ and run ``nosetests``
+or ``python setup.py nosetests`` in the root of the distribution. Tests are
+located in the ``test/`` directory.
+
+.. _PyMongo: http://pypi.python.org/pypi/pymongo/
.. _MongoDB: http://mongodb.org/
View
89 doc/Makefile
@@ -0,0 +1,89 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Motor.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Motor.qhc"
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
+ "run these through (pdf)latex."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
View
1 doc/__init__.py
@@ -0,0 +1 @@
+
View
81 doc/api/generator_interface.rst
@@ -0,0 +1,81 @@
+.. _generator-interface:
+
+Generator Interface
+===================
+
+.. currentmodule:: motor
+
+Motor provides yield points to be used with `tornado.gen
+<http://www.tornadoweb.org/documentation/gen.html>`_,
+within functions or methods decorated by ``@gen.engine``. See
+:ref:`generator-interface-example`.
+
+.. class:: Op
+
+ Subclass of `tornado.gen.Task`_. Runs a single asynchronous MongoDB operation
+ and resumes the generator with the result of the operation when it is
+ complete.
+ :class:`motor.Op` adds an additional convenience to ``Task``:
+ it assumes the callback is passed the standard ``result, error`` pair, and if
+ ``error`` is not ``None`` then ``Op`` raises the error.
+ Otherwise, ``result`` is returned from the ``yield`` expression:
+
+.. code-block:: python
+
+ from tornado import gen
+
+ @gen.engine
+ def get_some_documents(db):
+ cursor = db.collection.find().limit(10)
+
+ try:
+ # to_list is passed a callback, which is later executed with
+ # arguments (result, error). If error is not None, it is raised
+ # from this line. Otherwise 'result' is the value of the yield
+ # expression.
+ documents = yield motor.Op(cursor.to_list)
+ return documents
+ except Exception, e:
+ print e
+
+.. class:: WaitOp
+
+ For complex control flows, a :class:`motor.Op` can be split into two
+ parts, a `Callback`_ and a :class:`motor.WaitOp`.
+
+.. code-block:: python
+
+ @gen.engine
+ def get_some_documents(db):
+ cursor = db.collection.find().limit(10)
+ cursor.to_list(callback=(yield gen.Callback('key')))
+ try:
+ documents = yield motor.WaitOp('key')
+ return documents
+ except Exception, e:
+ print e
+
+.. _tornado.gen.Task: http://www.tornadoweb.org/documentation/gen.html#tornado.gen.Task
+
+.. _Callback: http://www.tornadoweb.org/documentation/gen.html#tornado.gen.Callback
+
+.. class:: WaitAllOps
+
+ To wait for multiple Callbacks to complete, yield
+ :class:`motor.WaitAllOps`:
+
+.. code-block:: python
+
+ @gen.engine
+ def get_two_documents_in_parallel(db, id_one, id_two):
+ db.collection.find_one(
+ {'_id': id_one}, callback=(yield gen.Callback('one')))
+
+ db.collection.find_one(
+ {'_id': id_two}, callback=(yield gen.Callback('two')))
+
+ try:
+ document_one, document_two = yield motor.WaitAllOps(['one', 'two'])
+ return document_one, document_two
+ except Exception, e:
+ print e
View
73 doc/api/gridfs.rst
@@ -0,0 +1,73 @@
+Motor GridFS Classes
+====================
+
+.. currentmodule:: motor
+
+Store blobs of data in `GridFS <http://www.mongodb.org/display/DOCS/GridFS>`_.
+
+.. seealso:: :ref:`gridfs-handler`
+
+.. TODO: doc all the differences, link from differences.rst
+
+.. autoclass:: MotorGridFS
+
+ .. automethod:: open
+ .. automotormethod:: new_file
+ .. automotormethod:: get
+ .. automotormethod:: get_version
+ .. automotormethod:: get_last_version
+ .. automotormethod:: list
+ .. automotormethod:: exists
+ .. automotormethod:: put
+ .. automotormethod:: delete
+
+
+.. autoclass:: MotorGridIn
+
+ .. automotorattribute:: _id
+ .. automotorattribute:: filename
+ .. automotorattribute:: name
+ .. automotorattribute:: content_type
+ .. automotorattribute:: length
+ .. automotorattribute:: chunk_size
+ .. automotorattribute:: upload_date
+ .. automotorattribute:: md5
+ .. automotorattribute:: closed
+ .. automethod:: open
+ .. automotormethod:: write
+ .. automotormethod:: writelines
+ .. method:: set(name, value, callback=None)
+
+ Set an arbitrary metadata attribute on the file. Stores value on the server
+ as a key-value pair within the file document once the file is closed. If
+ the file is already closed, calling `set` will immediately update the file
+ document on the server.
+
+ Metadata set on the file appears as attributes on a :class:`~MotorGridOut`
+ object created from the file.
+
+ :Parameters:
+ - `name`: Name of the attribute, will be stored as a key in the file
+ document on the server
+ - `value`: Value of the attribute
+ - `callback`: Optional callback to execute once attribute is set.
+
+ .. automotormethod:: close
+
+
+.. autoclass:: MotorGridOut
+
+ .. automotorattribute:: _id
+ .. automotorattribute:: filename
+ .. automotorattribute:: name
+ .. automotorattribute:: content_type
+ .. automotorattribute:: length
+ .. automotorattribute:: chunk_size
+ .. automotorattribute:: upload_date
+ .. automotorattribute:: md5
+ .. automethod:: open
+ .. automotormethod:: tell
+ .. automotormethod:: seek
+ .. automotormethod:: read
+ .. automotormethod:: readline
+ .. automethod:: stream_to_handler
View
17 doc/api/index.rst
@@ -0,0 +1,17 @@
+Motor API
+=========
+
+.. currentmodule:: motor
+
+.. seealso:: :doc:`../tutorial`
+
+.. toctree::
+
+ motor_client
+ motor_replica_set_client
+ motor_database
+ motor_collection
+ motor_cursor
+ gridfs
+ generator_interface
+ web
View
59 doc/api/motor_client.rst
@@ -0,0 +1,59 @@
+:class:`MotorClient` -- Connection to MongoDB
+=================================================
+
+.. currentmodule:: motor
+
+.. autoclass:: motor.MotorClient
+
+ .. automethod:: open
+ .. automethod:: open_sync
+ .. method:: disconnect
+
+ Disconnect from MongoDB.
+
+ Disconnecting will close all underlying sockets in the
+ connection pool. If the :class:`MotorClient` is used again it
+ will be automatically re-opened.
+
+ .. method:: close
+
+ Alias for :meth:`disconnect`.
+
+ .. describe:: c[db_name] || c.db_name
+
+ Get the `db_name` :class:`MotorDatabase` on :class:`MotorClient` `c`.
+
+ Raises :class:`~pymongo.errors.InvalidName` if an invalid database name is used.
+ Raises :class:`~pymongo.errors.InvalidOperation` if connection isn't opened yet.
+
+ .. automethod:: is_locked
+ .. automotorattribute:: host
+ .. automotorattribute:: port
+ .. automotorattribute:: nodes
+ .. automotorattribute:: max_pool_size
+ .. automotorattribute:: document_class
+ .. automotorattribute:: tz_aware
+ .. automotorattribute:: read_preference
+ .. automotorattribute:: tag_sets
+ .. automotorattribute:: secondary_acceptable_latency_ms
+ .. automotorattribute:: slave_okay
+ .. automotorattribute:: safe
+ .. automotorattribute:: is_locked
+ .. method:: sync_client
+
+ Get a :class:`~pymongo.mongo_client.MongoClient` with the same
+ configuration as this :class:`MotorClient`
+
+ .. automotormethod:: get_lasterror_options
+ .. automotormethod:: set_lasterror_options
+ .. automotormethod:: unset_lasterror_options
+ .. automotormethod:: database_names
+ .. automotormethod:: drop_database
+ .. automotormethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None[, callback=None]]]])
+ .. automotormethod:: server_info
+ .. automotormethod:: close_cursor
+ .. automotormethod:: kill_cursors
+ .. automotormethod:: fsync
+ .. automotormethod:: unlock
+ .. automethod:: start_request
+ .. automethod:: end_request
View
53 doc/api/motor_collection.rst
@@ -0,0 +1,53 @@
+:class:`MotorCollection`
+========================
+
+.. currentmodule:: motor
+
+.. autoclass:: MotorCollection
+
+ .. describe:: c[name] || c.name
+
+ Get the `name` sub-collection of :class:`MotorCollection` `c`.
+
+ Raises :class:`~pymongo.errors.InvalidName` if an invalid
+ collection name is used.
+
+ .. automotorattribute:: full_name
+ .. automotorattribute:: name
+ .. attribute:: database
+
+ The :class:`MotorDatabase` that this
+ :class:`MotorCollection` is a part of.
+
+ .. automotorattribute:: slave_okay
+ .. automotorattribute:: read_preference
+ .. automotorattribute:: tag_sets
+ .. automotorattribute:: secondary_acceptable_latency_ms
+ .. automotorattribute:: safe
+ .. automotorattribute:: uuid_subtype
+ .. automotormethod:: get_lasterror_options
+ .. automotormethod:: set_lasterror_options
+ .. automotormethod:: unset_lasterror_options
+ .. automotormethod:: insert(doc_or_docs[, manipulate=True[, safe=False[, check_keys=True[, continue_on_error=False[, callback=None [, **kwargs]]]]])
+ .. automotormethod:: save(to_save[, manipulate=True[, safe=False[, callback=None [, **kwargs]]])
+ .. automotormethod:: update(spec, document[, upsert=False[, manipulate=False[, safe=False[, multi=False[, callback=None [, **kwargs]]]]])
+ .. automotormethod:: remove([spec_or_id=None[, safe=False[, callback=None [, **kwargs]]])
+ .. automotormethod:: drop
+ .. automethod:: find([spec=None[, fields=None[, skip=0[, limit=0[, timeout=True[, snapshot=False[, tailable=False[, sort=None[, max_scan=None[, as_class=None[, slave_okay=False[, await_data=False[, partial=False[, manipulate=True[, read_preference=ReadPreference.PRIMARY[, **kwargs]]]]]]]]]]]]]]]])
+ .. automotormethod:: find_one([spec_or_id=None[, *args[, callback=<function> [, **kwargs]]])
+ .. automotormethod:: count
+ .. automotormethod:: create_index
+ .. automotormethod:: ensure_index
+ .. automotormethod:: drop_index
+ .. automotormethod:: drop_indexes
+ .. automotormethod:: reindex
+ .. automotormethod:: index_information
+ .. automotormethod:: options
+ .. automotormethod:: aggregate
+ .. automotormethod:: group
+ .. automotormethod:: rename
+ .. automotormethod:: distinct
+ .. automotormethod:: map_reduce
+ .. automotormethod:: inline_map_reduce
+ .. automotormethod:: find_and_modify
+
View
36 doc/api/motor_cursor.rst
@@ -0,0 +1,36 @@
+:class:`MotorCursor`
+========================
+
+.. currentmodule:: motor
+
+.. autoclass:: MotorCursor
+
+ .. autoattribute:: fetch_next
+ .. automethod:: next_object
+ .. automethod:: each
+ .. automethod:: to_list
+ .. automethod:: tail
+ .. automethod:: clone
+ .. automethod:: rewind
+ .. automotormethod:: close
+ .. automotormethod:: count
+ .. automotormethod:: distinct
+ .. automotormethod:: explain
+ .. automotormethod:: sort
+ .. automotormethod:: limit
+ .. automotormethod:: skip
+ .. automotormethod:: batch_size
+ .. automotormethod:: add_option
+ .. automotormethod:: remove_option
+ .. automotormethod:: max_scan
+ .. automotormethod:: hint
+ .. automotormethod:: where
+ .. autoattribute:: buffer_size
+ .. automotorattribute:: alive
+ .. automotorattribute:: cursor_id
+
+ .. describe:: c[index]
+
+ See :meth:`__getitem__`.
+
+ .. automethod:: __getitem__
View
46 doc/api/motor_database.rst
@@ -0,0 +1,46 @@
+:class:`MotorDatabase`
+======================
+
+.. currentmodule:: motor
+
+.. autoclass:: motor.MotorDatabase
+
+ .. describe:: db[collection_name] || db.collection_name
+
+ Get the `collection_name` :class:`MotorCollection` of
+ :class:`MotorDatabase` `db`.
+
+ Raises :class:`~pymongo.errors.InvalidName` if an invalid collection name is used.
+
+ .. automotormethod:: command
+ .. automotormethod:: create_collection
+ .. automotormethod:: drop_collection
+ .. automotormethod:: validate_collection
+ .. automotormethod:: collection_names
+ .. automotormethod:: current_op
+ .. automotormethod:: dereference
+ .. automotormethod:: error
+ .. automotormethod:: previous_error
+ .. automotormethod:: last_status
+ .. automotormethod:: reset_error_history
+ .. automotormethod:: set_profiling_level
+ .. automotormethod:: profiling_level
+ .. automotormethod:: profiling_info
+ .. automotormethod:: eval
+ .. automotormethod:: add_user
+ .. automotormethod:: remove_user
+ .. automotormethod:: authenticate
+ .. automotormethod:: logout
+ .. automethod:: add_son_manipulator
+ .. automotormethod:: incoming_manipulators
+ .. automotormethod:: incoming_copying_manipulators
+ .. automotormethod:: outgoing_manipulators
+ .. automotormethod:: outgoing_copying_manipulators
+ .. automotorattribute:: read_preference
+ .. automotorattribute:: tag_sets
+ .. automotorattribute:: secondary_acceptable_latency_ms
+ .. automotorattribute:: slave_okay
+ .. automotorattribute:: safe
+ .. automotormethod:: get_lasterror_options
+ .. automotormethod:: set_lasterror_options
+ .. automotormethod:: unset_lasterror_options
View
53 doc/api/motor_replica_set_client.rst
@@ -0,0 +1,53 @@
+:class:`MotorReplicaSetClient` -- Connection to MongoDB replica set
+=======================================================================
+
+.. currentmodule:: motor
+
+.. autoclass:: motor.MotorReplicaSetClient
+
+ .. automethod:: open
+ .. automethod:: open_sync
+ .. method:: disconnect
+
+ Disconnect from MongoDB.
+
+ Disconnecting will close all underlying sockets in the
+ connection pool. If the :class:`MotorReplicaSetClient` is used again it
+ will be automatically re-opened.
+
+ .. method:: close
+
+ Alias for :meth:`disconnect`.
+
+ .. describe:: c[db_name] || c.db_name
+
+ Get the `db_name` :class:`MotorDatabase` on :class:`MotorReplicaSetClient` `c`.
+
+ Raises :class:`~pymongo.errors.InvalidName` if an invalid database name is used.
+ Raises :class:`~pymongo.errors.InvalidOperation` if connection isn't opened yet.
+
+ .. autoattribute:: connected
+ .. automotorattribute:: seeds
+ .. automotorattribute:: hosts
+ .. automotorattribute:: arbiters
+ .. automotorattribute:: primary
+ .. automotorattribute:: secondaries
+ .. automotorattribute:: read_preference
+ .. automotorattribute:: tag_sets
+ .. automotorattribute:: secondary_acceptable_latency_ms
+ .. automotorattribute:: max_pool_size
+ .. automotorattribute:: document_class
+ .. automotorattribute:: tz_aware
+ .. automotorattribute:: safe
+ .. method:: sync_client
+
+ Get a :class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient`
+ with the same configuration as this :class:`MotorReplicaSetClient`
+
+ .. automotormethod:: get_lasterror_options
+ .. automotormethod:: set_lasterror_options
+ .. automotormethod:: unset_lasterror_options
+ .. automotormethod:: database_names
+ .. automotormethod:: drop_database
+ .. automotormethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None[, callback=None]]]])
+ .. automotormethod:: close_cursor
View
12 doc/api/web.rst
@@ -0,0 +1,12 @@
+:mod:`motor.web`
+=================
+
+.. automodule:: motor.web
+
+.. _gridfs-handler:
+
+GridFSHandler - Serve GridFS files as static media
+--------------------------------------------------
+
+ .. autoclass:: motor.web.GridFSHandler
+ :members:
View
2 doc/changelog.rst
@@ -0,0 +1,2 @@
+Changelog
+=========
View
155 doc/conf.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+#
+# Motor documentation build configuration file
+#
+# This file is execfile()d with the current directory set to its containing dir.
+
+import sys, os
+sys.path[0:0] = [os.path.abspath('..')]
+
+import motor
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.coverage',
+ 'sphinx.ext.todo', 'doc.mongo_extensions', 'doc.motor_extensions']
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Motor'
+copyright = u'2012 - 2013, 10gen, Inc.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = motor.version
+# The full version, including alpha/beta/rc tags.
+release = motor.version
+
+# List of documents that shouldn't be included in the build.
+unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = ['_build']
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+add_module_names = True
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+# -- Options for extensions ----------------------------------------------------
+autoclass_content = 'init'
+
+doctest_path = os.path.abspath('..')
+
+doctest_test_doctest_blocks = False
+
+doctest_global_setup = """
+"""
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+html_theme_options = {'collapsiblesidebar': True}
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+#html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+#html_static_path = ['_static']
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Motor' + release.replace('.', '_')
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'Motor.tex', u'Motor Documentation',
+ u'A. Jesse Jiryu Davis', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
View
7 doc/contributors.rst
@@ -0,0 +1,7 @@
+Contributors
+============
+The following is a list of people who have contributed to
+**Motor**. If you belong here and are missing please let us know
+(or send a pull request after adding yourself to the list):
+
+- A\. Jesse Jiryu Davis
View
415 doc/differences.rst
@@ -0,0 +1,415 @@
+=====================================
+Differences between Motor and PyMongo
+=====================================
+
+Major differences
+=================
+
+Creating a connection
+---------------------
+
+PyMongo's :class:`~pymongo.mongo_client.MongoClient` and
+:class:`~pymongo.mongo_replica_set_client.MongoReplicaSetClient` constructors
+block until they have established a connection to MongoDB. A
+:class:`~motor.MotorClient` or :class:`~motor.MotorReplicaSetClient`,
+however, is created unconnected. One should call
+:meth:`~motor.MotorClient.open_sync` at the beginning of a Tornado web
+application, before accepting requests:
+
+.. code-block:: python
+
+ import motor
+ connection = motor.MotorClient().open_sync()
+
+To make a connection asynchronously once the application is running, call
+:meth:`~motor.MotorClient.open`:
+
+.. code-block:: python
+
+ def connected(connection, error):
+ if error:
+ print 'Error connecting!', error
+ else:
+ # Use the connection
+ pass
+
+ motor.MotorClient().open(connected)
+
+Callbacks
+---------
+
+Motor supports nearly every method PyMongo does, but Motor methods that
+do network I/O take a callback function. The callback must accept two
+parameters:
+
+.. code-block:: python
+
+ def callback(result, error):
+ pass
+
+Motor's asynchronous methods return ``None`` immediately, and execute the
+callback, with either a result or an error, when the operation has completed.
+
+For example, one uses
+:meth:`~pymongo.collection.Collection.find_one` in PyMongo like:
+
+.. code-block:: python
+
+ db = MongoClient().test
+ user = db.users.find_one({'name': 'Jesse'})
+ print user
+
+But Motor's :meth:`~motor.MotorCollection.find_one` method works asynchronously:
+
+.. code-block:: python
+
+ db = MotorClient().open_sync().test
+
+ def got_user(user, error):
+ if error:
+ print 'error getting user!', error
+ else:
+ print user
+
+ db.users.find_one({'name': 'Jesse'}, callback=got_user)
+
+The callback must be passed as a keyword argument, not a positional argument.
+
+To find multiple documents, Motor
+provides :meth:`~motor.MotorCursor.to_list`:
+
+.. code-block:: python
+
+ def got_users(users, error):
+ if error:
+ print 'error getting users!', error
+ else:
+ for user in users:
+ print user
+
+ db.users.find().to_list(callback=got_users)
+
+.. seealso:: MotorCursor's :meth:`~motor.MotorCursor.fetch_next`
+
+.. _motor-acknowledged-writes:
+
+Acknowledged Writes
+-------------------
+
+PyMongo's default behavior for
+:meth:`~pymongo.collection.Collection.insert`,
+:meth:`~pymongo.collection.Collection.update`,
+:meth:`~pymongo.collection.Collection.save`, and
+:meth:`~pymongo.collection.Collection.remove` is to perform *unacknowledged
+writes*: the driver does not request nor await a response from the server unless
+the method is passed ``safe=True`` or another
+`getLastError option <http://www.mongodb.org/display/DOCS/getLastError+Command>`_.
+Unacknowledged writes are very low-latency but can mask errors.
+
+In Motor, writes are acknowledged (they are "safe writes") if passed a callback:
+
+.. code-block:: python
+
+ def inserted(result, error):
+ if error:
+ print 'error inserting!', error
+ else:
+ print 'added user'
+
+ db.users.insert({'name': 'Bernie'}, callback=inserted) # Acknowledged
+
+On success, the ``result`` parameter to the callback contains the
+client-generated ``_id`` of the document for `insert` or `save`, and MongoDB's
+`getLastError` response for `update` or `remove`. On error, ``result`` is `None`
+and the ``error`` parameter is an Exception.
+
+With no callback, Motor does unacknowledged writes.
+
+One can pass ``safe=False`` explicitly, along with a callback, to perform an
+unacknowledged write:
+
+.. code-block:: python
+
+ db.users.insert({'name': 'Jesse'}, callback=inserted, safe=False)
+
+In this case the callback is executed as soon as the message has been written to
+the socket connected to MongoDB, but no response is expected from the server.
+Passing a callback and ``safe=False`` can be useful to do fast writes without
+overrunning the output buffer.
+
+Result Values for Acknowledged and Unacknowledged Writes
+''''''''''''''''''''''''''''''''''''''''''''''''''''''''
+
+These are the values passed as the `result` parameter to your callback for
+acknowledged and unacknowledged writes with Motor:
+
++-----------+-------------------------+--------------------------------+
+| Operation | With Callback | With Callback and `safe=False` |
++===========+=========================+================================+
+| insert | New \_id | New \_id |
++-----------+-------------------------+--------------------------------+
+| save | \_id (whether new or existing document, safe or unsafe) |
++-----------+-------------------------+--------------------------------+
+| update | ``{'ok': 1.0, 'n': 1}`` | ``None`` |
++-----------+-------------------------+--------------------------------+
+| remove | ``{'ok': 1.0, 'n': 1}`` | ``None`` |
++-----------+-------------------------+--------------------------------+
+
+Unacknowledged Writes With gen.engine
+'''''''''''''''''''''''''''''''''''''
+
+When using Motor with `tornado.gen`_, each Motor operation is passed an implicit
+callback and is therefore acknowledged ("safe"):
+
+.. code-block:: python
+
+ from tornado import gen
+
+ @gen.engine
+ def f():
+ # Acknowledged
+ yield motor.Op(motor_db.collection.insert, {'name': 'Randall'})
+
+You can override this behavior and do unacknowledged writes by passing
+``safe=False``:
+
+.. code-block:: python
+
+ from tornado import gen
+
+ @gen.engine
+ def f():
+ # Unacknowledged
+ yield motor.Op(motor_db.collection.insert, {'name': 'Ross'}, safe=False)
+
+.. _tornado.gen: http://www.tornadoweb.org/documentation/gen.html
+
+.. seealso:: :ref:`generator-interface`
+
+Timeouts
+--------
+
+In PyMongo, you can set a network timeout which causes an
+:exc:`~pymongo.errors.AutoReconnect` exception if an operation does not complete
+in time::
+
+ db = MongoClient(socketTimeoutMS=500).test
+ try:
+ user = db.users.find_one({'name': 'Jesse'})
+ print user
+ except AutoReconnect:
+ print 'timed out'
+
+:class:`~motor.MotorClient` and :class:`~motor.MotorReplicaSetClient`
+support the same options. The exception isn't raised, instead it's passed to
+the callback as the ``error`` parameter, and the ``result`` parameter will be
+``None``. Code using `tornado.gen`_ ends up looking very similar to the
+PyMongo code::
+
+ @gen.engine
+ def f():
+ try:
+ user = yield motor.Op(db.users.find_one, {'name': 'Jesse'})
+ print user
+ except AutoReconnect:
+ print 'timed out'
+
+As in PyMongo, the default ``connectTimeoutMS`` is 20 seconds, and the default
+``socketTimeoutMS`` is no timeout.
+
+Requests
+--------
+
+PyMongo provides :doc:`requests </examples/requests>` to ensure that a series
+of operations are performed in order by the MongoDB server, even with
+unacknowledged writes. Motor does not support requests, so the only way to
+guarantee order is by doing acknowledged writes. Register a callback
+for each operation and perform the next operation in the callback::
+
+ def inserted(result, error):
+ if error:
+ raise error
+
+ db.users.find_one({'name': 'Ben'}, callback=found_one)
+
+ def found_one(result, error):
+ if error:
+ raise error
+
+ print result
+
+ # Acknowledged insert:
+ db.users.insert({'name': 'Ben', 'author': 'Tornado'}, callback=inserted)
+
+This ensures ``find_one`` isn't run until ``insert`` has been acknowledged by
+the server. Obviously, this code is improved by `tornado.gen`_::
+
+ @gen.engine
+ def f():
+ yield motor.Op(db.users.insert, {'name': 'Ben', 'author': 'Tornado'})
+ result = yield motor.Op(db.users.find_one, {'name': 'Ben'})
+ print result
+
+Motor ignores the ``auto_start_request`` parameter to
+:class:`~motor.MotorClient` or :class:`~motor.MotorReplicaSetClient`.
+
+.. _tornado.gen: http://www.tornadoweb.org/documentation/gen.html
+
+Threading and forking
+---------------------
+
+Multithreading and forking are not supported; Motor is intended to be used in
+a single-threaded Tornado application. See Tornado's documentation on
+`running Tornado in production`_ to take advantage of multiple cores.
+
+.. _`running Tornado in production`: http://www.tornadoweb.org/documentation/overview.html#running-tornado-in-production
+
+Minor differences
+=================
+
+MasterSlaveConnection
+---------------------
+
+PyMongo's :class:`~pymongo.master_slave_connection.MasterSlaveConnection`
+offers a few conveniences when connected to a MongoDB `master-slave pair`_.
+Master-slave replication has long been superseded by `replica sets`_, so Motor
+has no equivalent to MasterSlaveConnection.
+
+.. _master-slave pair: http://docs.mongodb.org/manual/administration/master-slave/
+
+.. _replica sets: http://docs.mongodb.org/manual/core/replication/
+
+Tailable cursors
+----------------
+
+Motor provides a convenience method :meth:`~motor.MotorCursor.tail` that
+hides some complexity involved in tailing a cursor on a capped collection.
+
+.. seealso:: `Tailable cursors <http://www.mongodb.org/display/DOCS/Tailable+Cursors>`_
+
+GridFS
+------
+
+- File-like
+
+ PyMongo's :class:`~gridfs.grid_file.GridIn` and
+ :class:`~gridfs.grid_file.GridOut` strive to act like Python's built-in
+ file objects, so they can be passed to many functions that expect files.
+ But the I/O methods of :class:`~motor.MotorGridIn` and
+ :class:`~motor.MotorGridOut` require callbacks, so they cannot obey the
+ file API and aren't suitable in the same circumstances as files.
+
+- Iteration
+
+ It's convenient in PyMongo to iterate a :class:`~gridfs.grid_file.GridOut`::
+
+ fs = gridfs.GridFS(db)
+ grid_out = fs.get(file_id)
+ for chunk in grid_out:
+ print chunk
+
+ :class:`~motor.MotorGridOut` cannot support this API asynchronously.
+ To read a ``MotorGridOut`` use the non-blocking
+ :meth:`~motor.MotorGridOut.read` method. For convenience ``MotorGridOut``
+ provides :meth:`~motor.MotorGridOut.stream_to_handler`.
+
+ .. seealso:: :ref:`reading-from-gridfs` and :ref:`GridFSHandler <gridfs-handler>`.
+
+- Setting properties
+
+ In PyMongo, you can set arbitrary attributes on
+ a :class:`~gridfs.grid_file.GridIn` and they're stored as metadata on
+ the server, even after the ``GridIn`` is closed::
+
+ grid_in = fs.new_file()
+ grid_in.close()
+ grid_in.my_field = 'my_value'
+
+ Updating metadata on a :class:`~motor.MotorGridIn` requires a callback, so
+ the API is different::
+
+ @gen.engine
+ def f():
+ fs = motor.MotorGridFS(db)
+ yield motor.Op(fs.open)
+ grid_in = yield motor.Op(fs.new_file)
+ yield motor.Op(grid_in.close)
+ yield motor.Op(grid_in.set, 'my_field', 'my_value')
+
+ .. seealso:: :ref:`setting-attributes-on-a-motor-gridin`
+
+- The "with" statement
+
+ :class:`~gridfs.grid_file.GridIn` is a context manager--you can use it in a
+ "with" statement and it is closed on exit::
+
+ with fs.new_file() as grid_in:
+ grid_in.write('data')
+
+ But ``MotorGridIn``'s :meth:`~motor.MotorGridIn.close` takes a callback, so
+ it must be called explicitly.
+
+is_locked
+---------
+
+:meth:`~motor.MotorClient.is_locked` in Motor is a method requiring a
+callback, whereas in PyMongo it is a property of
+:class:`~pymongo.mongo_client.MongoClient`.
+
+system_js
+---------
+
+PyMongo supports Javascript procedures stored in MongoDB with syntax like:
+
+.. code-block:: python
+
+ >>> db.system_js.my_func = 'function(x) { return x * x; }'
+ >>> db.system_js.my_func(2)
+ 4.0
+
+Motor does not. One should use ``system.js`` as a regular collection with Motor:
+
+.. code-block:: python
+
+ def saved(result, error):
+ if error:
+ print 'error saving function!', error
+ else:
+ db.eval('my_func(2)', callback=evaluated)
+
+ def evaluated(result, error):
+ if error:
+ print 'eval error!', error
+ else:
+ print 'eval result:', result # This will be 4.0
+
+ db.system.js.save(
+ {'_id': 'my_func', 'value': Code('function(x) { return x * x; }')},
+ callback=saved)
+
+.. seealso:: `Server-side code execution <http://www.mongodb.org/display/DOCS/Server-side+Code+Execution>`_
+
+Cursor slicing
+--------------
+
+In Pymongo, the following raises an ``IndexError`` if the collection has fewer
+than 101 documents:
+
+.. code-block:: python
+
+ db.collection.find()[100]
+
+In Motor, however, no exception is raised. The query simply has no results:
+
+.. code-block:: python
+
+ def callback(result, error):
+ # 'result' is [ ] and 'error' is None
+ print result, error
+
+ db.collection.find()[100].to_list(callback)
+
+The difference arises because the PyMongo :class:`~pymongo.cursor.Cursor`'s
+slicing operator blocks until it has queried the MongoDB server, and determines
+if a document exists at the desired offset; Motor simply returns a new
+:class:`~motor.MotorCursor` with a skip and limit applied.
View
55 doc/examples/authentication.rst
@@ -0,0 +1,55 @@
+Motor Authentication Examples
+=============================
+
+To use authentication, you must start ``mongod`` with ``--auth`` or, for
+replica sets or sharded clusters, ``--keyFile``.
+
+.. mongodoc:: authenticate
+
+Creating an admin user
+----------------------
+The first step to securing a MongoDB instance with authentication is to create
+a user in the ``admin`` database--this user is like a super-user who can add
+or remove regular users::
+
+ from tornado import gen
+ import motor
+
+ c = motor.MotorClient().open_sync()
+
+ @gen.engine
+ def create_admin_user():
+ yield motor.Op(c.admin.add_user, "admin", "secret password")
+
+.. seealso:: :meth:`~motor.MotorDatabase.add_user`
+
+Creating a regular user
+-----------------------
+Once you've created the admin user you must log in before any further
+operations, including creating a regular user::
+
+ @gen.engine
+ def create_regular_user():
+ yield motor.Op(c.admin.authenticate, "admin", "secret password")
+ yield motor.Op(c.my_database.add_user, "jesse", "jesse's password")
+
+.. seealso:: :meth:`~motor.MotorDatabase.authenticate`,
+ :meth:`~motor.MotorDatabase.add_user`
+
+Authenticating as a regular user
+--------------------------------
+Creating an admin user or authenticating as one is a rare task, and typically
+done with the mongo shell rather than with Motor. Your day-to-day operations
+will only need regular authentication::
+
+ @gen.engine
+ def login(c):
+ yield motor.Op(c.my_database.authenticate, "jesse", "jesse's password")
+
+After you've logged in to a database with a given :class:`~motor.MotorClient`
+or :class:`~motor.MotorReplicaSetClient`, all further operations on that
+database using that client will already be authenticated until you
+call :meth:`~motor.MotorDatabase.logout`.
+
+.. seealso:: :meth:`~motor.MotorDatabase.authenticate`,
+ :meth:`~motor.MotorDatabase.logout`
View
190 doc/examples/callbacks-and-generators.rst
@@ -0,0 +1,190 @@
+Examples With Callbacks And Generators
+======================================
+
+Programming with Motor is far easier using Tornado's ``gen`` module than when
+using raw callbacks. Here's an example that shows the difference.
+
+With callbacks
+--------------
+
+Here's an example of an application that can create and display short messages:
+
+.. code-block:: python
+
+ import tornado.web, tornado.ioloop
+ import motor
+
+ class NewMessageHandler(tornado.web.RequestHandler):
+ def get(self):
+ """Show a 'compose message' form"""
+ self.write('''
+ <form method="post">
+ <input type="text" name="msg">
+ <input type="submit">
+ </form>''')
+
+ # Method exits before the HTTP request completes, thus "asynchronous"
+ @tornado.web.asynchronous
+ def post(self):
+ """Insert a message
+ """
+ msg = self.get_argument('msg')
+
+ # Async insert; callback is executed when insert completes
+ self.settings['db'].messages.insert(
+ {'msg': msg},
+ callback=self._on_response)
+
+ def _on_response(self, result, error):
+ if error:
+ raise tornado.web.HTTPError(500, error)
+ else:
+ self.redirect('/')
+
+
+ class MessagesHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ def get(self):
+ """Display all messages
+ """
+ self.write('<a href="/compose">Compose a message</a><br>')
+ self.write('<ul>')
+ db = self.settings['db']
+ db.messages.find().sort([('_id', -1)]).each(self._got_message)
+
+ def _got_message(self, message, error):
+ if error:
+ raise tornado.web.HTTPError(500, error)
+ elif message:
+ self.write('<li>%s</li>' % message['msg'])
+ else:
+ # Iteration complete
+ self.write('</ul>')
+ self.finish()
+
+ db = motor.MotorClient().open_sync().test
+
+ application = tornado.web.Application(
+ [
+ (r'/compose', NewMessageHandler),
+ (r'/', MessagesHandler)
+ ],
+ db=db
+ )
+
+ print 'Listening on http://localhost:8888'
+ application.listen(8888)
+ tornado.ioloop.IOLoop.instance().start()
+
+The call to :meth:`~motor.MotorCursor.each` could be
+replaced with :meth:`~motor.MotorCursor.to_list`, which is easier to use
+with templates because the callback receives the entire result at once:
+
+.. code-block:: python
+
+ from tornado import template
+ messages_template = template.Template('''<ul>
+ {% for message in messages %}
+ <li>{{ message['msg'] }}</li>
+ {% end %}
+ </ul>''')
+
+ class MessagesHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ def get(self):
+ """Display all messages
+ """
+ self.write('<a href="/compose">Compose a message</a><br>')
+ self.write('<ul>')
+ db = self.settings['db']
+ (db.messages.find()
+ .sort([('_id', -1)])
+ .limit(10)
+ .to_list(self._got_messages))
+
+ def _got_messages(self, messages, error):
+ if error:
+ raise tornado.web.HTTPError(500, error)
+ elif messages:
+ self.write(messages_template.generate(messages=messages))
+ self.finish()
+
+It is extremely important to use :meth:`~motor.MotorCursor.limit` with
+:meth:`~motor.MotorCursor.to_list` to avoid buffering an unbounded number of
+documents in memory.
+
+.. _generator-interface-example:
+
+Using Tornado's generator interface
+-----------------------------------
+
+Motor provides :class:`~motor.Op`, :class:`~motor.WaitOp`, and
+:class:`~motor.WaitAllOps` for convenient use with the
+`tornado.gen module <http://www.tornadoweb.org/documentation/gen.html>`_. To
+use async methods without explicit callbacks:
+
+.. code-block:: python
+
+ from tornado import gen
+
+ class NewMessageHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ @gen.engine
+ def post(self):
+ """Insert a message
+ """
+ msg = self.get_argument('msg')
+ db = self.settings['db']
+
+ # motor.Op raises an exception on error, otherwise returns result
+ result = yield motor.Op(db.messages.insert, {'msg': msg})
+
+ # Success
+ self.redirect('/')
+
+
+ class MessagesHandler(tornado.web.RequestHandler):
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self):
+ """Display all messages
+ """
+ self.write('<a href="/compose">Compose a message</a><br>')
+ self.write('<ul>')
+ db = self.settings['db']
+ cursor = db.messages.find().sort([('_id', -1)])
+ while (yield cursor.fetch_next):
+ message = cursor.next_object()
+ self.write('<li>%s</li>' % message['msg'])
+
+ # Iteration complete
+ self.write('</ul>')
+ self.finish()
+
+Or using `to_list` instead of `next_object`:
+
+.. code-block:: python
+
+ cursor = db.messages.find().sort([('_id', -1)]).limit(100)
+ messages = yield motor.Op(cursor.to_list)
+ for message in messages:
+ self.write('<li>%s</li>' % message['msg'])
+
+One can also parallelize operations and wait for all to complete. To query for
+two messages at once and wait for both:
+
+.. code-block:: python
+
+ msg = yield motor.Op(db.messages.find_one, {'_id': msg_id})
+
+ # Get previous
+ db.messages.find_one(
+ {'_id': {'$lt': msg_id}},
+ callback=(yield gen.Callback('prev')))
+
+ # Get next
+ db.messages.find_one(
+ {'_id': {'$gt': msg_id}},
+ callback=(yield gen.Callback('next')))
+
+ previous_msg, next_msg = yield motor.WaitAllOps(['prev', 'next'])
View
129 doc/examples/gridfs.rst
@@ -0,0 +1,129 @@
+Motor GridFS Examples
+=====================
+
+.. seealso:: :ref:`gridfs-handler`
+
+Writing a file to GridFS with :meth:`~motor.MotorGridFS.put`
+------------------------------------------------------------
+
+.. code-block:: python
+
+ from tornado import gen
+ import motor
+
+ db = motor.MotorClient().open_sync().test
+
+ @gen.engine
+ def write_file():
+ fs = yield motor.Op(motor.MotorGridFS(db).open)
+
+ # file_id is the ObjectId of the resulting file
+ file_id = yield motor.Op(fs.put, 'Contents')
+
+ # put() can take a file or a file-like object, too
+ from cStringIO import StringIO
+ file_like = StringIO('Lengthy contents')
+ file_id = yield motor.Op(fs.put, file_like)
+
+ # Specify the _id
+ specified_id = yield motor.Op(fs.put, 'Contents', _id=42)
+ assert 42 == specified_id
+
+Streaming a file to GridFS with :class:`~motor.MotorGridIn`
+-----------------------------------------------------------
+
+.. code-block:: python
+
+ from tornado import gen
+ import motor
+
+ db = motor.MotorClient().open_sync().test
+
+ @gen.engine
+ def write_file_streaming():
+ fs = yield motor.Op(motor.MotorGridFS(db).open)
+
+ # Create a MotorGridIn and write in chunks, then close the file to flush
+ # all data to the server.
+ gridin = yield motor.Op(fs.new_file)
+ yield motor.Op(gridin.write, 'First part\n')
+ yield motor.Op(gridin.write, 'Second part')
+ yield motor.Op(gridin.close)
+
+ # By default, the MotorGridIn's _id is an ObjectId
+ file_id = gridin._id
+
+ gridout = yield motor.Op(fs.get, file_id)
+ content = yield motor.Op(gridout.read)
+ assert 'First part\nSecond part' == content
+
+ # Specify the _id
+ gridin = yield motor.Op(fs.new_file, _id=42)
+ assert 42 == gridin._id
+
+ # MotorGridIn can write from file-like objects, too
+ file = open('my_file.txt')
+ yield motor.Op(gridin.write, file)
+ yield motor.Op(gridin.close)
+
+.. _setting-attributes-on-a-motor-gridin:
+
+Setting attributes on a :class:`~motor.MotorGridIn`
+---------------------------------------------------
+
+.. code-block:: python
+
+ from tornado import gen
+ import motor
+
+ db = motor.MotorClient().open_sync().test
+
+ @gen.engine
+ def set_attributes():
+ fs = yield motor.Op(motor.MotorGridFS(db).open)
+ gridin = yield motor.Op(fs.new_file)
+
+ # Set metadata attributes
+ yield motor.Op(gridin.set, 'content_type', 'image/png')
+ yield motor.Op(gridin.close)
+
+ # Attributes set after closing are sent to the server immediately
+ yield motor.Op(gridin.set, 'my_field', 'my_value')
+
+ gridout = yield motor.Op(fs.get, gridin._id)
+ assert 'image/png' == gridin.content_type
+ assert 'image/png' == gridin.contentType # synonymous
+ assert 'my_value' == gridin.my_field
+
+.. _reading-from-gridfs:
+
+Reading from GridFS with :class:`~motor.MotorGridOut`
+-----------------------------------------------------
+
+.. code-block:: python
+
+ from tornado import gen
+ import motor
+
+ db = motor.MotorClient().open_sync().test
+
+ @gen.engine
+ def read_file(file_id):
+ fs = yield motor.Op(motor.MotorGridFS(db).open)
+
+ # Create a MotorGridOut and read it all at once
+ gridout = yield motor.Op(fs.get, file_id)
+ content = yield motor.Op(gridout.read)
+
+ # Or read in chunks - every chunk_size bytes is one MongoDB document
+ # in the db.fs.chunks collection
+ gridout = yield motor.Op(fs.get, file_id)
+ content = ''
+ while len(content) < gridout.length:
+ content += (yield motor.Op(gridout.read, gridout.chunk_size))
+
+ # Get a file by name
+ gridout = yield motor.Op(fs.get_last_version, filename='my_file')
+ content = yield motor.Op(gridout.read)
+
+.. TODO: examples of static-url generation
View
13 doc/examples/index.rst
@@ -0,0 +1,13 @@
+Motor Examples
+==============
+
+.. currentmodule:: motor
+
+.. seealso:: :doc:`../tutorial`
+
+.. toctree::
+
+ callbacks-and-generators
+ gridfs
+ tailable-cursors
+ authentication
View
38 doc/examples/tailable-cursors.rst
@@ -0,0 +1,38 @@
+Motor Tailable Cursor Example
+=============================
+
+Motor provides a convenience method :meth:`~motor.MotorCursor.tail`
+
+.. code-block:: python
+
+ import datetime
+ from tornado import ioloop, gen
+ import motor
+
+ db = motor.MotorClient().open_sync().test
+
+ @gen.engine
+ def tailable_example():
+ loop = ioloop.IOLoop.instance()
+
+ yield motor.Op(db.capped_collection.drop)
+ yield motor.Op(db.create_collection,
+ 'capped_collection', capped=True, size=1000, autoIndexId=True)
+
+ results = []
+
+ def each(result, error):
+ if error:
+ raise error
+
+ results.append(result['i'])
+
+ db.capped_collection.find().tail(callback=each)
+
+ for i in range(3):
+ yield motor.Op(db.capped_collection.insert, {'i': i})
+ yield gen.Task(loop.add_timeout, datetime.timedelta(seconds=0.5))
+
+ assert range(3) == results
+
+.. seealso:: `Tailable cursors <http://www.mongodb.org/display/DOCS/Tailable+Cursors>`_
View
72 doc/features.rst
@@ -0,0 +1,72 @@
+==============
+Motor Features
+==============
+
+Non-Blocking
+============
+Motor is an asynchronous driver for MongoDB and Tornado_.
+Motor never blocks Tornado's IOLoop while connecting to MongoDB or
+performing I/O.
+
+.. _Tornado: http://tornadoweb.org/
+
+Featureful
+==========
+Motor wraps almost all of PyMongo's API and makes it non-blocking. For the few
+PyMongo features not implemented in Motor, see :doc:`differences`.
+
+Convenient With `tornado.gen`
+=============================
+The `tornado.gen module`_ lets you use generators to simplify asynchronous code,
+combining operations and their callbacks in a single function. Motor provides
+yield points to be used with ``tornado.gen``.
+See :doc:`api/generator_interface`.
+
+.. _tornado.gen module: http://www.tornadoweb.org/documentation/gen.html
+
+Timeouts
+========
+Unlike most non-blocking libraries for Tornado, Motor provides a convenient
+timeout interface::
+
+ @gen.engine
+ def f():
+ try:
+ document = yield motor.Op(
+ db.collection.find_one, my_complex_query, network_timeout=5)
+ except pymongo.errors.ConnectionFailure:
+ # find_one took more than 5 seconds
+ pass
+
+Configurable IOLoops
+====================
+Motor supports Tornado applications with multiple IOLoops_. Pass the ``io_loop``
+argument to :class:`~motor.MotorClient`
+or :class:`~motor.MotorReplicaSetClient` to configure the loop for a
+client instance.
+
+.. _IOLoops: http://www.tornadoweb.org/documentation/ioloop.html
+
+Opens Connections Synchronously or Asynchronously
+=================================================
+A :class:`~motor.MotorClient` or :class:`~motor.MotorReplicaSetClient`
+can be opened synchronously with :meth:`~motor.MotorClient.open_sync`
+before your application begins serving request, or can be opened
+asynchronously with :meth:`~motor.MotorClient.open` to make the connection
+to MongoDB without blocking the Tornado IOLoop.
+
+Streams Static Files from GridFS
+================================
+Motor can stream data from GridFS_ to a Tornado RequestHandler_
+using :meth:`~motor.MotorGridOut.stream_to_handler` or
+the :class:`~motor.web.GridFSHandler` class.
+
+.. _GridFS: http://www.mongodb.org/display/DOCS/GridFS
+
+.. _RequestHandler: http://www.tornadoweb.org/documentation/web.html#request-handlers
+
+Tailable Cursors
+================
+MotorCursor provides a convenient :meth:`~motor.MotorCursor.tail` method to
+watch a MongoDB capped collection and execute a callback with each new document
+as it is inserted.
View
25 doc/index.rst
@@ -0,0 +1,25 @@
+:mod:`motor` -- Asynchronous Python driver for Tornado and MongoDB
+==================================================================
+
+.. automodule:: motor
+ :synopsis: Asynchronous Python driver for Tornado and MongoDB
+
+Motor presents a Tornado callback-based API for non-blocking access to
+MongoDB.
+
+.. toctree::
+
+ differences
+ features
+ prerequisites
+ tutorial
+ examples/index
+ changelog
+ contributors
+
+Classes
+-------
+
+.. toctree::
+
+ api/index
View
113 doc/make.bat
@@ -0,0 +1,113 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+set SPHINXBUILD=sphinx-build
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Motor.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Motor.ghc
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+:end
View
104 doc/mongo_extensions.py
@@ -0,0 +1,104 @@
+# Copyright 2009-2010 10gen, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""MongoDB specific extensions to Sphinx."""
+
+from docutils import nodes
+from docutils.writers import html4css1
+from sphinx import addnodes
+from sphinx.util.compat import (Directive,
+ make_admonition)
+
+
+class mongodoc(nodes.Admonition, nodes.Element):
+ pass
+
+
+class mongoref(nodes.reference):
+ pass
+
+
+def visit_mongodoc_node(self, node):
+ self.visit_admonition(node, "seealso")
+
+
+def depart_mongodoc_node(self, node):
+ self.depart_admonition(node)
+
+
+def visit_mongoref_node(self, node):
+ atts = {"class": "reference external",
+ "href": node["refuri"],
+ "name": node["name"]}
+ self.body.append(self.starttag(node, 'a', '', **atts))
+
+
+def depart_mongoref_node(self, node):
+ self.body.append('</a>')
+ if not isinstance(node.parent, nodes.TextElement):
+ self.body.append('\n')
+
+
+class MongodocDirective(Directive):
+
+ has_content = True
+ required_arguments = 0
+ optional_arguments = 0
+ final_argument_whitespace = False
+ option_spec = {}
+
+ def run(self):
+ env = self.state.document.settings.env
+
+ return make_admonition(mongodoc, self.name,
+ ['See general MongoDB documentation'],
+ self.options, self.content, self.lineno,
+ self.content_offset, self.block_text,
+ self.state, self.state_machine)
+
+
+def process_mongodoc_nodes(app, doctree, fromdocname):
+ env = app.builder.env
+
+ for node in doctree.traverse(mongodoc):
+ anchor = None
+ for name in node.parent.parent.traverse(addnodes.desc_signature):
+ anchor = name["ids"][0]
+ break
+ if not anchor:
+ for name in node.parent.traverse(nodes.section):
+ anchor = name["ids"][0]
+ break
+ for para in node.traverse(nodes.paragraph):
+ tag = str(para.traverse()[1])
+ link = mongoref("", "")
+ link["refuri"] = "http://dochub.mongodb.org/core/%s" % tag
+ link["name"] = anchor
+ link.append(nodes.emphasis(tag, tag))
+ new_para = nodes.paragraph()
+ new_para += link
+ node.replace(para, new_para)
+
+
+def setup(app):
+ app.add_node(mongodoc,
+ html=(visit_mongodoc_node, depart_mongodoc_node),
+ latex=(visit_mongodoc_node, depart_mongodoc_node),
+ text=(visit_mongodoc_node, depart_mongodoc_node))
+ app.add_node(mongoref,
+ html=(visit_mongoref_node, depart_mongoref_node))
+
+ app.add_directive("mongodoc", MongodocDirective)
+
+ app.connect("doctree-resolved", process_mongodoc_nodes)
View
237 doc/motor_extensions.py
@@ -0,0 +1,237 @@
+# Copyright 2012 10gen, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Motor specific extensions to Sphinx."""
+
+import inspect
+from distutils.version import LooseVersion
+
+from docutils.nodes import (
+ field, list_item, paragraph, title_reference, field_list, field_body,
+ bullet_list, Text, field_name)
+from sphinx.addnodes import desc, desc_content, versionmodified, desc_signature
+from sphinx.util.inspect import getargspec, safe_getattr
+from sphinx.ext.autodoc import MethodDocumenter, AttributeDocumenter
+
+import motor
+
+
+# TODO: HACK! This is a place to store info while parsing, to be used before
+# generating
+motor_info = {}
+
+
+class MotorAttribute(object):
+ def __init__(self, motor_class, name, delegate_property):
+ super(MotorAttribute, self).__init__()
+ self.motor_class = motor_class
+ self.name = name
+ self.delegate_property = delegate_property
+ self.pymongo_attr = getattr(motor_class.__delegate_class__, name)
+ if self.is_async_method():
+ full_name = '%s.%s.%s' % (
+ self.motor_class.__module__, self.motor_class.__name__,
+ self.name)
+
+ motor_info[full_name] = {
+ 'is_async_method': self.is_async_method(),
+ 'requires_callback': self.requires_callback(),
+ }
+
+ def is_async_method(self):
+ return isinstance(self.delegate_property, motor.Async)
+
+ def requires_callback(self):
+ return self.is_async_method() and self.delegate_property.cb_required
+
+ @property
+ def __doc__(self):
+ return self.pymongo_attr.__doc__ or ''
+
+ def getargspec(self):
+ args, varargs, kwargs, defaults = getargspec(self.pymongo_attr)
+
+ # This part is copied from Sphinx's autodoc.py
+ if args and args[0] in ('cls', 'self'):
+ del args[0]
+
+ # Add 'callback=None' argument
+ defaults = defaults or []
+ prop = self.delegate_property
+ if isinstance(prop, motor.Async):
+ args.append('callback')
+ defaults.append(None)
+
+ return (args, varargs, kwargs, defaults)
+
+ def format_args(self):
+ if self.is_async_method():
+ return inspect.formatargspec(*self.getargspec())
+ else:
+ return None
+
+
+def get_pymongo_attr(obj, name, *defargs):
+ """getattr() override for Motor DelegateProperty."""
+ if isinstance(obj, motor.MotorMeta):
+ for cls in inspect.getmro(obj):
+ if name in cls.__dict__:
+ attr = cls.__dict__[name]
+ if isinstance(attr, motor.DelegateProperty):
+ # 'name' set by MotorMeta
+ assert attr.get_name() == name, (
+ "Expected name %s, got %s" % (name, attr.get_name()))
+ return MotorAttribute(obj, name, attr)
+ return safe_getattr(obj, name, *defargs)
+
+
+class MotorMethodDocumenter(MethodDocumenter):
+ objtype = 'motormethod'
+ directivetype = 'method'
+
+ @staticmethod
+ def get_attr(obj, name, *defargs):
+ return get_pymongo_attr(obj, name, *defargs)
+
+ @classmethod
+ def can_document_member(cls, member, membername, isattr, parent):
+ return isinstance(member, motor.Async)
+
+ def format_args(self):