3
\$\begingroup\$

A while ago, I came up with a function that is similar to this one: Decorator to log function calls and return values, but with a different implementation.

What this function does: when applied to a function as a decorator, it captures the positional/named arguments and values passed to the function and outputs results to console (print), or to an active logger that you specify. The default behavior is to print.

The original aim was to debug code, especially unattended scripts, loops etc, and review the logs to figure out what values certain functions are actually receiving. This approach is to make it easy and convenient to monitor any function. Also, the logging feature provides a timestamp which is often relevant information when troubleshooting code.
One design goal is to have one single declaration so that the decorator can be used either with or without logger argument.

Possible improvements:

  • customize the output message by way of format string or similar, for example to facilitate analysis by a log parser (eg JSON)
  • there may be a better way than this implementation of partial that I am not sure about

The code:


debugtools.py:

from functools import wraps, partial


def show_args(func=None, logger=None):
    if func is None:
        return partial(show_args, logger=logger)

    @wraps(func)
    def wrapper(*args, **kwargs):
        msg = f"{func.__name__}: (args={args}, kwargs={kwargs})"

        if logger is None:
            print(msg)
        else:
            logger(msg)

        return func(*args, **kwargs)

    return wrapper

main.py:

import logging
import sys

from debugtools import show_args


logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)


@show_args
def my_function_with_print(*args, **kwargs):
    pass

@show_args(logger=logger.debug)
def my_function_with_logging(*args, **kwargs):
    pass

my_function_with_print(1, 2, 3, 4, 5, f=6, comment="Using print")
my_function_with_logging(123, comment="Using logger")

Sample output:

my_function_with_print(args=(1, 2, 3, 4, 5), kwargs={'f': 6, 'comment': 'Using print'})
2023-05-03 22:08:58,520 - DEBUG - my_function_with_logging(args=(123,), kwargs={'comment': 'Using logger'})
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

I will note in passing that a StringIO buffer can be a useful logger. The use case would be to freeze the behavior of a unit test, and verify that future invocations do the same thing. The absence of (changing!) timestamps is a boon to a unit test that wants to do self.assertEqual.

If f() calls g() and so on, I wonder if we could achieve nice composition. Perhaps they would need some common default. Maybe a with context handler would establish that?


    if func is None:
        return partial(show_args, logger=logger)

I found this slightly tricky upon first reading. It accommodates the with / without parentheses notation, and is worth a # comment.


This all looks good to me. Ship it!

I would be willing to delegate or accept maintenance tasks on this codebase.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.