"""Misc. class tests. These are more general class tests than CPython's
test_class which focuses on operators.

Made for Jython
"""
import __builtin__
import types
import unittest
from java.lang import Object
from test import test_support

class ClassGeneralTestCase(unittest.TestCase):

    TE_MSG = "can't set attributes of built-in/extension type 'str'"

    def test_dunder_module(self):
        self.assertEqual(str.__module__, '__builtin__')
        class Foo:
            pass
        Fu = types.ClassType('Fu', (), {})
        for cls in Foo, Fu:
            self.assert_('__module__' in cls.__dict__)
            self.assertEqual(cls.__module__, __name__)
            self.assertEqual(str(cls), '%s.%s' % (__name__, cls.__name__))
            self.assert_(repr(cls).startswith('<class %s.%s at' %
                                              (__name__, cls.__name__)))
            obj = cls()
            self.assert_(str(obj).startswith('<%s.%s instance at' %
                                             (__name__, cls.__name__)))

        class Bar(object):
            pass
        class Baz(Object):
            pass
        Bang = type('Bang', (), {})
        for cls in Bar, Baz, Bang:
            self.assert_('__module__' in cls.__dict__)
            self.assertEqual(cls.__module__, __name__)
            self.assertEqual(str(cls), "<class '%s.%s'>" % (__name__, cls.__name__))
            self.assertEqual(repr(cls), "<class '%s.%s'>" % (__name__, cls.__name__))
        self.assert_(str(Bar()).startswith('<%s.Bar object at' % __name__))
        self.assert_(str(Baz()).startswith("org.python.proxies.%s$Baz" % __name__))

    def test_builtin_attributes(self):
        for attr, val in dict(__name__='foo', __module__='bar', __dict__={},
                              __flags__=1, __base__=object,
                              __bases__=(unicode, object),
                              __mro__=(unicode, object)).iteritems():
            try:
                setattr(str, attr, val)
            except TypeError, te:
                self.assertEqual(str(te), self.TE_MSG)
            else:
                self.assert_(False,
                             'setattr str.%s expected a TypeError' % attr)
            try:
                delattr(str, attr)
            except TypeError, te:
                self.assertEqual(str(te), self.TE_MSG)
            else:
                self.assert_(False,
                             'delattr str.%s expected a TypeError' % attr)


    def test_attributes(self):
        class Foo(object):
            pass

        Foo.__name__ = 'Bar'
        self.assertEqual(Foo.__name__, 'Bar')
        try:
            del Foo.__name__
        except TypeError, te:
            self.assertEqual(str(te), "can't delete Bar.__name__")
        else:
            self.assert_(False, 'Expected a TypeError')

        Foo.__module__ = 'baz'
        self.assertEqual(Foo.__module__, 'baz')
        try:
            del Foo.__module__
        except TypeError, te:
            self.assertEqual(str(te), "can't delete Bar.__module__")
        else:
            self.assert_(False, 'Expected a TypeError')

        try:
            Foo.__dict__ = {}
        except AttributeError, ae:
            self.assertEqual(str(ae),
                             "attribute '__dict__' of 'type' objects is not "
                             "writable")
        else:
            self.assert_(False, 'Expected an AttributeError')
        try:
            del Foo.__dict__
        except AttributeError, ae:
            self.assertEqual(str(ae),
                             "attribute '__dict__' of 'type' objects is not "
                             "writable")
        else:
            self.assert_(False, 'Expected an AttributeError')

        for attr, val in dict(__flags__=1, __base__=object,
                              __bases__=(unicode, object),
                              __mro__=(unicode, object)).iteritems():
            try:
                setattr(str, attr, val)
            except TypeError, te:
                self.assertEqual(str(te), self.TE_MSG)
            else:
                self.assert_(False,
                             'setattr Foo.%s expected a TypeError' % attr)
            try:
                delattr(str, attr)
            except TypeError, te:
                self.assertEqual(str(te), self.TE_MSG)
            else:
                self.assert_(False,
                             'delattr Foo.%s expected a TypeError' % attr)

    def test_newstyle_new_classobj(self):
        # Ensure new.classobj can create new style classes
        class Foo(object):
            pass
        def hello(self):
            return 'hello'
        Bar = types.ClassType('Bar', (Foo,), dict(hello=hello))
        self.assert_(type(Bar), type)
        self.assert_(issubclass(Bar, Foo))
        self.assert_(hasattr(Bar, 'hello'))
        self.assertEquals(Bar().hello(), 'hello')

    def test_attribute_error_message(self):
        # Ensure that AttributeError matches the CPython message
        class Bar:
            pass
        try:
            Bar.bar
            self._assert(False) # The previous line should have raised
                                # AttributeError
        except AttributeError, e:
            self.assertEqual("class Bar has no attribute 'bar'", str(e))

        class Foo(object):
            pass
        try:
            Foo.bar
            self._assert(False) # The previous line should have raised
                                # AttributeError
        except AttributeError, e:
            self.assertEqual("type object 'Foo' has no attribute 'bar'",
                             str(e))

    def test_inner_class_dict(self):
        class z:
            class t:
                def moo(self):
                    pass
        # Printing this caused an NPE in Jython 2.1
        keys = list(z.t.__dict__)
        keys.sort()
        self.assertEqual(str(keys), "['__doc__', '__module__', 'moo']")

    def test_metaclass_and_slotted_base(self):
        class Meta(type):
            pass
        class SlottedBase(object):
            __slots__ = 'foo'
        # A regression up until 2.5a3: Defining Bar would cause a
        # TypeError "mro() returned base with unsuitable layout ('Bar')"
        class Bar(SlottedBase):
            __metaclass__ = Meta

    def test_slotted_diamond_problem_bug(self):
        class A(object):
            __slots__ = 'foo'
        class B(A):
            pass
        class C(A):
            pass
        # used to raise TypeError: multiple bases have instance lay-out
        # conflict
        class D(B, C):
            pass

    def test_getitem_exceptions(self):
        class A:
            def __getitem__(self, key):
                raise IndexError, "Fraid not"
        self.assertRaises(IndexError, A().__getitem__, 'b')

    def test_winning_metatype(self):
        class Meta(type):
            def __new__(cls, name, bases, attrs):
                attrs['spam'] = name
                attrs['counter'] = 0
                return type.__new__(cls, name, bases, attrs)
            def __init__(cls, name, bases, attrs):
                cls.counter += 1

        class Base(object):
            __metaclass__ = Meta
        Foo = type('Foo', (Base,), {})
        # Previously we called the wrong __new__
        self.assertEqual(Foo.spam, 'Foo')
        # and called __init__ twice
        self.assertEqual(Foo.counter, 1)

