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
functhe target functionargsthe supplied args ( including theselfinstance as the first element)kwargsthe keywords argsresultthe result ( initiallyNone)exceptiona 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!