New function `repr_args` in `pprint` or `reprlib`

During writing representation string builders, an often practice is to represent every argument from a known tuple-like structure and every keyword argument from a known dict-like structure using a variant of

arg_repr = []
arg_repr.extend(map(repr, args))
arg_repr.extend(f"{k}={v!r}" for k, v in kwargs.items())
return f"{type(self).__name__}({', '.join(arg_repr)})"

And well, I know some may disagree, but I’m pretty sure that a substantial amount of Python programmers would fall into writing this routine at some point.

Check out the following links:

This is not the only syntax you can write that in, and not all the results are on-topic to this proposal, but it seems like a pretty popular theme, which also happens to reside in the standard library, as visible above.

I’m tempted to think:

  • the cost of bringing this functionality in the stdlib to limit that boilerplate is small enough–a simple problem with a simple solution (yet multiline),
  • the benefit is high enough–it is an often repr-related problem, and the stdlib can already do some lifting for us.

Which leads me to believe it could be a nice, though not a super-important addition to pprint or reprlib.

I’m thinking copying over the existing implementation from asyncio (cpython/Lib/asyncio/format_helpers.py at 98b2ed7e239c807f379cd2bf864f372d79064aac · python/cpython · GitHub) could do the trick, maybe with a little bit of modifications, that is: parens being optional, a name akin to repr_args or whatever that maybe doesn’t strictly cause associations with being positional/keyword (no idea honestly), and a customizable callback for formatting values (default being repr).

What do you think?

9 Likes

+1. Had to write it numerous times and have seen many variants of it. And some incorrect implementations too. reprlib seems sensible.

Also, naming could be better. It is not representing args, but rather signature.

Also, it probably should be in line with recursive_repr so that repr comes after.

So maybe sig_repr / signature_repr?

I’m pretty sure it does not represent a signature.

A signature is part of a callable. We’re representing an undescribed set of arguments/keyword arguments, not parameters.

I was leaning towards pprint because reprlib’s main focus is to produce representation strings with size limits applied. repr_args has nothing to do with those limits.
pprint on the other hand aims to “provide a capability to “pretty-print” arbitrary Python data structures in a form which can be used as input to the interpreter”, which makes it far more suitable.

Yeah, you are right. It is not a signature. Then arg_repr? As per variable name in your example.

According to those definitions it would make sense, but from my experience, I intuitively go to reprlib for __repr__ utilities, and I go to pprint when I need to pretty print some big containers/structures.

I think “practicality beats purity” is somewhat applicable here.

Can be ambiguous whether it represents one argument or many, and whether to associate it with being positional (“arg” just feels positional, doesn’t it).

Maybe a slightly longer name format_args_and_kwargs as in asyncio is just good enough.

True, I’d also intuitively feel like reprlib is supposed to have all-about-repr things.

Especially since it already has recursive_repr(), which is also not contributing to the “limits” goal.

Not necessarily, but I see your point. Maybe something in the middle then. That one is very verbose and I think repr is a good word to have in it as it is a “representation of a call that would ideally reconstruct the object”.

repr_call—that one sounds tantalizing, but it takes two (three) to tango (ast.Call consists of a func in addition to args and keywords, so I think we don’t really represent a full call in that sense).

repr_args_and_kwargs is a name that simply works I guess.

I didn’t completely understand why so. I think it should also take func as an (maybe optional) arg and do:

def ???(..., func=None, module=False):
    if func is not None:
        name = func.__qualname__
        if module:
            name = func.__module__ + '.' + name

So that it could be a complete __repr__ if needed.

But idk, something is off to me about naming it call, just not intuitive.

Although I have built such representation strings only once in my ~50 Python projects, it feels like a useful and easy enough addition to the standard library :slightly_smiling_face:

as_python_statement(...)

In failprint/src/failprint/formats.py at 807a2346d657e6684bf91f84365813530a87d32e · pawamoy/failprint · GitHub.

def as_python_statement(func: Callable, args: Sequence | None = None, kwargs: dict | None = None) -> str:
    """Transform a callable and its arguments into a Python statement string.

    Arguments:
        func: The callable to transform.
        args: Positional arguments passed to the function.
        kwargs: Keyword arguments passed to the function.

    Returns:
        A Python statement.
    """
    callable_name = _get_callable_name(func)
    args_str = [repr(arg) for arg in args] if args else []
    kwargs_str = [f"{k}={v!r}" for k, v in kwargs.items()] if kwargs else []
    arguments = ", ".join(args_str + kwargs_str)
    return f"{callable_name}({arguments})"

(don’t pay too much attention to the wording or type annotations)

Sounds like a great candidate for “preferably one best way” to do it. +1.

1 Like

I’ve done this before also. Also done it with an inspect signature. It’s simple yet useful enough. +1.

Would be good to have module=False to add f'{func.__module__}.{func}'.

Also, maybe use __qualname__?

And finally, I don’t think _get_callable_name complexity needs to be a part of this. Instead, (func=None, ...) could omit name altogether and user could prepend whatever alternative is needed.

This function is actually already available as unittest.mock._format_call_signature, with the only missing part being that we need to obtain the name of callable for the call:

from unittest.mock import _format_call_signature

print(_format_call_signature(print.__name__, (1,), {'sep': '\n'}))

This outputs:

print(1, sep='\n')

So maybe we can move this private function to reprlib and make the new public function call the private function with the name of a given callable to avoid duplicate code in the stdlib.

1 Like

In this way the working code will be under unittest.mock. Quite confusing.
IMO the new code should use reprlib, and old code should adopt reprlib, if there’s someone so willing to do the cleaning work. 4 lines of duplicated code is not the end of the world.

No, what I suggested is that we can move the current private function to reprlib, and have unittest.mock import the private function from reprlib instead.

1 Like