""" Hacking Tools for Python

    This module includes some tools that I find useful to examine
    code from inside an interactive interpreter session. It includes
    functions to display doc strings, dictionaries, sequences, etc.
    in a more or less nice format and common stuff like disassembly
    routines, timing gadgets and exception helpers.

    (c) 1997-2014, Copyright by Marc-Andre Lemburg (mal@lemburg.com); 

         All Rights Reserved.

    Permission to use, copy, modify, and distribute this software and
    its documentation for any purpose and without fee or royalty is
    hereby granted, provided that the above copyright notice appear in
    all copies and that both the copyright notice and this permission
    notice appear in supporting documentation or portions thereof,
    including modifications, that you make.

    THE AUTHOR MARC-ANDRE LEMBURG DISCLAIMS ALL WARRANTIES WITH REGARD
    TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
    MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE
    LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
    DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
    WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
    ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
    PERFORMANCE OF THIS SOFTWARE !

"""
import types,sys,time,re

__version__ = '0.6'

INDENT = '    ' # string used to indent one level in show, docs, ...

NoDefault = 'no default'

### Tools

def docs(c,

         INDENT=INDENT):

    """ doc(c) -- print c's doc-strings """

    try:
        showobject(c,'',0)
        showdocstring(c.__doc__,1)
        if hasattr(c,'__version__'):
            print
            print INDENT+'[Version: %s]' % c.__version__
    except AttributeError:
        pass
    print
    items = []
    try:
        items = c.__dict__.items()
    except AttributeError:
        items = []
    try:
        for m in  c.__methods__:
            items.append((m,getattr(c,m)))
    except AttributeError:
        pass
    if items:
        items.sort()
        for name,obj in items:
            if hasattr(obj,'__doc__') and obj.__doc__:
                showobject(obj,name, 1)
                showdocstring(obj.__doc__, 2)

# Helper for docs:

def limited_repr(obj, limit=40):

    """ Return the repr() of obj.

        Limit the returned string length to limit chars.

    """
    try:
        r = repr(obj)
    except:
        r = '<repr-error>'
    if len(r) > limit:
        r = r[:limit] + '...'
    return r

def func_sig(func):

    """func_sig(func)

       Returns the signature of a Python function/method as string.
       Keyword initializers are also shown using
       repr(). Representations longer than 100 bytes are truncated.

       XXX Anonymous arguments ((a,b,c)=(1,2,3)) are not supported and
           probably never will be since they require disassembling the
           byte code which is bound to fail once byte code optimizers find
           their way into every Pythoneers home...

    """
    if hasattr(func,'im_func'):
        # func is a method
        func = func.im_func
    code = func.func_code
    fname = code.co_name
    callargs = code.co_argcount
    # XXX Uses hard coded values taken from Include/compile.h
    args = list(code.co_varnames[:callargs])
    if func.func_defaults:
        i = len(args) - len(func.func_defaults)
        for default in func.func_defaults:
            r = limited_repr(default, 100)
            arg = args[i]
            if arg[0] == '.':
                # anonymous arguments
                arg = '(...)'
            args[i] = '%s=%s' % (arg,r)
            i = i + 1
    if code.co_flags & 0x0004: # CO_VARARGS
        args.append('*'+code.co_varnames[callargs])
        callargs = callargs + 1
    if code.co_flags & 0x0008: # CO_VARKEYWORDS
        args.append('**'+code.co_varnames[callargs])
        callargs = callargs + 1
    return '%s(%s)' % (fname,', '.join(args))

spaces = re.compile('([ ]*)[^ ]')

def showdocstring(doc,level=0,

                  split=re.split,INDENT=INDENT,
                  spaces=spaces):

    try:
        l = split(doc.strip(),'\n\|\r\|\r\n')
    except:
        #print '%sno doc string available' % (INDENT*level)
        return
    if len(l) > 1:
        # Try to even out the indents
        indent = sys.maxint
        l = [line.expandtabs() for line in l]
        for i in range(1,len(l)):
            m = spaces.match(l[i])
            if m:
                sp = m.regs[1][1]
                if sp < indent:
                    indent = sp
            else:
                # Blank line
                pass
        l = [l[0].strip()] + map(lambda x,indent=indent: x[indent:],l[1:])
    else:
        l = [x.strip() for x in l]
    l = map( lambda x,n=level,INDENT=INDENT: INDENT*n + x, l)
    s = '\n'.join(l)+'\n'
    print s

