This commit is contained in:
Georg Brandl 2010-01-13 23:35:13 +00:00
commit 07f295df7c
22 changed files with 351 additions and 104 deletions

View File

@ -7,6 +7,10 @@ Release 1.0 (in development)
* Support for docutils 0.4 has been removed. * Support for docutils 0.4 has been removed.
* Added the ``viewcode`` extension.
* Added ``html-collect-pages`` event.
* Added ``tab-width`` option to ``literalinclude`` directive. * Added ``tab-width`` option to ``literalinclude`` directive.
* The ``html_sidebars`` config value can now contain patterns as * The ``html_sidebars`` config value can now contain patterns as

View File

@ -7,7 +7,7 @@ import sys, os, re
# Add any Sphinx extension module names here, as strings. They can be extensions # Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.addons.*') or your custom ones. # coming with Sphinx (named 'sphinx.addons.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo',
'sphinx.ext.autosummary'] 'sphinx.ext.autosummary', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']

View File

@ -360,7 +360,15 @@ registered event handlers.
.. versionadded:: 0.5 .. versionadded:: 0.5
.. event:: page-context (app, pagename, templatename, context, doctree) .. event:: html-collect-pages (app)
Emitted when the HTML builder is starting to write non-document pages. You
can add pages to write by returning an iterable from this event consisting of
``(pagename, context, templatename)``.
.. versionadded:: 1.0
.. event:: html-page-context (app, pagename, templatename, context, doctree)
Emitted when the HTML builder has created a context dictionary to render a Emitted when the HTML builder has created a context dictionary to render a
template with -- this can be used to add custom elements to the context. template with -- this can be used to add custom elements to the context.

19
doc/ext/viewcode.rst Normal file
View File

@ -0,0 +1,19 @@
:mod:`sphinx.ext.viewcode` -- Add links to highlighted source code
==================================================================
.. module:: sphinx.ext.viewcode
:synopsis: Add links to a highlighted version of the source code.
.. moduleauthor:: Georg Brandl
.. versionadded:: 1.0
This extension looks at your Python object descriptions (``.. class::``,
``.. function::`` etc.) and tries to find the source files where the objects are
contained. When found, a separate HTML page will be output for each module with
a highlighted version of the source code, and a link will be added to all object
descriptions that leads to the source code of the described object. A link back
from the source to the description will also be inserted.
There are currently no configuration values for this extension; you just need to
add ``'sphinx.ext.viewcode'`` to your :confval:`extensions` value for it to work.

View File

@ -52,6 +52,7 @@ These extensions are built in and can be activated by respective entries in the
ext/coverage ext/coverage
ext/todo ext/todo
ext/extlinks ext/extlinks
ext/viewcode
Third-party extensions Third-party extensions

View File

@ -47,6 +47,7 @@ events = {
'missing-reference': 'env, node, contnode', 'missing-reference': 'env, node, contnode',
'doctree-resolved': 'doctree, docname', 'doctree-resolved': 'doctree, docname',
'env-updated': 'env', 'env-updated': 'env',
'html-collect-pages': 'builder',
'html-page-context': 'pagename, context, doctree or None', 'html-page-context': 'pagename, context, doctree or None',
'build-finished': 'exception', 'build-finished': 'exception',
} }

View File

@ -381,8 +381,12 @@ class StandaloneHTMLBuilder(Builder):
def finish(self): def finish(self):
self.info(bold('writing additional files...'), nonl=1) self.info(bold('writing additional files...'), nonl=1)
# the global general index # pages from extensions
for pagelist in self.app.emit('html-collect-pages'):
for pagename, context, template in pagelist:
self.handle_page(pagename, context, template)
# the global general index
if self.config.html_use_index: if self.config.html_use_index:
# the total count of lines for each index letter, used to distribute # the total count of lines for each index letter, used to distribute
# the entries into two columns # the entries into two columns

View File

