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)
the MetaCallableObject, and add the matches to the
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:
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
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:
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):
ctx = CallContext(self)
a tuple to a list
ctx.args = list(args)
ctx.kwargs = kwargs
if self.beforeHooks is not None:
for advice in self.beforeHooks:
advice(ctx)
if not ctx.continueCall:
return ctx.retVal
try:
retVal = apply(self.callableObj, ctx.args, ctx.kwargs)
except Exception, ex:
ctx.exception = ex
if self.exceptionHooks is not None:
exChain = self.exceptionHooks
for exhandler in exChain:
exhandler(ctx)
raise ex
ctx.retVal = retVal
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)
advertising or publicity pertaining to distribution of the software
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:
baseClasses = list(mixInClass.__bases__)
baseClasses.reverse()
for baseClass in baseClasses:
addMixIn(pyClass, baseClass)
attrName = 'mixInsFor'+pyClass.__name__
mixIns = getattr(pyClass, attrName, None)
if mixIns is None:
mixIns = []
setattr(pyClass, attrName, mixIns)
mixIns.append(mixInClass)
for name in dir(mixInClass):
if not name.startswith('__'):
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:
baseClasses = list(mixInClass.__bases__)
baseClasses.reverse()
for baseClass in baseClasses:
removeMixIn(pyClass, baseClass)
attrName = 'mixInsFor'+pyClass.__name__
mixIns = getattr(pyClass, attrName, None)
if mixIns is not None:
mixIns.remove(mixInClass)
for name in dir(mixInClass):
if not name.startswith('__'):
member = getattr(mixInClass, name)
if type(member) is types.MethodType:
delattr(pyClass, name)