##\file mop.py
##\brief Part of \b MOPPY -- A Meta Object Programming toolkit for Python
##\author Joel Boehland
##
## Note-- many ideas for this module
## were taken from Pedro Rodriguez's and other's posts on comp.lang.python
##
##Actors in this module:
##
##   \li mop::CallInterceptor Represents a callable object in Python (unbound method, bound method, function, callable)
##      The CallInterceptor will intercept calls to the callable object.
##
##   \li mop::Selection An object containing a set of criteria (SelectionCriteria) and the
##      set of objects from the ObjectDomain that satisfy this criteria. A selection has
##      many similarities to the combination of a SELECT query and a RESULTSET containing
##      the objects that meet the SELECT query in a relational database.
##
##   \li mop::SelectionCriteria An object used to select which objects will be bound
##      to a Selection.
##
##   \li mop::BaseHook A function that will be called when a callsite is reached. It may be
##      triggered before the callsite is called, after, or when an exception
##      occurs. An hook may also be set around a callsite, which means it
##      will be able to act before and after the call.
##     
##   \li mop::MetaCallableObject A class that wraps and scripts (advises) a callable
##      object during its call execution. These advises can be simple operations orthoganal
##      to the original callable action(tracing, debugging), to fully scripting the call path
##      as it progresses through the execution(throw exceptions, alter return values, alter
##      incoming parameters, etc...).
##
##   \li mop::MetaStructuralObject A class that injects (introduces) structure to another
##      object, usually without the injected object being aware of the injection. The mechanism
##      used to do this is using one or more Mixin classes.
##
##   \li mop::ObjectDomain A class containing the set of modules,
##      classes, functions, methods etc that we can map our MetaObjects onto
##
##   \li mop::MetaObjectSystem A class representing the intersection the set of all the
##       objects in the ObjectDomain and the set of all of our active MetaObjects

import sys
import types
from kjbuckets import kjSet
import searchreplace as sr

#---------------------------------------------------------------------------
class ObjectDomain(object):
    """
    A container holding all of the regular runtime objects
    that we can map our MetaObjects onto.
    """
    
    def __init__(self):
        self.modules = kjSet()

    def addModuleToDomain(self, module):
        self.modules.add(module)

    def removeModuleFromDomain(self, module):
        del self.modules[module]
        
#---------------------------------------------------------------------------
class SysModulesObjectDomain(ObjectDomain):
    """
    Container for all of the objects in all the modules
    loaded into the python runtime
    """
    
    def __init__(self):
        ObjectDomain.__init__(self)
        for module in sys.modules.values():
            self.modules.add(module)

#---------------------------------------------------------------------------
class MetaObjectSystem(object):
    """
    Central Object of the Meta Object Programming System. A MetaObjectSystem
    defines a domain of candidate objects as well as a set of MetaObjects
    to map to this domain. It binds and unbinds the MetaObjects to the
    functions and structures within the object domain based on different criteria
    \sa ObjectDomain
    \sa MetaCallableObject
    \sa MetaStructuralObject
    \sa SelectionCriteria
    """

    def __init__(self, metaobjectDomain=None):
        self.mcos = kjSet()
        self.msos = kjSet()
        self.metaobjectDomain = metaobjectDomain
        if self.metaobjectDomain is None:
            self.metaobjectDomain = SysModulesObjectDomain()
        
    def addMetaCallableObject(self, mco):
        self.mcos.add(mco)
        #need to find matches for each selection
        #in the MetaCallableObject, and add the matches to the
        #selection
        for selection in mco.selections:
            criteria = selection.criteria
            for module in self.getModules():
                callsites = self.getCallInterceptorsForModule(criteria, module)
                for cs in callsites:
                    selection.matches.add(cs)

    
    def removeMetaCallableObject(self, mco):
        del self.mcos[mco]
        for selection in mco.selections:
            mco.removeSelection(selection)
            
            
    def addMetaStructuralObject(self, mso):
        """
        add in any mixins (introductions) of the
        mso to qualifying objects
        """
        for selection in mso.selections:
            criteria = selection.criteria
            for module in self.getModules():
                msosites = self.getMixinSitesForModule(criteria, module)
                for mi in msosites:
                    selection.matches.add(mi)

    def removeMetaStructuralObject(self, mso):
        """
        remove any mixins (introductions) from
        the selections in the mso
        """
        for selection in mso.selections:
            mso.removeSelection(selection)
                    
    
    def getCallInterceptorsForModule(self, criteria, module):
        nameFilter = [sr.STD_IGNORE]
        objectFilter = [criteria]
        ctx = sr.SearchContext(nameFilter, objectFilter)
        callsites = []
        matches = sr.recurseSelectMembers(module, ctx)

        for match in matches:
            #check to see if match object is already a callsite
            if hasattr(match, '_mop_callsite'):
                callsites.append(match)
            else:
                callsites.append(CallInterceptor(match))

        return callsites
    
    def getMixinSitesForModule(self, criteria, module):
        nameFilter = [sr.STD_IGNORE]
        objectFilter = [criteria]
        ctx = sr.SearchContext(nameFilter, objectFilter)
        mixinSites = []
        matches = sr.recurseSelectMembers(module, ctx)

        for match in matches:
            mixinSites.append(match)
            
        return mixinSites

    def getModules(self):
        return self.metaobjectDomain.modules.items()
    
#---------------------------------------------------------------------------
class Selection(object):
    """
    Selection
    
    This class represents a set of objects taken out of the
    object domain that match a set of criteria. This class
    is analogous to a combination of SELECT statement and the resulting
    ResultSet in RDBMS-land
    """
    def __init__(self, selectionCriteria, matches=None):
        
        if matches is None:
            matches = kjSet()
            
        self.matches = matches
        self.criteria = selectionCriteria

#---------------------------------------------------------------------------
class SelectionCriteria(object):
    """
    SelectionCriteria
    
    Pattern used to map a selection to a set of objects in the object domain.
    Can use attributes such as namespace (i.e. com.blah.mypackage.mymodule.*),
    class name (mypackage.mymodule.MyClass),
    method name (i.e. set*, get*, execute(), blah() ),
    attribute access (__getattr__, someFieldName or __setattr__ someFieldName),
    or using tagged metadata loaded elsewhere (@mutable-method, @security-required)
    """
    def __init__(self):
        self.andClauses = []
        self.orClauses = []
        
    def AND(self, andClause):
        self.andClauses.append(andClause)

    def OR(self, orClause):
        self.orClauses.append(orClause)

    def match(self, ctx):
        """
        If ANY of the orClauses match, return True
        else if ALL of the andClauses match, return True
        else return False
        """
        for oc in self.orClauses:
            if oc.match(ctx):
                return True

        for cc in self.andClauses:
            if not cc.match(ctx):
                return False
        #we got this far, so it passed all criteria
        return True
    
#---------------------------------------------------------------------------
class CallContext(object):
    """
    CallContext

    Provides a shared context to be used by all the hooks
    attatched to a call-site.
    """
    def __init__(self, callsiteObj):
        self.callsiteObj = callsiteObj
        self.args = []
        self.kwargs = {}
        self.retVal = None
        self.continueCall = True

#---------------------------------------------------------------------------
class CallInterceptor(object):
    """
    CallInterceptor
    
    A CallInterceptor is a given point in object-oriented code. It can be a method call,
    object initialization, variable retrieval...
    
    It provides the place to attatch hooks to the call site.

    A particular CallInterceptor can belong to many selections
    """
    def __init__(self, callableObj):
        self.callableObj = callableObj
        self.__name__ = callableObj.__name__
        self._mop_callsite = True
        self.beforeHooks = None
        self.afterHooks = None
        self.exceptionHooks= None

        self.bind()
        
    def addHook(self, hook):
        if hook.getBeforeAdvice() is not None:
            #lazy allocation for all hook lists
            if self.beforeHooks is None:
                self.beforeHooks = []
            self.beforeHooks.append(hook.getBeforeAdvice())
            
        if hook.getAfterAdvice() is not None:
            if self.afterHooks is None:
                self.afterHooks = []
            self.afterHooks.append(hook.getAfterAdvice())
            
        if hook.getExceptionHandler() is not None:
            if self.exceptionHooks is None:
                self.exceptionHooks = []
            self.exceptionHooks.append(hook.getExceptionHandler())
            
    def removeHook(self, hook):
        if hook.getBeforeAdvice() is not None:
            if self.beforeHooks is not None:
                self.beforeHooks.remove(hook.getBeforeAdvice())
                if 0 == len(self.beforeHooks):
                    self.beforeHooks = None
                
        if hook.getAfterAdvice() is not None:
            if self.afterHooks is not None:
                self.afterHooks.remove(hook.getAfterAdvice())
                if 0 == len(self.afterHooks):
                    self.afterHooks = None
                    
        if hook.getExceptionHandler() is not None:
            if self.exceptionHooks is not None:
                self.exceptionHooks.remove(hook.getExceptionHandler())
                if 0 == len(self.exceptionHooks):
                    self.exceptionHooks = None
                
        if None == self.beforeHooks == self.afterHooks == self.exceptionHooks:
            self.unbind()
            
    def bind(self):
        sr.replaceCallable(self.callableObj, self)
        
    def unbind(self):
        sr.replaceInGlobals(self, self.callableObj)
        sr.replaceCallable(self, self.callableObj, False)
        
    def __call__(self, *args, **kwargs):
        #set up the call context
        ctx = CallContext(self)
                
        #we may need to manipulate the args tuple
        #somewhere in the call chain, so convert it
        #from a tuple to a list
        ctx.args = list(args)
        ctx.kwargs = kwargs
        
        #run the before advices
        if self.beforeHooks is not None:
            for advice in self.beforeHooks:
                advice(ctx)
                if not ctx.continueCall:
                    return ctx.retVal
                
        #call the wrapped callable
        try:
            retVal = apply(self.callableObj, ctx.args, ctx.kwargs)
        except Exception, ex:
            #two possible paths here:
            #1. The handler does something and throws another ex,
            #   breaking out of the call-chain
            #2. The handler quietly does some cleanup, we then re-throw the ex
            ctx.exception = ex
            if self.exceptionHooks is not None:
                exChain = self.exceptionHooks
                for exhandler in exChain:
                    exhandler(ctx)
            
            #re-throw exception after all cleanup is done
            raise ex
            
        #add return value to the call context
        ctx.retVal = retVal
        #run the after advices
        if self.afterHooks is not None:
            for advice in self.afterHooks:
                advice(ctx)
                if not ctx.continueCall:
                    return ctx.retVal
                
        return ctx.retVal

