#!/usr/bin/env python

__author__ = "Patrick K. O'Brien <pobrien@orbtech.com>"

import sys
import os

import unittest
from wx.py import introspect

PY3 = sys.version_info[0] == 3

"""
These unittest methods are preferred:
-------------------------------------
self.assert_(expr, msg=None)
self.assertEqual(first, second, msg=None)
self.assertRaises(excClass, callableObj, *args, **kwargs)
self.fail(msg=None)
self.failIf(expr, msg=None)
"""


class ModuleTestCase(unittest.TestCase):

    def test_module(self):
        module = introspect
        self.assert_(module.__author__)
        self.assert_(module.getAllAttributeNames)
        self.assert_(module.getAttributeNames)
        self.assert_(module.getAutoCompleteList)
        self.assert_(module.getBaseObject)
        self.assert_(module.getCallTip)
        self.assert_(module.getConstructor)
        self.assert_(module.getRoot)
        self.assert_(module.rtrimTerminus)


class RtrimTerminusTestCase(unittest.TestCase):

    def test_rtrimTerminus(self):
        values = (
            ('', '', ''),
            ('', None, ''),
            ('', '.', ''),
            ('', '(', ''),

            ('.', '', '.'),
            ('.', None, '.'),
            ('.', '.', '.'),
            ('.', '(', '.'),

            ('(', '', '('),
            ('(', None, '('),
            ('(', '.', '('),
            ('(', '(', '('),

            ('spam', '', 'spam'),
            ('spam', None, 'spam'),
            ('spam', '.', 'spam'),
            ('spam', '(', 'spam'),

            ('spam.', '', 'spam.'),
            ('spam.', None, 'spam.'),
            ('spam.', '.', 'spam.'),
            ('spam.', '(', 'spam.'),

            ('spam(', '', 'spam('),
            ('spam(', None, 'spam('),
            ('spam(', '.', 'spam('),
            ('spam(', '(', 'spam('),

            ('spam.eggs', '.', 'spam.'),
            ('spam.eggs.', '.', 'spam.eggs.'),
            ('spam.eggs(', '(', 'spam.eggs('),
            ('spam.eggs.', '(', 'spam.eggs.'),
            ('spam.eggs(', '.', 'spam.'),

            ('x = spam.', '.', 'x = spam.'),
            ('x = spam.eggs', '.', 'x = spam.'),
            ('x = spam.eggs.', '.', 'x = spam.eggs.'),
            ('x = spam.eggs(', '(', 'x = spam.eggs('),
        )
        for input, terminator, output in values:
            result = introspect.rtrimTerminus(input, terminator)
            self.assertEqual(result, output,
                             ':in: %r :t: %r :out: %r :result: %r' %
                             (input, terminator, output, result))


