diff options
Diffstat (limited to 'sphinx/ext')
-rw-r--r-- | sphinx/ext/autodoc.py | 204 | ||||
-rw-r--r-- | sphinx/ext/autosummary/__init__.py | 15 | ||||
-rw-r--r-- | sphinx/ext/autosummary/generate.py | 18 | ||||
-rw-r--r-- | sphinx/ext/coverage.py | 23 | ||||
-rw-r--r-- | sphinx/ext/doctest.py | 51 | ||||
-rw-r--r-- | sphinx/ext/extlinks.py | 5 | ||||
-rw-r--r-- | sphinx/ext/graphviz.py | 85 | ||||
-rw-r--r-- | sphinx/ext/ifconfig.py | 4 | ||||
-rw-r--r-- | sphinx/ext/inheritance_diagram.py | 20 | ||||
-rw-r--r-- | sphinx/ext/intersphinx.py | 94 | ||||
-rw-r--r-- | sphinx/ext/jsmath.py | 2 | ||||
-rw-r--r-- | sphinx/ext/linkcode.py | 5 | ||||
-rw-r--r-- | sphinx/ext/mathjax.py | 6 | ||||
-rw-r--r-- | sphinx/ext/napoleon/__init__.py | 391 | ||||
-rw-r--r-- | sphinx/ext/napoleon/docstring.py | 860 | ||||
-rw-r--r-- | sphinx/ext/napoleon/iterators.py | 239 | ||||
-rw-r--r-- | sphinx/ext/oldcmarkup.py | 67 | ||||
-rw-r--r-- | sphinx/ext/pngmath.py | 27 | ||||
-rw-r--r-- | sphinx/ext/todo.py | 12 | ||||
-rw-r--r-- | sphinx/ext/viewcode.py | 79 |
20 files changed, 1887 insertions, 320 deletions
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index b4ecf32f..5b0bda17 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -17,10 +17,12 @@ import inspect import traceback from types import FunctionType, BuiltinFunctionType, MethodType +from six import iteritems, itervalues, text_type, class_types from docutils import nodes from docutils.utils import assemble_option_dict from docutils.statemachine import ViewList +import sphinx from sphinx.util import rpartition, force_decode from sphinx.locale import _ from sphinx.pycode import ModuleAnalyzer, PycodeError @@ -28,8 +30,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, safe_repr, is_builtin_class_method -from sphinx.util.pycompat import base_exception, class_types + safe_getattr, safe_repr, is_builtin_class_method from sphinx.util.docstrings import prepare_docstring @@ -49,14 +50,17 @@ class DefDict(dict): def __init__(self, default): dict.__init__(self) self.default = default + def __getitem__(self, key): try: return dict.__getitem__(self, key) except KeyError: return self.default - def __nonzero__(self): + + def __bool__(self): # docutils check "if option_spec" return True + __nonzero__ = __bool__ # for python2 compatibility identity = lambda x: x @@ -70,15 +74,47 @@ class Options(dict): return None +class _MockModule(object): + """Used by autodoc_mock_imports.""" + def __init__(self, *args, **kwargs): + pass + + def __call__(self, *args, **kwargs): + return _MockModule() + + @classmethod + def __getattr__(cls, name): + if name in ('__file__', '__path__'): + return '/dev/null' + elif name[0] == name[0].upper(): + # Not very good, we assume Uppercase names are classes... + mocktype = type(name, (), {}) + mocktype.__module__ = __name__ + return mocktype + else: + return _MockModule() + + +def mock_import(modname): + if '.' in modname: + pkg, _n, mods = modname.rpartition('.') + mock_import(pkg) + mod = _MockModule() + sys.modules[modname] = mod + return mod + + ALL = object() INSTANCEATTR = object() + def members_option(arg): """Used to convert the :members: option to auto directives.""" if arg is None: return ALL return [x.strip() for x in arg.split(',')] + def members_set_option(arg): """Used to convert the :members: option to auto directives.""" if arg is None: @@ -87,6 +123,7 @@ def members_set_option(arg): SUPPRESS = object() + def annotation_option(arg): if arg is None: # suppress showing the representation of the object @@ -94,6 +131,7 @@ def annotation_option(arg): else: return arg + def bool_option(arg): """Used to convert flag options to auto directives. (Instead of directives.flag(), which returns None). @@ -170,6 +208,7 @@ def cut_lines(pre, post=0, what=None): lines.append('') return process + def between(marker, what=None, keepempty=False, exclude=False): """Return a listener that either keeps, or if *exclude* is True excludes, lines between lines that match the *marker* regular expression. If no line @@ -180,6 +219,7 @@ def between(marker, what=None, keepempty=False, exclude=False): be processed. """ marker_re = re.compile(marker) + def process(app, what_, name, obj, options, lines): if what and what_ not in what: return @@ -235,7 +275,7 @@ class Documenter(object): @staticmethod def get_attr(obj, name, *defargs): """getattr() override for types such as Zope interfaces.""" - for typ, func in AutoDirective._special_attrgetters.iteritems(): + for typ, func in iteritems(AutoDirective._special_attrgetters): if isinstance(obj, typ): return func(obj, name, *defargs) return safe_getattr(obj, name, *defargs) @@ -294,7 +334,7 @@ class Documenter(object): # an autogenerated one try: explicit_modname, path, base, args, retann = \ - py_ext_sig_re.match(self.name).groups() + py_ext_sig_re.match(self.name).groups() except AttributeError: self.directive.warn('invalid signature for auto%s (%r)' % (self.objtype, self.name)) @@ -309,7 +349,7 @@ class Documenter(object): parents = [] self.modname, self.objpath = \ - self.resolve_name(modname, parents, path, base) + self.resolve_name(modname, parents, path, base) if not self.modname: return False @@ -332,6 +372,9 @@ class Documenter(object): self.modname, '.'.join(self.objpath)) 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) + mock_import(modname) __import__(self.modname) parent = None obj = self.module = sys.modules[self.modname] @@ -347,7 +390,7 @@ class Documenter(object): return True # this used to only catch SyntaxError, ImportError and AttributeError, # but importing modules with side effects can raise all kinds of errors - except (Exception, SystemExit), e: + except (Exception, SystemExit) as e: if self.objpath: errmsg = 'autodoc: failed to import %s %r from module %r' % \ (self.objtype, '.'.join(self.objpath), self.modname) @@ -415,7 +458,7 @@ class Documenter(object): # try to introspect the signature try: args = self.format_args() - except Exception, err: + except Exception as err: self.directive.warn('error while formatting arguments for ' '%s: %s' % (self.fullname, err)) args = None @@ -452,7 +495,7 @@ class Documenter(object): docstring = self.get_attr(self.object, '__doc__', None) # make sure we have Unicode docstrings, then sanitize and split # into lines - if isinstance(docstring, unicode): + if isinstance(docstring, text_type): return [prepare_docstring(docstring, ignore)] elif isinstance(docstring, str): # this will not trigger on Py3 return [prepare_docstring(force_decode(docstring, encoding), @@ -476,9 +519,9 @@ class Documenter(object): # set sourcename and add content from attribute documentation if self.analyzer: # prevent encoding errors when the file name is non-ASCII - if not isinstance(self.analyzer.srcname, unicode): - filename = unicode(self.analyzer.srcname, - sys.getfilesystemencoding(), 'replace') + if not isinstance(self.analyzer.srcname, text_type): + filename = text_type(self.analyzer.srcname, + sys.getfilesystemencoding(), 'replace') else: filename = self.analyzer.srcname sourcename = u'%s:docstring of %s' % (filename, self.fullname) @@ -522,7 +565,7 @@ class Documenter(object): if self.analyzer: attr_docs = self.analyzer.find_attr_docs() namespace = '.'.join(self.objpath) - for item in attr_docs.iteritems(): + for item in iteritems(attr_docs): if item[0][0] == namespace: analyzed_member_names.add(item[0][1]) if not want_all: @@ -603,19 +646,19 @@ class Documenter(object): keep = False if want_all and membername.startswith('__') and \ - membername.endswith('__') and len(membername) > 4: + membername.endswith('__') and len(membername) > 4: # special __methods__ if self.options.special_members is ALL and \ membername != '__doc__': keep = has_doc or self.options.undoc_members elif self.options.special_members and \ - self.options.special_members is not ALL and \ + self.options.special_members is not ALL and \ membername in self.options.special_members: keep = has_doc or self.options.undoc_members elif want_all and membername.startswith('_'): # ignore members whose name starts with _ by default keep = self.options.private_members and \ - (has_doc or self.options.undoc_members) + (has_doc or self.options.undoc_members) elif (namespace, membername) in attr_docs: # keep documented attributes keep = True @@ -651,7 +694,7 @@ class Documenter(object): self.env.temp_data['autodoc:class'] = self.objpath[0] want_all = all_members or self.options.inherited_members or \ - self.options.members is ALL + self.options.members is ALL # find out which members are documentable members_check_module, members = self.get_object_members(want_all) @@ -663,7 +706,7 @@ class Documenter(object): # document non-skipped members memberdocumenters = [] for (mname, member, isattr) in self.filter_members(members, want_all): - classes = [cls for cls in AutoDirective._registry.itervalues() + classes = [cls for cls in itervalues(AutoDirective._registry) if cls.can_document_member(member, mname, isattr, self)] if not classes: # don't know how to document this member @@ -673,11 +716,11 @@ class Documenter(object): # give explicitly separated module name, so that members # of inner classes can be documented full_mname = self.modname + '::' + \ - '.'.join(self.objpath + [mname]) + '.'.join(self.objpath + [mname]) documenter = classes[-1](self.directive, full_mname, self.indent) memberdocumenters.append((documenter, isattr)) member_order = self.options.member_order or \ - self.env.config.autodoc_member_order + self.env.config.autodoc_member_order if member_order == 'groupwise': # sort by group; relies on stable sort to keep items in the # same group sorted alphabetically @@ -685,6 +728,7 @@ class Documenter(object): elif member_order == 'bysource' and self.analyzer: # sort by source order, by virtue of the module analyzer tagorder = self.analyzer.tagorder + def keyfunc(entry): fullname = entry[0].name.split('::')[1] return tagorder.get(fullname, len(tagorder)) @@ -735,7 +779,7 @@ class Documenter(object): # parse right now, to get PycodeErrors on parsing (results will # be cached anyway) self.analyzer.find_attr_docs() - except PycodeError, err: + except PycodeError as err: self.env.app.debug('[autodoc] module analyzer failed: %s', err) # no source file -- e.g. for builtin and C modules self.analyzer = None @@ -838,7 +882,7 @@ class ModuleDocumenter(Documenter): self.directive.warn( 'missing attribute mentioned in :members: or __all__: ' 'module %s, attribute %s' % ( - safe_getattr(self.object, '__name__', '???'), mname)) + safe_getattr(self.object, '__name__', '???'), mname)) return False, ret @@ -857,7 +901,7 @@ class ModuleLevelDocumenter(Documenter): modname = self.env.temp_data.get('autodoc:module') # ... or in the scope of a module directive if not modname: - modname = self.env.temp_data.get('py:module') + modname = self.env.ref_context.get('py:module') # ... else, it stays None, which means invalid return modname, parents + [base] @@ -879,7 +923,7 @@ class ClassLevelDocumenter(Documenter): mod_cls = self.env.temp_data.get('autodoc:class') # ... or from a class directive if mod_cls is None: - mod_cls = self.env.temp_data.get('py:class') + mod_cls = self.env.ref_context.get('py:class') # ... if still None, there's no way to know if mod_cls is None: return None, [] @@ -889,7 +933,7 @@ class ClassLevelDocumenter(Documenter): if not modname: modname = self.env.temp_data.get('autodoc:module') if not modname: - modname = self.env.temp_data.get('py:module') + modname = self.env.ref_context.get('py:module') # ... else, it stays None, which means invalid return modname, parents + [base] @@ -943,6 +987,24 @@ class DocstringSignatureMixin(object): return Documenter.format_signature(self) +class DocstringStripSignatureMixin(DocstringSignatureMixin): + """ + Mixin for AttributeDocumenter to provide the + feature of stripping any function signature from the docstring. + """ + def format_signature(self): + if self.args is None and self.env.config.autodoc_docstring_signature: + # only act if a signature is not explicitly given already, and if + # the feature is enabled + result = self._find_signature() + if result is not None: + # Discarding _args is a only difference with + # DocstringSignatureMixin.format_signature. + # Documenter.format_signature use self.args value to format. + _args, self.retann = result + return Documenter.format_signature(self) + + class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): """ Specialized Documenter subclass for functions. @@ -956,7 +1018,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): def format_args(self): if inspect.isbuiltin(self.object) or \ - inspect.ismethoddescriptor(self.object): + inspect.ismethoddescriptor(self.object): # cannot introspect arguments of a C function or method return None try: @@ -1019,8 +1081,8 @@ class ClassDocumenter(ModuleLevelDocumenter): # classes without __init__ method, default __init__ or # __init__ written in C? if initmeth is None or \ - is_builtin_class_method(self.object, '__init__') or \ - not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): + is_builtin_class_method(self.object, '__init__') or \ + not(inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: argspec = getargspec(initmeth) @@ -1058,7 +1120,7 @@ class ClassDocumenter(ModuleLevelDocumenter): if not self.doc_as_attr and self.options.show_inheritance: self.add_line(u'', '<autodoc>') if hasattr(self.object, '__bases__') and len(self.object.__bases__): - bases = [b.__module__ == '__builtin__' and + bases = [b.__module__ in ('__builtin__', 'builtins') and u':class:`%s`' % b.__name__ or u':class:`%s.%s`' % (b.__module__, b.__name__) for b in self.object.__bases__] @@ -1091,7 +1153,7 @@ class ClassDocumenter(ModuleLevelDocumenter): # for new-style classes, no __init__ means default __init__ if (initdocstring is not None and (initdocstring == object.__init__.__doc__ or # for pypy - initdocstring.strip() == object.__init__.__doc__)): #for !pypy + initdocstring.strip() == object.__init__.__doc__)): # for !pypy initdocstring = None if initdocstring: if content == 'init': @@ -1100,7 +1162,7 @@ class ClassDocumenter(ModuleLevelDocumenter): docstrings.append(initdocstring) doc = [] for docstring in docstrings: - if not isinstance(docstring, unicode): + if not isinstance(docstring, text_type): docstring = force_decode(docstring, encoding) doc.append(prepare_docstring(docstring)) return doc @@ -1135,7 +1197,7 @@ class ExceptionDocumenter(ClassDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): return isinstance(member, class_types) and \ - issubclass(member, base_exception) + issubclass(member, BaseException) class DataDocumenter(ModuleLevelDocumenter): @@ -1182,48 +1244,31 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): return inspect.isroutine(member) and \ - not isinstance(parent, ModuleDocumenter) - - if sys.version_info >= (3, 0): - def import_object(self): - ret = ClassLevelDocumenter.import_object(self) - if not ret: - return ret - obj_from_parent = self.parent.__dict__.get(self.object_name) - if isinstance(obj_from_parent, classmethod): - self.directivetype = 'classmethod' - self.member_order = self.member_order - 1 - elif isinstance(obj_from_parent, staticmethod): - self.directivetype = 'staticmethod' - self.member_order = self.member_order - 1 - else: - self.directivetype = 'method' - return ret - else: - def import_object(self): - ret = ClassLevelDocumenter.import_object(self) - if not ret: - return ret - if isinstance(self.object, classmethod) or \ - (isinstance(self.object, MethodType) and - self.object.im_self is not None): - self.directivetype = 'classmethod' - # document class and static members before ordinary ones - self.member_order = self.member_order - 1 - elif isinstance(self.object, FunctionType) or \ - (isinstance(self.object, BuiltinFunctionType) and - hasattr(self.object, '__self__') and - self.object.__self__ is not None): - self.directivetype = 'staticmethod' - # document class and static members before ordinary ones - self.member_order = self.member_order - 1 - else: - self.directivetype = 'method' + not isinstance(parent, ModuleDocumenter) + + def import_object(self): + ret = ClassLevelDocumenter.import_object(self) + if not ret: return ret + # to distinguish classmethod/staticmethod + obj = self.parent.__dict__.get(self.object_name) + + if isinstance(obj, classmethod): + self.directivetype = 'classmethod' + # document class and static members before ordinary ones + self.member_order = self.member_order - 1 + elif isinstance(obj, staticmethod): + self.directivetype = 'staticmethod' + # document class and static members before ordinary ones + self.member_order = self.member_order - 1 + else: + self.directivetype = 'method' + return ret + def format_args(self): if inspect.isbuiltin(self.object) or \ - inspect.ismethoddescriptor(self.object): + inspect.ismethoddescriptor(self.object): # can never get arguments of a C function or method return None argspec = getargspec(self.object) @@ -1238,7 +1283,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): pass -class AttributeDocumenter(ClassLevelDocumenter): +class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): """ Specialized Documenter subclass for attributes. """ @@ -1256,9 +1301,9 @@ class AttributeDocumenter(ClassLevelDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): isdatadesc = isdescriptor(member) and not \ - isinstance(member, cls.method_types) and not \ - type(member).__name__ in ("type", "method_descriptor", - "instancemethod") + isinstance(member, cls.method_types) and not \ + type(member).__name__ in ("type", "method_descriptor", + "instancemethod") return isdatadesc or (not isinstance(parent, ModuleDocumenter) and not inspect.isroutine(member) and not isinstance(member, class_types)) @@ -1269,7 +1314,7 @@ class AttributeDocumenter(ClassLevelDocumenter): def import_object(self): ret = ClassLevelDocumenter.import_object(self) if isdescriptor(self.object) and \ - not isinstance(self.object, self.method_types): + not isinstance(self.object, self.method_types): self._datadescriptor = True else: # if it's not a data descriptor @@ -1278,7 +1323,7 @@ class AttributeDocumenter(ClassLevelDocumenter): def get_real_modname(self): return self.get_attr(self.parent or self.object, '__module__', None) \ - or self.modname + or self.modname def add_directive_header(self, sig): ClassLevelDocumenter.add_directive_header(self, sig) @@ -1401,7 +1446,7 @@ class AutoDirective(Directive): try: self.genopt = Options(assemble_option_dict( self.options.items(), doc_class.option_spec)) - except (KeyError, ValueError, TypeError), err: + except (KeyError, ValueError, TypeError) as err: # an option is either unknown or has a wrong type msg = self.reporter.error('An option to %s is either unknown or ' 'has an invalid value: %s' % (self.name, err), @@ -1445,7 +1490,7 @@ def add_documenter(cls): raise ExtensionError('autodoc documenter %r must be a subclass ' 'of Documenter' % cls) # actually, it should be possible to override Documenters - #if cls.objtype in AutoDirective._registry: + # if cls.objtype in AutoDirective._registry: # raise ExtensionError('autodoc documenter for %r is already ' # 'registered' % cls.objtype) AutoDirective._registry[cls.objtype] = cls @@ -1465,10 +1510,13 @@ def setup(app): app.add_config_value('autodoc_member_order', 'alphabetic', True) app.add_config_value('autodoc_default_flags', [], True) app.add_config_value('autodoc_docstring_signature', True, True) + app.add_config_value('autodoc_mock_imports', [], True) app.add_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') + return {'version': sphinx.__version__, 'parallel_read_safe': True} + class testcls: """test doc string""" diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 8798e7f6..908c96ca 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -60,10 +60,12 @@ import inspect import posixpath from types import ModuleType +from six import text_type from docutils.parsers.rst import directives from docutils.statemachine import ViewList from docutils import nodes +import sphinx from sphinx import addnodes from sphinx.util.compat import Directive from sphinx.pycode import ModuleAnalyzer, PycodeError @@ -118,7 +120,7 @@ def autosummary_table_visit_html(self, node): par = col1_entry[0] for j, subnode in enumerate(list(par)): if isinstance(subnode, nodes.Text): - new_text = unicode(subnode.astext()) + new_text = text_type(subnode.astext()) new_text = new_text.replace(u" ", u"\u00a0") par[j] = nodes.Text(new_text) except IndexError: @@ -271,11 +273,11 @@ class Autosummary(Directive): # try to also get a source code analyzer for attribute docs try: documenter.analyzer = ModuleAnalyzer.for_module( - documenter.get_real_modname()) + documenter.get_real_modname()) # parse right now, to get PycodeErrors on parsing (results will # be cached anyway) documenter.analyzer.find_attr_docs() - except PycodeError, err: + except PycodeError as err: documenter.env.app.debug( '[autodoc] module analyzer failed: %s', err) # no source file -- e.g. for builtin and C modules @@ -431,11 +433,11 @@ def get_import_prefixes_from_env(env): """ prefixes = [None] - currmodule = env.temp_data.get('py:module') + currmodule = env.ref_context.get('py:module') if currmodule: prefixes.insert(0, currmodule) - currclass = env.temp_data.get('py:class') + currclass = env.ref_context.get('py:class') if currclass: if currmodule: prefixes.insert(0, currmodule + "." + currclass) @@ -498,7 +500,7 @@ def _import_by_name(name): return obj, parent, modname else: return sys.modules[modname], None, modname - except (ValueError, ImportError, AttributeError, KeyError), e: + except (ValueError, ImportError, AttributeError, KeyError) as e: raise ImportError(*e.args) @@ -569,3 +571,4 @@ def setup(app): app.connect('doctree-read', process_autosummary_toc) app.connect('builder-inited', process_generate_options) app.add_config_value('autosummary_generate', [], True) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index 47ef9868..11b1dfe7 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -17,6 +17,7 @@ :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ +from __future__ import print_function import os import re @@ -70,10 +71,10 @@ def main(argv=sys.argv): template_dir=options.templates) def _simple_info(msg): - print msg + print(msg) def _simple_warn(msg): - print >> sys.stderr, 'WARNING: ' + msg + print('WARNING: ' + msg, file=sys.stderr) # -- Generating output --------------------------------------------------------- @@ -109,14 +110,11 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', # read items = find_autosummary_in_files(sources) - # remove possible duplicates - items = dict([(item, True) for item in items]).keys() - # keep track of new files new_files = [] # write - for name, path, template_name in sorted(items, key=str): + for name, path, template_name in sorted(set(items), key=str): if path is None: # The corresponding autosummary:: directive did not have # a :toctree: option @@ -127,7 +125,7 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', try: name, obj, parent, mod_name = import_by_name(name) - except ImportError, e: + except ImportError as e: warn('[autosummary] failed to import %r: %s' % (name, e)) continue @@ -240,9 +238,9 @@ def find_autosummary_in_docstring(name, module=None, filename=None): return find_autosummary_in_lines(lines, module=name, filename=filename) except AttributeError: pass - except ImportError, e: - print "Failed to import '%s': %s" % (name, e) - except SystemExit, e: + except ImportError as e: + print("Failed to import '%s': %s" % (name, e)) + except SystemExit as e: print("Failed to import '%s'; the module executes module level " "statement and it might call sys.exit()." % name) return [] diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 52be1bdb..399935cc 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -13,9 +13,12 @@ import re import glob import inspect -import cPickle as pickle from os import path +from six import iteritems +from six.moves import cPickle as pickle + +import sphinx from sphinx.builders import Builder @@ -52,7 +55,7 @@ class CoverageBuilder(Builder): self.warn('invalid regex %r in coverage_c_regexes' % exp) self.c_ignorexps = {} - for (name, exps) in self.config.coverage_ignore_c_items.iteritems(): + for (name, exps) in iteritems(self.config.coverage_ignore_c_items): self.c_ignorexps[name] = compile_regex_list( 'coverage_ignore_c_items', exps, self.warn) self.mod_ignorexps = compile_regex_list( @@ -81,7 +84,7 @@ class CoverageBuilder(Builder): # Fetch all the info from the header files c_objects = self.env.domaindata['c']['objects'] for filename in self.c_sourcefiles: - undoc = [] + undoc = set() f = open(filename, 'r') try: for line in f: @@ -94,7 +97,7 @@ class CoverageBuilder(Builder): if exp.match(name): break else: - undoc.append((key, name)) + undoc.add((key, name)) continue finally: f.close() @@ -109,9 +112,9 @@ class CoverageBuilder(Builder): write_header(op, 'Undocumented C API elements', '=') op.write('\n') - for filename, undoc in self.c_undoc.iteritems(): + for filename, undoc in iteritems(self.c_undoc): write_header(op, filename) - for typ, name in undoc: + for typ, name in sorted(undoc): op.write(' * %-50s [%9s]\n' % (name, typ)) op.write('\n') finally: @@ -134,7 +137,7 @@ class CoverageBuilder(Builder): try: mod = __import__(mod_name, fromlist=['foo']) - except ImportError, err: + except ImportError as err: self.warn('module %s could not be imported: %s' % (mod_name, err)) self.py_undoc[mod_name] = {'error': err} @@ -211,8 +214,7 @@ class CoverageBuilder(Builder): try: if self.config.coverage_write_headline: write_header(op, 'Undocumented Python objects', '=') - keys = self.py_undoc.keys() - keys.sort() + keys = sorted(self.py_undoc.keys()) for name in keys: undoc = self.py_undoc[name] if 'error' in undoc: @@ -229,7 +231,7 @@ class CoverageBuilder(Builder): if undoc['classes']: op.write('Classes:\n') for name, methods in sorted( - undoc['classes'].iteritems()): + iteritems(undoc['classes'])): if not methods: op.write(' * %s\n' % name) else: @@ -263,3 +265,4 @@ def setup(app): app.add_config_value('coverage_ignore_c_items', {}, False) app.add_config_value('coverage_write_headline', True, False) app.add_config_value('coverage_skip_undoc_in_source', False, False) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index 70beb9bf..216325cb 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -14,24 +14,25 @@ import re import sys import time import codecs -import StringIO from os import path # circumvent relative import doctest = __import__('doctest') +from six import itervalues, StringIO, binary_type from docutils import nodes from docutils.parsers.rst import directives +import sphinx from sphinx.builders import Builder from sphinx.util import force_decode from sphinx.util.nodes import set_source_info from sphinx.util.compat import Directive from sphinx.util.console import bold -from sphinx.util.pycompat import bytes blankline_re = re.compile(r'^\s*<BLANKLINE>', re.MULTILINE) doctestopt_re = re.compile(r'#\s*doctest:.+$', re.MULTILINE) + # set up the necessary directives class TestDirective(Directive): @@ -79,30 +80,35 @@ class TestDirective(Directive): option_strings = self.options['options'].replace(',', ' ').split() for option in option_strings: if (option[0] not in '+-' or option[1:] not in - doctest.OPTIONFLAGS_BY_NAME): + doctest.OPTIONFLAGS_BY_NAME): # XXX warn? continue flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]] node['options'][flag] = (option[0] == '+') return [node] + class TestsetupDirective(TestDirective): option_spec = {} + class TestcleanupDirective(TestDirective): option_spec = {} + class DoctestDirective(TestDirective): option_spec = { 'hide': directives.flag, 'options': directives.unchanged, } + class TestcodeDirective(TestDirective): option_spec = { 'hide': directives.flag, } + class TestoutputDirective(TestDirective): option_spec = { 'hide': directives.flag, @@ -112,6 +118,7 @@ class TestoutputDirective(TestDirective): parser = doctest.DocTestParser() + # helper classes class TestGroup(object): @@ -158,7 +165,7 @@ class TestCode(object): class SphinxDocTestRunner(doctest.DocTestRunner): def summarize(self, out, verbose=None): - string_io = StringIO.StringIO() + string_io = StringIO() old_stdout = sys.stdout sys.stdout = string_io try: @@ -196,7 +203,7 @@ class DocTestBuilder(Builder): def init(self): # default options self.opt = doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | \ - doctest.IGNORE_EXCEPTION_DETAIL + doctest.IGNORE_EXCEPTION_DETAIL # HACK HACK HACK # doctest compiles its snippets with type 'single'. That is nice @@ -233,7 +240,7 @@ Results of doctest builder run on %s self.info(text, nonl=True) if self.app.quiet: self.warn(text) - if isinstance(text, bytes): + if isinstance(text, binary_type): text = force_decode(text, None) self.outfile.write(text) @@ -247,6 +254,10 @@ Results of doctest builder run on %s # write executive summary def s(v): return v != 1 and 's' or '' + repl = (self.total_tries, s(self.total_tries), + self.total_failures, s(self.total_failures), + self.setup_failures, s(self.setup_failures), + self.cleanup_failures, s(self.cleanup_failures)) self._out(''' Doctest summary =============== @@ -254,10 +265,7 @@ Doctest summary %5d failure%s in tests %5d failure%s in setup code %5d failure%s in cleanup code -''' % (self.total_tries, s(self.total_tries), - self.total_failures, s(self.total_failures), - self.setup_failures, s(self.setup_failures), - self.cleanup_failures, s(self.cleanup_failures))) +''' % repl) self.outfile.close() if self.total_failures or self.setup_failures or self.cleanup_failures: @@ -289,14 +297,14 @@ Doctest summary if self.config.doctest_test_doctest_blocks: def condition(node): return (isinstance(node, (nodes.literal_block, nodes.comment)) - and node.has_key('testnodetype')) or \ - isinstance(node, nodes.doctest_block) + and 'testnodetype' in node) or \ + isinstance(node, nodes.doctest_block) else: def condition(node): return isinstance(node, (nodes.literal_block, nodes.comment)) \ - and node.has_key('testnodetype') + and 'testnodetype' in node for node in doctree.traverse(condition): - source = node.has_key('test') and node['test'] or node.astext() + source = 'test' in node and node['test'] or node.astext() if not source: self.warn('no code/output in %s block at %s:%s' % (node.get('testnodetype', 'doctest'), @@ -312,24 +320,24 @@ Doctest summary groups[groupname] = TestGroup(groupname) groups[groupname].add_code(code) for code in add_to_all_groups: - for group in groups.itervalues(): + for group in itervalues(groups): group.add_code(code) if self.config.doctest_global_setup: code = TestCode(self.config.doctest_global_setup, 'testsetup', lineno=0) - for group in groups.itervalues(): + for group in itervalues(groups): group.add_code(code, prepend=True) if self.config.doctest_global_cleanup: code = TestCode(self.config.doctest_global_cleanup, 'testcleanup', lineno=0) - for group in groups.itervalues(): + for group in itervalues(groups): group.add_code(code) if not groups: return self._out('\nDocument: %s\n----------%s\n' % (docname, '-'*len(docname))) - for group in groups.itervalues(): + for group in itervalues(groups): self.test_group(group, self.env.doc2path(docname, base=None)) # Separately count results from setup code res_f, res_t = self.setup_runner.summarize(self._out, verbose=False) @@ -364,7 +372,7 @@ Doctest summary filename, 0, None) sim_doctest.globs = ns old_f = runner.failures - self.type = 'exec' # the snippet may contain multiple statements + self.type = 'exec' # the snippet may contain multiple statements runner.run(sim_doctest, out=self._warn_out, clear_globs=False) if runner.failures > old_f: return False @@ -394,7 +402,7 @@ Doctest summary new_opt = code[0].options.copy() new_opt.update(example.options) example.options = new_opt - self.type = 'single' # as for ordinary doctests + self.type = 'single' # as for ordinary doctests else: # testcode and output separate output = code[1] and code[1].code or '' @@ -413,7 +421,7 @@ Doctest summary options=options) test = doctest.DocTest([example], {}, group.name, filename, code[0].lineno, None) - self.type = 'exec' # multiple statements again + self.type = 'exec' # multiple statements again # DocTest.__init__ copies the globs namespace, which we don't want test.globs = ns # also don't clear the globs namespace after running the doctest @@ -435,3 +443,4 @@ 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) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/extlinks.py b/sphinx/ext/extlinks.py index 18da573e..ae65dbb8 100644 --- a/sphinx/ext/extlinks.py +++ b/sphinx/ext/extlinks.py @@ -24,8 +24,10 @@ :license: BSD, see LICENSE for details. """ +from six import iteritems from docutils import nodes, utils +import sphinx from sphinx.util.nodes import split_explicit_title @@ -51,9 +53,10 @@ def make_link_role(base_url, prefix): return role def setup_link_roles(app): - for name, (base_url, prefix) in app.config.extlinks.iteritems(): + for name, (base_url, prefix) in iteritems(app.config.extlinks): app.add_role(name, make_link_role(base_url, prefix)) def setup(app): app.add_config_value('extlinks', {}, 'env') app.connect('builder-inited', setup_link_roles) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index 028560b1..71e7ba65 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -15,14 +15,14 @@ import codecs import posixpath from os import path from subprocess import Popen, PIPE -try: - from hashlib import sha1 as sha -except ImportError: - from sha import sha +from hashlib import sha1 +from six import text_type from docutils import nodes from docutils.parsers.rst import directives +from docutils.statemachine import ViewList +import sphinx from sphinx.errors import SphinxError from sphinx.locale import _ from sphinx.util.osutil import ensuredir, ENOENT, EPIPE, EINVAL @@ -40,6 +40,20 @@ class graphviz(nodes.General, nodes.Element): pass +def figure_wrapper(directive, node, caption): + figure_node = nodes.figure('', node) + + parsed = nodes.Element() + directive.state.nested_parse(ViewList([caption], source=''), + directive.content_offset, parsed) + caption_node = nodes.caption(parsed[0].rawsource, '', + *parsed[0].children) + caption_node.source = parsed[0].source + caption_node.line = parsed[0].line + figure_node += caption_node + return figure_node + + class Graphviz(Directive): """ Directive to insert arbitrary dot markup. @@ -85,9 +99,12 @@ class Graphviz(Directive): node['options'] = [] if 'alt' in self.options: node['alt'] = self.options['alt'] - if 'caption' in self.options: - node['caption'] = self.options['caption'] node['inline'] = 'inline' in self.options + + caption = self.options.get('caption') + if caption and not node['inline']: + node = figure_wrapper(self, node, caption) + return [node] @@ -112,9 +129,12 @@ class GraphvizSimple(Directive): node['options'] = [] if 'alt' in self.options: node['alt'] = self.options['alt'] - if 'caption' in self.options: - node['caption'] = self.options['caption'] node['inline'] = 'inline' in self.options + + caption = self.options.get('caption') + if caption and not node['inline']: + node = figure_wrapper(self, node, caption) + return [node] @@ -125,15 +145,9 @@ def render_dot(self, code, options, format, prefix='graphviz'): str(self.builder.config.graphviz_dot_args) ).encode('utf-8') - fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format) - if hasattr(self.builder, 'imgpath'): - # HTML - relfn = posixpath.join(self.builder.imgpath, fname) - outfn = path.join(self.builder.outdir, '_images', fname) - else: - # LaTeX - relfn = fname - outfn = path.join(self.builder.outdir, fname) + fname = '%s-%s.%s' % (prefix, sha1(hashkey).hexdigest(), format) + relfn = posixpath.join(self.builder.imgpath, fname) + outfn = path.join(self.builder.outdir, self.builder.imagedir, fname) if path.isfile(outfn): return relfn, outfn @@ -145,7 +159,7 @@ def render_dot(self, code, options, format, prefix='graphviz'): ensuredir(path.dirname(outfn)) # graphviz expects UTF-8 by default - if isinstance(code, unicode): + if isinstance(code, text_type): code = code.encode('utf-8') dot_args = [self.builder.config.graphviz_dot] @@ -156,7 +170,7 @@ def render_dot(self, code, options, format, prefix='graphviz'): dot_args.extend(['-Tcmapx', '-o%s.map' % outfn]) try: p = Popen(dot_args, stdout=PIPE, stdin=PIPE, stderr=PIPE) - except OSError, err: + except OSError as err: if err.errno != ENOENT: # No such file or directory raise self.builder.warn('dot command %r cannot be run (needed for graphviz ' @@ -168,7 +182,7 @@ def render_dot(self, code, options, format, prefix='graphviz'): # Graphviz may close standard input when an error occurs, # resulting in a broken pipe on communicate() stdout, stderr = p.communicate(code) - except (OSError, IOError), err: + except (OSError, IOError) as err: if err.errno not in (EPIPE, EINVAL): raise # in this case, read the standard output and standard error streams @@ -192,7 +206,7 @@ def render_dot_html(self, node, code, options, prefix='graphviz', raise GraphvizError("graphviz_output_format must be one of 'png', " "'svg', but is %r" % format) fname, outfn = render_dot(self, code, options, format, prefix) - except GraphvizError, exc: + except GraphvizError as exc: self.builder.warn('dot code %r: ' % code + str(exc)) raise nodes.SkipNode @@ -228,9 +242,6 @@ 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 node.get('caption') and not inline: - self.body.append('</p>\n<p class="caption">') - self.body.append(self.encode(node['caption'])) self.body.append('</%s>\n' % wrapper) raise nodes.SkipNode @@ -243,7 +254,7 @@ def html_visit_graphviz(self, node): def render_dot_latex(self, node, code, options, prefix='graphviz'): try: fname, outfn = render_dot(self, code, options, 'pdf', prefix) - except GraphvizError, exc: + except GraphvizError as exc: self.builder.warn('dot code %r: ' % code + str(exc)) raise nodes.SkipNode @@ -254,18 +265,8 @@ def render_dot_latex(self, node, code, options, prefix='graphviz'): para_separator = '\n' if fname is not None: - caption = node.get('caption') - # XXX add ids from previous target node - if caption and not inline: - self.body.append('\n\\begin{figure}[h!]') - self.body.append('\n\\begin{center}') - self.body.append('\n\\caption{%s}' % self.encode(caption)) - self.body.append('\n\\includegraphics{%s}' % fname) - self.body.append('\n\\end{center}') - self.body.append('\n\\end{figure}\n') - else: - self.body.append('%s\\includegraphics{%s}%s' % - (para_separator, fname, para_separator)) + self.body.append('%s\\includegraphics{%s}%s' % + (para_separator, fname, para_separator)) raise nodes.SkipNode @@ -276,16 +277,11 @@ def latex_visit_graphviz(self, node): def render_dot_texinfo(self, node, code, options, prefix='graphviz'): try: fname, outfn = render_dot(self, code, options, 'png', prefix) - except GraphvizError, exc: + except GraphvizError as exc: self.builder.warn('dot code %r: ' % code + str(exc)) raise nodes.SkipNode if fname is not None: - self.body.append('\n\n@float\n') - caption = node.get('caption') - if caption: - self.body.append('@caption{%s}\n' % self.escape_arg(caption)) - self.body.append('@image{%s,,,[graphviz],png}\n' - '@end float\n\n' % fname[:-4]) + self.body.append('@image{%s,,,[graphviz],png}\n' % fname[:-4]) raise nodes.SkipNode def texinfo_visit_graphviz(self, node): @@ -321,3 +317,4 @@ def setup(app): app.add_config_value('graphviz_dot', 'dot', 'html') app.add_config_value('graphviz_dot_args', [], 'html') app.add_config_value('graphviz_output_format', 'png', 'html') + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/ifconfig.py b/sphinx/ext/ifconfig.py index 3362e56a..a4e4a02d 100644 --- a/sphinx/ext/ifconfig.py +++ b/sphinx/ext/ifconfig.py @@ -22,6 +22,7 @@ from docutils import nodes +import sphinx from sphinx.util.nodes import set_source_info from sphinx.util.compat import Directive @@ -53,7 +54,7 @@ def process_ifconfig_nodes(app, doctree, docname): for node in doctree.traverse(ifconfig): try: res = eval(node['expr'], ns) - except Exception, err: + except Exception as err: # handle exceptions in a clean fashion from traceback import format_exception_only msg = ''.join(format_exception_only(err.__class__, err)) @@ -72,3 +73,4 @@ def setup(app): app.add_node(ifconfig) app.add_directive('ifconfig', IfConfig) app.connect('doctree-resolved', process_ifconfig_nodes) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 5b8eab3f..0b2e5ce3 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -39,15 +39,18 @@ r""" import re import sys import inspect -import __builtin__ as __builtin__ # as __builtin__ is for lib2to3 compatibility try: from hashlib import md5 except ImportError: from md5 import md5 +from six import text_type +from six.moves import builtins + from docutils import nodes from docutils.parsers.rst import directives +import sphinx from sphinx.ext.graphviz import render_dot_html, render_dot_latex, \ render_dot_texinfo from sphinx.pycode import ModuleAnalyzer @@ -145,10 +148,10 @@ class InheritanceGraph(object): displayed node names. """ all_classes = {} - builtins = vars(__builtin__).values() + py_builtins = vars(builtins).values() def recurse(cls): - if not show_builtins and cls in builtins: + if not show_builtins and cls in py_builtins: return if not private_bases and cls.__name__.startswith('_'): return @@ -162,7 +165,7 @@ class InheritanceGraph(object): if cls.__doc__: enc = ModuleAnalyzer.for_module(cls.__module__).encoding doc = cls.__doc__.strip().split("\n")[0] - if not isinstance(doc, unicode): + if not isinstance(doc, text_type): doc = force_decode(doc, enc) if doc: tooltip = '"%s"' % doc.replace('"', '\\"') @@ -172,7 +175,7 @@ class InheritanceGraph(object): baselist = [] all_classes[cls] = (nodename, fullname, baselist, tooltip) for base in cls.__bases__: - if not show_builtins and base in builtins: + if not show_builtins and base in py_builtins: continue if not private_bases and base.__name__.startswith('_'): continue @@ -192,7 +195,7 @@ class InheritanceGraph(object): completely general. """ module = cls.__module__ - if module == '__builtin__': + if module in ('__builtin__', 'builtins'): fullname = cls.__name__ else: fullname = '%s.%s' % (module, cls.__name__) @@ -308,10 +311,10 @@ class InheritanceDiagram(Directive): # Create a graph starting with the list of classes try: graph = InheritanceGraph( - class_names, env.temp_data.get('py:module'), + class_names, env.ref_context.get('py:module'), parts=node['parts'], private_bases='private-bases' in self.options) - except InheritanceException, err: + except InheritanceException as err: return [node.document.reporter.warning(err.args[0], line=self.lineno)] @@ -405,3 +408,4 @@ def setup(app): app.add_config_value('inheritance_graph_attrs', {}, False), app.add_config_value('inheritance_node_attrs', {}, False), app.add_config_value('inheritance_edge_attrs', {}, False), + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index c3adf563..6f3d44eb 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -27,27 +27,28 @@ import time import zlib import codecs -import urllib2 import posixpath from os import path import re +from six import iteritems +from six.moves.urllib import request from docutils import nodes from docutils.utils import relative_path +import sphinx from sphinx.locale import _ from sphinx.builders.html import INVENTORY_FILENAME -from sphinx.util.pycompat import b -handlers = [urllib2.ProxyHandler(), urllib2.HTTPRedirectHandler(), - urllib2.HTTPHandler()] +handlers = [request.ProxyHandler(), request.HTTPRedirectHandler(), + request.HTTPHandler()] try: - handlers.append(urllib2.HTTPSHandler) + handlers.append(request.HTTPSHandler) except AttributeError: pass -urllib2.install_opener(urllib2.build_opener(*handlers)) +request.install_opener(request.build_opener(*handlers)) UTF8StreamReader = codecs.lookup('utf-8')[2] @@ -55,9 +56,9 @@ UTF8StreamReader = codecs.lookup('utf-8')[2] def read_inventory_v1(f, uri, join): f = UTF8StreamReader(f) invdata = {} - line = f.next() + line = next(f) projname = line.rstrip()[11:] - line = f.next() + line = next(f) version = line.rstrip()[11:] for line in f: name, type, location = line.rstrip().split(None, 2) @@ -85,19 +86,19 @@ def read_inventory_v2(f, uri, join, bufsize=16*1024): def read_chunks(): decompressor = zlib.decompressobj() - for chunk in iter(lambda: f.read(bufsize), b('')): + for chunk in iter(lambda: f.read(bufsize), b''): yield decompressor.decompress(chunk) yield decompressor.flush() def split_lines(iter): - buf = b('') + buf = b'' for chunk in iter: buf += chunk - lineend = buf.find(b('\n')) + lineend = buf.find(b'\n') while lineend != -1: yield buf[:lineend].decode('utf-8') buf = buf[lineend+1:] - lineend = buf.find(b('\n')) + lineend = buf.find(b'\n') assert not buf for line in split_lines(read_chunks()): @@ -129,10 +130,10 @@ def fetch_inventory(app, uri, inv): join = localuri and path.join or posixpath.join try: if inv.find('://') != -1: - f = urllib2.urlopen(inv) + f = request.urlopen(inv) else: f = open(path.join(app.srcdir, inv), 'rb') - except Exception, err: + except Exception as err: app.warn('intersphinx inventory %r not fetchable due to ' '%s: %s' % (inv, err.__class__, err)) return @@ -149,7 +150,7 @@ def fetch_inventory(app, uri, inv): except ValueError: f.close() raise ValueError('unknown or unsupported inventory version') - except Exception, err: + except Exception as err: app.warn('intersphinx inventory %r not readable due to ' '%s: %s' % (inv, err.__class__.__name__, err)) else: @@ -167,7 +168,7 @@ def load_mappings(app): env.intersphinx_named_inventory = {} cache = env.intersphinx_cache update = False - for key, value in app.config.intersphinx_mapping.iteritems(): + for key, value in iteritems(app.config.intersphinx_mapping): if isinstance(value, tuple): # new format name, (uri, inv) = key, value @@ -179,19 +180,25 @@ def load_mappings(app): # we can safely assume that the uri<->inv mapping is not changed # during partial rebuilds since a changed intersphinx_mapping # setting will cause a full environment reread - if not inv: - inv = posixpath.join(uri, INVENTORY_FILENAME) - # decide whether the inventory must be read: always read local - # files; remote ones only if the cache time is expired - if '://' not in inv or uri not in cache \ - or cache[uri][1] < cache_time: - app.info('loading intersphinx inventory from %s...' % inv) - invdata = fetch_inventory(app, uri, inv) - if invdata: - cache[uri] = (name, now, invdata) - else: - cache.pop(uri, None) - update = True + if not isinstance(inv, tuple): + invs = (inv, ) + else: + invs = inv + + for inv in invs: + if not inv: + inv = posixpath.join(uri, INVENTORY_FILENAME) + # decide whether the inventory must be read: always read local + # files; remote ones only if the cache time is expired + if '://' not in inv or uri not in cache \ + or cache[uri][1] < cache_time: + app.info('loading intersphinx inventory from %s...' % inv) + invdata = fetch_inventory(app, uri, inv) + if invdata: + cache[uri] = (name, now, invdata) + update = True + break + if update: env.intersphinx_inventory = {} env.intersphinx_named_inventory = {} @@ -202,28 +209,34 @@ def load_mappings(app): # add the unnamed inventories last. This means that the # unnamed inventories will shadow the named ones but the named # ones can still be accessed when the name is specified. - cached_vals = list(cache.itervalues()) + cached_vals = list(cache.values()) named_vals = sorted(v for v in cached_vals if v[0]) unnamed_vals = [v for v in cached_vals if not v[0]] for name, _, invdata in named_vals + unnamed_vals: if name: env.intersphinx_named_inventory[name] = invdata - for type, objects in invdata.iteritems(): + for type, objects in iteritems(invdata): env.intersphinx_inventory.setdefault( type, {}).update(objects) def missing_reference(app, env, node, contnode): """Attempt to resolve a missing reference via intersphinx references.""" - domain = node.get('refdomain') - if not domain: - # only objects in domains are in the inventory - return target = node['reftarget'] - objtypes = env.domains[domain].objtypes_for_role(node['reftype']) - if not objtypes: - return - objtypes = ['%s:%s' % (domain, objtype) for objtype in objtypes] + if node['reftype'] == 'any': + # we search anything! + objtypes = ['%s:%s' % (domain.name, objtype) + for domain in env.domains.values() + for objtype in domain.object_types] + else: + domain = node.get('refdomain') + if not domain: + # only objects in domains are in the inventory + return + objtypes = env.domains[domain].objtypes_for_role(node['reftype']) + if not objtypes: + return + objtypes = ['%s:%s' % (domain, objtype) for objtype in objtypes] to_try = [(env.intersphinx_inventory, target)] in_set = None if ':' in target: @@ -241,7 +254,7 @@ def missing_reference(app, env, node, contnode): # get correct path in case of subdirectories uri = path.join(relative_path(node['refdoc'], env.srcdir), uri) newnode = nodes.reference('', '', internal=False, refuri=uri, - reftitle=_('(in %s v%s)') % (proj, version)) + reftitle=_('(in %s v%s)') % (proj, version)) if node.get('refexplicit'): # use whatever title was given newnode.append(contnode) @@ -269,3 +282,4 @@ def setup(app): app.add_config_value('intersphinx_cache_limit', 5, False) app.connect('missing-reference', missing_reference) app.connect('builder-inited', load_mappings) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/jsmath.py b/sphinx/ext/jsmath.py index 8907576f..9bf38f62 100644 --- a/sphinx/ext/jsmath.py +++ b/sphinx/ext/jsmath.py @@ -12,6 +12,7 @@ from docutils import nodes +import sphinx from sphinx.application import ExtensionError from sphinx.ext.mathbase import setup_math as mathbase_setup @@ -56,3 +57,4 @@ def setup(app): mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) app.add_config_value('jsmath_path', '', False) app.connect('builder-inited', builder_inited) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/linkcode.py b/sphinx/ext/linkcode.py index 77bd9f28..37e021e8 100644 --- a/sphinx/ext/linkcode.py +++ b/sphinx/ext/linkcode.py @@ -11,13 +11,16 @@ from docutils import nodes +import sphinx 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 @@ -67,6 +70,8 @@ def doctree_read(app, doctree): classes=['viewcode-link']) signode += onlynode + def setup(app): app.connect('doctree-read', doctree_read) app.add_config_value('linkcode_resolve', None, '') + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py index ee278667..f677ff48 100644 --- a/sphinx/ext/mathjax.py +++ b/sphinx/ext/mathjax.py @@ -13,6 +13,7 @@ from docutils import nodes +import sphinx from sphinx.application import ExtensionError from sphinx.ext.mathbase import setup_math as mathbase_setup @@ -60,9 +61,12 @@ def builder_inited(app): def setup(app): mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) + # more information for mathjax secure url is here: + # http://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn app.add_config_value('mathjax_path', - 'http://cdn.mathjax.org/mathjax/latest/MathJax.js?' + 'https://c328740.ssl.cf1.rackcdn.com/mathjax/latest/MathJax.js?' 'config=TeX-AMS-MML_HTMLorMML', False) app.add_config_value('mathjax_inline', [r'\(', r'\)'], 'html') app.add_config_value('mathjax_display', [r'\[', r'\]'], 'html') app.connect('builder-inited', builder_inited) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py new file mode 100644 index 00000000..9b43d8fd --- /dev/null +++ b/sphinx/ext/napoleon/__init__.py @@ -0,0 +1,391 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon + ~~~~~~~~~~~~~~~~~~~ + + Support for NumPy and Google style docstrings. + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys + +from six import PY2, iteritems + +import sphinx +from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring + + +class Config(object): + """Sphinx napoleon extension settings in `conf.py`. + + Listed below are all the settings used by napoleon and their default + values. These settings can be changed in the Sphinx `conf.py` file. Make + sure that both "sphinx.ext.autodoc" and "sphinx.ext.napoleon" are + enabled in `conf.py`:: + + # conf.py + + # Add any Sphinx extension module names here, as strings + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] + + # Napoleon settings + napoleon_google_docstring = True + napoleon_numpy_docstring = True + napoleon_include_private_with_doc = False + napoleon_include_special_with_doc = True + napoleon_use_admonition_for_examples = False + napoleon_use_admonition_for_notes = False + napoleon_use_admonition_for_references = False + napoleon_use_ivar = False + napoleon_use_param = True + napoleon_use_rtype = True + + .. _Google style: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html + .. _NumPy style: + https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + + Attributes + ---------- + napoleon_google_docstring : 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 + True to parse `NumPy style`_ docstrings. False to disable support + for NumPy style docstrings. + napoleon_include_private_with_doc : 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. + + **If True**:: + + def _included(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + pass + + def _skipped(self): + # This will NOT be included in the docs + pass + + napoleon_include_special_with_doc : bool, defaults to True + True to include special members (like ``__membername__``) with + docstrings in the documentation. False to fall back to Sphinx's + default behavior. + + **If True**:: + + def __str__(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + return unicode(self).encode('utf-8') + + def __unicode__(self): + # This will NOT be included in the docs + return unicode(self.__class__.__name__) + + napoleon_use_admonition_for_examples : 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 + theme is used. + + This `NumPy style`_ snippet will be converted as follows:: + + Example + ------- + This is just a quick example + + **If True**:: + + .. admonition:: Example + + This is just a quick example + + **If False**:: + + .. rubric:: Example + + This is just a quick example + + napoleon_use_admonition_for_notes : bool, defaults to False + True to use the ``.. admonition::`` directive for **Notes** sections. + False to use the ``.. rubric::`` directive instead. + + Note + ---- + The singular **Note** section will always be converted to a + ``.. note::`` directive. + + See Also + -------- + :attr:`napoleon_use_admonition_for_examples` + + napoleon_use_admonition_for_references : bool, defaults to False + True to use the ``.. admonition::`` directive for **References** + sections. False to use the ``.. rubric::`` directive instead. + + See Also + -------- + :attr:`napoleon_use_admonition_for_examples` + + napoleon_use_ivar : bool, defaults to False + True to use the ``:ivar:`` role for instance variables. False to use + the ``.. attribute::`` directive instead. + + This `NumPy style`_ snippet will be converted as follows:: + + Attributes + ---------- + attr1 : int + Description of `attr1` + + **If True**:: + + :ivar attr1: Description of `attr1` + :vartype attr1: int + + **If False**:: + + .. attribute:: attr1 + + *int* + + Description of `attr1` + + napoleon_use_param : 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. + + This `NumPy style`_ snippet will be converted as follows:: + + Parameters + ---------- + arg1 : str + Description of `arg1` + arg2 : int, optional + Description of `arg2`, defaults to 0 + + **If True**:: + + :param arg1: Description of `arg1` + :type arg1: str + :param arg2: Description of `arg2`, defaults to 0 + :type arg2: int, optional + + **If False**:: + + :parameters: * **arg1** (*str*) -- + Description of `arg1` + * **arg2** (*int, optional*) -- + Description of `arg2`, defaults to 0 + + napoleon_use_rtype : bool, defaults to True + True to use the ``:rtype:`` role for the return type. False to output + the return type inline with the description. + + This `NumPy style`_ snippet will be converted as follows:: + + Returns + ------- + bool + True if successful, False otherwise + + **If True**:: + + :returns: True if successful, False otherwise + :rtype: bool + + **If False**:: + + :returns: *bool* -- True if successful, False otherwise + + """ + _config_values = { + 'napoleon_google_docstring': (True, 'env'), + 'napoleon_numpy_docstring': (True, 'env'), + 'napoleon_include_private_with_doc': (False, 'env'), + 'napoleon_include_special_with_doc': (True, 'env'), + 'napoleon_use_admonition_for_examples': (False, 'env'), + 'napoleon_use_admonition_for_notes': (False, 'env'), + 'napoleon_use_admonition_for_references': (False, 'env'), + 'napoleon_use_ivar': (False, 'env'), + 'napoleon_use_param': (True, 'env'), + 'napoleon_use_rtype': (True, 'env'), + } + + def __init__(self, **settings): + for name, (default, rebuild) in iteritems(self._config_values): + setattr(self, name, default) + for name, value in iteritems(settings): + setattr(self, name, value) + + +def setup(app): + """Sphinx extension setup function. + + When the extension is loaded, Sphinx imports this module and executes + the ``setup()`` function, which in turn notifies Sphinx of everything + the extension offers. + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process + + See Also + -------- + The Sphinx documentation on `Extensions`_, the `Extension Tutorial`_, and + the `Extension API`_. + + .. _Extensions: http://sphinx-doc.org/extensions.html + .. _Extension Tutorial: http://sphinx-doc.org/ext/tutorial.html + .. _Extension API: http://sphinx-doc.org/ext/appapi.html + + """ + from sphinx.application import Sphinx + if not isinstance(app, Sphinx): + return # probably called by tests + + app.connect('autodoc-process-docstring', _process_docstring) + app.connect('autodoc-skip-member', _skip_member) + + for name, (default, rebuild) in iteritems(Config._config_values): + app.add_config_value(name, default, rebuild) + return {'version': sphinx.__version__, 'parallel_read_safe': True} + + +def _process_docstring(app, what, name, obj, options, lines): + """Process the docstring for a given python object. + + Called when autodoc has read and processed a docstring. `lines` is a list + of docstring lines that `_process_docstring` modifies in place to change + what Sphinx outputs. + + The following settings in conf.py control what styles of docstrings will + be parsed: + + * ``napoleon_google_docstring`` -- parse Google style docstrings + * ``napoleon_numpy_docstring`` -- parse NumPy style docstrings + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process. + what : str + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str + The fully qualified name of the object. + obj : module, class, exception, function, method, or attribute + The object to which the docstring belongs. + options : sphinx.ext.autodoc.Options + 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. + lines : list of str + The lines of the docstring, see above. + + .. note:: `lines` is modified *in place* + + """ + result_lines = lines + if app.config.napoleon_numpy_docstring: + docstring = NumpyDocstring(result_lines, app.config, app, what, name, + obj, options) + result_lines = docstring.lines() + if app.config.napoleon_google_docstring: + docstring = GoogleDocstring(result_lines, app.config, app, what, name, + obj, options) + result_lines = docstring.lines() + lines[:] = result_lines[:] + + +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: + + * ``napoleon_include_private_with_doc`` -- + include private members if they have docstrings + * ``napoleon_include_special_with_doc`` -- + include special members if they have docstrings + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process + what : str + A string specifying the type of the object to which the member + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str + The name of the member. + obj : module, class, exception, function, method, or attribute. + For example, if the member is the __init__ method of class A, then + `obj` will be `A.__init__`. + skip : bool + A boolean indicating if autodoc will skip this member if `_skip_member` + does not override the decision + options : sphinx.ext.autodoc.Options + 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. + + Returns + ------- + bool + True if the member should be skipped during creation of the docs, + False if it should be included in the docs. + + """ + 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: + cls_is_owner = False + if what == 'class' or what == 'exception': + if PY2: + cls = getattr(obj, 'im_class', getattr(obj, '__objclass__', + None)) + cls_is_owner = (cls and hasattr(cls, name) and + name in cls.__dict__) + elif sys.version_info >= (3, 3): + qualname = getattr(obj, '__qualname__', '') + cls_path, _, _ = qualname.rpartition('.') + if cls_path: + try: + if '.' in cls_path: + import importlib + import functools + + mod = importlib.import_module(obj.__module__) + mod_path = cls_path.split('.') + cls = functools.reduce(getattr, mod_path, mod) + else: + cls = obj.__globals__[cls_path] + except: + cls_is_owner = False + else: + cls_is_owner = (cls and hasattr(cls, name) and + name in cls.__dict__) + else: + cls_is_owner = False + else: + 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('_') + 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): + return False + return skip diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py new file mode 100644 index 00000000..19f5f395 --- /dev/null +++ b/sphinx/ext/napoleon/docstring.py @@ -0,0 +1,860 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon.docstring + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + Classes for docstring parsing and formatting. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import collections +import inspect +import re + +from six import string_types +from six.moves import range + +from sphinx.ext.napoleon.iterators import modify_iter +from sphinx.util.pycompat import UnicodeMixin + + +_directive_regex = re.compile(r'\.\. \S+::') +_google_untyped_arg_regex = re.compile(r'\s*(\w+)\s*:\s*(.*)') +_google_typed_arg_regex = re.compile(r'\s*(\w+)\s*\(\s*(.+?)\s*\)\s*:\s*(.*)') + + +class GoogleDocstring(UnicodeMixin): + """Parse Google style docstrings. + + Convert Google style docstrings to reStructuredText. + + Parameters + ---------- + docstring : str or list of str + The docstring to parse, given either as a string or split into + individual lines. + config : sphinx.ext.napoleon.Config or sphinx.config.Config, optional + 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. + + See Also + -------- + :class:`sphinx.ext.napoleon.Config` + + Other Parameters + ---------------- + app : sphinx.application.Sphinx, optional + Application object representing the Sphinx process. + what : str, optional + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : 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 : 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 + >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) + >>> docstring = '''One line summary. + ... + ... Extended description. + ... + ... Args: + ... arg1(int): Description of `arg1` + ... arg2(str): Description of `arg2` + ... Returns: + ... str: Description of return value. + ... ''' + >>> print(GoogleDocstring(docstring, config)) + One line summary. + <BLANKLINE> + Extended description. + <BLANKLINE> + :param arg1: Description of `arg1` + :type arg1: int + :param arg2: Description of `arg2` + :type arg2: str + <BLANKLINE> + :returns: Description of return value. + :rtype: str + + """ + def __init__(self, docstring, config=None, app=None, what='', name='', + obj=None, options=None): + self._config = config + self._app = app + + if not self._config: + from sphinx.ext.napoleon import Config + self._config = self._app and self._app.config or Config() + + if not what: + if inspect.isclass(obj): + what = 'class' + elif inspect.ismodule(obj): + what = 'module' + elif isinstance(obj, collections.Callable): + what = 'function' + else: + what = 'object' + + self._what = what + self._name = name + self._obj = obj + self._opt = options + if isinstance(docstring, string_types): + docstring = docstring.splitlines() + self._lines = docstring + self._line_iter = modify_iter(docstring, modifier=lambda s: s.rstrip()) + self._parsed_lines = [] + self._is_in_section = False + self._section_indent = 0 + if not hasattr(self, '_directive_sections'): + self._directive_sections = [] + if not hasattr(self, '_sections'): + self._sections = { + 'args': self._parse_parameters_section, + 'arguments': self._parse_parameters_section, + 'attributes': self._parse_attributes_section, + 'example': self._parse_examples_section, + 'examples': self._parse_examples_section, + 'keyword args': self._parse_keyword_arguments_section, + 'keyword arguments': self._parse_keyword_arguments_section, + 'methods': self._parse_methods_section, + 'note': self._parse_note_section, + 'notes': self._parse_notes_section, + 'other parameters': self._parse_other_parameters_section, + 'parameters': self._parse_parameters_section, + 'return': self._parse_returns_section, + 'returns': self._parse_returns_section, + 'raises': self._parse_raises_section, + 'references': self._parse_references_section, + 'see also': self._parse_see_also_section, + 'warning': self._parse_warning_section, + 'warnings': self._parse_warning_section, + 'warns': self._parse_warns_section, + 'yields': self._parse_yields_section, + } + self._parse() + + def __unicode__(self): + """Return the parsed docstring in reStructuredText format. + + Returns + ------- + unicode + Unicode version of the docstring. + + """ + return u'\n'.join(self.lines()) + + def lines(self): + """Return the parsed lines of the docstring in reStructuredText format. + + Returns + ------- + list of str + The lines of the docstring in a list. + + """ + return self._parsed_lines + + def _consume_indented_block(self, indent=1): + lines = [] + line = self._line_iter.peek() + while(not self._is_section_break() + and (not line or self._is_indented(line, indent))): + lines.append(next(self._line_iter)) + line = self._line_iter.peek() + return lines + + def _consume_contiguous(self): + lines = [] + while (self._line_iter.has_next() + and self._line_iter.peek() + and not self._is_section_header()): + lines.append(next(self._line_iter)) + return lines + + def _consume_empty(self): + lines = [] + line = self._line_iter.peek() + while self._line_iter.has_next() and not line: + lines.append(next(self._line_iter)) + line = self._line_iter.peek() + return lines + + def _consume_field(self, parse_type=True, prefer_type=False): + line = next(self._line_iter) + + match = None + _name, _type, _desc = line.strip(), '', '' + if parse_type: + match = _google_typed_arg_regex.match(line) + if match: + _name = match.group(1) + _type = match.group(2) + _desc = match.group(3) + + if not match: + match = _google_untyped_arg_regex.match(line) + if match: + _name = match.group(1) + _desc = match.group(2) + + if prefer_type and not _type: + _type, _name = _name, _type + indent = self._get_indent(line) + 1 + _desc = [_desc] + self._dedent(self._consume_indented_block(indent)) + _desc = self.__class__(_desc, self._config).lines() + return _name, _type, _desc + + def _consume_fields(self, parse_type=True, prefer_type=False): + self._consume_empty() + fields = [] + while not self._is_section_break(): + _name, _type, _desc = self._consume_field(parse_type, prefer_type) + if _name or _type or _desc: + fields.append((_name, _type, _desc,)) + return fields + + def _consume_returns_section(self): + lines = self._dedent(self._consume_to_next_section()) + if lines: + _name, _type, _desc = '', '', lines + match = _google_typed_arg_regex.match(lines[0]) + if match: + _name = match.group(1) + _type = match.group(2) + _desc = match.group(3) + else: + match = _google_untyped_arg_regex.match(lines[0]) + if match: + _type = match.group(1) + _desc = match.group(2) + if match: + lines[0] = _desc + _desc = lines + + _desc = self.__class__(_desc, self._config).lines() + return [(_name, _type, _desc,)] + else: + return [] + + def _consume_section_header(self): + section = next(self._line_iter) + stripped_section = section.strip(':') + if stripped_section.lower() in self._sections: + section = stripped_section + return section + + def _consume_to_next_section(self): + self._consume_empty() + lines = [] + while not self._is_section_break(): + lines.append(next(self._line_iter)) + return lines + self._consume_empty() + + def _dedent(self, lines, full=False): + if full: + return [line.lstrip() for line in lines] + else: + min_indent = self._get_min_indent(lines) + return [line[min_indent:] for line in lines] + + def _format_admonition(self, admonition, lines): + lines = self._strip_empty(lines) + if len(lines) == 1: + return ['.. %s:: %s' % (admonition, lines[0].strip()), ''] + elif lines: + lines = self._indent(self._dedent(lines), 3) + return ['.. %s::' % admonition, ''] + lines + [''] + else: + return ['.. %s::' % admonition, ''] + + def _format_block(self, prefix, lines, padding=None): + if lines: + if padding is None: + padding = ' ' * len(prefix) + result_lines = [] + for i, line in enumerate(lines): + if line: + if i == 0: + result_lines.append(prefix + line) + else: + result_lines.append(padding + line) + else: + result_lines.append('') + return result_lines + else: + return [prefix] + + def _format_field(self, _name, _type, _desc): + separator = any([s for s in _desc]) and ' --' or '' + if _name: + if _type: + if '`' in _type: + field = ['**%s** (%s)%s' % (_name, _type, separator)] + else: + field = ['**%s** (*%s*)%s' % (_name, _type, separator)] + else: + field = ['**%s**%s' % (_name, separator)] + elif _type: + if '`' in _type: + field = ['%s%s' % (_type, separator)] + else: + field = ['*%s*%s' % (_type, separator)] + else: + field = [] + return field + _desc + + def _format_fields(self, field_type, fields): + field_type = ':%s:' % field_type.strip() + padding = ' ' * len(field_type) + multi = len(fields) > 1 + lines = [] + for _name, _type, _desc in fields: + field = self._format_field(_name, _type, _desc) + if multi: + if lines: + lines.extend(self._format_block(padding + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' ', field)) + return lines + + def _get_current_indent(self, peek_ahead=0): + line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] + while line != self._line_iter.sentinel: + if line: + return self._get_indent(line) + peek_ahead += 1 + line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] + return 0 + + def _get_indent(self, line): + for i, s in enumerate(line): + if not s.isspace(): + return i + return len(line) + + def _get_min_indent(self, lines): + min_indent = None + for line in lines: + if line: + indent = self._get_indent(line) + if min_indent is None: + min_indent = indent + elif indent < min_indent: + min_indent = indent + return min_indent or 0 + + def _indent(self, lines, n=4): + return [(' ' * n) + line for line in lines] + + def _is_indented(self, line, indent=1): + for i, s in enumerate(line): + if i >= indent: + return True + elif not s.isspace(): + return False + return False + + def _is_section_header(self): + section = self._line_iter.peek().lower() + if section.strip(':') in self._sections: + header_indent = self._get_indent(section) + section_indent = self._get_current_indent(peek_ahead=1) + return section_indent > header_indent + elif self._directive_sections: + if _directive_regex.match(section): + for directive_section in self._directive_sections: + if section.startswith(directive_section): + return True + return False + + def _is_section_break(self): + line = self._line_iter.peek() + return (not self._line_iter.has_next() + or self._is_section_header() + or (self._is_in_section + and line + and not self._is_indented(line, self._section_indent))) + + def _parse(self): + self._parsed_lines = self._consume_empty() + while self._line_iter.has_next(): + if self._is_section_header(): + try: + section = self._consume_section_header() + self._is_in_section = True + self._section_indent = self._get_current_indent() + if _directive_regex.match(section): + lines = [section] + self._consume_to_next_section() + else: + lines = self._sections[section.lower()](section) + finally: + self._is_in_section = False + self._section_indent = 0 + else: + if not self._parsed_lines: + lines = self._consume_contiguous() + self._consume_empty() + else: + lines = self._consume_to_next_section() + self._parsed_lines.extend(lines) + + def _parse_attributes_section(self, section): + lines = [] + for _name, _type, _desc in self._consume_fields(): + if self._config.napoleon_use_ivar: + field = ':ivar %s: ' % _name + lines.extend(self._format_block(field, _desc)) + if _type: + lines.append(':vartype %s: %s' % (_name, _type)) + else: + lines.append('.. attribute:: ' + _name) + if _type: + lines.append('') + if '`' in _type: + lines.append(' %s' % _type) + else: + lines.append(' *%s*' % _type) + if _desc: + lines.extend([''] + self._indent(_desc, 3)) + lines.append('') + if self._config.napoleon_use_ivar: + lines.append('') + return lines + + def _parse_examples_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_examples + return self._parse_generic_section(section, use_admonition) + + def _parse_generic_section(self, section, use_admonition): + lines = self._strip_empty(self._consume_to_next_section()) + lines = self._dedent(lines) + if use_admonition: + header = '.. admonition:: %s' % section + lines = self._indent(lines, 3) + else: + header = '.. rubric:: %s' % section + if lines: + return [header, ''] + lines + [''] + else: + return [header, ''] + + def _parse_keyword_arguments_section(self, section): + return self._format_fields('Keyword Arguments', self._consume_fields()) + + def _parse_methods_section(self, section): + lines = [] + for _name, _, _desc in self._consume_fields(parse_type=False): + lines.append('.. method:: %s' % _name) + if _desc: + lines.extend([''] + self._indent(_desc, 3)) + lines.append('') + return lines + + def _parse_note_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('note', lines) + + def _parse_notes_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_notes + return self._parse_generic_section('Notes', use_admonition) + + def _parse_other_parameters_section(self, section): + return self._format_fields('Other Parameters', self._consume_fields()) + + def _parse_parameters_section(self, section): + fields = self._consume_fields() + if self._config.napoleon_use_param: + lines = [] + for _name, _type, _desc in fields: + field = ':param %s: ' % _name + lines.extend(self._format_block(field, _desc)) + if _type: + lines.append(':type %s: %s' % (_name, _type)) + return lines + [''] + else: + return self._format_fields('Parameters', fields) + + def _parse_raises_section(self, section): + fields = self._consume_fields() + field_type = ':raises:' + padding = ' ' * len(field_type) + multi = len(fields) > 1 + lines = [] + for _name, _type, _desc in fields: + sep = _desc and ' -- ' or '' + if _name: + if ' ' in _name: + _name = '**%s**' % _name + else: + _name = ':exc:`%s`' % _name + if _type: + if '`' in _type: + field = ['%s (%s)%s' % (_name, _type, sep)] + else: + field = ['%s (*%s*)%s' % (_name, _type, sep)] + else: + field = ['%s%s' % (_name, sep)] + elif _type: + if '`' in _type: + field = ['%s%s' % (_type, sep)] + else: + field = ['*%s*%s' % (_type, sep)] + else: + field = [] + field = field + _desc + if multi: + if lines: + lines.extend(self._format_block(padding + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' ', field)) + return lines + + def _parse_references_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_references + return self._parse_generic_section('References', use_admonition) + + def _parse_returns_section(self, section): + fields = self._consume_returns_section() + multi = len(fields) > 1 + if multi: + use_rtype = False + else: + use_rtype = self._config.napoleon_use_rtype + + lines = [] + for _name, _type, _desc in fields: + if use_rtype: + field = self._format_field(_name, '', _desc) + else: + field = self._format_field(_name, _type, _desc) + + if multi: + if lines: + lines.extend(self._format_block(' * ', field)) + else: + lines.extend(self._format_block(':returns: * ', field)) + else: + lines.extend(self._format_block(':returns: ', field)) + if _type and use_rtype: + lines.append(':rtype: %s' % _type) + lines.append('') + return lines + + def _parse_see_also_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('seealso', lines) + + def _parse_warning_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('warning', lines) + + def _parse_warns_section(self, section): + return self._format_fields('Warns', self._consume_fields()) + + def _parse_yields_section(self, section): + fields = self._consume_fields(prefer_type=True) + return self._format_fields('Yields', fields) + + def _strip_empty(self, lines): + if lines: + start = -1 + for i, line in enumerate(lines): + if line: + start = i + break + if start == -1: + lines = [] + end = -1 + for i in reversed(range(len(lines))): + line = lines[i] + if line: + end = i + break + if start > 0 or end + 1 < len(lines): + lines = lines[start:end + 1] + return lines + + +class NumpyDocstring(GoogleDocstring): + """Parse NumPy style docstrings. + + Convert NumPy style docstrings to reStructuredText. + + Parameters + ---------- + docstring : str or list of str + The docstring to parse, given either as a string or split into + individual lines. + config : sphinx.ext.napoleon.Config or sphinx.config.Config, optional + 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. + + See Also + -------- + :class:`sphinx.ext.napoleon.Config` + + Other Parameters + ---------------- + app : sphinx.application.Sphinx, optional + Application object representing the Sphinx process. + what : str, optional + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : 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 : 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 + >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) + >>> docstring = '''One line summary. + ... + ... Extended description. + ... + ... Parameters + ... ---------- + ... arg1 : int + ... Description of `arg1` + ... arg2 : str + ... Description of `arg2` + ... Returns + ... ------- + ... str + ... Description of return value. + ... ''' + >>> print(NumpyDocstring(docstring, config)) + One line summary. + <BLANKLINE> + Extended description. + <BLANKLINE> + :param arg1: Description of `arg1` + :type arg1: int + :param arg2: Description of `arg2` + :type arg2: str + <BLANKLINE> + :returns: Description of return value. + :rtype: str + + Methods + ------- + __str__() + Return the parsed docstring in reStructuredText format. + + Returns + ------- + str + UTF-8 encoded version of the docstring. + + __unicode__() + Return the parsed docstring in reStructuredText format. + + Returns + ------- + unicode + Unicode version of the docstring. + + lines() + Return the parsed lines of the docstring in reStructuredText format. + + Returns + ------- + list of str + The lines of the docstring in a list. + + """ + def __init__(self, docstring, config=None, app=None, what='', name='', + obj=None, options=None): + self._directive_sections = ['.. index::'] + super(NumpyDocstring, self).__init__(docstring, config, app, what, + name, obj, options) + + def _consume_field(self, parse_type=True, prefer_type=False): + line = next(self._line_iter) + if parse_type: + _name, _, _type = line.partition(':') + else: + _name, _type = line, '' + _name, _type = _name.strip(), _type.strip() + if prefer_type and not _type: + _type, _name = _name, _type + indent = self._get_indent(line) + _desc = self._dedent(self._consume_indented_block(indent + 1)) + _desc = self.__class__(_desc, self._config).lines() + return _name, _type, _desc + + def _consume_returns_section(self): + return self._consume_fields(prefer_type=True) + + def _consume_section_header(self): + section = next(self._line_iter) + if not _directive_regex.match(section): + # Consume the header underline + next(self._line_iter) + return section + + def _is_section_break(self): + line1, line2 = self._line_iter.peek(2) + return (not self._line_iter.has_next() + or self._is_section_header() + or ['', ''] == [line1, line2] + or (self._is_in_section + and line1 + and not self._is_indented(line1, self._section_indent))) + + def _is_section_header(self): + section, underline = self._line_iter.peek(2) + section = section.lower() + if section in self._sections and isinstance(underline, string_types): + pattern = r'[=\-`:\'"~^_*+#<>]{' + str(len(section)) + r'}$' + return bool(re.match(pattern, underline)) + elif self._directive_sections: + if _directive_regex.match(section): + for directive_section in self._directive_sections: + if section.startswith(directive_section): + return True + return False + + _name_rgx = re.compile(r"^\s*(:(?P<role>\w+):`(?P<name>[a-zA-Z0-9_.-]+)`|" + r" (?P<name2>[a-zA-Z0-9_.-]+))\s*", re.X) + + def _parse_see_also_section(self, section): + lines = self._consume_to_next_section() + try: + return self._parse_numpydoc_see_also_section(lines) + except ValueError: + return self._format_admonition('seealso', lines) + + def _parse_numpydoc_see_also_section(self, content): + """ + Derived from the NumpyDoc implementation of _parse_see_also. + + func_name : Descriptive text + continued text + another_func_name : Descriptive text + func_name1, func_name2, :meth:`func_name`, func_name3 + + """ + items = [] + + def parse_item_name(text): + """Match ':role:`name`' or 'name'""" + m = self._name_rgx.match(text) + if m: + g = m.groups() + if g[1] is None: + return g[3], None + else: + return g[2], g[1] + raise ValueError("%s is not a item name" % text) + + def push_item(name, rest): + if not name: + return + name, role = parse_item_name(name) + items.append((name, list(rest), role)) + del rest[:] + + current_func = None + rest = [] + + for line in content: + if not line.strip(): + continue + + m = self._name_rgx.match(line) + if m and line[m.end():].strip().startswith(':'): + push_item(current_func, rest) + current_func, line = line[:m.end()], line[m.end():] + rest = [line.split(':', 1)[1].strip()] + if not rest[0]: + rest = [] + elif not line.startswith(' '): + push_item(current_func, rest) + current_func = None + if ',' in line: + for func in line.split(','): + if func.strip(): + push_item(func, []) + elif line.strip(): + current_func = line + elif current_func is not None: + rest.append(line.strip()) + push_item(current_func, rest) + + if not items: + return [] + + roles = { + 'method': 'meth', + 'meth': 'meth', + 'function': 'func', + 'func': 'func', + 'class': 'class', + 'exception': 'exc', + 'exc': 'exc', + 'object': 'obj', + 'obj': 'obj', + 'module': 'mod', + 'mod': 'mod', + 'data': 'data', + 'constant': 'const', + 'const': 'const', + 'attribute': 'attr', + 'attr': 'attr' + } + if self._what is None: + func_role = 'obj' + else: + func_role = roles.get(self._what, '') + lines = [] + last_had_desc = True + for func, desc, role in items: + if role: + link = ':%s:`%s`' % (role, func) + elif func_role: + link = ':%s:`%s`' % (func_role, func) + else: + link = "`%s`_" % func + if desc or last_had_desc: + lines += [''] + lines += [link] + else: + lines[-1] += ", %s" % link + if desc: + lines += self._indent([' '.join(desc)]) + last_had_desc = True + else: + last_had_desc = False + lines += [''] + + return self._format_admonition('seealso', lines) diff --git a/sphinx/ext/napoleon/iterators.py b/sphinx/ext/napoleon/iterators.py new file mode 100644 index 00000000..482fe1dd --- /dev/null +++ b/sphinx/ext/napoleon/iterators.py @@ -0,0 +1,239 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon.iterators + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + A collection of helpful iterators. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import collections + + +class peek_iter(object): + """An iterator object that supports peeking ahead. + + Parameters + ---------- + o : iterable or callable + `o` is interpreted very differently depending on the presence of + `sentinel`. + + If `sentinel` is not given, then `o` must be a collection object + which supports either the iteration protocol or the sequence protocol. + + If `sentinel` is given, then `o` must be a callable object. + + sentinel : any value, optional + If given, the iterator will call `o` with no arguments for each + call to its `next` method; if the value returned is equal to + `sentinel`, :exc:`StopIteration` will be raised, otherwise the + value will be returned. + + See Also + -------- + `peek_iter` can operate as a drop in replacement for the built-in + `iter <http://docs.python.org/2/library/functions.html#iter>`_ function. + + Attributes + ---------- + sentinel + The value used to indicate the iterator is exhausted. If `sentinel` + was not given when the `peek_iter` was instantiated, then it will + be set to a new object instance: ``object()``. + + """ + def __init__(self, *args): + """__init__(o, sentinel=None)""" + self._iterable = iter(*args) + self._cache = collections.deque() + if len(args) == 2: + self.sentinel = args[1] + else: + self.sentinel = object() + + def __iter__(self): + return self + + def __next__(self, n=None): + # note: prevent 2to3 to transform self.next() in next(self) which + # causes an infinite loop ! + return getattr(self, 'next')(n) + + def _fillcache(self, n): + """Cache `n` items. If `n` is 0 or None, then 1 item is cached.""" + if not n: + n = 1 + try: + while len(self._cache) < n: + self._cache.append(next(self._iterable)) + except StopIteration: + while len(self._cache) < n: + self._cache.append(self.sentinel) + + def has_next(self): + """Determine if iterator is exhausted. + + Returns + ------- + bool + True if iterator has more items, False otherwise. + + Note + ---- + Will never raise :exc:`StopIteration`. + + """ + return self.peek() != self.sentinel + + def next(self, n=None): + """Get the next item or `n` items of the iterator. + + Parameters + ---------- + n : int or None + The number of items to retrieve. Defaults to None. + + Returns + ------- + item or list of items + The next item or `n` items of the iterator. If `n` is None, the + item itself is returned. If `n` is an int, the items will be + returned in a list. If `n` is 0, an empty list is returned. + + Raises + ------ + StopIteration + Raised if the iterator is exhausted, even if `n` is 0. + + """ + self._fillcache(n) + if not n: + if self._cache[0] == self.sentinel: + raise StopIteration + if n is None: + result = self._cache.popleft() + else: + result = [] + else: + if self._cache[n - 1] == self.sentinel: + raise StopIteration + result = [self._cache.popleft() for i in range(n)] + return result + + def peek(self, n=None): + """Preview the next item or `n` items of the iterator. + + The iterator is not advanced when peek is called. + + Returns + ------- + item or list of items + The next item or `n` items of the iterator. If `n` is None, the + item itself is returned. If `n` is an int, the items will be + returned in a list. If `n` is 0, an empty list is returned. + + If the iterator is exhausted, `peek_iter.sentinel` is returned, + or placed as the last item in the returned list. + + Note + ---- + Will never raise :exc:`StopIteration`. + + """ + self._fillcache(n) + if n is None: + result = self._cache[0] + else: + result = [self._cache[i] for i in range(n)] + return result + + +class modify_iter(peek_iter): + """An iterator object that supports modifying items as they are returned. + + Parameters + ---------- + o : iterable or callable + `o` is interpreted very differently depending on the presence of + `sentinel`. + + If `sentinel` is not given, then `o` must be a collection object + which supports either the iteration protocol or the sequence protocol. + + If `sentinel` is given, then `o` must be a callable object. + + sentinel : any value, optional + If given, the iterator will call `o` with no arguments for each + call to its `next` method; if the value returned is equal to + `sentinel`, :exc:`StopIteration` will be raised, otherwise the + value will be returned. + + modifier : callable, optional + The function that will be used to modify each item returned by the + iterator. `modifier` should take a single argument and return a + single value. Defaults to ``lambda x: x``. + + If `sentinel` is not given, `modifier` must be passed as a keyword + argument. + + Attributes + ---------- + modifier : callable + `modifier` is called with each item in `o` as it is iterated. The + return value of `modifier` is returned in lieu of the item. + + Values returned by `peek` as well as `next` are affected by + `modifier`. However, `modify_iter.sentinel` is never passed through + `modifier`; it will always be returned from `peek` unmodified. + + Example + ------- + >>> a = [" A list ", + ... " of strings ", + ... " with ", + ... " extra ", + ... " whitespace. "] + >>> modifier = lambda s: s.strip().replace('with', 'without') + >>> for s in modify_iter(a, modifier=modifier): + ... print('"%s"' % s) + "A list" + "of strings" + "without" + "extra" + "whitespace." + + """ + def __init__(self, *args, **kwargs): + """__init__(o, sentinel=None, modifier=lambda x: x)""" + if 'modifier' in kwargs: + self.modifier = kwargs['modifier'] + elif len(args) > 2: + self.modifier = args[2] + args = args[:2] + else: + self.modifier = lambda x: x + if not callable(self.modifier): + raise TypeError('modify_iter(o, modifier): ' + 'modifier must be callable') + super(modify_iter, self).__init__(*args) + + def _fillcache(self, n): + """Cache `n` modified items. If `n` is 0 or None, 1 item is cached. + + Each item returned by the iterator is passed through the + `modify_iter.modified` function before being cached. + + """ + if not n: + n = 1 + try: + while len(self._cache) < n: + self._cache.append(self.modifier(next(self._iterable))) + except StopIteration: + while len(self._cache) < n: + self._cache.append(self.sentinel) diff --git a/sphinx/ext/oldcmarkup.py b/sphinx/ext/oldcmarkup.py deleted file mode 100644 index aa10246b..00000000 --- a/sphinx/ext/oldcmarkup.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.ext.oldcmarkup - ~~~~~~~~~~~~~~~~~~~~~ - - Extension for compatibility with old C markup (directives and roles). - - :copyright: Copyright 2007-2013 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -""" - -from docutils.parsers.rst import directives - -from sphinx.util.compat import Directive - -_warned_oldcmarkup = False -WARNING_MSG = 'using old C markup; please migrate to new-style markup ' \ - '(e.g. c:function instead of cfunction), see ' \ - 'http://sphinx-doc.org/domains.html' - - -class OldCDirective(Directive): - has_content = True - required_arguments = 1 - optional_arguments = 0 - final_argument_whitespace = True - option_spec = { - 'noindex': directives.flag, - 'module': directives.unchanged, - } - - def run(self): - env = self.state.document.settings.env - if not env.app._oldcmarkup_warned: - self.state_machine.reporter.warning(WARNING_MSG, line=self.lineno) - env.app._oldcmarkup_warned = True - newname = 'c:' + self.name[1:] - newdir = env.lookup_domain_element('directive', newname)[0] - return newdir(newname, self.arguments, self.options, - self.content, self.lineno, self.content_offset, - self.block_text, self.state, self.state_machine).run() - - -def old_crole(typ, rawtext, text, lineno, inliner, options={}, content=[]): - env = inliner.document.settings.env - if not typ: - typ = env.config.default_role - if not env.app._oldcmarkup_warned: - inliner.reporter.warning(WARNING_MSG, line=lineno) - env.app._oldcmarkup_warned = True - newtyp = 'c:' + typ[1:] - newrole = env.lookup_domain_element('role', newtyp)[0] - return newrole(newtyp, rawtext, text, lineno, inliner, options, content) - - -def setup(app): - app._oldcmarkup_warned = False - app.add_directive('cfunction', OldCDirective) - app.add_directive('cmember', OldCDirective) - app.add_directive('cmacro', OldCDirective) - app.add_directive('ctype', OldCDirective) - app.add_directive('cvar', OldCDirective) - app.add_role('cdata', old_crole) - app.add_role('cfunc', old_crole) - app.add_role('cmacro', old_crole) - app.add_role('ctype', old_crole) - app.add_role('cmember', old_crole) diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index abac15cd..81a6f9d5 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -8,7 +8,6 @@ :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ -from __future__ import with_statement import re import codecs @@ -17,17 +16,16 @@ import tempfile import posixpath from os import path from subprocess import Popen, PIPE -try: - from hashlib import sha1 as sha -except ImportError: - from sha import sha +from hashlib import sha1 +from six import text_type from docutils import nodes +import sphinx from sphinx.errors import SphinxError from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.osutil import ensuredir, ENOENT, cd -from sphinx.util.pycompat import b, sys_encoding +from sphinx.util.pycompat import sys_encoding from sphinx.ext.mathbase import setup_math as mathbase_setup, wrap_displaymath class MathExtError(SphinxError): @@ -67,7 +65,7 @@ DOC_BODY_PREVIEW = r''' \end{document} ''' -depth_re = re.compile(b(r'\[\d+ depth=(-?\d+)\]')) +depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]') def render_math(self, math): """Render the LaTeX math expression *math* using latex and dvipng. @@ -86,9 +84,9 @@ def render_math(self, math): latex = DOC_HEAD + self.builder.config.pngmath_latex_preamble latex += (use_preview and DOC_BODY_PREVIEW or DOC_BODY) % math - shasum = "%s.png" % sha(latex.encode('utf-8')).hexdigest() + shasum = "%s.png" % sha1(latex.encode('utf-8')).hexdigest() relfn = posixpath.join(self.builder.imgpath, 'math', shasum) - outfn = path.join(self.builder.outdir, '_images', 'math', shasum) + outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', shasum) if path.isfile(outfn): depth = read_png_depth(outfn) return relfn, depth @@ -121,7 +119,7 @@ def render_math(self, math): with cd(tempdir): try: p = Popen(ltx_args, stdout=PIPE, stderr=PIPE) - except OSError, err: + except OSError as err: if err.errno != ENOENT: # No such file or directory raise self.builder.warn('LaTeX command %r cannot be run (needed for math ' @@ -146,7 +144,7 @@ def render_math(self, math): dvipng_args.append(path.join(tempdir, 'math.dvi')) try: p = Popen(dvipng_args, stdout=PIPE, stderr=PIPE) - except OSError, err: + except OSError as err: if err.errno != ENOENT: # No such file or directory raise self.builder.warn('dvipng command %r cannot be run (needed for math ' @@ -186,8 +184,8 @@ def get_tooltip(self, node): def html_visit_math(self, node): try: fname, depth = render_math(self, '$'+node['latex']+'$') - except MathExtError, exc: - msg = unicode(exc) + except MathExtError as exc: + msg = text_type(exc) sm = nodes.system_message(msg, type='WARNING', level=2, backrefs=[], source=node['latex']) sm.walkabout(self) @@ -211,7 +209,7 @@ def html_visit_displaymath(self, node): latex = wrap_displaymath(node['latex'], None) try: fname, depth = render_math(self, latex) - except MathExtError, exc: + except MathExtError as exc: sm = nodes.system_message(str(exc), type='WARNING', level=2, backrefs=[], source=node['latex']) sm.walkabout(self) @@ -243,3 +241,4 @@ def setup(app): app.add_config_value('pngmath_latex_preamble', '', 'html') app.add_config_value('pngmath_add_tooltips', True, 'html') app.connect('build-finished', cleanup_tempdir) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 9f521fb4..ae434dd4 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -14,6 +14,7 @@ from docutils import nodes +import sphinx from sphinx.locale import _ from sphinx.environment import NoUri from sphinx.util.nodes import set_source_info @@ -149,6 +150,14 @@ def purge_todos(app, env, docname): if todo['docname'] != docname] +def merge_info(app, env, docnames, other): + if not hasattr(other, 'todo_all_todos'): + return + if not hasattr(env, 'todo_all_todos'): + env.todo_all_todos = [] + env.todo_all_todos.extend(other.todo_all_todos) + + def visit_todo_node(self, node): self.visit_admonition(node) @@ -171,4 +180,5 @@ def setup(app): app.connect('doctree-read', process_todos) app.connect('doctree-resolved', process_todo_nodes) app.connect('env-purge-doc', purge_todos) - + app.connect('env-merge-info', merge_info) + return {'version': sphinx.__version__, 'parallel_read_safe': True} diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 74a00463..cd3f2ac7 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -9,12 +9,37 @@ :license: BSD, see LICENSE for details. """ +import traceback + +from six import iteritems, text_type from docutils import nodes +import sphinx from sphinx import addnodes from sphinx.locale import _ from sphinx.pycode import ModuleAnalyzer +from sphinx.util import get_full_modname from sphinx.util.nodes import make_refnode +from sphinx.util.console import blue + + +def _get_full_modname(app, modname, attribute): + try: + return get_full_modname(modname, attribute) + except AttributeError: + # sphinx.ext.viewcode can't follow class instance attribute + # then AttributeError logging output only verbose mode. + app.verbose('Didn\'t find %s in %s' % (attribute, modname)) + return None + except Exception as e: + # sphinx.ext.viewcode follow python domain directives. + # because of that, if there are no real modules exists that specified + # by py:function or other directives, viewcode emits a lot of warnings. + # It should be displayed only verbose mode. + app.verbose(traceback.format_exc().rstrip()) + app.verbose('viewcode can\'t import %s, failed with error "%s"' % + (modname, e)) + return None def doctree_read(app, doctree): @@ -22,24 +47,24 @@ def doctree_read(app, doctree): if not hasattr(env, '_viewcode_modules'): env._viewcode_modules = {} - def has_tag(modname, fullname, docname): + def has_tag(modname, fullname, docname, refname): entry = env._viewcode_modules.get(modname, None) try: analyzer = ModuleAnalyzer.for_module(modname) except Exception: env._viewcode_modules[modname] = False return - if not isinstance(analyzer.code, unicode): + if not isinstance(analyzer.code, text_type): code = analyzer.code.decode(analyzer.encoding) else: code = analyzer.code if entry is None or entry[0] != code: analyzer.find_tags() - entry = code, analyzer.tags, {} + entry = code, analyzer.tags, {}, refname env._viewcode_modules[modname] = entry elif entry is False: return - code, tags, used = entry + _, tags, used, _ = entry if fullname in tags: used[fullname] = docname return True @@ -52,10 +77,14 @@ def doctree_read(app, doctree): if not isinstance(signode, addnodes.desc_signature): continue modname = signode.get('module') + fullname = signode.get('fullname') + refname = modname + if env.config.viewcode_import: + modname = _get_full_modname(app, modname, fullname) if not modname: continue fullname = signode.get('fullname') - if not has_tag(modname, fullname, env.docname): + if not has_tag(modname, fullname, env.docname, refname): continue if fullname in names: # only one link per name, please @@ -72,6 +101,16 @@ def doctree_read(app, doctree): signode += onlynode +def env_merge_info(app, env, docnames, other): + if not hasattr(other, '_viewcode_modules'): + return + # create a _viewcode_modules dict on the main environment + if not hasattr(env, '_viewcode_modules'): + env._viewcode_modules = {} + # now merge in the information from the subprocess + env._viewcode_modules.update(other._viewcode_modules) + + def missing_reference(app, env, node, contnode): # resolve our "viewcode" reference nodes -- they need special treatment if node['reftype'] == 'viewcode': @@ -88,13 +127,15 @@ def collect_pages(app): modnames = set(env._viewcode_modules) - app.builder.info(' (%d module code pages)' % - len(env._viewcode_modules), nonl=1) +# app.builder.info(' (%d module code pages)' % +# len(env._viewcode_modules), nonl=1) - for modname, entry in env._viewcode_modules.iteritems(): + for modname, entry in app.status_iterator( + iteritems(env._viewcode_modules), 'highlighting module code... ', + blue, len(env._viewcode_modules), lambda x: x[0]): if not entry: continue - code, tags, used = entry + code, tags, used, refname = entry # construct a page name for the highlighted source pagename = '_modules/' + modname.replace('.', '/') # highlight the source using the builder's highlighter @@ -109,9 +150,9 @@ def collect_pages(app): # the collected tags (HACK: this only works if the tag boundaries are # properly nested!) maxindex = len(lines) - 1 - for name, docname in used.iteritems(): + for name, docname in iteritems(used): type, start, end = tags[name] - backlink = urito(pagename, docname) + '#' + modname + '.' + name + backlink = urito(pagename, docname) + '#' + refname + '.' + name lines[start] = ( '<div class="viewcode-block" id="%s"><a class="viewcode-back" ' 'href="%s">%s</a>' % (name, backlink, _('[docs]')) @@ -134,15 +175,14 @@ def collect_pages(app): context = { 'parents': parents, 'title': modname, - 'body': _('<h1>Source code for %s</h1>') % modname + \ - '\n'.join(lines) + 'body': (_('<h1>Source code for %s</h1>') % modname + + '\n'.join(lines)), } yield (pagename, context, 'page.html') if not modnames: return - app.builder.info(' _modules/index', nonl=True) html = ['\n'] # the stack logic is needed for using nested lists for submodules stack = [''] @@ -162,16 +202,19 @@ def collect_pages(app): html.append('</ul>' * (len(stack) - 1)) context = { 'title': _('Overview: module code'), - 'body': _('<h1>All modules for which code is available</h1>') + \ - ''.join(html), + 'body': (_('<h1>All modules for which code is available</h1>') + + ''.join(html)), } yield ('_modules/index', context, 'page.html') def setup(app): + app.add_config_value('viewcode_import', True, False) app.connect('doctree-read', doctree_read) + app.connect('env-merge-info', env_merge_info) 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') + # app.add_config_value('viewcode_include_modules', [], 'env') + # app.add_config_value('viewcode_exclude_modules', [], 'env') + return {'version': sphinx.__version__, 'parallel_read_safe': True} |