"""Test descriptors, binary ops, etc.

Made for Jython.
"""
import types
import unittest
from test import test_support

class Old:
    pass


class New(object):
    pass


old = Old()
new = New()


class TestDescrTestCase(unittest.TestCase):

    def test_class_dict_is_copy(self):
        class FooMeta(type):
            def __new__(meta, name, bases, class_dict):
                cls = type.__new__(meta, name, bases, class_dict)
                self.assert_('foo' not in class_dict)
                cls.foo = 'bar'
                self.assert_('foo' not in class_dict)
                return cls

        class Foo(object):
            __metaclass__ = FooMeta

    def test_descr___get__(self):
        class Foo(object):
            __slots__ = 'bar'
            def hello(self):
                pass
            def hi(self):
                pass
            hi = staticmethod(hi)
        foo = Foo()
        foo.bar = 'baz'

        self.assertEqual(Foo.bar.__get__(foo), 'baz')
        self.assertEqual(Foo.bar.__get__(None, Foo), Foo.bar)

        bound = Foo.hello.__get__(foo)
        self.assert_(isinstance(bound, types.MethodType))
        self.assert_(bound.im_self is foo)
        self.assertEqual(Foo.hello.__get__(None, Foo), Foo.hello)

        bound = Foo.hi.__get__(foo)
        self.assert_(isinstance(bound, types.MethodType))
        self.assert_(bound.im_self is foo)
        unbound = Foo.hi.__get__(None, foo)
        self.assert_(isinstance(unbound, types.MethodType))
        self.assert_(unbound.im_self is None)

    def test_ints(self):
        class C(int):
            pass
        try:
            foo = int(None)
        except TypeError:
            pass
        else:
            self.assert_(False, "should have raised TypeError")
        try:
            foo = C(None)
        except TypeError:
            pass
        else:
            self.assert_(False, "should have raised TypeError")

    def test_raising_custom_attribute_error(self):
        class RaisesCustomMsg(object):
            def __get__(self, instance, type):
                raise AttributeError("Custom message")


        class CustomAttributeError(AttributeError): pass

        class RaisesCustomErr(object):
            def __get__(self, instance, type):
                raise CustomAttributeError

        class Foo(object):
            custom_msg = RaisesCustomMsg()
            custom_err = RaisesCustomErr()

        self.assertRaises(CustomAttributeError, lambda: Foo().custom_err)
        try:
            Foo().custom_msg
            self.assert_(False) # Previous line should raise AttributteError
        except AttributeError, e:
            self.assertEquals("Custom message", str(e))

    def test_set_without_get(self):
        class Descr(object):

            def __init__(self, name):
                self.name = name

            def __set__(self, obj, value):
                obj.__dict__[self.name] = value
        descr = Descr("a")

        class X(object):
            a = descr

        x = X()
        self.assertTrue(x.a is descr)
        x.a = 42
        self.assertEqual(x.a, 42)


