Skip to content

Instantly share code, notes, and snippets.

@pthom
Last active December 15, 2022 14:19
Show Gist options
  • Save pthom/e0a3b6819965c5a1eea8655289ca3805 to your computer and use it in GitHub Desktop.
Save pthom/e0a3b6819965c5a1eea8655289ca3805 to your computer and use it in GitHub Desktop.
Python decorator to log function call details (can include: input parameters, output parameters, return value)
import logging
import inspect
def verbose_function(dump_args: bool = True, dump_return: bool = False, dump_args_at_exit: bool = False):
"""
Decorator to print function call details.
This can include:
* input parameters names and effective values
* output parameters (if they were modified by the function)
* return value
:param dump_args: output function args
:param dump_return: output function return values
:param dump_args_at_exit: output function args at exit, for functions that modify their input args
"""
def inner(func):
def wrapper(*args, **kwargs):
def indent(s: str, indent_size: int):
return "\n".join(map(lambda s: " " * indent_size + s, s.split("\n")))
def do_dump_arg_multiline(arg):
arg_name = str(arg[0])
arg_value_str = str(arg[1])
if "\n" in arg_value_str:
return f"{arg_name} = \n{indent(arg_value_str, 4)}"
else:
return f"{arg_name} = {arg_value_str}"
def do_dump_args(joining_str: str):
try:
# For standard functions, inspect the signature
signature = inspect.signature(func)
func_args = signature.bind(*args, **kwargs).arguments
func_args_strs = map(do_dump_arg_multiline, func_args.items())
func_args_str = joining_str.join(func_args_strs)
arg_str = f"{func_args_str}"
except ValueError:
# For native functions, the signature cannot be inspected
annotated_args = map(lambda arg_and_idx: (f"arg_{arg_and_idx[0]}", str(arg_and_idx[1])), enumerate(args))
args_strs = list(map(do_dump_arg_multiline, annotated_args ))
kwargs_strs = list(map(do_dump_arg_multiline, kwargs.items()))
func_args_str = joining_str.join(args_strs + kwargs_strs)
arg_str = f"{func_args_str}"
return arg_str
def do_dump_fn_name():
try:
# For standard functions, inspect the signature
fn_str = f"{func.__module__}.{func.__qualname__}"
return fn_str
except AttributeError:
# For native functions, the signature cannot be inspected
fn_str = f"{func.__module__}.{func.__name__}"
return fn_str
join_str = "\n" if dump_args_at_exit else ", "
initial_args_str = f"{do_dump_args(join_str)}"
if not dump_args_at_exit:
initial_args_str = f"({initial_args_str})"
if not dump_args:
initial_args_str = ""
function_output = func(*args, **kwargs)
function_output_str = str(function_output)
final_args_str = do_dump_args(join_str)
function_name = do_dump_fn_name()
if not dump_args_at_exit:
if dump_return:
msg = f"{function_name}{initial_args_str} -> {function_output_str}"
else:
msg = f"{function_name}{initial_args_str}"
else:
msg = f"{do_dump_fn_name()}\n"
msg += f" args in : \n{indent(initial_args_str, 8)}\n"
msg += f" args out: \n{indent(final_args_str, 8)}\n"
if dump_return:
msg += f" return : \n{indent(function_output_str, 8)}"
logging.debug(msg)
return function_output
return wrapper
return inner
from dataclasses import dataclass
@dataclass
class TwoNumbers:
a: int = 0
b: int = 0
sum: int = 0
@verbose_function(dump_return=False, dump_args_at_exit=False)
def add_simple(two_number: TwoNumbers):
r = two_number.a + two_number.b
return r
@verbose_function(dump_return=True, dump_args_at_exit=False)
def add_dump_return(two_number: TwoNumbers):
r = two_number.a + two_number.b
return r
@verbose_function(dump_return=False, dump_args_at_exit=True)
def add_dump_exit(two_number: TwoNumbers):
two_number.sum = two_number.a + two_number.b
@verbose_function(dump_return=True, dump_args_at_exit=True)
def add_dump_return_exit(two_number: TwoNumbers):
two_number.sum = two_number.a + two_number.b
def test_dump_args_python_functions():
add_simple(TwoNumbers(1, 2))
add_dump_return(TwoNumbers(1, 2))
add_dump_exit(TwoNumbers(1, 2))
add_dump_return_exit(TwoNumbers(1, 2))
def test_dump_args_native_functions():
import cv2
import numpy as np
m = np.zeros((2, 2, 3), np.uint8)
m[:,:,:] = (1, 2, 3)
cv2.cvtColor = verbose_function(dump_return=True, dump_args_at_exit=True)(cv2.cvtColor)
m2 = np.zeros((2, 2, 3), np.uint8)
cv2.cvtColor(m, cv2.COLOR_BGR2GRAY, dst=m2)
@pthom
Copy link
Author

