"""
pylog.py

Macro recorder for Python

C. Tismer - 981018

The idea: (based on PythonWin)
PythonWin does a lot of checks until it finds out what it has
to execute finally. But it ends up by calling the builtin
function compile and checks if it works.

We intercept compile to grab it's string arguments and
to collect them in a list.
This gives exactly all the lines which were syntactically
correct.

Usage:
>>> from pylog import logger
switches the logger on by default, and logs into an internal list.

If there is no other interpreter running than the built-in one,
the interpreter from module "code" is invoked and patched for logging.

logger methods:

    on()             starts logging.
    off()            stops logging.
    clear()          wipes the log out
    save(filename)   appends the current buffer to the file
    file(filename)   starts fail-safe logging
    replay()         an attempt to replay a session
    load(filename)   loading a session for replay
    
On fail-safe logging:
  The log file is opened and closed for every command.
  This can be useful if you are trying code which is 
  likely to kill PythonWin.
  
Installation: 
  Currently only PythonWin is supported.
  Copy pylog.py into a directory where it is always reachable
  by sys.path. The python root dir is a good place.
  From PythonWin's interactive window,
  - run pylog.py
  - execute install_pywin()   # to install pylog
  - execute uninstall_pywin() # to get rid of it

"""

__version__ = (0, 5)
__author__ = "Christian Tismer <tismer@pns.cc>"

"""
change log

981018 Initial version 0.0.1 as a proof of concept.

981021  Major redesign. The code was shortened, the logic improved.
        lock and unlock methods are removed. The logger now
        patches the interpreting code at two places and figures
        out wether logging commands are about to be logged or not.
    
        Semantic change: The logger will log successful commands only.
        It is quite likely that your logged sessions run just fine.
        
        Support for plain vanilla Python logging was added.
        The module was therefore renamed from "pywinlog" to "pylog"
    
        The logger can stay on, even when a replay is done.

        Currently, the logger calls must be inserted by hand :-(    
        PyWin's interact.py needs several patches.
        
981022  Simplified again.
        Now we have two approaches:
        1) manually insert a logging call after each compile/eval/exec
        2) just replace all exec commands to a do_exec function call.
        
981105  added installation feature
"""


import string, sys

class session_logger:

    def after_compile(self, strg, *ign):
        # this is a direct interface for logging
        if self.active:
            self.current = strg
        
    def after_execution(self, *ign):
        # committing a log entry
        if self.active:
            strg = string.strip(self.current)
            self.current = ""
            if strg:
                self.append(strg+"\n\n")

    def __init__(self, filename=None):
        self.lines = []
        self.filename = filename
        self.active = 0
        self.current = ""

    def append(self, str):
        if self.filename:
            self.append_to_file(str, self.filename)
        else:
            self.lines.append(str)

    def inhibit(self):
        # prevend logging of ourself
        # comment us out
        if self.current and self.current[0] != "#":
            self.current = "#-- " + self.current
        
    def off(self):
        self.inhibit()
        self.active = 0

    def on(self):
        self.inhibit()
        self.active = 1
        
    def clear(self):
        self.inhibit()
        self.lines = []

    def file(self, filename=None):
        self.inhibit()
        self.filename = filename
        self.on()
        if filename:
            self.save(filename)

    def save(self, filename):
        self.inhibit()
        txt = string.join(self.lines, "")
        self.append_to_file(txt, filename)
        self.clear()
    
    def append_to_file(self, str, filename):
        # fail-safe method, will always open and close file
        if filename:
            try:
                f=open(filename, "a")
                f.write(str)
                f.close()
            except:
                import sys
                sys.stderr.write("*** Error writing to %s - logger stopped\n" \
                      % filename)
                self.off()

    def __repr__(self):
        res = __name__+".logger - %s" % ["off", "active"][self.active]
        if self.filename: res = res + " >> " + self.filename
        return res
        
    def replay(self):
        self.inhibit()
        ps1 = sys.ps1
        ps2 = sys.ps2
        for cmd in self.lines:
            cmd = string.strip(cmd)
            lis = string.split(cmd, "\n")
            print ps1+lis[0]
            for more in lis[1:]:
                print ps2+more
            cmd = cmd + "\n"
            codeObj = compile(cmd,'<interactive input>','exec')
            try:
                # mostly copied from interact.py:
                import __main__
                globs =__main__.__dict__
                locs = globs				
                codeObj = compile(cmd,'<interactive input>','eval')
                # worked - eval it
                ret = eval(codeObj, globs, locs)
                if ret is not None:
                    print repr(ret)
            except SyntaxError: # means bad syntax for eval, but exec is OK
                exec codeObj in globs, locs

    def load(self, filename):
        self.inhibit()
        lines = open(filename).read()
        lines = string.split(lines, "\n\n")
        self.lines = map(lambda s:s+"\n\n", lines)
        