class GetRootTestCase(unittest.TestCase):

    def _checkRoot(self, input, terminator, output):
        root = introspect.getRoot(command=input, terminator=terminator)
        self.assertEqual(root, output,
                         ':in: %r :t: %r :out: %r :root: %r' %
                         (input, terminator, output, root))

    def test_getRoot(self):
        values = (
            ('', '', ''),
            ('', None, ''),
            ('', '.', ''),
            ('', '(', ''),

            ('.', '', '.'),
            ('.', None, '.'),
            ('.', '.', ''),
            ('.', '(', '.'),

            ('(', '', ''),
            ('(', None, ''),
            ('(', '.', ''),
            ('(', '(', ''),

            ('spam', '', 'spam'),
            ('spam', None, 'spam'),
            ('spam', '.', ''),
            ('spam', '(', 'spam'),

            ('spam.', '', 'spam.'),
            ('spam.', None, 'spam.'),
            ('spam.', '.', 'spam'),
            ('spam.', '(', 'spam.'),

            ('spam(', '', ''),
            ('spam(', None, ''),
            ('spam(', '.', ''),
            ('spam(', '(', 'spam'),

            ('spam.eggs', '.', 'spam'),
            ('spam.eggs.', '.', 'spam.eggs'),
            ('spam.eggs(', '(', 'spam.eggs'),
            ('spam.eggs.', '(', 'spam.eggs.'),
            ('spam.eggs(', '.', 'spam'),

            ('x = spam.', '.', 'spam'),
            ('x = spam.eggs', '.', 'spam'),
            ('x = spam.eggs.', '.', 'spam.eggs'),
            ('x = spam.eggs(', '(', 'spam.eggs'),

            ('for n in range(3):\n    d.', '.', 'd'),
            ('for n in range(3):\n...    d.', '.', 'd'),
        )
        for input, terminator, output in values:
            self._checkRoot(input, terminator, output)

    def test_getRoot_Advanced(self):
        values = (
            ('spam_', '', 'spam_'),
            ('spam_', None, 'spam_'),
            ('spam_', '.', ''),
            ('spam_', '(', 'spam_'),

            ('_spam', '', '_spam'),
            ('_spam', None, '_spam'),
            ('_spam', '.', ''),
            ('_spam', '(', '_spam'),

            ('spam_eggs', '', 'spam_eggs'),
            ('spam_eggs', None, 'spam_eggs'),
            ('spam_eggs', '.', ''),
            ('spam_eggs', '(', 'spam_eggs'),

            ('spam123', '', 'spam123'),
            ('spam123', None, 'spam123'),
            ('spam123', '.', ''),
            ('spam123', '(', 'spam123'),

            ('spam_123', '', 'spam_123'),
            ('spam_123', None, 'spam_123'),
            ('spam_123', '.', ''),
            ('spam_123', '(', 'spam_123'),
        )
        for input, terminator, output in values:
            self._checkRoot(input, terminator, output)

## The original intent was to detect when we were inside a string.
## That has proven to be very difficult, for little benefit.
## The fact that autocomplete or calltips might be triggered inside
## a string is not a big deal. Sometimes it is even helpful.

