From 135bb6f6c8c7b75f16939a725173aa26f3896d25 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 11 Mar 2012 17:48:51 +0100 Subject: [PATCH 1/2] ENH: add a linkcode extension --- doc/ext/linkcode.rst | 47 +++++++++++++++++++++++++++ doc/extensions.rst | 1 + sphinx/ext/linkcode.py | 72 ++++++++++++++++++++++++++++++++++++++++++ tests/root/conf.py | 22 +++++++++++++ tests/test_linkcode.py | 28 ++++++++++++++++ 5 files changed, 170 insertions(+) create mode 100644 doc/ext/linkcode.rst create mode 100644 sphinx/ext/linkcode.py create mode 100644 tests/test_linkcode.py diff --git a/doc/ext/linkcode.rst b/doc/ext/linkcode.rst new file mode 100644 index 000000000..9ae247563 --- /dev/null +++ b/doc/ext/linkcode.rst @@ -0,0 +1,47 @@ +:mod:`sphinx.ext.linkcode` -- Add external links to source code +=============================================================== + +.. module:: sphinx.ext.linkcode + :synopsis: Add external links to source code. +.. moduleauthor:: Pauli Virtanen + +.. versionadded:: 1.1 + +This extension looks at your Python object descriptions +(``.. class::``, ``.. function::`` etc.) and adds external links to +code hosted somewhere on the web. The intent is similar to the +``sphinx.ext.viewcode`` extension, but assumes the source code can be +found somewhere on the Internet. + +In your configuration, you need to specify a :confval:`linkcode_resolve` +function that returns an URL based on the name of a module and +the full name of the object. + +.. confval:: linkcode_resolve + + This is a function ``linkcode_resolve(domain, info)``, + which should return the URL to source code corresponding to + the object in given domain with given information. + + The function should return ``None`` if no link is to be added. + + The argument ``domain`` specifies the language domain the object is + in. ``info`` is a dictionary with the following keys guaranteed to + be present (dependent on the domain): + + - ``c``: ``names`` (list of names for the object) + - ``cpp``: ``names`` (list of names for the object) + - ``py``: ``module`` (name of the module), ``fullname`` (name of the object) + - ``javascript``: ``object`` (name of the object), ``fullname`` (name of the item) + + Example: + + .. code-block:: python + + def linkcode_resolve(domain, info): + if domain != 'py': + return None + if not info['module']: + return None + filename = info['module'].replace('.', '/') + return "http://somesite/sourcerepo/%s.py" % filename diff --git a/doc/extensions.rst b/doc/extensions.rst index b93974486..07bc7fe4b 100644 --- a/doc/extensions.rst +++ b/doc/extensions.rst @@ -53,6 +53,7 @@ These extensions are built in and can be activated by respective entries in the ext/todo ext/extlinks ext/viewcode + ext/linkcode ext/oldcmarkup diff --git a/sphinx/ext/linkcode.py b/sphinx/ext/linkcode.py new file mode 100644 index 000000000..ffe5b9c09 --- /dev/null +++ b/sphinx/ext/linkcode.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.linkcode + ~~~~~~~~~~~~~~~~~~~ + + Add external links to module code in Python object descriptions. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from docutils import nodes + +from sphinx import addnodes +from sphinx.locale import _ +from sphinx.errors import SphinxError + +class LinkcodeError(SphinxError): + category = "linkcode error" + +def doctree_read(app, doctree): + env = app.builder.env + + resolve_target = getattr(env.config, 'linkcode_resolve', None) + if not callable(env.config.linkcode_resolve): + raise LinkcodeError( + "Function `linkcode_resolve` is not given in conf.py") + + domain_keys = dict( + py=['module', 'fullname'], + c=['names'], + cpp=['names'], + js=['object', 'fullname'], + ) + + for objnode in doctree.traverse(addnodes.desc): + domain = objnode.get('domain') + uris = set() + for signode in objnode: + if not isinstance(signode, addnodes.desc_signature): + continue + + # Convert signode to a specified format + info = {} + for key in domain_keys.get(domain, []): + value = signode.get(key) + if not value: + value = '' + info[key] = value + if not info: + continue + + # Call user code to resolve the link + uri = resolve_target(domain, info) + if not uri: + # no source + continue + + if uri in uris or not uri: + # only one link per name, please + continue + uris.add(uri) + + onlynode = addnodes.only(expr='html') + onlynode += nodes.reference('', '', internal=False, refuri=uri) + onlynode[0] += nodes.inline('', _('[source]'), + classes=['viewcode-link']) + signode += onlynode + +def setup(app): + app.connect('doctree-read', doctree_read) + app.add_config_value('linkcode_resolve', None, 'env') diff --git a/tests/root/conf.py b/tests/root/conf.py index b97ddfcc1..37d5e91a7 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -67,6 +67,28 @@ extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), # modify tags from conf.py tags.add('confpytag') +# -- linkcode + +if 'test_linkcode' in tags: + import glob + + extensions.remove('sphinx.ext.viewcode') + extensions.append('sphinx.ext.linkcode') + + exclude_patterns.extend(glob.glob('*.txt') + glob.glob('*/*.txt')) + exclude_patterns.remove('contents.txt') + exclude_patterns.remove('objects.txt') + + def linkcode_resolve(domain, info): + if domain == 'py': + fn = info['module'].replace('.', '/') + return "http://foobar/source/%s.py" % fn + elif domain == "js": + return "http://foobar/js/" + info['fullname'] + elif domain in ("c", "cpp"): + return "http://foobar/%s/%s" % (domain, "".join(info['names'])) + else: + raise AssertionError() # -- extension API diff --git a/tests/test_linkcode.py b/tests/test_linkcode.py new file mode 100644 index 000000000..b4b333328 --- /dev/null +++ b/tests/test_linkcode.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" + test_linkcode + ~~~~~~~~~~~~~ + + Test the sphinx.ext.linkcode extension. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import os +from util import * + +@with_app(srcdir='(temp)', buildername='html', tags=['test_linkcode']) +def test_html(app): + app.builder.build_all() + + fp = open(os.path.join(app.outdir, 'objects.html'), 'rb') + try: + stuff = fp.read() + finally: + fp.close() + + assert 'http://foobar/source/foolib.py' in stuff + assert 'http://foobar/js/' in stuff + assert 'http://foobar/c/' in stuff + assert 'http://foobar/cpp/' in stuff From da4ba903a3f88310a76bc9a4826cd20575cec864 Mon Sep 17 00:00:00 2001 From: Pauli Virtanen Date: Sun, 11 Mar 2012 17:55:29 +0100 Subject: [PATCH 2/2] DOC: linkcode: fix up documentation a bit --- doc/ext/linkcode.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/ext/linkcode.rst b/doc/ext/linkcode.rst index 9ae247563..a4ac394c7 100644 --- a/doc/ext/linkcode.rst +++ b/doc/ext/linkcode.rst @@ -7,15 +7,14 @@ .. versionadded:: 1.1 -This extension looks at your Python object descriptions -(``.. class::``, ``.. function::`` etc.) and adds external links to -code hosted somewhere on the web. The intent is similar to the +This extension looks at your object descriptions (``.. class::``, +``.. function::`` etc.) and adds external links to code hosted +somewhere on the web. The intent is similar to the ``sphinx.ext.viewcode`` extension, but assumes the source code can be found somewhere on the Internet. In your configuration, you need to specify a :confval:`linkcode_resolve` -function that returns an URL based on the name of a module and -the full name of the object. +function that returns an URL based on the object. .. confval:: linkcode_resolve @@ -29,9 +28,9 @@ the full name of the object. in. ``info`` is a dictionary with the following keys guaranteed to be present (dependent on the domain): + - ``py``: ``module`` (name of the module), ``fullname`` (name of the object) - ``c``: ``names`` (list of names for the object) - ``cpp``: ``names`` (list of names for the object) - - ``py``: ``module`` (name of the module), ``fullname`` (name of the object) - ``javascript``: ``object`` (name of the object), ``fullname`` (name of the item) Example: