import linecache, types
import traceback, sys

"""Trace calls to and returns from functions, a'la Lisp's trace/untrace.

>>> class FibException(Exception):
...     pass
... 
>>> def fib(n):
...     if n < 0:
...         raise FibException("fib() doesn't take negative numbers")
...     if n == 0 or n == 1:
...         return 1
...     return fib(n-1) + fib(n-2)
... 
>>> from ftrace import trace, untrace
>>> trace(fib)
tracing fib

>>> fib(5)
 fib (n=5)
     fib (n=4)
         fib (n=3)
             fib (n=2)
                 fib (n=1)
                 fib (n=1) ==> 1
                 fib (n=0)
                 fib (n=0) ==> 1
             fib (n=2) ==> 2
             fib (n=1)
             fib (n=1) ==> 1
         fib (n=3) ==> 3
         fib (n=2)
             fib (n=1)
             fib (n=1) ==> 1
             fib (n=0)
             fib (n=0) ==> 1
         fib (n=2) ==> 2
     fib (n=4) ==> 5
     fib (n=3)
         fib (n=2)
             fib (n=1)
             fib (n=1) ==> 1
             fib (n=0)
             fib (n=0) ==> 1
         fib (n=2) ==> 2
         fib (n=1)
         fib (n=1) ==> 1
     fib (n=3) ==> 3
 fib (n=5) ==> 8
8

>>> from ftrace import trace, untrace, ltrace
>>> ltrace(fib) # for "line trace"
tracing fib

>>> fib(4)
 fib (n=4)
         if n < 0:
         if n == 0 or n == 1:
         return fib(n-1) + fib(n-2)
     fib (n=3)
             if n < 0:
             if n == 0 or n == 1:
             return fib(n-1) + fib(n-2)
         fib (n=2)
                 if n < 0:
                 if n == 0 or n == 1:
                 return fib(n-1) + fib(n-2)
             fib (n=1)
                     if n < 0:
                     if n == 0 or n == 1:
                         return 1
             fib (n=1) ==> 1
             fib (n=0)
                     if n < 0:
                     if n == 0 or n == 1:
                         return 1
             fib (n=0) ==> 1
         fib (n=2) ==> 2
         fib (n=1)
                 if n < 0:
                 if n == 0 or n == 1:
                     return 1
         fib (n=1) ==> 1
     fib (n=3) ==> 3
     fib (n=2)
             if n < 0:
             if n == 0 or n == 1:
             return fib(n-1) + fib(n-2)
         fib (n=1)
                 if n < 0:
                 if n == 0 or n == 1:
                     return 1
         fib (n=1) ==> 1
         fib (n=0)
                 if n < 0:
                 if n == 0 or n == 1:
                     return 1
         fib (n=0) ==> 1
     fib (n=2) ==> 2
 fib (n=4) ==> 5
5
"""
# The docstring doesn't really work with doctest -- I think it's
# because doctest is careful about making sure previous statements
# don't affect succeeding statements ???... :-(

class TracerData:
    traced = {}
    
class Tracer(TracerData):
    def __init__(self, indent=0, indent_step=4):
        self.indent = indent
        self.indent_step = indent_step

    def __call__(self, frame, event, arg):
        import inspect
        name =  frame.f_code.co_name
        try:
            if self.__class__.traced.has_key(name):
                if event == 'call':
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame))
                    self.indent += self.indent_step
                    return self
                elif event == 'c_call':
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame))
                    self.indent += self.indent_step
                    return self
                elif event == 'return':
                    self.indent -= self.indent_step
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame)), '==>', `arg`
                elif event == 'c_return':
                    self.indent -= self.indent_step
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame)), '==>', '(C return)'
                elif event == 'exception':
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame)), '==>', traceback.format_exception_only(arg[0], arg[1])
                    return self
                elif event == 'c_exception':
                    print ' '*self.indent, name, inspect.formatargvalues(*inspect.getargvalues(frame)), '==>', '(C exception)'
                    return self
                elif self.__class__.traced[name] == 'line' and event == 'line':
                    print ' '*self.indent, linecache.getline(frame.f_code.co_filename, frame.f_lineno).rstrip(), '#%s:%s' % (frame.f_code.co_filename, frame.f_lineno)
                    # print ' '*self.indent, self.get_vars(frame)
        finally:
            del frame
            del name

    def indent_step_accessor(self, step=None):
        import types
        if type(step) is types.IntType:
            self.indent_step = step
        return self.indent_step

    def get_vars(self, frame, names=('revision', 'cset')):
        def get_value(name): # closes over the "frame" variable
            try:
                return `eval(name, frame.f_locals, frame.f_globals)`
            except Exception, ex:
                return traceback.format_exception_only(type(ex), ex)
        return dict((n, get_value(n)) for n in names)


class TraceData(TracerData):
    tracer = Tracer()
    
class trace(TraceData):
    def __init__(self, *funcs):
        traced = self.__class__.traced
        tracer = self.__class__.tracer
        sys.settrace(tracer)
        if not funcs:
            lst = traced.keys()
            lst.sort
            print lst
        else:
            for f in funcs:
                try:
                    traced[f.__name__] = True
                    print "tracing", f.__name__
                except AttributeError:
                    print `f`, "is not a function"
                    if type(f) is types.StringType:
                        traced[f] = True
                    else:
                        traced[`f`] = True
                    print "(but tracing it anyways)"

    # hide from the Python Read-Eval-Print loop
    def __repr__(self):
        return ''
    def __str__(self):
        return ''

class ltrace(trace):
    def __init__(self, *funcs):
        trace.__init__(self, *funcs)
        traced = self.__class__.traced
        for f in funcs:
            try:
                traced[f.__name__] = 'line'
            except AttributeError:
                if traced.has_key(f):
                    traced[f] = 'line'
                elif traced.has_key(`f`):
                    traced[`f`] = 'line'
        
class untrace(TraceData):
    def __init__(self, *funcs):
        traced = self.__class__.traced
        lst = list()
        for f in funcs:
            try:
                lst.append(f.__name__)
            except AttributeError:
                if type(f) is types.StringType:
                    lst.append(f)
                else:
                    lst.append(`f`)
        if not lst:
            lst = traced.keys()
        for name in lst:
            if traced.pop(name, False):
                print "untracing", name
            else:
                print name, "wasn't traced"

    # hide from the Python Read-Eval-Print loop
    def __repr__(self):
        return ''
    def __str__(self):
        return ''

class indent_step(TraceData):
    def __init__(self, step=None):
        print self.__class__.tracer.indent_step_accessor(step)
        return None

    # hide from the Python Read-Eval-Print loop
    def __repr__(self):
        return ''
    def __str__(self):
        return ''

if __name__ == '__main__':
    import getopt
    opts = [p[0] for p in getopt.getopt(sys.argv[1:], "tv", ("test","verbose"))[0]]
    if "-t" in opts or "--test" in opts:
        verbose = False
        if "-v" in opts or "--verbose" in opts:
            verbose = True
        import doctest
        doctest.testmod(verbose=verbose)
