diff --git a/doc/api/pymongo/collection.rst b/doc/api/pymongo/collection.rst index da87f7447..46dae1093 100644 --- a/doc/api/pymongo/collection.rst +++ b/doc/api/pymongo/collection.rst @@ -23,12 +23,10 @@ .. autoattribute:: read_preference .. autoattribute:: tag_sets .. autoattribute:: secondary_acceptable_latency_ms + .. autoattribute:: write_concern .. autoattribute:: slave_okay .. autoattribute:: safe .. autoattribute:: uuid_subtype - .. automethod:: get_lasterror_options - .. automethod:: set_lasterror_options - .. automethod:: unset_lasterror_options .. automethod:: insert(doc_or_docs[, manipulate=True[, safe=False[, check_keys=True[, continue_on_error=False[, **kwargs]]]]]) .. automethod:: save(to_save[, manipulate=True[, safe=False[, check_keys=True[, **kwargs]]]]) .. automethod:: update(spec, document[, upsert=False[, manipulate=False[, safe=False[, multi=False[, check_keys=True[, **kwargs]]]]]]) @@ -51,4 +49,7 @@ .. automethod:: map_reduce .. automethod:: inline_map_reduce .. automethod:: find_and_modify + .. automethod:: get_lasterror_options + .. automethod:: set_lasterror_options + .. automethod:: unset_lasterror_options diff --git a/doc/api/pymongo/connection.rst b/doc/api/pymongo/connection.rst index 41c22a0da..fbcd20e28 100644 --- a/doc/api/pymongo/connection.rst +++ b/doc/api/pymongo/connection.rst @@ -28,12 +28,10 @@ .. autoattribute:: read_preference .. autoattribute:: tag_sets .. autoattribute:: secondary_acceptable_latency_ms + .. autoattribute:: write_concern .. autoattribute:: slave_okay .. autoattribute:: safe .. autoattribute:: is_locked - .. automethod:: get_lasterror_options - .. automethod:: set_lasterror_options - .. automethod:: unset_lasterror_options .. automethod:: database_names .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) @@ -45,3 +43,6 @@ .. automethod:: set_cursor_manager .. automethod:: fsync .. automethod:: unlock + .. automethod:: get_lasterror_options + .. automethod:: set_lasterror_options + .. automethod:: unset_lasterror_options diff --git a/doc/api/pymongo/database.rst b/doc/api/pymongo/database.rst index 6cf0afef1..5f79725d4 100644 --- a/doc/api/pymongo/database.rst +++ b/doc/api/pymongo/database.rst @@ -21,6 +21,7 @@ .. autoattribute:: read_preference .. autoattribute:: tag_sets .. autoattribute:: secondary_acceptable_latency_ms + .. autoattribute:: write_concern .. autoattribute:: slave_okay .. autoattribute:: safe .. automethod:: get_lasterror_options diff --git a/doc/api/pymongo/replica_set_connection.rst b/doc/api/pymongo/replica_set_connection.rst index 1251cbd2a..8d67b28a4 100644 --- a/doc/api/pymongo/replica_set_connection.rst +++ b/doc/api/pymongo/replica_set_connection.rst @@ -29,11 +29,12 @@ .. autoattribute:: read_preference .. autoattribute:: tag_sets .. autoattribute:: secondary_acceptable_latency_ms + .. autoattribute:: write_concern .. autoattribute:: safe - .. automethod:: get_lasterror_options - .. automethod:: set_lasterror_options - .. automethod:: unset_lasterror_options .. automethod:: database_names .. automethod:: drop_database .. automethod:: copy_database(from_name, to_name[, from_host=None[, username=None[, password=None]]]) .. automethod:: close_cursor + .. automethod:: get_lasterror_options + .. automethod:: set_lasterror_options + .. automethod:: unset_lasterror_options diff --git a/pymongo/collection.py b/pymongo/collection.py index 97c5afeff..e9956922a 100644 --- a/pymongo/collection.py +++ b/pymongo/collection.py @@ -88,7 +88,7 @@ class Collection(common.BaseObject): secondary_acceptable_latency_ms=( database.secondary_acceptable_latency_ms), safe=database.safe, - **(database.get_lasterror_options())) + **database.write_concern) if not isinstance(name, basestring): raise TypeError("name must be an instance " diff --git a/pymongo/common.py b/pymongo/common.py index 122d4857c..405466413 100644 --- a/pymongo/common.py +++ b/pymongo/common.py @@ -182,6 +182,22 @@ SAFE_OPTIONS = frozenset([ ]) +class WriteConcern(dict): + + def __init__(self, *args, **kwargs): + """A subclass of dict that overrides __setitem__ to + validate write concern options. + """ + super(WriteConcern, self).__init__(*args, **kwargs) + + def __setitem__(self, key, value): + if key not in SAFE_OPTIONS: + raise ConfigurationError("%s is not a valid write " + "concern option." % (key,)) + key, value = validate(key, value) + super(WriteConcern, self).__setitem__(key, value) + + class BaseObject(object): """A base class that provides attributes and methods common to multiple pymongo classes. @@ -196,7 +212,7 @@ class BaseObject(object): self.__tag_sets = [{}] self.__secondary_acceptable_latency_ms = 15 self.__safe = None - self.__safe_opts = {} + self.__write_concern = WriteConcern() self.__set_options(options) if (self.__read_pref == ReadPreference.PRIMARY and self.__tag_sets != [{}] @@ -212,16 +228,14 @@ class BaseObject(object): "but write concerns have been set making safe True. " "Please set safe to True.", UserWarning) - def __set_safe_option(self, option, value, check=False): + def __set_safe_option(self, option, value): """Validates and sets getlasterror options for this object (Connection, Database, Collection, etc.) """ if value is None: - self.__safe_opts.pop(option, None) + self.__write_concern.pop(option, None) else: - if check: - option, value = validate(option, value) - self.__safe_opts[option] = value + self.__write_concern[option] = value self.__safe = True def __set_options(self, options): @@ -247,6 +261,47 @@ class BaseObject(object): else: self.__set_safe_option(option, value) + def __set_write_concern(self, value): + """Property setter for write_concern.""" + if not isinstance(value, dict): + raise ConfigurationError("write_concern must be an " + "instance of dict or a subclass.") + # Make a copy here to avoid users accidentally setting the + # same dict on multiple instances. + wc = WriteConcern() + for k, v in value.iteritems(): + # Make sure we validate each option. + wc[k] = v + self.__write_concern = wc + + def __get_write_concern(self): + """The write concern for this instance. + + Supports dict style access for getting/setting write concern + options. Valid options include w=, wtimeout=, + j=, fsync=. + + >>> c.write_concern + {} + >>> c.write_concern = {'w': 2, 'wtimeout': 1000} + >>> c.write_concern + {'wtimeout': 1000, 'w': 2} + >>> c.write_concern['j'] = True + >>> c.write_concern + {'wtimeout': 1000, 'j': True, 'w': 2} + >>> c.write_concern = {'j': True} + >>> c.write_concern + {'j': True} + + .. note:: Accessing :attr:`write_concern` returns its value + (a subclass of :class:`dict`), not a copy. + """ + # To support dict style access we have to return the actual + # WriteConcern here, not a copy. + return self.__write_concern + + write_concern = property(__get_write_concern, __set_write_concern) + def __get_slave_okay(self): """DEPRECATED. Use `read_preference` instead. @@ -334,30 +389,43 @@ class BaseObject(object): safe = property(__get_safe, __set_safe) def get_lasterror_options(self): - """Returns a dict of the getlasterror options set - on this instance. + """DEPRECATED: Use :attr:`write_concern` instead. + Returns a dict of the getlasterror options set on this instance. + + .. versionchanged:: 2.3+ + Deprecated get_lasterror_options. .. versionadded:: 2.0 """ - return self.__safe_opts.copy() + warnings.warn("get_lasterror_options is deprecated. Please use " + "write_concern instead.", DeprecationWarning) + return self.__write_concern.copy() def set_lasterror_options(self, **kwargs): - """Set getlasterror options for this instance. + """DEPRECATED: Use :attr:`write_concern` instead. - Valid options include j=, w=, wtimeout=, + Set getlasterror options for this instance. + + Valid options include j=, w=, wtimeout=, and fsync=. Implies safe=True. :Parameters: - `**kwargs`: Options should be passed as keyword arguments (e.g. w=2, fsync=True) + .. versionchanged:: 2.3+ + Deprecated set_lasterror_options. .. versionadded:: 2.0 """ + warnings.warn("set_lasterror_options is deprecated. Please use " + "write_concern instead.", DeprecationWarning) for key, value in kwargs.iteritems(): - self.__set_safe_option(key, value, check=True) + self.__set_safe_option(key, value) def unset_lasterror_options(self, *options): - """Unset getlasterror options for this instance. + """DEPRECATED: Use :attr:`write_concern` instead. + + Unset getlasterror options for this instance. If no options are passed unsets all getlasterror options. This does not set `safe` to False. @@ -365,13 +433,17 @@ class BaseObject(object): :Parameters: - `*options`: The list of options to unset. + .. versionchanged:: 2.3+ + Deprecated unset_lasterror_options. .. versionadded:: 2.0 """ + warnings.warn("unset_lasterror_options is deprecated. Please use " + "write_concern instead.", DeprecationWarning) if len(options): for option in options: - self.__safe_opts.pop(option, None) + self.__write_concern.pop(option, None) else: - self.__safe_opts = {} + self.__write_concern = WriteConcern() def _get_safe_and_lasterror_options(self, safe=None, **options): """Get the current safe mode and any getLastError options. @@ -392,5 +464,5 @@ class BaseObject(object): if safe or options: safe = True if not options: - options.update(self.get_lasterror_options()) + options.update(self.__write_concern) return safe, options diff --git a/pymongo/database.py b/pymongo/database.py index 45def9a4f..7dc9db8dd 100644 --- a/pymongo/database.py +++ b/pymongo/database.py @@ -67,7 +67,7 @@ class Database(common.BaseObject): secondary_acceptable_latency_ms=( connection.secondary_acceptable_latency_ms), safe=connection.safe, - **(connection.get_lasterror_options())) + **connection.write_concern) if not isinstance(name, basestring): raise TypeError("name must be an instance " diff --git a/test/test_common.py b/test/test_common.py index c66f70d31..305ee3db1 100644 --- a/test/test_common.py +++ b/test/test_common.py @@ -15,9 +15,13 @@ """Test the pymongo common module.""" import os +import sys import unittest import warnings +sys.path[0:0] = [""] + +from bson.son import SON from pymongo.connection import Connection from pymongo.errors import ConfigurationError, OperationFailure from test.utils import drop_collections @@ -106,14 +110,17 @@ class TestCommon(unittest.TestCase): self.assertTrue(c.safe) d = {'w': 1, 'wtimeout': 300, 'fsync': True, 'j': True} self.assertEqual(d, c.get_lasterror_options()) + self.assertEqual(d, c.write_concern) db = c.test self.assertTrue(db.slave_okay) self.assertTrue(db.safe) self.assertEqual(d, db.get_lasterror_options()) + self.assertEqual(d, db.write_concern) coll = db.test self.assertTrue(coll.slave_okay) self.assertTrue(coll.safe) self.assertEqual(d, coll.get_lasterror_options()) + self.assertEqual(d, coll.write_concern) cursor = coll.find() self.assertTrue(cursor._Cursor__slave_okay) cursor = coll.find(slave_okay=False) @@ -127,14 +134,17 @@ class TestCommon(unittest.TestCase): c.slave_okay = False self.assertFalse(c.slave_okay) self.assertEqual({}, c.get_lasterror_options()) + self.assertEqual({}, c.write_concern) db = c.test self.assertFalse(db.slave_okay) self.assertFalse(db.safe) self.assertEqual({}, db.get_lasterror_options()) + self.assertEqual({}, db.write_concern) coll = db.test self.assertFalse(coll.slave_okay) self.assertFalse(coll.safe) self.assertEqual({}, coll.get_lasterror_options()) + self.assertEqual({}, coll.write_concern) cursor = coll.find() self.assertFalse(cursor._Cursor__slave_okay) cursor = coll.find(slave_okay=True) @@ -142,15 +152,21 @@ class TestCommon(unittest.TestCase): coll.set_lasterror_options(j=True) self.assertEqual({'j': True}, coll.get_lasterror_options()) + self.assertEqual({'j': True}, coll.write_concern) self.assertEqual({}, db.get_lasterror_options()) + self.assertEqual({}, db.write_concern) self.assertFalse(db.safe) self.assertEqual({}, c.get_lasterror_options()) + self.assertEqual({}, c.write_concern) self.assertFalse(c.safe) db.set_lasterror_options(w='majority') self.assertEqual({'j': True}, coll.get_lasterror_options()) + self.assertEqual({'j': True}, coll.write_concern) self.assertEqual({'w': 'majority'}, db.get_lasterror_options()) + self.assertEqual({'w': 'majority'}, db.write_concern) self.assertEqual({}, c.get_lasterror_options()) + self.assertEqual({}, c.write_concern) self.assertFalse(c.safe) db.slave_okay = True self.assertTrue(db.slave_okay) @@ -179,6 +195,51 @@ class TestCommon(unittest.TestCase): warnings.resetwarnings() + def test_write_concern(self): + c = Connection(pair) + + self.assertEqual({}, c.write_concern) + wc = {'w': 2, 'wtimeout': 1000} + c.write_concern = wc + self.assertEqual(wc, c.write_concern) + wc = {'w': 3, 'wtimeout': 1000} + c.write_concern['w'] = 3 + self.assertEqual(wc, c.write_concern) + wc = {'w': 3} + del c.write_concern['wtimeout'] + self.assertEqual(wc, c.write_concern) + + wc = {'w': 3, 'wtimeout': 1000} + c = Connection(w=3, wtimeout=1000) + self.assertEqual(wc, c.write_concern) + wc = {'w': 2, 'wtimeout': 1000} + c.write_concern = wc + self.assertEqual(wc, c.write_concern) + + db = c.test + self.assertEqual(wc, db.write_concern) + coll = db.test + self.assertEqual(wc, coll.write_concern) + coll.write_concern = {'j': True} + self.assertEqual({'j': True}, coll.write_concern) + self.assertEqual(wc, db.write_concern) + + wc = SON([('w', 2)]) + coll.write_concern = wc + self.assertEqual(wc.to_dict(), coll.write_concern) + + def f(): + c.write_concern = {'foo': 'bar'} + self.assertRaises(ConfigurationError, f) + + def f(): + c.write_concern['foo'] = 'bar' + self.assertRaises(ConfigurationError, f) + + def f(): + c.write_concern = [('foo', 'bar')] + self.assertRaises(ConfigurationError, f) + if __name__ == "__main__": unittest.main()