|
|
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.
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.
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
- 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)
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.
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 object | Future 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. |
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:
- It is ensured that __proceed method exists in the class. If
there is no such method, then it is plugged into the class.
- The wrapped method foo is renamed to __wrappedifoo,
where i is the number of wraps around the method before
wrapping it. Initially i=0.
- The advice is plugged into the class by name
__wrapifoo, where i is the same as
in the previous step.
- 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.
- 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"
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:
- time to wait before timeout in seconds, the default is 1.0
- 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.
Latest version: python-aspects-1.3.tar.gz
Aspects library is released under LGPL 2.1.
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.
|