pthom commented May 3, 2022

You can run the test and see their output with:

pytest -o log_cli=true -o log_cli_level=DEBUG  verbose_function_decorator.py

Tests output:

============================= test session starts ==============================

verbose_function_decorator.py::test_dump_args_python_functions 
-------------------------------- live log call ---------------------------------
DEBUG    root:verbose_function_decorator.py:82 verbose_function_decorator.add_simple(two_number = TwoNumbers(a=1, b=2, sum=0))
DEBUG    root:verbose_function_decorator.py:82 verbose_function_decorator.add_dump_return(two_number = TwoNumbers(a=1, b=2, sum=0)) -> 3
DEBUG    root:verbose_function_decorator.py:82 verbose_function_decorator.add_dump_exit
    args in : 
        two_number = TwoNumbers(a=1, b=2, sum=0)
    args out: 
        two_number = TwoNumbers(a=1, b=2, sum=3)

DEBUG    root:verbose_function_decorator.py:82 verbose_function_decorator.add_dump_return_exit
    args in : 
        two_number = TwoNumbers(a=1, b=2, sum=0)
    args out: 
        two_number = TwoNumbers(a=1, b=2, sum=3)
    return  : 
        None
PASSED                                                                   [ 50%]
verbose_function_decorator.py::test_dump_args_native_functions 
-------------------------------- live log call ---------------------------------
DEBUG    root:verbose_function_decorator.py:82 None.cvtColor
    args in : 
        arg_0 = 
            [[[1 2 3]
              [1 2 3]]
            
             [[1 2 3]
              [1 2 3]]]
        arg_1 = 6
        dst = 
            [[[0 0 0]
              [0 0 0]]
            
             [[0 0 0]
              [0 0 0]]]
    args out: 
        arg_0 = 
            [[[1 2 3]
              [1 2 3]]
            
             [[1 2 3]
              [1 2 3]]]
        arg_1 = 6
        dst = 
            [[[0 0 0]
              [0 0 0]]
            
             [[0 0 0]
              [0 0 0]]]
    return  : 
        [[2 2]
         [2 2]]
PASSED                                                                   [100%]

============================== 2 passed in 0.72s ===============================

@pthom
Copy link
Author

pthom commented May 3, 2022

As a side note, this shows a probable bug in opencv-python: cv2.cvtColor(m, cv2.COLOR_BGR2GRAY, dst=m2) is supposed to modify m2 but it does not.

@stuaxo
Copy link

stuaxo commented Dec 14, 2022

This doesn't say much about defaults - here's `a modification of the version from the SO answer that does.
I wasn't sure about "native functions" - will this include anthing using cpython interface, could I test with pycairo ?

def dump_args(func):
    """
    Decorator to print function call details.
    This includes parameters names and effective values.
    """
    def wrapper(*args, **kwargs):
        try:
            # Grab the signaure and use it to get values of arguments and their defaults
            signature = inspect.signature(func)
            bound_args = signature.bind(*args, **kwargs)
            bound_args.apply_defaults()
            func_args_str = ", ".join(map("{0[0]} = {0[1]!r}".format, bound_args.arguments.items()))
            msg =  f"{func.__module__}.{func.__qualname__} ( {func_args_str} )"
        except ValueError:
            # For native functions, the signature cannot be inspected
            args_strs = map(lambda arg: f"arg_{arg[0]} = {arg[1]}", enumerate(args) )
            kwargs_strs = map(lambda kwarg: f"{kwarg[0]} = {kwarg[1]}", kwargs )
            func_args_str = ", ".join(list(args_strs) + list(kwargs_strs))
            msg =  f"{func.__module__}.{func.__name__} ( {func_args_str} )"
        logging.debug(msg)
        return func(*args, **kwargs)
    return wrapper

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment