From dafb55a4673dbcd6e54f1e895d15b4395a8903d7 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Tue, 23 Dec 2008 20:05:56 +0100 Subject: [PATCH] Fix documentation of inner classes in autodoc. Messy! --- CHANGES | 4 +- sphinx/ext/autodoc.py | 117 +++++++++++++++++++++++++----------------- tests/test_autodoc.py | 48 +++++++++++------ 3 files changed, 104 insertions(+), 65 deletions(-) diff --git a/CHANGES b/CHANGES index 260ea781d..dd8c4d89b 100644 --- a/CHANGES +++ b/CHANGES @@ -35,7 +35,9 @@ New features added - Italian by Sandro Dentella. -* Extension API: +* Extensions and API: + + - Autodoc now handles inner classes and their methods. - There is now a ``Sphinx.add_lexer()`` method to be able to use custom Pygments lexers easily. diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index 6b8da3862..7be801691 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -23,7 +23,6 @@ from docutils.parsers.rst import directives from docutils.statemachine import ViewList from sphinx.util import rpartition, nested_parse_with_titles -from sphinx.directives.desc import py_sig_re try: base_exception = BaseException @@ -33,6 +32,14 @@ except NameError: _charset_re = re.compile(r'coding[:=]\s*([-\w.]+)') _module_charsets = {} +py_ext_sig_re = re.compile( + r'''^ ([\w.]+::)? # explicit module name + ([\w.]+\.)? # module and/or class name(s) + (\w+) \s* # thing name + (?: \((.*)\) # optional arguments + (\s* -> \s* .*)? )? $ # optional return annotation + ''', re.VERBOSE) + class Options(object): pass @@ -282,55 +289,64 @@ class RstGenerator(object): # first, parse the definition -- auto directives for classes and functions # can contain a signature which is then used instead of an autogenerated one try: - path, base, args, retann = py_sig_re.match(name).groups() + mod, path, base, args, retann = py_ext_sig_re.match(name).groups() except: self.warn('invalid signature for auto%s (%r)' % (what, name)) - return - # fullname is the fully qualified name, base the name after the last dot - fullname = (path or '') + base + return None, [], None, None + + # support explicit module and class name separation via :: + if mod is not None: + mod = mod[:-2] + parents = path and path.rstrip('.').split('.') or [] + else: + parents = [] if what == 'module': + if mod is not None: + self.warn('"::" in automodule name doesn\'t make sense') if args or retann: self.warn('ignoring signature arguments and return annotation ' - 'for automodule %s' % fullname) - return fullname, fullname, [], None, None + 'for automodule %s' % name) + return (path or '') + base, [], None, None - elif what in ('class', 'exception', 'function'): - if path: - mod = path.rstrip('.') - else: - mod = None - # if documenting a toplevel object without explicit module, it can - # be contained in another auto directive ... - if hasattr(self.env, 'autodoc_current_module'): - mod = self.env.autodoc_current_module - # ... or in the scope of a module directive - if not mod: - mod = self.env.currmodule - return fullname, mod, [base], args, retann + elif what in ('exception', 'function', 'class'): + if mod is None: + if path: + mod = path.rstrip('.') + else: + # if documenting a toplevel object without explicit module, it can + # be contained in another auto directive ... + if hasattr(self.env, 'autodoc_current_module'): + mod = self.env.autodoc_current_module + # ... or in the scope of a module directive + if not mod: + mod = self.env.currmodule + return mod, parents + [base], args, retann else: - if path: - mod_cls = path.rstrip('.') - else: - mod_cls = None - # if documenting a class-level object without path, there must be a - # current class, either from a parent auto directive ... - if hasattr(self.env, 'autodoc_current_class'): - mod_cls = self.env.autodoc_current_class - # ... or from a class directive - if mod_cls is None: - mod_cls = self.env.currclass - # ... if still None, there's no way to know - if mod_cls is None: - return fullname, None, [], args, retann - mod, cls = rpartition(mod_cls, '.') - # if the module name is still missing, get it like above - if not mod and hasattr(self.env, 'autodoc_current_module'): - mod = self.env.autodoc_current_module - if not mod: - mod = self.env.currmodule - return fullname, mod, [cls, base], args, retann + if mod is None: + if path: + mod_cls = path.rstrip('.') + else: + mod_cls = None + # if documenting a class-level object without path, there must be a + # current class, either from a parent auto directive ... + if hasattr(self.env, 'autodoc_current_class'): + mod_cls = self.env.autodoc_current_class + # ... or from a class directive + if mod_cls is None: + mod_cls = self.env.currclass + # ... if still None, there's no way to know + if mod_cls is None: + return None, [], None, None + mod, cls = rpartition(mod_cls, '.') + parents = [cls] + # if the module name is still missing, get it like above + if not mod and hasattr(self.env, 'autodoc_current_module'): + mod = self.env.autodoc_current_module + if not mod: + mod = self.env.currmodule + return mod, parents + [base], args, retann def format_signature(self, what, name, obj, args, retann): """ @@ -386,13 +402,15 @@ class RstGenerator(object): """ Generate reST for the object in self.result. """ - fullname, mod, objpath, args, retann = self.resolve_name(what, name) + mod, objpath, args, retann = self.resolve_name(what, name) if not mod: # need a module to import self.warn('don\'t know which module to import for autodocumenting %r ' '(try placing a "module" or "currentmodule" directive in the ' - 'document, or giving an explicit module name)' % fullname) + 'document, or giving an explicit module name)' % name) return + # fully-qualified name + fullname = mod + (objpath and '.' + '.'.join(objpath) or '') # the name to put into the generated directive -- doesn't contain the module name_in_directive = '.'.join(objpath) or mod @@ -423,7 +441,7 @@ class RstGenerator(object): # format the object's signature, if any try: - sig = self.format_signature(what, name, todoc, args, retann) + sig = self.format_signature(what, fullname, todoc, args, retann) except Exception, err: self.warn('error while formatting signature for %s: %s' % (fullname, err)) @@ -548,8 +566,7 @@ class RstGenerator(object): if isinstance(member, (types.FunctionType, types.BuiltinFunctionType)): memberwhat = 'function' - elif isinstance(member, types.ClassType) or \ - isinstance(member, type): + elif isinstance(member, (types.ClassType, type)): if issubclass(member, base_exception): memberwhat = 'exception' else: @@ -558,14 +575,18 @@ class RstGenerator(object): # XXX: todo -- attribute docs continue else: - if callable(member): + if isinstance(member, (types.ClassType, type)): + memberwhat = 'class' + elif callable(member): memberwhat = 'method' elif isdescriptor(member): memberwhat = 'attribute' else: # XXX: todo -- attribute docs continue - full_membername = fullname + '.' + membername + # give explicitly separated module name, so that members of inner classes + # can be documented + full_membername = mod + '::' + '.'.join(objpath + [membername]) self.generate(memberwhat, full_membername, ['__all__'], None, indent, check_module=members_check_module) diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 746c9b422..20ac34ce5 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -82,41 +82,41 @@ def skip_member(app, what, name, obj, skip, options): def test_resolve_name(): # for modules assert gen.resolve_name('module', 'test_autodoc') == \ - ('test_autodoc', 'test_autodoc', [], None, None) + ('test_autodoc', [], None, None) assert gen.resolve_name('module', 'test.test_autodoc') == \ - ('test.test_autodoc', 'test.test_autodoc', [], None, None) + ('test.test_autodoc', [], None, None) assert gen.resolve_name('module', 'test(arg)') == \ - ('test', 'test', [], None, None) + ('test', [], None, None) assert 'ignoring signature arguments' in gen.warnings[0] del gen.warnings[:] # for functions/classes assert gen.resolve_name('function', 'util.raises') == \ - ('util.raises', 'util', ['raises'], None, None) + ('util', ['raises'], None, None) assert gen.resolve_name('function', 'util.raises(exc) -> None') == \ - ('util.raises', 'util', ['raises'], 'exc', ' -> None') + ('util', ['raises'], 'exc', ' -> None') gen.env.autodoc_current_module = 'util' assert gen.resolve_name('function', 'raises') == \ - ('raises', 'util', ['raises'], None, None) + ('util', ['raises'], None, None) gen.env.autodoc_current_module = None gen.env.currmodule = 'util' assert gen.resolve_name('function', 'raises') == \ - ('raises', 'util', ['raises'], None, None) + ('util', ['raises'], None, None) assert gen.resolve_name('class', 'TestApp') == \ - ('TestApp', 'util', ['TestApp'], None, None) + ('util', ['TestApp'], None, None) # for members gen.env.currmodule = 'foo' assert gen.resolve_name('method', 'util.TestApp.cleanup') == \ - ('util.TestApp.cleanup', 'util', ['TestApp', 'cleanup'], None, None) + ('util', ['TestApp', 'cleanup'], None, None) gen.env.currmodule = 'util' gen.env.currclass = 'Foo' gen.env.autodoc_current_class = 'TestApp' assert gen.resolve_name('method', 'cleanup') == \ - ('cleanup', 'util', ['TestApp', 'cleanup'], None, None) + ('util', ['TestApp', 'cleanup'], None, None) assert gen.resolve_name('method', 'TestApp.cleanup') == \ - ('TestApp.cleanup', 'util', ['TestApp', 'cleanup'], None, None) + ('util', ['TestApp', 'cleanup'], None, None) # and clean up gen.env.currmodule = None @@ -321,17 +321,17 @@ def test_generate(): assert_works('exception', 'test_autodoc.CustomEx', [], None) # test diverse inclusion settings for members - should = [('class', 'Class')] + should = [('class', 'test_autodoc.Class')] assert_processes(should, 'class', 'Class', [], None) - should.extend([('method', 'Class.meth')]) + should.extend([('method', 'test_autodoc.Class.meth')]) assert_processes(should, 'class', 'Class', ['meth'], None) - should.extend([('attribute', 'Class.prop')]) + should.extend([('attribute', 'test_autodoc.Class.prop')]) assert_processes(should, 'class', 'Class', ['__all__'], None) options.undoc_members = True - should.append(('method', 'Class.undocmeth')) + should.append(('method', 'test_autodoc.Class.undocmeth')) assert_processes(should, 'class', 'Class', ['__all__'], None) options.inherited_members = True - should.append(('method', 'Class.inheritedmeth')) + should.append(('method', 'test_autodoc.Class.inheritedmeth')) assert_processes(should, 'class', 'Class', ['__all__'], None) # test module flags @@ -363,6 +363,12 @@ def test_generate(): assert_result_contains('.. class:: CustomDict', 'class', 'CustomDict', ['__all__'], None) + # test inner class handling + assert_processes([('class', 'test_autodoc.Outer'), + ('class', 'test_autodoc.Outer.Inner'), + ('method', 'test_autodoc.Outer.Inner.meth')], + 'class', 'Outer', ['__all__'], None) + # --- generate fodder ------------ @@ -404,3 +410,13 @@ def function(foo, *args, **kwds): Return spam. """ pass + + +class Outer(object): + """Foo""" + + class Inner(object): + """Foo""" + + def meth(self): + """Foo"""