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:
myfunc(*args, **kwargs)
Obscure Addition
- Parameters:
- Returns:
foo + bar
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.
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.
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.