class ClassNamelessModuleTestCase(unittest.TestCase):

    def setUp(self):
        global __name__
        self.name = __name__
        del __name__

    def tearDown(self):
        global __name__
        __name__ = self.name

    def test_nameless_module(self):
        class Foo:
            pass
        self.assertEqual(Foo.__module__, '__builtin__')
        self.assertEqual(str(Foo), '__builtin__.Foo')
        self.assert_(repr(Foo).startswith('<class __builtin__.Foo at'))
        foo = Foo()
        self.assert_(str(foo).startswith('<__builtin__.Foo instance at'))

        class Bar(object):
            pass
        self.assertEqual(Bar.__module__, '__builtin__')
        self.assertEqual(str(Bar), "<class 'Bar'>")
        self.assertEqual(repr(Bar), "<class 'Bar'>")
        bar = Bar()
        self.assert_(str(bar).startswith('<Bar '))
        self.assert_(repr(bar).startswith('<Bar object at'))


class BrokenNameTestCase(unittest.TestCase):

    def setUp(self):
        global __name__
        self.name = __name__
        self.builtin_name = __builtin__.__name__
        del __name__
        del __builtin__.__name__

    def tearDown(self):
        global __name__
        __builtin__.__name__ = self.builtin_name
        __name__ = self.name

    def test_broken_name(self):
        try:
            class Foobar:
                pass
        except NameError:
            pass
        else:
            self.assert_(False, "Expected a NameError")


class ClassLocalsTestCase(unittest.TestCase):

    def test_class_locals(self):
        class Foo(object):
            pass

        class Bar(object):
            foo = Foo()
        self.assert_(not hasattr(Bar, 'Foo'))

        class Bar2(object):
            foo = Foo()
            locals()
        # Observer effect: Bar2 differs because we looked at
        # locals. This might be considered 'buggy' behavior; but it
        # matches CPython and Pypy. see below for an example
        self.assert_(hasattr(Bar2, 'Foo'))

    def test_class_locals_realworld(self):
        # A more real world test of the above situation, for reference
        class FieldGathererMeta(type):
            def __new__(meta, name, bases, class_dict):
                cls = type.__new__(meta, name, bases, class_dict)
                cls.fields = [field.upper() for field in class_dict.iterkeys() \
                                  if not field.startswith('_')]
                cls.fields.sort()
                return cls

        class SomeClass(object):
            pass

        class MyFields(object):
            __metaclass__ = FieldGathererMeta
            jython = 'foo'
            java = ('bar', SomeClass())
        # Technically SomeClass and FieldGathererMeta are actually
        # locals in the MyFields' class definition scope, but we expect
        # them to be omitted from its class_dict
        self.assertEqual(MyFields.fields, ['JAVA', 'JYTHON'])

        class MyFields2(object):
            __metaclass__ = FieldGathererMeta
            jython = 'foo'
            java = ('bar', SomeClass())
            locals()
        # Oops, locals() updates f_locals. Hilarity ensues
        self.assertEqual(MyFields2.fields, ['FIELDGATHERERMETA', 'JAVA',
                                            'JYTHON', 'SOMECLASS'])

    def test___doc__(self):
        class Test(object):
            """doc"""
            test = 'Test %s' % __doc__
        self.assertEqual(Test.test, 'Test doc')


