diff options
Diffstat (limited to 'sphinx/ext')
-rw-r--r-- | sphinx/ext/autodoc.py | 39 | ||||
-rw-r--r-- | sphinx/ext/autosummary/__init__.py | 33 | ||||
-rw-r--r-- | sphinx/ext/coverage.py | 20 | ||||
-rw-r--r-- | sphinx/ext/doctest.py | 7 | ||||
-rw-r--r-- | sphinx/ext/graphviz.py | 38 | ||||
-rw-r--r-- | sphinx/ext/imgmath.py | 5 | ||||
-rw-r--r-- | sphinx/ext/inheritance_diagram.py | 8 | ||||
-rw-r--r-- | sphinx/ext/intersphinx.py | 119 | ||||
-rw-r--r-- | sphinx/ext/jsmath.py | 6 | ||||
-rw-r--r-- | sphinx/ext/mathbase.py | 186 | ||||
-rw-r--r-- | sphinx/ext/mathjax.py | 5 | ||||
-rw-r--r-- | sphinx/ext/napoleon/__init__.py | 95 | ||||
-rw-r--r-- | sphinx/ext/napoleon/docstring.py | 127 | ||||
-rw-r--r-- | sphinx/ext/todo.py | 7 | ||||
-rw-r--r-- | sphinx/ext/viewcode.py | 5 |
15 files changed, 441 insertions, 259 deletions
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index 405d24c88..59e585678 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -31,7 +31,7 @@ from sphinx.application import ExtensionError from sphinx.util.nodes import nested_parse_with_titles from sphinx.util.compat import Directive from sphinx.util.inspect import getargspec, isdescriptor, safe_getmembers, \ - safe_getattr, object_description, is_builtin_class_method + safe_getattr, object_description, is_builtin_class_method, isenumattribute from sphinx.util.docstrings import prepare_docstring try: @@ -86,17 +86,24 @@ class Options(dict): class _MockModule(object): """Used by autodoc_mock_imports.""" + __file__ = '/dev/null' + __path__ = '/dev/null' + def __init__(self, *args, **kwargs): - pass + self.__all__ = [] def __call__(self, *args, **kwargs): + if args and type(args[0]) in [FunctionType, MethodType]: + # Appears to be a decorator, pass through unchanged + return args[0] return _MockModule() + def _append_submodule(self, submod): + self.__all__.append(submod) + @classmethod def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name[0] == name[0].upper(): + if name[0] == name[0].upper(): # Not very good, we assume Uppercase names are classes... mocktype = type(name, (), {}) mocktype.__module__ = __name__ @@ -109,9 +116,12 @@ def mock_import(modname): if '.' in modname: pkg, _n, mods = modname.rpartition('.') mock_import(pkg) - mod = _MockModule() - sys.modules[modname] = mod - return mod + if isinstance(sys.modules[pkg], _MockModule): + sys.modules[pkg]._append_submodule(mods) + + if modname not in sys.modules: + mod = _MockModule() + sys.modules[modname] = mod ALL = object() @@ -304,8 +314,9 @@ def format_annotation(annotation): return '%s[%s]' % (qualified_name, param_str) elif hasattr(typing, 'CallableMeta') and \ isinstance(annotation, typing.CallableMeta) and \ - hasattr(annotation, '__args__') and \ + getattr(annotation, '__args__', None) is not None and \ hasattr(annotation, '__result__'): + # Skipped in the case of plain typing.Callable args = annotation.__args__ if args is None: return qualified_name @@ -372,7 +383,9 @@ def formatargspec(function, args, varargs=None, varkw=None, defaults=None, formatted.append('*' + format_arg_with_annotation(varargs)) if kwonlyargs: - formatted.append('*') + if not varargs: + formatted.append('*') + for kwarg in kwonlyargs: arg_fd = StringIO() arg_fd.write(format_arg_with_annotation(kwarg)) @@ -524,7 +537,7 @@ class Documenter(object): try: dbg('[autodoc] import %s', self.modname) for modname in self.env.config.autodoc_mock_imports: - dbg('[autodoc] adding a mock module %s!', self.modname) + dbg('[autodoc] adding a mock module %s!', modname) mock_import(modname) __import__(self.modname) parent = None @@ -1284,7 +1297,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): u':class:`%s`' % b.__name__ or u':class:`%s.%s`' % (b.__module__, b.__name__) for b in self.object.__bases__] - self.add_line(_(u' Bases: %s') % ', '.join(bases), + self.add_line(u' ' + _(u'Bases: %s') % ', '.join(bases), sourcename) def get_doc(self, encoding=None, ignore=1): @@ -1479,6 +1492,8 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): def import_object(self): ret = ClassLevelDocumenter.import_object(self) + if isenumattribute(self.object): + self.object = self.object.value if isdescriptor(self.object) and \ not isinstance(self.object, self.method_types): self._datadescriptor = True diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index a941aa33a..030fec301 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -58,6 +58,7 @@ import re import sys import inspect import posixpath +from six import string_types from types import ModuleType from six import text_type @@ -67,7 +68,7 @@ from docutils import nodes import sphinx from sphinx import addnodes -from sphinx.util import rst +from sphinx.util import import_object, rst from sphinx.util.compat import Directive from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.ext.autodoc import Options @@ -136,7 +137,7 @@ def autosummary_table_visit_html(self, node): # -- autodoc integration ------------------------------------------------------- -class FakeDirective: +class FakeDirective(object): env = {} genopt = Options() @@ -543,6 +544,22 @@ def autolink_role(typ, rawtext, etext, lineno, inliner, return r +def get_rst_suffix(app): + def get_supported_format(suffix): + parser_class = app.config.source_parsers.get(suffix) + if parser_class is None: + return ('restructuredtext',) + if isinstance(parser_class, string_types): + parser_class = import_object(parser_class, 'source parser') + return parser_class.supported + + for suffix in app.config.source_suffix: + if 'restructuredtext' in get_supported_format(suffix): + return suffix + + return None + + def process_generate_options(app): genfiles = app.config.autosummary_generate @@ -556,12 +573,18 @@ def process_generate_options(app): from sphinx.ext.autosummary.generate import generate_autosummary_docs - ext = app.config.source_suffix[0] - genfiles = [genfile + (not genfile.endswith(ext) and ext or '') + ext = app.config.source_suffix + genfiles = [genfile + (not genfile.endswith(tuple(ext)) and ext[0] or '') for genfile in genfiles] + suffix = get_rst_suffix(app) + if suffix is None: + app.warn('autosummary generats .rst files internally. ' + 'But your source_suffix does not contain .rst. Skipped.') + return + generate_autosummary_docs(genfiles, builder=app.builder, - warn=app.warn, info=app.info, suffix=ext, + warn=app.warn, info=app.info, suffix=suffix, base_path=app.srcdir) diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 78281bb85..c08b1e706 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -87,8 +87,7 @@ class CoverageBuilder(Builder): c_objects = self.env.domaindata['c']['objects'] for filename in self.c_sourcefiles: undoc = set() - f = open(filename, 'r') - try: + with open(filename, 'r') as f: for line in f: for key, regex in self.c_regexes: match = regex.match(line) @@ -101,15 +100,12 @@ class CoverageBuilder(Builder): else: undoc.add((key, name)) continue - finally: - f.close() if undoc: self.c_undoc[filename] = undoc def write_c_coverage(self): output_file = path.join(self.outdir, 'c.txt') - op = open(output_file, 'w') - try: + with open(output_file, 'w') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented C API elements', '=') op.write('\n') @@ -119,8 +115,6 @@ class CoverageBuilder(Builder): for typ, name in sorted(undoc): op.write(' * %-50s [%9s]\n' % (name, typ)) op.write('\n') - finally: - op.close() def build_py_coverage(self): objects = self.env.domaindata['py']['objects'] @@ -214,9 +208,8 @@ class CoverageBuilder(Builder): def write_py_coverage(self): output_file = path.join(self.outdir, 'python.txt') - op = open(output_file, 'w') failed = [] - try: + with open(output_file, 'w') as op: if self.config.coverage_write_headline: write_header(op, 'Undocumented Python objects', '=') keys = sorted(self.py_undoc.keys()) @@ -247,17 +240,12 @@ class CoverageBuilder(Builder): if failed: write_header(op, 'Modules that failed to import') op.writelines(' * %s -- %s\n' % x for x in failed) - finally: - op.close() def finish(self): # dump the coverage data to a pickle file too picklepath = path.join(self.outdir, 'undoc.pickle') - dumpfile = open(picklepath, 'wb') - try: + with open(picklepath, 'wb') as dumpfile: pickle.dump((self.py_undoc, self.c_undoc), dumpfile) - finally: - dumpfile.close() def setup(app): diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 0f5241a19..244762b69 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -214,8 +214,7 @@ class DocTestBuilder(Builder): def init(self): # default options - self.opt = doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | \ - doctest.IGNORE_EXCEPTION_DETAIL + self.opt = self.config.doctest_default_flags # HACK HACK HACK # doctest compiles its snippets with type 'single'. That is nice @@ -464,4 +463,8 @@ def setup(app): app.add_config_value('doctest_test_doctest_blocks', 'default', False) app.add_config_value('doctest_global_setup', '', False) app.add_config_value('doctest_global_cleanup', '', False) + app.add_config_value( + 'doctest_default_flags', + doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL, + False) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 4e06677ca..5e76eb8ba 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -43,6 +43,8 @@ class graphviz(nodes.General, nodes.Inline, nodes.Element): def figure_wrapper(directive, node, caption): figure_node = nodes.figure('', node) + if 'align' in node: + figure_node['align'] = node.attributes.pop('align') parsed = nodes.Element() directive.state.nested_parse(ViewList([caption], source=''), @@ -55,6 +57,10 @@ def figure_wrapper(directive, node, caption): return figure_node +def align_spec(argument): + return directives.choice(argument, ('left', 'center', 'right')) + + class Graphviz(Directive): """ Directive to insert arbitrary dot markup. @@ -65,6 +71,7 @@ class Graphviz(Directive): final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, + 'align': align_spec, 'inline': directives.flag, 'caption': directives.unchanged, 'graphviz_dot': directives.unchanged, @@ -82,11 +89,8 @@ class Graphviz(Directive): rel_filename, filename = env.relfn2path(argument) env.note_dependency(rel_filename) try: - fp = codecs.open(filename, 'r', 'utf-8') - try: + with codecs.open(filename, 'r', 'utf-8') as fp: dotcode = fp.read() - finally: - fp.close() except (IOError, OSError): return [document.reporter.warning( 'External Graphviz file %r not found or reading ' @@ -104,6 +108,8 @@ class Graphviz(Directive): node['options']['graphviz_dot'] = self.options['graphviz_dot'] if 'alt' in self.options: node['alt'] = self.options['alt'] + if 'align' in self.options: + node['align'] = self.options['align'] if 'inline' in self.options: node['inline'] = True @@ -124,6 +130,7 @@ class GraphvizSimple(Directive): final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, + 'align': align_spec, 'inline': directives.flag, 'caption': directives.unchanged, 'graphviz_dot': directives.unchanged, @@ -138,6 +145,8 @@ class GraphvizSimple(Directive): node['options']['graphviz_dot'] = self.options['graphviz_dot'] if 'alt' in self.options: node['alt'] = self.options['alt'] + if 'align' in self.options: + node['align'] = self.options['align'] if 'inline' in self.options: node['inline'] = True @@ -239,11 +248,11 @@ def render_dot_html(self, node, code, options, prefix='graphviz', <p class="warning">%s</p></object>\n''' % (fname, alt) self.body.append(svgtag) else: - mapfile = open(outfn + '.map', 'rb') - try: + if 'align' in node: + self.body.append('<div align="%s" class="align-%s">' % + (node['align'], node['align'])) + with open(outfn + '.map', 'rb') as mapfile: imgmap = mapfile.readlines() - finally: - mapfile.close() if len(imgmap) == 2: # nothing in image map (the lines are <map> and </map>) self.body.append('<img src="%s" alt="%s" %s/>\n' % @@ -254,6 +263,8 @@ def render_dot_html(self, node, code, options, prefix='graphviz', self.body.append('<img src="%s" alt="%s" usemap="#%s" %s/>\n' % (fname, alt, mapname, imgcss)) self.body.extend([item.decode('utf-8') for item in imgmap]) + if 'align' in node: + self.body.append('</div>\n') raise nodes.SkipNode @@ -277,8 +288,19 @@ def render_dot_latex(self, node, code, options, prefix='graphviz'): para_separator = '\n' if fname is not None: + post = None + if not is_inline and 'align' in node: + if node['align'] == 'left': + self.body.append('{') + post = '\\hspace*{\\fill}}' + elif node['align'] == 'right': + self.body.append('{\\hspace*{\\fill}') + post = '}' self.body.append('%s\\includegraphics{%s}%s' % (para_separator, fname, para_separator)) + if post: + self.body.append(post) + raise nodes.SkipNode diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index f8e402be3..e5b8b26c5 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -22,6 +22,7 @@ from six import text_type from docutils import nodes import sphinx +from sphinx.locale import _ from sphinx.errors import SphinxError, ExtensionError from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.osutil import ensuredir, ENOENT, cd @@ -253,7 +254,9 @@ def html_visit_displaymath(self, node): self.body.append(self.starttag(node, 'div', CLASS='math')) self.body.append('<p>') if node['number']: - self.body.append('<span class="eqno">(%s)</span>' % node['number']) + self.body.append('<span class="eqno">(%s)' % node['number']) + self.add_permalink_ref(node, _('Permalink to this equation')) + self.body.append('</span>') if fname is None: # something failed -- use text-only as a bad substitute self.body.append('<span class="math">%s</span></p>\n</div>' % diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index a06c4b17c..11af67dc5 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -52,7 +52,7 @@ from docutils.parsers.rst import directives import sphinx from sphinx.ext.graphviz import render_dot_html, render_dot_latex, \ - render_dot_texinfo + render_dot_texinfo, figure_wrapper from sphinx.pycode import ModuleAnalyzer from sphinx.util import force_decode from sphinx.util.compat import Directive @@ -297,6 +297,7 @@ class InheritanceDiagram(Directive): option_spec = { 'parts': directives.nonnegative_int, 'private-bases': directives.flag, + 'caption': directives.unchanged, } def run(self): @@ -330,6 +331,11 @@ class InheritanceDiagram(Directive): # Store the graph object so we can use it to generate the # dot file later node['graph'] = graph + + # wrap the result in figure node + caption = self.options.get('caption') + if caption: + node = figure_wrapper(self, node, caption) return [node] diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 7b68b3bb0..4ef7e4b9b 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -34,7 +34,6 @@ from os import path import re from six import iteritems, string_types -from six.moves.urllib import request from six.moves.urllib.parse import urlsplit, urlunsplit from docutils import nodes from docutils.utils import relative_path @@ -42,17 +41,9 @@ from docutils.utils import relative_path import sphinx from sphinx.locale import _ from sphinx.builders.html import INVENTORY_FILENAME +from sphinx.util.requests import requests, useragent_header -default_handlers = [request.ProxyHandler(), request.HTTPRedirectHandler(), - request.HTTPHandler()] -try: - default_handlers.append(request.HTTPSHandler) -except AttributeError: - pass - -default_opener = request.build_opener(*default_handlers) - UTF8StreamReader = codecs.lookup('utf-8')[2] @@ -125,6 +116,14 @@ def read_inventory_v2(f, uri, join, bufsize=16*1024): return invdata +def read_inventory(f, uri, join, bufsize=16*1024): + line = f.readline().rstrip().decode('utf-8') + if line == '# Sphinx inventory version 1': + return read_inventory_v1(f, uri, join) + elif line == '# Sphinx inventory version 2': + return read_inventory_v2(f, uri, join, bufsize=bufsize) + + def _strip_basic_auth(url): """Returns *url* with basic auth credentials removed. Also returns the basic auth username and password if they're present in *url*. @@ -136,30 +135,17 @@ def _strip_basic_auth(url): :param url: url which may or may not contain basic auth credentials :type url: ``str`` - :return: 3-``tuple`` of: - - * (``str``) -- *url* with any basic auth creds removed - * (``str`` or ``NoneType``) -- basic auth username or ``None`` if basic - auth username not given - * (``str`` or ``NoneType``) -- basic auth password or ``None`` if basic - auth password not given - - :rtype: ``tuple`` + :return: *url* with any basic auth creds removed + :rtype: ``str`` """ - url_parts = urlsplit(url) - username = url_parts.username - password = url_parts.password - frags = list(url_parts) + frags = list(urlsplit(url)) # swap out "user[:pass]@hostname" for "hostname" - if url_parts.port: - frags[1] = "%s:%s" % (url_parts.hostname, url_parts.port) - else: - frags[1] = url_parts.hostname - url = urlunsplit(frags) - return (url, username, password) + if '@' in frags[1]: + frags[1] = frags[1].split('@')[1] + return urlunsplit(frags) -def _read_from_url(url): +def _read_from_url(url, timeout=None): """Reads data from *url* with an HTTP *GET*. This function supports fetching from resources which use basic HTTP auth as @@ -175,48 +161,35 @@ def _read_from_url(url): :return: data read from resource described by *url* :rtype: ``file``-like object """ - url, username, password = _strip_basic_auth(url) - if username is not None and password is not None: - # case: url contains basic auth creds - password_mgr = request.HTTPPasswordMgrWithDefaultRealm() - password_mgr.add_password(None, url, username, password) - handler = request.HTTPBasicAuthHandler(password_mgr) - opener = request.build_opener(*(default_handlers + [handler])) - else: - opener = default_opener - - return opener.open(url) + r = requests.get(url, stream=True, timeout=timeout, headers=dict(useragent_header)) + r.raise_for_status() + r.raw.url = r.url + return r.raw def _get_safe_url(url): """Gets version of *url* with basic auth passwords obscured. This function returns results suitable for printing and logging. - E.g.: https://user:12345@example.com => https://user:********@example.com - - .. note:: - - The number of astrisks is invariant in the length of the basic auth - password, so minimal information is leaked. + E.g.: https://user:12345@example.com => https://user@example.com :param url: a url :type url: ``str`` - :return: *url* with password obscured + :return: *url* with password removed :rtype: ``str`` """ - safe_url = url - url, username, _ = _strip_basic_auth(url) - if username is not None: - # case: url contained basic auth creds; obscure password - url_parts = urlsplit(url) - safe_netloc = '{0}@{1}'.format(username, url_parts.hostname) - # replace original netloc w/ obscured version - frags = list(url_parts) - frags[1] = safe_netloc - safe_url = urlunsplit(frags) + parts = urlsplit(url) + if parts.username is None: + return url + else: + frags = list(parts) + if parts.port: + frags[1] = '{0}@{1}:{2}'.format(parts.username, parts.hostname, parts.port) + else: + frags[1] = '{0}@{1}'.format(parts.username, parts.hostname) - return safe_url + return urlunsplit(frags) def fetch_inventory(app, uri, inv): @@ -226,11 +199,10 @@ def fetch_inventory(app, uri, inv): localuri = '://' not in uri if not localuri: # case: inv URI points to remote resource; strip any existing auth - uri, _, _ = _strip_basic_auth(uri) - join = localuri and path.join or posixpath.join + uri = _strip_basic_auth(uri) try: if '://' in inv: - f = _read_from_url(inv) + f = _read_from_url(inv, timeout=app.config.intersphinx_timeout) else: f = open(path.join(app.srcdir, inv), 'rb') except Exception as err: @@ -238,25 +210,19 @@ def fetch_inventory(app, uri, inv): '%s: %s' % (inv, err.__class__, err)) return try: - if hasattr(f, 'geturl'): - newinv = f.geturl() + if hasattr(f, 'url'): + newinv = f.url if inv != newinv: app.info('intersphinx inventory has moved: %s -> %s' % (inv, newinv)) if uri in (inv, path.dirname(inv), path.dirname(inv) + '/'): uri = path.dirname(newinv) - line = f.readline().rstrip().decode('utf-8') - try: - if line == '# Sphinx inventory version 1': - invdata = read_inventory_v1(f, uri, join) - elif line == '# Sphinx inventory version 2': - invdata = read_inventory_v2(f, uri, join) - else: - raise ValueError - f.close() - except ValueError: - f.close() - raise ValueError('unknown or unsupported inventory version') + with f: + try: + join = localuri and path.join or posixpath.join + invdata = read_inventory(f, uri, join) + except ValueError: + raise ValueError('unknown or unsupported inventory version') except Exception as err: app.warn('intersphinx inventory %r not readable due to ' '%s: %s' % (inv, err.__class__.__name__, err)) @@ -394,6 +360,7 @@ def missing_reference(app, env, node, contnode): def setup(app): app.add_config_value('intersphinx_mapping', {}, True) app.add_config_value('intersphinx_cache_limit', 5, False) + app.add_config_value('intersphinx_timeout', None, False) app.connect('missing-reference', missing_reference) app.connect('builder-inited', load_mappings) return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/jsmath.py b/sphinx/ext/jsmath.py index f36e12fed..9981ffd3a 100644 --- a/sphinx/ext/jsmath.py +++ b/sphinx/ext/jsmath.py @@ -13,6 +13,7 @@ from docutils import nodes import sphinx +from sphinx.locale import _ from sphinx.application import ExtensionError from sphinx.ext.mathbase import setup_math as mathbase_setup @@ -34,8 +35,9 @@ def html_visit_displaymath(self, node): if i == 0: # necessary to e.g. set the id property correctly if node['number']: - self.body.append('<span class="eqno">(%s)</span>' % - node['number']) + self.body.append('<span class="eqno">(%s)' % node['number']) + self.add_permalink_ref(node, _('Permalink to this equation')) + self.body.append('</span>') self.body.append(self.starttag(node, 'div', CLASS='math')) else: # but only once! diff --git a/sphinx/ext/mathbase.py b/sphinx/ext/mathbase.py index 430813a64..ae4b439b7 100644 --- a/sphinx/ext/mathbase.py +++ b/sphinx/ext/mathbase.py @@ -12,7 +12,10 @@ from docutils import nodes, utils from docutils.parsers.rst import directives -from sphinx.util.nodes import set_source_info +from sphinx.roles import XRefRole +from sphinx.locale import _ +from sphinx.domains import Domain +from sphinx.util.nodes import make_refnode, set_source_info from sphinx.util.compat import Directive @@ -28,6 +31,76 @@ class eqref(nodes.Inline, nodes.TextElement): pass +class EqXRefRole(XRefRole): + def result_nodes(self, document, env, node, is_ref): + node['refdomain'] = 'math' + return [node], [] + + +class MathDomain(Domain): + """Mathematics domain.""" + name = 'math' + label = 'mathematics' + + initial_data = { + 'objects': {}, # labelid -> (docname, eqno) + } + dangling_warnings = { + 'eq': 'equation not found: %(target)s', + } + + def clear_doc(self, docname): + for labelid, (doc, eqno) in list(self.data['objects'].items()): + if doc == docname: + del self.data['objects'][labelid] + + def merge_domaindata(self, docnames, otherdata): + for labelid, (doc, eqno) in otherdata['objects'].items(): + if doc in docnames: + self.data['objects'][labelid] = doc + + def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): + assert typ == 'eq' + docname, number = self.data['objects'].get(target, (None, None)) + if docname: + if builder.name == 'latex': + newnode = eqref('', **node.attributes) + newnode['docname'] = docname + newnode['target'] = target + return newnode + else: + title = nodes.Text("(%d)" % number) + return make_refnode(builder, fromdocname, docname, + "equation-" + target, title) + else: + return None + + def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): + refnode = self.resolve_xref(env, fromdocname, builder, 'eq', target, node, contnode) + if refnode is None: + return [] + else: + return [refnode] + + def get_objects(self): + return [] + + def add_equation(self, env, docname, labelid): + equations = self.data['objects'] + if labelid in equations: + path = env.doc2path(equations[labelid][0]) + msg = _('duplicate label of equation %s, other instance in %s') % (labelid, path) + raise UserWarning(msg) + else: + eqno = self.get_next_equation_number(docname) + equations[labelid] = (docname, eqno) + return eqno + + def get_next_equation_number(self, docname): + targets = [eq for eq in self.data['objects'].values() if eq[0] == docname] + return len(targets) + 1 + + def wrap_displaymath(math, label, numbering): def is_equation(part): return part.strip() @@ -68,13 +141,6 @@ def math_role(role, rawtext, text, lineno, inliner, options={}, content=[]): return [math(latex=latex)], [] -def eq_role(role, rawtext, text, lineno, inliner, options={}, content=[]): - text = utils.unescape(text) - node = eqref('(?)', '(?)', target=text) - node['docname'] = inliner.document.settings.env.docname - return [node], [] - - def is_in_section_title(node): """Determine whether the node is in a section title""" from sphinx.util.nodes import traverse_parent @@ -104,21 +170,47 @@ class MathDirective(Directive): latex = self.arguments[0] + '\n\n' + latex node = displaymath() node['latex'] = latex - node['label'] = self.options.get('name', None) - if node['label'] is None: - node['label'] = self.options.get('label', None) + node['number'] = None + node['label'] = None + if 'name' in self.options: + node['label'] = self.options['name'] + if 'label' in self.options: + node['label'] = self.options['label'] node['nowrap'] = 'nowrap' in self.options node['docname'] = self.state.document.settings.env.docname ret = [node] set_source_info(self, node) if hasattr(self, 'src'): node.source = self.src - if node['label']: - tnode = nodes.target('', '', ids=['equation-' + node['label']]) - self.state.document.note_explicit_target(tnode) - ret.insert(0, tnode) + self.add_target(ret) return ret + def add_target(self, ret): + node = ret[0] + env = self.state.document.settings.env + + # assign label automatically if math_number_all enabled + if node['label'] == '' or (env.config.math_number_all and not node['label']): + seq = env.new_serialno('sphinx.ext.math#equations') + node['label'] = "%s:%d" % (env.docname, seq) + + # no targets and numbers are needed + if not node['label']: + return + + # register label to domain + domain = env.get_domain('math') + try: + eqno = domain.add_equation(env, env.docname, node['label']) + node['number'] = eqno + + # add target node + target = nodes.target('', '', ids=['equation-' + node['label']]) + self.state.document.note_explicit_target(target) + ret.insert(0, target) + except UserWarning as exc: + self.state_machine.reporter.warning(exc.args[0], line=self.lineno) + def latex_visit_math(self, node): if is_in_section_title(node): @@ -131,7 +223,11 @@ def latex_visit_math(self, node): def latex_visit_displaymath(self, node): - label = node['label'] and node['docname'] + '-' + node['label'] or None + if not node['label']: + label = None + else: + label = "equation:%s:%s" % (node['docname'], node['label']) + if node['nowrap']: if label: self.body.append(r'\label{%s}' % label) @@ -143,7 +239,8 @@ def latex_visit_displaymath(self, node): def latex_visit_eqref(self, node): - self.body.append('\\eqref{%s-%s}' % (node['docname'], node['target'])) + label = "equation:%s:%s" % (node['docname'], node['target']) + self.body.append('\\eqref{%s}' % label) raise nodes.SkipNode @@ -159,11 +256,6 @@ def text_visit_displaymath(self, node): raise nodes.SkipNode -def text_visit_eqref(self, node): - self.add_text(node['target']) - raise nodes.SkipNode - - def man_visit_math(self, node): self.body.append(node['latex']) raise nodes.SkipNode @@ -177,11 +269,6 @@ def man_depart_displaymath(self, node): self.depart_centered(node) -def man_visit_eqref(self, node): - self.body.append(node['target']) - raise nodes.SkipNode - - def texinfo_visit_math(self, node): self.body.append('@math{' + self.escape_arg(node['latex']) + '}') raise nodes.SkipNode @@ -198,40 +285,9 @@ def texinfo_depart_displaymath(self, node): pass -def texinfo_visit_eqref(self, node): - self.add_xref(node['docname'] + ':' + node['target'], - node['target'], node) - raise nodes.SkipNode - - -def html_visit_eqref(self, node): - self.body.append('<a href="#equation-%s">' % node['target']) - - -def html_depart_eqref(self, node): - self.body.append('</a>') - - -def number_equations(app, doctree, docname): - num = 0 - numbers = {} - for node in doctree.traverse(displaymath): - if node['label'] is not None or app.config.math_number_all: - num += 1 - node['number'] = num - if node['label'] is not None: - numbers[node['label']] = num - else: - node['number'] = None - for node in doctree.traverse(eqref): - if node['target'] not in numbers: - continue - num = '(%d)' % numbers[node['target']] - node[0] = nodes.Text(num, num) - - def setup_math(app, htmlinlinevisitors, htmldisplayvisitors): - app.add_config_value('math_number_all', False, 'html') + app.add_config_value('math_number_all', False, 'env') + app.add_domain(MathDomain) app.add_node(math, override=True, latex=(latex_visit_math, None), text=(text_visit_math, None), @@ -244,13 +300,7 @@ def setup_math(app, htmlinlinevisitors, htmldisplayvisitors): man=(man_visit_displaymath, man_depart_displaymath), texinfo=(texinfo_visit_displaymath, texinfo_depart_displaymath), html=htmldisplayvisitors) - app.add_node(eqref, - latex=(latex_visit_eqref, None), - text=(text_visit_eqref, None), - man=(man_visit_eqref, None), - texinfo=(texinfo_visit_eqref, None), - html=(html_visit_eqref, html_depart_eqref)) + app.add_node(eqref, latex=(latex_visit_eqref, None)) app.add_role('math', math_role) - app.add_role('eq', eq_role) + app.add_role('eq', EqXRefRole(warn_dangling=True)) app.add_directive('math', MathDirective) - app.connect('doctree-resolved', number_equations) diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py index bdaab55e0..2f414f4e7 100644 --- a/sphinx/ext/mathjax.py +++ b/sphinx/ext/mathjax.py @@ -14,6 +14,7 @@ from docutils import nodes import sphinx +from sphinx.locale import _ from sphinx.errors import ExtensionError from sphinx.ext.mathbase import setup_math as mathbase_setup @@ -35,7 +36,9 @@ def html_visit_displaymath(self, node): # necessary to e.g. set the id property correctly if node['number']: - self.body.append('<span class="eqno">(%s)</span>' % node['number']) + self.body.append('<span class="eqno">(%s)' % node['number']) + self.add_permalink_ref(node, _('Permalink to this equation')) + self.body.append('</span>') self.body.append(self.builder.config.mathjax_display[0]) parts = [prt for prt in node['latex'].split('\n\n') if prt.strip()] if len(parts) > 1: # Add alignment if there are more than 1 equation diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py index 85c8acec8..651355c57 100644 --- a/sphinx/ext/napoleon/__init__.py +++ b/sphinx/ext/napoleon/__init__.py @@ -33,6 +33,7 @@ class Config(object): # Napoleon settings napoleon_google_docstring = True napoleon_numpy_docstring = True + napoleon_include_init_with_doc = False napoleon_include_private_with_doc = False napoleon_include_special_with_doc = False napoleon_use_admonition_for_examples = False @@ -41,6 +42,7 @@ class Config(object): napoleon_use_ivar = False napoleon_use_param = True napoleon_use_rtype = True + napoleon_use_keyword = True .. _Google style: http://google.github.io/styleguide/pyguide.html @@ -49,13 +51,29 @@ class Config(object): Attributes ---------- - napoleon_google_docstring : bool, defaults to True + napoleon_google_docstring : :obj:`bool` (Defaults to True) True to parse `Google style`_ docstrings. False to disable support for Google style docstrings. - napoleon_numpy_docstring : bool, defaults to True + napoleon_numpy_docstring : :obj:`bool` (Defaults to True) True to parse `NumPy style`_ docstrings. False to disable support for NumPy style docstrings. - napoleon_include_private_with_doc : bool, defaults to False + napoleon_include_init_with_doc : :obj:`bool` (Defaults to False) + True to list ``__init___`` docstrings separately from the class + docstring. False to fall back to Sphinx's default behavior, which + considers the ``__init___`` docstring as part of the class + documentation. + + **If True**:: + + def __init__(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + + def __init__(self): + # This will NOT be included in the docs + + napoleon_include_private_with_doc : :obj:`bool` (Defaults to False) True to include private members (like ``_membername``) with docstrings in the documentation. False to fall back to Sphinx's default behavior. @@ -71,7 +89,7 @@ class Config(object): # This will NOT be included in the docs pass - napoleon_include_special_with_doc : bool, defaults to False + napoleon_include_special_with_doc : :obj:`bool` (Defaults to False) True to include special members (like ``__membername__``) with docstrings in the documentation. False to fall back to Sphinx's default behavior. @@ -88,7 +106,7 @@ class Config(object): # This will NOT be included in the docs return unicode(self.__class__.__name__) - napoleon_use_admonition_for_examples : bool, defaults to False + napoleon_use_admonition_for_examples : :obj:`bool` (Defaults to False) True to use the ``.. admonition::`` directive for the **Example** and **Examples** sections. False to use the ``.. rubric::`` directive instead. One may look better than the other depending on what HTML @@ -112,7 +130,7 @@ class Config(object): This is just a quick example - napoleon_use_admonition_for_notes : bool, defaults to False + napoleon_use_admonition_for_notes : :obj:`bool` (Defaults to False) True to use the ``.. admonition::`` directive for **Notes** sections. False to use the ``.. rubric::`` directive instead. @@ -125,7 +143,7 @@ class Config(object): -------- :attr:`napoleon_use_admonition_for_examples` - napoleon_use_admonition_for_references : bool, defaults to False + napoleon_use_admonition_for_references : :obj:`bool` (Defaults to False) True to use the ``.. admonition::`` directive for **References** sections. False to use the ``.. rubric::`` directive instead. @@ -133,7 +151,7 @@ class Config(object): -------- :attr:`napoleon_use_admonition_for_examples` - napoleon_use_ivar : bool, defaults to False + napoleon_use_ivar : :obj:`bool` (Defaults to False) True to use the ``:ivar:`` role for instance variables. False to use the ``.. attribute::`` directive instead. @@ -157,7 +175,7 @@ class Config(object): Description of `attr1` - napoleon_use_param : bool, defaults to True + napoleon_use_param : :obj:`bool` (Defaults to True) True to use a ``:param:`` role for each function parameter. False to use a single ``:parameters:`` role for all the parameters. @@ -184,7 +202,22 @@ class Config(object): * **arg2** (*int, optional*) -- Description of `arg2`, defaults to 0 - napoleon_use_rtype : bool, defaults to True + napoleon_use_keyword : :obj:`bool` (Defaults to True) + True to use a ``:keyword:`` role for each function keyword argument. + False to use a single ``:keyword arguments:`` role for all the + keywords. + + This behaves similarly to :attr:`napoleon_use_param`. Note unlike + docutils, ``:keyword:`` and ``:param:`` will not be treated the same + way - there will be a separate "Keyword Arguments" section, rendered + in the same fashion as "Parameters" section (type links created if + possible) + + See Also + -------- + :attr:`napoleon_use_param` + + napoleon_use_rtype : :obj:`bool` (Defaults to True) True to use the ``:rtype:`` role for the return type. False to output the return type inline with the description. @@ -208,6 +241,7 @@ class Config(object): _config_values = { 'napoleon_google_docstring': (True, 'env'), 'napoleon_numpy_docstring': (True, 'env'), + 'napoleon_include_init_with_doc': (False, 'env'), 'napoleon_include_private_with_doc': (False, 'env'), 'napoleon_include_special_with_doc': (False, 'env'), 'napoleon_use_admonition_for_examples': (False, 'env'), @@ -216,6 +250,7 @@ class Config(object): 'napoleon_use_ivar': (False, 'env'), 'napoleon_use_param': (True, 'env'), 'napoleon_use_rtype': (True, 'env'), + 'napoleon_use_keyword': (True, 'env') } def __init__(self, **settings): @@ -251,6 +286,8 @@ def setup(app): if not isinstance(app, Sphinx): return # probably called by tests + _patch_python_domain() + app.connect('autodoc-process-docstring', _process_docstring) app.connect('autodoc-skip-member', _skip_member) @@ -259,6 +296,26 @@ def setup(app): return {'version': sphinx.__display_version__, 'parallel_read_safe': True} +def _patch_python_domain(): + try: + from sphinx.domains.python import PyTypedField + except ImportError: + pass + else: + import sphinx.domains.python + import sphinx.locale + l_ = sphinx.locale.lazy_gettext + for doc_field in sphinx.domains.python.PyObject.doc_field_types: + if doc_field.name == 'parameter': + doc_field.names = ('param', 'parameter', 'arg', 'argument') + break + sphinx.domains.python.PyObject.doc_field_types.append( + PyTypedField('keyword', label=l_('Keyword Arguments'), + names=('keyword', 'kwarg', 'kwparam'), + typerolename='obj', typenames=('paramtype', 'kwtype'), + can_collapse=True)) + + def _process_docstring(app, what, name, obj, options, lines): """Process the docstring for a given python object. @@ -311,8 +368,10 @@ def _skip_member(app, what, name, obj, skip, options): """Determine if private and special class members are included in docs. The following settings in conf.py determine if private and special class - members are included in the generated documentation: + members or init methods are included in the generated documentation: + * ``napoleon_include_init_with_doc`` -- + include init methods if they have docstrings * ``napoleon_include_private_with_doc`` -- include private members if they have docstrings * ``napoleon_include_special_with_doc`` -- @@ -349,7 +408,7 @@ def _skip_member(app, what, name, obj, skip, options): """ has_doc = getattr(obj, '__doc__', False) is_member = (what == 'class' or what == 'exception' or what == 'module') - if name != '__weakref__' and name != '__init__' and has_doc and is_member: + if name != '__weakref__' and has_doc and is_member: cls_is_owner = False if what == 'class' or what == 'exception': if PY2: @@ -382,10 +441,16 @@ def _skip_member(app, what, name, obj, skip, options): cls_is_owner = True if what == 'module' or cls_is_owner: - is_special = name.startswith('__') and name.endswith('__') - is_private = not is_special and name.startswith('_') + is_init = (name == '__init__') + is_special = (not is_init and name.startswith('__') and + name.endswith('__')) + is_private = (not is_init and not is_special and + name.startswith('_')) + inc_init = app.config.napoleon_include_init_with_doc inc_special = app.config.napoleon_include_special_with_doc inc_private = app.config.napoleon_include_private_with_doc - if (is_special and inc_special) or (is_private and inc_private): + if ((is_special and inc_special) or + (is_private and inc_private) or + (is_init and inc_init)): return False return skip diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 742a9fbba..e526a11ae 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -24,8 +24,9 @@ from sphinx.util.pycompat import UnicodeMixin _directive_regex = re.compile(r'\.\. \S+::') _google_section_regex = re.compile(r'^(\s|\w)+:\s*$') -_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.+?)\s*\)') +_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)') _numpy_section_regex = re.compile(r'^[=\-`:\'"~^_*+#<>]{2,}\s*$') +_single_colon_regex = re.compile(r'(?<!:):(?!:)') _xref_regex = re.compile(r'(:\w+:\S+:`.+?`|:\S+:`.+?`|`.+?`)') _bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)') _enumerated_list_regex = re.compile( @@ -39,36 +40,34 @@ class GoogleDocstring(UnicodeMixin): Parameters ---------- - docstring : str or List[str] + docstring : :obj:`str` or :obj:`list` of :obj:`str` The docstring to parse, given either as a string or split into individual lines. - config : Optional[sphinx.ext.napoleon.Config or sphinx.config.Config] + config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config` The configuration settings to use. If not given, defaults to the config object on `app`; or if `app` is not given defaults to the - a new `sphinx.ext.napoleon.Config` object. + a new :class:`sphinx.ext.napoleon.Config` object. - See Also - -------- - :class:`sphinx.ext.napoleon.Config` Other Parameters ---------------- - app : Optional[sphinx.application.Sphinx] + app : :class:`sphinx.application.Sphinx`, optional Application object representing the Sphinx process. - what : Optional[str] + what : :obj:`str`, optional A string specifying the type of the object to which the docstring belongs. Valid values: "module", "class", "exception", "function", "method", "attribute". - name : Optional[str] + name : :obj:`str`, optional The fully qualified name of the object. obj : module, class, exception, function, method, or attribute The object to which the docstring belongs. - options : Optional[sphinx.ext.autodoc.Options] + options : :class:`sphinx.ext.autodoc.Options`, optional The options given to the directive: an object with attributes inherited_members, undoc_members, show_inheritance and noindex that are True if the flag option of same name was given to the auto directive. + Example ------- >>> from sphinx.ext.napoleon import Config @@ -174,7 +173,7 @@ class GoogleDocstring(UnicodeMixin): Returns ------- - List[str] + list(str) The lines of the docstring in a list. """ @@ -256,12 +255,7 @@ class GoogleDocstring(UnicodeMixin): else: _desc = lines[1:] - match = _google_typed_arg_regex.match(before) - if match: - _name = match.group(1) - _type = match.group(2) - else: - _type = before + _type = before _desc = self.__class__(_desc, self._config).lines() return [(_name, _type, _desc,)] @@ -307,6 +301,19 @@ class GoogleDocstring(UnicodeMixin): else: return name + def _fix_field_desc(self, desc): + if self._is_list(desc): + desc = [''] + desc + elif desc[0].endswith('::'): + desc_block = desc[1:] + indent = self._get_indent(desc[0]) + block_indent = self._get_initial_indent(desc_block) + if block_indent > indent: + desc = [''] + desc + else: + desc = ['', desc[0]] + self._indent(desc_block, 4) + return desc + def _format_admonition(self, admonition, lines): lines = self._strip_empty(lines) if len(lines) == 1: @@ -333,6 +340,22 @@ class GoogleDocstring(UnicodeMixin): else: return [prefix] + def _format_docutils_params(self, fields, field_role='param', + type_role='type'): + lines = [] + for _name, _type, _desc in fields: + _desc = self._strip_empty(_desc) + if any(_desc): + _desc = self._fix_field_desc(_desc) + field = ':%s %s: ' % (field_role, _name) + lines.extend(self._format_block(field, _desc)) + else: + lines.append(':%s %s:' % (field_role, _name)) + + if _type: + lines.append(':%s %s: %s' % (type_role, _name, _type)) + return lines + [''] + def _format_field(self, _name, _type, _desc): _desc = self._strip_empty(_desc) has_desc = any(_desc) @@ -354,9 +377,11 @@ class GoogleDocstring(UnicodeMixin): field = '' if has_desc: - if self._is_list(_desc): - return [field, ''] + _desc - return [field + _desc[0]] + _desc[1:] + _desc = self._fix_field_desc(_desc) + if _desc[0]: + return [field + _desc[0]] + _desc[1:] + else: + return [field] + _desc else: return [field] @@ -393,6 +418,12 @@ class GoogleDocstring(UnicodeMixin): return i return len(line) + def _get_initial_indent(self, lines): + for line in lines: + if line: + return self._get_indent(line) + return 0 + def _get_min_indent(self, lines): min_indent = None for line in lines: @@ -527,7 +558,14 @@ class GoogleDocstring(UnicodeMixin): return [header, ''] def _parse_keyword_arguments_section(self, section): - return self._format_fields('Keyword Arguments', self._consume_fields()) + fields = self._consume_fields() + if self._config.napoleon_use_keyword: + return self._format_docutils_params( + fields, + field_role="keyword", + type_role="kwtype") + else: + return self._format_fields('Keyword Arguments', fields) def _parse_methods_section(self, section): lines = [] @@ -552,21 +590,7 @@ class GoogleDocstring(UnicodeMixin): def _parse_parameters_section(self, section): fields = self._consume_fields() if self._config.napoleon_use_param: - lines = [] - for _name, _type, _desc in fields: - _desc = self._strip_empty(_desc) - if any(_desc): - if self._is_list(_desc): - _desc = [''] + _desc - field = ':param %s: ' % _name - lines.extend(self._format_block(field, _desc)) - else: - lines.append(':param %s:' % _name) - - if _type: - lines.append(':type %s: %s' % (_name, _type)) - - return lines + [''] + return self._format_docutils_params(fields) else: return self._format_fields('Parameters', fields) @@ -668,11 +692,12 @@ class GoogleDocstring(UnicodeMixin): if found_colon: after_colon.append(source) else: - if (i % 2) == 0 and ":" in source: + m = _single_colon_regex.search(source) + if (i % 2) == 0 and m: found_colon = True - before, colon, after = source.partition(":") - before_colon.append(before) - after_colon.append(after) + colon = source[m.start(): m.end()] + before_colon.append(source[:m.start()]) + after_colon.append(source[m.end():]) else: before_colon.append(source) @@ -705,36 +730,34 @@ class NumpyDocstring(GoogleDocstring): Parameters ---------- - docstring : str or List[str] + docstring : :obj:`str` or :obj:`list` of :obj:`str` The docstring to parse, given either as a string or split into individual lines. - config : Optional[sphinx.ext.napoleon.Config or sphinx.config.Config] + config: :obj:`sphinx.ext.napoleon.Config` or :obj:`sphinx.config.Config` The configuration settings to use. If not given, defaults to the config object on `app`; or if `app` is not given defaults to the - a new `sphinx.ext.napoleon.Config` object. + a new :class:`sphinx.ext.napoleon.Config` object. - See Also - -------- - :class:`sphinx.ext.napoleon.Config` Other Parameters ---------------- - app : Optional[sphinx.application.Sphinx] + app : :class:`sphinx.application.Sphinx`, optional Application object representing the Sphinx process. - what : Optional[str] + what : :obj:`str`, optional A string specifying the type of the object to which the docstring belongs. Valid values: "module", "class", "exception", "function", "method", "attribute". - name : Optional[str] + name : :obj:`str`, optional The fully qualified name of the object. obj : module, class, exception, function, method, or attribute The object to which the docstring belongs. - options : Optional[sphinx.ext.autodoc.Options] + options : :class:`sphinx.ext.autodoc.Options`, optional The options given to the directive: an object with attributes inherited_members, undoc_members, show_inheritance and noindex that are True if the flag option of same name was given to the auto directive. + Example ------- >>> from sphinx.ext.napoleon import Config @@ -791,7 +814,7 @@ class NumpyDocstring(GoogleDocstring): Returns ------- - List[str] + list(str) The lines of the docstring in a list. """ diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index a853ec93c..f3b526ce6 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -70,6 +70,8 @@ def process_todos(app, doctree): if not hasattr(env, 'todo_all_todos'): env.todo_all_todos = [] for node in doctree.traverse(todo_node): + app.emit('todo-defined', node) + try: targetnode = node.parent[node.parent.index(node) - 1] if not isinstance(targetnode, nodes.target): @@ -86,6 +88,9 @@ def process_todos(app, doctree): 'target': targetnode, }) + if env.config.todo_emit_warnings: + env.warn_node("TODO entry found: %s" % node[1].astext(), node) + class TodoList(Directive): """ @@ -187,8 +192,10 @@ def depart_todo_node(self, node): def setup(app): + app.add_event('todo-defined') app.add_config_value('todo_include_todos', False, 'html') app.add_config_value('todo_link_only', False, 'html') + app.add_config_value('todo_emit_warnings', False, 'html') app.add_node(todolist) app.add_node(todo_node, diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index a071c5533..276a137d5 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -46,6 +46,10 @@ def doctree_read(app, doctree): env = app.builder.env if not hasattr(env, '_viewcode_modules'): env._viewcode_modules = {} + if app.builder.name == "singlehtml": + return + if app.builder.name.startswith("epub") and not env.config.viewcode_enable_epub: + return def has_tag(modname, fullname, docname, refname): entry = env._viewcode_modules.get(modname, None) @@ -215,6 +219,7 @@ def collect_pages(app): def setup(app): app.add_config_value('viewcode_import', True, False) + app.add_config_value('viewcode_enable_epub', False, False) app.connect('doctree-read', doctree_read) app.connect('env-merge-info', env_merge_info) app.connect('html-collect-pages', collect_pages) |