@ -190,7 +190,7 @@ class ObjectDescription(Directive):
return [strip_backslash_re.sub('', sig.strip()) return [strip_backslash_re.sub('', sig.strip())
for sig in self.arguments[0].split('\n')] for sig in self.arguments[0].split('\n')]
def parse_signature(self, sig, signode): def handle_signature(self, sig, signode):
""" """
Parse the signature *sig* into individual nodes and append them to Parse the signature *sig* into individual nodes and append them to
*signode*. If ValueError is raised, parsing is aborted and the whole *signode*. If ValueError is raised, parsing is aborted and the whole
@ -242,8 +242,10 @@ class ObjectDescription(Directive):
signode['first'] = False signode['first'] = False
node.append(signode) node.append(signode)
try: try:
# name can also be a tuple, e.g. (classname, objname) # name can also be a tuple, e.g. (classname, objname);
name = self.parse_signature(sig, signode) # this is strictly domain-specific (i.e. no assumptions may
# be made in this base class)
name = self.handle_signature(sig, signode)
except ValueError, err: except ValueError, err:
# signature parsing failed # signature parsing failed
signode.clear() signode.clear()

View File

@ -215,7 +215,8 @@ class VersionChange(Directive):
env = self.state.document.settings.env env = self.state.document.settings.env
env.versionchanges.setdefault(node['version'], []).append( env.versionchanges.setdefault(node['version'], []).append(
(node['type'], env.doc_read_data['docname'], self.lineno, (node['type'], env.doc_read_data['docname'], self.lineno,
env.doc_read_data.get('py_module'), # XXX: python domain specific
env.doc_read_data.get('py:module'),
env.doc_read_data.get('object'), env.doc_read_data.get('object'),
node.astext())) node.astext()))
return ret return ret

View File

@ -66,7 +66,7 @@ class CObject(ObjectDescription):
else: else:
node += tnode node += tnode
def parse_signature(self, sig, signode): def handle_signature(self, sig, signode):
"""Transform a C (or C++) signature into RST nodes.""" """Transform a C (or C++) signature into RST nodes."""
# first try the function pointer signature regex, it's more specific # first try the function pointer signature regex, it's more specific
m = c_funcptr_sig_re.match(sig) m = c_funcptr_sig_re.match(sig)

View File

