1

Suppose that we have some function:

def funky_the_function(pos_zero, pos_one, *, default=None, sep = " "):
    pass

Can you write a function-decorator which prints the function call?

That is, we want to print the function name along with the to-stringed arguments substituted into the function parameters.

@print_calling_args
def funky_the_function(pos_zero, pos_one, *, default=None, sep = " "):
    pass

funky_the_function(1, 2, sep = "|-|")

# prints: `funky_the_function(1, 2, sep = "|-|")`  

Some Requirements:

  • the names of key-word arguments must be shown provided that the key-word argument is not assigned a default value. For example, we might print kwarg = 99
  • the names of positional arguments must be NOT shown. we print 1, 2 not a = 1, b = 2
  • default values must not be printed.
  • Each stringed argument must be at most 10 characters long. The 10 character limit does not include the keyword if the argument is a keyword argument
  • Each stringed argument must not contain line-breaks
  • We must have a function name for functors (class objects with a __call__ method)

Approach One

def print_calling_args(f):
    assert(callable(f))
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        sargs     = ", ".join(str(arg) for arg in args)
        skwargs   = ", ".join(kw + " = " + str(arg) for kw, arg in kwargs.items())
        all_sargs = ", ".join((sargs, skwargs))
        print(f.__qualname__, "(", all_sargs, ")", sep = "")
        return f(*args, **kwargs)
    return wrapper

There are some issues with Attempt One. I will explain thee issues later.

Approach Two (Using inspect.getcallargs)

import inspect

def print_calling_args(f):
    assert(callable(f))
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        cargs = inspect.getcallargs(f, *args, **kwargs)
        print(cargs)
        all_sargs = ", ".join(kw + " = " + str(arg) for kw, arg in cargs.items())
        print(f.__qualname__, "(", all_sargs, ")", sep = "")
        return f(*args, **kwargs)
    return wrapper

Approach Three (Using inspect.signature)

import functools
import inspect

def print_calling_args(f):
    assert(callable(f))
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        siggy = inspect.signature(f)
        bound_siggy = siggy.bind(*args, **kwargs)
        all_sargs = ", ".join(kw + " = " + str(arg) for kw, arg in bound_siggy.arguments.items())
        print(f.__qualname__, "(", all_sargs, ")", sep = "")
        return f(*args, **kwargs)
    return wrapper

Mistakes with my Attempts

Mistake: Printing Line Breaks and Overly Long Strings

If you convert an argument to string by using str(arg) then the resulting string can contain new-line characters \n or carriage returns \r. For example, suppose you pass a pandas dataframe into a function?

  • I require that the stringed arguments all fit on one line (no line feeds)
  • I require that string arguments be shorter than one mile long (we must truncate the string)

We can truncate the strings as follows:

def truncate(stryng:str, nchars:int = 10):
    # BEGIN ERROR CHECKING
    stryng = "".join(str(ch) for ch in stryng)
    nchars = int(str(nchars))
    # END ERROR CHECKING
    snip = " [...]"
    if nchars > len(stryng):
        return stryng
    return (stryng[:nchars - len(snip)] + snip)[:min(len(stryng), nchars)]

We can also get rid of the line-feeds (\n), etc....

def sani(whole:str) ->str:
    """
        gets rid of all of the line-feed characters, 
        carriage-return characters, and other nasty characters in a string

        INPUT:
            A passage of English text containing new line character
                "hello\nworld"
                "hello" + chr(10) + "world"
        OUTPUT:
            string containing back-slash followed by a letter "n"
                "".join(["hello", chr(92), chr(110), "world"])  
    """  
    result = "\\".join(repr(part)[1:-1] for part in whole.split("\\"))
    return result

Mistake: Cannot Get the Name of a Functor

Another issue with my attempts is that Functors (also known as "function objects") do not have an attribute named __qualname__. That is, if you define a class which has a __call__ method, then we cannot easily print the function name to string. I require that we print something intelligible for functors; maybe the name of the class they are instantiated from followed by ().

funky = Functor()
funky = decorate(funky)
r = funky(1, 2, 3, kwargy = int("python", 36))

# prints:
#     Functor()(1, 2, 3 kwargy = 1570137287)

Here is a functor:

# EXAMPLE OF A `FUNCTOR`
class Multiplier:   
    def __init__(self, m:int):
        self._m = int(str(m))
    def __call__(self, num:float)
        return num * self._m;

multi_the_multiplier = Multiplier(10**6)

# Decorate `multi_the_multiplier` with ` print_calling_args`
multi_the_multiplier = print_calling_args(multi_the_multiplier)

# ERROR: `multi_the_multiplier` does not have a `__qualname__`

r = multi_the_multiplier(1)
r = multi_the_multiplier(2)
r = multi_the_multiplier(3)
Toothpick Anemone
  • 4,290
  • 2
  • 20
  • 42
  • Unless this is just an academic exercise, this reeks of the XY problem (https://en.wikipedia.org/wiki/XY_problem). Do you need this for some real-world application? – jr15 Aug 10 '22 at 19:43
  • 2
    What is wrong with attempt 1? It seems to get the name of the function correctly and there are no utterly long strings – DeepSpace Aug 10 '22 at 19:45
  • Does this answer your question? [Print/log names and values of all parameters a function was called with (without eval)](https://stackoverflow.com/questions/62353138/print-log-names-and-values-of-all-parameters-a-function-was-called-with-without) – fsimonjetz Aug 10 '22 at 19:52
  • In your decorator you can run additional check for functor. E.g. `if getattr(f, '__call__', None): callable_name = f.__call__.__qualname__` Here, f is a Functor instance. Returns `Functor.__call__`. – user47 Aug 10 '22 at 19:56
  • 1
    @fsimonjetz My question is almost a duplicate of the the question you linked to. However, I require a solution which works for class objects having a `__call__` method. – Toothpick Anemone Aug 10 '22 at 20:01
  • @jr15 An example of a real-world application would be to print a tree of function calls. `foo(1,2, 3)` called `bar("a", "b", "c")` and `bar` called `baz*(` It would be nice to use a logging library to print a log of everything that a program did. We could print `"while John was making dinner:\n\tJohn made a salad\n\t\tJohn chopped some lettuce"` – Toothpick Anemone Aug 10 '22 at 20:17
  • @DeepSpace You pointed out that for the test case I provided my code gets the function name without raising any exceptions and the stringed argument list looks pretty. However, most use-cases are not that simple. What if there is an input argument `arg` such that `len(str(arg)) > 100` or `"\n" in str(arg)`? That is not uncommon. – Toothpick Anemone Aug 10 '22 at 20:20
  • 1
    So this question becomes about "pretty" string formatting? For the `\n` case you should be able to use `repr` instead of `str` – DeepSpace Aug 10 '22 at 20:24
  • 1
    @SamuelMuldoon if you're interested in printing function calls doesn't `traceback.print_stack()` help? It can be curated to list only the calls (invocation points) and no other information. – user47 Aug 10 '22 at 20:36

0 Answers0