##    def test_getRoot_InsideStrings(self):
##        values = (
##            ('x = ".', '.', ''),
##            ("x = '.", '.', ''),
##            ('x = """.', '.', ''),
##            ("x = '''.", '.', ''),
##
##            ('x = "(', '(', ''),
##            ("x = '(", '(', ''),
##            ('x = """(', '(', ''),
##            ("x = '''(", '(', ''),
##
##            ('x = "spam', '.', ''),
##            ('x = "spam.', '.', ''),
##            ("x = 'spam.", '.', ''),
##            ('x = """spam.', '.', ''),
##            ("x = '''spam.", '.', ''),
##
##            ('x = "spam', '(', ''),
##            ('x = "spam(', '(', ''),
##            ("x = 'spam(", '(', ''),
##            ('x = """spam(', '(', ''),
##            ("x = '''spam(", '(', ''),
##
##            ('x = "spam.eggs.', '.', ''),
##            ("x = 'spam.eggs.", '.', ''),
##            ('x = """spam.eggs.', '.', ''),
##            ("x = '''spam.eggs.", '.', ''),
##
##            ('x = "spam.eggs(', '(', ''),
##            ("x = 'spam.eggs(", '(', ''),
##            ('x = """spam.eggs(', '(', ''),
##            ("x = '''spam.eggs(", '(', ''),
##        )
##        for input, terminator, output in values:
##            self._checkRoot(input, terminator, output)

    def test_getRoot_EmptyTypes(self):
        values = (
            ("''.", '.', "''"),
            ('"".', '.', '""'),
            ('"""""".', '.', '""""""'),
            ("''''''.", '.', "''''''"),

            ('[].', '.', '[]'),
            ('().', '.', '()'),
            ('{}.', '.', '{}'),

            ('[](', '(', '[]'),
            ('()(', '(', '()'),
            ('{}(', '(', '{}'),

            ("x = ''.", '.', "''"),
            ('x = "".', '.', '""'),
            ('x = """""".', '.', '""""""'),
            ("x = ''''''.", '.', "''''''"),

            ('x = [].', '.', '[]'),
            ('x = ().', '.', '()'),
            ('x = {}.', '.', '{}'),

            ('x = [](', '(', '[]'),
            ('x = ()(', '(', '()'),
            ('x = {}(', '(', '{}'),

            ('print [].', '.', '[]'),
            ('print ().', '.', '()'),
            ('print {}.', '.', '{}'),

            ('print [](', '(', '[]'),
            ('print ()(', '(', '()'),
            ('print {}(', '(', '{}'),

            ("''.attr.", '.', "''.attr"),
            ('"".attr.', '.', '"".attr'),
            ('"""""".attr.', '.', '"""""".attr'),
            ("''''''.attr.", '.', "''''''.attr"),

            ('[].attr.', '.', '[].attr'),
            ('().attr.', '.', '().attr'),
            ('{}.attr.', '.', '{}.attr'),

            ('[].attr(', '(', '[].attr'),
            ('().attr(', '(', '().attr'),
            ('{}.attr(', '(', '{}.attr'),

            ('spam().', '.', ''),
            ('spam_().', '.', ''),
            ('spam5().', '.', ''),
            ('spam[]().', '.', ''),
            ('spam()[].', '.', ''),
            ('spam[]{}.', '.', ''),

            ("spam(''.", '.', "''"),
            ('spam("".', '.', '""'),
            ('spam("""""".', '.', '""""""'),
            ("spam(''''''.", '.', "''''''"),

            ('spam([].', '.', '[]'),
            ('spam(().', '.', '()'),
            ('spam({}.', '.', '{}'),
            ('spam[[].', '.', '[]'),
            ('spam[().', '.', '()'),
            ('spam[{}.', '.', '{}'),
            ('x = {[].', '.', '[]'),
            ('x = {().', '.', '()'),
            ('x = {{}.', '.', '{}'),

            ('spam,[].', '.', '[]'),
            ('spam+[].', '.', '[]'),
            ('spam-[].', '.', '[]'),
            ('spam*[].', '.', '[]'),
            ('spam/[].', '.', '[]'),
            ('spam=[].', '.', '[]'),
            ('spam%[].', '.', '[]'),
            ('spam<[].', '.', '[]'),
            ('spam>[].', '.', '[]'),
            ('spam&[].', '.', '[]'),
            ('spam|[].', '.', '[]'),
            ('spam^[].', '.', '[]'),
            ('spam~[].', '.', '[]'),
            ('spam:[].', '.', '[]'),

            ('spam,().', '.', '()'),
            ('spam+().', '.', '()'),
            ('spam-().', '.', '()'),
            ('spam*().', '.', '()'),
            ('spam/().', '.', '()'),
            ('spam=().', '.', '()'),
            ('spam%().', '.', '()'),
            ('spam<().', '.', '()'),
            ('spam>().', '.', '()'),
            ('spam&().', '.', '()'),
            ('spam|().', '.', '()'),
            ('spam^().', '.', '()'),
            ('spam~().', '.', '()'),
            ('spam:().', '.', '()'),

            ('spam,{}.', '.', '{}'),
            ('spam+{}.', '.', '{}'),
            ('spam-{}.', '.', '{}'),
            ('spam*{}.', '.', '{}'),
            ('spam/{}.', '.', '{}'),
            ('spam={}.', '.', '{}'),
            ('spam%{}.', '.', '{}'),
            ('spam<{}.', '.', '{}'),
            ('spam>{}.', '.', '{}'),
            ('spam&{}.', '.', '{}'),
            ('spam|{}.', '.', '{}'),
            ('spam^{}.', '.', '{}'),
            ('spam~{}.', '.', '{}'),
            ('spam:{}.', '.', '{}'),
        )
        for input, terminator, output in values:
            self._checkRoot(input, terminator, output)


# Support for GetBaseObjectTestCase and GetAttributeNamesTestCase.