@ -53,7 +53,7 @@ class PyObject(ObjectDescription):
""" """
return False return False
def parse_signature(self, sig, signode): def handle_signature(self, sig, signode):
""" """
Transform a Python signature into RST nodes. Transform a Python signature into RST nodes.
Returns (fully qualified name of the thing, classname if any). Returns (fully qualified name of the thing, classname if any).
@ -65,37 +65,49 @@ class PyObject(ObjectDescription):
m = py_sig_re.match(sig) m = py_sig_re.match(sig)
if m is None: if m is None:
raise ValueError raise ValueError
classname, name, arglist, retann = m.groups() name_prefix, name, arglist, retann = m.groups()
currclass = self.env.doc_read_data.get('py_class') # determine module and class name (if applicable), as well as full name
if currclass: modname = self.options.get(
'module', self.env.doc_read_data.get('py:module'))
classname = self.env.doc_read_data.get('py:class')
if classname:
add_module = False add_module = False
if classname and classname.startswith(currclass): if name_prefix and name_prefix.startswith(classname):
fullname = classname + name fullname = name_prefix + name
# class name is given again in the signature # class name is given again in the signature
classname = classname[len(currclass):].lstrip('.') name_prefix = name_prefix[len(classname):].lstrip('.')
elif classname: elif name_prefix:
# class name is given in the signature, but different # class name is given in the signature, but different
# (shouldn't happen) # (shouldn't happen)
fullname = currclass + '.' + classname + name fullname = classname + '.' + name_prefix + name
else: else:
# class name is not given in the signature # class name is not given in the signature
fullname = currclass + '.' + name fullname = classname + '.' + name
else: else:
add_module = True add_module = True
fullname = classname and classname + name or name if name_prefix:
classname = name_prefix.rstrip('.')
fullname = name_prefix + name
else:
classname = ''
fullname = name
prefix = self.get_signature_prefix(sig) signode['module'] = modname
if prefix: signode['class'] = classname
signode += addnodes.desc_annotation(prefix, prefix) signode['fullname'] = fullname
if classname: sig_prefix = self.get_signature_prefix(sig)
signode += addnodes.desc_addname(classname, classname) if sig_prefix:
signode += addnodes.desc_annotation(sig_prefix, sig_prefix)
if name_prefix:
signode += addnodes.desc_addname(name_prefix, name_prefix)
# exceptions are a special case, since they are documented in the # exceptions are a special case, since they are documented in the
# 'exceptions' module. # 'exceptions' module.
elif add_module and self.env.config.add_module_names: elif add_module and self.env.config.add_module_names:
modname = self.options.get( modname = self.options.get(
'module', self.env.doc_read_data.get('py_module')) 'module', self.env.doc_read_data.get('py:module'))
if modname and modname != 'exceptions': if modname and modname != 'exceptions':
nodetext = modname + '.' nodetext = modname + '.'
signode += addnodes.desc_addname(nodetext, nodetext) signode += addnodes.desc_addname(nodetext, nodetext)
@ -107,7 +119,7 @@ class PyObject(ObjectDescription):
signode += addnodes.desc_parameterlist() signode += addnodes.desc_parameterlist()
if retann: if retann:
signode += addnodes.desc_returns(retann, retann) signode += addnodes.desc_returns(retann, retann)
return fullname, classname return fullname, name_prefix
signode += addnodes.desc_parameterlist() signode += addnodes.desc_parameterlist()
stack = [signode[-1]] stack = [signode[-1]]
@ -130,7 +142,7 @@ class PyObject(ObjectDescription):
raise ValueError raise ValueError
if retann: if retann:
signode += addnodes.desc_returns(retann, retann) signode += addnodes.desc_returns(retann, retann)
return fullname, classname return fullname, name_prefix
def get_index_text(self, modname, name): def get_index_text(self, modname, name):
""" """
@ -140,7 +152,7 @@ class PyObject(ObjectDescription):
def add_target_and_index(self, name_cls, sig, signode): def add_target_and_index(self, name_cls, sig, signode):
modname = self.options.get( modname = self.options.get(
'module', self.env.doc_read_data.get('py_module')) 'module', self.env.doc_read_data.get('py:module'))
fullname = (modname and modname + '.' or '') + name_cls[0] fullname = (modname and modname + '.' or '') + name_cls[0]
# note target # note target
if fullname not in self.state.document.ids: if fullname not in self.state.document.ids:
@ -169,7 +181,7 @@ class PyObject(ObjectDescription):
def after_content(self): def after_content(self):
if self.clsname_set: if self.clsname_set:
self.env.doc_read_data['py_class'] = None self.env.doc_read_data['py:class'] = None
class PyModulelevel(PyObject): class PyModulelevel(PyObject):
@ -214,7 +226,7 @@ class PyClasslike(PyObject):
def before_content(self): def before_content(self):
PyObject.before_content(self) PyObject.before_content(self)
if self.names: if self.names:
self.env.doc_read_data['py_class'] = self.names[0][0] self.env.doc_read_data['py:class'] = self.names[0][0]
self.clsname_set = True self.clsname_set = True
@ -292,8 +304,8 @@ class PyClassmember(PyObject):
def before_content(self): def before_content(self):
PyObject.before_content(self) PyObject.before_content(self)
lastname = self.names and self.names[-1][1] lastname = self.names and self.names[-1][1]
if lastname and not self.env.doc_read_data.get('py_class'): if lastname and not self.env.doc_read_data.get('py:class'):
self.env.doc_read_data['py_class'] = lastname.strip('.') self.env.doc_read_data['py:class'] = lastname.strip('.')
self.clsname_set = True self.clsname_set = True
@ -317,7 +329,7 @@ class PyModule(Directive):
env = self.state.document.settings.env env = self.state.document.settings.env
modname = self.arguments[0].strip() modname = self.arguments[0].strip()
noindex = 'noindex' in self.options noindex = 'noindex' in self.options
env.doc_read_data['py_module'] = modname env.doc_read_data['py:module'] = modname
env.domaindata['py']['modules'][modname] = \ env.domaindata['py']['modules'][modname] = \
(env.docname, self.options.get('synopsis', ''), (env.docname, self.options.get('synopsis', ''),
self.options.get('platform', ''), 'deprecated' in self.options) self.options.get('platform', ''), 'deprecated' in self.options)
@ -361,16 +373,16 @@ class PyCurrentModule(Directive):
env = self.state.document.settings.env env = self.state.document.settings.env
modname = self.arguments[0].strip() modname = self.arguments[0].strip()
if modname == 'None': if modname == 'None':
env.doc_read_data['py_module'] = None env.doc_read_data['py:module'] = None
else: else:
env.doc_read_data['py_module'] = modname env.doc_read_data['py:module'] = modname
return [] return []
class PyXRefRole(XRefRole): class PyXRefRole(XRefRole):
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env, refnode, has_explicit_title, title, target):
refnode['py_module'] = env.doc_read_data.get('py_module') refnode['py:module'] = env.doc_read_data.get('py:module')
refnode['py_class'] = env.doc_read_data.get('py_class') refnode['py:class'] = env.doc_read_data.get('py:class')
if not has_explicit_title: if not has_explicit_title:
title = title.lstrip('.') # only has a meaning for the target title = title.lstrip('.') # only has a meaning for the target
target = target.lstrip('~') # only has a meaning for the title target = target.lstrip('~') # only has a meaning for the title
@ -497,8 +509,8 @@ class PythonDomain(Domain):
return make_refnode(builder, fromdocname, docname, return make_refnode(builder, fromdocname, docname,
'module-' + target, contnode, title) 'module-' + target, contnode, title)
else: else:
modname = node.get('py_module') modname = node.get('py:module')
clsname = node.get('py_class') clsname = node.get('py:class')
searchorder = node.hasattr('refspecific') and 1 or 0 searchorder = node.hasattr('refspecific') and 1 or 0
name, obj = self.find_obj(env, modname, clsname, name, obj = self.find_obj(env, modname, clsname,
target, typ, searchorder) target, typ, searchorder)

