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.
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).
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.
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.
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.
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
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)
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.
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.