class SubclassDescrTestCase(unittest.TestCase):

    def test_subclass_cmp_right_op(self):
        # Case 1: subclass of int

        class B(int):
            def __ge__(self, other):
                return "B.__ge__"
            def __le__(self, other):
                return "B.__le__"

        self.assertEqual(B(1) >= 1, "B.__ge__")
        self.assertEqual(1 >= B(1), "B.__le__")

        # Case 2: subclass of object

        class C(object):
            def __ge__(self, other):
                return "C.__ge__"
            def __le__(self, other):
                return "C.__le__"

        self.assertEqual(C() >= 1, "C.__ge__")
        self.assertEqual(1 >= C(), "C.__le__")

        # Case 3: subclass of new-style class; here it gets interesting

        class D(C):
            def __ge__(self, other):
                return "D.__ge__"
            def __le__(self, other):
                return "D.__le__"

        self.assertEqual(D() >= C(), "D.__ge__")
        self.assertEqual(C() >= D(), "D.__le__")

        # Case 4: comparison is different than other binops

        class E(C):
            pass

        self.assertEqual(E.__le__, C.__le__)

        self.assertEqual(E() >= 1, "C.__ge__")
        self.assertEqual(1 >= E(), "C.__le__")
        self.assertEqual(E() >= C(), "C.__ge__")
        self.assertEqual(C() >= E(), "C.__le__") # different

    def test_subclass_binop(self):
        def raises(exc, expected, callable, *args):
            try:
                callable(*args)
            except exc, msg:
                if str(msg) != expected:
                    self.assert_(False, "Message %r, expected %r" % (str(msg),
                                                                     expected))
            else:
                self.assert_(False, "Expected %s" % exc)

        class B(object):
            pass

        class C(object):
            def __radd__(self, o):
                return '%r + C()' % (o,)

            def __rmul__(self, o):
                return '%r * C()' % (o,)

        # Test strs, unicode, lists and tuples
        mapping = []

        # + binop
        mapping.append((lambda o: 'foo' + o,
                        TypeError, "cannot concatenate 'str' and 'B' objects",
                        "'foo' + C()"))
        # XXX: There's probably work to be done here besides just emulating this
        # message
        if test_support.is_jython:
            mapping.append((lambda o: u'foo' + o,
                            TypeError, "cannot concatenate 'unicode' and 'B' objects",
                            "u'foo' + C()"))
        else:
            mapping.append((lambda o: u'foo' + o,
                            TypeError,
                            'coercing to Unicode: need string or buffer, B found',
                            "u'foo' + C()"))
        mapping.append((lambda o: [1, 2] + o,
                        TypeError, 'can only concatenate list (not "B") to list',
                        '[1, 2] + C()'))
        mapping.append((lambda o: ('foo', 'bar') + o,
                        TypeError, 'can only concatenate tuple (not "B") to tuple',
                        "('foo', 'bar') + C()"))

        # * binop
        mapping.append((lambda o: 'foo' * o,
                        TypeError, "can't multiply sequence by non-int of type 'B'",
                        "'foo' * C()"))
        mapping.append((lambda o: u'foo' * o,
                        TypeError, "can't multiply sequence by non-int of type 'B'",
                        "u'foo' * C()"))
        mapping.append((lambda o: [1, 2] * o,
                        TypeError, "can't multiply sequence by non-int of type 'B'",
                        '[1, 2] * C()'))
        mapping.append((lambda o: ('foo', 'bar') * o,
                        TypeError, "can't multiply sequence by non-int of type 'B'",
                        "('foo', 'bar') * C()"))

        for func, bexc, bexc_msg, cresult in mapping:
            raises(bexc, bexc_msg, lambda : func(B()))
            self.assertEqual(func(C()), cresult)

    def test_overriding_base_binop(self):
        class MulBase(object):
            def __init__(self, value):
                self.value = value
            def __mul__(self, other):
                return self.value * other.value
            def __rmul__(self, other):
                return other.value * self.value
        class DoublerBase(MulBase):
            def __mul__(self, other):
                return 2 * (self.value * other.value)
        class AnotherDoubler(DoublerBase):
            pass
        self.assertEquals(DoublerBase(2) * AnotherDoubler(3), 12)

    def test_oldstyle_binop_notimplemented(self):
        class Foo:
            pass
        class Bar(object):
            def __radd__(self, other):
                return 3
        self.assertEqual(Foo() + Bar(), 3)

    def test_int_mul(self):
        # http://bugs.jython.org/issue1332
        class Foo(tuple):
            def __rmul__(self, other):
                return 'foo'
        foo = Foo()
        self.assertEqual(3.0 * foo, 'foo')
        self.assertEqual(4 * foo, 'foo')


class InPlaceTestCase(unittest.TestCase):

    def test_iadd(self):
        class Foo(object):
            def __add__(self, other):
                return 1
            def __radd__(self, other):
                return 2
        class Bar(object):
            pass
        class Baz(object):
            def __iadd__(self, other):
                return NotImplemented
        foo = Foo()
        foo += Bar()
        self.assertEqual(foo, 1)
        bar = Bar()
        bar += Foo()
        self.assertEqual(bar, 2)
        baz = Baz()
        baz += Foo()
        self.assertEqual(baz, 2)

    def test_imul(self):
        class FooInplace(list):
            def __imul__(self, other):
                return [1]
        class Bar(FooInplace):
            def __mul__(self, other):
                return [2]
        foo = FooInplace()
        foo *= 3
        self.assertEqual(foo, [1])
        foo = Bar([3])
        foo *= 3
        self.assertEqual(foo, [1])

        class Baz(FooInplace):
            def __mul__(self, other):
                return [3]
        baz = Baz()
        baz *= 3
        self.assertEqual(baz, [1])

    def test_list(self):
        class Foo(list):
            def __mul__(self, other):
                return [1]
        foo = Foo([2])
        foo *= 3
        if test_support.is_jython:
            self.assertEqual(foo, [2, 2, 2])
        else:
            # CPython ignores list.__imul__ on a subclass with __mul__
            # (unlike Jython and PyPy)
            self.assertEqual(foo, [1])

        class Bar(object):
            def __radd__(self, other):
                return 1
            def __rmul__(self, other):
                return 2
        l = []
        l += Bar()
        self.assertEqual(l, 1)
        l = []
        l *= Bar()
        self.assertEqual(l, 2)

    def test_iand(self):
        # Jython's set __iand__ (as well as isub, ixor, etc) was
        # previously broken
        class Foo(set):
            def __and__(self, other):
                return set([1])
        foo = Foo()
        foo &= 3
        self.assertEqual(foo, set([1]))


