diff -Nru python-redis-2.10.3/CHANGES python-redis-2.10.5/CHANGES --- python-redis-2.10.3/CHANGES 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/CHANGES 2015-11-03 00:20:02.000000000 +0000 @@ -1,3 +1,31 @@ +* 2.10.5 + * Allow URL encoded parameters in Redis URLs. Characters like a "/" can + now be URL encoded and redis-py will correctly decode them. Thanks + Paul Keene. + * Added support for the WAIT command. Thanks https://github.com/eshizhan + * Better shutdown support for the PubSub Worker Thread. It now properly + cleans up the connection, unsubscribes from any channels and patterns + previously subscribed to and consumes any waiting messages on the socket. + * Added the ability to sleep for a brief period in the event of a + WatchError occuring. Thanks Joshua Harlow. + * Fixed a bug with pipeline error reporting when dealing with characters + in error messages that could not be encoded to the connection's + character set. Thanks Hendrik Muhs. + * Fixed a bug in Sentinel connections that would inadvertantly connect + to the master when the connection pool resets. Thanks + https://github.com/df3n5 + * Better timeout support in Pubsub get_message. Thanks Andy Isaacson. + * Fixed a bug with the HiredisParser that would cause the parser to + get stuck in an endless loop if a specific number of bytes were + delivered from the socket. This fix also increases performance of + parsing large responses from the Redis server. + * Added support for ZREVRANGEBYLEX. + * ConnectionErrors are now raised if Redis refuses a connection due to + the maxclients limit being exceeded. Thanks Roman Karpovich. + * max_connections can now be set when instantiating client instances. + Thanks Ohad Perry. +* 2.10.4 + (skipped due to a PyPI snafu) * 2.10.3 * Fixed a bug with the bytearray support introduced in 2.10.2. Thanks Josh Owen. diff -Nru python-redis-2.10.3/debian/changelog python-redis-2.10.5/debian/changelog --- python-redis-2.10.3/debian/changelog 2015-06-08 10:27:39.000000000 +0000 +++ python-redis-2.10.5/debian/changelog 2015-11-20 16:00:30.000000000 +0000 @@ -1,3 +1,70 @@ +python-redis (2.10.5-1ubuntu1) xenial; urgency=low + + * Merge from Debian unstable. Remaining changes: + - d/control: Drop python-hiredis down to a Suggests to avoid main + inclusion in Ubuntu. + + -- James Page Fri, 20 Nov 2015 16:00:27 +0000 + +python-redis (2.10.5-1) unstable; urgency=medium + + * New upstream release. + * wrap-and-sort -sa + * Drop 001-Fix-tests-under-Redis-3.x-we-can-be-of-an-embedded-s.patch; merged + upstream. + + -- Chris Lamb Wed, 04 Nov 2015 12:10:07 +0000 + +python-redis (2.10.3-8) unstable; urgency=medium + + * Add '@' to autopkgtest `Depends:` line so that the packages we wish to test + is actually installed. + + -- Chris Lamb Sat, 03 Oct 2015 17:31:11 +0200 + +python-redis (2.10.3-7) unstable; urgency=medium + + * Actually test against the installed (and patched) version by removing the + library from the local source tree. + + We remove the contents (ie. with "redis/*") instead of the entire directory + so that upstream's setup.py does not complain. + + -- Chris Lamb Tue, 15 Sep 2015 18:47:22 +0100 + +python-redis (2.10.3-6) unstable; urgency=medium + + * Replace `-B` with `Restrictions: allow-stderr` in autopkgtests. + + Failures were due to "no previously-included files found matching + '__pycache__'" being written to stderr, caused by MANIFEST.in attempting to + exclude these files which did not exist as it is a clean source checking. + + Adding '-B' in 2.10.3-5 therefore had no effect, and would have actually + prevented these files ever existing. + + It does not appear possible to silence this message, so the options are + either to: + + a) Ensure these files exist so the warning never occurs (!). + b) Remove the MANIFEST.in file. + c) Permit stderr output. This is presumably safe as pytest is fairly + robust. + + -- Chris Lamb Tue, 15 Sep 2015 16:58:11 +0100 + +python-redis (2.10.3-5) unstable; urgency=medium + + * Run tests with -B flag to try and avoid .pyc and __pycache__ issues. + + -- Chris Lamb Sat, 12 Sep 2015 23:58:49 +0100 + +python-redis (2.10.3-4) unstable; urgency=medium + + * Add patch to fix tests under Redis 3.x; discovered by debci. + + -- Chris Lamb Tue, 18 Aug 2015 13:31:15 +0200 + python-redis (2.10.3-3ubuntu1) wily; urgency=low * Merge from Debian unstable. Remaining changes: diff -Nru python-redis-2.10.3/debian/control python-redis-2.10.5/debian/control --- python-redis-2.10.3/debian/control 2015-06-08 10:25:13.000000000 +0000 +++ python-redis-2.10.5/debian/control 2015-11-20 15:56:34.000000000 +0000 @@ -3,12 +3,13 @@ Priority: optional Maintainer: Ubuntu Developers XSBC-Original-Maintainer: Chris Lamb -Build-Depends: debhelper (>= 9), - dh-python, - python-all (>= 2.6.6-3~), - python-setuptools, - python3-all, - python3-setuptools +Build-Depends: + debhelper (>= 9), + dh-python, + python-all (>= 2.6.6-3~), + python-setuptools, + python3-all, + python3-setuptools X-Python-Version: >= 2.6 X-Python3-Version: >= 3.2 Standards-Version: 3.9.6 @@ -18,8 +19,11 @@ Package: python-redis Architecture: all -Depends: ${misc:Depends}, ${python:Depends} -Suggests: python-hiredis +Depends: + ${misc:Depends}, + ${python:Depends} +Suggests: + python-hiredis Description: Persistent key-value database with network interface (Python library) Redis is a key-value database in a similar vein to memcache but the dataset is non-volatile. Redis additionally provides native support for atomically @@ -31,7 +35,9 @@ Package: python3-redis Architecture: all -Depends: ${misc:Depends}, ${python3:Depends} +Depends: + ${misc:Depends}, + ${python3:Depends} Description: Persistent key-value database with network interface (Python 3 library) Redis is a key-value database in a similar vein to memcache but the dataset is non-volatile. Redis additionally provides native support for atomically diff -Nru python-redis-2.10.3/debian/tests/control python-redis-2.10.5/debian/tests/control --- python-redis-2.10.3/debian/tests/control 2015-05-17 23:44:54.000000000 +0000 +++ python-redis-2.10.5/debian/tests/control 2015-11-04 16:49:17.000000000 +0000 @@ -1,5 +1,7 @@ -Depends: redis-server, python-pytest, python-setuptools -Test-Command: python setup.py test +Depends: @, redis-server, python-pytest, python-setuptools +Restrictions: allow-stderr +Test-Command: rm -rf redis/*; python setup.py test -Depends: redis-server, python3-pytest, python3-setuptools -Test-Command: python3 setup.py test +Depends: @, redis-server, python3-pytest, python3-setuptools +Restrictions: allow-stderr +Test-Command: rm -rf redis/*; python3 setup.py test diff -Nru python-redis-2.10.3/README.rst python-redis-2.10.5/README.rst --- python-redis-2.10.3/README.rst 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/README.rst 2015-11-03 00:20:02.000000000 +0000 @@ -68,7 +68,7 @@ `this comment on issue #151 `_ for details). -* **SCAN/SSCAN/HSCAN/ZSCAN**: The *SCAN commands are implemented as they +* **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 @@ -126,7 +126,7 @@ you want to control the socket behavior within an async framework. To instantiate a client class using your own connection, you need to create a connection pool, passing your class to the connection_class argument. -Other keyword parameters your pass to the pool will be passed to the class +Other keyword parameters you pass to the pool will be passed to the class specified during initialization. .. code-block:: pycon @@ -613,7 +613,7 @@ >>> sentinel.discover_slaves('mymaster') [('127.0.0.1', 6380)] -You can also create Redis client connections from a Sentinel instnace. You can +You can also create Redis client connections from a Sentinel instance. You can connect to either the master (for write operations) or a slave (for read-only operations). @@ -643,7 +643,7 @@ Scan Iterators ^^^^^^^^^^^^^^ -The *SCAN commands introduced in Redis 2.8 can be cumbersome to use. While +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`. diff -Nru python-redis-2.10.3/redis/client.py python-redis-2.10.5/redis/client.py --- python-redis-2.10.3/redis/client.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/redis/client.py 2015-11-03 00:20:02.000000000 +0000 @@ -3,10 +3,12 @@ import datetime import sys import warnings +import time import threading import time as mod_time from redis._compat import (b, basestring, bytes, imap, iteritems, iterkeys, - itervalues, izip, long, nativestr, unicode) + itervalues, izip, long, nativestr, unicode, + safe_unicode) from redis.connection import (ConnectionPool, UnixDomainSocketConnection, SSLConnection, Token) from redis.lock import Lock, LuaLock @@ -57,7 +59,8 @@ def dict_merge(*dicts): merged = {} - [merged.update(d) for d in dicts] + for d in dicts: + merged.update(d) return merged @@ -397,7 +400,8 @@ charset=None, errors=None, decode_responses=False, retry_on_timeout=False, ssl=False, ssl_keyfile=None, ssl_certfile=None, - ssl_cert_reqs=None, ssl_ca_certs=None): + ssl_cert_reqs=None, ssl_ca_certs=None, + max_connections=None): if not connection_pool: if charset is not None: warnings.warn(DeprecationWarning( @@ -415,7 +419,8 @@ 'encoding': encoding, 'encoding_errors': encoding_errors, 'decode_responses': decode_responses, - 'retry_on_timeout': retry_on_timeout + 'retry_on_timeout': retry_on_timeout, + 'max_connections': max_connections } # based on input, setup appropriate connection args if unix_socket_path is not None: @@ -476,6 +481,7 @@ """ shard_hint = kwargs.pop('shard_hint', None) value_from_callable = kwargs.pop('value_from_callable', False) + watch_delay = kwargs.pop('watch_delay', None) with self.pipeline(True, shard_hint) as pipe: while 1: try: @@ -485,6 +491,8 @@ exec_value = pipe.execute() return func_value if value_from_callable else exec_value except WatchError: + if watch_delay is not None and watch_delay > 0: + time.sleep(watch_delay) continue def lock(self, name, timeout=None, sleep=0.1, blocking_timeout=None, @@ -762,6 +770,15 @@ """ return self.execute_command('TIME') + def wait(self, num_replicas, timeout): + """ + Redis synchronous replication + That returns the number of replicas that processed the query when + we finally have at least ``num_replicas``, or when the ``timeout`` was + reached. + """ + return self.execute_command('WAIT', num_replicas, timeout) + # BASIC KEY COMMANDS def append(self, key, value): """ @@ -1646,6 +1663,22 @@ pieces.extend([Token('LIMIT'), start, num]) return self.execute_command(*pieces) + def zrevrangebylex(self, name, max, min, start=None, num=None): + """ + Return the reversed lexicographical range of values from sorted set + ``name`` between ``max`` and ``min``. + + If ``start`` and ``num`` are specified, then return a slice of the + range. + """ + if (start is not None and num is None) or \ + (num is not None and start is None): + raise RedisError("``start`` and ``num`` must both be specified") + pieces = ['ZREVRANGEBYLEX', name, max, min] + if start is not None and num is not None: + pieces.extend([Token('LIMIT'), start, num]) + return self.execute_command(*pieces) + def zrangebyscore(self, name, min, max, start=None, num=None, withscores=False, score_cast_func=float): """ @@ -1799,12 +1832,12 @@ "Adds the specified elements to the specified HyperLogLog." return self.execute_command('PFADD', name, *values) - def pfcount(self, name): + def pfcount(self, *sources): """ Return the approximated cardinality of - the set observed by the HyperLogLog at key. + the set observed by the HyperLogLog at key(s). """ - return self.execute_command('PFCOUNT', name) + return self.execute_command('PFCOUNT', *sources) def pfmerge(self, dest, *sources): "Merge N different HyperLogLogs into a single one." @@ -2142,10 +2175,10 @@ # previously listening to return command(*args) - def parse_response(self, block=True): + def parse_response(self, block=True, timeout=0): "Parse the response from a publish/subscribe command" connection = self.connection - if not block and not connection.can_read(): + if not block and not connection.can_read(timeout=timeout): return None return self._execute(connection, connection.read_response) @@ -2216,9 +2249,15 @@ if response is not None: yield response - def get_message(self, ignore_subscribe_messages=False): - "Get the next message if one is available, otherwise None" - response = self.parse_response(block=False) + def get_message(self, ignore_subscribe_messages=False, timeout=0): + """ + Get the next message if one is available, otherwise None. + + If timeout is specified, the system will wait for `timeout` seconds + before returning. Timeout should be specified as a floating point + number. + """ + response = self.parse_response(block=False, timeout=timeout) if response: return self.handle_message(response, ignore_subscribe_messages) return None @@ -2282,30 +2321,39 @@ for pattern, handler in iteritems(self.patterns): if handler is None: raise PubSubError("Pattern: '%s' has no handler registered") - pubsub = self - class WorkerThread(threading.Thread): - def __init__(self, *args, **kwargs): - super(WorkerThread, self).__init__(*args, **kwargs) - self._running = False - - def run(self): - if self._running: - return - self._running = True - while self._running and pubsub.subscribed: - pubsub.get_message(ignore_subscribe_messages=True) - mod_time.sleep(sleep_time) - - def stop(self): - self._running = False - self.join() - - thread = WorkerThread() + thread = PubSubWorkerThread(self, sleep_time) thread.start() return thread +class PubSubWorkerThread(threading.Thread): + def __init__(self, pubsub, sleep_time): + super(PubSubWorkerThread, self).__init__() + self.pubsub = pubsub + self.sleep_time = sleep_time + self._running = False + + def run(self): + if self._running: + return + self._running = True + pubsub = self.pubsub + sleep_time = self.sleep_time + while pubsub.subscribed: + pubsub.get_message(ignore_subscribe_messages=True, + timeout=sleep_time) + pubsub.close() + self._running = False + + def stop(self): + # stopping simply unsubscribes from all channels and patterns. + # the unsubscribe responses that are generated will short circuit + # the loop in run(), calling pubsub.close() to clean up the connection + self.pubsub.unsubscribe() + self.pubsub.punsubscribe() + + class BasePipeline(object): """ Pipelines provide a way to transmit multiple commands to the Redis server @@ -2526,9 +2574,9 @@ raise r def annotate_exception(self, exception, number, command): - cmd = unicode(' ').join(imap(unicode, command)) + cmd = safe_unicode(' ').join(imap(safe_unicode, command)) msg = unicode('Command # %d (%s) of pipeline caused error: %s') % ( - number, cmd, unicode(exception.args[0])) + number, cmd, safe_unicode(exception.args[0])) exception.args = (msg,) + exception.args[1:] def parse_response(self, connection, command_name, **options): diff -Nru python-redis-2.10.3/redis/_compat.py python-redis-2.10.5/redis/_compat.py --- python-redis-2.10.3/redis/_compat.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/redis/_compat.py 2015-11-03 00:20:02.000000000 +0000 @@ -3,6 +3,7 @@ if sys.version_info[0] < 3: + from urllib import unquote from urlparse import parse_qs, urlparse from itertools import imap, izip from string import letters as ascii_letters @@ -12,15 +13,40 @@ except ImportError: from StringIO import StringIO as BytesIO - iteritems = lambda x: x.iteritems() - iterkeys = lambda x: x.iterkeys() - itervalues = lambda x: x.itervalues() - nativestr = lambda x: \ - x if isinstance(x, str) else x.encode('utf-8', 'replace') - u = lambda x: x.decode() - b = lambda x: x - next = lambda x: x.next() - byte_to_chr = lambda x: x + # special unicode handling for python2 to avoid UnicodeDecodeError + def safe_unicode(obj, *args): + """ return the unicode representation of obj """ + try: + return unicode(obj, *args) + except UnicodeDecodeError: + # obj is byte string + ascii_text = str(obj).encode('string_escape') + return unicode(ascii_text) + + def iteritems(x): + return x.iteritems() + + def iterkeys(x): + return x.iterkeys() + + def itervalues(x): + return x.itervalues() + + def nativestr(x): + return x if isinstance(x, str) else x.encode('utf-8', 'replace') + + def u(x): + return x.decode() + + def b(x): + return x + + def next(x): + return x.next() + + def byte_to_chr(x): + return x + unichr = unichr xrange = xrange basestring = basestring @@ -28,19 +54,32 @@ bytes = str long = long else: - from urllib.parse import parse_qs, urlparse + from urllib.parse import parse_qs, unquote, urlparse from io import BytesIO from string import ascii_letters from queue import Queue - iteritems = lambda x: iter(x.items()) - iterkeys = lambda x: iter(x.keys()) - itervalues = lambda x: iter(x.values()) - byte_to_chr = lambda x: chr(x) - nativestr = lambda x: \ - x if isinstance(x, str) else x.decode('utf-8', 'replace') - u = lambda x: x - b = lambda x: x.encode('latin-1') if not isinstance(x, bytes) else x + def iteritems(x): + return iter(x.items()) + + def iterkeys(x): + return iter(x.keys()) + + def itervalues(x): + return iter(x.values()) + + def byte_to_chr(x): + return chr(x) + + def nativestr(x): + return x if isinstance(x, str) else x.decode('utf-8', 'replace') + + def u(x): + return x + + def b(x): + return x.encode('latin-1') if not isinstance(x, bytes) else x + next = next unichr = chr imap = map @@ -48,6 +87,7 @@ xrange = range basestring = str unicode = str + safe_unicode = str bytes = bytes long = int diff -Nru python-redis-2.10.3/redis/connection.py python-redis-2.10.5/redis/connection.py --- python-redis-2.10.3/redis/connection.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/redis/connection.py 2015-11-03 00:20:02.000000000 +0000 @@ -16,7 +16,8 @@ from redis._compat import (b, xrange, imap, byte_to_chr, unicode, bytes, long, BytesIO, nativestr, basestring, iteritems, - LifoQueue, Empty, Full, urlparse, parse_qs) + LifoQueue, Empty, Full, urlparse, parse_qs, + unquote) from redis.exceptions import ( RedisError, ConnectionError, @@ -79,7 +80,9 @@ class BaseParser(object): EXCEPTION_CLASSES = { - 'ERR': ResponseError, + 'ERR': { + 'max number of clients reached': ConnectionError + }, 'EXECABORT': ExecAbortError, 'LOADING': BusyLoadingError, 'NOSCRIPT': NoScriptError, @@ -91,7 +94,10 @@ error_code = response.split(' ')[0] if error_code in self.EXCEPTION_CLASSES: response = response[len(error_code) + 1:] - return self.EXCEPTION_CLASSES[error_code](response) + exception_class = self.EXCEPTION_CLASSES[error_code] + if isinstance(exception_class, dict): + exception_class = exception_class.get(response, ResponseError) + return exception_class(response) return ResponseError(response) @@ -179,8 +185,16 @@ self.bytes_read = 0 def close(self): - self.purge() - self._buffer.close() + try: + self.purge() + self._buffer.close() + except: + # issue #633 suggests the purge/close somehow raised a + # BadFileDescriptor error. Perhaps the client ran out of + # memory or something else? It's probably OK to ignore + # any error being raised from purge/close since we're + # removing the reference to the instance below. + pass self._buffer = None self._sock = None @@ -345,14 +359,6 @@ self._reader.feed(self._buffer, 0, bufflen) else: self._reader.feed(buffer) - # proactively, but not conclusively, check if more data is in the - # buffer. if the data received doesn't end with \r\n, there's more. - if HIREDIS_USE_BYTE_BUFFER: - if bufflen > 2 and self._buffer[bufflen - 2:bufflen] != SYM_CRLF: - continue - else: - if not buffer.endswith(SYM_CRLF): - continue response = self._reader.gets() # if an older version of hiredis is installed, we need to attempt # to convert ResponseErrors to their appropriate types. @@ -542,11 +548,12 @@ e = sys.exc_info()[1] self.disconnect() if len(e.args) == 1: - _errno, errmsg = 'UNKNOWN', e.args[0] + errno, errmsg = 'UNKNOWN', e.args[0] else: - _errno, errmsg = e.args + errno = e.args[0] + errmsg = e.args[1] raise ConnectionError("Error %s while writing to socket. %s." % - (_errno, errmsg)) + (errno, errmsg)) except: self.disconnect() raise @@ -555,13 +562,14 @@ "Pack and send a command to the Redis server" self.send_packed_command(self.pack_command(*args)) - def can_read(self): + def can_read(self, timeout=0): "Poll the socket to see if there's data that can be read." sock = self._sock if not sock: self.connect() sock = self._sock - return bool(select([sock], [], [], 0)[0]) or self._parser.can_read() + return self._parser.can_read() or \ + bool(select([sock], [], [], timeout)[0]) def read_response(self): "Read the response from a previously sent command" @@ -585,7 +593,7 @@ elif isinstance(value, float): value = b(repr(value)) elif not isinstance(value, basestring): - value = str(value) + value = unicode(value) if isinstance(value, unicode): value = value.encode(self.encoding, self.encoding_errors) return value @@ -728,7 +736,7 @@ class ConnectionPool(object): "Generic connection pool" @classmethod - def from_url(cls, url, db=None, **kwargs): + def from_url(cls, url, db=None, decode_components=False, **kwargs): """ Return a connection pool configured from the given URL. @@ -752,6 +760,12 @@ If none of these options are specified, db=0 is used. + The ``decode_components`` argument allows this function to work with + percent-encoded URLs. If this argument is set to ``True`` all ``%xx`` + escapes will be replaced by their single-character equivalents after + the URL has been parsed. This only applies to the ``hostname``, + ``path``, and ``password`` components. + Any additional querystring arguments and keyword arguments will be passed along to the ConnectionPool class's initializer. In the case of conflicting arguments, querystring arguments always win. @@ -776,26 +790,35 @@ if value and len(value) > 0: url_options[name] = value[0] + if decode_components: + password = unquote(url.password) if url.password else None + path = unquote(url.path) if url.path else None + hostname = unquote(url.hostname) if url.hostname else None + else: + password = url.password + path = url.path + hostname = url.hostname + # We only support redis:// and unix:// schemes. if url.scheme == 'unix': url_options.update({ - 'password': url.password, - 'path': url.path, + 'password': password, + 'path': path, 'connection_class': UnixDomainSocketConnection, }) else: url_options.update({ - 'host': url.hostname, + 'host': hostname, 'port': int(url.port or 6379), - 'password': url.password, + 'password': password, }) # If there's a path argument, use it as the db argument if a # querystring value wasn't specified - if 'db' not in url_options and url.path: + if 'db' not in url_options and path: try: - url_options['db'] = int(url.path.replace('/', '')) + url_options['db'] = int(path.replace('/', '')) except (AttributeError, ValueError): pass diff -Nru python-redis-2.10.3/redis/__init__.py python-redis-2.10.5/redis/__init__.py --- python-redis-2.10.3/redis/__init__.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/redis/__init__.py 2015-11-03 00:20:02.000000000 +0000 @@ -22,7 +22,7 @@ ) -__version__ = '2.10.3' +__version__ = '2.10.5' VERSION = tuple(map(int, __version__.split('.'))) __all__ = [ diff -Nru python-redis-2.10.3/redis/sentinel.py python-redis-2.10.5/redis/sentinel.py --- python-redis-2.10.3/redis/sentinel.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/redis/sentinel.py 2015-11-03 00:20:02.000000000 +0000 @@ -129,6 +129,8 @@ self.disconnect() self.reset() self.__init__(self.service_name, self.sentinel_manager, + is_master=self.is_master, + check_connection=self.check_connection, connection_class=self.connection_class, max_connections=self.max_connections, **self.connection_kwargs) diff -Nru python-redis-2.10.3/setup.cfg python-redis-2.10.5/setup.cfg --- python-redis-2.10.3/setup.cfg 1970-01-01 00:00:00.000000000 +0000 +++ python-redis-2.10.5/setup.cfg 2015-11-03 00:20:02.000000000 +0000 @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff -Nru python-redis-2.10.3/setup.py python-redis-2.10.5/setup.py --- python-redis-2.10.3/setup.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/setup.py 2015-11-03 00:20:02.000000000 +0000 @@ -23,7 +23,9 @@ except ImportError: from distutils.core import setup - PyTest = lambda x: x + + def PyTest(x): + x f = open(os.path.join(os.path.dirname(__file__), 'README.rst')) long_description = f.read() diff -Nru python-redis-2.10.3/tests/test_commands.py python-redis-2.10.5/tests/test_commands.py --- python-redis-2.10.3/tests/test_commands.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/tests/test_commands.py 2015-11-03 00:20:02.000000000 +0000 @@ -112,7 +112,7 @@ r['a'] = 'foo' assert isinstance(r.object('refcount', 'a'), int) assert isinstance(r.object('idletime', 'a'), int) - assert r.object('encoding', 'a') == b('raw') + assert r.object('encoding', 'a') in (b('raw'), b('embstr')) assert r.object('idletime', 'invalid-key') is None def test_ping(self, r): @@ -959,6 +959,17 @@ assert r.zrangebylex('a', '[f', '+') == [b('f'), b('g')] assert r.zrangebylex('a', '-', '+', start=3, num=2) == [b('d'), b('e')] + @skip_if_server_version_lt('2.9.9') + def test_zrevrangebylex(self, r): + r.zadd('a', a=0, b=0, c=0, d=0, e=0, f=0, g=0) + assert r.zrevrangebylex('a', '[c', '-') == [b('c'), b('b'), b('a')] + assert r.zrevrangebylex('a', '(c', '-') == [b('b'), b('a')] + assert r.zrevrangebylex('a', '(g', '[aaa') == \ + [b('f'), b('e'), b('d'), b('c'), b('b')] + assert r.zrevrangebylex('a', '+', '[f') == [b('g'), b('f')] + assert r.zrevrangebylex('a', '+', '-', start=3, num=2) == \ + [b('d'), b('c')] + def test_zrangebyscore(self, r): r.zadd('a', a1=1, a2=2, a3=3, a4=4, a5=5) assert r.zrangebyscore('a', 2, 4) == [b('a2'), b('a3'), b('a4')] @@ -1106,6 +1117,10 @@ members = set([b('1'), b('2'), b('3')]) r.pfadd('a', *members) assert r.pfcount('a') == len(members) + members_b = set([b('2'), b('3'), b('4')]) + r.pfadd('b', *members_b) + assert r.pfcount('b') == len(members_b) + assert r.pfcount('a', 'b') == len(members_b.union(members)) @skip_if_server_version_lt('2.8.9') def test_pfmerge(self, r): diff -Nru python-redis-2.10.3/tests/test_connection_pool.py python-redis-2.10.5/tests/test_connection_pool.py --- python-redis-2.10.3/tests/test_connection_pool.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/tests/test_connection_pool.py 2015-11-03 00:20:02.000000000 +0000 @@ -163,6 +163,17 @@ 'password': None, } + def test_quoted_hostname(self): + pool = redis.ConnectionPool.from_url('redis://my %2F host %2B%3D+', + decode_components=True) + assert pool.connection_class == redis.Connection + assert pool.connection_kwargs == { + 'host': 'my / host +=+', + 'port': 6379, + 'db': 0, + 'password': None, + } + def test_port(self): pool = redis.ConnectionPool.from_url('redis://localhost:6380') assert pool.connection_class == redis.Connection @@ -183,6 +194,18 @@ 'password': 'mypassword', } + def test_quoted_password(self): + pool = redis.ConnectionPool.from_url( + 'redis://:%2Fmypass%2F%2B word%3D%24+@localhost', + decode_components=True) + assert pool.connection_class == redis.Connection + assert pool.connection_kwargs == { + 'host': 'localhost', + 'port': 6379, + 'db': 0, + 'password': '/mypass/+ word=$+', + } + def test_db_as_argument(self): pool = redis.ConnectionPool.from_url('redis://localhost', db='1') assert pool.connection_class == redis.Connection @@ -259,6 +282,28 @@ 'db': 0, 'password': 'mypassword', } + + def test_quoted_password(self): + pool = redis.ConnectionPool.from_url( + 'unix://:%2Fmypass%2F%2B word%3D%24+@/socket', + decode_components=True) + assert pool.connection_class == redis.UnixDomainSocketConnection + assert pool.connection_kwargs == { + 'path': '/socket', + 'db': 0, + 'password': '/mypass/+ word=$+', + } + + def test_quoted_path(self): + pool = redis.ConnectionPool.from_url( + 'unix://:mypassword@/my%2Fpath%2Fto%2F..%2F+_%2B%3D%24ocket', + decode_components=True) + assert pool.connection_class == redis.UnixDomainSocketConnection + assert pool.connection_kwargs == { + 'path': '/my/path/to/../+_+=$ocket', + 'db': 0, + 'password': 'mypassword', + } def test_db_as_argument(self): pool = redis.ConnectionPool.from_url('unix:///socket', db=1) diff -Nru python-redis-2.10.3/tests/test_encoding.py python-redis-2.10.5/tests/test_encoding.py --- python-redis-2.10.3/tests/test_encoding.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/tests/test_encoding.py 2015-11-03 00:20:02.000000000 +0000 @@ -23,6 +23,13 @@ r.rpush('a', *result) assert r.lrange('a', 0, -1) == result + def test_object_value(self, r): + unicode_string = unichr(3456) + u('abcd') + unichr(3421) + r['unicode-string'] = Exception(unicode_string) + cached_val = r['unicode-string'] + assert isinstance(cached_val, unicode) + assert unicode_string == cached_val + class TestCommandsAndTokensArentEncoded(object): @pytest.fixture() diff -Nru python-redis-2.10.3/tests/test_scripting.py python-redis-2.10.5/tests/test_scripting.py --- python-redis-2.10.3/tests/test_scripting.py 2014-08-14 17:18:53.000000000 +0000 +++ python-redis-2.10.5/tests/test_scripting.py 2015-11-03 00:20:02.000000000 +0000 @@ -10,6 +10,17 @@ value = tonumber(value) return value * ARGV[1]""" +msgpack_hello_script = """ +local message = cmsgpack.unpack(ARGV[1]) +local name = message['name'] +return "hello " .. name +""" +msgpack_hello_script_broken = """ +local message = cmsgpack.unpack(ARGV[1]) +local names = message['name'] +return "hello " .. name +""" + class TestScripting(object): @pytest.fixture(autouse=True) @@ -80,3 +91,25 @@ assert r.script_exists(multiply.sha) == [False] # [SET worked, GET 'a', result of multiple script] assert pipe.execute() == [True, b('2'), 6] + + def test_eval_msgpack_pipeline_error_in_lua(self, r): + msgpack_hello = r.register_script(msgpack_hello_script) + assert not msgpack_hello.sha + + pipe = r.pipeline() + + # avoiding a dependency to msgpack, this is the output of + # msgpack.dumps({"name": "joe"}) + msgpack_message_1 = b'\x81\xa4name\xa3Joe' + + msgpack_hello(args=[msgpack_message_1], client=pipe) + + assert r.script_exists(msgpack_hello.sha) == [True] + assert pipe.execute()[0] == b'hello Joe' + + msgpack_hello_broken = r.register_script(msgpack_hello_script_broken) + + msgpack_hello_broken(args=[msgpack_message_1], client=pipe) + with pytest.raises(exceptions.ResponseError) as excinfo: + pipe.execute() + assert excinfo.type == exceptions.ResponseError