Refactor generate_rst into a class.

This commit is contained in:
Georg Brandl
2008-07-20 11:12:14 +00:00
parent ae2570c269
commit 870a2aaef1

View File

@@ -202,309 +202,312 @@ def get_module_charset(module):
return charset
def get_doc(what, name, obj, options, env):
"""Format and yield lines of the docstring(s) for the object."""
docstrings = []
if getattr(obj, '__doc__', None):
docstrings.append(obj.__doc__)
# skip some lines in module docstrings if configured
if what == 'module' and env.config.automodule_skip_lines and docstrings:
docstrings[0] = '\n'.join(docstrings[0].splitlines()
[env.config.automodule_skip_lines:])
# for classes, what the "docstring" is can be controlled via an option
if what in ('class', 'exception'):
content = env.config.autoclass_content
if content in ('both', 'init'):
initdocstring = getattr(obj, '__init__', None).__doc__
# for new-style classes, no __init__ means default __init__
if initdocstring == object.__init__.__doc__:
initdocstring = None
if initdocstring:
if content == 'init':
docstrings = [initdocstring]
class RstGenerator(object):
def __init__(self, options, document, lineno):
self.options = options
self.env = document.settings.env
self.reporter = document.reporter
self.lineno = lineno
self.filename_set = set()
self.warnings = []
self.result = ViewList()
def warn(self, msg):
self.warnings.append(self.reporter.warning(msg, line=self.lineno))
def get_doc(self, what, name, obj):
"""Format and yield lines of the docstring(s) for the object."""
docstrings = []
if getattr(obj, '__doc__', None):
docstrings.append(obj.__doc__)
# skip some lines in module docstrings if configured
if what == 'module' and self.env.config.automodule_skip_lines and docstrings:
docstrings[0] = '\n'.join(docstrings[0].splitlines()
[self.env.config.automodule_skip_lines:])
# for classes, what the "docstring" is can be controlled via an option
if what in ('class', 'exception'):
content = self.env.config.autoclass_content
if content in ('both', 'init'):
initdocstring = getattr(obj, '__init__', None).__doc__
# for new-style classes, no __init__ means default __init__
if initdocstring == object.__init__.__doc__:
initdocstring = None
if initdocstring:
if content == 'init':
docstrings = [initdocstring]
else:
docstrings.append('\n\n' + initdocstring)
# the default is only the class docstring
# decode the docstrings using the module's source encoding
charset = None
module = getattr(obj, '__module__', None)
if module is not None:
charset = get_module_charset(module)
for docstring in docstrings:
if isinstance(docstring, str):
if charset:
docstring = docstring.decode(charset)
else:
docstrings.append('\n\n' + initdocstring)
# the default is only the class docstring
try:
# try decoding with utf-8, should only work for real UTF-8
docstring = docstring.decode('utf-8')
except UnicodeError:
# last resort -- can't fail
docstring = docstring.decode('latin1')
docstringlines = prepare_docstring(docstring)
if self.env.app:
# let extensions preprocess docstrings
self.env.app.emit('autodoc-process-docstring',
what, name, obj, self.options, docstringlines)
for line in docstringlines:
yield line
# decode the docstrings using the module's source encoding
charset = None
module = getattr(obj, '__module__', None)
if module is not None:
charset = get_module_charset(module)
def resolve_name(self, what, name):
"""
Determine what module to import and what attribute to document.
for docstring in docstrings:
if isinstance(docstring, str):
if charset:
docstring = docstring.decode(charset)
else:
try:
# try decoding with utf-8, should only work for real UTF-8
docstring = docstring.decode('utf-8')
except UnicodeError:
# last resort -- can't fail
docstring = docstring.decode('latin1')
docstringlines = prepare_docstring(docstring)
if env.app:
# let extensions preprocess docstrings
env.app.emit('autodoc-process-docstring',
what, name, obj, options, docstringlines)
for line in docstringlines:
yield line
def format_signature(what, obj):
"""Return the signature of the object, formatted for display."""
if what not in ('class', 'method', 'function'):
return ''
if what == 'class':
# for classes, the relevant signature is the __init__ method's
obj = getattr(obj, '__init__', None)
# classes without __init__ method?
if obj is None or obj is object.__init__ or not \
(inspect.ismethod(obj) or inspect.isfunction(obj)):
return ''
argspec = inspect.getargspec(obj)
if what in ('class', 'method') and argspec[0] and \
argspec[0][0] in ('cls', 'self'):
del argspec[0][0]
return inspect.formatargspec(*argspec)
def generate_rst(what, name, members, options, add_content, document, lineno,
indent=u'', filename_set=None, check_module=False):
env = document.settings.env
result = None
# 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, signature, retann = py_sig_re.match(name).groups()
except:
warning = document.reporter.warning(
'invalid signature for auto%s (%r)' % (what, name), line=lineno)
return [warning], result
# fullname is the fully qualified name, base the name after the last dot
fullname = (path or '') + base
# path is the name up to the last dot
path = path and path.rstrip('.')
warnings = []
# determine what module to import -- mod is the module name, objpath the
# path of names to get via getattr
mod = None
if what == 'module':
mod = fullname
if signature:
warnings.append(document.reporter.warning(
'ignoring arguments for automodule %s' % mod, line=lineno))
objpath = []
elif what in ('class', 'exception', 'function'):
if path:
mod = path
else:
# if documenting a toplevel object without explicit module, it can
# be contained in another auto directive ...
if hasattr(env, 'autodoc_current_module'):
mod = env.autodoc_current_module
# ... or in the scope of a module directive
if not mod:
mod = env.currmodule
objpath = [base]
else:
if path:
mod_cls = path
else:
# if documenting a class-level object without path, there must be a
# current class, either from a parent auto directive ...
if hasattr(env, 'autodoc_current_class'):
mod_cls = env.autodoc_current_class
# ... or from a class directive
if not mod_cls:
mod_cls = env.currclass
mod, cls = rpartition(mod_cls, '.')
# if the module name is still missing, get it like above
if not mod and hasattr(env, 'autodoc_current_module'):
mod = env.autodoc_current_module
if not mod:
mod = env.currmodule
objpath = [cls, base]
# by this time, a module *must* be determined
if mod is None:
warnings.append(document.reporter.warning(
'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, line=lineno))
return warnings, result
# the name to put into the generated directive -- doesn't contain the module
name_in_directive = '.'.join(objpath) or mod
# now, import the module and get docstring(s) of object to document
try:
todoc = module = __import__(mod, None, None, ['foo'])
if hasattr(module, '__file__') and module.__file__:
modfile = module.__file__
if modfile[-4:].lower() in ('.pyc', '.pyo'):
modfile = modfile[:-1]
if filename_set is not None:
filename_set.add(modfile)
else:
modfile = None # e.g. for builtin and C modules
for part in objpath:
todoc = getattr(todoc, part)
except (ImportError, AttributeError), err:
warnings.append(document.reporter.warning(
'autodoc can\'t import/find %s %r, it reported error: "%s",'
'please check your spelling and sys.path' %
(what, str(fullname), err), line=lineno))
return warnings, result
# check __module__ of object if wanted (for members not given explicitly)
if check_module:
if hasattr(todoc, '__module__'):
if todoc.__module__ != mod:
return warnings, result
# format the object's signature, if any
if signature is not None:
# signature given explicitly -- the parentheses were stripped by the regex
args = '(%s)' % signature
if retann:
args += retann
else:
Returns a tuple of: the full name, the module name, a path of
names to get via getattr, the signature and return annotation.
"""
# first, parse the definition -- auto directives for classes and functions
# can contain a signature which is then used instead of an autogenerated one
try:
args = format_signature(what, todoc)
except Exception, err:
warnings.append(document.reporter.warning(
'error while formatting signature for %s: %s' %
(fullname, err), line=lineno))
args = ''
path, base, args, retann = py_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
# make sure that the view list starts with an empty line. This is
# necessary for some situations where another directive preprocesses
# reST and no starting newline is present
result = ViewList()
result.append(u'', '')
# now, create the directive header
directive = (what == 'method' and is_static_method(todoc)) \
and 'staticmethod' or what
result.append(indent + u'.. %s:: %s%s' % (directive, name_in_directive, args),
'<autodoc>')
if what == 'module':
# Add some module-specific options
if options.synopsis:
result.append(indent + u' :synopsis: ' + options.synopsis, '<autodoc>')
if options.platform:
result.append(indent + u' :platform: ' + options.platform, '<autodoc>')
if options.deprecated:
result.append(indent + u' :deprecated:', '<autodoc>')
else:
# Be explicit about the module, this is necessary since .. class:: doesn't
# support a prepended module name
result.append(indent + u' :module: %s' % mod, '<autodoc>')
if options.noindex:
result.append(indent + u' :noindex:', '<autodoc>')
result.append(u'', '<autodoc>')
if options.show_inheritance and what in ('class', 'exception'):
if len(todoc.__bases__):
bases = [b.__module__ == '__builtin__' and
u':class:`%s`' % b.__name__ or
u':class:`%s.%s`' % (b.__module__, b.__name__)
for b in todoc.__bases__]
result.append(indent + u' Bases: %s' % ', '.join(bases), '<autodoc>')
result.append(u'', '<autodoc>')
# the module directive doesn't have content
if what != 'module':
indent += u' '
if modfile:
sourcename = '%s:docstring of %s' % (modfile, fullname)
else:
sourcename = 'docstring of %s' % fullname
# add content from docstrings
for i, line in enumerate(get_doc(what, fullname, todoc, options, env)):
result.append(indent + line, sourcename, 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])
# document members?
if not members or what in ('function', 'method', 'attribute'):
return warnings, result
# set current namespace for finding members
env.autodoc_current_module = mod
if objpath:
env.autodoc_current_class = objpath[0]
# add members, if possible
_all = members == ['__all__']
members_check_module = False
if _all:
# unqualified :members: given
if what == 'module':
# for implicit module members, check __module__ to avoid documenting
# imported objects if __all__ is not defined
members_check_module = not hasattr(todoc, '__all__')
all_members = inspect.getmembers(todoc)
if args or retann:
self.warn('ignoring signature arguments and return annotation '
'for automodule %s' % mod)
return fullname, fullname, [], 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
else:
if options.inherited_members:
# getmembers() uses dir() which pulls in members from all base classes
if path:
mod_cls = path.rstrip('.')
else:
# 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 not mod_cls:
mod_cls = self.env.currclass
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
def format_signature(self, what, obj, args, retann):
"""
Return the signature of the object, formatted for display.
"""
if what not in ('class', 'method', 'function'):
return ''
if args is not None:
# signature given explicitly
return '(%s)%s' % (args, retann or '')
if what == 'class':
# for classes, the relevant signature is the __init__ method's
obj = getattr(obj, '__init__', None)
# classes without __init__ method?
if obj is None or obj is object.__init__ or not \
(inspect.ismethod(obj) or inspect.isfunction(obj)):
return ''
argspec = inspect.getargspec(obj)
if what in ('class', 'method') and argspec[0] and \
argspec[0][0] in ('cls', 'self'):
del argspec[0][0]
return inspect.formatargspec(*argspec)
def generate(self, what, name, members, add_content, indent=u'', check_module=False):
"""
Generate reST for the object in self.result.
"""
fullname, 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)
return
# the name to put into the generated directive -- doesn't contain the module
name_in_directive = '.'.join(objpath) or mod
# now, import the module and get object to document
try:
todoc = module = __import__(mod, None, None, ['foo'])
if hasattr(module, '__file__') and module.__file__:
modfile = module.__file__
if modfile[-4:].lower() in ('.pyc', '.pyo'):
modfile = modfile[:-1]
self.filename_set.add(modfile)
else:
modfile = None # e.g. for builtin and C modules
for part in objpath:
todoc = getattr(todoc, part)
except (ImportError, AttributeError), err:
self.warn('autodoc can\'t import/find %s %r, it reported error: "%s",'
'please check your spelling and sys.path' %
(what, str(fullname), err))
return
# check __module__ of object if wanted (for members not given explicitly)
if check_module:
if hasattr(todoc, '__module__'):
if todoc.__module__ != mod:
return
# format the object's signature, if any
try:
sig = self.format_signature(what, todoc, args, retann)
except Exception, err:
self.warn('error while formatting signature for %s: %s' %
(fullname, err))
sig = ''
# make sure that the result starts with an empty line. This is
# necessary for some situations where another directive preprocesses
# reST and no starting newline is present
self.result.append(u'', '')
# now, create the directive header
directive = (what == 'method' and is_static_method(todoc)) \
and 'staticmethod' or what
self.result.append(indent + u'.. %s:: %s%s' %
(directive, name_in_directive, sig), '<autodoc>')
if what == 'module':
# Add some module-specific options
if self.options.synopsis:
self.result.append(indent + u' :synopsis: ' + self.options.synopsis,
'<autodoc>')
if self.options.platform:
self.result.append(indent + u' :platform: ' + self.options.platform,
'<autodoc>')
if self.options.deprecated:
self.result.append(indent + u' :deprecated:', '<autodoc>')
else:
# Be explicit about the module, this is necessary since .. class:: doesn't
# support a prepended module name
self.result.append(indent + u' :module: %s' % mod, '<autodoc>')
if self.options.noindex:
self.result.append(indent + u' :noindex:', '<autodoc>')
self.result.append(u'', '<autodoc>')
if self.options.show_inheritance and what in ('class', 'exception'):
if len(todoc.__bases__):
bases = [b.__module__ == '__builtin__' and
u':class:`%s`' % b.__name__ or
u':class:`%s.%s`' % (b.__module__, b.__name__)
for b in todoc.__bases__]
self.result.append(indent + u' Bases: %s' % ', '.join(bases),
'<autodoc>')
self.result.append(u'', '<autodoc>')
# the module directive doesn't have content
if what != 'module':
indent += u' '
if modfile:
sourcename = '%s:docstring of %s' % (modfile, fullname)
else:
sourcename = 'docstring of %s' % fullname
# add content from docstrings
for i, line in enumerate(self.get_doc(what, fullname, todoc)):
self.result.append(indent + line, sourcename, i)
# add source content, if present
if add_content:
for line, src in zip(add_content.data, add_content.items):
self.result.append(indent + line, src[0], src[1])
# document members?
if not members or what in ('function', 'method', 'attribute'):
return
# set current namespace for finding members
self.env.autodoc_current_module = mod
if objpath:
self.env.autodoc_current_class = objpath[0]
# add members, if possible
_all = members == ['__all__']
members_check_module = False
if _all:
# unqualified :members: given
if what == 'module':
# for implicit module members, check __module__ to avoid documenting
# imported objects if __all__ is not defined
members_check_module = not hasattr(todoc, '__all__')
all_members = inspect.getmembers(todoc)
else:
# __dict__ contains only the members directly defined in the class
all_members = sorted(todoc.__dict__.iteritems())
else:
all_members = [(mname, getattr(todoc, mname)) for mname in members]
for (membername, member) in all_members:
# ignore members whose name starts with _ by default
if _all and membername.startswith('_'):
continue
# ignore undocumented members if :undoc-members: is not given
doc = getattr(member, '__doc__', None)
if not options.undoc_members 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'
if self.options.inherited_members:
# getmembers() uses dir() which pulls in members from all
# base classes
all_members = inspect.getmembers(todoc)
else:
memberwhat = 'class'
else:
# XXX: todo -- attribute docs
continue
# __dict__ contains only the members directly defined in the class
all_members = sorted(todoc.__dict__.iteritems())
else:
if callable(member):
memberwhat = 'method'
elif isdescriptor(member):
memberwhat = 'attribute'
else:
# XXX: todo -- attribute docs
all_members = [(mname, getattr(todoc, mname)) for mname in members]
for (membername, member) in all_members:
# ignore members whose name starts with _ by default
if _all and membername.startswith('_'):
continue
full_membername = fullname + '.' + membername
subwarn, subres = generate_rst(memberwhat, full_membername, ['__all__'],
options, None, document, lineno, indent,
check_module=members_check_module)
warnings.extend(subwarn)
if subres is not None:
result.extend(subres)
# ignore undocumented members if :undoc-members: is not given
doc = getattr(member, '__doc__', None)
if not self.options.undoc_members 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 isdescriptor(member):
memberwhat = 'attribute'
else:
# XXX: todo -- attribute docs
continue
full_membername = fullname + '.' + membername
self.generate(memberwhat, full_membername, ['__all__'], None, indent,
check_module=members_check_module)
env.autodoc_current_module = None
env.autodoc_current_class = None
return warnings, result
self.env.autodoc_current_module = None
self.env.autodoc_current_class = None
def _auto_directive(dirname, arguments, options, content, lineno,
@@ -524,20 +527,19 @@ def _auto_directive(dirname, arguments, options, content, lineno,
genopt.platform = options.get('platform', '')
genopt.deprecated = 'deprecated' in options
filename_set = set()
warnings, result = generate_rst(what, name, members, genopt, content, state.document,
lineno, filename_set=filename_set)
if result is None:
return warnings
generator = RstGenerator(genopt, state.document, lineno)
generator.generate(what, name, members, content)
if not generator.result:
return generator.warnings
# record all filenames as dependencies -- this will at least partially make
# automatic invalidation possible
for fn in filename_set:
for fn in generator.filename_set:
state.document.settings.env.note_dependency(fn)
# use a custom reporter that correctly assigns lines to source and lineno
old_reporter = state.memo.reporter
state.memo.reporter = AutodocReporter(result, state.memo.reporter)
state.memo.reporter = AutodocReporter(generator.result, state.memo.reporter)
if dirname == 'automodule':
node = nodes.section()
# hack around title style bookkeeping
@@ -545,14 +547,14 @@ def _auto_directive(dirname, arguments, options, content, lineno,
surrounding_section_level = state.memo.section_level
state.memo.title_styles = []
state.memo.section_level = 0
state.nested_parse(result, 0, node, match_titles=1)
state.nested_parse(generator.result, 0, node, match_titles=1)
state.memo.title_styles = surrounding_title_styles
state.memo.section_level = surrounding_section_level
else:
node = nodes.paragraph()
state.nested_parse(result, 0, node)
state.nested_parse(generator.result, 0, node)
state.memo.reporter = old_reporter
return warnings + node.children
return generator.warnings + node.children
def auto_directive(*args, **kwds):
return _auto_directive(*args, **kwds)