Source code for libutilitaspy.aspects.core

"""
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 WeaverMetaClassFactory(*aspects): """ This factory function produces a meta-class which weaves the given aspects in all classes which are instances of the meta-class. :param aspects: a sequence of :py:class:`Aspect` instances :returns: An aspect weaver meta-class (to be assigned to the `__metaclass__` attribute of the client classes. """ assert all(isinstance(aspect, Aspect) for aspect in aspects) pointcut_exprs = [(re.compile(aspect.classes), re.compile(aspect.methods)) \ for aspect in aspects] aspects_and_pointcuts = zip(aspects, pointcut_exprs) class WeaverMetaClass(type): def __new__(cls, class_name, bases, class_dict): for attribute_name, attribute in class_dict.items(): if type(attribute) == FunctionType: new_attribute = attribute joinpoint = (class_name, attribute_name) for aspect, pointcut_expr in aspects_and_pointcuts: if joinpoint_match(pointcut_expr, joinpoint): new_attribute = wrap(aspect, cls, attribute, new_attribute) class_dict[attribute_name] = new_attribute return super(WeaverMetaClass, cls).__new__(cls, class_name, bases, class_dict) return WeaverMetaClass
[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