import functools
import sys


PY3K = sys.version_info >= (3,)


methods = set([
    "__iter__",
    "__len__",
    "__contains__",
    "__getitem__",
    "__setitem__",
    "__delitem__",

    "__enter__",
    "__exit__",

    "__lt__",
    "__le__",
    "__eq__",
    "__ne__",
    "__gt__",
    "__ge__",

    "__add__",
    "__and__",
    "__divmod__",
    "__floordiv__",
    "__lshift__",
    "__mod__",
    "__mul__",
    "__or__",
    "__pow__",
    "__rshift__",
    "__sub__",
    "__truediv__",
    "__xor__",

    "__repr__",
])
if PY3K:
    methods.add("__next__")
    methods.add("__bool__")
else:
    methods.add("__div__")
    methods.add("__nonzero__")
MAGIC_METHODS = frozenset(methods)
del methods


def _build_magic_dispatcher(method):
    def inner(self, *args, **kwargs):
        return self.__dict__[method](*args, **kwargs)
    inner.__name__ = method
    return inner


class stub(object):
    _classes_cache = {}

    def __new__(cls, **kwargs):
        magic_methods_present = MAGIC_METHODS.intersection(kwargs)
        if magic_methods_present not in cls._classes_cache:
            attrs = dict(
                (method, _build_magic_dispatcher(method))
                for method in magic_methods_present
            )
            attrs["__module__"] = cls.__module__
            cls._classes_cache[magic_methods_present] = (
                type("stub", (cls,), attrs)
            )
        new_cls = cls._classes_cache[magic_methods_present]
        return super(stub, new_cls).__new__(new_cls)

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

    def __repr__(self):
        return '<stub(%s)>' % ', '.join([
            '%s=%r' % (key, val)
            for key, val in self.__dict__.items()
        ])


def raiser(exc):
    if (
        not (
            isinstance(exc, BaseException) or
            isinstance(exc, type) and issubclass(exc, BaseException)
        )
    ):
        raise TypeError("exc must be either an exception instance or class.")

    def inner(*args, **kwargs):
        raise exc
    return inner


class call(object):
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

    def __eq__(self, other):
        if not isinstance(other, call):
            return NotImplemented
        return self.args == other.args and self.kwargs == other.kwargs

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

    def __hash__(self):
        return hash((
            self.args,
            frozenset(self.kwargs.items())
        ))

    def __repr__(self):
        args = ", ".join(map(repr, self.args))
        kwargs = ", ".join("%s=%r" % (k, v) for k, v in self.kwargs.items())
        comma = ", " if args and kwargs else ""
        return "<call(%s%s%s)>" % (args, comma, kwargs)


def call_recorder(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        inner.calls.append(call(*args, **kwargs))
        return func(*args, **kwargs)
    inner.calls = []
    return inner