class DescrExceptionsTestCase(unittest.TestCase):

    def test_hex(self):
        self._test(hex)

    def test_oct(self):
        self._test(oct)

    def test_other(self):
        for op in '-', '+', '~':
            try:
                eval('%s(old)' % op)
            except AttributeError:
                pass
            else:
                self._assert(False, 'Expected an AttributeError, op: %s' % op)
            try:
                eval('%s(new)' % op)
            except TypeError:
                pass
            else:
                self._assert(False, 'Expected a TypeError, op: %s' % op)

    def _test(self, func):
        self.assertRaises(AttributeError, func, old)
        self.assertRaises(TypeError, func, new)

    def test_eq(self):
        class A(object):
            def __eq__(self, other):
                return self.value == other.value
        self.assertRaises(AttributeError, lambda: A() == A())


class GetAttrTestCase(unittest.TestCase):
    def test_raising_custom_attribute_error(self):
        # Very similar to
        # test_descr_jy.TestDescrTestCase.test_raising_custom_attribute_error
        class BarAttributeError(AttributeError): pass

        class Bar(object):
            def __getattr__(self, name):
                raise BarAttributeError

        class BarClassic:
            def __getattr__(self, name):
                raise BarAttributeError

        class Foo(object):
            def __getattr__(self, name):
                raise AttributeError("Custom message")

        class FooClassic:
            def __getattr__(self, name):
                raise AttributeError("Custom message")

        self.assertRaises(BarAttributeError, lambda: Bar().x)
        self.assertRaises(BarAttributeError, lambda: BarClassic().x)

        try:
            Foo().x
            self.assert_(False) # Previous line should raise AttributteError
        except AttributeError, e:
            self.assertEquals("Custom message", str(e))

        try:
            FooClassic().x
            self.assert_(False) # Previous line should raise AttributteError
        except AttributeError, e:
            self.assertEquals("Custom message", str(e))


class Base(object):
    def __init__(self, name):
        self.name = name


def lookup_where(obj, name):
    mro = type(obj).__mro__
    for t in mro:
        if name in t.__dict__:
            return t.__dict__[name], t
    return None, None


def refop(x, y, opname, ropname):
    # this has been validated by running the tests on top of cpython
    # so for the space of possibilities that the tests touch it is known
    # to behave like cpython as long as the latter doesn't change its own
    # algorithm
    t1 = type(x)
    t2 = type(y)
    op, where1 = lookup_where(x, opname)
    rop, where2 = lookup_where(y, ropname)
    if op is None and rop is not None:
        return rop(y, x)
    if rop and where1 is not where2:
        if (issubclass(t2, t1) and not issubclass(where1, where2) and
            not issubclass(t1, where2)):
            return rop(y, x)
    if op is None:
        return "TypeError"
    return op(x,y)


def do_test(X, Y, name, impl):
    x = X('x')
    y = Y('y')
    opname = '__%s__' % name
    ropname = '__r%s__' % name

    count = [0]
    fail = []

    def check(z1, z2):
        ref = refop(z1, z2, opname, ropname)
        try:
            v = impl(z1, z2)
        except TypeError:
            v = "TypeError"
        if v != ref:
            fail.append(count[0])

    def override_in_hier(n=6):
        if n == 0:
            count[0] += 1
            check(x, y)
            check(y, x)
            return

        f = lambda self, other: (n, self.name, other.name)
        if n % 2 == 0:
            name = opname
        else:
            name = ropname

        for C in Y.__mro__:
            if name in C.__dict__:
                continue
            if C is not object:
                setattr(C, name, f)
            override_in_hier(n - 1)
            if C is not object:
                delattr(C, name)

    override_in_hier()
    #print count[0]
    return fail


class BinopCombinationsTestCase(unittest.TestCase):

    """Try to test more exhaustively binop overriding combination
    cases"""

    def test_binop_combinations_mul(self):
        class X(Base):
            pass
        class Y(X):
            pass

        fail = do_test(X, Y, 'mul', lambda x, y: x*y)
        #print len(fail)
        self.assert_(not fail)

    def test_binop_combinations_sub(self):
        class X(Base):
            pass
        class Y(X):
            pass

        fail = do_test(X, Y, 'sub', lambda x, y: x-y)
        #print len(fail)
        self.assert_(not fail)

    def test_binop_combinations_pow(self):
        class X(Base):
            pass
        class Y(X):
            pass

        fail = do_test(X, Y, 'pow', lambda x, y: x**y)
        #print len(fail)
        self.assert_(not fail)

    def test_binop_combinations_more_exhaustive(self):
        class X(Base):
            pass

        class B1(object):
            pass

        class B2(object):
            pass

        class X1(B1, X, B2):
            pass

        class C1(object):
            pass

        class C2(object):
            pass

        class Y(C1, X1, C2):
            pass

        fail = do_test(X, Y, 'sub', lambda x, y: x - y)
        #print len(fail)
        self.assert_(not fail)


def test_main():
    test_support.run_unittest(TestDescrTestCase,
                              SubclassDescrTestCase,
                              InPlaceTestCase,
                              DescrExceptionsTestCase,
                              GetAttrTestCase,
                              BinopCombinationsTestCase)


if __name__ == '__main__':
    test_main()
