Use __signature__ in help() and perhaps somewhere else too

Hi! *# First simple idea:* TL;DR: Let's show a inspect.Signature instance to the help messages instead of just writing its actual "vague" signature [usually (*args, **kwargs)]. Suppose we have a decorator that returns a wrapper: @functools.wraps(func) def wrapper(*args, **kwargs): ... # some code And suppose this decorator doesn't change the wrapped function signature (e.g. it just build some context or check some inputs). The code might have something like this (before returning the wrapper): wrapper.__signature__ = inspect.signature(func) It would help a lot if the list of valid arguments would be kept in the help message of the wrapped function. If the wrapper function changes the signature, one could just create another inspect.Signature object to properly document its behavior. Setting the __signature__ would suffice for that. Actually, the same applies to other functions, not just wrappers. This might not be possible beforehand as the parameter names/kinds/defaults might be dynamic (e.g. the distinct signatures of the wrapped functions). *# Second idea:* The partial objects might include an automatic propagation of the __signature__, if there's one already set. That can be lazy (the partial object __signature__ is created only when requested). *# Third idea:* Setting the function signature itself from an inspect.Signature object. That might be a new functools.wraps keyword argument, like: @functools.wraps(wrapped_func, signature=wrapped_func.__signature__) def wrapper_func(*args, **kwargs): ... # some code Or perhaps a new syntax, like: def wrapper_func(*args, **kwargs) with wrapped_func.__signature__: ... # some code Where the signature object is an inspect.Signature instance, or perhaps a callable to have its signature copied from. Externally, this would behave like a function with the given (more restrict) signature (e.g. raising TypeError if it doesn't match the given wrapped_func.__signature__ in the example). Internally, the function would just use the explicit args and kwargs (or whatever the explicit signature happen to be). As an example, suppose the wrapped_func has the (a, b, c=3, *, d=4) signature, the wrapper_func of the proposed syntax would behave like this for this example: def wrapper_func(a, b, c=3, *, d=4): def internal_wrapper_func(*args, **kwargs): ... # some code return internal_wrapper_func(a, b, c=3, *, d=4) The most obvious use cases are the creation of simple function signature validators (to avoid losing a parameter left untouched in the kwargs dict, to "fail fast" and fix the argument name in code) and the propagation of help messages documentation (e.g. to avoid showing just a "**kwargs" when the valid keyword arguments can be grabbed from the function that uses them). Regards, Danilo J. S. Bellini --------------- "*It is not our business to set up prohibitions, but to arrive at conventions.*" (R. Carnap)

