Generic Wrapper¶
As the name implies, a wrapper encloses the original function with an
(optional)
before
call functionality
and/or an
(optional)
after
call functionality.
This implementation additionally supports an
(optional)
replace
call functionality.
A generic wrapper is all the more broadly applicable, the more flexibly
these three activities can be formulated. All three decorator parameters,
before
, after
and replace
, can be combined with each other and
support both single callables and (nested) lists of F
-types
(imported from module decoratory.basic, see F signature below for details).
In addition, replace
supports passing a result object from successive
replacement calls through an optional keyword argument named result
with
a defaut value, e.g. result=None
.
Even without any of these arguments, such an empty wrapper can be used to overwrite default values, for example.
from decoratory.wrapper import Wrapper
# Case 1: Dynamic decoration with decorator arguments, only
def some_function(value: str = "original"):
print(f"value = '{value}'")
# Function call with default parameters
some_function() # value = 'original'
some_function = Wrapper(some_function, value="changed")
some_function() # value = 'changed'
The functionality of some_function()
itself remains unchanged. For the
sake of clarity, the principle of all or nothing is applied, i.e. defaults
must be defined for all parameters and they are only used if no current
parameters at all are transmitted. There is no mixing of current and default
parameters. Thus, even a call of the decorated function with an incomplete
parameter set is explicitly not supported and will raise a TypeError
.
A typical
scenario for a wrapper is, of course, the execution of additional functionality
before and/or after a given functionality, which itself remains unchanged,
such as enter/leave
markers, call data caches, runtime measurements, etc.
Here is a typical example:
from decoratory.wrapper import Wrapper
from decoratory.basic import F
# Case 2: Decoration with before and after functionalities
def print_message(message: str = "ENTER"):
print(message)
@Wrapper(before=print_message, after=F(print_message, "LEAVE"))
def some_function(value: str = "original"):
print(f"value = '{value}'")
some_function() # ENTER
# value = 'original'
# LEAVE
While before
calls print_message
with its default parameters the
after
parameter uses the F
-function from decoratory.basic
to overwrite this default. F
has a signature
F(callable, *args, **kwargs)
and encapsulates the passing of any function
with optional positional and keyword parameters. Accordingly, the keyword
parameter after=F(print_message, message="LEAVE")
would also be possible.
The idea behind the replace
option is not so much to replace the complete
original functionality, because you could simply create your own functionality
for that but to wrap the original functionality, e.g. according to the principle:
Edit and/or prepare the call parameters for the original functionality
Execute the original functionality with these modified call parameters
Edit and/or revise the result and return this modified result
All this together could then look like this:
# Case 3: Decoration with replace functionality
def replace_wrapper(value: str="replace"):
# 1. Edit and/or prepare the call parameters
value = value.upper()
# 2. Execute the original functionality with these modified parameters
result = some_function.substitute.callee(value)
# 3. Edit and/or revise the result and return this modified result
return f"result: '{result}'"
@Wrapper(replace=replace_wrapper)
def some_function(value: str = "original"):
print(f"value = '{value}'")
return value
result = some_function() # value = 'REPLACE'
print(result) # result: 'REPLACE'
The first output value = 'REPLACE'
comes from the original function
some_function()
but using value
modified to uppercase letters by the
replace_wrapper()
. The second line result: 'REPLACE'
is the result
of the return
revised by the replace_wrapper()
. Note the
highlighted line in the replace_wrapper()
: It is important not to
call the decorated some_function
but the original
some_function.substitute.callee
function to avoid self-recursion.
Warning
Wrapper — Avoidance of self-recursion in a replacement wrapper
In a replacement wrapper, the undecorated version of the original functionality must always be called. It is accessible via the substitute.callee attribute of the wrapper!
For the sake of completeness, a rather more complex example illustrates
the replacement of the original functionality with a sequence of replacement
functionalities, passing a result
object of type int
between
successive calls.
# Case 4: Decoration with before, after and multiple replacements
def print_message(message: str = "UNDEFINED"):
print(message)
def replacement_printer(add: int = 1, *, result=None):
result += add if isinstance(result, int) else 0
print(f"result = {result}")
return result
@Wrapper(before=F(print, "ENTER"), # Python's print()
replace=[F(replacement_printer, 1, result=0),
F(replacement_printer, 3),
F(replacement_printer, 5)],
after=F(print_message, "LEAVE"))
def result_printer(message: str = "UNKNOWN"):
print(message)
# Execute the decorated result_printer
result_printer() # ENTER (before)
# result = 1 (replacement_printer, 1)
# result = 4 (replacement_printer, 3)
# result = 9 (replacement_printer, 5)
# LEAVE (after)
# 9 (result_printer return)
The absence of the outputs of UNDEFINED
and UNKNOWN
reflects the
correct replacements by the decoration, and the order of execution is exactly
as expected: before
then replace
then after
and in each of these
variables the lists are processed in ascending order.
With Wrapper
and custom service functions, a private library of wrapper
instances can be built for reused.
# Case 5: Define a private wrapper library
before_wrapper = Wrapper(before=F(print, "BEFORE"))
after_wrapper = Wrapper(after=F(print, "AFTER"))
# Multiple decorations for specialized functionality encapsulation
@before_wrapper
@after_wrapper
def some_function(value: str = "original"):
print(f"value = '{value}'")
some_function() # BEFORE
# value = 'original'
# AFTER
For a quick and easy creation of typical wrapper decorators, the Decorator Arguments Methods Template from Pyc. 15 is also useful.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |