summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES12
-rwxr-xr-xredis/client.py21
-rwxr-xr-xredis/connection.py33
-rw-r--r--tests/conftest.py28
-rw-r--r--tests/test_commands.py36
-rw-r--r--tests/test_connection_pool.py4
6 files changed, 111 insertions, 23 deletions
diff --git a/CHANGES b/CHANGES
index 25af3a3..783f869 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,4 +1,16 @@
* 3.1.0 (in development)
+ * Connection URLs must have one of the following schemes:
+ redis://, rediss://, unix://. Thanks @jdupl123. #961/#969
+ * Fixed an issue with retry_on_timeout logic that caused some TimeoutErrors
+ to be retried. Thanks Aaron Yang. #1022/#1023
+ * Added support for SNI for SSL. Thanks @oridistor and Roey Prat. #1087
+ * Fixed ConnectionPool repr for pools with no connections. Thanks
+ Cody Scott. #1043/#995
+ * Fixed GEOHASH to return a None value when specifying a place that
+ doesn't exist on the server. Thanks @guybe7. #1126
+ * Fixed XREADGROUP to return an empty dictionary for messages that
+ have been deleted but still exist in the unacknowledged queue. Thanks
+ @xeizmendi. #1116
* Added an owned method to Lock objects. owned returns a boolean
indicating whether the current lock instance still owns the lock.
Thanks Dave Johansen. #1112
diff --git a/redis/client.py b/redis/client.py
index 2645e83..24120d2 100755
--- a/redis/client.py
+++ b/redis/client.py
@@ -191,6 +191,8 @@ def parse_sentinel_get_master(response):
def pairs_to_dict(response, decode_keys=False):
"Create a dict given a list of key/value pairs"
+ if response is None:
+ return {}
if decode_keys:
# the iter form is faster, but I don't know how to make that work
# with a nativestr() map
@@ -244,6 +246,12 @@ def int_or_none(response):
return int(response)
+def nativestr_or_none(response):
+ if response is None:
+ return None
+ return nativestr(response)
+
+
def parse_stream_list(response):
if response is None:
return None
@@ -508,7 +516,7 @@ class Redis(object):
'CONFIG RESETSTAT': bool_ok,
'CONFIG SET': bool_ok,
'DEBUG OBJECT': parse_debug_object,
- 'GEOHASH': lambda r: list(map(nativestr, r)),
+ 'GEOHASH': lambda r: list(map(nativestr_or_none, r)),
'GEOPOS': lambda r: list(map(lambda ll: (float(ll[0]),
float(ll[1]))
if ll is not None else None, r)),
@@ -767,7 +775,8 @@ class Redis(object):
return self.parse_response(connection, command_name, **options)
except (ConnectionError, TimeoutError) as e:
connection.disconnect()
- if not connection.retry_on_timeout and isinstance(e, TimeoutError):
+ if not (connection.retry_on_timeout and
+ isinstance(e, TimeoutError)):
raise
connection.send_command(*args)
return self.parse_response(connection, command_name, **options)
@@ -2199,7 +2208,7 @@ class Redis(object):
return self.execute_command('XREAD', *pieces)
def xreadgroup(self, groupname, consumername, streams, count=None,
- block=None):
+ block=None, noack=False):
"""
Read from a stream via a consumer group.
groupname: name of the consumer group.
@@ -2209,6 +2218,7 @@ class Redis(object):
count: if set, only return this many items, beginning with the
earliest available.
block: number of milliseconds to wait, if nothing already present.
+ noack: do not add messages to the PEL
"""
pieces = [Token.get_token('GROUP'), groupname, consumername]
if count is not None:
@@ -2222,6 +2232,8 @@ class Redis(object):
"integer")
pieces.append(Token.get_token("BLOCK"))
pieces.append(str(block))
+ if noack:
+ pieces.append(Token.get_token("NOACK"))
if not isinstance(streams, dict) or len(streams) == 0:
raise DataError('XREADGROUP streams must be a non empty dict')
pieces.append(Token.get_token('STREAMS'))
@@ -3001,7 +3013,8 @@ class PubSub(object):
return command(*args)
except (ConnectionError, TimeoutError) as e:
connection.disconnect()
- if not connection.retry_on_timeout and isinstance(e, TimeoutError):
+ if not (connection.retry_on_timeout and
+ isinstance(e, TimeoutError)):
raise
# Connect manually here. If the Redis server is down, this will
# fail and raise a ConnectionError as desired.
diff --git a/redis/connection.py b/redis/connection.py
index 7575b76..c81e4c1 100755
--- a/redis/connection.py
+++ b/redis/connection.py
@@ -727,11 +727,24 @@ class SSLConnection(Connection):
def _connect(self):
"Wrap the socket with SSL support"
sock = super(SSLConnection, self)._connect()
- sock = ssl.wrap_socket(sock,
- cert_reqs=self.cert_reqs,
- keyfile=self.keyfile,
- certfile=self.certfile,
- ca_certs=self.ca_certs)
+ if hasattr(ssl, "create_default_context"):
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = self.cert_reqs
+ if self.certfile and self.keyfile:
+ context.load_cert_chain(certfile=self.certfile,
+ keyfile=self.keyfile)
+ if self.ca_certs:
+ context.load_verify_locations(self.ca_certs)
+ sock = context.wrap_socket(sock, server_hostname=self.host)
+ else:
+ # In case this code runs in a version which is older than 2.7.9,
+ # we want to fall back to old code
+ sock = ssl.wrap_socket(sock,
+ cert_reqs=self.cert_reqs,
+ keyfile=self.keyfile,
+ certfile=self.certfile,
+ ca_certs=self.ca_certs)
return sock
@@ -870,7 +883,7 @@ class ConnectionPool(object):
path = url.path
hostname = url.hostname
- # We only support redis:// and unix:// schemes.
+ # We only support redis://, rediss:// and unix:// schemes.
if url.scheme == 'unix':
url_options.update({
'password': password,
@@ -878,7 +891,7 @@ class ConnectionPool(object):
'connection_class': UnixDomainSocketConnection,
})
- else:
+ elif url.scheme in ('redis', 'rediss'):
url_options.update({
'host': hostname,
'port': int(url.port or 6379),
@@ -895,6 +908,10 @@ class ConnectionPool(object):
if url.scheme == 'rediss':
url_options['connection_class'] = SSLConnection
+ else:
+ valid_schemes = ', '.join(('redis://', 'rediss://', 'unix://'))
+ raise ValueError('Redis URL must specify one of the following'
+ 'schemes (%s)' % valid_schemes)
# last shot at the db value
url_options['db'] = int(url_options.get('db', db or 0))
@@ -939,7 +956,7 @@ class ConnectionPool(object):
def __repr__(self):
return "%s<%s>" % (
type(self).__name__,
- self.connection_class.description_format % self.connection_kwargs,
+ repr(self.connection_class(**self.connection_kwargs)),
)
def reset(self):
diff --git a/tests/conftest.py b/tests/conftest.py
index 6e046f2..bb4c9a9 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,18 +5,18 @@ from mock import Mock
from distutils.version import StrictVersion
-_REDIS_VERSIONS = {}
+_REDIS_INFO = {}
-def get_version(**kwargs):
+def get_info(**kwargs):
params = {'host': 'localhost', 'port': 6379, 'db': 9}
params.update(kwargs)
key = '%s:%s' % (params['host'], params['port'])
- if key not in _REDIS_VERSIONS:
+ if key not in _REDIS_INFO:
client = redis.Redis(**params)
- _REDIS_VERSIONS[key] = client.info()['redis_version']
+ _REDIS_INFO[key] = client.info()
client.connection_pool.disconnect()
- return _REDIS_VERSIONS[key]
+ return _REDIS_INFO[key]
def _get_client(cls, request=None, **kwargs):
@@ -26,22 +26,34 @@ def _get_client(cls, request=None, **kwargs):
client.flushdb()
if request:
def teardown():
- client.flushdb()
+ try:
+ client.flushdb()
+ except redis.ConnectionError:
+ # handle cases where a test disconnected a client
+ # just manually retry the flushdb
+ client.flushdb()
client.connection_pool.disconnect()
request.addfinalizer(teardown)
return client
def skip_if_server_version_lt(min_version):
- check = StrictVersion(get_version()) < StrictVersion(min_version)
+ redis_version = get_info()['redis_version']
+ check = StrictVersion(redis_version) < StrictVersion(min_version)
return pytest.mark.skipif(check, reason="")
def skip_if_server_version_gte(min_version):
- check = StrictVersion(get_version()) >= StrictVersion(min_version)
+ redis_version = get_info()['redis_version']
+ check = StrictVersion(redis_version) >= StrictVersion(min_version)
return pytest.mark.skipif(check, reason="")
+def skip_unless_arch_bits(arch_bits):
+ return pytest.mark.skipif(get_info()['arch_bits'] != arch_bits,
+ reason="server is not {}-bit".format(arch_bits))
+
+
@pytest.fixture()
def r(request, **kwargs):
return _get_client(redis.Redis, request, **kwargs)
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 566437d..33f78d5 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -11,7 +11,8 @@ from redis._compat import (unichr, ascii_letters, iteritems, iterkeys,
from redis.client import parse_info
from redis import exceptions
-from .conftest import skip_if_server_version_lt, skip_if_server_version_gte
+from .conftest import (skip_if_server_version_lt, skip_if_server_version_gte,
+ skip_unless_arch_bits)
@pytest.fixture()
@@ -1686,9 +1687,10 @@ class TestRedisCommands(object):
(2.1873744593677, 41.406342043777, 'place2')
r.geoadd('barcelona', *values)
- assert r.geohash('barcelona', 'place1', 'place2') ==\
- ['sp3e9yg3kd0', 'sp3e9cbc3t0']
+ assert r.geohash('barcelona', 'place1', 'place2', 'place3') ==\
+ ['sp3e9yg3kd0', 'sp3e9cbc3t0', None]
+ @skip_unless_arch_bits(64)
@skip_if_server_version_lt('3.2.0')
def test_geopos(self, r):
values = (2.1909389952632, 41.433791470673, 'place1') +\
@@ -1735,6 +1737,7 @@ class TestRedisCommands(object):
assert r.georadius('barcelona', 2.191, 41.433, 1, unit='km') ==\
[b'place1']
+ @skip_unless_arch_bits(64)
@skip_if_server_version_lt('3.2.0')
def test_georadius_with(self, r):
values = (2.1909389952632, 41.433791470673, 'place1') +\
@@ -1792,6 +1795,7 @@ class TestRedisCommands(object):
r.georadius('barcelona', 2.191, 41.433, 1000, store='places_barcelona')
assert r.zrange('places_barcelona', 0, -1) == [b'place1']
+ @skip_unless_arch_bits(64)
@skip_if_server_version_lt('3.2.0')
def test_georadius_store_dist(self, r):
values = (2.1909389952632, 41.433791470673, 'place1') +\
@@ -1803,6 +1807,7 @@ class TestRedisCommands(object):
# instead of save the geo score, the distance is saved.
assert r.zscore('places_barcelona', 'place1') == 88.05060698409301
+ @skip_unless_arch_bits(64)
@skip_if_server_version_lt('3.2.0')
def test_georadiusmember(self, r):
values = (2.1909389952632, 41.433791470673, 'place1') +\
@@ -2205,6 +2210,31 @@ class TestRedisCommands(object):
# xread starting after the last message returns an empty message list
assert r.xreadgroup(group, consumer, streams={stream: '>'}) == expected
+ # xreadgroup with noack does not have any items in the PEL
+ r.xgroup_destroy(stream, group)
+ r.xgroup_create(stream, group, '0')
+ assert len(r.xreadgroup(group, consumer, streams={stream: '>'},
+ noack=True)[0][1]) == 2
+ # now there should be nothing pending
+ assert len(r.xreadgroup(group, consumer,
+ streams={stream: '0'})[0][1]) == 0
+
+ r.xgroup_destroy(stream, group)
+ r.xgroup_create(stream, group, '0')
+ # delete all the messages in the stream
+ expected = [
+ [
+ stream.encode(),
+ [
+ (m1, {}),
+ (m2, {}),
+ ]
+ ]
+ ]
+ r.xreadgroup(group, consumer, streams={stream: '>'})
+ r.xtrim(stream, 0)
+ assert r.xreadgroup(group, consumer, streams={stream: '0'}) == expected
+
@skip_if_server_version_lt('5.0.0')
def test_xrevrange(self, r):
stream = 'stream'
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
index b0dec67..ca56a76 100644
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -308,6 +308,10 @@ class TestConnectionPoolURLParsing(object):
'password': None,
}
+ def test_invalid_scheme_raises_error(self):
+ with pytest.raises(ValueError):
+ redis.ConnectionPool.from_url('localhost')
+
class TestConnectionPoolUnixSocketURLParsing(object):
def test_defaults(self):