Last active
January 23, 2025 15:02
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
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.
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
You can run the test and see their output with:
Tests output: