diff --git a/doc/api/pymongo/collection.rst b/doc/api/pymongo/collection.rst index 86dbbaa71..7b596637f 100644 --- a/doc/api/pymongo/collection.rst +++ b/doc/api/pymongo/collection.rst @@ -30,6 +30,7 @@ .. autoattribute:: secondary_acceptable_latency_ms .. autoattribute:: write_concern .. autoattribute:: uuid_subtype + .. automethod:: with_options .. automethod:: insert(doc_or_docs[, manipulate=True[, safe=None[, check_keys=True[, continue_on_error=False[, **kwargs]]]]]) .. automethod:: save(to_save[, manipulate=True[, safe=None[, check_keys=True[, **kwargs]]]]) .. automethod:: update(spec, document[, upsert=False[, manipulate=False[, safe=None[, multi=False[, check_keys=True[, **kwargs]]]]]]) diff --git a/doc/api/pymongo/connection.rst b/doc/api/pymongo/connection.rst index 8601dd1b3..88417b2a1 100644 --- a/doc/api/pymongo/connection.rst +++ b/doc/api/pymongo/connection.rst @@ -41,6 +41,7 @@ .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) .. automethod:: get_default_database + .. automethod:: get_database .. automethod:: server_info .. automethod:: start_request .. automethod:: in_request diff --git a/doc/api/pymongo/mongo_client.rst b/doc/api/pymongo/mongo_client.rst index 13c9fa824..38bfee5c4 100644 --- a/doc/api/pymongo/mongo_client.rst +++ b/doc/api/pymongo/mongo_client.rst @@ -41,6 +41,7 @@ .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) .. automethod:: get_default_database + .. automethod:: get_database .. automethod:: server_info .. automethod:: start_request .. automethod:: in_request diff --git a/doc/api/pymongo/mongo_replica_set_client.rst b/doc/api/pymongo/mongo_replica_set_client.rst index 9e6a35f5c..0569bbed2 100644 --- a/doc/api/pymongo/mongo_replica_set_client.rst +++ b/doc/api/pymongo/mongo_replica_set_client.rst @@ -41,4 +41,5 @@ .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) .. automethod:: get_default_database + .. automethod:: get_database .. automethod:: close_cursor diff --git a/doc/api/pymongo/replica_set_connection.rst b/doc/api/pymongo/replica_set_connection.rst index 3b9b1042c..c0ad1cc16 100644 --- a/doc/api/pymongo/replica_set_connection.rst +++ b/doc/api/pymongo/replica_set_connection.rst @@ -40,6 +40,7 @@ .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) .. automethod:: get_default_database + .. automethod:: get_database .. automethod:: close_cursor .. automethod:: get_lasterror_options .. automethod:: set_lasterror_options diff --git a/doc/changelog.rst b/doc/changelog.rst index 849126c08..dfcb57224 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -1,6 +1,36 @@ Changelog ========= +Changes in Version 2.9 +---------------------- + +.. warning:: + In previous versions of PyMongo, changing the value of + :attr:`~pymongo.mongo_client.MongoClient.document_class` changed + the behavior of all existing instances of + :class:`~pymongo.collection.Collection`:: + + >>> coll = client.test.test + >>> coll.find_one() + {u'_id': ObjectId('5579dc7cfba5220cc14d9a18')} + >>> from bson.son import SON + >>> client.document_class = SON + >>> coll.find_one() + SON([(u'_id', ObjectId('5579dc7cfba5220cc14d9a18'))]) + + The document_class setting is now configurable at the client, + database, collection, and per-operation level. This required breaking + the existing behavior. To change the document class per operation in a + forward compatible way use + :meth:`~pymongo.collection.Collection.with_options`:: + + >>> coll.find_one() + {u'_id': ObjectId('5579dc7cfba5220cc14d9a18')} + >>> from bson.codec_options import CodecOptions + >>> coll.with_options(CodecOptions(SON)).find_one() + SON([(u'_id', ObjectId('5579dc7cfba5220cc14d9a18'))]) + + Changes in Version 2.8.1 ------------------------ diff --git a/pymongo/__init__.py b/pymongo/__init__.py index 3d46c5aff..fbe33745f 100644 --- a/pymongo/__init__.py +++ b/pymongo/__init__.py @@ -95,6 +95,7 @@ from pymongo.mongo_client import MongoClient from pymongo.mongo_replica_set_client import MongoReplicaSetClient from pymongo.replica_set_connection import ReplicaSetConnection from pymongo.read_preferences import ReadPreference +from pymongo.write_concern import WriteConcern def has_c(): """Is the C extension installed? diff --git a/pymongo/collection.py b/pymongo/collection.py index 104dd8e60..4a729e31c 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -24,7 +24,7 @@ from pymongo import (bulk, helpers, message) from pymongo.command_cursor import CommandCursor -from pymongo.cursor import Cursor, CursorType +from pymongo.cursor import Cursor from pymongo.errors import InvalidName, OperationFailure from pymongo.helpers import _check_write_command_response from pymongo.message import _INSERT, _UPDATE, _DELETE @@ -48,7 +48,8 @@ class Collection(common.BaseObject): """A Mongo collection. """ - def __init__(self, database, name, create=False, **kwargs): + def __init__(self, database, name, create=False, codec_options=None, + read_preference=None, write_concern=None, **kwargs): """Get / create a Mongo collection. Raises :class:`TypeError` if `name` is not an instance of @@ -69,9 +70,20 @@ class Collection(common.BaseObject): - `name`: the name of the collection to get - `create` (optional): if ``True``, force collection creation even without options being set + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) database.codec_options is used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) database.read_preference is used. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) database.write_concern is used. - `**kwargs` (optional): additional keyword arguments will be passed as options for the create collection command + .. versionchanged:: 2.9 + Added the codec_options, read_preference, and write_concern options. + .. versionchanged:: 2.2 Removed deprecated argument: options @@ -86,15 +98,18 @@ class Collection(common.BaseObject): .. mongodoc:: collections """ + opts, mode, tags, wc_doc = helpers._get_common_options( + database, codec_options, read_preference, write_concern) + salms = database.secondary_acceptable_latency_ms + super(Collection, self).__init__( + codec_options=opts, + read_preference=mode, + tag_sets=tags, + secondary_acceptable_latency_ms=salms, slave_okay=database.slave_okay, - read_preference=database.read_preference, - tag_sets=database.tag_sets, - secondary_acceptable_latency_ms=( - database.secondary_acceptable_latency_ms), safe=database.safe, - codec_options=database.codec_options, - **database.write_concern) + **wc_doc) if not isinstance(name, basestring): raise TypeError("name must be an instance " @@ -189,6 +204,43 @@ class Collection(common.BaseObject): """ return self.__database + def with_options( + self, codec_options=None, read_preference=None, write_concern=None): + """Get a clone of this collection changing the specified settings. + + >>> from pymongo import ReadPreference + >>> coll1.read_preference == ReadPreference.PRIMARY + True + >>> coll2 = coll1.with_options(read_preference=ReadPreference.SECONDARY) + >>> coll1.read_preference == ReadPreference.PRIMARY + True + >>> coll2.read_preference == ReadPreference.SECONDARY + True + + :Parameters: + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Collection` + is used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Collection` is used. See :mod:`~pymongo.read_preferences` + for options. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Collection` + is used. + + .. versionadded:: 2.9 + """ + opts, mode, tags, wc_doc = helpers._get_common_options( + self, codec_options, read_preference, write_concern) + coll = Collection(self.__database, self.__name, False, opts) + coll.write_concern = wc_doc + coll.read_preference = mode + coll.tag_sets = tags + return coll + def initialize_unordered_bulk_op(self): """Initialize an unordered batch of write operations. @@ -1537,7 +1589,7 @@ class Collection(common.BaseObject): use_master = not self.slave_okay and not self.read_preference return self.__database.command("group", group, - uuid_subtype=self.uuid_subtype, + codec_options=self.codec_options, read_preference=self.read_preference, tag_sets=self.tag_sets, secondary_acceptable_latency_ms=( @@ -1650,7 +1702,7 @@ class Collection(common.BaseObject): must_use_master = True response = self.__database.command("mapreduce", self.__name, - uuid_subtype=self.uuid_subtype, + codec_options=self.codec_options, map=map, reduce=reduce, read_preference=self.read_preference, tag_sets=self.tag_sets, @@ -1706,7 +1758,7 @@ class Collection(common.BaseObject): use_master = not self.slave_okay and not self.read_preference res = self.__database.command("mapreduce", self.__name, - uuid_subtype=self.uuid_subtype, + codec_options=self.codec_options, read_preference=self.read_preference, tag_sets=self.tag_sets, secondary_acceptable_latency_ms=( @@ -1814,7 +1866,7 @@ class Collection(common.BaseObject): out = self.__database.command("findAndModify", self.__name, allowable_errors=[no_obj_error], read_preference=ReadPreference.PRIMARY, - uuid_subtype=self.uuid_subtype, + codec_options=self.codec_options, **kwargs) if not out['ok']: diff --git a/pymongo/common.py b/pymongo/common.py index bff0d7540..a29ce1dac 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -416,7 +416,10 @@ class BaseObject(object): def __init__(self, **options): - self._codec_options = None + self._codec_options = options.get('codec_options') + if not isinstance(self._codec_options, CodecOptions): + raise TypeError("codec_options must be an instance of " + "bson.codec_options.CodecOptions") self.__slave_okay = False self.__read_pref = ReadPreference.PRIMARY self.__tag_sets = [{}] @@ -465,8 +468,6 @@ class BaseObject(object): self.__read_pref = validate_read_preference(option, value) elif option in ('tag_sets', 'readpreferencetags'): self.__tag_sets = validate_tag_sets(option, value) - elif option == 'codec_options': - self._codec_options = value elif option in ('secondaryacceptablelatencyms', 'secondary_acceptable_latency_ms'): self.__secondary_acceptable_latency_ms = ( diff --git a/pymongo/cursor.py b/pymongo/cursor.py index 97a1a5967..bc03b750a 100644 --- a/pymongo/cursor.py +++ b/pymongo/cursor.py @@ -18,6 +18,7 @@ from collections import deque from bson import RE_TYPE from bson.code import Code +from bson.codec_options import CodecOptions as _CodecOptions from bson.son import SON from pymongo import helpers, message, read_preferences from pymongo.read_preferences import ReadPreference, secondary_ok_commands @@ -104,7 +105,7 @@ class Cursor(object): read_preference=ReadPreference.PRIMARY, tag_sets=[{}], secondary_acceptable_latency_ms=None, exhaust=False, compile_re=True, oplog_replay=False, - modifiers=None, _must_use_master=False, _uuid_subtype=None, + modifiers=None, _must_use_master=False, _codec_options=None, **kwargs): """Create a new cursor. @@ -178,9 +179,6 @@ class Cursor(object): if not isinstance(fields, dict): fields = helpers._fields_list_to_dict(fields) - if as_class is None: - as_class = collection.database.connection.document_class - self.__collection = collection self.__spec = spec self.__fields = fields @@ -216,16 +214,19 @@ class Cursor(object): self.__explain = False self.__hint = None self.__comment = None - self.__as_class = as_class self.__slave_okay = slave_okay self.__manipulate = manipulate self.__read_preference = read_preference self.__tag_sets = tag_sets self.__secondary_acceptable_latency_ms = secondary_acceptable_latency_ms - self.__tz_aware = collection.database.connection.tz_aware self.__compile_re = compile_re self.__must_use_master = _must_use_master - self.__uuid_subtype = _uuid_subtype or collection.uuid_subtype + + copts = _codec_options or collection.codec_options + if as_class is not None: + copts = _CodecOptions( + as_class, copts.tz_aware, copts.uuid_representation) + self.__codec_options = copts self.__data = deque() self.__connection_id = None @@ -311,10 +312,10 @@ class Cursor(object): values_to_clone = ("spec", "fields", "skip", "limit", "max_time_ms", "comment", "max", "min", "snapshot", "ordering", "explain", "hint", - "batch_size", "max_scan", "as_class", "slave_okay", + "batch_size", "max_scan", "slave_okay", "manipulate", "read_preference", "tag_sets", "secondary_acceptable_latency_ms", - "must_use_master", "uuid_subtype", "compile_re", + "must_use_master", "codec_options", "compile_re", "query_flags", "modifiers", "kwargs") data = dict((k, v) for k, v in self.__dict__.iteritems() if k.startswith('_Cursor__') and k[9:] in values_to_clone) @@ -825,7 +826,7 @@ class Cursor(object): database = self.__collection.database r = database.command("count", self.__collection.name, allowable_errors=["ns missing"], - uuid_subtype=self.__uuid_subtype, + codec_options=self.__codec_options, compile_re=self.__compile_re, **command) if r.get("errmsg", "") == "ns missing": @@ -878,7 +879,7 @@ class Cursor(object): database = self.__collection.database return database.command("distinct", self.__collection.name, - uuid_subtype=self.__uuid_subtype, + codec_options=self.__codec_options, compile_re=self.__compile_re, **options)["values"] @@ -1013,11 +1014,13 @@ class Cursor(object): raise try: - response = helpers._unpack_response(response, self.__id, - self.__as_class, - self.__tz_aware, - self.__uuid_subtype, - self.__compile_re) + response = helpers._unpack_response( + response, + self.__id, + self.__codec_options.document_class, + self.__codec_options.tz_aware, + self.__codec_options.uuid_representation, + self.__compile_re) except OperationFailure: self.__killed = True # Make sure exhaust socket is returned immediately, if necessary. @@ -1075,7 +1078,7 @@ class Cursor(object): self.__collection.full_name, self.__skip, ntoreturn, self.__query_spec(), self.__fields, - self.__uuid_subtype)) + self.__codec_options.uuid_representation)) if not self.__id: self.__killed = True elif self.__id: # Get More diff --git a/pymongo/database.py b/pymongo/database.py index 6750814b6..e99761dc5 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -18,6 +18,7 @@ import warnings from bson.binary import OLD_UUID_SUBTYPE from bson.code import Code +from bson.codec_options import CodecOptions as _CodecOptions from bson.dbref import DBRef from bson.son import SON from pymongo import auth, common, helpers @@ -28,7 +29,8 @@ from pymongo.errors import (CollectionInvalid, OperationFailure) from pymongo.read_preferences import (modes, secondary_ok_commands, - ReadPreference) + ReadPreference, + _ServerMode) from pymongo.son_manipulator import SONManipulator @@ -36,7 +38,8 @@ class Database(common.BaseObject): """A Mongo database. """ - def __init__(self, connection, name): + def __init__(self, connection, name, codec_options=None, + read_preference=None, write_concern=None): """Get a database by connection and name. Raises :class:`TypeError` if `name` is not an instance of @@ -47,18 +50,32 @@ class Database(common.BaseObject): :Parameters: - `connection`: a client instance - `name`: database name + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) connection.codec_options is used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) connection.read_preference is used. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) connection.write_concern is used. .. mongodoc:: databases + + .. versionchanged:: 2.9 + Added the codec_options, read_preference, and write_concern options. """ - super(Database, - self).__init__(slave_okay=connection.slave_okay, - read_preference=connection.read_preference, - tag_sets=connection.tag_sets, - secondary_acceptable_latency_ms=( - connection.secondary_acceptable_latency_ms), - safe=connection.safe, - codec_options=connection.codec_options, - **connection.write_concern) + opts, mode, tags, wc_doc = helpers._get_common_options( + connection, codec_options, read_preference, write_concern) + salms = connection.secondary_acceptable_latency_ms + + super(Database, self).__init__( + codec_options=opts, + read_preference=mode, + tag_sets=tags, + secondary_acceptable_latency_ms=salms, + slave_okay=connection.slave_okay, + safe=connection.safe, + **wc_doc) if not isinstance(name, basestring): raise TypeError("name must be an instance " @@ -209,7 +226,48 @@ class Database(common.BaseObject): """ return self.__getattr__(name) - def create_collection(self, name, **kwargs): + def get_collection(self, name, codec_options=None, + read_preference=None, write_concern=None): + """Get a :class:`~pymongo.collection.Collection` with the given name + and options. + + Useful for creating a :class:`~pymongo.collection.Collection` with + different codec options, read preference, and/or write concern from + this :class:`Database`. + + >>> from pymongo import ReadPreference + >>> db.read_preference == ReadPreference.PRIMARY + True + >>> coll1 = db.test + >>> coll1.read_preference == ReadPreference.PRIMARY + True + >>> coll2 = db.get_collection( + ... 'test', read_preference=ReadPreference.SECONDARY) + >>> coll2.read_preference == SECONDARY + True + + :Parameters: + - `name`: The name of the collection - a string. + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Database` is + used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Database` is used. See :mod:`~pymongo.read_preferences` + for options. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Database` is + used. + + .. versionadded:: 2.9 + """ + return Collection( + self, name, False, codec_options, read_preference, write_concern) + + def create_collection(self, name, codec_options=None, + read_preference=None, write_concern=None, **kwargs): """Create a new :class:`~pymongo.collection.Collection` in this database. @@ -232,22 +290,34 @@ class Database(common.BaseObject): :Parameters: - `name`: the name of the collection to create + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`Database` is + used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`Database` is used. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`Database` is + used. - `**kwargs` (optional): additional keyword arguments will be passed as options for the create collection command + .. versionchanged:: 2.9 + Added the codec_options, read_preference, and write_concern options. + .. versionchanged:: 2.2 Removed deprecated argument: options .. versionchanged:: 1.5 deprecating `options` in favor of kwargs """ - opts = {"create": True} - opts.update(kwargs) - if name in self.collection_names(): raise CollectionInvalid("collection %s already exists" % name) - return Collection(self, name, **opts) + return Collection(self, name, True, codec_options, + read_preference, write_concern, **kwargs) def _apply_incoming_manipulators(self, son, collection): for manipulator in self.__incoming_manipulators: @@ -285,7 +355,8 @@ class Database(common.BaseObject): def _command(self, command, value=1, check=True, allowable_errors=None, - uuid_subtype=OLD_UUID_SUBTYPE, compile_re=True, **kwargs): + uuid_subtype=OLD_UUID_SUBTYPE, compile_re=True, + read_preference=None, codec_options=None, **kwargs): """Internal command helper. """ @@ -310,19 +381,34 @@ class Database(common.BaseObject): must_use_master = True break + if codec_options is None or 'as_class' in kwargs: + opts = {} + if 'as_class' in kwargs: + opts['document_class'] = kwargs.pop('as_class') + # 'as_class' must be in kwargs so don't use document_class + if codec_options: + opts['tz_aware'] = codec_options.tz_aware + opts['uuid_representation'] = codec_options.uuid_representation + else: + opts['uuid_representation'] = uuid_subtype + codec_options = _CodecOptions(**opts) + extra_opts = { - 'as_class': kwargs.pop('as_class', None), 'slave_okay': kwargs.pop('slave_okay', self.slave_okay), + '_codec_options': codec_options, '_must_use_master': must_use_master, - '_uuid_subtype': uuid_subtype } - extra_opts['read_preference'] = kwargs.pop( - 'read_preference', - self.read_preference) - extra_opts['tag_sets'] = kwargs.pop( - 'tag_sets', - self.tag_sets) + if isinstance(read_preference, _ServerMode): + extra_opts['read_preference'] = read_preference.mode + extra_opts['tag_sets'] = read_preference.tag_sets + else: + if read_preference is None: + read_preference = self.read_preference + extra_opts['read_preference'] = read_preference + extra_opts['tag_sets'] = kwargs.pop( + 'tag_sets', + self.tag_sets) extra_opts['secondary_acceptable_latency_ms'] = kwargs.pop( 'secondary_acceptable_latency_ms', self.secondary_acceptable_latency_ms) @@ -357,7 +443,8 @@ class Database(common.BaseObject): def command(self, command, value=1, check=True, allowable_errors=[], - uuid_subtype=OLD_UUID_SUBTYPE, compile_re=True, **kwargs): + uuid_subtype=OLD_UUID_SUBTYPE, compile_re=True, + read_preference=None, codec_options=None, **kwargs): """Issue a MongoDB command. Send command `command` to the database and return the @@ -445,7 +532,8 @@ class Database(common.BaseObject): .. _localThreshold: http://docs.mongodb.org/manual/reference/mongos/#cmdoption-mongos--localThreshold """ return self._command(command, value, check, allowable_errors, - uuid_subtype, compile_re, **kwargs)[0] + uuid_subtype, compile_re, read_preference, + codec_options, **kwargs)[0] def collection_names(self, include_system_collections=True): """Get a list of all the collection names in this database. diff --git a/pymongo/helpers.py b/pymongo/helpers.py index 1b3b9849d..e5df5ca50 100644 --- a/pymongo/helpers.py +++ b/pymongo/helpers.py @@ -32,6 +32,37 @@ from pymongo.errors import (AutoReconnect, OperationFailure, ExecutionTimeout, WTimeoutError) +from pymongo.read_preferences import _ServerMode +from pymongo.write_concern import WriteConcern as _WriteConcern + + +def _get_common_options(obj, codec_options, read_preference, write_concern): + """Get the codec options, read preference mode and tags, and write concern + necessary to create a new Database of Collection instance. + """ + if codec_options is None: + codec_options = obj.codec_options + + if read_preference is None: + rp_mode = obj.read_preference + rp_tags = obj.tag_sets + else: + if isinstance(read_preference, _ServerMode): + rp_mode = read_preference.mode + rp_tags = read_preference.tag_sets + else: + rp_mode = read_preference + rp_tags = [{}] + + if write_concern is None: + wc_document = obj.write_concern + else: + if not isinstance(write_concern, _WriteConcern): + raise TypeError("write_concern must be an instance of " + "pymongo.write_concern.WriteConcern") + wc_document = write_concern.document + + return codec_options, rp_mode, rp_tags, wc_document def _index_list(key_or_list, direction=None): diff --git a/pymongo/mongo_client.py b/pymongo/mongo_client.py index 84de75e67..573f03502 100644 --- a/pymongo/mongo_client.py +++ b/pymongo/mongo_client.py @@ -1512,6 +1512,46 @@ class MongoClient(common.BaseObject): return self[self.__default_database_name] + def get_database(self, name, codec_options=None, + read_preference=None, write_concern=None): + """Get a :class:`~pymongo.database.Database` with the given name and + options. + + Useful for creating a :class:`~pymongo.database.Database` with + different codec options, read preference, and/or write concern from + this :class:`MongoClient`. + + >>> from pymongo import ReadPreference + >>> client.read_preference == ReadPreference.PRIMARY + True + >>> db1 = client.test + >>> db1.read_preference == ReadPreference.PRIMARY + True + >>> db2 = client.get_database( + ... 'test', read_preference=ReadPreference.SECONDARY) + >>> db2.read_preference == ReadPreference.SECONDARY + True + + :Parameters: + - `name`: The name of the database - a string. + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`MongoClient` is + used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`MongoClient` is used. See :mod:`~pymongo.read_preferences` + for options. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`MongoClient` is + used. + + .. versionadded:: 2.9 + """ + return database.Database( + self, name, codec_options, read_preference, write_concern) + @property def is_locked(self): """Is this server locked? While locked, all write operations diff --git a/pymongo/mongo_replica_set_client.py b/pymongo/mongo_replica_set_client.py index 31a6a6a00..c2b43bbb1 100644 --- a/pymongo/mongo_replica_set_client.py +++ b/pymongo/mongo_replica_set_client.py @@ -1977,3 +1977,43 @@ class MongoReplicaSetClient(common.BaseObject): raise ConfigurationError('No default database defined') return self[self.__default_database_name] + + def get_database(self, name, codec_options=None, + read_preference=None, write_concern=None): + """Get a :class:`~pymongo.database.Database` with the given name and + options. + + Useful for creating a :class:`~pymongo.database.Database` with + different codec options, read preference, and/or write concern from + this :class:`MongoClient`. + + >>> from pymongo import ReadPreference + >>> client.read_preference == ReadPreference.PRIMARY + True + >>> db1 = client.test + >>> db1.read_preference == ReadPreference.PRIMARY + True + >>> db2 = client.get_database( + ... 'test', read_preference=ReadPreference.SECONDARY) + >>> db2.read_preference == ReadPreference.SECONDARY + True + + :Parameters: + - `name`: The name of the database - a string. + - `codec_options` (optional): An instance of + :class:`~bson.codec_options.CodecOptions`. If ``None`` (the + default) the :attr:`codec_options` of this :class:`MongoClient` is + used. + - `read_preference` (optional): The read preference to use. If + ``None`` (the default) the :attr:`read_preference` of this + :class:`MongoClient` is used. See :mod:`~pymongo.read_preferences` + for options. + - `write_concern` (optional): An instance of + :class:`~pymongo.write_concern.WriteConcern`. If ``None`` (the + default) the :attr:`write_concern` of this :class:`MongoClient` is + used. + + .. versionadded:: 2.9 + """ + return database.Database( + self, name, codec_options, read_preference, write_concern) diff --git a/test/test_client.py b/test/test_client.py index 69089059e..6f05d3532 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -29,6 +29,8 @@ sys.path[0:0] = [""] from nose.plugins.skip import SkipTest +from bson.binary import JAVA_LEGACY, PYTHON_LEGACY +from bson.codec_options import CodecOptions from bson.son import SON from bson.tz_util import utc from pymongo.mongo_client import MongoClient @@ -40,7 +42,8 @@ from pymongo.errors import (AutoReconnect, ConnectionFailure, InvalidName, OperationFailure) -from pymongo.read_preferences import ReadPreference +from pymongo.read_preferences import ReadPreference, Secondary +from pymongo.write_concern import WriteConcern from test import version, host, port, pair, skip_restricted_localhost from test.pymongo_mocks import MockClient from test.utils import (assertRaisesExactly, @@ -226,6 +229,28 @@ class TestClient(unittest.TestCase, TestRequestMixin): self.assertEqual(client.test, client["test"]) self.assertEqual(client.test, Database(client, "test")) + def test_get_database(self): + client = MongoClient(host, port, _connect=False) + codec_options = CodecOptions( + tz_aware=True, uuid_representation=JAVA_LEGACY) + write_concern = WriteConcern(w=2, j=True) + db = client.get_database( + 'foo', codec_options, ReadPreference.SECONDARY, write_concern) + self.assertEqual('foo', db.name) + self.assertEqual(codec_options, db.codec_options) + self.assertEqual(JAVA_LEGACY, db.uuid_subtype) + self.assertEqual(ReadPreference.SECONDARY, db.read_preference) + self.assertEqual([{}], db.tag_sets) + self.assertEqual(write_concern.document, db.write_concern) + + pref = Secondary([{"dc": "sf"}]) + db = client.get_database('foo', read_preference=pref) + self.assertEqual(pref.mode, db.read_preference) + self.assertEqual(pref.tag_sets, db.tag_sets) + self.assertEqual({}, db.write_concern) + self.assertEqual(CodecOptions(), db.codec_options) + self.assertEqual(PYTHON_LEGACY, db.uuid_subtype) + def test_database_names(self): client = MongoClient(host, port) @@ -526,6 +551,7 @@ class TestClient(unittest.TestCase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(), SON)) c.document_class = SON + db = c.pymongo_test self.assertEqual(SON, c.document_class) self.assertTrue(isinstance(db.test.find_one(), SON)) @@ -539,6 +565,7 @@ class TestClient(unittest.TestCase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(as_class=dict), SON)) c.document_class = dict + db = c.pymongo_test self.assertEqual(dict, c.document_class) self.assertTrue(isinstance(db.test.find_one(), dict)) diff --git a/test/test_collection.py b/test/test_collection.py index 2d875ce26..853f525b9 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -28,9 +28,10 @@ from nose.plugins.skip import SkipTest sys.path[0:0] = [""] -from bson.binary import Binary +from bson.binary import Binary, JAVA_LEGACY from bson.regex import Regex from bson.code import Code +from bson.codec_options import CodecOptions from bson.dbref import DBRef from bson.objectid import ObjectId from bson.py3compat import b @@ -40,9 +41,6 @@ from pymongo import (ASCENDING, DESCENDING, GEO2D, from pymongo import message as message_module from pymongo.collection import Collection from pymongo.command_cursor import CommandCursor -from pymongo.mongo_replica_set_client import MongoReplicaSetClient -from pymongo.read_preferences import ReadPreference -from pymongo.son_manipulator import SONManipulator from pymongo.errors import (DocumentTooLarge, DuplicateKeyError, InvalidDocument, @@ -50,6 +48,10 @@ from pymongo.errors import (DocumentTooLarge, InvalidOperation, OperationFailure, WTimeoutError) +from pymongo.mongo_replica_set_client import MongoReplicaSetClient +from pymongo.read_preferences import ReadPreference, Secondary +from pymongo.son_manipulator import SONManipulator +from pymongo.write_concern import WriteConcern from test.test_client import get_client from test.utils import (catch_warnings, enable_text_search, get_pool, is_mongos, joinall, oid_generated_on_client) @@ -109,6 +111,26 @@ class TestCollection(unittest.TestCase): # No exception self.db.drop_collection('test') + def test_with_options(self): + coll = self.db.test + codec_options = CodecOptions( + tz_aware=True, uuid_representation=JAVA_LEGACY) + write_concern = WriteConcern(w=2, j=True) + coll2 = coll.with_options( + codec_options, ReadPreference.SECONDARY, write_concern) + self.assertEqual(codec_options, coll2.codec_options) + self.assertEqual(JAVA_LEGACY, coll2.uuid_subtype) + self.assertEqual(ReadPreference.SECONDARY, coll2.read_preference) + self.assertEqual(write_concern.document, coll2.write_concern) + + pref = Secondary([{"dc": "sf"}]) + coll2 = coll.with_options(read_preference=pref) + self.assertEqual(pref.mode, coll2.read_preference) + self.assertEqual(pref.tag_sets, coll2.tag_sets) + self.assertEqual(coll.codec_options, coll2.codec_options) + self.assertEqual(coll.uuid_subtype, coll2.uuid_subtype) + self.assertEqual(coll.write_concern, coll2.write_concern) + def test_create_index(self): db = self.db diff --git a/test/test_cursor.py b/test/test_cursor.py index 874743130..f6447b554 100644 --- a/test/test_cursor.py +++ b/test/test_cursor.py @@ -686,8 +686,8 @@ class TestCursor(unittest.TestCase): 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__as_class), - type(cursor2._Cursor__as_class)) + self.assertEqual(type(cursor._Cursor__codec_options), + type(cursor2._Cursor__codec_options)) self.assertEqual(cursor._Cursor__slave_okay, cursor2._Cursor__slave_okay) self.assertEqual(cursor._Cursor__manipulate, diff --git a/test/test_database.py b/test/test_database.py index 8fd0c9c49..0839d260f 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -24,7 +24,9 @@ import unittest from nose.plugins.skip import SkipTest +from bson.binary import JAVA_LEGACY from bson.code import Code +from bson.codec_options import CodecOptions from bson.regex import Regex from bson.dbref import DBRef from bson.objectid import ObjectId @@ -33,18 +35,19 @@ from pymongo import (ALL, auth, OFF, SLOW_ONLY, - helpers, - ReadPreference) + helpers) from pymongo.collection import Collection from pymongo.database import Database from pymongo.errors import (CollectionInvalid, ExecutionTimeout, InvalidName, OperationFailure) +from pymongo.read_preferences import ReadPreference, Secondary from pymongo.son_manipulator import (AutoReference, NamespaceInjector, SONManipulator, ObjectIdShuffler) +from pymongo.write_concern import WriteConcern from test import version, skip_restricted_localhost from test.utils import (catch_warnings, get_command_line, is_mongos, server_started_with_auth) @@ -92,6 +95,27 @@ class TestDatabase(unittest.TestCase): self.assertNotEqual(db.test, Collection(db, "mike")) self.assertEqual(db.test.mike, db["test.mike"]) + def test_get_collection(self): + db = Database(self.client, "pymongo_test") + codec_options = CodecOptions( + tz_aware=True, uuid_representation=JAVA_LEGACY) + write_concern = WriteConcern(w=2, j=True) + coll = db.get_collection( + 'foo', codec_options, ReadPreference.SECONDARY, write_concern) + self.assertEqual('foo', coll.name) + self.assertEqual(codec_options, coll.codec_options) + self.assertEqual(JAVA_LEGACY, coll.uuid_subtype) + self.assertEqual(ReadPreference.SECONDARY, coll.read_preference) + self.assertEqual(write_concern.document, coll.write_concern) + + pref = Secondary([{"dc": "sf"}]) + coll = db.get_collection('foo', read_preference=pref) + self.assertEqual(pref.mode, coll.read_preference) + self.assertEqual(pref.tag_sets, coll.tag_sets) + self.assertEqual(db.codec_options, coll.codec_options) + self.assertEqual(db.uuid_subtype, coll.uuid_subtype) + self.assertEqual(db.write_concern, coll.write_concern) + def test_create_collection(self): db = Database(self.client, "pymongo_test") diff --git a/test/test_master_slave_connection.py b/test/test_master_slave_connection.py index e9fa41720..988145277 100644 --- a/test/test_master_slave_connection.py +++ b/test/test_master_slave_connection.py @@ -471,6 +471,7 @@ class TestMasterSlaveConnection(unittest.TestCase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(), SON)) c.document_class = SON + db = c.pymongo_test self.assertEqual(SON, c.document_class) self.assertTrue(isinstance(db.test.find_one(), SON)) @@ -484,6 +485,7 @@ class TestMasterSlaveConnection(unittest.TestCase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(as_class=dict), SON)) c.document_class = dict + db = c.pymongo_test self.assertEqual(dict, c.document_class) self.assertTrue(isinstance(db.test.find_one(), dict)) diff --git a/test/test_replica_set_client.py b/test/test_replica_set_client.py index a3f07b9e5..548b4ce25 100644 --- a/test/test_replica_set_client.py +++ b/test/test_replica_set_client.py @@ -565,6 +565,7 @@ class TestReplicaSetClient(TestReplicaSetClientBase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(), SON)) c.document_class = SON + db = c.pymongo_test self.assertEqual(SON, c.document_class) self.assertTrue(isinstance(db.test.find_one(), SON)) @@ -579,6 +580,7 @@ class TestReplicaSetClient(TestReplicaSetClientBase, TestRequestMixin): self.assertFalse(isinstance(db.test.find_one(as_class=dict), SON)) c.document_class = dict + db = c.pymongo_test self.assertEqual(dict, c.document_class) self.assertTrue(isinstance(db.test.find_one(), dict))