sphinx.ext.autodoc: Keeping names of constants in signature
I'm using S开发者_开发知识库phinx's autodoc feature to document my API.
Example:
DEFAULT_OPTION = 'default'
def do_something(msg, option=DEFAULT_OPTION):
print msg
The generated documentation now shows the following signature:
do_something(msg, option='default')
How can I tell Sphinx to keep the name of the constant value i.e.
do_something(msg, option=DEFAULT_OPTION)
?
Is there an option I have overlooked? If at all possible, I'd like NOT to write all signature by hand again.
Since version 4.0 of Sphinx there is a new configuration option (autodoc_preserve_defaults
). Setting
autodoc_preserve_defaults = True
in your conf.py
will preserve the default values as they are in the source code.
You probably have to override the signature by hand in the reST file.
It's hard to come up with a better answer. Autodoc imports the modules it documents, so all module-level code (including default function arguments) is executed.
See also these similar questions: here and here.
Update:
I just realized that there is another option. You can override the signature by including it as the very first line of the docstring. See the documentation of the autodoc_docstring_signature configuration variable, and this answer.
You can get the signature with constant names from an AST and "unparse" it back to Python code.
Put this in your conf.py
file:
import ast
import inspect
from unparser import Unparser
unparse = Unparser()
def get_signature_from_ast(app, what, name, obj, options, signature,
return_annotation):
if what in ('class', 'exception', 'function', 'method'):
remove_args = 0
if what == 'method':
remove_args += 1 # Remove self from instance methods.
while True:
if inspect.isclass(obj):
obj = obj.__init__
elif inspect.ismethod(obj):
remove_args += 1 # Remove self from instance methods.
obj = obj.__func__
elif hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
else:
break
filename = sys.modules[obj.__module__].__file__
with open(filename) as file:
node = ast.parse(file.read(), filename)
lineno = obj.__code__.co_firstlineno
for n in ast.walk(node):
if isinstance(n, ast.FunctionDef) and n.lineno == lineno:
signature = '(' + unparse.argspec(n.args, remove_args) + ')'
if n.returns:
return_annotation = unparse.expr(n.returns)
break
return signature, return_annotation
def setup(app):
app.connect('autodoc-process-signature', get_signature_from_ast)
And this in some unparser.py
file, importable from conf.py
:
Note: This "unparser" probably has many bugs.
import ast
from itertools import zip_longest
class _Ast(object):
"""Type that returns a dummy type on failed attribute access.
Used for backwards compatibility when accessing new types in the :mod:`ast`
module.
"""
def __getattribute__(self, attr):
"""Return a type from :mod:`ast` or a dummy type when the attribute
does not exist in the module.
"""
return getattr(ast, attr, type(self))
_ast = _Ast()
class Unparser(object):
"""Unparse an AST back to Python code.
Supports only expressions, up to Python 3.3.
"""
#: Mapping of AST types to Python code strings.
ast_symbols = {
# Boolean binary operators.
_ast.And: 'and',
_ast.Or: 'or',
# Binary operators.
_ast.Add: '+',
_ast.Sub: '-',
_ast.Mult: '*',
_ast.Div: '/',
_ast.FloorDiv: '//',
_ast.Mod: '%',
_ast.LShift: '<<',
_ast.RShift: '>>',
_ast.BitOr: '|',
_ast.BitAnd: '&',
_ast.BitXor: '^',
# Comparison operators.
_ast.Eq: '==',
_ast.Gt: '>',
_ast.GtE: '>=',
_ast.In: 'in',
_ast.Is: 'is',
_ast.IsNot: 'is not',
_ast.Lt: '<',
_ast.LtE: '<=',
_ast.NotEq: '!=',
_ast.NotIn: 'not in',
# Unary operators.
_ast.Invert: '~',
_ast.Not: 'not',
_ast.UAdd: '+',
_ast.USub: '-'
}
def args(unparse, args, defaults, remove_args=0, override_args={}):
"""Unparse arguments from an argspec. This can strip out positional
arguments and replace keyword arguments.
"""
l = []
defaults = list(map(unparse.expr, defaults))
args = list(zip_longest(reversed(args), reversed(defaults)))
args.reverse()
for arg, default in args[remove_args:]:
a = arg.arg
if a in override_args:
default = repr(override_args[a])
if arg.annotation:
a += ': ' + unparse.expr(arg.annotation)
if default is not None:
a += '=' + default
l.append(a)
return l
def argspec(unparse, node, remove_args=0, override_args={}):
"""Unparse an argspec from a function definition. This can strip out
positional arguments and replace keyword arguments."""
s = []
s.extend(unparse.args(node.args, node.defaults,
remove_args, override_args))
if node.vararg or node.kwonlyargs:
vararg = '*'
if node.vararg:
vararg += node.vararg
if node.varargannotation:
vararg += ': ' + unparse.expr(node.varargannotation)
s.append(vararg)
s.extend(unparse.args(node.kwonlyargs, node.kw_defaults,
override_args=override_args))
kwarg = node.kwarg
if kwarg:
if node.kwargannotation:
kwarg += ': ' + unparse.expr(node.kwargannotation)
s.append('**' + kwarg)
return ', '.join(s)
def comprehension(unparse, node):
"""Unparse a comprehension."""
s = ['for', unparse.expr(node.target), 'in', unparse.expr(node.iter)]
for cond in node.ifs:
s.extend(('if', cond))
return ' '.join(s)
def slice(unparse, node):
"""Unparse a slice."""
s = ''
if isinstance(node, _ast.Slice):
s = []
if node.lower:
s.append(unparse.expr(node.lower))
else:
s.append('')
if node.upper:
s.append(unparse.expr(node.upper))
else:
s.append('')
if node.step:
s.append(unparse.expr(node.step))
s = ':'.join(s)
elif isinstance(node, _ast.ExtSlice):
s = ', '.join(map(unparse.slice, node.dims))
elif isinstance(node, _ast.Index):
s = unparse.expr(node.value)
return s
def expr(unparse, node, parenthesise=False):
"""Unparse an expression."""
s = 'None'
if isinstance(node, _ast.BoolOp):
s = []
for expr in node.values:
s.append(unparse.expr(expr, parenthesise=True))
s = (' ' + unparse.ast_symbols[type(node.op)] + ' ').join(s)
elif isinstance(node, _ast.BinOp):
s = ' '.join((unparse.expr(node.left, parenthesise=True),
unparse.ast_symbols[type(node.op)],
unparse.expr(node.right, parenthesise=True)))
elif isinstance(node, _ast.UnaryOp):
s = (unparse.ast_symbols[type(node.op)] +
unparse.expr(node.operand, parenthesise=True))
elif isinstance(node, _ast.Lambda):
s = ('lambda ' + unparse.argspec(node.args) + ': ' +
unparse.expr(node.body))
elif isinstance(node, _ast.IfExp):
s = ' '.join((unparse.expr(node.body),
'if', unparse.expr(node.test),
'else', unparse.expr(node.orelse)))
elif isinstance(node, _ast.Dict):
s = []
for key, value in zip(node.keys, node.values):
s.append(unparse.expr(key) + ': ' + unparse.expr(value))
s = '{' + ', '.join(s) + '}'
parenthesise = False
elif isinstance(node, _ast.Set):
s = '{' + ', '.join(map(unparse.expr, node.elts)) + '}'
parenthesise = False
elif isinstance(node, _ast.ListComp):
s = [unparse.expr(node.elt)]
s.extend(map(unparse.comprehension, node.generators))
s = '[' + ' '.join(map(unparse.expr, node.elts)) + ']'
parenthesise = False
elif isinstance(node, _ast.SetComp):
s = [unparse.expr(node.elt)]
s.extend(map(unparse.comprehension, node.generators))
s = '{' + ' '.join(map(unparse.expr, node.elts)) + '}'
parenthesise = False
elif isinstance(node, _ast.DictComp):
s = [unparse.expr(node.key) + ': ' + unparse.expr(node.value)]
s.extend(map(unparse.comprehension, node.generators))
s = '{' + ' '.join(map(unparse.expr, node.elts)) + '}'
parenthesise = False
elif isinstance(node, _ast.GeneratorExp):
s = [unparse.expr(node.elt)]
s.extend(map(unparse.comprehension, node.generators))
s = '(' + ' '.join(map(unparse.expr, node.elts)) + ')'
parenthesise = False
elif isinstance(node, _ast.Yield):
s = ['yield']
if node.value:
s.append(unparse.expr(node.value))
s = ' '.join(s)
parenthesise = False
elif isinstance(node, _ast.YieldFrom):
s = ['yield from']
if node.value:
s.append(unparse.expr(node.value))
s = ' '.join(s)
parenthesise = False
elif isinstance(node, _ast.Compare):
s = [unparse.expr(node.left, parenthesise=True)]
for op, operand in zip(node.ops, node.comparators):
s.append(unparse.ast_symbols[type(op)])
s.append(unparse.expr(operand, parenthesise=True))
s = ' '.join(s)
elif isinstance(node, _ast.Call):
s = list(map(unparse.expr, node.args))
if node.starargs:
s.append('*' + unparse.expr(node.starargs, parenthesise=True))
for kw in node.keywords:
s.append(kw.arg + '=' +
unparse.expr(kw.value, parenthesise=True))
if node.kwargs:
s.append('**' + unparse.expr(node.kwargs, parenthesise=True))
s = (unparse.expr(node.func, parenthesise=True) +
'(' + ', '.join(s) + ')')
parenthesise = False
elif isinstance(node, _ast.Num):
s = repr(node.n)
parenthesise = False
elif isinstance(node, (_ast.Str, _ast.Bytes)):
s = repr(node.s)
parenthesise = False
elif isinstance(node, _ast.Ellipsis):
s = '...'
parenthesise = False
elif isinstance(node, _ast.Attribute):
s = unparse.expr(node.value) + '.' + node.attr
parenthesise = False
elif isinstance(node, _ast.Subscript):
s = (unparse.expr(node.value, parenthesise=True) +
'[' + unparse.slice(node.slice) + ']')
parenthesise = False
elif isinstance(node, _ast.Starred):
s = '*' + unparse.expr(node.value)
parenthesise = False
elif isinstance(node, _ast.Name):
s = node.id
parenthesise = False
elif isinstance(node, _ast.List):
s = '[' + ', '.join(map(unparse.expr, node.elts)) + ']'
parenthesise = False
elif isinstance(node, _ast.Tuple):
s = ', '.join(map(unparse.expr, node.elts))
if len(node.elts) == 1:
s += ','
s = '(' + s + ')'
parenthesise = False
if parenthesise:
s = '(' + s + ')'
return s
精彩评论