class fake_function:
    def __init__(self, func, pre=None, post=None):
        self.pre = pre
        self.func = func
        self.post = post
    def __call__(self, *args, **kw):
        if self.pre: apply(self.pre, args, kw)
        ret = apply(self.func, args, kw)
        if self.post: apply(self.post, args, kw)
        return ret

def do_exec(cmd, globs, locs):
    exec cmd in globs, locs

class faking_logger(session_logger):
    def __init__(self, *args, **kw):
        apply(session_logger.__init__, (self,)+ args, kw)
        self._new_compile = fake_function(compile, None, self.after_compile)
        self._new_eval    = fake_function(eval, None, self.after_execution)
        self._new_exec    = fake_function(do_exec, None, self.after_execution)
        
    def _install(self, namespace):
        dic = namespace.__dict__
        dic["compile"] = self._new_compile
        dic["eval"] = self._new_eval
        dic["do_exec"] = self._new_exec

class pywin_logger(session_logger):
    def __init__(self, filename=None):
        session_logger.__init__(self, filename)
        import sys
        sys.modules[self.__module__].logger=self
        self.on()

class pywin_logger(faking_logger):
    def __init__(self, filename=None):
        faking_logger.__init__(self, filename)
        import sys
        sys.modules[self.__module__].logger=self
        mod = sys.modules["pywin.framework.interact"]
        self._install(mod)
        self.on()

_pywin_snippet = """

def do_exec(cmd, globs, locs):
    exec cmd in globs, locs

try: 
  from pylog import pywin_logger
except: 
  class pywin_logger:
    def after_compile(self, str):pass
    def after_execution(self):pass
logger = pywin_logger()

"""

_pywin_orig = "exec codeObj in globs, locs"
_pywin_patch = "do_exec (codeObj, globs, locs)"

def install_pywin():
    "tries to patch pywin.framework.interact to support logging"
    import string, sys, os
    dir = os.path.split(sys.modules["pywin.framework.interact"].__file__)[0]
    fname = os.path.join(dir, "interact.py")
    txt = open(fname).read()
    if string.find(txt, _pywin_snippet) >= 0 \
    or string.find(txt, _pywin_orig) < 0:
        print "pylog has already been installed into PythonWin"
        return
    open(os.path.join(dir, "interact.orig.py"), "w").write(txt)
    txt = string.replace(txt, _pywin_orig, _pywin_patch) + _pywin_snippet
    open(fname, "w").write(txt)
    print "*** pylog has been installed. Please restart PythonWin ***"
    
def uninstall_pywin():
    "tries to revert to the original pywin.framework.interact"
    import string, sys, os
    dir = os.path.split(sys.modules["pywin.framework.interact"].__file__)[0]
    fname = os.path.join(dir, "interact.orig.py")
    txt = open(fname).read()
    open(os.path.join(dir, "interact.py"), "w").write(txt)
    os.unlink(fname)
    print "*** pylog has been uninstalled. Please restart PythonWin ***"
