AOP¶
It is possible to define different aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor.
Advice classes need to be part of classes that add a @advice()
decorator and can define methods that add aspects.
@advice
class SampleAdvice:
def __init__(self): # could inject dependencies
pass
@before(methods().named("hello").of_type(Foo))
def call_before(self, invocation: Invocation):
# arguments: invocation.args and invocation.kwargs
...
@after(methods().named("hello").of_type(Foo))
def call_after(self, invocation: Invocation):
# arguments: invocation.args and invocation.kwargs
...
@error(methods().named("hello").of_type(Foo))
def call_error(self, invocation: Invocation):
# error: invocation.exception
...
@around(methods().named("hello"))
def call_around(self, invocation: Invocation):
try:
...
return invocation.proceed() # will leave a result in invocation.result or invocation.exception in case of an exception
finally:
...
Different aspects - with the appropriate decorator - are possible:
before
methods that will be executed prior to the original methodaround
methods that will be executed around to the original method allowing you to add side effects or even modify parameters.after
methods that will be executed after to the original methoderror
methods that will be executed in case of a caught exception
The different aspects can be supplemented with an @order(<prio>)
decorator that controls the execution order based on the passed number. Smaller values get executed first.
All methods are expected to have single Invocation
parameter, that stores
func
the target functionargs
the supplied args ( including theself
instance as the first element)kwargs
the keywords argsresult
the result ( initiallyNone
)exception
a possible caught exception ( initiallyNone
)
⚠️ Attention: It is essential for around
methods to call proceed()
on the invocation, which will call the next around method in the chain and finally the original method.
If the proceed
is called with parameters, they will replace the original parameters!
Example: Parameter modifications
@around(methods().named("say"))
def call_around(self, invocation: Invocation):
return invocation.proceed(invocation.args[0], invocation.args[1] + "!") # 0 is self!
The argument list to the corresponding decorators control which methods are targeted by the advice.
A fluent interface is used describe the mapping.
The parameters restrict either methods or classes and are constructed by a call to either methods()
or classes()
.
Both add the fluent methods:
of_type(type: Type)
defines the matching classesnamed(name: str)
defines method or class namesthat_are_async()
defines async methodsmatches(re: str)
defines regular expressions for methods or classesdecorated_with(type: Type)
defines decorators on methods or classes
The fluent methods named
, matches
and of_type
can be called multiple times!
Example: react on both transactional
decorators on methods or classes
@advice
class TransactionAdvice:
def __init__(self):
pass
@around(methods().decorated_with(transactional), classes().decorated_with(transactional))
def establish_transaction(self, invocation: Invocation):
...
With respect to async methods, you need to make sure, to replace a proceed()
with a await proceed_async()
to have the overall chain async!