#---------------------------------------------------------------------------
class BaseHook(object):
    """
    BaseHook
    
    a callable that will be called when a callsite is reached. It may be
    triggered before the callsite is called, after, or when an exception
    occurs.
    An hook may also wrap around a callsite, meaning that it will be
    able to do some job before and after the callsite executes.
    """

    def getBeforeAdvice(self):
        return getattr(self, "doBeforeAdvice", None)

    def getAfterAdvice(self):
        return getattr(self, "doAfterAdvice", None)

    def getExceptionHandler(self):
        return getattr(self, "handleException", None)


#---------------------------------------------------------------------------
class MetaCallableObject(object):
    """
    MetaCallableObject A class that wraps and scripts (advises) a callable
    object during its call execution. These advises can be simple operations orthoganal
    to the original callable(tracing, debugging), to fully scripting the call path
    as it progresses through the call execution(throw exceptions, alter return values, alter
    incoming parameters, etc...).
     
    """
    def __init__(self):
        self.hooks = []
        self.selections = []
        
    def addSelection(self, selection):
        self.selections.append(selection)
        for hook in self.hooks:
            self.__addHookToSelection(hook, selection)
        
    def removeSelection(self, selection):
        self.selections.remove(selection)
        for hook in self.hooks:
            self.__removeHookFromSelection(hook, selection)
        
    def addHook(self, hook):
        self.hooks.append(hook)
        for selection in self.selections:
            self.__addHookToSelection(hook, selection)
                
    def removeHook(self, hook):
        self.hooks.remove(hook)
        for selection in self.selections:
            self.__removeHookFromSelection(hook, selection)
        
    def __addHookToSelection(self, hook, selection):
        for ci in selection.matches.items():
            ci.addHook(hook)
            
    def __removeHookFromSelection(self, hook, selection):
        for ci in selection.matches.items():
            ci.removeHook(hook)

#---------------------------------------------------------------------------
class MetaStructuralObject(object):
    """
    MetaStructuralObject A class that injects (introduces) structure to another
    objects, usually without the injected object being aware of the injection. The mechanism
    used to do this is using one or more Mixin classes.
    """
    def __init__(self):
        self.mixins = []
        self.selections = []
        
    def addSelection(self, selection):
        self.selections.append(selection)
        for mixin in self.mixins:
            self.__addMixinToSelection(mixin, selection)
        
    def removeSelection(self, selection):
        self.selections.remove(selection)
        for mixin in self.mixins:
            self.__removeMixinFromSelection(mixin, selection)
        
    def addMixin(self, mixin):
        self.mixins.append(mixin)
        for selection in self.selections:
            self.__addMixinToSelection(mixin, selection)
                
    def removeMixin(self, mixin):
        self.mixins.remove(mixin)
        for selection in self.selections:
            self.__removeMixinFromSelection(mixin, selection)
        
    def __addMixinToSelection(self, mixin, selection):
        for cls in selection.matches.items():
            addMixIn(cls, mixin)
            
    def __removeMixinFromSelection(self, mixin, selection):
        for obj in selection.matches.items():
            removeMixIn(obj, mixin)
                
                

