From 138ea16594dd4e37baaac03493bf12ffa4902691 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Fri, 14 Mar 2008 23:08:22 +0000 Subject: [PATCH] Add first version of sphinx.ext.autodoc that generates documentation from docstrings. --- sphinx/ext/autodoc.py | 219 ++++++++++++++++++++++++++++++++++++++++ sphinx/ext/coverage.py | 9 +- sphinx/util/__init__.py | 8 ++ 3 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 sphinx/ext/autodoc.py diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py new file mode 100644 index 000000000..1822821a9 --- /dev/null +++ b/sphinx/ext/autodoc.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.autodoc + ~~~~~~~~~~~~~~~~~~ + + Automatically insert docstrings for functions, classes or whole modules into + the doctree, thus avoiding duplication between docstrings and documentation + for those who like elaborate docstrings. + + :copyright: 2008 by Georg Brandl. + :license: BSD. +""" + +import types +import inspect +import textwrap + +from docutils import nodes +from docutils.parsers.rst import directives +from docutils.statemachine import ViewList + +from sphinx import addnodes +from sphinx.util import rpartition + +try: + base_exception = BaseException +except NameError: + base_exception = Exception + + +def prepare_docstring(s): + """Convert a docstring into lines of parseable reST.""" + if not s or s.isspace(): + return [''] + nl = s.rstrip().find('\n') + if nl == -1: + # Only one line... + return [s.strip(), ''] + # The first line may be indented differently... + firstline = s[:nl].strip() + otherlines = textwrap.dedent(s[nl+1:]) + return [firstline] + otherlines.splitlines() + [''] + + +def generate_rst(what, name, members, undoc, add_content, + document, lineno, indent=''): + env = document.settings.env + + # find out what to import + if what == 'module': + mod = obj = name + objpath = [] + elif what in ('class', 'exception', 'function'): + mod, obj = rpartition(name, '.') + if not mod: + mod = env.autodoc_current_module + if not mod: + mod = env.currmodule + objpath = [obj] + else: + mod_cls, obj = rpartition(name, '.') + if not mod_cls: + mod_cls = env.autodoc_current_class + if not mod_cls: + mod_cls = env.currclass + mod, cls = rpartition(mod_cls, '.') + if not mod: + mod = env.autodoc_current_module + if not mod: + mod = env.currmodule + objpath = [cls, obj] + + result = ViewList() + + try: + todoc = module = __import__(mod, None, None, ['foo']) + for part in objpath: + todoc = getattr(todoc, part) + if hasattr(todoc, '__module__'): + if todoc.__module__ != mod: + return [], result + docstring = todoc.__doc__ + except (ImportError, AttributeError): + warning = document.reporter.warning( + 'autodoc can\'t import/find %s %r, check your spelling ' + 'and sys.path' % (what, str(name)), line=lineno) + return [warning], result + + # add directive header + try: + if what == 'class': + args = inspect.formatargspec(*inspect.getargspec(todoc.__init__)) + elif what in ('function', 'method'): + args = inspect.formatargspec(*inspect.getargspec(todoc)) + if what == 'method': + if args[1:7] == 'self, ': + args = '(' + args[7:] + elif args == '(self)': + args = '()' + else: + args = '' + except: + args = '' + if len(objpath) == 2: + qualname = '%s.%s' % (cls, obj) + else: + qualname = obj + result.append(indent + '.. %s:: %s%s' % (what, qualname, args), '') + result.append('', '') + + # the module directive doesn't like content + if what != 'module': + indent += ' ' + + # add docstring content + if what == 'module' and env.config.automodule_skip_lines: + docstring = '\n'.join(docstring.splitlines() + [env.config.automodule_skip_lines:]) + docstring = prepare_docstring(docstring) + for i, line in enumerate(docstring): + result.append(indent + line, '' % name, i) + + # add source content, if present + if add_content: + for line, src in zip(add_content.data, add_content.items): + result.append(indent + line, src[0], src[1]) + + if not members or what in ('function', 'method', 'attribute'): + return [], result + + env.autodoc_current_module = mod + if objpath: + env.autodoc_current_class = objpath[0] + + warnings = [] + # add members, if possible + _all = members == ['__all__'] + if _all: + all_members = sorted(inspect.getmembers(todoc)) + else: + all_members = [(mname, getattr(todoc, mname)) for mname in members] + for (membername, member) in all_members: + if _all and membername.startswith('_'): + continue + doc = getattr(member, '__doc__', None) + if not undoc and not doc: + continue + if what == 'module': + if isinstance(member, types.FunctionType): + memberwhat = 'function' + elif isinstance(member, types.ClassType) or \ + isinstance(member, type): + if issubclass(member, base_exception): + memberwhat = 'exception' + else: + memberwhat = 'class' + else: + # XXX: todo -- attribute docs + continue + else: + if callable(member): + memberwhat = 'method' + elif isinstance(member, property): + memberwhat = 'attribute' + else: + # XXX: todo -- attribute docs + continue + full_membername = name + '.' + membername + subwarn, subres = generate_rst(memberwhat, full_membername, ['__all__'], + undoc, None, document, lineno, indent) + warnings.extend(subwarn) + result.extend(subres) + + env.autodoc_current_module = None + env.autodoc_current_class = None + + return warnings, result + + + +def _auto_directive(dirname, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + what = dirname[4:] + name = arguments[0] + members = options.get('members', []) + undoc = 'undoc-members' in options + + warnings, result = generate_rst(what, name, members, undoc, content, + state.document, lineno) + + node = nodes.paragraph() + state.nested_parse(result, content_offset, node) + return warnings + [node] + +def auto_directive(*args, **kwds): + return _auto_directive(*args, **kwds) + +def auto_directive_withmembers(*args, **kwds): + return _auto_directive(*args, **kwds) + + +def members_directive(arg): + if arg is None: + return ['__all__'] + return [x.strip() for x in arg.split(',')] + + +def setup(app): + options = {'members': members_directive, 'undoc-members': directives.flag} + app.add_directive('automodule', auto_directive_withmembers, + 1, (1, 0, 1), **options) + app.add_directive('autoclass', auto_directive_withmembers, + 1, (1, 0, 1), **options) + app.add_directive('autoexception', auto_directive_withmembers, + 1, (1, 0, 1), **options) + app.add_directive('autofunction', auto_directive, 1, (1, 0, 1)) + app.add_directive('automethod', auto_directive, 1, (1, 0, 1)) + app.add_directive('autoattribute', auto_directive, 1, (1, 0, 1)) + app.add_config_value('automodule_skip_lines', 0, True) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index ed29ceea7..e3ebf1db2 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -36,9 +36,6 @@ def compile_regex_list(name, exps, warnfunc): class CoverageBuilder(Builder): - """ - Checks the completeness of Python's C-API documentation. - """ name = 'coverage' @@ -237,10 +234,10 @@ class CoverageBuilder(Builder): def setup(app): app.add_builder(CoverageBuilder) - app.add_config_value('coverage_c_path', [], False) - app.add_config_value('coverage_c_regexes', [], False) app.add_config_value('coverage_ignore_modules', [], False) app.add_config_value('coverage_ignore_functions', [], False) app.add_config_value('coverage_ignore_classes', [], False) - app.add_config_value('coverage_ignore_c_items', [], False) + app.add_config_value('coverage_c_path', [], False) + app.add_config_value('coverage_c_regexes', {}, False) + app.add_config_value('coverage_ignore_c_items', {}, False) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 3c63c7bdd..c470eaa8a 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -112,3 +112,11 @@ class attrdict(dict): def fmt_ex(ex): """Format a single line with an exception description.""" return traceback.format_exception_only(ex.__class__, ex)[-1].strip() + + +def rpartition(s, t): + """Similar to str.rpartition from 2.5.""" + i = s.rfind(t) + if i != -1: + return s[:i], s[i+len(t):] + return '', s