On Nov 15, 2019, at 16:07, Danilo J. S. Bellini <danilo.bellini@gmail.com> wrote:
But that’s what already happens: >>> import functools >>> def f(a, b=2, **kw): ... print(a,b,kw) ... >>> @functools.wraps(f) ... def g(*args, **kw): ... return f(*args, **kw) ... >>> import inspect >>> inspect.signature(f) <Signature (a, b=2, **kw)> >>> inspect.signature(g) <Signature (a, b=2, **kw)> >>> help(f) Help on function f in module __main__: f(a, b=2, **kw) >>> help(g) Help on function f in module __main__: f(a, b=2, **kw) The signature is there, including with functions.wraps. So that’s your main idea, and your first idea.
# Second idea:
The partial objects might include an automatic propagation of the __signature__, if there's one already set. That can be lazy (the partial object __signature__ is created only when requested).
So that’s already there too.
# Third idea:
Setting the function signature itself from an inspect.Signature object.
>>> f.__signature__ = inspect.signature(h) >>> inspect.signature(f) <Signature (b=2, **kw)>
You can already add __signature__ to assigned if you want, which will copy it from wrapped_func, and I don’t know why you’d want to set any signature besides that one. But normally you don’t need to do that.
Why would you want to elevate __signature__ that way? What’s wrong with treating it the same as the other mutable attributes and just assigning it normally (or with a decorator)?
Externally, this would behave like a function with the given (more restrict) signature (e.g. raising TypeError if it doesn't match the given wrapped_func.__signature__ in the example).
But that’s not what signatures are for in Python. If you want to force the function to check a certain parameter signature, just make that the actual parameter list. Why make the parameter list *args, **kw instead, just to try to duplicate what you’d have gotten if you didn’t do that? And for the case of wrapper functions, you usually don’t have to do anything. If the arguments don’t match the intended signature, you’ll get a TypeError from the wrapped function, without having to do anything in the wrapper.
You can’t call a function that way. And if you could, you’d be ignoring any values the user passed in for c and d to force the defaults instead, which doesn’t sound like a good idea. What were you actually trying to do? Maybe (a, b, c, d=d)?
The most obvious use cases are the creation of simple function signature validators (to avoid losing a parameter left untouched in the kwargs dict, to "fail fast" and fix the argument name in code)
If you really want this, you can write a decorator that takes a signature and a function, and returns a function that binds the signature before passing the result on to the function. This is about 5 lines of code, and not that complicated. But you rarely want it. Again, you can just define your function with the right signature in the first place and you get this for free, or for a wrapper do nothing and also get it for free. Binding a signature is a lot slower, doesn’t work on uncooperative functions (like non-argclinic C functions), and doesn’t give you any benefits except in special cases (s.g., it could be useful for an auto-curry decorator that accumulates args into a partial until the entire signature is bound, and then calls the function). Maybe those special cases are common enough to put a helper in functools or something. But i don’t think they’re common enough for a whole new protocol, much less new syntax.

On Nov 15, 2019, at 16:07, Danilo J. S. Bellini <danilo.bellini@gmail.com> wrote:
But that’s what already happens: >>> import functools >>> def f(a, b=2, **kw): ... print(a,b,kw) ... >>> @functools.wraps(f) ... def g(*args, **kw): ... return f(*args, **kw) ... >>> import inspect >>> inspect.signature(f) <Signature (a, b=2, **kw)> >>> inspect.signature(g) <Signature (a, b=2, **kw)> >>> help(f) Help on function f in module __main__: f(a, b=2, **kw) >>> help(g) Help on function f in module __main__: f(a, b=2, **kw) The signature is there, including with functions.wraps. So that’s your main idea, and your first idea.
# Second idea:
The partial objects might include an automatic propagation of the __signature__, if there's one already set. That can be lazy (the partial object __signature__ is created only when requested).
So that’s already there too.
# Third idea:
Setting the function signature itself from an inspect.Signature object.
>>> f.__signature__ = inspect.signature(h) >>> inspect.signature(f) <Signature (b=2, **kw)>
You can already add __signature__ to assigned if you want, which will copy it from wrapped_func, and I don’t know why you’d want to set any signature besides that one. But normally you don’t need to do that.
Why would you want to elevate __signature__ that way? What’s wrong with treating it the same as the other mutable attributes and just assigning it normally (or with a decorator)?
Externally, this would behave like a function with the given (more restrict) signature (e.g. raising TypeError if it doesn't match the given wrapped_func.__signature__ in the example).
But that’s not what signatures are for in Python. If you want to force the function to check a certain parameter signature, just make that the actual parameter list. Why make the parameter list *args, **kw instead, just to try to duplicate what you’d have gotten if you didn’t do that? And for the case of wrapper functions, you usually don’t have to do anything. If the arguments don’t match the intended signature, you’ll get a TypeError from the wrapped function, without having to do anything in the wrapper.
You can’t call a function that way. And if you could, you’d be ignoring any values the user passed in for c and d to force the defaults instead, which doesn’t sound like a good idea. What were you actually trying to do? Maybe (a, b, c, d=d)?
The most obvious use cases are the creation of simple function signature validators (to avoid losing a parameter left untouched in the kwargs dict, to "fail fast" and fix the argument name in code)
If you really want this, you can write a decorator that takes a signature and a function, and returns a function that binds the signature before passing the result on to the function. This is about 5 lines of code, and not that complicated. But you rarely want it. Again, you can just define your function with the right signature in the first place and you get this for free, or for a wrapper do nothing and also get it for free. Binding a signature is a lot slower, doesn’t work on uncooperative functions (like non-argclinic C functions), and doesn’t give you any benefits except in special cases (s.g., it could be useful for an auto-curry decorator that accumulates args into a partial until the entire signature is bound, and then calls the function). Maybe those special cases are common enough to put a helper in functools or something. But i don’t think they’re common enough for a whole new protocol, much less new syntax.
participants (2)
-
Andrew Barnert
-
Danilo J. S. Bellini