class Foo:
    def __init__(self):
        pass

    def __del__(self):
        pass

    def _private(self):
        pass

class Bar:
    pass

class Spam:
    def __call__(self):
        pass

    def foo(self):
        pass

    def bar(spam):
        # It shouldn't matter what we call "self".
        pass

    def eggs(self):
        pass

def ham(eggs):
    pass

class GetBaseObjectTestCase(unittest.TestCase):

    def test_getBaseObject(self):
        spam = Spam()
        eggs = Spam.eggs
        listappend = [].append
        spamda = lambda: None
        values = [
            ('spam', 'spam', 0),
            (123, 123, 0),
            (12.3, 12.3, 0),
            ([], [], 0),
            ((), (), 0),
            ({}, {}, 0),
            # Builtin function.
            (len, len, 0),
            # Builtin method.
            (listappend, listappend, 0),
            # User function.
            (ham, ham, 0),
            # Lambda.
            (spamda, spamda, 0),
            # Class with no init.
            (Bar, Bar, 0),
        ]
        if not PY3:
            values.extend([
            # Byte-compiled code.
            (ham.func_code, ham.func_code, 0),
            # Class with init.
            (Foo, Foo.__init__.im_func, 1),
            # Bound method.
            (spam.foo, spam.foo.im_func, 1),
            # Bound method with self named something else (spam).
            (spam.bar, spam.bar.im_func, 1),
            # Unbound method. (Do not drop the self argument.)
            (eggs, eggs.im_func, 0),
            # Callable instance.
            (spam, spam.__call__.im_func, 1),
            ])
        for object, baseObject, dropSelf in values:
            result = introspect.getBaseObject(object)
            self.assertEqual(result, (baseObject, dropSelf))


class GetAttributeTestCase(unittest.TestCase):
    """Base class for other test case classes."""

    def setUp(self):
        self.values = (
            '__abs__',
            '__add__',
            '__and__',
            '__base__',
            '__bases__',
            '__basicsize__',
            '__builtins__',
            '__call__',
            '__class__',
            '__cmp__',
            '__coerce__',
            '__contains__',
            '__del__',
            '__delattr__',
            '__delitem__',
            '__delslice__',
            '__dict__',
            '__dictoffset__',
            '__div__',
            '__divmod__',
            '__doc__',
            '__eq__',
            '__file__',
            '__flags__',
            '__float__',
            '__floordiv__',
            '__ge__',
            '__get__',
            '__getattr__',
            '__getattribute__',
            '__getitem__',
            '__getslice__',
            '__gt__',
            '__hash__',
            '__hex__',
            '__iadd__',
            '__imul__',
            '__init__',
            '__int__',
            '__invert__',
            '__itemsize__',
            '__iter__',
            '__le__',
            '__len__',
            '__long__',
            '__lshift__',
            '__lt__',
            '__mod__',
            '__module__',
            '__mro__',
            '__mul__',
            '__name__',
            '__ne__',
            '__neg__',
            '__new__',
            '__nonzero__',
            '__oct__',
            '__or__',
            '__path__',
            '__pos__',
            '__pow__',
            '__radd__',
            '__rand__',
            '__rdiv__',
            '__rdivmod__',
            '__reduce__',
            '__repr__',
            '__rfloordiv__',
            '__rlshift__',
            '__rmod__',
            '__rmul__',
            '__ror__',
            '__rpow__',
            '__rrshift__',
            '__rshift__',
            '__rsub__',
            '__rtruediv__',
            '__rxor__',
            '__self__',
            '__setattr__',
            '__setitem__',
            '__setslice__',
            '__str__',
            '__sub__',
            '__subclasses__',
            '__truediv__',
            '__warningregistry__',
            '__weakrefoffset__',
            '__xor__',
            'append',
            'capitalize',
            'center',
            'clear',
            'close',
            'closed',
            'co_argcount',
            'co_cellvars',
            'co_code',
            'co_consts',
            'co_filename',
            'co_firstlineno',
            'co_flags',
            'co_freevars',
            'co_lnotab',
            'co_name',
            'co_names',
            'co_nlocals',
            'co_stacksize',
            'co_varnames',
            'conjugate',
            'copy',
            'count',
            'decode',
            'encode',
            'endswith',
            'expandtabs',
            'extend',
            'fileno',
            'find',
            'flush',
            'func_closure',
            'func_code',
            'func_defaults',
            'func_dict',
            'func_doc',
            'func_globals',
            'func_name',
            'get',
            'has_key',
            'im_class',
            'im_func',
            'im_self',
            'imag',
            'index',
            'insert',
            'isalnum',
            'isalpha',
            'isatty',
            'isdigit',
            'islower',
            'isspace',
            'istitle',
            'isupper',
            'items',
            'iteritems',
            'iterkeys',
            'itervalues',
            'join',
            'keys',
            'ljust',
            'lower',
            'lstrip',
            'mode',
            'mro',
            'name',
            'pop',
            'popitem',
            'real',
            'read',
            'readinto',
            'readline',
            'readlines',
            'remove',
            'replace',
            'reverse',
            'rfind',
            'rindex',
            'rjust',
            'rstrip',
            'seek',
            'setdefault',
            'softspace',
            'sort',
            'split',
            'splitlines',
            'start',
            'startswith',
            'step',
            'stop',
            'strip',
            'swapcase',
            'tell',
            'title',
            'tolist',
            'translate',
            'truncate',
            'update',
            'upper',
            'values',
            'write',
            'writelines',
            'xreadlines',
        )