def showobject(obj,name='',level=0):

    if type(obj) in (types.FunctionType, types.MethodType):
        try:
            objrepr = func_sig(obj)
        except AttributeError:
            objrepr = name
    elif type(obj) is types.ModuleType:
        objrepr = 'Module ' + name
    elif type(obj) is types.ClassType:
        objrepr = 'Class ' + name
    else:
        objrepr = name
    if not objrepr:
        try:
            objrepr = obj.__name__
        except AttributeError:
            objrepr = limited_repr(obj)
    print INDENT*level + objrepr,':'

###

def info(c):

    """ info(c) -- print all the information known about c """

    print 'Documentation:'
    print '-'*72
    docs(c)
    print
    print 'Attributes, Internals, etc.:'
    print '-'*72
    show(c, 2)

def show(c, maxdepth=2, level=0):

    """ show(c) -- print all the internals of c """

    try:
        r = limited_repr(c)
        print '%s%s' % (INDENT*(level), r)
    except:
        return
    level = level + 1
    if level > maxdepth:
        return
    attributes = set(dir(c))
    attributes.update((
        '__name__',
        '__class__',
        '__bases__',
        '__methods__',
        '__members__',
        '__attributes__',
        '__dict__',
        '__code__',
        '__call__',
        ))
    attributes = sorted(attributes)
    for attrname in attributes:
        if attrname in (
            '__name__',
            '__class__',
            '__code__',
            '__call__',
            ):
            showobj(c, attrname, maxdepth, level)
        elif attrname in (
            '__bases__',
            ):
            showseq(c, attrname, maxdepth, level)
        elif attrname in (
            '__methods__',
            '__members__',
            '__attributes__',
            ):
            showattr(c, attrname, maxdepth, level)
        elif attrname in (
            '__dict__',
            ):
            showdict(c, attrname, maxdepth, level)
        else:
            # Object type based
            obj = getattr(c, attrname, NoDefault)
            if obj is NoDefault:
                # Attribute doesn't exist
                pass
            elif isinstance(obj, (list, tuple)):
                showseq(c, attrname, maxdepth, level)
            elif isinstance(obj, dict):
                showdict(c, attrname, maxdepth, level)
            else:
                showattr(c, attrname, maxdepth, level)

# Helpers for show:

def showattr(c,name,maxdepth=1,level=0,
             INDENT=INDENT):

    """ showattr(c,name) -- for a in c.name: print c.a """

    try:
        items = getattr(c,name)
        items.sort()
    except AttributeError:
        return

    print '%s%s :' % (INDENT*level,name)
    level = level + 1
    if level > maxdepth:
        return

    for x in items:
        try:
            a = getattr(c,x)
            r = limited_repr(a)
        except:
            a = None
            r = '*exception*'
        if level < maxdepth and a is not None:
            print '%s%s :' % (INDENT*level,x)
            show(a,maxdepth,level+1)
        else:
            print '%s%s : %s' % (INDENT*level,x,r)

def showobj(c,name,maxdepth=1,level=0,
            INDENT=INDENT):

    """ showobj(c,name) -- print object c.name """

    try:
        x = getattr(c,name)
    except AttributeError:
        return

    print '%s%s :' % (INDENT*level,name)
    level = level + 1
    if level > maxdepth:
        return

    show(x,maxdepth,level)

def showseq(c,name,maxdepth=1,level=0,
            INDENT=INDENT):

    """ showseq(c,name) -- print sequence c.name """

    try:
        items = getattr(c,name)
    except AttributeError:
        return

    print '%s%s :' % (INDENT*level,name)
    level = level + 1
    if level > maxdepth:
        return

    if not items:
        print '%s%s' % (INDENT*level,items)
        return

    for x in items:
        try:
            r = limited_repr(x)
        except:
            x = None
            r = '*exception*'
        if level < maxdepth and x is not None:
            print '%s%s :' % (INDENT*level,r)
            show(x,maxdepth,level+1)
        else:
            print '%s%s' % (INDENT*level,r)

def showdict(c,name,maxdepth=1,level=0,
             INDENT=INDENT):
    
    """ showdict(c,name) -- print c.name.items() """

    try:
        dict = getattr(c,name)
        items = dict.items()
        items.sort()
    except AttributeError:
        return

    print '%s%s :' % (INDENT*level,name)
    level = level + 1
    if level > maxdepth:
        return

    if not items:
        print '%s%s' % (INDENT*level,dict)
        return

    for key,value in items:
        try:
            k = str(key)
        except:
            k = '*exception*'
        if len(k) > 40:
            k = k[:40]+' ...'
        try:
            v = limited_repr(value)
        except:
            v = '*exception*'
            value = None
        if level < maxdepth and value is not None:
            print '%s%s :' % (INDENT*level,k)
            show(value,maxdepth,level+1)
        else:
            print '%s%s : %s' % (INDENT*level,k,v)