View File

@ -35,7 +35,7 @@ class GenericObject(ObjectDescription):
indextemplate = '' indextemplate = ''
parse_node = None parse_node = None
def parse_signature(self, sig, signode): def handle_signature(self, sig, signode):
if self.parse_node: if self.parse_node:
name = self.parse_node(self.env, sig, signode) name = self.parse_node(self.env, sig, signode)
else: else:
@ -146,7 +146,7 @@ class Cmdoption(ObjectDescription):
def add_target_and_index(self, name, sig, signode): def add_target_and_index(self, name, sig, signode):
targetname = name.replace('/', '-') targetname = name.replace('/', '-')
currprogram = self.env.doc_read_data.get('std_program') currprogram = self.env.doc_read_data.get('std:program')
if currprogram: if currprogram:
targetname = '-' + currprogram + targetname targetname = '-' + currprogram + targetname
targetname = 'cmdoption' + targetname targetname = 'cmdoption' + targetname
@ -175,9 +175,9 @@ class Program(Directive):
env = self.state.document.settings.env env = self.state.document.settings.env
program = ws_re.sub('-', self.arguments[0].strip()) program = ws_re.sub('-', self.arguments[0].strip())
if program == 'None': if program == 'None':
env.doc_read_data['std_program'] = None env.doc_read_data['std:program'] = None
else: else:
env.doc_read_data['std_program'] = program env.doc_read_data['std:program'] = program
return [] return []
@ -185,7 +185,7 @@ class OptionXRefRole(XRefRole):
innernodeclass = addnodes.literal_emphasis innernodeclass = addnodes.literal_emphasis
def process_link(self, env, refnode, has_explicit_title, title, target): def process_link(self, env, refnode, has_explicit_title, title, target):
program = env.doc_read_data.get('std_program') program = env.doc_read_data.get('std:program')
if not has_explicit_title: if not has_explicit_title:
if ' ' in title and not (title.startswith('/') or if ' ' in title and not (title.startswith('/') or
title.startswith('-')): title.startswith('-')):

View File

@ -713,12 +713,12 @@ class BuildEnvironment:
@property @property
def currmodule(self): def currmodule(self):
"""Backwards compatible alias.""" """Backwards compatible alias."""
return self.doc_read_data.get('py_module') return self.doc_read_data.get('py:module')
@property @property
def currclass(self): def currclass(self):
"""Backwards compatible alias.""" """Backwards compatible alias."""
return self.doc_read_data.get('py_class') return self.doc_read_data.get('py:class')
def new_serialno(self, category=''): def new_serialno(self, category=''):
"""Return a serial number, e.g. for index entry targets.""" """Return a serial number, e.g. for index entry targets."""

View File

@ -46,3 +46,11 @@ class ExtensionError(SphinxError):
class ThemeError(SphinxError): class ThemeError(SphinxError):
category = 'Theme error' category = 'Theme error'
class PycodeError(Exception):
def __str__(self):
res = self.args[0]
if len(self.args) > 1:
res += ' (exception was: %r)' % self.args[1]
return res

View File

