# Copyright (c) 2007 Liraz Siri <liraz@turnkeylinux.org>
#
# This file is part of turnkey-pylib.
#
# turnkey-pylib is open source software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of the
# License, or (at your option) any later version.

"""This module contains a generic magical mechanism for
hooking into the methods of a derived class. The hook interface
provides hooks into the following events:
   pre - before the method is invoked
   post - after the method is invoked
   exception - if the method raises an exception

You can use inheritance to the same effect, but for some things
using this hooking interface is much easier.

Applications:
* tracing
* debugging
* logging
* pre/post conditionals
* wrapping class methods with arbitrary functionality

Features:
* configurable support for hooking into all types of methods:
  normalmethods
  staticmethod
  classmethods
* support for class-level and instance-level hooks
* support for hooking base class methods
* support for hooking internal methods, e.g.,:
  __new__
  __init__

The implementation involves a metaclass (MetaHooker) that wraps the 
methods of the class that it creates. Which types of methods
are wrapped is controlled by the configuration of the MetaHooker class,
which is generated by the internal _class_metahooker function.

Usage notes:
* hooked classes should subclass from Hooked, or...

If you want to configure which type of methods are hooked:
create your own Hooked subclass with the class_hooked function

* the _hook attribute of your class should be an instance of a class
implementing the same interface as the HookInterface class.

The default _hook attribute (inherited from the Hooked base class)
does nothing.

Copyright and disclaimer:

Copyright (c) 2007 Liraz Siri <liraz@liraz.org>

This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.

IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.

"""

import new
import types

class ReturnNone:
    """Symbolic for forcing a None return value in the pre/post hooks"""
    pass

class _ApplyMetaHooker(object):
    """Dummy class for signalling the activation of the hook mechanism
    on a class created by this metaclass.

    By marking which classes we want hooked, it doesn't matter if MetaHooker
    is set as a global metaclass, because only classes that derive from this
    dummy class will actually have their methods wrapped.

    """
    pass

def _class_metahooker(hook_methods, hook_classmethods, hook_staticmethods):
    """makes a deeply magical MetaHooker metaclass."""
    class MetaHooker(type):
        def __init__(cls, name, bases, dct):
            type.__init__(name, bases, dct)

            if issubclass(cls, _ApplyMetaHooker):
                for attrname in dir(cls):
                    attr = getattr(cls, attrname)
                    if type(attr) not in (types.FunctionType, types.MethodType):
                        continue

                    new_attr = cls.hook(attr)
                    if new_attr != attr:
                        setattr(cls, attrname, new_attr)

        def hook(cls, attr):
            unbound_method = cls.wrap(attr)
            
            if type(attr) is types.FunctionType: # @staticmethod
                if hook_staticmethods is False:
                    return attr

                # static method is only simulated (unbound_method does not pass cls as first argument)
                # (workaround for setattr() transforming functions into MethodType)
                return new.instancemethod(unbound_method,
                                          cls, cls) 

            if attr.im_self: # @classmethod
                if hook_classmethods is False:
                    return attr

                # rebind the new method to the class
                return new.instancemethod(unbound_method,
                                          attr.im_self, attr.im_class)

            if hook_methods is False:
                return attr

            return unbound_method # wrapped regular method

        def wrap(cls, attr):
            def wrapper(hook_instance, *args, **kws):
                im = None
                if type(attr) is types.MethodType:
                    im = hook_instance

                args = list(args) 
	        shorted = hook_instance._hook.pre(im, attr, args, kws)
                if shorted: # I.e., shorted circuit
                    if shorted is ReturnNone:
                        return None
                    return shorted

                try:
                    if type(attr) is types.MethodType:
                        result = attr.im_func(hook_instance, *args, **kws)
                    else:
                        result = attr(*args, **kws)
                except Exception, e:
                    shorted = hook_instance._hook.exception(im, attr, e)
                    if shorted:
                        if shorted is ReturnNone:
                            return None
                        return shorted

                    raise # reraise last exception transparently

                shorted = hook_instance._hook.post(im, attr, result)
                if shorted: # I.e., shorted circuit
                    if shorted is ReturnNone:
                        return None
                    return shorted
                
                return result

            return wrapper
            
    return MetaHooker

