a lightweight object

A Lightweight Approach to Aspect-Oriented Programming in Python

Introduction
Features
Screenshots
Module interface
Implementation
    with_wrap
    wrap_around
(Old) examples
Download
Version history

Valid HTML 4.01 Transitional

aspects.py is a lightweight and low-level library for intercepting function calls. Functions and methods (also in Python standard library and third party code) can be wrapped so that when they are called, the wrap is invoked first. Depending on the wrap, the execution of the original function can be omitted, or the function can be called arbitrarily many times. Wraps are able to modify the call arguments and the return values of wrapped functions and handle exceptions. There can be many wraps on the same function. The wraps can be enabled, disabled and removed in any order. In the terminology of aspect-oriented programming, the library allows applying advices (wraps) to call join points of methods and functions in around fashion.

Features

In Python 2.1 and later (including Jython)
  • All other methods except built-in methods can be wrapped.
  • Wraps can be added, peeled, enabled and disabled on-the-fly.
  • Wraps can be enabled in all instances of a class or only in given instances.
  • Wraps can modify the arguments passed to the wrapped method.
  • Wraps can modify the return value returned from the wrapped method.
  • Wraps can handle exceptions raising from the wrapped method.
  • The source code of the wrapped methods and their classes is not needed. The byte code is enough.
  • Thread safe: several threads can execute the same wrapped methods.
Additional features in Python 2.5 and later
  • Functions (including many built-in functions) can be wrapped.
  • Any wrap can be removed.
  • Wraps are generators (that is, functions that yield)
  • The execution of a wrap can be continued (instead of restarting) when the wrapped method is called next time.

Screenshots

with_wrap and without_wrap in Python 2.5+

Wrapping methods and functions

aspects.with_wrap(wrap, func[, func]...[, instances]). In Python 2.5 and later you can wrap methods and functions with with_wrap. If you want to execute the wrap when the wrapped method is called through certain instances only, give the list of the instances with "instances" keyword argument.

>>> import aspects
>>> def mywrap(*args, **kwargs):
...     print "mywrap called with args",args
...     retval = yield aspects.proceed(*args, **kwargs)
...     print "mywrap catched return value:", retval
...     yield aspects.return_stop(retval)
... 
>>> def increase(x):
...     return x+1
... 
>>> class c(object):                  
...     def decrease(self, x):
...         return x-1
... 
>>> aspects.with_wrap(mywrap, increase, c.decrease)
[0, 1]
>>> increase(9)
mywrap called with args (9,)
mywrap catched return value: 10
10
>>> c().decrease(43)
mywrap called with args (<__main__.c object at 0xb7de044c>, 43)
mywrap catched return value: 42
42
>>> aspects.without_wrap(mywrap, increase)
<function mywrap at 0xb7a586bc>
>>> increase(9)
10
>>> c().decrease(43)
mywrap called with args (<__main__.c object at 0xb7a61dcc>, 43)
mywrap catched return value: 42
42

Catching exceptions from wrapped functions

>>> import aspects
>>> 
>>> def divide(x,y):
...     return x/y
... 
>>> def suppress_errors(*a, **kw):
...     try:
...         yield aspects.proceed
...     except Exception, e:
...         print "sshh!", e
...         yield aspects.return_stop(None)
... 
>>> aspects.with_wrap(suppress_errors, divide)
0
>>> divide(42, 2)
21
>>> divide(1, 0)
sshh! integer division or modulo by zero
>>> 

Wrapping built-in functions

Built-in functions can be wrapped as well. If a return value is not explicitely given in the wrap, the return value of the wrapped method or function is returned.

>>> import aspects
>>> def mywrap(*a, **kw):
...     print "args:", a, kw
...     rv = yield aspects.proceed(*a, **kw)
...     print "retval:", rv
... 
>>> import os
>>> import sys
>>> aspects.with_wrap(mywrap, os.open, os.close, sys.exit)
[0, 0, 0]
>>> fd=os.open("aspects.py", os.O_RDONLY)
args: ('aspects.py', 0) {}
retval: 3
>>> os.close(fd)
args: (3,) {}
retval: None
>>> sys.exit()
args: () {}

Wrap's execution can be continued

Yielding return_cont runs the wrapped function and returns the arguments and keyword arguments of the next function call. Note that when yielded proceed and return_(cont|stop) are not given parameters, the call arguments and the return value of the wrapped function call are used automatically.