@ -562,9 +562,9 @@ class Documenter(object):
do all members, else those given by *self.options.members*. do all members, else those given by *self.options.members*.
""" """
# set current namespace for finding members # set current namespace for finding members
self.env.doc_read_data['autodoc_module'] = self.modname self.env.doc_read_data['autodoc:module'] = self.modname
if self.objpath: if self.objpath:
self.env.doc_read_data['autodoc_class'] = self.objpath[0] self.env.doc_read_data['autodoc:class'] = self.objpath[0]
want_all = all_members or self.options.inherited_members or \ want_all = all_members or self.options.inherited_members or \
self.options.members is ALL self.options.members is ALL
@ -605,8 +605,8 @@ class Documenter(object):
check_module=members_check_module) check_module=members_check_module)
# reset current objects # reset current objects
self.env.doc_read_data['autodoc_module'] = None self.env.doc_read_data['autodoc:module'] = None
self.env.doc_read_data['autodoc_class'] = None self.env.doc_read_data['autodoc:class'] = None
def generate(self, more_content=None, real_modname=None, def generate(self, more_content=None, real_modname=None,
check_module=False, all_members=False): check_module=False, all_members=False):
@ -764,10 +764,10 @@ class ModuleLevelDocumenter(Documenter):
else: else:
# if documenting a toplevel object without explicit module, # if documenting a toplevel object without explicit module,
# it can be contained in another auto directive ... # it can be contained in another auto directive ...
modname = self.env.doc_read_data.get('autodoc_module') modname = self.env.doc_read_data.get('autodoc:module')
# ... or in the scope of a module directive # ... or in the scope of a module directive
if not modname: if not modname:
modname = self.env.doc_read_data.get('py_module') modname = self.env.doc_read_data.get('py:module')
# ... else, it stays None, which means invalid # ... else, it stays None, which means invalid
return modname, parents + [base] return modname, parents + [base]
@ -786,10 +786,10 @@ class ClassLevelDocumenter(Documenter):
# if documenting a class-level object without path, # if documenting a class-level object without path,
# there must be a current class, either from a parent # there must be a current class, either from a parent
# auto directive ... # auto directive ...
mod_cls = self.env.doc_read_data.get('autodoc_class') mod_cls = self.env.doc_read_data.get('autodoc:class')
# ... or from a class directive # ... or from a class directive
if mod_cls is None: if mod_cls is None:
mod_cls = self.env.doc_read_data.get('py_class') mod_cls = self.env.doc_read_data.get('py:class')
# ... if still None, there's no way to know # ... if still None, there's no way to know
if mod_cls is None: if mod_cls is None:
return None, [] return None, []
@ -797,9 +797,9 @@ class ClassLevelDocumenter(Documenter):
parents = [cls] parents = [cls]
# if the module name is still missing, get it like above # if the module name is still missing, get it like above
if not modname: if not modname:
modname = self.env.doc_read_data.get('autodoc_module') modname = self.env.doc_read_data.get('autodoc:module')
if not modname: if not modname:
modname = self.env.doc_read_data.get('py_module') modname = self.env.doc_read_data.get('py:module')
# ... else, it stays None, which means invalid # ... else, it stays None, which means invalid
return modname, parents + [base] return modname, parents + [base]

View File

@ -228,7 +228,7 @@ class Autosummary(Directive):
env = self.state.document.settings.env env = self.state.document.settings.env
prefixes = [''] prefixes = ['']
currmodule = env.doc_read_data.get('py_module') currmodule = env.doc_read_data.get('py:module')
if currmodule: if currmodule:
prefixes.insert(0, currmodule) prefixes.insert(0, currmodule)

View File

@ -284,7 +284,7 @@ class InheritanceDiagram(Directive):
# Create a graph starting with the list of classes # Create a graph starting with the list of classes
try: try:
graph = InheritanceGraph(class_names, graph = InheritanceGraph(class_names,
env.doc_read_data.get('py_module')) env.doc_read_data.get('py:module'))
except InheritanceException, err: except InheritanceException, err:
return [node.document.reporter.warning(err.args[0], return [node.document.reporter.warning(err.args[0],
line=self.lineno)] line=self.lineno)]

