Reputation: 7020
How can I replace *args
and **kwargs
with the real signature in the documentation of decorated functions?
Let's say I have the following decorator and decorated function:
import functools
def mywrapper(func):
@functools.wraps(func)
def new_func(*args, **kwargs):
print('Wrapping Ho!')
return func(*args, **kwargs)
return new_func
@mywrapper
def myfunc(foo=42, bar=43):
"""Obscure Addition
:param foo: bar!
:param bar: bla bla
:return: foo + bar
"""
return foo + bar
Accordingly, calling print(myfunc(3, 4))
gives us:
Wrapping Ho!
7
So far so good. I also want my library containing myfunc
properly documented with Sphinx.
However, if I include my function in my sphinx html page via:
.. automodule:: mymodule
:members: myfunc
It will actually show up as:
Obscure Addition
How can I get rid of the generic myfunc(*args, **kwargs)
in the title? This should be replaced by myfunc(foo=42, bar=43). How can I change sphinx or my decorator mywrapper
such that the default keyword arguments are preserved in the documentation?
EDIT:
As pointed out this question has been asked before, but the answers are not so helpful.
However, I had an idea and wonder if this is possible. Does Sphinx set some environment variable that tells my module that it is actually imported by Sphinx? If so, I could simply monkey-patch my own wrappers. If my module is imported by Sphinx my wrappers return the original functions instead of wrapping them. Thus, the signature is preserved.
Upvotes: 8
Views: 2384
Reputation: 7020
I came up with a monkey-patch for functools.wraps
.
Accordingly, I simply added this to the conf.py
script in my project documentation's sphinx source
folder:
# Monkey-patch functools.wraps
import functools
def no_op_wraps(func):
"""Replaces functools.wraps in order to undo wrapping.
Can be used to preserve the decorated function's signature
in the documentation generated by Sphinx.
"""
def wrapper(decorator):
return func
return wrapper
functools.wraps = no_op_wraps
Hence, when building the html page via make html
, functools.wraps
is replaced with this decorator no_op_wraps
that does absolutely nothing but simply return the original function.
Upvotes: 5
Reputation: 110311
You ordinarily can't. That is because the variable names used as parameters in the wrapped function are not even present on the wrapped function - so Sphinx do not know about them.
That is a known complicated issue in Python - so much that recent versions - including not only Python 3, but also Python 2.7 included a __wrapped__
attribute on class decorated that make the proper use from functools.wraps
-
that way, upon inspecting the decorated function one is able to know about the actual wrrapped function by looking at __wrapped__
. Unfortunatelly, Sphinxs ignores the __wrapped__
, and show the info on the wrapper function instead.
SO, one thing to do is certainly to report this as a bug to the Sphinx project itself - it should take __wrapped__
in account.
A meantime workaround for that would be to change the wrapper function to actually include more information about the wrapped - like its signature - so you could write another function to be called in place of "functools.wraps" for your project, which does just that: pre-pend the function signature to its docstring, if any. Unfortunatelly, retrieving the function signatures in Python older than 3.3 is tricky - (for 3.3 and newer, check https://docs.python.org/3/library/inspect.html#inspect-signature-object ) - but anyway, for a naive form, you could write another version of "wraps" along:
def wraps(original_func):
wrap_decorator = functools.wraps(original_func)
def re_wrapper(func):
wrapper = wrap_decorator(func)
poorman_sig = original_func.__code__.co_varnames[
:original_func.__code__.co_argcount]
wrapper.__doc__ = "{} ({})\n\n{}".format (
original_func.__name__, ", ".join(poorman_sig),
wrapper.__doc__)
return wrapper
return re_wrapper
And use that instead of "functools.wraps". It would at least add a line with the parameter names, (but not th e defalt values) as first line in the docs.
---Hmm..maybe it would be easier just to patch Sphinx to use __wrapped__
before getting this done right.
Upvotes: 2