>>> import aspects
>>> def foo(x):
...     print "foo(%s) = %s" % (x, x+42)
...     return x+42
... 
>>> def wrap(*a, **kw):
...     i=0
...     while len(a)>0 and a[0]>0:
...         i+=1
...         print "called",i,"times with arg[0] > 0"
...         yield aspects.proceed
...         a, kw = yield aspects.return_cont
...     yield aspects.proceed
... 
>>> aspects.with_wrap(wrap, foo)
0
>>> foo(1)
called 1 times with arg[0] > 0
foo(1) = 43
43
>>> foo(2)
called 2 times with arg[0] > 0
foo(2) = 44
44
>>> foo(3)
called 3 times with arg[0] > 0
foo(3) = 45
45
>>> foo(-1)
foo(-1) = 41
41
>>> foo(4)
called 1 times with arg[0] > 0
foo(4) = 46
46

wrap_around in Python 2.1+

aspects.wrap_around(method, wrap[, instances]). (This will be deprecated.) In Python 2.1 and later you can wrap methods inside your code. However, functions at module level cannot be wrapped.

>>> import aspects
>>> 
>>> def mywrap(self, *args, **kwargs):
...     print "mywrap called with args",args
...     retval = self.__proceed(*args, **kwargs)
...     print "mywrap catched return value:", retval
...     return retval
... 
>>> class c:
...     def decrease(self, x):
...         return x-1
... 
>>> aspects.wrap_around(c.decrease, mywrap)
0
>>> 
>>> c().decrease(43)
mywrap called with args (43,)
mywrap catched return value: 42
42

Module interface

  • with_wrap(wrap, func [, func] ... [, instances])
    Wraps the new-style (generator) wrap around all funcs. Returns the number of the wrap over a function (if only one function was wrapped), or a list of numbers of the wraps over the functions (if multiple functions were wrapped).
  • without_wrap(wrap_or_wrapid, func)
    Removes the new-style wrap around the func function. Returns the removed wrap.
  • disable_wrap(func, wrap_id)
    Disables the wrap with number wrap_id over the function func. Disabled wraps are omitted when func is called.
  • enable_wrap(func, wrap_id)
    Enables the wrap with number wrap_id over the function func.
  • wrap_is_enabled(func, wrap_id)
    Returns 1 if the wrap with number wrap_id over the function func is enabled, otherwise 0.
  • peel_around(func) (Will be deprecated.)
    Removes the topmost wrap over the function func. Returns the wrap.
  • wrap_around(method, wrap [, instances]) (Will be deprecated.)
    Wraps an old-style wrap around a method. Returns the number of the wrap over the method.
  • wrap_around_re(class, regexp, wrap)
    Wraps wrap around all methods whose names match to regexp.

More detailed specification can be found in aspects.py. Try:

>>> import aspects
>>> help(aspects)

Implementation

Yet the two functions, wrap_around (for Python 2.1+) and with_wrap (for Python 2.5+), do pretty much the same thing, their implementations are very different. The former is rather complicated. It uses locks for thread safety and adds several entries into the namespace of the class of the method being wrap. The latter is simpler and runs faster.

Implementation of with_wrap

If a function (or a method) f is wrapped in an advice using with_wrap, the function f will be replaced by a "middleman" function in the dictionary of its module (or its class). The middleman knows both function f and the advice. Due to the replacement, the middleman will be invoked instead of f in the future when module.f is called.

When the middleman is invoked, it has two options. It either

  • creates a new instance of the advice generator by calling it with the arguments with which the middleman was called, or
  • continues the execution of an existing advice generator by sending it the call arguments.

Indeed, the middleman and the advice run as coroutines, which were made possible by enhanced generators in Python 2.5.

When the advice is running, the middleman waits for it to yield. The future behaviour depends on the object that the advice yields. The possibilities are
Yielded objectFuture action
proceed class
rv = yield aspects.proceed
the wrapped function f is called with the arguments with which the middleman was called. If an exception is raised from f, the middleman will pass it to the advice. The return value of f will be sent to the advice. If the advice does not yield anything after proceed, the same return value will be returned by the middleman.
proceed object
rv = yield aspects.proceed(x,y)
otherwise the same as above, but the function f is called with the arguments that were given to the constructor of the proceed object.
return_stop class
yield aspects.return_stop
the middleman returns the value that was returned by the last call of f. The execution of the advice generator is stopped: when the middleman is called next time, the advice will be restarted.
return_stop object
yield aspects.return_stop(42)
the middleman returns the value given as the argument to the constructor of return_stop. The execution of the advice is stopped.
return_cont class
args, kwargs = yield aspects.return_cont
the middleman returns the value that was returned by the last call of f. The execution of the advice will be continued. When the middleman is called next time, the same advice generator instance will be sent the new call arguments.
return_cont object
args, kwargs = yield aspects.return_cont(42)
otherwise same as above, but the middleman returns the value given as the argument to the constructor of return_cont.
get_wrapped class
fun = yield aspects.get_wrapped
the middleman sends function f to the advice. Note that f may be the original function or the middleman of a lower wrap.