class IsDescendentTestCase(unittest.TestCase):

    def test_newstyle_descendent_of_oldstyle(self):
        class NewStyle(object):
            pass
        class OldStyle:
            pass
        class Retro(NewStyle, OldStyle):
            pass
        self.assert_(issubclass(Retro, NewStyle))
        self.assert_(issubclass(Retro, OldStyle))
        retro = Retro()
        self.assert_(isinstance(retro, NewStyle))
        self.assert_(isinstance(retro, OldStyle))


class JavaClassNamingTestCase(unittest.TestCase):

    """Tests for PyJavaClass naming."""

    def test_java_class_name(self):
        # The __name__ and __module__ attributes of Java classes should
        # be set according to the same convention that Python uses.
        from java.lang import String
        self.assertEqual(String.__name__, "String")
        self.assertEqual(String.__module__, "java.lang")


module_name = __name__

class ClassDefinesDunderModule(unittest.TestCase):

    """Verifies http://bugs.jython.org/issue1022 is fixed"""

    def test_dundermodule_in_classdef(self):
        class Foo:
            self.assertEqual(__module__, module_name)
        class Bar(object):
            self.assertEqual(__module__, module_name)

    def test_dundermodule_in_class_dict_copy(self):
        class_dict = {'a': 'this is a', 'b': 'this is b'}
        Foo = type.__new__(type, 'Foo', (object,), class_dict)
        Foo.keys = class_dict.keys
        assert sorted(Foo().keys()) == sorted(['a', 'b']), sorted(Foo().keys())


class ClassMetaclassRepr(unittest.TestCase):

    def test_repr_with_metaclass(self):
        # http://bugs.jython.org/issue1131
        class FooMetaclass(type):
            def __new__(cls, name, bases, attrs):
                return super(FooMetaclass, cls).__new__(cls, name, bases, attrs)

        class Foo(object):
            __metaclass__ = FooMetaclass
        self.assertEqual("<class '%s.Foo'>" % __name__, repr(Foo))

    def test_metaclass_str(self):
        class Foo(type):
            def __repr__(cls):
                return 'foo'
        class Bar(object):
            __metaclass__ = Foo
        self.assertEqual(repr(Bar), 'foo')
        # type.__str__ previously broke this
        self.assertEqual(str(Bar), 'foo')


class LenTestCase(unittest.TestCase):
    """__len__ of new-style classes should raise OverflowError if length is too long"""

    # Verifies fix for http://bugs.jython.org/issue1929

    def test_len(self):
        class C(object):
            def __len__(self):
                return 2 ** 70
        with self.assertRaises(OverflowError) as cm:
            len(C())
        self.assertEqual(str(cm.exception), "long int too large to convert to int")

        class D(object):
            def __len__(self):
                return "foo"
        with self.assertRaises(TypeError) as cm:
            len(D())
        self.assertEqual(str(cm.exception), "an integer is required")

    def test_len_faux_int(self):
        class C(object):
            def __len__(self):
                class FauxInt(object):
                    def __int__(self):
                        return 2 ** 70
                return FauxInt()
        with self.assertRaises(OverflowError) as cm:
            len(C())
        self.assertEqual(str(cm.exception), "long int too large to convert to int")

    def test_len_derived_int(self):
        class C(object):
            def __len__(self):
                class MyInt(int):
                    pass
                return MyInt(2 ** 70)
        with self.assertRaises(OverflowError) as cm:
            len(C())
        self.assertEqual(str(cm.exception), "long int too large to convert to int")


def test_main():
    test_support.run_unittest(
        ClassGeneralTestCase,
        ClassNamelessModuleTestCase,
        BrokenNameTestCase,
        ClassLocalsTestCase,
        IsDescendentTestCase,
        JavaClassNamingTestCase,
        ClassDefinesDunderModule,
        ClassMetaclassRepr,
        LenTestCase)


if __name__ == "__main__":
    test_main()
