Hello community,

here is the log from the commit of package python3-redis for openSUSE:Factory 
checked in at 2015-11-17 14:22:40
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python3-redis (Old)
 and      /work/SRC/openSUSE:Factory/.python3-redis.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python3-redis"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python3-redis/python3-redis.changes      
2014-09-06 12:18:00.000000000 +0200
+++ /work/SRC/openSUSE:Factory/.python3-redis.new/python3-redis.changes 
2015-11-17 14:22:42.000000000 +0100
@@ -1,0 +2,63 @@
+Wed Nov  4 04:37:30 UTC 2015 - a...@gmx.de
+
+- update to version 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.
+
+-------------------------------------------------------------------
+Tue Nov  3 01:01:51 UTC 2015 - a...@gmx.de
+
+- specfile:
+  * update copyright year
+
+- update to version 2.10.4:
+  * 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.
+
+-------------------------------------------------------------------

Old:
----
  redis-2.10.3.tar.gz

New:
----
  redis-2.10.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python3-redis.spec ++++++
--- /var/tmp/diff_new_pack.koZq63/_old  2015-11-17 14:22:42.000000000 +0100
+++ /var/tmp/diff_new_pack.koZq63/_new  2015-11-17 14:22:42.000000000 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python3-redis
 #
-# Copyright (c) 2014 SUSE LINUX Products GmbH, Nuernberg, Germany.
+# Copyright (c) 2015 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python3-redis
-Version:        2.10.3
+Version:        2.10.5
 Release:        0
 Url:            http://github.com/andymccurdy/redis-py
 Summary:        Python client for Redis key-value store
@@ -26,8 +26,8 @@
 Source:         
https://pypi.python.org/packages/source/r/redis/redis-%{version}.tar.gz
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
 BuildRequires:  python3-devel
-BuildRequires:  python3-setuptools
 BuildRequires:  python3-py
+BuildRequires:  python3-setuptools
 Requires:       python3-py
 BuildArch:      noarch
 

++++++ redis-2.10.3.tar.gz -> redis-2.10.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/CHANGES new/redis-2.10.5/CHANGES
--- old/redis-2.10.3/CHANGES    2014-08-14 19:18:42.000000000 +0200
+++ new/redis-2.10.5/CHANGES    2015-11-03 01:19:33.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/PKG-INFO new/redis-2.10.5/PKG-INFO
--- old/redis-2.10.3/PKG-INFO   2014-08-14 19:19:16.000000000 +0200
+++ new/redis-2.10.5/PKG-INFO   2015-11-03 01:21:05.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: redis
-Version: 2.10.3
+Version: 2.10.5
 Summary: Python client for Redis key-value store
 Home-page: http://github.com/andymccurdy/redis-py
 Author: Andy McCurdy
@@ -76,7 +76,7 @@
           `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
+        * **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
@@ -134,7 +134,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
@@ -621,7 +621,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).
         
@@ -651,7 +651,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/README.rst new/redis-2.10.5/README.rst
--- old/redis-2.10.3/README.rst 2014-06-16 22:42:56.000000000 +0200
+++ new/redis-2.10.5/README.rst 2015-01-04 01:21:38.000000000 +0100
@@ -68,7 +68,7 @@
   `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
+* **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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis/__init__.py 
new/redis-2.10.5/redis/__init__.py
--- old/redis-2.10.3/redis/__init__.py  2014-08-14 19:17:30.000000000 +0200
+++ new/redis-2.10.5/redis/__init__.py  2015-11-03 01:19:40.000000000 +0100
@@ -22,7 +22,7 @@
 )
 
 
-__version__ = '2.10.3'
+__version__ = '2.10.5'
 VERSION = tuple(map(int, __version__.split('.')))
 
 __all__ = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis/_compat.py 
