Get names of positional arguments in wrapping function

Imagine this made-up decorator that tries to capture the arguments of the wrapped function and save them to some key-value store for logging purposes:

import json
from functools import wraps

some_path = ...

def log_args_to_json(func):

    @wraps(func)
    def wrapper(*args, **kwargs):
        # What to do with the args?
        data = json.dumps("func_name": func.__name__, "kwargs": kwargs)
        some_path.write_text(data)
        return func(*args, **kwargs)

    return wrapper

@log_args_to_json
def say_hello(name, suffix):
     print(f"Hello, {name}{suffix}")

With the following calls:

say_hello("John", "!") # File has: {}
say_hello("John", suffix="!") # File has {"suffix": "!"}

This decorator doesn’t store the *args because the destination is a key-value store (I chose a JSON file for simplicity, but this could go anywhere) and it doesn’t know the argument names. It could store it as a list, but that would be much less readable to a person viewing the logs (they wouldn’t know what argument each value corresponds to)

Obviously, the decorator can’t just know the names of the arguments, because they are positional - and would correspond to different names for each function. But python is able to parse the function’s signature and figure out which positional argument corresponds to each value.

I’m wondering if there’s any simple way for me to do this in my code. Maybe I’m missing something incredibly obvious here, but it sucked that I had to make all my arguments keyword only just so I could get the names, when there clearly is a way for Python to figure it out. The only soluiton I could think of is analyzing the names using the function’s __code__, but that seems like it would be complicated (Again, maybe it’s simple and I’m wrong here).

The inspect module has a lot of what you’re looking for!

2 Likes

It seems lik Signature.bind is exactly what I needed, that’s awesome! Thank you!

My fear with using a Signature object is this sentence from the documentation:

Raises ValueError if no signature can be provided, and TypeError if that type of object is not supported.

It sounds from this some callables aren’t supported. Do you happen to know when that might happen? I wouldn’t want to have an exception raised because of that, and would also like to know the limitations of signature.

AFAIK, this will only happen if the object is a builtin function or method and the implementor has not provided a __text_signature__ (or similar) attribute for inspect to use.

TypeError you will get if you pass in something that isn’t callable.

For normal python functions you don’t have to worry.

You’re talking about standard Python functions so you shouldn’t have to worry about it. There are certain things that can be called but aren’t normal functions (mostly C-implemented), and as Megalng says, TypeError means it’s not a callable.

Thank you! Now I just have to figure out how to get this through a code review😅

1 Like