diff --git a/doc/api/pymongo/collection.rst b/doc/api/pymongo/collection.rst index 1534f84e9..b2d51b31f 100644 --- a/doc/api/pymongo/collection.rst +++ b/doc/api/pymongo/collection.rst @@ -11,6 +11,11 @@ .. autodata:: pymongo.GEOSPHERE .. autodata:: pymongo.HASHED .. autodata:: pymongo.TEXT + .. autodata:: pymongo.cursor.NON_TAILABLE + .. autodata:: pymongo.cursor.TAILABLE + .. autodata:: pymongo.cursor.TAILABLE_AWAIT + .. autodata:: pymongo.cursor.EXHAUST + .. autoclass:: pymongo.collection.Collection(database, name[, create=False[, **kwargs]]]) @@ -35,8 +40,8 @@ .. automethod:: initialize_unordered_bulk_op .. automethod:: initialize_ordered_bulk_op .. automethod:: 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[, await_data=False[, partial=False[, manipulate=True[, exhaust=False]]]]]]]]]]]]]]) - .. automethod:: find_one([spec_or_id=None[, *args[, **kwargs]]]) + .. automethod:: find([filter=None[, projection=None[, skip=0[, limit=0[, no_cursor_timeout=False[, cursor_type=NON_TAILABLE[, sort=None[, allow_partial_results=False[, oplog_replay=False[, modifiers=None[, manipulate=True]]]]]]]]]]]) + .. automethod:: find_one([filter_or_id=None[, *args[, **kwargs]]]) .. automethod:: parallel_scan .. automethod:: count .. automethod:: create_index diff --git a/doc/api/pymongo/cursor.rst b/doc/api/pymongo/cursor.rst index f169accf7..e5da66a5a 100644 --- a/doc/api/pymongo/cursor.rst +++ b/doc/api/pymongo/cursor.rst @@ -4,7 +4,7 @@ .. automodule:: pymongo.cursor :synopsis: Tools for iterating over MongoDB query results - .. autoclass:: pymongo.cursor.Cursor(collection, spec=None, fields=None, skip=0, limit=0, timeout=True, snapshot=False, tailable=False, sort=None, max_scan=None, as_class=None, await_data=False, partial=False, manipulate=True, exhaust=False) + .. autoclass:: pymongo.cursor.Cursor(collection, filter=None, projection=None, skip=0, limit=0, no_cursor_timeout=False, cursor_type=NON_TAILABLE, sort=None, allow_partial_results=False, oplog_replay=False, modifiers=None, manipulate=True) :members: .. describe:: c[index] diff --git a/doc/faq.rst b/doc/faq.rst index 49bedb765..8992fa19c 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -99,7 +99,7 @@ raised when attempting to iterate the cursor. How do I change the timeout value for cursors? ---------------------------------------------- MongoDB doesn't support custom timeouts for cursors, but cursor -timeouts can be turned off entirely. Pass ``timeout=False`` to +timeouts can be turned off entirely. Pass ``no_cursor_timeout=True`` to :meth:`~pymongo.collection.Collection.find`. How can I store :mod:`decimal.Decimal` instances? @@ -273,5 +273,5 @@ out documents with values outside of the range supported by Another option, assuming you don't need the datetime field, is to filter out just that field:: - >>> cur = coll.find({}, fields={'dt': False}) + >>> cur = coll.find({}, projection={'dt': False}) diff --git a/gridfs/__init__.py b/gridfs/__init__.py index 1a521df8f..39d720873 100644 --- a/gridfs/__init__.py +++ b/gridfs/__init__.py @@ -248,7 +248,7 @@ class GridFS(object): name for name in self.__files.distinct("filename") if name is not None] - def find_one(self, spec_or_id=None, *args, **kwargs): + def find_one(self, filter=None, *args, **kwargs): """Get a single file from gridfs. All arguments to :meth:`find` are also valid arguments for @@ -259,18 +259,18 @@ class GridFS(object): file = fs.find_one({"filename": "lisa.txt"}) :Parameters: - - `spec_or_id` (optional): a dictionary specifying - the query to be performing OR any other type to be used as + - `filter` (optional): a dictionary specifying + the query to be performing OR any other type to be used as the value for a query for ``"_id"`` in the file collection. - `*args` (optional): any additional positional arguments are the same as the arguments to :meth:`find`. - `**kwargs` (optional): any additional keyword arguments are the same as the arguments to :meth:`find`. """ - if spec_or_id is not None and not isinstance(spec_or_id, dict): - spec_or_id = {"_id": spec_or_id} + if filter is not None and not isinstance(filter, dict): + filter = {"_id": filter} - for f in self.find(spec_or_id, *args, **kwargs): + for f in self.find(filter, *args, **kwargs): return f return None @@ -282,12 +282,14 @@ class GridFS(object): arbitrary queries on the files collection. Can be combined with other modifiers for additional control. For example:: - for grid_out in fs.find({"filename": "lisa.txt"}, timeout=False): + for grid_out in fs.find({"filename": "lisa.txt"}, + no_cursor_timeout=True): data = grid_out.read() would iterate through all versions of "lisa.txt" stored in GridFS. - Note that setting timeout to False may be important to prevent the - cursor from timing out during long multi-file processing work. + Note that setting no_cursor_timeout to True may be important to + prevent the cursor from timing out during long multi-file processing + work. As another example, the call:: @@ -301,23 +303,21 @@ class GridFS(object): in :class:`~pymongo.collection.Collection`. :Parameters: - - `spec` (optional): a SON object specifying elements which + - `filter` (optional): a SON object specifying elements which must be present for a document to be included in the result set - `skip` (optional): the number of files to omit (from the start of the result set) when returning the results - `limit` (optional): the maximum number of results to return - - `timeout` (optional): if True (the default), any returned - cursor is closed by the server after 10 minutes of - inactivity. If set to False, the returned cursor will never + - `no_cursor_timeout` (optional): if False (the default), any + returned cursor is closed by the server after 10 minutes of + inactivity. If set to True, the returned cursor will never time out on the server. Care should be taken to ensure that - cursors with timeout turned off are properly closed. + cursors with no_cursor_timeout turned on are properly closed. - `sort` (optional): a list of (key, direction) pairs specifying the sort order for this query. See :meth:`~pymongo.cursor.Cursor.sort` for details. - - `max_scan` (optional): limit the number of file documents - examined when performing the query Raises :class:`TypeError` if any of the arguments are of improper type. Returns an instance of diff --git a/gridfs/grid_file.py b/gridfs/grid_file.py index f88c705dc..e615bd5bb 100644 --- a/gridfs/grid_file.py +++ b/gridfs/grid_file.py @@ -594,8 +594,8 @@ class GridOutCursor(Cursor): """A cursor / iterator for returning GridOut objects as the result of an arbitrary query against the GridFS files collection. """ - def __init__(self, collection, spec=None, skip=0, limit=0, - timeout=True, sort=None, max_scan=None): + def __init__(self, collection, filter=None, skip=0, limit=0, + no_cursor_timeout=False, sort=None): """Create a new cursor, similar to the normal :class:`~pymongo.cursor.Cursor`. @@ -610,8 +610,8 @@ class GridOutCursor(Cursor): self.__root_collection = collection super(GridOutCursor, self).__init__( - collection.files, spec, skip=skip, limit=limit, timeout=timeout, - sort=sort, max_scan=max_scan) + collection.files, filter, skip=skip, limit=limit, + no_cursor_timeout=no_cursor_timeout, sort=sort) def next(self): """Get next GridOut object from cursor. diff --git a/pymongo/collection.py b/pymongo/collection.py index 794429fda..d2da882f3 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -732,7 +732,7 @@ class Collection(common.BaseObject): concern, uuid_representation, int(not multi)), safe) - def find_one(self, spec_or_id=None, *args, **kwargs): + def find_one(self, filter=None, *args, **kwargs): """Get a single document from the database. All arguments to :meth:`find` are also valid arguments for @@ -745,7 +745,7 @@ class Collection(common.BaseObject): :Parameters: - - `spec_or_id` (optional): a dictionary specifying + - `filter` (optional): a dictionary specifying the query to be performed OR any other type to be used as the value for a query for ``"_id"``. @@ -760,12 +760,12 @@ class Collection(common.BaseObject): >>> find_one(max_time_ms=100) """ - if (spec_or_id is not None and not - isinstance(spec_or_id, collections.Mapping)): - spec_or_id = {"_id": spec_or_id} + if (filter is not None and not + isinstance(filter, collections.Mapping)): + filter = {"_id": filter} max_time_ms = kwargs.pop("max_time_ms", None) - cursor = self.find(spec_or_id, + cursor = self.find(filter, *args, **kwargs).max_time_ms(max_time_ms) for result in cursor.limit(-1): @@ -775,15 +775,15 @@ class Collection(common.BaseObject): def find(self, *args, **kwargs): """Query the database. - The `spec` argument is a prototype document that all results + The `filter` argument is a prototype document that all results must match. For example: >>> db.test.find({"hello": "world"}) only matches documents that have a key "hello" with value "world". Matches can have other keys *in addition* to - "hello". The `fields` argument is used to specify a subset of - fields that should be included in the result documents. By + "hello". The `projection` argument is used to specify a subset + of fields that should be included in the result documents. By limiting results to a certain subset of fields you can cut down on network traffic and decoding time. @@ -795,78 +795,86 @@ class Collection(common.BaseObject): this :class:`Collection`. :Parameters: - - `spec` (optional): a SON object specifying elements which + - `filter` (optional): a SON object specifying elements which must be present for a document to be included in the result set - - `fields` (optional): a list of field names that should be + - `projection` (optional): a list of field names that should be returned in the result set or a dict specifying the fields - to include or exclude. If `fields` is a list "_id" will + to include or exclude. If `projection` is a list "_id" will always be returned. Use a dict to exclude fields from - the result (e.g. fields={'_id': False}). + the result (e.g. projection={'_id': False}). - `skip` (optional): the number of documents to omit (from the start of the result set) when returning the results - `limit` (optional): the maximum number of results to return - - `timeout` (optional): if True (the default), any returned - cursor is closed by the server after 10 minutes of - inactivity. If set to False, the returned cursor will never + - `no_cursor_timeout` (optional): if False (the default), any + returned cursor is closed by the server after 10 minutes of + inactivity. If set to True, the returned cursor will never time out on the server. Care should be taken to ensure that - cursors with timeout turned off are properly closed. - - `snapshot` (optional): if True, snapshot mode will be used - for this query. Snapshot mode assures no duplicates are - returned, or objects missed, which were present at both - the start and end of the query's execution. For details, - see the `snapshot documentation - `_. - - `tailable` (optional): the result of this find call will - be a tailable cursor - tailable cursors aren't closed when - the last data is retrieved but are kept open and the - cursors location marks the final document's position. if - more data is received iteration of the cursor will - continue from the last document received. For details, see - the `tailable cursor documentation - `_. + cursors with no_cursor_timeout turned on are properly closed. + - `cursor_type` (optional): the type of cursor to return. The valid + options are: + + - :data:`~pymongo.cursor.NON_TAILABLE` - the result of this find + call will return a standard cursor over the result set. + - :data:`~pymongo.cursor.TAILABLE` - the result of this find call + will be a tailable cursor - tailable cursors are only for use + with capped collections. They are not closed when the last data + is retrieved but are kept open and the cursor location marks the + final document position. If more data is received iteration of + the cursor will continue from the last document received. For + details, see the `tailable cursor documentation + `_. + - :data:`~pymongo.cursor.TAILABLE_AWAIT` - the result of this find + call will be a tailable cursor with the await flag set. The + server will wait for a few seconds after returning the full + result set so that it can capture and return additional data + added during the query. + - :data:`~pymongo.cursor.EXHAUST` - the result of this find call + will be an exhaust cursor. MongoDB will stream batched results to + the client without waiting for the client to request each batch, + reducing latency. See notes on compatibility below. + - `sort` (optional): a list of (key, direction) pairs specifying the sort order for this query. See :meth:`~pymongo.cursor.Cursor.sort` for details. - - `max_scan` (optional): limit the number of documents - examined when performing the query - - `as_class` (optional): class to use for documents in the - query result (default is - :attr:`~pymongo.mongo_client.MongoClient.document_class`) - - `await_data` (optional): if True, the server will block for - some extra time before returning, waiting for more data to - return. Ignored if `tailable` is False. - - `partial` (optional): if True, mongos will return partial - results if some shards are down instead of returning an error. + - `allow_partial_results` (optional): if True, mongos will return + partial results if some shards are down instead of returning an + error. + - `oplog_replay` (optional): If True, set the oplogReplay query + flag. + - `modifiers` (optional): A dict specifying the MongoDB query + modifiers that should be used for this query. - `manipulate`: (optional): If True (the default), apply any outgoing SON manipulators before returning. - - `exhaust` (optional): If ``True`` create an "exhaust" cursor. - MongoDB will stream batched results to the client without waiting - for the client to request each batch, reducing latency. - .. note:: There are a number of caveats to using the `exhaust` - parameter: + .. note:: There are a number of caveats to using + :data:`~pymongo.cursor.EXHAUST` as cursor_type: - 1. The `exhaust` and `limit` options are incompatible and can - not be used together. + 1. The `limit` option can not be used with an exhaust cursor. - 2. The `exhaust` option is not supported by mongos and can not be + 2. Exhaust cursors are not supported by mongos and can not be used with a sharded cluster. 3. A :class:`~pymongo.cursor.Cursor` instance created with the - `exhaust` option requires an exclusive :class:`~socket.socket` - connection to MongoDB. If the :class:`~pymongo.cursor.Cursor` is - discarded without being completely iterated the underlying - :class:`~socket.socket` connection will be closed and discarded - without being returned to the connection pool. + cursor.EXHAUST cursor_type requires an exclusive + :class:`~socket.socket` connection to MongoDB. If the + :class:`~pymongo.cursor.Cursor` is discarded without being + completely iterated the underlying :class:`~socket.socket` + connection will be closed and discarded without being returned to + the connection pool. .. note:: The `manipulate` parameter may default to False in a future release. .. versionchanged:: 3.0 - Removed the `network_timeout`, `read_preference`, `tag_sets`, and - `secondary_acceptable_latency_ms` parameters. + Changed the parameter names `spec`, `fields`, `timeout`, and + `partial` to `filter`, `projection`, `no_cursor_timeout`, and + `allow_partial_results` respectively. + Added the `cursor_type`, `oplog_replay`, and `modifiers` options. + Removed the `network_timeout`, `read_preference`, `tag_sets`, + `secondary_acceptable_latency_ms`, `max_scan`, `snapshot`, + `tailable`, `await_data`, `exhaust`, and `as_class` parameters. Removed `compile_re` option: PyMongo now always represents BSON regular expressions as :class:`~bson.regex.Regex` objects. Use :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a @@ -1291,8 +1299,9 @@ class Collection(common.BaseObject): else: raw = self.__database.get_collection( "system.indexes", + codec_options=CodecOptions(as_class=SON), read_preference=ReadPreference.PRIMARY).find( - {"ns": self.__full_name}, {"ns": 0}, as_class=SON) + {"ns": self.__full_name}, {"ns": 0}) info = {} for index in raw: index["key"] = index["key"].items() @@ -1696,7 +1705,7 @@ class Collection(common.BaseObject): - `remove`: remove rather than updating (default ``False``) - `new`: return updated rather than original object (default ``False``) - - `fields`: see second argument to :meth:`find` (default all) + - `fields`: see `projection` argument to :meth:`find` (default all) - `manipulate`: (optional): If ``True``, apply any outgoing SON manipulators before returning. Ignored when `full_response` is set to True. Defaults to ``False``. diff --git a/pymongo/cursor.py b/pymongo/cursor.py index cad96895b..0c9bb2c09 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -26,7 +26,6 @@ from bson.py3compat import (iteritems, string_type) from bson.son import SON from pymongo import helpers, message -from pymongo.codec_options import CodecOptions from pymongo.read_preferences import ReadPreference from pymongo.errors import (AutoReconnect, InvalidOperation, @@ -43,6 +42,34 @@ _QUERY_OPTIONS = { "partial": 128} +NON_TAILABLE = 0 +"""The standard cursor type.""" + +TAILABLE = _QUERY_OPTIONS["tailable_cursor"] +"""The tailable cursor type. + +Tailable cursors are only for use with capped collections. They are not closed +when the last data is retrieved but are kept open and the cursor location marks +the final document position. If more data is received iteration of the cursor +will continue from the last document received. +""" + +TAILABLE_AWAIT = TAILABLE | _QUERY_OPTIONS["await_data"] +"""A tailable cursor with the await option set. + +Creates a tailable cursor that will wait for a few seconds after returning the +full result set so that it can capture and return additional data added during +the query. +""" + +EXHAUST = _QUERY_OPTIONS["exhaust"] +"""An exhaust cursor. + +MongoDB will stream batched results to the client without waiting for the +client to request each batch, reducing latency. +""" + + # This has to be an old style class due to # http://bugs.jython.org/issue1057 class _SocketManager: @@ -65,17 +92,14 @@ class _SocketManager: self.sock, self.pool = None, None -# TODO might be cool to be able to do find().include("foo") or -# find().exclude(["bar", "baz"]) or find().slice("a", 1, 2) as an -# alternative to the fields specifier. class Cursor(object): """A cursor / iterator over Mongo query results. """ - def __init__(self, collection, spec=None, fields=None, skip=0, limit=0, - timeout=True, snapshot=False, tailable=False, sort=None, - max_scan=None, as_class=None, await_data=False, partial=False, - manipulate=True, exhaust=False): + def __init__(self, collection, filter=None, projection=None, skip=0, + limit=0, no_cursor_timeout=False, cursor_type=NON_TAILABLE, + sort=None, allow_partial_results=False, oplog_replay=False, + modifiers=None, manipulate=True): """Create a new cursor. Should not be called directly by application developers - see @@ -85,61 +109,62 @@ class Cursor(object): """ self.__id = None + spec = filter if spec is None: spec = {} if not isinstance(spec, Mapping): - raise TypeError("spec must be a mapping type.") + raise TypeError("filter must be a mapping type.") if not isinstance(skip, int): raise TypeError("skip must be an instance of int") if not isinstance(limit, int): raise TypeError("limit must be an instance of int") - if not isinstance(timeout, bool): - raise TypeError("timeout must be an instance of bool") - if not isinstance(snapshot, bool): - raise TypeError("snapshot must be an instance of bool") - if not isinstance(tailable, bool): - raise TypeError("tailable must be an instance of bool") - if not isinstance(await_data, bool): - raise TypeError("await_data must be an instance of bool") - if not isinstance(partial, bool): - raise TypeError("partial must be an instance of bool") - if not isinstance(exhaust, bool): - raise TypeError("exhaust must be an instance of bool") + if not isinstance(no_cursor_timeout, bool): + raise TypeError("no_cursor_timeout must be an instance of bool") + if cursor_type not in (NON_TAILABLE, TAILABLE, + TAILABLE_AWAIT, EXHAUST): + raise ValueError("not a valid value for cursor_type") + if not isinstance(allow_partial_results, bool): + raise TypeError("allow_partial_results " + "must be an instance of bool") + if not isinstance(oplog_replay, bool): + raise TypeError("oplog_replay must be an instance of bool") + if modifiers is not None and not isinstance(modifiers, Mapping): + raise TypeError("modifiers must be a mapping type.") - if fields is not None: - if not fields: - fields = {"_id": 1} - if not isinstance(fields, Mapping): - fields = helpers._fields_list_to_dict(fields) - - # XXX: Continue to support passing as_class to find() and find_one()? - if as_class is None: - self.__codec_options = collection.codec_options - else: - cco = collection.codec_options - self.__codec_options = CodecOptions(as_class, - cco.tz_aware, - cco.uuid_representation) + if projection is not None: + if not projection: + projection = {"_id": 1} + if not isinstance(projection, Mapping): + projection = helpers._fields_list_to_dict(projection) self.__collection = collection self.__spec = spec - self.__fields = fields + self.__projection = projection self.__skip = skip self.__limit = limit - self.__max_time_ms = None self.__batch_size = 0 + self.__modifiers = modifiers and modifiers.copy() or {} + self.__ordering = sort and helpers._index_document(sort) or None + self.__max_scan = None + self.__explain = False + self.__hint = None + self.__comment = None + self.__max_time_ms = None self.__max = None self.__min = None + self.__manipulate = manipulate # Exhaust cursor support - if self.__collection.database.connection.is_mongos and exhaust: - raise InvalidOperation('Exhaust cursors are ' - 'not supported by mongos') - if limit and exhaust: - raise InvalidOperation("Can't use limit and exhaust together.") - self.__exhaust = exhaust + self.__exhaust = False self.__exhaust_mgr = None + if cursor_type == EXHAUST: + if self.__collection.database.connection.is_mongos: + raise InvalidOperation('Exhaust cursors are ' + 'not supported by mongos') + if limit: + raise InvalidOperation("Can't use limit and exhaust together.") + self.__exhaust = True # This is ugly. People want to be able to do cursor[5:5] and # get an empty result set (old behavior was an @@ -149,34 +174,23 @@ class Cursor(object): # it anytime we change __limit. self.__empty = False - self.__snapshot = snapshot - self.__ordering = sort and helpers._index_document(sort) or None - self.__max_scan = max_scan - self.__explain = False - self.__hint = None - self.__comment = None - self.__manipulate = manipulate - self.__data = deque() self.__address = None self.__retrieved = 0 self.__killed = False + self.__codec_options = collection.codec_options self.__read_preference = collection.read_preference - self.__query_flags = 0 - if tailable: - self.__query_flags |= _QUERY_OPTIONS["tailable_cursor"] - if self.__read_preference.mode != ReadPreference.PRIMARY.mode: + self.__query_flags = cursor_type + if self.__read_preference != ReadPreference.PRIMARY: self.__query_flags |= _QUERY_OPTIONS["slave_okay"] - if not timeout: + if no_cursor_timeout: self.__query_flags |= _QUERY_OPTIONS["no_timeout"] - if tailable and await_data: - self.__query_flags |= _QUERY_OPTIONS["await_data"] - if exhaust: - self.__query_flags |= _QUERY_OPTIONS["exhaust"] - if partial: + if allow_partial_results: self.__query_flags |= _QUERY_OPTIONS["partial"] + if oplog_replay: + self.__query_flags |= _QUERY_OPTIONS["oplog_replay"] @property def collection(self): @@ -224,10 +238,11 @@ class Cursor(object): def _clone(self, deepcopy=True): clone = self._clone_base() - values_to_clone = ("spec", "fields", "skip", "limit", "max_time_ms", - "comment", "max", "min", "snapshot", "ordering", - "explain", "hint", "batch_size", "max_scan", - "manipulate", "query_flags", "codec_options") + values_to_clone = ("spec", "projection", "skip", "limit", + "max_time_ms", "comment", "max", "min", + "ordering", "explain", "hint", "batch_size", + "max_scan", "manipulate", "query_flags", + "modifiers") data = dict((k, v) for k, v in iteritems(self.__dict__) if k.startswith('_Cursor__') and k[9:] in values_to_clone) if deepcopy: @@ -266,7 +281,7 @@ class Cursor(object): def __query_spec(self): """Get the spec to use for a query. """ - operators = {} + operators = self.__modifiers.copy() if self.__ordering: operators["$orderby"] = self.__ordering if self.__explain: @@ -275,8 +290,6 @@ class Cursor(object): operators["$hint"] = self.__hint if self.__comment: operators["$comment"] = self.__comment - if self.__snapshot: - operators["$snapshot"] = True if self.__max_scan: operators["$maxScan"] = self.__max_scan if self.__max_time_ms is not None: @@ -898,7 +911,7 @@ class Cursor(object): message.query(self.__query_flags, self.__collection.full_name, self.__skip, ntoreturn, - self.__query_spec(), self.__fields, + self.__query_spec(), self.__projection, self.__codec_options.uuid_representation)) if not self.__id: self.__killed = True diff --git a/pymongo/database.py b/pymongo/database.py index 7b85f10ba..618277a25 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -1048,4 +1048,4 @@ class SystemJS(object): def list(self): """Get a list of the names of the functions stored in this database.""" - return [x["_id"] for x in self._db.system.js.find(fields=["_id"])] + return [x["_id"] for x in self._db.system.js.find(projection=["_id"])] diff --git a/test/test_client.py b/test/test_client.py index 09e997b62..05c7a23dd 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -29,9 +29,9 @@ sys.path[0:0] = [""] from bson.py3compat import thread, u from bson.son import SON from bson.tz_util import utc -from pymongo.mongo_client import MongoClient -from pymongo.database import Database from pymongo import auth, message +from pymongo.cursor import EXHAUST +from pymongo.database import Database from pymongo.codec_options import CodecOptions from pymongo.errors import (AutoReconnect, ConfigurationError, @@ -41,6 +41,7 @@ from pymongo.errors import (AutoReconnect, CursorNotFound, NetworkTimeout, InvalidURI) +from pymongo.mongo_client import MongoClient from pymongo.read_preferences import ReadPreference from pymongo.server_selectors import (any_server_selector, writable_server_selector) @@ -477,7 +478,6 @@ class TestClient(IntegrationTest): self.assertEqual(SON, c.document_class) self.assertTrue(isinstance(db.test.find_one(), SON)) - self.assertFalse(isinstance(db.test.find_one(as_class=dict), SON)) # document_class is read-only in PyMongo 3.0. with self.assertRaises(AttributeError): @@ -764,7 +764,7 @@ class TestClient(IntegrationTest): # Cause a network error. sock_info = one(pool.sockets) sock_info.sock.close() - cursor = collection.find(exhaust=True) + cursor = collection.find(cursor_type=EXHAUST) with self.assertRaises(ConnectionFailure): next(cursor) @@ -856,7 +856,7 @@ class TestExhaustCursor(IntegrationTest): # This will cause OperationFailure in all mongo versions since # the value for $orderby must be a document. cursor = collection.find( - SON([('$query', {}), ('$orderby', True)]), exhaust=True) + SON([('$query', {}), ('$orderby', True)]), cursor_type=EXHAUST) self.assertRaises(OperationFailure, cursor.next) self.assertFalse(sock_info.closed) @@ -880,7 +880,7 @@ class TestExhaustCursor(IntegrationTest): pool._check_interval_seconds = None # Never check. sock_info = one(pool.sockets) - cursor = collection.find(exhaust=True) + cursor = collection.find(cursor_type=EXHAUST) # Initial query succeeds. cursor.next() @@ -905,7 +905,7 @@ class TestExhaustCursor(IntegrationTest): sock_info = one(pool.sockets) sock_info.sock.close() - cursor = collection.find(exhaust=True) + cursor = collection.find(cursor_type=EXHAUST) self.assertRaises(ConnectionFailure, cursor.next) self.assertTrue(sock_info.closed) @@ -923,7 +923,7 @@ class TestExhaustCursor(IntegrationTest): pool = get_pool(client) pool._check_interval_seconds = None # Never check. - cursor = collection.find(exhaust=True) + cursor = collection.find(cursor_type=EXHAUST) # Initial query succeeds. cursor.next() diff --git a/test/test_collection.py b/test/test_collection.py index 179a9a24e..b237bfae5 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -37,6 +37,7 @@ from pymongo import MongoClient from pymongo.codec_options import CodecOptions from pymongo.collection import Collection from pymongo.command_cursor import CommandCursor +from pymongo.cursor import EXHAUST from pymongo.errors import (DocumentTooLarge, DuplicateKeyError, InvalidDocument, @@ -606,17 +607,20 @@ class TestCollection(IntegrationTest): self.assertEqual(doc["_id"], id) self.assertTrue(isinstance(id, ObjectId)) - doc_class = None + doc_class = dict # Work around http://bugs.jython.org/issue1728 if (sys.platform.startswith('java') and sys.version_info[:3] >= (2, 5, 2)): doc_class = SON + db = self.client.get_database( + db.name, codec_options=CodecOptions(as_class=doc_class)) + def remove_insert_find_one(doc): db.test.remove({}) db.test.insert(doc) # SON equality is order sensitive. - return db.test.find_one(as_class=doc_class) == doc.to_dict() + return db.test.find_one() == doc.to_dict() qcheck.check_unittest(self, remove_insert_find_one, qcheck.gen_mongo_dict(3)) @@ -693,10 +697,10 @@ class TestCollection(IntegrationTest): self.assertEqual([1, 2, 3], db.test.find_one()["x"]) if client_context.version.at_least(1, 5, 1): self.assertEqual([2, 3], - db.test.find_one(fields={"x": {"$slice": - -2}})["x"]) - self.assertTrue("x" not in db.test.find_one(fields={"x": 0})) - self.assertTrue("mike" in db.test.find_one(fields={"x": 0})) + db.test.find_one( + projection={"x": {"$slice": -2}})["x"]) + self.assertTrue("x" not in db.test.find_one(projection={"x": 0})) + self.assertTrue("mike" in db.test.find_one(projection={"x": 0})) def test_find_w_regex(self): db = self.db @@ -1515,16 +1519,6 @@ class TestCollection(IntegrationTest): self.assertRaises(OperationFailure, db.foo.rename, "test") db.foo.rename("test", dropTarget=True) - # doesn't really test functionality, just that the option is set correctly - def test_snapshot(self): - db = self.db - - self.assertRaises(TypeError, db.test.find, snapshot=5) - - list(db.test.find(snapshot=True)) - self.assertRaises(OperationFailure, list, - db.test.find(snapshot=True).sort("foo", 1)) - def test_find_one(self): db = self.db db.drop_collection("test") @@ -1538,9 +1532,9 @@ class TestCollection(IntegrationTest): self.assertEqual(db.test.find_one({"hello": "world"}), db.test.find_one()) - self.assertTrue("hello" in db.test.find_one(fields=["hello"])) - self.assertTrue("hello" not in db.test.find_one(fields=["foo"])) - self.assertEqual(["_id"], list(db.test.find_one(fields=[]))) + self.assertTrue("hello" in db.test.find_one(projection=["hello"])) + self.assertTrue("hello" not in db.test.find_one(projection=["foo"])) + self.assertEqual(["_id"], list(db.test.find_one(projection=[]))) self.assertEqual(None, db.test.find_one({"hello": "foo"})) self.assertEqual(None, db.test.find_one(ObjectId())) @@ -1615,20 +1609,19 @@ class TestCollection(IntegrationTest): # TODO doesn't actually test functionality, just that it doesn't blow up def test_cursor_timeout(self): - list(self.db.test.find(timeout=False)) - list(self.db.test.find(timeout=True)) + list(self.db.test.find(no_cursor_timeout=True)) + list(self.db.test.find(no_cursor_timeout=False)) def test_exhaust(self): if is_mongos(self.db.connection): self.assertRaises(InvalidOperation, - self.db.test.find, exhaust=True) + self.db.test.find, cursor_type=EXHAUST) return - self.assertRaises(TypeError, self.db.test.find, exhaust=5) # Limit is incompatible with exhaust. self.assertRaises(InvalidOperation, - self.db.test.find, exhaust=True, limit=5) - cur = self.db.test.find(exhaust=True) + self.db.test.find, cursor_type=EXHAUST, limit=5) + cur = self.db.test.find(cursor_type=EXHAUST) self.assertRaises(InvalidOperation, cur.limit, 5) cur = self.db.test.find(limit=5) self.assertRaises(InvalidOperation, cur.add_option, 64) @@ -1644,7 +1637,7 @@ class TestCollection(IntegrationTest): socks = get_pool(client).sockets # Make sure the socket is returned after exhaustion. - cur = client[self.db.name].test.find(exhaust=True) + cur = client[self.db.name].test.find(cursor_type=EXHAUST) next(cur) self.assertEqual(0, len(socks)) for doc in cur: @@ -1652,14 +1645,14 @@ class TestCollection(IntegrationTest): self.assertEqual(1, len(socks)) # Same as previous but don't call next() - for doc in client[self.db.name].test.find(exhaust=True): + for doc in client[self.db.name].test.find(cursor_type=EXHAUST): pass self.assertEqual(1, len(socks)) # If the Cursor instance is discarded before being # completely iterated we have to close and # discard the socket. - cur = client[self.db.name].test.find(exhaust=True) + cur = client[self.db.name].test.find(cursor_type=EXHAUST) next(cur) self.assertEqual(0, len(socks)) if sys.platform.startswith('java') or 'PyPy' in sys.version: @@ -1963,26 +1956,6 @@ class TestCollection(IntegrationTest): c.insert(id_only, check_keys=False) self.assertEqual(id_only, c.find_one()) - def test_as_class(self): - c = self.db.test - c.drop() - c.insert({"x": 1}) - - doc = next(c.find()) - self.assertTrue(isinstance(doc, dict)) - doc = next(c.find()) - self.assertFalse(isinstance(doc, SON)) - doc = next(c.find(as_class=SON)) - self.assertTrue(isinstance(doc, SON)) - - self.assertTrue(isinstance(c.find_one(), dict)) - self.assertFalse(isinstance(c.find_one(), SON)) - self.assertTrue(isinstance(c.find_one(as_class=SON), SON)) - - self.assertEqual(1, c.find_one(as_class=SON)["x"]) - doc = next(c.find(as_class=SON)) - self.assertEqual(1, doc["x"]) - def test_find_and_modify(self): c = self.db.test c.drop() diff --git a/test/test_cursor.py b/test/test_cursor.py index 21b7c6d79..0b60944e8 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -30,6 +30,7 @@ from pymongo import (MongoClient, ALL, OFF) from pymongo.command_cursor import CommandCursor +from pymongo.cursor import TAILABLE, TAILABLE_AWAIT, EXHAUST from pymongo.cursor_manager import CursorManager from pymongo.errors import (InvalidOperation, OperationFailure, @@ -67,18 +68,17 @@ class TestCursorNoConnect(unittest.TestCase): cursor = self.db.test.find() self.assertEqual(0, cursor._Cursor__query_flags) cursor.add_option(2) - cursor2 = self.db.test.find(tailable=True) + cursor2 = self.db.test.find(cursor_type=TAILABLE) self.assertEqual(2, cursor2._Cursor__query_flags) self.assertEqual(cursor._Cursor__query_flags, cursor2._Cursor__query_flags) cursor.add_option(32) - cursor2 = self.db.test.find(tailable=True, await_data=True) + cursor2 = self.db.test.find(cursor_type=TAILABLE_AWAIT) self.assertEqual(34, cursor2._Cursor__query_flags) self.assertEqual(cursor._Cursor__query_flags, cursor2._Cursor__query_flags) cursor.add_option(128) - cursor2 = self.db.test.find(tailable=True, - await_data=True).add_option(128) + cursor2 = self.db.test.find(cursor_type=TAILABLE_AWAIT).add_option(128) self.assertEqual(162, cursor2._Cursor__query_flags) self.assertEqual(cursor._Cursor__query_flags, cursor2._Cursor__query_flags) @@ -88,12 +88,12 @@ class TestCursorNoConnect(unittest.TestCase): self.assertEqual(162, cursor._Cursor__query_flags) cursor.remove_option(128) - cursor2 = self.db.test.find(tailable=True, await_data=True) + cursor2 = self.db.test.find(cursor_type=TAILABLE_AWAIT) self.assertEqual(34, cursor2._Cursor__query_flags) self.assertEqual(cursor._Cursor__query_flags, cursor2._Cursor__query_flags) cursor.remove_option(32) - cursor2 = self.db.test.find(tailable=True) + cursor2 = self.db.test.find(cursor_type=TAILABLE) self.assertEqual(2, cursor2._Cursor__query_flags) self.assertEqual(cursor._Cursor__query_flags, cursor2._Cursor__query_flags) @@ -103,7 +103,7 @@ class TestCursorNoConnect(unittest.TestCase): self.assertEqual(2, cursor._Cursor__query_flags) # Timeout - cursor = self.db.test.find(timeout=False) + cursor = self.db.test.find(no_cursor_timeout=True) self.assertEqual(16, cursor._Cursor__query_flags) cursor2 = self.db.test.find().add_option(16) self.assertEqual(cursor._Cursor__query_flags, @@ -112,7 +112,7 @@ class TestCursorNoConnect(unittest.TestCase): self.assertEqual(0, cursor._Cursor__query_flags) # Tailable / Await data - cursor = self.db.test.find(tailable=True, await_data=True) + cursor = self.db.test.find(cursor_type=TAILABLE_AWAIT) self.assertEqual(34, cursor._Cursor__query_flags) cursor2 = self.db.test.find().add_option(34) self.assertEqual(cursor._Cursor__query_flags, @@ -121,7 +121,7 @@ class TestCursorNoConnect(unittest.TestCase): self.assertEqual(2, cursor._Cursor__query_flags) # Exhaust - which mongos doesn't support - cursor = self.db.test.find(exhaust=True) + cursor = self.db.test.find(cursor_type=EXHAUST) self.assertEqual(64, cursor._Cursor__query_flags) cursor2 = self.db.test.find().add_option(64) self.assertEqual(cursor._Cursor__query_flags, @@ -132,7 +132,7 @@ class TestCursorNoConnect(unittest.TestCase): self.assertFalse(cursor._Cursor__exhaust) # Partial - cursor = self.db.test.find(partial=True) + cursor = self.db.test.find(allow_partial_results=True) self.assertEqual(128, cursor._Cursor__query_flags) cursor2 = self.db.test.find().add_option(128) self.assertEqual(cursor._Cursor__query_flags, @@ -735,26 +735,14 @@ class TestCursor(IntegrationTest): self.assertNotEqual(cursor, cursor.clone()) - class MyClass(dict): - pass - - cursor = self.db.test.find(as_class=MyClass) - for e in cursor: - self.assertEqual(type(MyClass()), type(e)) - cursor = self.db.test.find(as_class=MyClass) - self.assertEqual(type(MyClass()), type(cursor[0])) - # Just test attributes cursor = self.db.test.find({"x": re.compile("^hello.*")}, skip=1, - timeout=False, - snapshot=True, - tailable=True, - as_class=MyClass, - await_data=True, - partial=True, + no_cursor_timeout=True, + cursor_type=TAILABLE_AWAIT, + allow_partial_results=True, manipulate=False, - fields={'_id': False}).limit(2) + projection={'_id': False}).limit(2) cursor.min([('a', 1)]).max([('b', 3)]) cursor.add_option(128) cursor.comment('hi!') @@ -762,7 +750,6 @@ class TestCursor(IntegrationTest): cursor2 = cursor.clone() self.assertEqual(cursor._Cursor__skip, cursor2._Cursor__skip) self.assertEqual(cursor._Cursor__limit, cursor2._Cursor__limit) - self.assertEqual(cursor._Cursor__snapshot, cursor2._Cursor__snapshot) self.assertEqual(type(cursor._Cursor__codec_options), type(cursor2._Cursor__codec_options)) self.assertEqual(cursor._Cursor__manipulate, @@ -778,17 +765,17 @@ class TestCursor(IntegrationTest): # Shallow copies can so can mutate cursor2 = copy.copy(cursor) - cursor2._Cursor__fields['cursor2'] = False - self.assertTrue('cursor2' in cursor._Cursor__fields) + cursor2._Cursor__projection['cursor2'] = False + self.assertTrue('cursor2' in cursor._Cursor__projection) # Deepcopies and shouldn't mutate cursor3 = copy.deepcopy(cursor) - cursor3._Cursor__fields['cursor3'] = False - self.assertFalse('cursor3' in cursor._Cursor__fields) + cursor3._Cursor__projection['cursor3'] = False + self.assertFalse('cursor3' in cursor._Cursor__projection) cursor4 = cursor.clone() - cursor4._Cursor__fields['cursor4'] = False - self.assertFalse('cursor4' in cursor._Cursor__fields) + cursor4._Cursor__projection['cursor4'] = False + self.assertFalse('cursor4' in cursor._Cursor__projection) # Test memo when deepcopying queries query = {"hello": "world"} @@ -957,7 +944,7 @@ class TestCursor(IntegrationTest): db.create_collection("test", capped=True, size=1000, max=3) try: - cursor = db.test.find(tailable=True) + cursor = db.test.find(cursor_type=TAILABLE) db.test.insert({"x": 1}) count = 0 @@ -1027,7 +1014,7 @@ class TestCursor(IntegrationTest): self.db.test.insert({}) self.assertEqual(100, len(list(self.db.test.find()))) - self.assertEqual(50, len(list(self.db.test.find(max_scan=50)))) + self.assertEqual(50, len(list(self.db.test.find().max_scan(50)))) self.assertEqual(50, len(list(self.db.test.find() .max_scan(90).max_scan(50)))) diff --git a/test/test_database.py b/test/test_database.py index 7fd14a4fa..1b2f81a1a 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -594,7 +594,9 @@ class TestDatabase(IntegrationTest): db.test.insert(SON([("hello", "world"), ("_id", 5)])) - cursor = db.test.find(as_class=SON) + db = self.client.get_database( + "pymongo_test", codec_options=CodecOptions(as_class=SON)) + cursor = db.test.find() for x in cursor: for (k, v) in x.items(): self.assertEqual(k, "_id") @@ -627,10 +629,11 @@ class TestDatabase(IntegrationTest): db.test.remove({}) db.test.insert({"_id": 4, "foo": "bar"}) + db = self.client.get_database( + "pymongo_test", codec_options=CodecOptions(as_class=SON)) self.assertEqual(SON([("foo", "bar")]), db.dereference(DBRef("test", 4), - fields={"_id": False}, - as_class=SON)) + projection={"_id": False})) @client_context.require_no_auth def test_eval(self): diff --git a/test/test_grid_file.py b/test/test_grid_file.py index ed951e6c2..1af514a24 100644 --- a/test/test_grid_file.py +++ b/test/test_grid_file.py @@ -75,9 +75,7 @@ class TestGridFileNoConnect(unittest.TestCase): def test_grid_out_cursor_options(self): self.assertRaises(TypeError, GridOutCursor.__init__, self.db.fs, {}, - tailable=True) - self.assertRaises(TypeError, GridOutCursor.__init__, self.db.fs, {}, - fields={"filename": 1}) + projection={"filename": 1}) cursor = GridOutCursor(self.db.fs, {}) cursor_clone = cursor.clone() diff --git a/test/test_gridfs.py b/test/test_gridfs.py index 3d9f70ef5..521d6c9d8 100644 --- a/test/test_gridfs.py +++ b/test/test_gridfs.py @@ -394,7 +394,8 @@ class TestGridfs(IntegrationTest): self.fs.put(b"test2++", filename="two") self.assertEqual(3, self.fs.find({"filename":"two"}).count()) self.assertEqual(4, self.fs.find().count()) - cursor = self.fs.find(timeout=False).sort("uploadDate", -1).skip(1).limit(2) + cursor = self.fs.find( + no_cursor_timeout=False).sort("uploadDate", -1).skip(1).limit(2) gout = next(cursor) self.assertEqual(b"test1", gout.read()) cursor.rewind()