####################
#Following method taken from Ian Bicking's and Chuck Esterbrook's
#Webware project.
# 
#
#Copyright 1999-2001 by Chuck Esterbrook.
#All Rights Reserved
#
#License
#=======
#
#Permission to use, copy, modify, and distribute this software and its
#documentation for any purpose and without fee is hereby granted,
#provided that the above copyright notice appear in all copies and that
#both that copyright notice and this permission notice appear in
#supporting documentation, and that the names of the authors not be used
#in advertising or publicity pertaining to distribution of the software
#without specific, written prior permission.
#
#
#Disclaimer
#==========
#
#THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
#INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
#EVENT SHALL THE AUTHORS 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.
#
#
#Trademarks
#==========
#
#All trademarks referred to in the source code, documentation, sample
#files or otherwise, are reserved by their respective owners.
###################

def addMixIn(pyClass, mixInClass, makeAncestor=0):
    """
    Mixes in the attributes of the mixInClass into the pyClass.
    These attributes are typically methods (but don't have to be).
    Note that private attributes, denoted by a double underscore,
    are not mixed in. Collisions are resolved by the mixInClass'
    attribute overwriting the pyClass'. This gives mix-ins the
    power to override the behavior of the pyClass.
    
    After using MixIn(), instances of the pyClass will respond to
    the messages of the mixInClass.
    
    An assertion fails if you try to mix in a class with itself.
    
    The pyClass will be given a new attribute mixInsForCLASSNAME
    which is a list of all mixInClass' that have ever been installed,
    in the order they were installed. You may find this useful for
    inspection and debugging.
    
    You are advised to install your mix-ins at the start up of
    your program, prior to the creation of any objects. This approach
    will result in less headaches. But like most things in Python, you're
    free to do whatever you're willing to live with.  :-)
    
    There is a bitchin' article in the Linux Journal, April 2001,
    Using Mix-ins with Python by Chuck Esterbrook which gives a
    thorough treatment of this topic.
    
    An example, that resides in Webware, is MiddleKit.Core.ModelUser.py,
    which install mix-ins for SQL adapters. Search for MixIn(.
    
    If makeAncestor is 1, then a different technique is employed: the
    mixInClass is made the first base class of the pyClass. You probably
    don't need to use this and if you do, be aware that your mix-in can no
    longer override attributes/methods in pyClass.
    
    This function only exists if you are using Python 2.0 or later. Python 1.5.2
    has a problem where functions (as in aMethod.im_func) are tied to their class,
    when in fact, they should be totally generic with only the methods being tied to
    their class. Apparently this was fixed in Py 2.0.
    """

    assert mixInClass is not pyClass, 'mixInClass = %r, pyClass = %r' % (mixInClass, pyClass)
    if makeAncestor:
        if mixInClass not in pyClass.__bases__:
            pyClass.__bases__ = (mixInClass,) + pyClass.__bases__
    else:
        # Recursively traverse the mix-in ancestor classes in order
        # to support inheritance
        baseClasses = list(mixInClass.__bases__)
        baseClasses.reverse()
        for baseClass in baseClasses:
            addMixIn(pyClass, baseClass)
            
        # Track the mix-ins made for a particular class
        attrName = 'mixInsFor'+pyClass.__name__
        mixIns = getattr(pyClass, attrName, None)
        if mixIns is None:
            mixIns = []
            setattr(pyClass, attrName, mixIns)
                
        # Make sure we haven't done this before
        # Er, woops. Turns out we like to mix-in more than once sometimes.
        #assert not mixInClass in mixIns, 'pyClass = %r, mixInClass = %r, mixIns = %r' % (pyClass, mixInClass, mixIns)
        
        # Record our deed for future inspection
        mixIns.append(mixInClass)
        
        # Install the mix-in methods into the class
        for name in dir(mixInClass):
            if not name.startswith('__'): # skip private members
                member = getattr(mixInClass, name)
                if type(member) is types.MethodType:
                    member = member.im_func
                    setattr(pyClass, name, member)


def removeMixIn(pyClass, mixInClass, removeAncestor=0):
    assert mixInClass is not pyClass, 'mixInClass = %r, pyClass = %r' % (mixInClass, pyClass)
    if removeAncestor:
        if mixInClass in pyClass.__bases__:
            pyClass.__bases__ = pyClass.__bases__ - (mixInClass,)
    else:
        # Recursively traverse the mix-in ancestor classes in order
        # to support inheritance
        baseClasses = list(mixInClass.__bases__)
        baseClasses.reverse()
        for baseClass in baseClasses:
            removeMixIn(pyClass, baseClass)
            
        # Track the mix-ins made for a particular class
        attrName = 'mixInsFor'+pyClass.__name__
        mixIns = getattr(pyClass, attrName, None)
        if mixIns is not None:
            mixIns.remove(mixInClass)
        # Remove the mix-in methods from the class
        for name in dir(mixInClass):
            if not name.startswith('__'): # skip private members
                member = getattr(mixInClass, name)
                if type(member) is types.MethodType:
                    delattr(pyClass, name)