Skip to content

Commit bee60a6

Browse files
committed
Merge branch 'pr/441'
Conflicts: redis/client.py tests/test_commands.py
2 parents 82a76b8 + 8cd9f23 commit bee60a6

File tree

4 files changed

+145
-12
lines changed

4 files changed

+145
-12
lines changed

CHANGES

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
Pepijn de Vos and Vincent Ohprecio.
88
* Updated TTL and PTTL commands with Redis 2.8+ semantics. Thanks Markus
99
Kaiserswerth.
10+
* Added extra *SCAN commands that return iterators instead of the normal
11+
[cursor, data] type. Use scan_iter, hscan_iter, sscan_iter, and
12+
zscan_iter for iterators. Thanks Mathieu Longtin.
1013
* 2.9.1
1114
* IPv6 support. Thanks https://github.com/amashinchi
1215
* 2.9.0

README.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ to the official command syntax. There are a few exceptions:
6868
`this comment on issue #151
6969
<https://github.com/andymccurdy/redis-py/issues/151#issuecomment-1545015>`_
7070
for details).
71+
* **SCAN/SSCAN/HSCAN/ZSCAN**: The *SCAN commands are implemented as they
72+
exist in the Redis documentation. In addition, each command has an equivilant
73+
iterator method. These are purely for convenience so the user doesn't have
74+
to keep track of the cursor while iterating. Use the
75+
scan_iter/sscan_iter/hscan_iter/zscan_iter methods for this behavior.
7176
7277
In addition to the changes above, the Redis class, a subclass of StrictRedis,
7378
overrides several other commands to provide backwards compatibility with older
@@ -635,6 +640,24 @@ master.
635640
See `Guidelines for Redis clients with support for Redis Sentinel
636641
<http://redis.io/topics/sentinel-clients>`_ to learn more about Redis Sentinel.
637642

643+
Scan Iterators
644+
^^^^^^^^^^^^^^
645+
646+
The *SCAN commands introduced in Redis 2.8 can be cumbersome to use. While
647+
these commands are fully supported, redis-py also exposes the following methods
648+
that return Python iterators for convenience: `scan_iter`, `hscan_iter`,
649+
`sscan_iter` and `zscan_iter`.
650+
651+
.. code-block:: pycon
652+
653+
>>> for key, value in (('A', '1'), ('B', '2'), ('C', '3')):
654+
... r.set(key, value)
655+
>>> for key in r.scan_iter():
656+
... print key, r.get(key)
657+
A 1
658+
B 2
659+
C 3
660+
638661
Author
639662
^^^^^^
640663

redis/client.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -244,18 +244,20 @@ def parse_script(response, **options):
244244

245245

246246
def parse_scan(response, **options):
247-
return response
247+
cursor, r = response
248+
return nativestr(cursor), r
248249

249250

250251
def parse_hscan(response, **options):
251252
cursor, r = response
252-
return cursor, r and pairs_to_dict(r) or {}
253+
return nativestr(cursor), r and pairs_to_dict(r) or {}
253254

254255

255256
def parse_zscan(response, **options):
256257
score_cast_func = options.get('score_cast_func', float)
257-
it = iter(response[1])
258-
return [response[0], list(izip(it, imap(score_cast_func, it)))]
258+
cursor, r = response
259+
it = iter(r)
260+
return nativestr(cursor), list(izip(it, imap(score_cast_func, it)))
259261

260262

