Skip to content

Commit

Permalink
Merge branch 'pr/441'
Browse files Browse the repository at this point in the history
Conflicts:
	redis/client.py
	tests/test_commands.py
  • Loading branch information
andymccurdy committed Apr 8, 2014
2 parents 82a76b8 + 8cd9f23 commit bee60a6
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
Pepijn de Vos and Vincent Ohprecio.
* Updated TTL and PTTL commands with Redis 2.8+ semantics. Thanks Markus
Kaiserswerth.
* Added extra *SCAN commands that return iterators instead of the normal
[cursor, data] type. Use scan_iter, hscan_iter, sscan_iter, and
zscan_iter for iterators. Thanks Mathieu Longtin.
* 2.9.1
* IPv6 support. Thanks https://github.com/amashinchi
* 2.9.0
Expand Down
23 changes: 23 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ to the official command syntax. There are a few exceptions:
`this comment on issue #151
<https://github.com/andymccurdy/redis-py/issues/151#issuecomment-1545015>`_
for details).
* **SCAN/SSCAN/HSCAN/ZSCAN**: The *SCAN commands are implemented as they
exist in the Redis documentation. In addition, each command has an equivilant
iterator method. These are purely for convenience so the user doesn't have
to keep track of the cursor while iterating. Use the
scan_iter/sscan_iter/hscan_iter/zscan_iter methods for this behavior.
In addition to the changes above, the Redis class, a subclass of StrictRedis,
overrides several other commands to provide backwards compatibility with older
Expand Down Expand Up @@ -635,6 +640,24 @@ master.
See `Guidelines for Redis clients with support for Redis Sentinel
<http://redis.io/topics/sentinel-clients>`_ to learn more about Redis Sentinel.

Scan Iterators
^^^^^^^^^^^^^^

The *SCAN commands introduced in Redis 2.8 can be cumbersome to use. While
these commands are fully supported, redis-py also exposes the following methods
that return Python iterators for convenience: `scan_iter`, `hscan_iter`,
`sscan_iter` and `zscan_iter`.
.. code-block:: pycon
>>> for key, value in (('A', '1'), ('B', '2'), ('C', '3')):
... r.set(key, value)
>>> for key in r.scan_iter():
... print key, r.get(key)
A 1
B 2
C 3
Author
^^^^^^

Expand Down
89 changes: 81 additions & 8 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,20 @@ def parse_script(response, **options):


def parse_scan(response, **options):
return response
cursor, r = response
return nativestr(cursor), r


def parse_hscan(response, **options):
cursor, r = response
return cursor, r and pairs_to_dict(r) or {}
return nativestr(cursor), r and pairs_to_dict(r) or {}


def parse_zscan(response, **options):
score_cast_func = options.get('score_cast_func', float)
it = iter(response[1])
return [response[0], list(izip(it, imap(score_cast_func, it)))]
cursor, r = response
it = iter(r)
return nativestr(cursor), list(izip(it, imap(score_cast_func, it)))


class StrictRedis(object):
Expand Down Expand Up @@ -1182,7 +1184,8 @@ def sort(self, name, start=None, num=None, by=None, get=None,
# SCAN COMMANDS
def scan(self, cursor=0, match=None, count=None):
"""
Scan and return (nextcursor, keys)
Incrementally return lists of key names. Also return a cursor
indicating the scan position.
``match`` allows for filtering the keys by pattern
Expand All @@ -1195,9 +1198,25 @@ def scan(self, cursor=0, match=None, count=None):
pieces.extend(['COUNT', count])
return self.execute_command('SCAN', *pieces)

def scan_iter(self, match=None, count=None):
"""
Make an iterator using the SCAN command so that the client doesn't
need to remember the cursor position.
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
"""
cursor = 0
while cursor != '0':
cursor, data = self.scan(cursor=cursor, match=match, count=count)
for item in data:
yield item

def sscan(self, name, cursor=0, match=None, count=None):
"""
Scan and return (nextcursor, members_of_set)
Incrementally return lists of elements in a set. Also return a cursor
indicating the scan position.
``match`` allows for filtering the keys by pattern
Expand All @@ -1210,9 +1229,26 @@ def sscan(self, name, cursor=0, match=None, count=None):
pieces.extend(['COUNT', count])
return self.execute_command('SSCAN', *pieces)

def sscan_iter(self, name, match=None, count=None):
"""
Make an iterator using the SSCAN command so that the client doesn't
need to remember the cursor position.
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
"""
cursor = 0
while cursor != '0':
cursor, data = self.sscan(name, cursor=cursor,
match=match, count=count)
for item in data:
yield item

def hscan(self, name, cursor=0, match=None, count=None):
"""
Scan and return (nextcursor, dict)
Incrementally return key/value slices in a hash. Also return a cursor
indicating the scan position.
``match`` allows for filtering the keys by pattern
Expand All @@ -1225,10 +1261,27 @@ def hscan(self, name, cursor=0, match=None, count=None):
pieces.extend(['COUNT', count])
return self.execute_command('HSCAN', *pieces)

def hscan_iter(self, name, match=None, count=None):
"""
Make an iterator using the HSCAN command so that the client doesn't
need to remember the cursor position.
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
"""
cursor = 0
while cursor != '0':
cursor, data = self.hscan(name, cursor=cursor,
match=match, count=count)
for item in data.items():
yield item

def zscan(self, name, cursor=0, match=None, count=None,
score_cast_func=float):
"""
Scan and return (nextcursor, pairs)
Incrementally return lists of elements in a sorted set. Also return a
cursor indicating the scan position.
``match`` allows for filtering the keys by pattern
Expand All @@ -1244,6 +1297,26 @@ def zscan(self, name, cursor=0, match=None, count=None,
options = {'score_cast_func': score_cast_func}
return self.execute_command('ZSCAN', *pieces, **options)

def zscan_iter(self, name, match=None, count=None,
score_cast_func=float):
"""
Make an iterator using the ZSCAN command so that the client doesn't
need to remember the cursor position.
``match`` allows for filtering the keys by pattern
``count`` allows for hint the minimum number of returns
``score_cast_func`` a callable used to cast the score return value
"""
cursor = 0
while cursor != '0':
cursor, data = self.zscan(name, cursor=cursor, match=match,
count=count,
score_cast_func=score_cast_func)
for item in data:
yield item

# SET COMMANDS
def sadd(self, name, *values):
"Add ``value(s)`` to set ``name``"
Expand Down
42 changes: 38 additions & 4 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,38 +633,72 @@ def test_scan(self, r):
r.set('b', 2)
r.set('c', 3)
cursor, keys = r.scan()
assert cursor == b('0')
assert cursor == '0'
assert set(keys) == set([b('a'), b('b'), b('c')])
_, keys = r.scan(match='a')
assert set(keys) == set([b('a')])

@skip_if_server_version_lt('2.8.0')
def test_scan_iter(self, r):
r.set('a', 1)
r.set('b', 2)
r.set('c', 3)
keys = list(r.scan_iter())
assert set(keys) == set([b('a'), b('b'), b('c')])
keys = list(r.scan_iter(match='a'))
assert set(keys) == set([b('a')])

@skip_if_server_version_lt('2.8.0')
def test_sscan(self, r):
r.sadd('a', 1, 2, 3)
cursor, members = r.sscan('a')
assert cursor == b('0')
assert cursor == '0'
assert set(members) == set([b('1'), b('2'), b('3')])
_, members = r.sscan('a', match=b('1'))
assert set(members) == set([b('1')])

@skip_if_server_version_lt('2.8.0')
def test_sscan_iter(self, r):
r.sadd('a', 1, 2, 3)
members = list(r.sscan_iter('a'))
assert set(members) == set([b('1'), b('2'), b('3')])
members = list(r.sscan_iter('a', match=b('1')))
assert set(members) == set([b('1')])

@skip_if_server_version_lt('2.8.0')
def test_hscan(self, r):
r.hmset('a', {'a': 1, 'b': 2, 'c': 3})
cursor, dic = r.hscan('a')
assert cursor == b('0')
assert cursor == '0'
assert dic == {b('a'): b('1'), b('b'): b('2'), b('c'): b('3')}
_, dic = r.hscan('a', match='a')
assert dic == {b('a'): b('1')}

@skip_if_server_version_lt('2.8.0')
def test_hscan_iter(self, r):
r.hmset('a', {'a': 1, 'b': 2, 'c': 3})
dic = dict(r.hscan_iter('a'))
assert dic == {b('a'): b('1'), b('b'): b('2'), b('c'): b('3')}
dic = dict(r.hscan_iter('a', match='a'))
assert dic == {b('a'): b('1')}

@skip_if_server_version_lt('2.8.0')
def test_zscan(self, r):
r.zadd('a', 'a', 1, 'b', 2, 'c', 3)
cursor, pairs = r.zscan('a')
assert cursor == b('0')
assert cursor == '0'
assert set(pairs) == set([(b('a'), 1), (b('b'), 2), (b('c'), 3)])
_, pairs = r.zscan('a', match='a')
assert set(pairs) == set([(b('a'), 1)])

@skip_if_server_version_lt('2.8.0')
def test_zscan_iter(self, r):
r.zadd('a', 'a', 1, 'b', 2, 'c', 3)
pairs = list(r.zscan_iter('a'))
assert set(pairs) == set([(b('a'), 1), (b('b'), 2), (b('c'), 3)])
pairs = list(r.zscan_iter('a', match='a'))
assert set(pairs) == set([(b('a'), 1)])

# SET COMMANDS
def test_sadd(self, r):
members = set([b('1'), b('2'), b('3')])
Expand Down

0 comments on commit bee60a6

Please sign in to comment.