"""
This module implements an aspect weaver.
It provides mainly two things:
* A superclass :py:class:`Aspect`, to be the parent class of aspect classes. Each aspect contains a *pointcut* describing the classes and methods to which the aspect will be applied, and *advice* in the form of *before* and *after* methods, to be executed in any join-point in the pointcut.
* An *aspect weaver* in the form of meta-class factory, which generates a meta-class of any class to be affected (weaved) by a given list of aspects.
Typical use:
Define aspect classes in some modules. For example:
Module `myaspect1.py`::
from libutilitaspy.aspects import Aspect
class MyAspect1(Aspect):
classes = 'SomeClass[0-9]+' # a regular expression defining the (names of the) classes to which this aspect will be applied
methods = 'some_method' # a regular expression defining the (names of the) methods to which this aspect will be applied
def before(self, klass, method, obj, *args, **kwargs):
print "(aspect1) before"
# do something...
def after(self, klass, method, obj, retval, exc_type, exc_val, traceback):
print "(aspect1) after"
# do something...
Module `myaspect2.py`::
from libutilitaspy.aspects import Aspect
class MyAspect2(Aspect):
classes = 'SomeClass[0-9]+' # a regular expression
methods = '.*_method' # a regular expression
def before(self, klass, method, obj, *args, **kwargs):
print "(aspect2) before"
# do something...
def after(self, klass, method, obj, retval, exc_type, exc_val, traceback):
print "(aspect2) after"
# do something...
Now, you create the aspect weaver which is a metaclass, as follows:
Module `metaclassconfig.py`::
from libutilitaspy.aspects.core import WeaverMetaClassFactory
from myaspect1 import MyAspect1
from myaspect2 import MyAspect2
MyMetaClass = WeaverMetaClassFactory(MyAspect1(), MyAspect2())
.. note::
The order of aspects matters; the aspects are applied from left-to-right, with
left being the most deeply nested (the innermost)
Also note that the weaver expects *instances* of aspect classes.
Now, each class which may be affected by aspects, should declare its meta-class
to be the aspect weaver. For example:
Module `someclass1.py`::
from metaclassconfig import MyMetaClass
class SomeClass1(object):
__metaclass__ = MyMetaClass
def some_method(self):
# ...
pass
def some_other_method(self):
# ...
pass
Here, :py:class:`MyAspect1` will be applied only to
`SomeClass1.some_method`, while both :py:class:`MyAspect1` and
:py:class:`MyAspect2` will be applied, in that order to both methods of
:py:class:`SomeClass1`
"""
from abc import ABCMeta, abstractmethod
from types import FunctionType
import re
import sys
__all__ = ['Aspect', 'WeaverMetaClassFactory', 'make_aspect_from_generator']
[docs]class Aspect(object):
"""
An aspect class defines a *pointcut* (the set of *joinpoints* where the
aspect is to be applied, and *advice* methods *before* and *after* to be
executed before (resp. after) the joinpoint.
The pointcut is specified by defining the following class attributes:
* `classes`
* `methods`
In a future version, the following will be supported as well:
* `get`
* `set`
Each of these class attributes is a regular expression (see
http://docs.python.org/library/re.html). Together they determine the classes
and methods where the aspect is to be applied.
.. note:: This is an abstract class, meant to be subclassed by concrete aspects.
"""
__metaclass__ = ABCMeta
classes = '.*'
methods = '.*'
@abstractmethod
[docs] def before(self, klass, method, obj, *args, **kwargs):
"""
Performs some actions before the execution of the method.
:param klass: The class of the joinpoint.
:param method: The method of the joinpoint (i.e. the method executed).
:param obj: The instance to which the method was applied.
:param args: The (positional) arguments to the method.
:param kwargs: The keyword arguments to the method.
:returns:
None: Indicates that execution proceeds normally and control
is passed to the method.
value: Any value. Indicates that the wrapper does not call the
method, and overrides it by returning value instead.
"""
pass
@abstractmethod
[docs] def after(self, klass, method, obj, retval, exc_type, exc_val, traceback):
"""
Performs some actions after the execution of the method.
:param klass: The class of the joinpoint.
:param method: The method of the joinpoint (i.e. the method executed).
:param obj: The instance to which the method was applied.
:param retval: The value which was returned by the method, if it
returned normally, or None if it raised an exception.
:param exc_type: The type of exception if one was raised, or None if
the method returned normally.
:param exc_val: The exception instance if one was raised, or None if
the method returned normally.
:param traceback: The traceback of the exception if one was raised, or
None if the method returned normally.
:returns:
None: Indicates that the wrapper should return retval when the
method returns normally or raise exc_val when the method
raises an exception.
value: Any value. Indicates that the wrapper overrides the
method's return value or exceptions, and returns value
instead.
"""
pass
def wrap(aspect, klass, original_method, modified_method):
def new_method(self, *args, **kwargs):
before_result = aspect.before(klass, original_method, self, *args, **kwargs)
if before_result is not None:
return before_result
exc = False
try:
retval = modified_method(self, *args, **kwargs)
except:
exc = True
finally:
after_result = aspect.after(klass, original_method, self, None, *sys.exc_info())
if after_result is None:
if exc:
raise
else:
return retval
else:
return after_result
return new_method
def joinpoint_match(pointcut_expr, joinpoint):
pointcut_class_expr, pointcut_method_expr = pointcut_expr
class_name, method_name = joinpoint
return pointcut_class_expr.match(class_name) is not None \
and pointcut_method_expr.match(method_name) is not None
[docs]def make_aspect_from_generator(generator):
"""
Makes an :py:class:`Aspect` class from a given generator. The idea is that
whatever comes in the generator function before a `yield` becomes the
*before* part of the aspect, and whatever comes after the `yield` becomes
the *after* part of the aspect.
:param GeneratorType generator: some generator (typycally a generator function).
:returns: An aspect with before and after methods
:rtype: :py:class:`Aspect`
"""
class NewAspect(Aspect):
def before(self, klass, method, obj, *args, **kwargs):
self.generator = generator(klass, method, obj, *args, **kwargs)
self.generator.next()
def after(self, klass, method, obj, retval, exc_type, exc_val, traceback):
try:
self.generator.send(retval, exc_type, exc_val, traceback)
self.generator.next()
except StopIteration:
pass
return NewAspect