162
sphinx/ext/viewcode.py Normal file
View File

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""
sphinx.ext.viewcode
~~~~~~~~~~~~~~~~~~~
Add links to module code in Python object descriptions.
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
from docutils import nodes
from sphinx import addnodes
from sphinx.util import make_refnode
from sphinx.pycode import ModuleAnalyzer
def doctree_read(app, doctree):
env = app.builder.env
if not hasattr(env, '_viewcode_modules'):
env._viewcode_modules = {}
def has_tag(modname, fullname, docname):
entry = env._viewcode_modules.get(modname, None)
if entry is None:
try:
analyzer = ModuleAnalyzer.for_module(modname)
except Exception:
env._viewcode_modules[modname] = False
return
analyzer.find_tags()
entry = analyzer.code, analyzer.tags, {}
env._viewcode_modules[modname] = entry
elif entry is False:
return
code, tags, used = entry
if fullname in tags:
used[fullname] = docname
return True
for objnode in doctree.traverse(addnodes.desc):
if objnode['domain'] != 'py':
continue
names = set()
for signode in objnode.traverse(addnodes.desc_signature):
modname = signode['module']
if not modname:
continue
fullname = signode['fullname']
if not has_tag(modname, fullname, env.docname):
continue
if fullname in names:
# only one link per name, please
continue
names.add(fullname)
pagename = '_modules/' + modname.replace('.', '/')
onlynode = addnodes.only(expr='html')
onlynode += addnodes.pending_xref(
'', reftype='viewcode', refdomain='std', refexplicit=False,
reftarget=pagename, refid=fullname,
refdoc=env.docname)
onlynode[0] += nodes.inline('', _('[source]'),
classes=['viewcode-link'])
signode += onlynode
def missing_reference(app, env, node, contnode):
# resolve our "viewcode" reference nodes -- they need special treatment
if node['reftype'] == 'viewcode':
return make_refnode(app.builder, node['refdoc'], node['reftarget'],
node['refid'], contnode)
def collect_pages(app):
env = app.builder.env
if not hasattr(env, '_viewcode_modules'):
return
highlighter = app.builder.highlighter
urito = app.builder.get_relative_uri
modnames = set(env._viewcode_modules)
for modname, (code, tags, used) in env._viewcode_modules.iteritems():
# construct a page name for the highlighted source
pagename = '_modules/' + modname.replace('.', '/')
# highlight the source using the builder's highlighter
highlighted = highlighter.highlight_block(code, 'python', False)
# split the code into lines
lines = highlighted.splitlines()
# split off wrap markup from the first line of the actual code
before, after = lines[0].split('<pre>')
lines[0:1] = [before + '<pre>', after]
# nothing to do for the last line; it always starts with </pre> anyway
# now that we have code lines (starting at index 1), insert anchors for
# the collected tags (HACK: this only works if the tag boundaries are
# properly nested!)
for name, docname in used.iteritems():
type, start, end = tags[name]
backlink = urito(pagename, docname) + '#' + modname + '.' + name
lines[start] = (
'<div class="viewcode-block" id="%s"><a class="viewcode-back" '
'href="%s">%s</a>' % (name, backlink, _('[docs]')) + lines[start])
lines[end - 1] += '</div>'
# try to find parents (for submodules)
parents = []
parent = modname
while '.' in parent:
parent = parent.rsplit('.', 1)[0]
if parent in modnames:
parents.append({
'link': urito(pagename, '_modules/' +
parent.replace('.', '/')),
'title': parent})
parents.append({'link': urito(pagename, '_modules/index'),
'title': _('Module code')})
parents.reverse()
# putting it all together
context = {
'parents': parents,
'title': modname,
'body': _('<h2>Source code for %s</h2>') % modname + '\n'.join(lines)
}
app.builder.info(' '+pagename, nonl=1)
yield (pagename, context, 'page.html')
if not modnames:
return
app.builder.info(' _modules/index')
html = ['\n']
# the stack logic is needed for using nested lists for submodules
stack = ['']
for modname in sorted(modnames):
if modname.startswith(stack[-1]):
stack.append(modname + '.')
html.append('<ul>')
else:
stack.pop()
while not modname.startswith(stack[-1]):
stack.pop()
html.append('</ul>')
stack.append(modname + '.')
html.append('<li><a href="%s">%s</a></li>\n' % (
urito('_modules/index', '_modules/' + modname.replace('.', '/')),
modname))
html.append('</ul>' * (len(stack) - 1))
context = {
'title': _('Overview: module code'),
'body': _('<h2>All modules for which code is available</h2>') + \
''.join(html),
}
yield ('_modules/index', context, 'page.html')
def setup(app):
app.connect('doctree-read', doctree_read)
app.connect('html-collect-pages', collect_pages)
app.connect('missing-reference', missing_reference)
#app.add_config_value('viewcode_include_modules', [], 'env')
#app.add_config_value('viewcode_exclude_modules', [], 'env')