Implementation of wrap_around

When wrap_around is called with a method of a class and an advice (a function) as its parameters, wrapping is implemented in the following way:

  1. It is ensured that __proceed method exists in the class. If there is no such method, then it is plugged into the class.
  2. The wrapped method foo is renamed to __wrappedifoo, where i is the number of wraps around the method before wrapping it. Initially i=0.
  3. The advice is plugged into the class by name __wrapifoo, where i is the same as in the previous step.
  4. A new method is created and plugged into the class. The method is given the name of the original method. The new method pushes __wrappedifoo to __proceed_stack of the running thread and calls __wrapifoo. When the call finishes, the method pops the element from the same __proceed_stack and returns the return value of __wrapifoo.
  5. After plugging the new method, i is incremented and stored to __wrapcountfoo-attribute.

Now, consider the following example:

>>> import aspects
>>> class c:
...     def foo(self):
...         print "foo"
... 
>>> def adv(self, *a, **kw):
...     print "before"
...     rv = self.__proceed(*a, **kw)
...     print "after"
...     return rv
... 
>>> aspects.wrap_around(c.foo, adv)
>>> o=c()
>>> o.foo()
before
foo
after

In the example, the function calls caused by line o.foo() are:

c.foo           # this is the new method created by wrap_around
c.__wrap0foo    # this is actually function adv, prints "before"
c.__proceed     # this function is called by adv, the code is in
                # aspects.py
c.__wrapped0foo # this is the original method foo, prints "foo"

Old examples

These examples demonstrate the use of wrap_around.

Tracer

A tracer advice is defined in the tracer_advice.py module. The advice prints the name of the wrapped method, the class of the method and the class of the instance whose method is called. Then the advice passes control to the wrapped method and finally returns the return value of the wrapped method.

The tracer is used in the tracer_example.py module. This example demonstrates how the same advice can be used in several classes and around several methods. It also shows that a method can be wrapped in a derived class DD1 even if it is defined and already wrapped in its base class.

In this situation some attention should be paid to the order in which the methods are wrapped in the classes when using wrap_around. If the method is wrapped first in the derived class and there are no wraps around methods of its base classes, then the proceed stack is created into the derived class. Now, if the same method of the base class is wrapped later on, the wrap in the base class will not be executed. Therefore, it is better to wrap first the methods of the base classes and then the methods of the derived classes.

Timeout

Timeout advice generator create_timeout_advice is given in timeout_advice.py. The generator takes two optional parameters:

  1. time to wait before timeout in seconds, the default is 1.0
  2. a value which is
    • 0, if you do not want the advice to catch all exceptions coming through the wrapped method. Note that in this case any such exception halts your program.
    • 1, if you wish the advice to catch all exceptions (this is the default value).

The generator returns a function that can be used as an advice in wrap_around calls.

Assume that timeout advice t is wrapped around method m. When m is called, t receives control. It initialises its timer and starts a new thread. The thread calls __proceed and thus passes control to the original method. When the method finishes, the thread saves the return value to a variable which can be accessed by the timeout advice.

In the meantime, the timeout advice waits for the thread to finish or time to run out. If time runs out, t returns None and leaves the thread running on its own. If the thread finishes before the timeout, t returns the value saved by the thread.

There are two examples where a timeout advice is used. First, timeout_example.py is a simple example that demonstrates how the timeout advice works when catching exceptions.

The second example is a more sophisticated, ``real-life'' program httpget.py. This program wraps timeout aspect around open method of URLopener class. The class is located in urllib which is a part of the standard library included in Python package. urllib provides high-level interface for fetching data from given URLs.

httpget takes URLs as command line parameters and prints their contents in standard output (stdout). If the contents cannot be read before timeout, which is as short as one second, nothing is printed in stdout. In addition, fetching every URL creates a result that is printed in standard error. The result contains information about the successfulness and duration of the operation.

What is remarkable in httpget is that the behaviour of the standard library is altered to match what is wanted without touching its code. Here the timeout code is merged inside the library: our httpget program calls library function urlopen, which uses URLopener class like before.

It has come to my attention that the timeout example does not work in Jython (thanks to Xavier Farrero for pointing this out).

Conclusions and comparisons

aspects.py module implements one low-level functionality which helps to take a step towards AOP in Python. It does not provide a way to pack all aspect-related data in the same structure or any wildcards to define several join points in one pointcut like AspectJ does. But on the other hand, it does not introduce new syntax and it all can be done in a nice and dynamic Python way: while the program is running.