# Since getAllAttributeNames() calls str(object),
# we need to test for a broken __str__ method.
class BrokenStr:
    def __str__(self):
        raise Exception

brokenStr = BrokenStr()

class GetAttributeNamesTestCase(GetAttributeTestCase):

    def setUp(self):
        GetAttributeTestCase.setUp(self)
        from wx import py
        spam = Spam()
        self.f = open(os.path.join(os.path.dirname(__file__), 'test_introspect.py'))
        self.items = [
            None,
            int(123),
            float(123),
            complex(123),
            "",
            [],
            (),
            {},
            # Builtin function.
            len,
            # Builtin method.
            [].append,
            # User function.
            ham,
            # Lambda.
            lambda: None,
            # Class with no init.
            Bar,
            # Instance with no init.
            Bar(),
            # Class with init and del.
            Foo,
            # Instance with init and del.
            Foo(),
            # Bound method.
            spam.foo,
            # Unbound method.
            Spam.eggs,
            # Callable instance.
            spam,
            # Module.
            introspect,
            # Package.
            py,
            # File.
            self.f,
            # Slice.
            slice(0),
            # Ellipsis.
            Ellipsis,
            # BrokenStr class.
            BrokenStr,
            # BrokenStr instance.
            brokenStr,
        ]
        if not PY3:
            self.items.extend([
                long(123),
                unicode(""),
                xrange(0),
                # Byte-compiled code.
                ham.func_code,
                # Buffer.
                buffer(''),
            ])

    def tearDown(self):
        self.items = None
        self.f.close()

    def test_getAttributeNames(self):
        for item in self.items:
            self._checkAttributeNames(item)
        if 'object' in __builtins__:
            self._checkAttributeNames(object)

    def test_getAttributeNames_NoSingle(self):
        for item in self.items:
            result = list(introspect.getAttributeNames(item, includeSingle=0))
            attributes = [attribute for attribute in result \
                          if attribute[0] != '_' or attribute[:2] == '__']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))

    def test_getAttributeNames_NoDouble(self):
        for item in self.items:
            result = list(introspect.getAttributeNames(item, includeDouble=0))
            attributes = [attribute for attribute in result \
                          if attribute[:2] != '__']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))

    def test_getAttributeNames_NoSingleOrDouble(self):
        for item in self.items:
            result = list(introspect.getAttributeNames(item, includeSingle=0,
                                                       includeDouble=0))
            attributes = [attribute for attribute in result \
                          if attribute[0] != '_']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))

    def _checkAttributeNames(self, item):
        result = introspect.getAttributeNames(item)
        attributes = [attribute for attribute in self.values \
                      if hasattr(item, attribute)]
        for attribute in attributes:
            self.assert_(attribute in result,
                         ':item: %r :attribute: %r' % (item, attribute))