261263
class StrictRedis(object):
@@ -1182,7 +1184,8 @@ def sort(self, name, start=None, num=None, by=None, get=None,
11821184
# SCAN COMMANDS
11831185
def scan(self, cursor=0, match=None, count=None):
11841186
"""
1185-
Scan and return (nextcursor, keys)
1187+
Incrementally return lists of key names. Also return a cursor
1188+
indicating the scan position.
11861189
11871190
``match`` allows for filtering the keys by pattern
11881191
@@ -1195,9 +1198,25 @@ def scan(self, cursor=0, match=None, count=None):
11951198
pieces.extend(['COUNT', count])
11961199
return self.execute_command('SCAN', *pieces)
11971200

1201+
def scan_iter(self, match=None, count=None):
1202+
"""
1203+
Make an iterator using the SCAN command so that the client doesn't
1204+
need to remember the cursor position.
1205+
1206+
``match`` allows for filtering the keys by pattern
1207+
1208+
``count`` allows for hint the minimum number of returns
1209+
"""
1210+
cursor = 0
1211+
while cursor != '0':
1212+
cursor, data = self.scan(cursor=cursor, match=match, count=count)
1213+
for item in data:
1214+
yield item
1215+
11981216
def sscan(self, name, cursor=0, match=None, count=None):
11991217
"""
1200-
Scan and return (nextcursor, members_of_set)
1218+
Incrementally return lists of elements in a set. Also return a cursor
1219+
indicating the scan position.
12011220
12021221
``match`` allows for filtering the keys by pattern
12031222
@@ -1210,9 +1229,26 @@ def sscan(self, name, cursor=0, match=None, count=None):
12101229
pieces.extend(['COUNT', count])
12111230
return self.execute_command('SSCAN', *pieces)
12121231

1232+
def sscan_iter(self, name, match=None, count=None):
1233+
"""
1234+
Make an iterator using the SSCAN command so that the client doesn't
1235+
need to remember the cursor position.
1236+
1237+
``match`` allows for filtering the keys by pattern
1238+
1239+
``count`` allows for hint the minimum number of returns
1240+
"""
1241+
cursor = 0
1242+
while cursor != '0':
1243+
cursor, data = self.sscan(name, cursor=cursor,
1244+
match=match, count=count)
1245+
for item in data:
1246+
yield item
1247+
12131248
def hscan(self, name, cursor=0, match=None, count=None):
12141249
"""
1215-
Scan and return (nextcursor, dict)
1250+
Incrementally return key/value slices in a hash. Also return a cursor
1251+
indicating the scan position.
12161252
12171253
``match`` allows for filtering the keys by pattern
12181254
@@ -1225,10 +1261,27 @@ def hscan(self, name, cursor=0, match=None, count=None):
12251261
pieces.extend(['COUNT', count])
12261262
return self.execute_command('HSCAN', *pieces)
12271263

1264+
def hscan_iter(self, name, match=None, count=None):
1265+
"""
1266+
Make an iterator using the HSCAN command so that the client doesn't
1267+
need to remember the cursor position.
1268+
1269+
``match`` allows for filtering the keys by pattern
1270+
1271+
``count`` allows for hint the minimum number of returns
1272+
"""
1273+
cursor = 0
1274+
while cursor != '0':
1275+
cursor, data = self.hscan(name, cursor=cursor,
1276+
match=match, count=count)
1277+
for item in data.items():
1278+
yield item
1279+
12281280
def zscan(self, name, cursor=0, match=None, count=None,
12291281
score_cast_func=float):
12301282
"""
1231-
Scan and return (nextcursor, pairs)
1283+
Incrementally return lists of elements in a sorted set. Also return a
1284+
cursor indicating the scan position.
12321285
12331286
``match`` allows for filtering the keys by pattern
12341287
@@ -1244,6 +1297,26 @@ def zscan(self, name, cursor=0, match=None, count=None,
12441297
options = {'score_cast_func': score_cast_func}
12451298
return self.execute_command('ZSCAN', *pieces, **options)
12461299

1300+
def zscan_iter(self, name, match=None, count=None,
1301+
score_cast_func=float):
1302+
"""
1303+
Make an iterator using the ZSCAN command so that the client doesn't
1304+
need to remember the cursor position.
1305+
1306+
``match`` allows for filtering the keys by pattern
1307+
1308+
``count`` allows for hint the minimum number of returns
1309+
1310+
``score_cast_func`` a callable used to cast the score return value
1311+
"""
1312+
cursor = 0
1313+
while cursor != '0':
1314+
cursor, data = self.zscan(name, cursor=cursor, match=match,
1315+
count=count,
1316+
score_cast_func=score_cast_func)
1317+
for item in data:
1318+
yield item
1319+
12471320
# SET COMMANDS
12481321
def sadd(self, name, *values):
12491322
"Add ``value(s)`` to set ``name``"

tests/test_commands.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -633,38 +633,72 @@ def test_scan(self, r):
633633
r.set('b', 2)
634634
r.set('c', 3)
635635
cursor, keys = r.scan()
636-
assert cursor == b('0')
636+
assert cursor == '0'
637637
assert set(keys) == set([b('a'), b('b'), b('c')])
638638
_, keys = r.scan(match='a')
639639
assert set(keys) == set([b('a')])
640640

641+
@skip_if_server_version_lt('2.8.0')
642+
def test_scan_iter(self, r):
643+
r.set('a', 1)
644+
r.set('b', 2)
645+
r.set('c', 3)
646+
keys = list(r.scan_iter())
647+
assert set(keys) == set([b('a'), b('b'), b('c')])
648+
keys = list(r.scan_iter(match='a'))
649+
assert set(keys) == set([b('a')])
650+
641651
@skip_if_server_version_lt('2.8.0')
642652
def test_sscan(self, r):
643653
r.sadd('a', 1, 2, 3)
644654
cursor, members = r.sscan('a')
645-
assert cursor == b('0')
655+
assert cursor == '0'
646656
assert set(members) == set([b('1'), b('2'), b('3')])
647657
_, members = r.sscan('a', match=b('1'))
648658
assert set(members) == set([b('1')])
649659

660+
@skip_if_server_version_lt('2.8.0')
661+
def test_sscan_iter(self, r):
662+
r.sadd('a', 1, 2, 3)
663+
members = list(r.sscan_iter('a'))
664+
assert set(members) == set([b('1'), b('2'), b('3')])
665+
members = list(r.sscan_iter('a', match=b('1')))
666+
assert set(members) == set([b('1')])
667+
650668
@skip_if_server_version_lt('2.8.0')
651669
def test_hscan(self, r):
652670
r.hmset('a', {'a': 1, 'b': 2, 'c': 3})
653671
cursor, dic = r.hscan('a')
654-
assert cursor == b('0')
672+
assert cursor == '0'
655673
assert dic == {b('a'): b('1'), b('b'): b('2'), b('c'): b('3')}
656674
_, dic = r.hscan('a', match='a')
657675
assert dic == {b('a'): b('1')}
658676

677+
@skip_if_server_version_lt('2.8.0')
678+
def test_hscan_iter(self, r):
679+
r.hmset('a', {'a': 1, 'b': 2, 'c': 3})
680+
dic = dict(r.hscan_iter('a'))
681+
assert dic == {b('a'): b('1'), b('b'): b('2'), b('c'): b('3')}
682+
dic = dict(r.hscan_iter('a', match='a'))
683+
assert dic == {b('a'): b('1')}
684+
659685
@skip_if_server_version_lt('2.8.0')
660686
def test_zscan(self, r):
661687
r.zadd('a', 'a', 1, 'b', 2, 'c', 3)
662688
cursor, pairs = r.zscan('a')
663-
assert cursor == b('0')
689+
assert cursor == '0'
664690
assert set(pairs) == set([(b('a'), 1), (b('b'), 2), (b('c'), 3)])
665691
_, pairs = r.zscan('a', match='a')
666692
assert set(pairs) == set([(b('a'), 1)])
667693

694+
@skip_if_server_version_lt('2.8.0')
695+
def test_zscan_iter(self, r):
696+
r.zadd('a', 'a', 1, 'b', 2, 'c', 3)
697+
pairs = list(r.zscan_iter('a'))
698+
assert set(pairs) == set([(b('a'), 1), (b('b'), 2), (b('c'), 3)])
699+
pairs = list(r.zscan_iter('a', match='a'))
700+
assert set(pairs) == set([(b('a'), 1)])
701+
668702
# SET COMMANDS
669703
def test_sadd(self, r):
670704
members = set([b('1'), b('2'), b('3')])

0 commit comments

Comments
 (0)