new/redis-2.10.5/redis/_compat.py
--- old/redis-2.10.3/redis/_compat.py   2014-06-17 00:34:58.000000000 +0200
+++ new/redis-2.10.5/redis/_compat.py   2015-09-29 00:28:42.000000000 +0200
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis/client.py 
new/redis-2.10.5/redis/client.py
--- old/redis-2.10.3/redis/client.py    2014-07-21 19:53:42.000000000 +0200
+++ new/redis-2.10.5/redis/client.py    2015-11-03 00:02:54.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis/connection.py 
new/redis-2.10.5/redis/connection.py
--- old/redis-2.10.3/redis/connection.py        2014-08-14 19:15:19.000000000 
+0200
+++ new/redis-2.10.5/redis/connection.py        2015-11-02 20:25:13.000000000 
+0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis/sentinel.py 
new/redis-2.10.5/redis/sentinel.py
--- old/redis-2.10.3/redis/sentinel.py  2014-06-17 00:34:58.000000000 +0200
+++ new/redis-2.10.5/redis/sentinel.py  2014-09-18 18:01:10.000000000 +0200
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis.egg-info/PKG-INFO 
new/redis-2.10.5/redis.egg-info/PKG-INFO
--- old/redis-2.10.3/redis.egg-info/PKG-INFO    2014-08-14 19:19:15.000000000 
+0200
+++ new/redis-2.10.5/redis.egg-info/PKG-INFO    2015-11-03 01:21:05.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: redis
-Version: 2.10.3
+Version: 2.10.5
 Summary: Python client for Redis key-value store
 Home-page: http://github.com/andymccurdy/redis-py
 Author: Andy McCurdy
@@ -76,7 +76,7 @@
           `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
+        * **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
@@ -134,7 +134,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
@@ -621,7 +621,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).
         
@@ -651,7 +651,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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/redis.egg-info/SOURCES.txt 
new/redis-2.10.5/redis.egg-info/SOURCES.txt
--- old/redis-2.10.3/redis.egg-info/SOURCES.txt 2014-08-14 19:19:16.000000000 
+0200
+++ new/redis-2.10.5/redis.egg-info/SOURCES.txt 2015-11-03 01:21:05.000000000 
+0100
@@ -3,6 +3,7 @@
 LICENSE
 MANIFEST.in
 README.rst
+setup.cfg
 setup.py
 redis/__init__.py
 redis/_compat.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/setup.cfg new/redis-2.10.5/setup.cfg
--- old/redis-2.10.3/setup.cfg  2014-08-14 19:19:16.000000000 +0200
+++ new/redis-2.10.5/setup.cfg  2015-11-03 01:21:05.000000000 +0100
@@ -1,3 +1,6 @@
+[bdist_wheel]
+universal = 1
+
 [egg_info]
 tag_build = 
 tag_date = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/setup.py new/redis-2.10.5/setup.py
--- old/redis-2.10.3/setup.py   2014-06-17 00:34:58.000000000 +0200
+++ new/redis-2.10.5/setup.py   2015-09-29 00:29:14.000000000 +0200
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/tests/test_commands.py 
new/redis-2.10.5/tests/test_commands.py
--- old/redis-2.10.3/tests/test_commands.py     2014-07-21 19:53:42.000000000 
+0200
+++ new/redis-2.10.5/tests/test_commands.py     2015-11-02 19:08:25.000000000 
+0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/tests/test_connection_pool.py 
new/redis-2.10.5/tests/test_connection_pool.py
--- old/redis-2.10.3/tests/test_connection_pool.py      2014-06-17 
00:34:58.000000000 +0200
+++ new/redis-2.10.5/tests/test_connection_pool.py      2015-02-10 
02:18:07.000000000 +0100
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/tests/test_encoding.py 
new/redis-2.10.5/tests/test_encoding.py
--- old/redis-2.10.3/tests/test_encoding.py     2014-06-17 00:34:58.000000000 
+0200
+++ new/redis-2.10.5/tests/test_encoding.py     2015-06-06 21:44:46.000000000 
+0200
@@ -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 -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/redis-2.10.3/tests/test_scripting.py 
new/redis-2.10.5/tests/test_scripting.py
--- old/redis-2.10.3/tests/test_scripting.py    2014-06-16 22:42:56.000000000 
+0200
+++ new/redis-2.10.5/tests/test_scripting.py    2015-01-02 20:04:16.000000000 
+0100
@@ -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


Reply via email to