class HookInterface(object):
    """abstract class for hook"""
    def pre(self, im, called,  args, kws):
        """called right before the method is called with its arguments.

        Arguments:
        'im' - can vary depending on the type of method invoked:
            if the intercepted method is a classmethod, this will be the invoked class.
            if the intercepted method is a staticmethod, this will be None.
            otherwise, this will be the invoked instance.
        'called' - the invoked method (a types.MethodType object)
        'args' - list of arguments passed to method
        'kws' - dictionary of keyword style arguments passed to method

        Returning any value other than None will short-circuit the method,
        preventing its execution and returning the value returned by the pre() hook instead.

        As a special case, to short-circuit a method while returning None: return ReturnNone.
        """
        pass

    def post(self, im, called, result):
        """called right after the method has returned with its result
        Arguments:
        'im' - can vary depending on the type of method invoked:
            if the intercepted method is a classmethod, this will be the invoked class.
            if the intercepted method is a staticmethod, this will be None.
            otherwise, this will be the invoked instance.
        'called' - the invoked method (a types.MethodType object)
        'result' - the result returned from the original method
        
        Returning from post() any value other than None will return to the original caller
        that value instead of the result of the original method.
        
        As a special case, if you want to force a return value of None instead of the
        original result, return ReturnNone.
        """
        pass

    def exception(self, im, called, e):
        """called right after the method has raised an exception
        Arguments:
        'im' - can vary depending on the type of method invoked:
            if the intercepted method is a classmethod, this will be the invoked class.
            if the intercepted method is a staticmethod, this will be None.
            otherwise, this will be the invoked instance.
        'called' - the invoked method (a types.MethodType object)
        'e' - the exception raised
        
        Returning from exception() any value other than None will return to the original caller
        that value instead of raising the intercepted exception. 
        
        As a special case, if you want to force a return value of None instead of raising
        an exception, return ReturnNone.
        """
        pass

def class_hooked(methods=True, classmethods=True, staticmethods=True):
    """Make a magical base class that uses MetaHooker metaclass to hook into the
    methods of your derived class.

    The hooking mechanism invokes the _hook object, in the class or
    instance (as the case may be), which is an instance of the HookInterface class.
    See its documentation for further details on the hooking API.

    Arguments specify which types of methods are hooked (all by default).
    'methods' - boolean hook methods?
    'classmethods' - boolean hook classmethods?
    'staticmethods' - boolean hook staticmethods?

    Pre-method-call, exception-raised-in-method and post-method-call generate calls to
    self._hook, which should implement the HookInterface.

    How does it work?
    
    In a nutshell, deriving from the generated class uses a MetaHooker
    metaclass created by the class_metahooker function to hook all
    class methods.

    """
    class Hooked(_ApplyMetaHooker):
        __metaclass__ = _class_metahooker(methods, classmethods, staticmethods)
        _hook = HookInterface()

    return Hooked

Hooked = class_hooked()

class HookPrint(object):
    """convenience hook class that prints how its methods are invoked.
    useful for debugging"""
    def pre(self, im, called, args, kws):
        "called right before the method is called with its arguments"
        print "_hook.pre(im=%s, name=%s,args=%s,kws=%s)" % (`im`, `called.__name__`, `args`, `kws`)

    def post(self, im, called, result):
        "called right after the method has returned with its result"
        print "_hook.post(im=%s, name=%s,result=%s)" % (`im`, `called.__name__`, `result`)

    def exception(self, im, called, e):
        "called right after the method has raised an exception"
        print "_hook.exception(im=%s, name=%s,e=%s)" % (`im`, `called.__name__`, `e`)

