#!/usr/bin/env python
#
# Class for following the execution of python code, RWW Hooft, 2002/05/02
# Based on profile module from python 2.2, rev 1.0  6/2/94
#
# Based on prior profile module by Sjoerd Mullender...
#   which was hacked somewhat by: Guido van Rossum

"""Class for following the execution of Python code."""

# Copyright 1994, by InfoSeek Corporation, all rights reserved.
# Written by James Roskind
#
# Permission to use, copy, modify, and distribute this Python software
# and its associated documentation for any purpose (subject to the
# restriction in the following sentence) without fee is hereby granted,
# provided that the above copyright notice appears in all copies, and
# that both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of InfoSeek not be used in
# advertising or publicity pertaining to distribution of the software
# without specific, written prior permission.  This permission is
# explicitly restricted to the copying and modification of the software
# to remain in Python, compiled Python, or other languages (such as C)
# wherein the modified or derived code is exclusively imported into a
# Python module.
#
# INFOSEEK CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL INFOSEEK CORPORATION 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.

completefilenames=0
maxindent=80

indent=' '*maxindent

import sys,os

__all__ = ["run","Follow"]

if completefilenames:
    # Enable this if you want to see all filenames completely
    def fnuniq(fn):
        return fn
else:
    # This shortens them as much as possible
    _fnuniq={}
    _fnvals={}
    def fnuniq(fn):
        if not _fnuniq.has_key(fn):
            fn,u=os.path.split(fn)
            while fn and _fnvals.has_key(u):
                fn,t=os.path.split(fn)
                u=os.path.join(t,u)
            _fnuniq[fn]=u
        return _fnuniq[fn]

def run(statement):
    """Run statement under follower

    This function takes a single argument that can be passed to the
    "exec" statement.  This routine attempts to "exec" its first argument
    and will print a log of all calls and returns to stderr
    """
    folw = Follow()
    try:
        folw.run(statement)
    except SystemExit:
        print "Follow> SystemExit"
        raise

class Follow:
    def __init__(self,out=sys.stderr):
        self.cur = None
        self.level = 0
        self.outwrite = out.write

    def trace_dispatch_exception(self, frame):
        rfn, rframe, rcur = self.cur
        if (rframe is not frame) and rcur:
            return self.trace_dispatch_return(rframe)
        return 1

    def trace_dispatch_call(self, frame):
        self.level += 1
        if self.cur and frame.f_back is not self.cur[-2]:
            rfn, rframe, rcur = self.cur
            assert rframe.f_back is frame.f_back, ("Bad call", rfn,
                                                   rframe, rframe.f_back,
                                                   frame, frame.f_back)
            self.trace_dispatch_return(rframe)
            assert (self.cur is None or
                    frame.f_back is self.cur[-2]), ("Bad call",
                                                    self.cur[-3])
        fcode = frame.f_code
        fn = "%s() @ %s:%d "%(fcode.co_name,fnuniq(fcode.co_filename), fcode.co_firstlineno, )
        self.outwrite("Follow>%scall %s\n"%(indent[:self.level],fn))

        self.cur = fn, frame, self.cur
        return 1

    def trace_dispatch_return(self, frame):
        if frame is not self.cur[-2]:
            assert frame is self.cur[-2].f_back, ("Bad return", self.cur[-3])
            self.trace_dispatch_return(self.cur[-2])

        fcode = frame.f_code
        fn = "%s() @ %s:%d "%(fcode.co_name,fnuniq(fcode.co_filename), fcode.co_firstlineno, )
        self.outwrite("Follow>%sret  %s\n"%(indent[:self.level],fn))

        self.cur = self.cur[-1]
        self.level -= 1
        return 1

    dispatch = {
        "call": trace_dispatch_call,
        "exception": trace_dispatch_exception,
        "return": trace_dispatch_return,
        }

    def trace_dispatch(self, frame, event, arg):
        self.dispatch[event](self, frame)

    # The following two methods can be called by clients to use
    # a follower to follow a statement, given as a string.
    def run(self, cmd):
        import __main__
        dict = __main__.__dict__
        return self.runctx(cmd, dict, dict)

    def runctx(self, cmd, globals, locals):
        sys.setprofile(self.trace_dispatch)
        try:
            exec cmd in globals, locals
        finally:
            sys.setprofile(None)
        return self

    # This method is more useful to follow a single function call.
    def runcall(self, func, *args, **kw):
        sys.setprofile(self.trace_dispatch)
        try:
            return apply(func, args, kw)
        finally:
            sys.setprofile(None)

# When invoked as main program, invoke the follower on a script
if __name__ == '__main__':
    if not sys.argv[1:]:
        print "usage: follow.py scriptfile [arg] ..."
        sys.exit(2)

    filename = sys.argv[1]  # Get script filename

    del sys.argv[0]         # Hide "follow.py" from argument list

    # Insert script directory in front of module search path
    sys.path.insert(0, os.path.dirname(filename))

    run('execfile(' + `filename` + ')')
