class NInt(Integer):
    """Saves integers as its negative number.  Testing purpose."""

    def encode(self, value):
        if not isinstance(value, numbers.Integral):
            raise TypeError('expected an integer, not {0!r}; something went '
                            'wrong!'.format(value))
        return Integer.encode(self, -value + 6)

    def decode(self, bulk):
        return -(Integer.decode(self, bulk) - 6) def get_client(cls=StrictRedis):
    host = os.environ.get('SIDERTEST_HOST', 'localhost')
    port = int(os.environ.get('SIDERTEST_PORT', 6379))
    db = int(os.environ.get('SIDERTEST_DB', 0))
    try:
        client = cls(host=host, port=port, db=db)
        # To connect on the server forcibly,
        # send a ping explicitly at this line.
        client.ping()
        return client
    except ConnectionError as e:
        pytest.fail(str(e), pytrace=False) def key(key):
    global prefix
    return prefix + str(key) @pytest.fixture
def session(request):
    client = get_client()
    session = get_session(client)

    @request.addfinalizer
    def fin():
        #: .. note::
        #:
        #:    Using ``FLUSHALL`` command is more easier and faster, but it's
        #:    also harmful because it flushes all data in the selected db.
        #:
        #:    However if it is assumed that there is no meaningful data in the
        #:    db, ``client.flushall()`` can be used safely and the testing
        #:    speed will be improved.
        used_keys = client.keys(prefix + '*')
        if used_keys:
            client.delete(*used_keys)
    return session class Session(object):
    """Session is an object which manages Python objects that represent Redis
    values e.g. lists, sets, hashes.  It maintains identity maps between
    Redis values and Python objects, and deals with transactions.

    :param client: the Redis client
    :type client: :class:`redis.client.StrictRedis`

    """

    #: (:class:`bool`) If it is set to ``True``, error messages raised by
    #: transactions will contain tracebacks where they started query/commit
    #: phase.
    #:
    #: It is mostly for debug purpose, and you can set this to ``True``
    #: if it's needed.
    verbose_transaction_error = None

    def __init__(self, client):
        if not isinstance(client, StrictRedis):
            raise TypeError('client must be a redis.client.StrictRedis object'
                            ', not ' + repr(client))
        elif isinstance(client, Redis):
            warnings.warn('redis.client.Redis is deprecated and would be '
                          'broken in the future; use redis.client.StrictRedis '
                          'instead', DeprecationWarning)
        self.client = client
        self.basic_client = client
        self.context_locals = LocalDict(transaction=None)
        self.verbose_transaction_error = False

    @property
    def server_version(self):
        """(:class:`str`) Redis server version string e.g. ``'2.2.11'``."""
        try:
            info = self._server_info
        except AttributeError:
            client = self.client
            if isinstance(client, BasePipeline):
                client = self.context_locals['original_client']
            info = client.info()
            self._server_info = info
        return info['redis_version']

    @property
    def server_version_info(self):
        """(:class:`tuple`) Redis server version triple e.g. ``(2, 2, 11)``.
        You can compare versions using this property.

        """
        return tuple(int(v) for v in self.server_version.split('.'))

    def get(self, key, value_type=ByteString):
        """Loads the value from the ``key``.
        If ``value_type`` is present the value will be treated as it,
        or :class:`~sider.types.ByteString` by default.

        :param key: the Redis key to load
        :type key: :class:`str`
        :param value_type: the type of the value to load.  default is
                           :class:`~sider.types.ByteString`
        :type value_type: :class:`~sider.types.Value`, :class:`type`
        :returns: the loaded value

        """
        value_type = Value.ensure_value_type(value_type,
                                             parameter='value_type')
        return value_type.load_value(self, key)

    def set(self, key, value, value_type=ByteString):
        """Stores the ``value`` into the ``key``.
        If ``value_type`` is present the value will be treated as it,
        or :class:`~sider.types.ByteString` by default.

        :param key: the Redis key to save the value into
        :type key: :class:`str`
        :param value: the value to be saved
        :param value_type: the type of the ``value``.  default is
                           :class:`~sider.types.ByteString`
        :type value_type: :class:`~sider.types.Value`, :class:`type`
        :returns: the Python representation of the saved value.
                  it is equivalent to the given ``value`` but
                  may not equal nor the same to

        """
        value_type = Value.ensure_value_type(value_type,
                                             parameter='value_type')
        return value_type.save_value(self, key, value)

    @property
    def current_transaction(self):
        """(:class:`~sider.transaction.Transaction`) The current transaction.
        It could be ``None`` when it's not on any transaction.

        """
        return self.context_locals['transaction']

    @property
    def transaction(self):
        """(:class:`sider.transaction.Transaction`) The transaction object
        for the session.

        :class:`~sider.transaction.Transaction` objects are callable and so
        you can use this :attr:`transaction` property as like a method::

            def block(trial, transaction):
                list_[0] = list_[0].upper()
            session.transaction(block)

        Or you can use it in a :keyword:`for` loop::

            for trial in session.transaction:
                list_[0] = list_[0].upper()

        .. seealso::

           Method :meth:`sider.transaction.Transaction.__call__()`
              Executes a given block in the transaction.

           Method :meth:`sider.transaction.Transaction.__iter__()`
              More explicit way to execute a routine in
              the transaction than :meth:`Transaction.__call__()
              <sider.transaction.Transaction.__call__>`

        """
        return Transaction(self)

    def mark_manipulative(self, keys=frozenset()):
        """Marks it is manipulative.

        :param keys: optional set of keys to watch
        :type keys: :class:`collections.Iterable`

        .. note::

           This method is for internal use.

        """
        transaction = self.current_transaction
        if transaction is None:
            return
        transaction.watch(keys)
        if not transaction.commit_phase:
            transaction.begin_commit()

    def mark_query(self, keys=frozenset()):
        """Marks it is querying.

        :param keys: optional set of keys to watch
        :type keys: :class:`collections.Iterable`

        :raises sider.exceptions.CommitError:
           when it is tried during commit phase

        .. note::

           This method is for internal use.

        """
        transaction = self.current_transaction
        if transaction is None:
            return
        if transaction.commit_phase:
            raise CommitError('query operation was tried during commit phase' +
                              transaction.format_commit_stack())
        else:
            transaction.watch(keys) class Set(Value):
    """The type object for :class:`sider.set.Set` objects and other
    :class:`collections.Set` objects.

    :param value_type: the type of values the set will contain.
                       default is :class:`String`
    :type value_type: :class:`Bulk`, :class:`type`

    """

    def __init__(self, value_type=None):
        if value_type is None:
            self.value_type = String()
        else:
            self.value_type = Bulk.ensure_value_type(value_type,
                                                     parameter='value_type')

    def load_value(self, session, key):
        return set.Set(session, key, value_type=self.value_type)

    def save_value(self, session, key, value):
        if not isinstance(value, collections.Set):
            raise TypeError('expected a set-like object, not ' +
                            repr(value))
        obj = set.Set(session, key, value_type=self.value_type)
        pipe = session.client.pipeline()
        pipe.delete(key)
        obj._raw_update(value, pipe)
        pipe.execute()
        return obj

    def __hash__(self):
        return super(Set, self).__hash__() * hash(self.value_type)

    def __eq__(self, operand):
        if super(Set, self).__eq__(operand):
            return self.value_type == operand.value_type
        return False class List(Value):
    """The type object for :class:`sider.list.List` objects and other
    :class:`collections.Sequence` objects except strings.
    (Use :class:`ByteString` or :class:`UnicodeString` for strings.)

    :param value_type: the type of values the list will contain.
                       default is :class:`String`
    :type value_type: :class:`Bulk`, :class:`type`

    """

    def __init__(self, value_type=None):
        if value_type is None:
            self.value_type = String()
        else:
            self.value_type = Bulk.ensure_value_type(value_type,
                                                     parameter='value_type')

    def load_value(self, session, key):
        return list.List(session, key, value_type=self.value_type)

    def save_value(self, session, key, value):
        if not isinstance(value, collections.Sequence):
            raise TypeError('expected a list-like sequence, not ' +
                            repr(value))
        obj = list.List(session, key, value_type=self.value_type)
        pipe = session.client.pipeline()
        pipe.delete(key)
        obj._raw_extend(value, pipe)
        pipe.execute()
        return obj

    def __hash__(self):
        return super(List, self).__hash__() * hash(self.value_type)

    def __eq__(self, operand):
        if super(List, self).__eq__(operand):
            return self.value_type == operand.value_type
        return False class Hash(Value):
    """The type object for :class:`sider.hash.Hash` objects and other
    :class:`collections.Mapping` objects.

    :param key_type: the type of keys the hash will contain.
                     default is :class:`String`
    :type key_type: :class:`Bulk`, :class:`type`
    :param value_type: the type of values the hash will contain.
                       default is :class:`String`
    :type value_type: :class:`Bulk`, :class:`type`

    """

    def __init__(self, key_type=None, value_type=None):
        if key_type is None:
            key_type = String()
        else:
            key_type = Bulk.ensure_value_type(key_type, parameter='key_type')
        if value_type is None:
            value_type = String()
        else:
            value_type = Bulk.ensure_value_type(value_type,
                                                parameter='value_type')
        self.key_type = key_type
        self.value_type = value_type

    def load_value(self, session, key):
        from .hash import Hash
        return Hash(session, key,
                    key_type=self.key_type, value_type=self.value_type)

    def save_value(self, session, key, value):
        if not isinstance(value, collections.Mapping):
            raise TypeError('expected a mapping object, not ' + repr(value))
        from .hash import Hash
        obj = Hash(session, key,
                   key_type=self.key_type, value_type=self.value_type)
        if value:
            pipe = session.client.pipeline()
            pipe.delete(key)
            obj._raw_update(value, pipe)
            pipe.execute()
        else:
            session.client.delete(key)
        return obj class SortedSet(Set):
    """The type object for :class:`sider.sortedset.SortedSet` objects.

    :param value_type: the type of values the sorted set will contain.
                       default is :class:`String`
    :type value_type: :class:`Bulk`, :class:`type`

    """

    def load_value(self, session, key):
        return sortedset.SortedSet(session, key, value_type=self.value_type)

    def save_value(self, session, key, value):
        if not isinstance(value, (collections.Set, collections.Mapping)):
            raise TypeError('expected a set-like or mapping object, not ' +
                            repr(value))
        obj = sortedset.SortedSet(session, key, value_type=self.value_type)
        encode = self.value_type.encode
        if session.server_version_info < (2, 4, 0):
            if isinstance(value, collections.Mapping):
                pairs = [
                    (encode(el), score)
                    for el, score in getattr(value, 'iteritems', value.items)()
                ]
            else:
                pairs = [(encode(el), 1) for el in value]
            def block(trial, transaction):
                obj.clear()
                zadd = session.client.zadd
                for el, score in pairs:
                    zadd(key, score, el)
        else:
            if isinstance(value, collections.Mapping):
                items = getattr(value, 'iteritems', value.items)
                def args():
                    for el, score in items():
                        yield score
                        yield encode(el)
            else:
                def args():
                    for el in value:
                        yield 1
                        yield encode(el)
            args = tuple(args())
            def block(trial, transaction):
                obj.clear()
                if args:
                    session.client.zadd(key, *args)
        session.transaction(block, [key], ignore_double=True)
        return obj class Integer(Bulk):
    """Stores integers as decimal strings.  For example:

    .. sourcecode:: pycon

       >>> integer = Integer()
       >>> integer.encode(42)
       '42'
       >>> integer.decode('42')
       42

    Why it doesn't store integers as binaries but decimals is that
    Redis provides :redis:`INCR`, :redis:`INCRBY`, :redis:`DECR` and
    :redis:`DECRBY` for decimal strings.  You can simply add and
    subtract integers.

    """

    def encode(self, value):
        if not isinstance(value, numbers.Integral):
            raise TypeError('expected an integer, not ' + repr(value))
        return str(value).encode('ascii')

    def decode(self, bulk):
        return int(bulk) class Set(collections.MutableSet):
    """The Python-side representaion of Redis set value.  It behaves
    alike built-in Python :class:`set` object.  More exactly, it
    implements :class:`collections.MutableSet` protocol.

    .. table:: Mappings of Redis commands--:class:`Set` methods

       ==================== ==========================================
       Redis commands       :class:`Set` methods
       ==================== ==========================================
       :redis:`DEL`         :meth:`Set.clear()`
       :redis:`SADD`        :meth:`Set.add()`,
                            :meth:`Set.update()`
       :redis:`SCARD`       :func:`len()` (:meth:`Set.__len__()`)
       :redis:`SDIFF`       :meth:`Set.difference()`,
                            :token:`-` (:meth:`Set.__sub__()`)
       :redis:`SDIFFSTORE`  :meth:`Set.difference_update()`,
                            :token:`-=` (:meth:`Set.__isub__()`)
       :redis:`SINTER`      :meth:`Set.intersection()`,
                            :token:`&` (:meth:`Set.__and__()`)
       :redis:`SINTERSTORE` :meth:`Set.intersection_update()`,
                            :token:`&=` (:meth:`Set.__iand__()`)
       :redis:`SISMEMBER`   :keyword:`in` (:meth:`Set.__contains__()`)
       :redis:`SMEMBERS`    :func:`iter()` (:meth:`Set.__iter__()`)
       :redis:`SMOVE`       N/A
       :redis:`SPOP`        :meth:`Set.pop()`
       :redis:`SRANDMEMBER` N/A
       :redis:`SREM`        :meth:`Set.discard()`,
                            :meth:`Set.remove()`
       :redis:`SUNION`      :meth:`Set.union()`,
                            :token:`|` (:meth:`Set.__or__()`)
       :redis:`SUNIONSTORE` :meth:`Set.update()`,
                            :token:`|=` (:meth:`Set.__ior__()`)
       N/A                  :meth:`Set.symmetric_difference()`,
                            :token:`^` (:meth:`Set.__xor__()`)
       N/A                  :meth:`Set.symmetric_difference_update()`,
                            :token:`^=` (:meth:`Set.__ixor__()`)
       ==================== ==========================================

    .. todo::

       There currently are too many duplications implementations
       of its methods.  These should be done refactoring.

    """

    def __init__(self, session, key, value_type=String):
        if not isinstance(session, Session):
            raise TypeError('session must be a sider.session.Session '
                            'instance, not ' + repr(session))
        self.session = session
        self.key = key
        self.value_type = Bulk.ensure_value_type(value_type,
                                                 parameter='value_type')

    @query
    def __iter__(self):
        decode = self.value_type.decode
        for member in self.session.client.smembers(self.key):
            yield decode(member)

    @query
    def __len__(self):
        """Gets the cardinality of the set.

        Use this with the built-in :func:`len()` function.

        :returns: the cardinality of the set
        :rtype: :class:`numbers.Integral`

        .. note::

           This method is directly mapped to :redis:`SCARD`
           command.

        """
        return self.session.client.scard(self.key)

    @query
    def __contains__(self, member):
        """:keyword:`in` operator.  Tests whether the set contains
        the given operand ``member``.

        :param member: the value to test
        :returns: ``True`` if the set contains the given
                  operand ``member``
        :rtype: :class:`bool`

        .. note::

           This method is directly mapped to :redis:`SISMEMBER`
           command.

        """
        try:
            data = self.value_type.encode(member)
        except TypeError:
            return False
        return bool(self.session.client.sismember(self.key, data))

    def __eq__(self, operand):
        if isinstance(operand, (set, frozenset)):
            return frozenset(self) == operand
        elif isinstance(operand, Set) and self.session is operand.session:
            length = len(self)
            if length == 0:
                return len(operand) == 0
            elif self.value_type == operand.value_type:
                self.session.mark_query([self.key])
                for _ in self.session.client.sdiff(self.key, operand.key):
                    return False
                for _ in self.session.client.sdiff(operand.key, self.key):
                    return False
                return True
            else:
                return False
        elif isinstance(operand, collections.Set):
            return frozenset(self) == frozenset(operand)
        return False

    def __ne__(self, operand):
        return not (self == operand)

    def __lt__(self, operand):
        """Less-than (:token:`<`) operator.  Tests whether the set is
        a *proper* (or *strict*) subset of the given ``operand`` or not.

        To eleborate, the key difference between this less-than
        (:token:`<`) operator and less-than or equal-to (:token:`<=`)
        operator, which is equivalent to :meth:`issubset()` method,
        is that it returns ``False`` even if two sets are exactly
        the same.

        Let this show a simple example:

        .. sourcecode:: pycon

           >>> assert isinstance(s, sider.set.Set)  # doctest: +SKIP
           >>> set(s)  # doctest: +SKIP
           set([1, 2, 3])
           >>> s < set([1, 2]), s <= set([1, 2])  # doctest: +SKIP
           (False, False)
           >>> s < set([1, 2, 3]), s <= set([1, 2, 3])  # doctest: +SKIP
           (False, True)
           >>> s < set([1, 2, 3, 4]), s <= set([1, 2, 3, 4]) # doctest: +SKIP
           (True, True)

        :param operand: another set to test
        :type operand: :class:`collections.Set`
        :returns: ``True`` if the set is a proper subset of ``operand``
        :rtype: :class:`bool`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for < must be an instance of '
                            'collections.Set, not ' + repr(operand))
        if (isinstance(operand, Set) and self.session is operand.session and
            self.value_type == operand.value_type):
            client = self.session.client
            self.session.mark_query([self.key])
            for _ in client.sdiff(self.key, operand.key):
                return False
            card = len(self)
            if card != len(client.sinter(self.key, operand.key)):
                return False
            return card < client.scard(operand.key)
        return frozenset(self) < frozenset(operand)

    def __le__(self, operand):
        """Less-than or equal to (:token:`<=`) operator.
        Tests whether the set is a subset of the given ``operand``.

        It's the same operation to :meth:`issubset()` method except
        it can take a set-like operand only.  On the other hand
        :meth:`issubset()` can take an any iterable operand as well.

        :param operand: another set to test
        :type operand: :class:`collections.Set`
        :returns: ``True`` if the ``operand`` set contains the set
        :rtype: :class:`bool`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for <= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.issubset(operand)

    def __gt__(self, operand):
        """Greater-than (:token:`>`) operator.  Tests whether the set
        is a *proper* (or *strict*) superset of the given ``operand``.

        To eleborate, the key difference between this greater-than
        (:token:`>`) operator and greater-than or equal-to
        (:token:`>=`) operator, which is equivalent to
        :meth:`issuperset()` method, is that it returns ``False``
        even if two sets are exactly the same.

        Let this show a simple example:

        .. sourcecode:: pycon

           >>> assert isinstance(s, sider.set.Set)  # doctest: +SKIP
           >>> set(s)  # doctest: +SKIP
           set([1, 2, 3])
           >>> s > set([1, 2]), s >= set([1, 2])  # doctest: +SKIP
           (True, True)
           >>> s > set([1, 2, 3]), s >= set([1, 2, 3])  # doctest: +SKIP
           (False, True)
           >>> s > set([1, 2, 3, 4]), s >= set([1, 2, 3, 4]) # doctest: +SKIP
           (False, False)

        :param operand: another set to test
        :type operand: :class:`collections.Set`
        :returns: ``True`` if the set is a proper superset of
                  ``operand``
        :rtype: :class:`bool`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for > must be an instance of '
                            'collections.Set, not ' + repr(operand))
        if isinstance(operand, Set):
            return operand < self
        return frozenset(self) > frozenset(operand)

    def __ge__(self, operand):
        """Greater-than or equal to (:token:`>=`) operator.
        Tests whether the set is a superset of the given ``operand``.

        It's the same operation to :meth:`issuperset()` method except
        it can take a set-like operand only.  On the other hand
        :meth:`issuperset()` can take an any iterable operand as well.

        :param operand: another set to test
        :type operand: :class:`collections.Set`
        :returns: ``True`` if the set contains the ``operand``
        :rtype: :class:`bool`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for >= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.issuperset(operand)

    def __sub__(self, operand):
        """Minus (:token:`-`) operator.  Gets the relative complement
        of the ``operand`` in the set.

        Mostly equivalent to :meth:`difference()` method except it
        can take a set-like operand only.  On the other hand
        :meth:`difference()` can take an any iterable operand as well.

        :param operand: another set to get the relative complement
        :type operand: :class:`collections.Set`
        :returns: the relative complement
        :rtype: :class:`set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for - must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.difference(operand)

    def __rsub__(self, operand):
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for - must be an instance of '
                            'collections.Set, not ' + repr(operand))
        elif isinstance(operand, (Set, set, frozenset)):
            return operand.difference(self)
        operand = set(operand)
        operand.difference_update(self)
        return operand

    def __isub__(self, operand):
        """Minus augmented assignment (:token:`-=`).  Removes all
        elements of the ``operand`` from this set.

        Mostly equivalent to :meth:`difference_update()` method
        except it can take only one set-like operand.  On the other
        hand :meth:`difference_update()` can take zero or more
        iterable operands (not only set-like objects).

        :param operand: another set which has elements to remove
                        from this set
        :type operand: :class:`collections.Set`
        :returns: the set itself
        :rtype: :class:`Set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for -= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        self.difference_update(operand)
        return self

    def __xor__(self, operand):
        """Bitwise exclusive or (:token:`^`) operator.
        Returns a new set with elements in either the set or
        the ``operand`` but not both.

        Mostly equivalent to :meth:`symmetric_difference()` method
        except it can take a set-like operand only.  On the other hand
        :meth:`symmetric_difference()` can take an any iterable
        operand as well.

        :param operand: other set
        :type operand: :class:`collections.Set`
        :returns: a new set with elements in either the set or
                  the ``operand`` but not both
        :rtype: :class:`set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for ^ must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.symmetric_difference(operand)

    def __rxor__(self, operand):
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for ^ must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.symmetric_difference(operand)

    def __ixor__(self, operand):
        """Bitwise exclusive argumented assignment (:token:`^=`).
        Updates the set with the symmetric difference of itself
        and ``operand``.

        Mostly equivalent to :meth:`symmetric_difference_update()`
        method except it can take a set-like operand only.
        On the other hand :meth:`symmetric_difference_update()`
        can take an any iterable operand as well.

        :param operand: another set
        :type operand: :class:`collections.Set`
        :returns: the set itself
        :rtype: :class:`Set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for ^= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        self.symmetric_difference_update(operand)
        return self

    def __or__(self, operand):
        """Bitwise or (:token:`|`) operator.  Gets the union of
        operands.

        Mostly equivalent to :meth:`union()` method except it can
        take only one set-like operand.  On the other hand
        :meth:`union()` can take zero or more iterable operands
        (not only set-like objects).

        :param operand: another set to union
        :type operand: :class:`collections.Set`
        :returns: the union set
        :rtype: :class:`set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for | must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.union(operand)

    def __ror__(self, operand):
        return self | operand

    def __ior__(self, operand):
        """Bitwise or (:token:`|=`) assignment.  Updates the set
        with the union of itself and the ``operand``.

        Mostly equivalent to :meth:`update()` method except it can
        take only one set-like operand.  On the other hand
        :meth:`update()` can take zero or more iterable operands
        (not only set-like objects).

        :param operand: another set to union
        :type operand: :class:`collections.Set`
        :returns: the set itself
        :rtype: :class:`Set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for |= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        self.update(operand)
        return self

    def __and__(self, operand):
        """Bitwise and (:token:`&`) operator.  Gets the union of
        operands.

        Mostly equivalent to :meth:`intersection()` method except it
        can take only one set-like operand.  On the other hand
        :meth:`intersection()` can take zero or more iterable
        operands (not only set-like objects).

        :param operand: another set to get intersection
        :type operand: :class:`collections.Set`
        :returns: the intersection
        :rtype: :class:`set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for & must be an instance of '
                            'collections.Set, not ' + repr(operand))
        return self.intersection(operand)

    def __rand__(self, operand):
        return self & operand

    def __iand__(self, operand):
        """Bitwise and (:token:`&=`) assignment.  Updates the set
        with the intersection of itself and the ``operand``.

        Mostly equivalent to :meth:`intersection_update()` method
        except it can take only one set-like operand.  On the other
        hand :meth:`intersection_update()` can take zero or more
        iterable operands (not only set-like objects).

        :param operand: another set to intersection
        :type operand: :class:`collections.Set`
        :returns: the set itself
        :rtype: :class:`Set`

        """
        if not isinstance(operand, collections.Set):
            raise TypeError('operand for &= must be an instance of '
                            'collections.Set, not ' + repr(operand))
        self.intersection_update(operand)
        return self

    def issubset(self, operand):
        """Tests whether the set is a subset of the given ``operand`` or not.
        To test proper (strict) subset, use :token:`<` operator instead.

        :param operand: another set to test
        :type operand: :class:`collections.Iterable`
        :returns: ``True`` if the ``operand`` set contains the set
        :rtype: :class:`bool`

        .. note::

           This method consists of following Redis commands:

           1. :redis:`SDIFF` for this set and ``operand``
           2. :redis:`SLEN` for this set
           3. :redis:`SLEN` for ``operand``

           If the first :redis:`SDIFF` returns anything, it sends
           no commands of the rest and simply returns ``False``.

        """
        if (isinstance(operand, Set) and self.session is operand.session and
            self.value_type == operand.value_type):
            client = self.session.client
            self.session.mark_query([self.key])
            for _ in client.sdiff(self.key, operand.key):
                return False
            return len(self) == len(client.sinter(self.key, operand.key))
        return frozenset(self).issubset(operand)

    def issuperset(self, operand):
        """Tests whether the set is a superset of the given
        ``operand``.  To test proper (strict) superset,
        use :token:`>` operator instead.

        :param operand: another set to test
        :type operand: :class:`collections.Iterable`
        :returns: ``True`` if the set contains ``operand``
        :rtype: :class:`bool`

        """
        if isinstance(operand, (Set, set, frozenset)):
            return operand.issubset(self)
        return frozenset(self).issuperset(operand)

    def isdisjoint(self, operand):
        """Tests whether two sets are disjoint or not.

        :param operand: another set to test
        :type operand: :class:`collections.Iterable`
        :returns: ``True`` if two sets have a null intersection
        :rtype: :class:`bool`

        .. note::

           It internally uses :redis:`SINTER` command.

        """
        if isinstance(operand, Set) and self.session is operand.session:
            if self.value_type != operand.value_type:
                return True
            self.session.mark_query([self.key])
            for _ in self.session.client.sinter(self.key, operand.key):
                return False
            return True
        return super(Set, self).isdisjoint(operand)

    def difference(self, *sets):
        """Returns the difference of two or more ``sets``
        as a new :class:`set` i.e. all elements that are in
        this set but not the others.

        :param sets: other iterables to get the difference
        :returns: the relative complement
        :rtype: :class:`set`

        .. note::

           This method is mapped to :redis:`SDIFF` command.

        """
        online_sets = []
        offline_sets = []
        for operand in sets:
            if isinstance(operand, Set) and self.session is operand.session:
                if self.value_type == operand.value_type:
                    online_sets.append(operand)
            else:
                offline_sets.append(operand)
        keys = (operand.key for operand in online_sets)
        self.session.mark_query([self.key])
        fetched = self.session.client.sdiff(self.key, *keys)
        decode = self.value_type.decode
        diff = set(decode(member) for member in fetched)
        diff.difference_update(*offline_sets)
        return diff

    def symmetric_difference(self, operand):
        """Returns a new set with elements in either the set or
        the ``operand`` but not both.

        :param operand: other set
        :type operand: :class:`collections.Iterable`
        :returns: a new set with elements in either the set or
                  the ``operand`` but not both
        :rtype: :class:`set`

        .. note::

           This method consists of following two commands:

           1. :redis:`SUNION` of this set and the ``operand``
           2. :redis:`SINTER` of this set and the ``operand``

           and then makes a new :class:`set` with elements in
           the first result are that are not in the second result.

        """
        if (isinstance(operand, Set) and self.session is operand.session and
            self.value_type == operand.value_type):
            self.session.mark_query([self.key])
            union = self.session.client.sunion(self.key, operand.key)
            inter = self.session.client.sinter(self.key, operand.key)
            symdiff = set(union)
            symdiff.difference_update(inter)
            decode = self.value_type.decode
            return set(decode(member) for member in symdiff)
        return set(self).symmetric_difference(operand)

    def union(self, *sets):
        """Gets the union of the given sets.

        :param \*sets: zero or more operand sets to union.
                       all these must be iterable
        :returns: the union set
        :rtype: :class:`set`

        .. note::

           It sends a :redis:`SUNION` command for other :class:`Set`
           objects.  For other ordinary Python iterables, it unions
           all in the memory.

        """
        online_sets = {self.value_type: [self]}
        offline_sets = []
        for operand in sets:
            if (isinstance(operand, Set) and self.session is operand.session):
                group = online_sets.setdefault(operand.value_type, [])
                group.append(operand)
            else:
                offline_sets.append(operand)
        union = set()
        for value_type in online_sets:
            group = online_sets[value_type]
            keys = (s.key for s in group)
            self.session.mark_query([self.key])
            subset = self.session.client.sunion(*keys)
            decode = value_type.decode
            union.update(decode(member) for member in subset)
        for operand in offline_sets:
            union.update(operand)
        return union

    def intersection(self, *sets):
        """Gets the intersection of the given sets.

        :param \*sets: zero or more operand sets to get intersection.
                       all these must be iterable
        :returns: the intersection
        :rtype: :class:`set`

        """
        online_sets = []
        offline_sets = []
        for operand in sets:
            if (isinstance(operand, Set) and self.session is operand.session):
                if self.value_type != operand.value_type:
                    return set()
                online_sets.append(operand)
            else:
                offline_sets.append(operand)
        keys = frozenset(s.key for s in online_sets)
        if keys:
            self.session.mark_query([self.key])
            inter = self.session.client.sinter(self.key, *keys)
            decode = self.value_type.decode
            online = set(decode(m) for m in inter)
        else:
            online = self
        if offline_sets:
            base = set(offline_sets.pop())
            base.intersection_update(online, *offline_sets)
            return base
        return online if isinstance(online, set) else set(online)

    @manipulative
    def add(self, element):
        """Adds an ``element`` to the set.  This has no effect
        if the ``element`` is already present.

        :param element: an element to add

        .. note::

           This method is a direct mapping to :redis:`SADD` comamnd.

        """
        member = self.value_type.encode(element)
        self.session.client.sadd(self.key, member)

    @manipulative
    def discard(self, element):
        """Removes an ``element`` from the set if it is a member.
        If the ``element`` is not a member, does nothing.

        :param element: an element to remove

        .. note::

           This method is mapped to :redis:`SREM` command.

        """
        try:
            member = self.value_type.encode(element)
        except TypeError:
            return
        self.session.client.srem(self.key, member)

    def pop(self):
        """Removes an arbitrary element from the set and returns it.
        Raises :exc:`~exceptions.KeyError` if the set is empty.

        :returns: a removed arbitrary element
        :raises exceptions.KeyError: if the set is empty

        .. note::

           This method is directly mapped to :redis:`SPOP` command.

        """
        if self.session.current_transaction is None:
            popped = self.session.client.spop(self.key)
            if popped is None:
                raise KeyError('pop from an empty set')
            return self.value_type.decode(popped)
        else:
            self.session.mark_query([self.key])
            popped = self.session.client.srandmember(self.key)
            if popped is None:
                raise KeyError('pop from an empty set')
            value = self.value_type.decode(popped)
            self.session.mark_manipulative()
            self._raw_delete([value], self.session.client)
            return value

    @manipulative
    def clear(self):
        """Removes all elements from this set.

        .. note::

           Under the hood it simply :redis:`DEL` the key.

        """
        self.session.client.delete(self.key)

    def update(self, *sets):
        """Updates the set with union of itself and operands.

        :param \*sets: zero or more operand sets to union.
                       all these must be iterable

        .. note::

           It sends a :redis:`SUNIONSTORE` command for other
           :class:`Set` objects and a :redis:`SADD` command for
           other ordinary Python iterables.

           Multiple operands of :redis:`SADD` command has been supported
           since Redis 2.4.0, so it would send multiple :redis:`SADD`
           commands if the Redis version is less than 2.4.0.

        """
        online_sets = []
        offline_sets = []
        for operand in sets:
            if isinstance(operand, Set) and self.session is operand.session:
                if self.value_type == operand.value_type:
                    online_sets.append(operand)
                else:
                    raise TypeError(
                        'value_type mismatch; tried union of {0!r} and '
                        '{1!r}'.format(self.value_type, operand.value_type)
                    )
            else:
                offline_sets.append(operand)
        def block(trial, transaction):
            pipe = self.session.client
            if online_sets:
                keys = [operand.key for operand in online_sets]
                self.session.mark_manipulative()
                pipe.sunionstore(self.key, self.key, *keys)
            update = self._raw_update
            for operand in offline_sets:
                update(operand, pipe)
        self.session.transaction(block, [self.key], ignore_double=True)

    def _raw_update(self, members, pipe):
        key = self.key
        encode = self.value_type.encode
        members = (encode(v) for v in members)
        self.session.mark_manipulative()
        if self.session.server_version_info < (2, 4, 0):
            for member in members:
                pipe.sadd(key, member)
        else:
            n = 100  # FIXME: it is an arbitarary magic number.
            for chunk in utils.chunk(members, n):
                pipe.sadd(key, *chunk)

    def intersection_update(self, *sets):
        """Updates the set with the intersection of itself and
        other ``sets``.

        :param \*sets: zero or more operand sets to intersection.
                       all these must be iterable

        .. note::

           It sends a :redis:`SINTERSTORE` command for other
           :class:`Set` objects and a :redis:`SREM` command for
           other ordinary Python iterables.

           Multiple operands of :redis:`SREM` command has been supported
           since Redis 2.4.0, so it would send multiple :redis:`SREM`
           commands if the Redis version is less than 2.4.0.

           Used commands: :redis:`SINTERSTORE`, :redis:`SMEMBERS`
           and :redis:`SREM`.

        """
        online_sets = []
        offline_sets = []
        for operand in sets:
            if isinstance(operand, Set) and self.session is operand.session:
                if self.value_type == operand.value_type:
                    online_sets.append(operand)
                else:
                    self.session.mark_manipulative()
                    self.session.client.delete(self.key)
                    return
            else:
                offline_sets.append(operand)
        try:
            memory_set = offline_sets.pop()
        except IndexError:
            memory_set = frozenset()
        else:
            if offline_sets:
                memory_set = set(memory_set)
                memory_set.intersection_update(*offline_sets)
        keys = tuple(operand.key for operand in online_sets)
        def block(trial, transaction):
            pipe = self.session.client
            if memory_set:
                self.session.mark_query()
                diff = self.difference(memory_set)
                self.session.mark_manipulative()
                self._raw_delete(diff, pipe)
            if keys:
                self.session.mark_manipulative()
                pipe.sinterstore(self.key, self.key, *keys)
        self.session.transaction(block, (self.key,) + keys, ignore_double=True)

    def difference_update(self, *sets):
        """Removes all elements of other ``sets`` from this set.

        :param \*sets: other sets that have elements to remove
                       from this set

        .. note::

           For :class:`Set` objects of the same session it internally
           uses :redis:`SDIFFSTORE` command.

           For other ordinary Python iterables, it uses :redis:`SREM`
           commands.  If the version of Redis is less than 2.4,
           sends :redis:`SREM` multiple times.  Because multiple
           operands of :redis:`SREM` command has been supported since
           Redis 2.4.

        """
        online_sets = []
        offline_sets = []
        for operand in sets:
            if isinstance(operand, Set) and self.session is operand.session:
                if self.value_type == operand.value_type:
                    online_sets.append(operand)
            else:
                offline_sets.append(operand)
        def block(trial, transaction):
            pipe = self.session.client
            if online_sets:
                keys = tuple(operand.key for operand in online_sets)
                self.session.mark_manipulative(keys)
                pipe.sdiffstore(self.key, self.key, *keys)
            for elements in offline_sets:
                self.session.mark_manipulative()
                self._raw_delete(elements, pipe)
        self.session.transaction(block, [self.key], ignore_double=True)

    def symmetric_difference_update(self, operand):
        """Updates the set with the symmetric difference of itself
        and ``operand``.

        :param operand: another set to get symmetric difference
        :type operand: :class:`collections.Iterable`

        .. note::

           This method consists of several Redis commands in a
           transaction: :redis:`SINTER`, :redis:`SUNIONSTORE` and
           :redis:`SREM`.

        """
        if isinstance(operand, Set) and self.session == operand.session:
            if self.value_type == operand.value_type:
                def block(trial, transaction):
                    pipe = self.session.client
                    self.session.mark_query()
                    inter = pipe.sinter(self.key, operand.key)
                    self.session.mark_manipulative()
                    pipe.sunionstore(self.key, self.key, operand.key)
                    self._raw_delete(inter, pipe, encoded=True)
                self.session.transaction(block, [self.key], ignore_double=True)
            else:
                raise TypeError(
                    'value_type mismatch; tried update {0!r} with '
                    '{1!r}'.format(self.value_type, operand.value_type)
                )
        else:
            operand = set(operand)
            inter = self & operand
            operand.difference_update(inter)
            pipe = self.session.client.pipeline()
            self._raw_update(operand, pipe)
            self._raw_delete(inter, pipe)
            pipe.execute()

    @manipulative
    def _raw_delete(self, elements, pipe, encoded=False):
        if not encoded:
            encode = self.value_type.encode
            def get_elements():
                for el in elements:
                    try:
                        yield encode(el)
                    except TypeError:
                        pass
            enc_elements = get_elements()
        else:
            enc_elements = elements
        if self.session.server_version_info < (2, 4, 0):
            for el in enc_elements:
                pipe.srem(self.key, el)
        else:
            pipe.srem(self.key, *enc_elements)

    def __repr__(self):
        cls = type(self)
        values = list(self)
        values.sort()
        elements = ', '.join(repr(v) for v in values)
        return '<{0}.{1} ({2!r}) {{{3}}}>'.format(
            cls.__module__, cls.__name__, self.key, elements
        ) class List(collections.MutableSequence):
    """The Python-side representaion of Redis list value.  It behaves
    alike built-in Python :class:`list` object.  More exactly, it
    implements :class:`collections.MutableSequence` protocol.

    .. table:: Mappings of Redis commands--:class:`List` methods

       =================== ===========================================
       Redis commands      :class:`List` methods
       =================== ===========================================
       :redis:`LLEN`       :func:`len()` (:meth:`List.__len__()`)
       :redis:`LPUSH`      :meth:`List.insert()`
       :redis:`LPUSHX`     N/A
       :redis:`LPOP`       :meth:`List.pop()`
       :redis:`RPUSH`      :meth:`List.append()`,
                           :meth:`List.extend()`
       :redis:`RPUSHX`     N/A
       :redis:`RPOP`       :meth:`List.pop()`
       :redis:`RPOPLPUSH`  N/A
       :redis:`LINDEX`     :meth:`List.__getitem__()`,
       :redis:`LINSERT`    N/A
       :redis:`LRANGE`     :func:`iter()` (:meth:`List.__iter__()`),
                           :meth:`List.__getitem__()`,
       :redis:`LREM`       N/A
       :redis:`LTRIM`      :keyword:`del` (:meth:`List.__delitem__()`)
       :redis:`DEL`        :keyword:`del` (:meth:`List.__delitem__()`)
       :redis:`LSET`       :token:`=` (:meth:`List.__setitem__()`)
       :redis:`BLPOP`      N/A
       :redis:`BRPOP`      N/A
       :redis:`BRPOPLPUSH` N/A
       =================== ===========================================

    .. todo::

       Methods automatically filled by :class:`collections.MutableSequence`
       should warn :exc:`~sider.list.PerformanceWarning`.

    """

    #: (:class:`sider.types.Bulk`) The type of list values.
    value_type = None

    def __init__(self, session, key, value_type=String):
        if not isinstance(session, Session):
            raise TypeError('session must be a sider.session.Session '
                            'instance, not ' + repr(session))
        self.session = session
        self.key = key
        self.value_type = Bulk.ensure_value_type(value_type,
                                                 parameter='value_type')

    def __iter__(self):
        i = 0
        step = 100  # FIXME: it is an arbitarary magic number.
        chunk = None
        decode = self.value_type.decode
        self.session.mark_query([self.key])
        while chunk is None or len(chunk) >= step:
            chunk = self.session.client.lrange(self.key, i, i + step - 1)
            for val in chunk:
                yield decode(val)
            i += step

    @query
    def __len__(self):
        """Gets the number of the list elements.

        Use this with the built-in :func:`len()` function.

        :returns: the number of the list elements
        :rtype: :class:`numbers.Integral`

        .. note::

           This method is directly mapped to :redis:`LLEN`
           command.

        """
        return self.session.client.llen(self.key)

    def __getitem__(self, index):
        """Gets or slices the element of the given ``index``.

        :param index: the index of the element to get,
                      or the slice of a range to get
        :type index: :class:`numbers.Integral`, :class:`slice`
        :returns: the element value, or the sliced new list
        :raises exceptions.TypeError:
           when ``index`` is not an integer nor a slice of integers
        :raises exceptions.IndexError:
           when ``index`` is out of range

        .. note::

           This is mapped to :redis:`LINDEX` for integer indices and
           :redis:`LRANGE` for slices.

        """
        decode = self.value_type.decode
        if isinstance(index, numbers.Integral):
            self.session.mark_query([self.key])
            result = self.session.client.lindex(self.key, index)
            if result is None:
                raise IndexError(index)
            return decode(result)
        elif isinstance(index, slice):
            start = 0 if index.start is None else index.start
            stop = (0 if index.stop is None else index.stop) - 1
            self.session.mark_query([self.key])
            result = self.session.client.lrange(self.key, start, stop)
            if index.step is not None:
                result = result[::index.step]
            return list(map(decode, result))
        raise TypeError('indices must be integers, not ' + repr(index))

    def __setitem__(self, index, value):
        encode = self.value_type.encode
        if isinstance(index, numbers.Integral):
            value = encode(value)
            if self.session.current_transaction is None:
                try:
                    self.session.client.lset(self.key, index, value)
                except ResponseError:
                    raise IndexError(index)
            else:
                self.session.mark_query([self.key])
                length = len(self)
                if 0 <= index < length or -length <= index < 0:
                    self.session.mark_manipulative()
                    self.session.client.lset(self.key, index, value)
                else:
                    raise IndexError(index)
        elif isinstance(index, slice):
            if index.step is not None:
                raise ValueError('slice with step is not supported for '
                                 'assignment')
            elif index.start in (0, None) and index.stop == 1:
                seq = list(map(encode, value))
                seq.reverse()
                self.session.mark_manipulative([self.key])
                if self.session.server_version_info < (2, 4, 0):
                    pipe = self.session.client
                    if self.session.current_transaction is None:
                        pipe = pipe.pipeline()
                    key = self.key
                    for v in seq:
                        pipe.lpush(key, v)
                    if self.session.current_transaction is None:
                        pipe.execute()
                else:
                    self.session.client.lpush(self.key, *seq)
            else:
                cls = type(self)
                warnings.warn(
                    'assignment into slice of {0}.{1} may cause performance '
                    'issues'.format(cls.__module__, cls.__name__),
                    category=PerformanceWarning, stacklevel=2
                )
                def block(trial, transaction):
                    pipe = self.session.client
                    self.session.mark_query()
                    list_ = pipe.lrange(self.key, 0, -1)
                    list_[index] = map(encode, value)
                    self.session.mark_manipulative()
                    pipe.delete(self.key)
                    self._raw_extend(list_, pipe, encoded=True)
                self.session.transaction(block, [self.key], ignore_double=True)
        else:
            raise TypeError('indices must be integers, not ' + repr(index))

    def __delitem__(self, index):
        if isinstance(index, slice):
            if index.step is not None:
                raise ValueError('slice with step is not supported for '
                                 'deletion')
            if index.start is index.stop is None:
                self.session.mark_manipulative([self.key])
                self.session.client.delete(self.key)
            elif not all(v is None or isinstance(v, numbers.Integral)
                         for v in (index.start, index.stop)):
                raise TypeError('slice indices must be integers or None')
            elif index.start is None:
                self.session.mark_manipulative([self.key])
                self.session.client.ltrim(self.key, index.stop, -1)
            elif index.stop is None:
                self.session.mark_manipulative([self.key])
                self.session.client.ltrim(self.key, 0, index.start - 1)
            else:
                cls = type(self)
                warnings.warn(
                    'deleting middle of {0}.{1} may cause performance '
                    'issues'.format(cls.__module__, cls.__name__),
                    category=PerformanceWarning, stacklevel=2
                )
                def block(trial, transaction):
                    pipe = self.session.client
                    self.session.mark_query([self.key])
                    tail = pipe.lrange(self.key, index.stop, -1)
                    self.session.mark_manipulative()
                    pipe.ltrim(self.key, 0, index.start - 1)
                    self._raw_extend(tail, pipe, encoded=True)
                self.session.transaction(block, [self.key], ignore_double=True)
        elif isinstance(index, numbers.Integral):
            self.pop(index, _stacklevel=2)
        else:
            raise TypeError('index must be an integer')

    @manipulative
    def append(self, value):
        """Inserts the ``value`` at the tail of the list.

        :param value: an value to insert

        """
        data = self.value_type.encode(value)
        self.session.client.rpush(self.key, data)

    def extend(self, iterable):
        """Extends the list with the ``iterable``.

        :param iterable: an iterable object that extend the list with
        :type iterable: :class:`collections.Iterable`
        :raises exceptions.TypeError:
           if the given ``iterable`` is not iterable

        .. warning::

           Appending multiple values is supported since Redis 2.4.0.
           This may send :redis:`RPUSH` multiple times in a transaction
           if Redis version is not 2.4.0 nor higher.

        """
        pipe = self.session.client
        if self.session.current_transaction is None:
            pipe = pipe.pipeline()
        self._raw_extend(iterable, pipe)
        if self.session.current_transaction is None:
            pipe.execute()

    @manipulative
    def _raw_extend(self, iterable, pipe, encoded=False):
        encode = self.value_type.encode
        if self.session.server_version_info < (2, 4, 0):
            if encoded:
                for val in iterable:
                    pipe.rpush(self.key, val)
            else:
                for val in iterable:
                    pipe.rpush(self.key, encode(val))
        else:
            if not encoded:
                iterable = (encode(v) for v in iterable)
            n = 100  # FIXME: it is an arbitarary magic number.
            for chunk in utils.chunk(iterable, n):
                pipe.rpush(self.key, *chunk)

    def insert(self, index, value):
        """Inserts the ``value`` right after the offset ``index``.

        :param index: the offset of the next before the place
                      where ``value`` would be inserted to
        :type index: :class:`numbers.Integral`
        :param value: the value to insert
        :raises exceptions.TypeError:
           if the given ``index`` is not an integer

        .. warning::

           Redis does not provide any primitive operations for random
           insertion.  You only can prepend or append a value into lists.
           If index is 0 it'll send :redis:`LPUSH` command, but otherwise
           it inefficiently :redis:`LRANGE` the whole list to manipulate
           it in offline, and then :redis:`DEL` the key so that empty
           the whole list, and then :redis:`RPUSH` the whole result again.
           Moreover all the commands execute in a transaction.

           So you should not treat this method as the same method of
           Python built-in :class:`list` object.  It is just for being
           compatible to :class:`collections.MutableSequence` protocol.

           If it faced the case, it also will warn you
           :class:`~sider.warnings.PerformanceWarning`.

        """
        if not isinstance(index, numbers.Integral):
            raise TypeError('index must be an integer, not ' + repr(index))
        data = self.value_type.encode(value)
        if index == 0:
            self.session.mark_manipulative([self.key])
            self.session.client.lpush(self.key, data)
        else:
            cls = type(self)
            warnings.warn(
                '{0}.{1}.insert() may cause performance issues when index is '
                'not 0 nor -1'.format(cls.__module__, cls.__name__),
                category=PerformanceWarning, stacklevel=2
            )
            def block(trial, transaction):
                pipe = self.session.client
                self.session.mark_query()
                list_ = pipe.lrange(self.key, 0, -1)
                list_.insert(index, data)
                self.session.mark_manipulative()
                pipe.delete(self.key)
                self._raw_extend(list_, pipe, encoded=True)
            self.session.transaction(block, [self.key], ignore_double=True)

    def pop(self, index=-1, _stacklevel=1):
        """Removes and returns an item at ``index``.  Negative index means
        ``len(list) + index`` (counts from the last).

        :param index: an index of an item to remove and return
        :type index: :class:`numbers.Integral`
        :param _stacklevel: internal use only. base stacklevel for warning.
                            default is 1
        :type _stacklevel: :class:`numbers.Integral`
        :returns: the removed element
        :raises exceptions.IndexError:
           if the given ``index`` doesn't exist
        :raises exceptions.TypeError:
           if the given ``index`` is not an integer

        .. warning::

           Redis doesn't offer any primitive operations for random deletion.
           You can pop only the last or the first.  Other middle elements
           cannot be popped in a command, so it emulates the operation
           inefficiently.

           Internal emulation routine to pop an other index than -1 or 0
           consists of three commands in a transaction:

           - :redis:`LINDEX`
           - :redis:`LTRIM`
           - :redis:`RPUSH` (In worst case, this command would be sent
             N times where N is the cardinality of elements placed after
             popped index.  Because multiple operands for :redis:`RPUSH`
             was supported since Redis 2.4.0.)

           So you should not treat this method as the same method of
           Python built-in :class:`list` object.  It is just for being
           compatible to :class:`collections.MutableSequence` protocol.

           If it faced the case, it also will warn you
           :class:`~sider.warnings.PerformanceWarning`.

        """
        client = self.session.client
        if not isinstance(index, numbers.Integral):
            raise TypeError('index must be an integer, not ' + repr(index))
        elif index == 0:
            if self.session.current_transaction is None:
                popped = client.lpop(self.key)
            else:
                self.session.mark_query([self.key])
                popped = client.lindex(self.key, 0)
                self.session.mark_manipulative()
                client.ltrim(self.key, 1, -1)
        elif index == -1:
            if self.session.current_transaction is None:
                popped = client.rpop(self.key)
            else:
                self.session.mark_query([self.key])
                popped = client.lindex(self.key, -1)
                self.session.mark_manipulative()
                client.ltrim(self.key, 0, -2)
        else:
            cls = type(self)
            warnings.warn(
                '{0}.{1}.pop() may cause performance issues when index '
                'is not 0 nor -1'.format(cls.__module__, cls.__name__),
                category=PerformanceWarning, stacklevel=_stacklevel + 1
            )
            result = [None]
            def block(trial, transaction):
                pipe = self.session.client
                self.session.mark_query([self.key])
                popped = pipe.lindex(self.key, index)
                if popped is None:
                    raise IndexError(index)
                result[0] = popped
                tail = pipe.lrange(self.key, index + 1, -1)
                self.session.mark_manipulative()
                pipe.ltrim(self.key, 0, index - 1)
                self._raw_extend(tail, pipe, encoded=True)
            self.session.transaction(block, [self.key], ignore_double=True)
            popped = result[0]
        if popped is None:
            raise IndexError(index)
        return self.value_type.decode(popped)

    @query
    def __repr__(self):
        def get_50():
            values = self.session.client.lrange(self.key, 0, 20)
            length = len(values)
            decode = self.value_type.decode
            for i, value in enumerate(values):
                if i < 20 or i == 20 and length <= 20:
                    yield repr(decode(value))
                else:
                    yield '...'
        cls = type(self)
        elements = ', '.join(get_50())
        return '<{0}.{1} ({2!r}) [{3}]>'.format(cls.__module__, cls.__name__,
                                                self.key, elements) class Hash(collections.MutableMapping):
    """The Python-side representaion of Redis hash value.  It behaves
    such as built-in Python :class:`dict` object.  More exactly, it
    implements :class:`collections.MutableMapping` protocol.

    .. table:: Mappings of Redis commands--:class:`Hash` methods

       ===================== ===========================================
       Redis commands        :class:`Hash` methods
       ===================== ===========================================
       :redis:`DEL`          :meth:`Hash.clear()`
       :redis:`HDEL`         :keyword:`del` (:meth:`Hash.__delitem__()`)
       :redis:`HEXISTS`      :keyword:`in` (:meth:`Hash.__contains__()`)
       :redis:`HGET`         :meth:`Hash.__getitem__()`,
                             :meth:`Hash.get()`
       :redis:`HGETALL`      :meth:`Hash.items()`
       :redis:`HINCRBY`      N/A
       :redis:`HINCRBYFLOAT` N/A
       :redis:`HKEYS`        :func:`iter()` (:meth:`Hash.__iter__()`),
                             :meth:`Hash.keys()`
       :redis:`HLEN`         :func:`len()` (:meth:`Hash.__len__()`)
       :redis:`HMGET`        N/A
       :redis:`HMSET`        :meth:`Hash.update()`
       :redis:`HSET`         :token:`=` (:meth:`Hash.__setitem__()`)
       :redis:`HSETNX`       :meth:`Hash.setdefault()`
       :redis:`HVALS`        :meth:`Hash.values()`
       N/A                   :meth:`Hash.pop()`
       N/A                   :meth:`Hash.popitem()`
       ===================== ===========================================

    """

    #: (:class:`sider.types.Bulk`) The type of hash keys.
    key_type = None

    #: (:class:`sider.types.Bulk`) The type of hash values.
    value_type = None

    def __init__(self, session, key,
                 key_type=String, value_type=String):
        if not isinstance(session, Session):
            raise TypeError('session must be a sider.session.Session '
                            'instance, not ' + repr(session))
        self.session = session
        self.key = key
        self.key_type = Bulk.ensure_value_type(key_type, parameter='key_type')
        self.value_type = Bulk.ensure_value_type(value_type,
                                                 parameter='value_type')

    @query
    def __iter__(self):
        """Iterates over its :meth:`keys()`.

        :returns: the iterator which yields its keys
        :rtype: :class:`collections.Iterator`

        .. note::

           It is directly mapped to Redis :redis:`HKEYS` command.

        """
        keys = self.session.client.hkeys(self.key)
        decode = self.key_type.decode
        for key in keys:
            yield decode(key)

    @query
    def __len__(self):
        """Gets the number of items.

        :returns: the number of items
        :rtype: :class:`numbers.Integral`

        .. note::

           It is directly mapped to Redis :redis:`HLEN` command.

        """
        return self.session.client.hlen(self.key)

    @query
    def __contains__(self, key):
        """Tests whether the given ``key`` exists.

        :param key: the key
        :returns: ``True`` if the ``key`` exists or ``False``
        :rtype: :class:`bool`

        .. note::

           It is directly mapped to Redis :redis:`HEXISTS` command.

        """
        try:
            encoded_key = self.key_type.encode(key)
        except TypeError:
            return False
        exists = self.session.client.hexists(self.key, encoded_key)
        return bool(exists)

    @query
    def __getitem__(self, key):
        """Gets the value of the given ``key``.

        :param key: the key to get its value
        :returns: the value of the ``key``
        :raises exceptions.TypeError:
           if the given ``key`` is not acceptable by
           its :attr:`key_type`
        :raises exceptions.KeyError:
           if there's no such ``key``

        .. note::

           It is directly mapped to Redis :redis:`HGET` command.

        """
        field = self.key_type.encode(key)
        value = self.session.client.hget(self.key, field)
        if value is None:
            raise KeyError(key)
        return self.value_type.decode(value)

    @manipulative
    def __setitem__(self, key, value):
        """Sets the ``key`` with the ``value``.

        :param key: the key to set
        :param value: the value to set
        :raises exceptions.TypeError:
           if the given ``key`` is not acceptable by
           its :attr:`key_type` or the given ``value``
           is not acceptable by its :attr:`value_type`

        .. note::

           It is directly mapped to Redis :redis:`HSET` command.

        """
        encoded_key = self.key_type.encode(key)
        encoded_val = self.value_type.encode(value)
        self.session.client.hset(self.key, encoded_key, encoded_val)

    def __delitem__(self, key):
        """Removes the ``key``.

        :param key: the key to delete
        :raises exceptions.TypeError:
           if the given ``key`` is not acceptable by
           its :attr:`key_type`
        :raises exceptions.KeyError:
           if there's no such ``key``

        .. note::

           It is directly mapped to Redis :redis:`HDEL` command.

        """
        session = self.session
        encoded = self.key_type.encode(key)
        if session.current_transaction is None:
            ok = session.client.hdel(self.key, encoded)
        else:
            session.mark_query([self.key])
            ok = session.client.hexists(self.key, encoded)
            if ok:
                session.mark_manipulative()
                session.client.hdel(self.key, encoded)
        if not ok:
            raise KeyError(key)

    def keys(self):
        """Gets its all keys.  Equivalent to :meth:`__iter__()` except
        it returns a :class:`~collections.Set` instead of iterable.
        There isn't any meaningful order of keys.

        :returns: the set of its all keys
        :rtype: :class:`collections.KeysView`

        .. note::

           This method is directly mapped to Redis :redis:`HKEYS`
           command.

        """
        return frozenset(self)

    @query
    def values(self):
        """Gets its all values.  It returns a :class:`list` but
        there isn't any meaningful order of values.

        :returns: its all values
        :rtype: :class:`collections.ValuesView`

        .. note::

           This method is directly mapped to Redis :redis:`HVALS`
           command.

        """
        values = self.session.client.hvals(self.key)
        decode = self.value_type.decode
        for i, val in enumerate(values):
            values[i] = decode(val)
        return values

    @query
    def items(self):
        """Gets its all ``(key, value)`` pairs.
        There isn't any meaningful order of pairs.

        :returns: the set of ``(key, value)`` pairs (:class:`tuple`)
        :rtype: :class:`collections.ItemsView`

        .. note::

           This method is mapped to Redis :redis:`HGETALL` command.

        """
        items = self.session.client.hgetall(self.key)
        decode_key = self.key_type.decode
        decode_value = self.value_type.decode
        return frozenset((decode_key(k), decode_value(items[k]))
                         for k in items)

    @manipulative
    def clear(self):
        """Removes all items from this hash.

        .. note::

           Under the hood it simply :redis:`DEL` the key.

        """
        self.session.client.delete(self.key)

    def setdefault(self, key, default=None):
        """Sets the given ``default`` value to the ``key``
        if it doesn't exist and then returns the current value
        of the ``key``.

        For example, the following code is::

            val = hash.setdefault('key', 'set this if not exist')

        equivalent to::

            try:
                val = hash['key']
            except KeyError:
                val = hash['key'] = 'set this if not exist'

        except :meth:`setdefault()` method is an atomic operation.

        :param key: the key to get or set
        :param default: the value to be set if the ``key``
                        doesn't exist
        :raises exceptions.TypeError:
           when the given ``key`` is not acceptable by its
           :attr:`key_type` or the given ``default`` value
           is not acceptable by its :attr:`value_type`

        .. note::

           This method internally uses Redis :redis:`HSETNX`
           command which is atomic.

        """
        if self.session.transaction is not None:
            self.session.mark_query()
            try:
                val = self[key]
            except KeyError:
                self.session.mark_manipulative([self.key])
                self[key] = val = default
            return val
        encoded_key = self.key_type.encode(key)
        encoded_val = self.value_type.encode(default)
        result = [None]
        def block(pipe):
            ok = pipe.hsetnx(self.key, encoded_key, encoded_val)
            if ok:
                result[0] = default
            else:
                value = pipe.hget(self.key, encoded_key)
                result[0] = self.value_type.decode(value)
            pipe.multi()
        self.session.client.transaction(block, self.key)
        return result[0]

    @manipulative
    def update(self, mapping={}, **keywords):
        """Updates the hash from the given ``mapping`` and keyword
        arguments.

        - If ``mapping`` has :meth:`~collections.Mapping.keys()`
          method, it does::

              for k in mapping:
                  self[k] = mapping[k]

        - If ``mapping`` lacks :meth:`~collections.Mapping.keys()` 
          method, it does::

              for k, v in mapping:
                  self[k] = v

        - In either case, this is followed by (where ``keywords``
          is a :class:`dict` of keyword arguments)::

              for k, v in keywords.items():
                  self[k] = v

        :param mapping: an iterable object of ``(key, value)``
                        pairs or a mapping object (which has
                        :meth:`~collections.Mapping.keys()`
                        method).  default is empty
        :type mapping: :class:`collections.Mapping`
        :param \*\*keywords: the keywords to update as well.
                             if its :attr:`key_type` doesn't
                             accept byte strings (:class:`str`)
                             it raises :exc:`~exceptions.TypeError`
        :raises exceptions.TypeError:
           if the ``mapping`` is not actually mapping or iterable,
           or the given keys and values to update aren't acceptable
           by its :attr:`key_type` and :attr:`value_type`
        :raises exceptions.ValueError:
           if the ``mapping`` is an iterable object which yields
           non-pair values e.g. ``[(1, 2, 3), (4,)]``

        """
        if isinstance(mapping, Hash):
            mapping = mapping.items()
        value = dict(mapping)
        value.update(keywords)
        pipe = self.session.client
        if self.session.current_transaction is None:
            pipe = pipe.pipeline()
        self._raw_update(value, pipe)
        if self.session.current_transaction is None:
            pipe.execute()

    def _raw_update(self, value, pipe, encoded=False):
        items = getattr(value, 'iteritems', value.items)()
        if encoded:
            flatten = (val for k, v in items for val in (k, v))
        else:
            encode_key = self.key_type.encode
            encode_value = self.value_type.encode
            flatten = (val for k, v in items
                           for val in (encode_key(k), encode_value(v)))
        n = 100  # FIXME: it is an arbitarary magic number.
        for chunk in utils.chunk(flatten, n * 2):
            pipe.execute_command('HMSET', self.key, *chunk)

    def __repr__(self):
        cls = type(self)
        items = list(self.items())
        items.sort(key=lambda elem: elem[0])
        elements = ', '.join('{0!r}: {1!r}'.format(*pair) for pair in items)
        return '<{0}.{1} ({2!r}) {{{3}}}>'.format(cls.__module__, cls.__name__,
                                                  self.key, elements)
import warnings
from redis.client import StrictRedis, Redis
from pytest import raises
from .env import NInt, get_client, key
from .env import session
from sider.session import Session
from sider.types import (Set as SetT,
                         List as ListT,
                         Hash as HashT,
                         SortedSet as SortedSetT,
                         Integer)
from sider.set import Set
from sider.list import List
from sider.hash import Hash
from sider.sortedset import SortedSet


class CustomRedis(StrictRedis):
    """A custom subclass of StrictRedis for test."""


def ensure_encoding_error(excinfo):
    """Ensure that given error is raised from :meth:`~Bulk.encode()`

    .. seealso:: <https://gist.github.com/Kroisse/5211709>

    """
    assert 'argument after * must be a sequence' not in str(excinfo.value), \
        'Ensure to not use an iterable object as a variadic arugments'
    assert excinfo.traceback[-1].name == 'encode'


def test_warn_old_client():
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter('always')

        Session(get_client(cls=CustomRedis))