Skip to content

Commit 16551e8

Browse files
committed
Validate sort param for find_and_modify PYTHON-416
- find_and_modify now accepts a list of (key, direction) pairs - passing SON or OrderedDict is still accepted but deprecated - passing dict of len 1 is still accepted but deprecated - passing dict of len > 1 raises TypeError
1 parent ae3a534 commit 16551e8

File tree

2 files changed

+80
-3
lines changed

2 files changed

+80
-3
lines changed

pymongo/collection.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
from pymongo.errors import ConfigurationError, InvalidName
2727

2828

29+
try:
30+
from collections import OrderedDict
31+
ordered_types = (SON, OrderedDict)
32+
except ImportError:
33+
ordered_types = SON
34+
35+
2936
def _gen_index_name(keys):
3037
"""Generate an index name from the set of fields it is over.
3138
"""
@@ -1228,7 +1235,8 @@ def inline_map_reduce(self, map, reduce, full_response=False, **kwargs):
12281235
else:
12291236
return res.get("results")
12301237

1231-
def find_and_modify(self, query={}, update=None, upsert=False, **kwargs):
1238+
def find_and_modify(self, query={}, update=None,
1239+
upsert=False, sort=None, **kwargs):
12321240
"""Update and return an object.
12331241
12341242
This is a thin wrapper around the findAndModify_ command. The
@@ -1243,13 +1251,15 @@ def find_and_modify(self, query={}, update=None, upsert=False, **kwargs):
12431251
12441252
:Parameters:
12451253
- `query`: filter for the update (default ``{}``)
1246-
- `sort`: priority if multiple objects match (default ``{}``)
12471254
- `update`: see second argument to :meth:`update` (no default)
1255+
- `upsert`: insert if object doesn't exist (default ``False``)
1256+
- `sort`: a list of (key, direction) pairs specifying the sort
1257+
order for this query. See :meth:`~pymongo.cursor.Cursor.sort`
1258+
for details.
12481259
- `remove`: remove rather than updating (default ``False``)
12491260
- `new`: return updated rather than original object
12501261
(default ``False``)
12511262
- `fields`: see second argument to :meth:`find` (default all)
1252-
- `upsert`: insert if object doesn't exist (default ``False``)
12531263
- `**kwargs`: any other options the findAndModify_ command
12541264
supports can be passed here.
12551265
@@ -1260,6 +1270,9 @@ def find_and_modify(self, query={}, update=None, upsert=False, **kwargs):
12601270
12611271
.. note:: Requires server version **>= 1.3.0**
12621272
1273+
.. versionchanged:: 2.3+
1274+
Deprecated the use of mapping types for the sort parameter
1275+
12631276
.. versionadded:: 1.10
12641277
"""
12651278
if (not update and not kwargs.get('remove', None)):
@@ -1275,6 +1288,22 @@ def find_and_modify(self, query={}, update=None, upsert=False, **kwargs):
12751288
kwargs['update'] = update
12761289
if upsert:
12771290
kwargs['upsert'] = upsert
1291+
if sort:
1292+
# Accept a list of tuples to match Cursor's sort parameter.
1293+
if isinstance(sort, list):
1294+
kwargs['sort'] = helpers._index_document(sort)
1295+
# Accept OrderedDict, SON, and dict with len == 1 so we
1296+
# don't break existing code already using find_and_modify.
1297+
elif (isinstance(sort, ordered_types) or
1298+
isinstance(sort, dict) and len(sort) == 1):
1299+
warnings.warn("Passing mapping types for `sort` is deprecated,"
1300+
" use a list of (key, direction) pairs instead",
1301+
DeprecationWarning)
1302+
kwargs['sort'] = sort
1303+
else:
1304+
raise TypeError("sort must be a list of (key, direction) "
1305+
"pairs, a dict of len 1, or an instance of "
1306+
"SON or OrderedDict")
12781307

12791308
no_obj_error = "No matching object found"
12801309

test/test_collection.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,54 @@ class ExtendedDict(dict):
17091709
as_class=ExtendedDict)
17101710
self.assertTrue(isinstance(result, ExtendedDict))
17111711

1712+
def test_find_and_modify_with_sort(self):
1713+
c = self.db.test
1714+
c.drop()
1715+
for j in xrange(5):
1716+
c.insert({'j': j, 'i': 0}, safe=True)
1717+
1718+
sort={'j': DESCENDING}
1719+
self.assertEqual(4, c.find_and_modify({},
1720+
{'$inc': {'i': 1}},
1721+
sort=sort)['j'])
1722+
sort={'j': ASCENDING}
1723+
self.assertEqual(0, c.find_and_modify({},
1724+
{'$inc': {'i': 1}},
1725+
sort=sort)['j'])
1726+
sort=[('j', DESCENDING)]
1727+
self.assertEqual(4, c.find_and_modify({},
1728+
{'$inc': {'i': 1}},
1729+
sort=sort)['j'])
1730+
sort=[('j', ASCENDING)]
1731+
self.assertEqual(0, c.find_and_modify({},
1732+
{'$inc': {'i': 1}},
1733+
sort=sort)['j'])
1734+
sort=SON([('j', DESCENDING)])
1735+
self.assertEqual(4, c.find_and_modify({},
1736+
{'$inc': {'i': 1}},
1737+
sort=sort)['j'])
1738+
sort=SON([('j', ASCENDING)])
1739+
self.assertEqual(0, c.find_and_modify({},
1740+
{'$inc': {'i': 1}},
1741+
sort=sort)['j'])
1742+
try:
1743+
from collections import OrderedDict
1744+
sort=OrderedDict([('j', DESCENDING)])
1745+
self.assertEqual(4, c.find_and_modify({},
1746+
{'$inc': {'i': 1}},
1747+
sort=sort)['j'])
1748+
sort=OrderedDict([('j', ASCENDING)])
1749+
self.assertEqual(0, c.find_and_modify({},
1750+
{'$inc': {'i': 1}},
1751+
sort=sort)['j'])
1752+
except ImportError:
1753+
pass
1754+
# Test that a standard dict with two keys is rejected.
1755+
sort={'j': DESCENDING, 'foo': DESCENDING}
1756+
self.assertRaises(TypeError, c.find_and_modify, {},
1757+
{'$inc': {'i': 1}},
1758+
sort=sort)
1759+
17121760
def test_find_with_nested(self):
17131761
if not version.at_least(self.db.connection, (2, 0, 0)):
17141762
raise SkipTest("nested $and and $or requires MongoDB >= 2.0")

0 commit comments

Comments
 (0)