View File

@ -14,8 +14,10 @@ import sys
from os import path from os import path
from cStringIO import StringIO from cStringIO import StringIO
from sphinx.errors import PycodeError
from sphinx.pycode import nodes from sphinx.pycode import nodes
from sphinx.pycode.pgen2 import driver, token, tokenize, parse, literals from sphinx.pycode.pgen2 import driver, token, tokenize, parse, literals
from sphinx.util import get_module_source
from sphinx.util.docstrings import prepare_docstring, prepare_commentdoc from sphinx.util.docstrings import prepare_docstring, prepare_commentdoc
@ -136,14 +138,6 @@ class AttrDocVisitor(nodes.NodeVisitor):
self.collected[namespace, name] = docstring self.collected[namespace, name] = docstring
class PycodeError(Exception):
def __str__(self):
res = self.args[0]
if len(self.args) > 1:
res += ' (exception was: %r)' % self.args[1]
return res
class ModuleAnalyzer(object): class ModuleAnalyzer(object):
# cache for analyzer objects -- caches both by module and file name # cache for analyzer objects -- caches both by module and file name
cache = {} cache = {}
@ -173,33 +167,11 @@ class ModuleAnalyzer(object):
return entry return entry
try: try:
if modname not in sys.modules: type, source = get_module_source(modname)
try: if type == 'string':
__import__(modname)
except ImportError, err:
raise PycodeError('error importing %r' % modname, err)
mod = sys.modules[modname]
if hasattr(mod, '__loader__'):
try:
source = mod.__loader__.get_source(modname)
except Exception, err:
raise PycodeError('error getting source for %r' % modname,
err)
obj = cls.for_string(source, modname) obj = cls.for_string(source, modname)
cls.cache['module', modname] = obj else:
return obj obj = cls.for_file(source, modname)
filename = getattr(mod, '__file__', None)
if filename is None:
raise PycodeError('no source found for module %r' % modname)
filename = path.normpath(path.abspath(filename))
lfilename = filename.lower()
if lfilename.endswith('.pyo') or lfilename.endswith('.pyc'):
filename = filename[:-1]
elif not lfilename.endswith('.py'):
raise PycodeError('source is not a .py file: %r' % filename)
if not path.isfile(filename):
raise PycodeError('source file is not present: %r' % filename)
obj = cls.for_file(filename, modname)
except PycodeError, err: except PycodeError, err:
cls.cache['module', modname] = err cls.cache['module', modname] = err
raise raise
@ -214,6 +186,11 @@ class ModuleAnalyzer(object):
# file-like object yielding source lines # file-like object yielding source lines
self.source = source self.source = source
# cache the source code as well
pos = self.source.tell()
self.code = self.source.read()
self.source.seek(pos)
# will be filled by tokenize() # will be filled by tokenize()
self.tokens = None self.tokens = None
# will be filled by parse() # will be filled by parse()

View File