# End of show helpers

def dis(c):

    """ dis(c) -- disassemble c; can be a code-string, -object a function
        or a method
    """
    if type(c) == types.StringType:
        c = compile(c, '<hack.py>', 'exec')
    elif type(c) == types.FunctionType:
        c = c.func_code
    elif type(c) == types.MethodType or type(c) == types.UnboundMethodType:
        c = c.im_func.func_code
    import dis
    dis.disco(c)

def clock(code, namespace=None):

    """ clock(code[,namespace]) -- clock the code executed in namespace which
        defaults to the top level namespace __main__.
    """

    code =  """import time;hack_timer=time.time(),time.clock()\n"""+\
            code+\
            """\nhack_timer=time.time()-hack_timer[0],time.clock()-hack_timer[1]; print '%.3fabs %.3fusr sec.' % hack_timer\n"""
    c = compile(code,'hack.clock-code','exec')
    if namespace:
        exec c in namespace
    else:
        import __main__
        exec c in __main__.__dict__
    return ''

class Timer:

    """ Timer class with a quite obvious interface
        - .start() starts a fairly accurate CPU-time timer plus an
          absolute timer
        - .stop() stops the timer and returns a tuple: the CPU-time in seconds
          and the absolute time elapsed since .start() was called
    """

    utime = 0
    atime = 0

    def start(self,
              clock=time.clock,time=time.time):
        self.atime = time()
        self.utime = clock()

    def stop(self,
             clock=time.clock,time=time.time):
        self.utime = clock() - self.utime
        self.atime = time() - self.atime
        return self.utime,self.atime

    def usertime(self,
                 clock=time.clock,time=time.time):
        self.utime = clock() - self.utime
        self.atime = time() - self.atime
        return self.utime

    def abstime(self,
                clock=time.clock,time=time.time):
        self.utime = clock() - self.utime
        self.atime = time() - self.atime
        return self.utime

    def __str__(self):

        return '%0.2fu %0.2fa sec.' % (self.utime,self.atime)

def profile(code,namespace=None):

    """ profile(code[,namespace]) -- profile the code executed in namespace
        which defaults to the top level namespace __main__.
    """
    code = 'import profile;profile.run("'+code+'")'
    c = compile(code,'profiling','exec')
    if namespace:
        exec c in namespace
    else:
        import __main__
        exec c in __main__.__dict__

def why():

    """ why() -- show locals that caused the last exception """

    if hasattr(sys,'last_traceback'):
        tb = sys.last_traceback
        while tb.tb_next != None: tb = tb.tb_next
        frame = tb.tb_frame
        print 'locals() of the last exception:'
        dict(frame.f_locals)
        #return(frame.f_locals)
    else:
        print 'no exception available !'

def showdict(d,maxindent=3,indent=0,
         INDENT=INDENT):

    """ showdict(d,maxindent,indent) -- show dict d with given indentation (=0)
    """

    if hasattr(d,'items'):
        print indent*INDENT+'{'
        if indent < maxindent:
            items = d.items()
            items.sort()
            for k,v in items:
                print indent*INDENT+' ',k,':'
                try:
                    print_here = dict(v,maxindent,indent+1)
                    if print_here:
                        s = limited_repr(v)
                        print (indent+1)*INDENT,s
                except: 
                    print (indent+1)*INDENT,'*exception*'
        else:
            print indent*INDENT,'...'
        print indent*INDENT+'}'
        return None
    else:
        return 'Error: no items-method'

def seq(l,maxindent=10,indent=0):

    """ seq(l,maxindent=10,indent=0) 
        -- show sequence l with given indentation (=0), limiting
            the indent-depth at maxindent (=10)
    """

    try:
        len(l)
        if type(l) == type('') or indent > maxindent: 
            raise TypeError
        for i in l:
            try:
                seq(i,maxindent,indent+1)
            except ValueError: 
                print '*exception*',
        print indent*' |'
        return
    except TypeError:
        print indent*' |',
        s = limited_repr(l)
        print s

#t = (1,2,3,(4,5,6),(3,(4,5,6),(3,4)))
#t = t + t
#seq(t)

def modules():

    """ modules() -- pretty print a list of loaded modules and packages;
        cached miss entries in sys.modules are not shown.
    """
    l = sys.modules.items()
    l.sort()
    print 'Loaded modules and packages:'
    for k,v in l:
        p = k.split('.')
        for i in range(len(p)-1):
            p[i] = '   '
        n = ''.join(p)
        if v is not None:
            if hasattr(v,'__path__'):
                print ' %s[%s]' % (''.join(p[:-1]),p[-1])
            else:
                print ' %s' % (n)