class GetAutoCompleteListTestCase(GetAttributeTestCase):

    def setUp(self):
        GetAttributeTestCase.setUp(self)
        self.items = (
            'None.',
            '123 .',
            '"".',
            '[].',
            '().',
            '{}.',
            # Builtin function.
            'len.',
            # Builtin method.
            '[].append.',
        )

    def test_getAutoCompleteList(self):
        for item in self.items:
            result = introspect.getAutoCompleteList(item)
            object = eval(item[:-1])
            attributes = [attribute for attribute in self.values \
                          if hasattr(object, attribute)]
            for attribute in attributes:
                self.assert_(attribute in result,
                             ':item: %r :attribute: %r' % (item, attribute))

    def test_getAutoCompleteList_NoSingle(self):
        for item in self.items:
            result = list(introspect.getAutoCompleteList(item, includeSingle=0))
            attributes = [attribute for attribute in result \
                          if attribute[0] != '_' or attribute[:2] == '__']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))

    def test_getAutoCompleteList_NoDouble(self):
        for item in self.items:
            result = list(introspect.getAutoCompleteList(item, includeDouble=0))
            attributes = [attribute for attribute in result \
                          if attribute[:2] != '__']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))

    def test_getAutoCompleteList_NoSingleOrDouble(self):
        for item in self.items:
            result = list(introspect.getAutoCompleteList(item, includeSingle=0,
                                                         includeDouble=0))
            attributes = [attribute for attribute in result \
                          if attribute[0] != '_']
            self.assertEqual(result, attributes,
                             ':item: %r' % (item,))


# Support for GetConstructorTestCase.

class A1:
    def __init__(self, a):
        self.a = a

class B1(A1):
    def __init__(self, b):
        self.b = b

class C1(A1):
    pass

class D1(C1, B1):
    pass

if 'object' in __builtins__:
    class A2(object):
        def __init__(self, a):
            self.a = a

    class B2(A2):
        def __init__(self, b):
            self.b = b

    class C2(A2):
        pass

    class D2(C2, B2):
        pass

class N:
    pass

class O:
    def __init__(self, a, b=2, *args, **kwargs):
        pass

class P(O):
    pass

class Q(P):
    def __init__(self, c, d=4):
        pass

class GetConstructorTestCase(unittest.TestCase):

    @unittest.skipIf(PY3, "Python2 specific test")
    def test_getConstructor(self):
        args = ('self', 'a', 'b', 'args', 'kwargs')
        varnames = introspect.getConstructor(O).func_code.co_varnames
        self.assertEqual(varnames, args)
        varnames = introspect.getConstructor(P).func_code.co_varnames
        self.assertEqual(varnames, args)
        args = ('self', 'c', 'd')
        varnames = introspect.getConstructor(Q).func_code.co_varnames
        self.assertEqual(varnames, args)

    def test_getConstructor_None(self):
        values = (N, 1, 'spam', {}, [], (), dir)
        for value in values:
            self.assertEqual(introspect.getConstructor(N), None)

    @unittest.skipIf(PY3, "Python2 specific test")
    def test_getConstructor_MultipleInheritance(self):
        # Test old style inheritance rules.
        args = ('self', 'a')
        varnames = introspect.getConstructor(D1).func_code.co_varnames
        self.assertEqual(varnames, args)
        if 'object' in __builtins__:
            # Test new style inheritance rules as well.
            args = ('self', 'b')
            varnames = introspect.getConstructor(D2).func_code.co_varnames
            self.assertEqual(varnames, args)


if __name__ == '__main__':
    unittest.main()