@ -368,6 +368,22 @@ dl.glossary dt {
margin-left: 1.5em; margin-left: 1.5em;
} }
.viewcode-link {
float: right;
}
.viewcode-back {
float: right;
font-family: sans-serif;
}
div.viewcode-block:target {
background-color: #f4debf;
border: 1px solid #D5BB73;
margin: -1px -10px;
padding: 0 10px;
}
/* -- code displays --------------------------------------------------------- */ /* -- code displays --------------------------------------------------------- */
pre { pre {

View File

@ -508,6 +508,38 @@ def make_refnode(builder, fromdocname, todocname, targetid, child, title=None):
return node return node
def get_module_source(modname):
"""Try to find the source code for a module.
Can return ('file', 'filename') in which case the source is in the given
file, or ('string', 'source') which which case the source is the string.
"""
if modname not in sys.modules:
try:
__import__(modname)
except Exception, err:
raise PycodeError('error importing %r' % modname, err)
mod = sys.modules[modname]
if hasattr(mod, '__loader__'):
try:
source = mod.__loader__.get_source(modname)
except Exception, err:
raise PycodeError('error getting source for %r' % modname, err)
return 'string', source
filename = getattr(mod, '__file__', None)
if filename is None:
raise PycodeError('no source found for module %r' % modname)
filename = path.normpath(path.abspath(filename))
lfilename = filename.lower()
if lfilename.endswith('.pyo') or lfilename.endswith('.pyc'):
filename = filename[:-1]
elif not lfilename.endswith('.py'):
raise PycodeError('source is not a .py file: %r' % filename)
if not path.isfile(filename):
raise PycodeError('source file is not present: %r' % filename)
return 'file', filename
try: try:
any = any any = any
except NameError: except NameError:

View File

@ -97,28 +97,28 @@ def test_parse_name():
verify('function', 'util.raises', ('util', ['raises'], None, None)) verify('function', 'util.raises', ('util', ['raises'], None, None))
verify('function', 'util.raises(exc) -> None', verify('function', 'util.raises(exc) -> None',
('util', ['raises'], 'exc', 'None')) ('util', ['raises'], 'exc', 'None'))
directive.env.doc_read_data['autodoc_module'] = 'util' directive.env.doc_read_data['autodoc:module'] = 'util'
verify('function', 'raises', ('util', ['raises'], None, None)) verify('function', 'raises', ('util', ['raises'], None, None))
del directive.env.doc_read_data['autodoc_module'] del directive.env.doc_read_data['autodoc:module']
directive.env.doc_read_data['py_module'] = 'util' directive.env.doc_read_data['py:module'] = 'util'
verify('function', 'raises', ('util', ['raises'], None, None)) verify('function', 'raises', ('util', ['raises'], None, None))
verify('class', 'TestApp', ('util', ['TestApp'], None, None)) verify('class', 'TestApp', ('util', ['TestApp'], None, None))
# for members # for members
directive.env.doc_read_data['py_module'] = 'foo' directive.env.doc_read_data['py:module'] = 'foo'
verify('method', 'util.TestApp.cleanup', verify('method', 'util.TestApp.cleanup',
('util', ['TestApp', 'cleanup'], None, None)) ('util', ['TestApp', 'cleanup'], None, None))
directive.env.doc_read_data['py_module'] = 'util' directive.env.doc_read_data['py:module'] = 'util'
directive.env.doc_read_data['py_class'] = 'Foo' directive.env.doc_read_data['py:class'] = 'Foo'
directive.env.doc_read_data['autodoc_class'] = 'TestApp' directive.env.doc_read_data['autodoc:class'] = 'TestApp'
verify('method', 'cleanup', ('util', ['TestApp', 'cleanup'], None, None)) verify('method', 'cleanup', ('util', ['TestApp', 'cleanup'], None, None))
verify('method', 'TestApp.cleanup', verify('method', 'TestApp.cleanup',
('util', ['TestApp', 'cleanup'], None, None)) ('util', ['TestApp', 'cleanup'], None, None))
# and clean up # and clean up
del directive.env.doc_read_data['py_module'] del directive.env.doc_read_data['py:module']
del directive.env.doc_read_data['py_class'] del directive.env.doc_read_data['py:class']
del directive.env.doc_read_data['autodoc_class'] del directive.env.doc_read_data['autodoc:class']
def test_format_signature(): def test_format_signature():
@ -353,7 +353,7 @@ def test_generate():
'function', 'util.foobar', more_content=None) 'function', 'util.foobar', more_content=None)
# test auto and given content mixing # test auto and given content mixing
directive.env.doc_read_data['py_module'] = 'test_autodoc' directive.env.doc_read_data['py:module'] = 'test_autodoc'
assert_result_contains(' Function.', 'method', 'Class.meth') assert_result_contains(' Function.', 'method', 'Class.meth')
add_content = ViewList() add_content = ViewList()
add_content.append('Content.', '', 0) add_content.append('Content.', '', 0)
@ -437,7 +437,7 @@ def test_generate():
'attribute', 'test_autodoc.Class.descr') 'attribute', 'test_autodoc.Class.descr')
# test generation for C modules (which have no source file) # test generation for C modules (which have no source file)
directive.env.doc_read_data['py_module'] = 'time' directive.env.doc_read_data['py:module'] = 'time'
assert_processes([('function', 'time.asctime')], 'function', 'asctime') assert_processes([('function', 'time.asctime')], 'function', 'asctime')
assert_processes([('function', 'time.asctime')], 'function', 'asctime') assert_processes([('function', 'time.asctime')], 'function', 'asctime')