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.

Pyc. 32 An empty wrapper to overwrite default parameter values
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:

Pyc. 33 A typical wrapper that encloses an original function
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:

  1. Edit and/or prepare the call parameters for the original functionality

  2. Execute the original functionality with these modified call parameters

  3. Edit and/or revise the result and return this modified result

All this together could then look like this:

Pyc. 34 A typical replacement wrapper that encloses an original function
# 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.

Pyc. 35 A more complex wrapper with result transfer
# 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.

Pyc. 36 Building a private library of wrapper instances
# 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.


◄ prev

up

next ►

Legal Notice

Privacy Policy

Cookie Consent

Sphinx 7.2.6 & Alabaster 0.7.12

© Copyright 2020-, Martin Abel, eVation. All Rights Reserved.