The Pythius project has produced utilities to assess the quality of Python code. The Pythius package also includes code for AOP in a much higher level than presented here.

However, there are some drawbacks in the Pythius's approach to AOP. For example, the classes to which aspects are applied must contain certain lines of code: the class defines exactly one aspect which is applied to it. In aspect oriented paradigm it would be more convenient to let the aspect define join points for its advices. This low-level implementation leaves this question open: you can define advices in one place and classes in another and bind them together in a third place. This makes modules that contain advices (such as tracer and timeout modules above) very reusable.

Nowadays, there are a number of other implementations.

Download

Latest version: python-aspects-1.3.tar.gz

Aspects library is released under LGPL 2.1.

Version history

python-aspects-1.3.tar.gz released in October, 2008. What's new:

  • with_wrap(w, instance.method) wraps the execution of the method only when it is bound to the instance. That is, the result is equal to wrapping
    	with_wrap(w, class_of_instance.method, instances=[instance])
          
    Thanks to Leandro Rodrigo Saad Cruz for pointing out a bug in the handling of bound methods.
  • GNUmakefile uses the PYTHON environment variable. That is, you can run the unit tests and install the aspects library to different Pythons as follows:
    	PYTHON=python2.4 make check
    	PYTHON=python2.5 make check
    	PYTHON=python2.6 make check
    
    	PYTHON=python2.5 make install
    	PYTHON=python2.6 make install
          

python-aspects-1.2.tar.gz released in October, 2008. What's new:

  • without_wrap() function for removing arbitrary wraps around functions or methods. This works for new-style wraps only. So far, only the topmost wraps could be peeled.

aspects-1.1.tar.gz released in April, 2008. What's new:

  • Built-in function wrapping improved (fixes a bug in finding a built-in module)

aspects-1.0.tar.gz released in September, 2007. What's new:

  • with_wrap() function for new-style wraps.
Furthermore, package was changed to be more friendly for both users and developers
  • GNUmakefile included for
    • execution of unit tests (make check)
    • creation of distribution package (make dist)
    • installation (make install)
    • clean-up (make clean)
  • New unit tests
  • setup.py included for standard installation and distribution package creation.

aspects-0.6.tar.gz released in June, 2007. What's new:

  • Thread safety. Multithreading was not considered in __proceed, wrap_around and peel_around functions before this release. For thread safety, it was necessary to replace a single class variable __proceed_stack with a separate stack for every thread. Thus, self.__proceed_stack is no more a list, but it is a function that returns the stack (of list type) of the thread in which it was called. Therefore, those advices that use the stack do not work anymore (like the tracer example). However, everything should work again if you substitute __proceed_stack for __proceed_stack(). I'm grateful to Miguel Branco at CERN for his contribution!
  • Updated examples.
  • Performance of disabled advices optimized.

aspects-0.5.tar.gz released in April, 2007. What's new:

  • wrap_around(method, advice, instances=[]) can be given a list of instances whose method calls will be wrapped. The advice is omitted if the method is called for other instances. (In the implementation, the instance is identified with id() function.)

aspects-0.4.tar.gz released in March, 2007. What's new:

  • wrap_around(method, advice) returns the number of the new wrap made from the advice on the method. Numbering starts from zero.
  • enable_wrap(method, wrapnum) and disable_wrap(method, wrapnum) switch wraps on and off. If a wrap is disabled when the method is called, the associated advice is not executed.
  • wrap_is_enabled(method, wrapnum) returns 1 (0) when the wrap is enabled (disabled).
  • wrap_count(method) returns the number of wraps on the method. You can, for example, disable all wraps by executing: for i in range(wrap_count(m)): disable_wrap(m,i)
  • unit tests have been moved to aspects_test.py file. Tests passed with Python ("CPython") versions 2.1, 2.3, 2.4 and 2.5. Other versions have not been tested.

aspects-0.3.tar.gz released in May, 2006. Nothing is changed in the library itself, but the type checking example is extended to support also checking the types of keyword arguments. (Thanks to Pedro Salgado for his contribution on this.)

New version, 0.2, of the aspects library, aspects-0.2.tar.gz has been released in November, 2004. Changes:

  • the new version of the library has been released under GNU Lesser General Public License. LGPL will be used in the later releases.
  • wrap_around_re(class, method-name-regexp, advice) has been added to the library. (Thanks to Tiago Cogumbreiro!)
  • an example of implementing type checking with aspects has been added to the package
  • now private methods can be wrapped

The first release in August, 2003: aspects.tar.gz.


Antti Kervinen email: ask@cs.tut.fi
Back to my homepage
Last modified: October 11, 17:00:00 EET 2008