def _test():
    print "NOTICE: creating globals ClassOrig, ClassHooked"
    global ClassOrig
    global ClassHooked

    class ClassBase(object):
        def __init__(self, *args):
            print  "ClassBase __init__ args: " + `args`

        @classmethod
        def bcm(*args):
            print "bcm args: " + `args`
            return 111

        @staticmethod
        def bsm(*args):
            print "bsm args: " + `args`
            return 111

        def bm(*args):
            print "bm args: " + `args`
            return 111

    class ClassOrig(ClassBase):
        def __new__(cls):
            return super(ClassOrig, cls).__new__(cls)

        @classmethod
        def cm(*args):
            print "cm args: " + `args`
            return 111

        @staticmethod
        def sm(*args):
            print "sm args: " + `args`
            return 111

        def m(*args):
            print "m args: " + `args`
            return 111

        def e(*args):
            raise Exception("argh!")

    class ClassHooked(Hooked, ClassBase):
        class Hook(HookPrint):
            """example hook class - useful for debugging"""
            def pre(self, im, called, args, kws):
                super(type(self), self).pre(im, called, args, kws)

                if called.__name__ != '__new__':
                    pass
# example handling
#                    args.append('injected')
#                    return 666
#                    return ReturnNone

            def post(self, im, called, result):
                super(type(self), self).post(im, called, result)

# example handling
#                if called.__name__ != '__new__':
#                    return 666
#                    return ReturnNone

            def exception(self, im, called, e):
                super(type(self), self).exception(im, called, e)

# example handling
#                return 666
#                return ReturnNone
#                raise Exception("hooked argh")
            

        # if you comment this out, comparing the output with exercise_co will be easier
        # (the base hook doesn't modify the behavior of the hooked methods)
        _hook = Hook()

        def __new__(cls):
            return super(ClassHooked, cls).__new__(cls)

        @classmethod
        def cm(*args):
            print "cm args: " + `args`
            return 111

        @staticmethod
        def sm(*args):
            print "sm args: " + `args`
            return 111

        def m(*args):
            print "m args: " + `args`
            return 111

        def e(*args):
            raise Exception("argh!")

    def exercise_co():
        print "=== exercising ClassOrig"
        
        print "testing: ClassOrig.bsm() # base staticmethod"
        result = ClassOrig.bsm()
        print result
        print

        print "testing: ClassOrig.bcm() # base classmethod"
        result = ClassOrig.bcm()
        print result
        print
        
        print "testing: ClassOrig.sm() # staticmethod"
        result = ClassOrig.sm()
        print result
        print

        print "testing: ClassOrig.cm() # classmethod"
        result = ClassOrig.cm()
        print result
        print

        print "testing: co = ClassOrig()"
        co = ClassOrig()

        print "testing: co.sm() # staticmethod"
        result = co.sm()
        print result
        print

        print "testing: co.cm() # classmethod"
        result = co.cm()
        print result
        print

        print "testing: co.m() # method"
        result = co.m()
        print result
        print

        try:
            print "testing co.e() # method raises an exception"
            result= co.e()
            print result
        except Exception, e:
            print "caught exception: repr=%s str=%s" % (`e`, str(e))
        
        print

    def exercise_ch():
        print "=== exercising ClassHooked"
        
        print "testing: ClassHooked.bsm() # base staticmethod"
        result = ClassHooked.bsm()
        print result
        print

        print "testing: ClassHooked.bcm() # base classmethod"
        result = ClassHooked.bcm()
        print result
        print
        
        print "testing: ClassHooked.sm() # staticmethod"
        result = ClassHooked.sm()
        print result
        print

        print "testing: ClassHooked.cm() # classmethod"
        result = ClassHooked.cm()
        print result
        print

        print "testing: ch = ClassHooked()"
        ch = ClassHooked()

        print "testing: ch.sm() # staticmethod"
        result = ch.sm()
        print result
        print

        print "testing: ch.cm() # classmethod"
        result = ch.cm()
        print result
        print

        print "testing: ch.m() # method"
        result = ch.m()
        print result
        print

        try:
            print "testing ch.e() # method raises an exception"
            result= ch.e()
            print result
        except Exception, e:
            print "caught exception: repr=%s str=%s" % (`e`, str(e))

        print

    exercise_co()
    exercise_ch()
    
    print "NOTICE: creating globals co and ch"
    # for convenience
    global co
    co = ClassOrig()

    global ch
    ch = ClassHooked()

if __name__ == '__main__':
    _test()












