diff options
| author | Georg Brandl <georg@python.org> | 2011-09-22 13:56:16 +0200 |
|---|---|---|
| committer | Georg Brandl <georg@python.org> | 2011-09-22 13:56:16 +0200 |
| commit | 554b4d7314f6916882cc8156d12639dcb010063e (patch) | |
| tree | 111248273d29bfb328cd4bccbf3d12fdeaa61f51 /sphinx/ext | |
| parent | 8759282ac0f0b86373c82576dcb4ee0d6bcb70e1 (diff) | |
| parent | 7c1dba82ea9a95e54ce4478ef30f83da438f8fff (diff) | |
| download | sphinx-554b4d7314f6916882cc8156d12639dcb010063e.tar.gz | |
Merge with 1.0
Diffstat (limited to 'sphinx/ext')
| -rw-r--r-- | sphinx/ext/autodoc.py | 334 | ||||
| -rw-r--r-- | sphinx/ext/autosummary/__init__.py | 119 | ||||
| -rw-r--r-- | sphinx/ext/autosummary/generate.py | 21 | ||||
| -rw-r--r-- | sphinx/ext/coverage.py | 29 | ||||
| -rw-r--r-- | sphinx/ext/doctest.py | 88 | ||||
| -rw-r--r-- | sphinx/ext/graphviz.py | 146 | ||||
| -rw-r--r-- | sphinx/ext/inheritance_diagram.py | 53 | ||||
| -rw-r--r-- | sphinx/ext/intersphinx.py | 26 | ||||
| -rw-r--r-- | sphinx/ext/mathbase.py | 50 | ||||
| -rw-r--r-- | sphinx/ext/mathjax.py | 68 | ||||
| -rw-r--r-- | sphinx/ext/oldcmarkup.py | 1 | ||||
| -rw-r--r-- | sphinx/ext/pngmath.py | 29 | ||||
| -rw-r--r-- | sphinx/ext/todo.py | 3 | ||||
| -rw-r--r-- | sphinx/ext/viewcode.py | 8 |
14 files changed, 695 insertions, 280 deletions
diff --git a/sphinx/ext/autodoc.py b/sphinx/ext/autodoc.py index f72e7dfa..f19334a0 100644 --- a/sphinx/ext/autodoc.py +++ b/sphinx/ext/autodoc.py @@ -15,7 +15,7 @@ import re import sys import inspect import traceback -from types import FunctionType, BuiltinFunctionType, MethodType, ClassType +from types import FunctionType, BuiltinFunctionType, MethodType from docutils import nodes from docutils.utils import assemble_option_dict @@ -27,16 +27,12 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError 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 isdescriptor, safe_getmembers, safe_getattr +from sphinx.util.inspect import getargspec, isdescriptor, safe_getmembers, \ + safe_getattr, safe_repr +from sphinx.util.pycompat import base_exception, class_types from sphinx.util.docstrings import prepare_docstring -try: - base_exception = BaseException -except NameError: - base_exception = Exception - - #: extended signature RE: with explicit module name separated by :: py_ext_sig_re = re.compile( r'''^ ([\w.]+::)? # explicit module name @@ -91,7 +87,8 @@ def members_set_option(arg): def bool_option(arg): """Used to convert flag options to auto directives. (Instead of - directives.flag(), which returns None.)""" + directives.flag(), which returns None). + """ return True @@ -139,8 +136,7 @@ class AutodocReporter(object): # Some useful event listener factories for autodoc-process-docstring. def cut_lines(pre, post=0, what=None): - """ - Return a listener that removes the first *pre* and last *post* + """Return a listener that removes the first *pre* and last *post* lines of every docstring. If *what* is a sequence of strings, only docstrings of a type in *what* will be processed. @@ -166,9 +162,8 @@ def cut_lines(pre, post=0, what=None): 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 + """Return a listener that either keeps, or if *exclude* is True excludes, + lines between lines that match the *marker* regular expression. If no line matches, the resulting docstring would be empty, so no change will be made unless *keepempty* is true. @@ -270,8 +265,7 @@ class Documenter(object): self.directive.result.append(self.indent + line, source, *lineno) def resolve_name(self, modname, parents, path, base): - """ - Resolve the module and name of the object to document given by the + """Resolve the module and name of the object to document given by the arguments and the current module/class. Must return a pair of the module name and a chain of attributes; for @@ -281,8 +275,7 @@ class Documenter(object): raise NotImplementedError('must be implemented in subclasses') def parse_name(self): - """ - Determine what module to import and what attribute to document. + """Determine what module to import and what attribute to document. Returns True and sets *self.modname*, *self.objpath*, *self.fullname*, *self.args* and *self.retann* if parsing and resolving was successful. @@ -319,8 +312,7 @@ class Documenter(object): return True def import_object(self): - """ - Import the object given by *self.modname* and *self.objpath* and sets + """Import the object given by *self.modname* and *self.objpath* and set it as *self.object*. Returns True if successful, False if an error occurred. @@ -349,15 +341,15 @@ class Documenter(object): return False def get_real_modname(self): - """ - Get the real module name of an object to document. (It can differ - from the name of the module through which the object was imported.) + """Get the real module name of an object to document. + + It can differ from the name of the module through which the object was + imported. """ return self.get_attr(self.object, '__module__', None) or self.modname def check_module(self): - """ - Check if *self.object* is really defined in the module given by + """Check if *self.object* is really defined in the module given by *self.modname*. """ modname = self.get_attr(self.object, '__module__', None) @@ -366,25 +358,26 @@ class Documenter(object): return True def format_args(self): - """ - Format the argument signature of *self.object*. Should return None if - the object does not have a signature. + """Format the argument signature of *self.object*. + + Should return None if the object does not have a signature. """ return None def format_name(self): - """ - Format the name of *self.object*. This normally should be something - that can be parsed by the generated directive, but doesn't need to be - (Sphinx will display it unparsed then). + """Format the name of *self.object*. + + This normally should be something that can be parsed by the generated + directive, but doesn't need to be (Sphinx will display it unparsed + then). """ # normally the name doesn't contain the module (except for module # directives of course) return '.'.join(self.objpath) or self.modname def format_signature(self): - """ - Format the signature (arguments and return annotation) of the object. + """Format the signature (arguments and return annotation) of the object. + Let the user process it via the ``autodoc-process-signature`` event. """ if self.args is not None: @@ -426,13 +419,16 @@ class Documenter(object): # etc. don't support a prepended module name self.add_line(u' :module: %s' % self.modname, '<autodoc>') - def get_doc(self, encoding=None): + def get_doc(self, encoding=None, ignore=1): """Decode and return lines of the docstring(s) for the object.""" docstring = self.get_attr(self.object, '__doc__', None) - if docstring: - # make sure we have Unicode docstrings, then sanitize and split - # into lines - return [prepare_docstring(force_decode(docstring, encoding))] + # make sure we have Unicode docstrings, then sanitize and split + # into lines + if isinstance(docstring, unicode): + return [prepare_docstring(docstring, ignore)] + elif docstring: + return [prepare_docstring(force_decode(docstring, encoding), + ignore)] return [] def process_doc(self, docstrings): @@ -451,8 +447,11 @@ class Documenter(object): # set sourcename and add content from attribute documentation if self.analyzer: # prevent encoding errors when the file name is non-ASCII - filename = unicode(self.analyzer.srcname, - sys.getfilesystemencoding(), 'replace') + if not isinstance(self.analyzer.srcname, unicode): + filename = unicode(self.analyzer.srcname, + sys.getfilesystemencoding(), 'replace') + else: + filename = self.analyzer.srcname sourcename = u'%s:docstring of %s' % (filename, self.fullname) attr_docs = self.analyzer.find_attr_docs() @@ -484,8 +483,7 @@ class Documenter(object): self.add_line(line, src[0], src[1]) def get_object_members(self, want_all): - """ - Return `(members_check_module, members)` where `members` is a + """Return `(members_check_module, members)` where `members` is a list of `(membername, member)` pairs of the members of *self.object*. If *want_all* is True, return all members. Else, only return those @@ -529,11 +527,15 @@ class Documenter(object): return False, sorted(members) def filter_members(self, members, want_all): - """ - Filter the given member list: members are skipped if + """Filter the given member list. - - they are private (except if given explicitly) - - they are undocumented (except if undoc-members is given) + Members are skipped if + + - they are private (except if given explicitly or the private-members + option is set) + - they are special methods (except if given explicitly or the + special-members option is set) + - they are undocumented (except if the undoc-members option is set) The user can override the skipping decision by connecting to the ``autodoc-skip-member`` event. @@ -553,18 +555,33 @@ class Documenter(object): # if isattr is True, the member is documented as an attribute isattr = False - if want_all and membername.startswith('_'): + doc = self.get_attr(member, '__doc__', None) + # if the member __doc__ is the same as self's __doc__, it's just + # inherited and therefore not the member's doc + cls = self.get_attr(member, '__class__', None) + if cls: + cls_doc = self.get_attr(cls, '__doc__', None) + if cls_doc == doc: + doc = None + has_doc = bool(doc) + + keep = False + if want_all and membername.startswith('__') and \ + membername.endswith('__') and len(membername) > 4: + # special __methods__ + if self.options.special_members and membername != '__doc__': + keep = has_doc or self.options.undoc_members + elif want_all and membername.startswith('_'): # ignore members whose name starts with _ by default - skip = True + keep = self.options.private_members and \ + (has_doc or self.options.undoc_members) elif (namespace, membername) in attr_docs: # keep documented attributes - skip = False + keep = True isattr = True else: - # ignore undocumented members if :undoc-members: - # is not given - doc = self.get_attr(member, '__doc__', None) - skip = not self.options.undoc_members and not doc + # ignore undocumented members if :undoc-members: is not given + keep = has_doc or self.options.undoc_members # give the user a chance to decide whether this member # should be skipped @@ -572,20 +589,20 @@ class Documenter(object): # let extensions preprocess docstrings skip_user = self.env.app.emit_firstresult( 'autodoc-skip-member', self.objtype, membername, member, - skip, self.options) + not keep, self.options) if skip_user is not None: - skip = skip_user - if skip: - continue + keep = not skip_user - ret.append((membername, member, isattr)) + if keep: + ret.append((membername, member, isattr)) return ret def document_members(self, all_members=False): - """ - Generate reST for member documentation. If *all_members* is True, - do all members, else those given by *self.options.members*. + """Generate reST for member documentation. + + If *all_members* is True, do all members, else those given by + *self.options.members*. """ # set current namespace for finding members self.env.temp_data['autodoc:module'] = self.modname @@ -643,8 +660,8 @@ class Documenter(object): def generate(self, more_content=None, real_modname=None, check_module=False, all_members=False): - """ - Generate reST for the object given by *self.name*, and possibly members. + """Generate reST for the object given by *self.name*, and possibly for + its members. If *more_content* is given, include that content. If *real_modname* is given, use that module name to find attribute docs. If *check_module* is @@ -727,6 +744,7 @@ class ModuleDocumenter(Documenter): 'show-inheritance': bool_option, 'synopsis': identity, 'platform': identity, 'deprecated': bool_option, 'member-order': identity, 'exclude-members': members_set_option, + 'private-members': bool_option, 'special-members': bool_option, } @classmethod @@ -833,7 +851,53 @@ class ClassLevelDocumenter(Documenter): return modname, parents + [base] -class FunctionDocumenter(ModuleLevelDocumenter): +class DocstringSignatureMixin(object): + """ + Mixin for FunctionDocumenter and MethodDocumenter to provide the + feature of reading the signature from the docstring. + """ + + def _find_signature(self, encoding=None): + docstrings = Documenter.get_doc(self, encoding, 2) + if len(docstrings) != 1: + return + doclines = docstrings[0] + setattr(self, '__new_doclines', doclines) + if not doclines: + return + # match first line of docstring against signature RE + match = py_ext_sig_re.match(doclines[0]) + if not match: + return + exmod, path, base, args, retann = match.groups() + # the base name must match ours + if not self.objpath or base != self.objpath[-1]: + return + # ok, now jump over remaining empty lines and set the remaining + # lines as the new doclines + i = 1 + while i < len(doclines) and not doclines[i].strip(): + i += 1 + setattr(self, '__new_doclines', doclines[i:]) + return args, retann + + def get_doc(self, encoding=None, ignore=1): + lines = getattr(self, '__new_doclines', None) + if lines is not None: + return [lines] + return Documenter.get_doc(self, encoding, ignore) + + 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: + self.args, self.retann = result + return Documenter.format_signature(self) + + +class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): """ Specialized Documenter subclass for functions. """ @@ -847,18 +911,18 @@ class FunctionDocumenter(ModuleLevelDocumenter): def format_args(self): if inspect.isbuiltin(self.object) or \ inspect.ismethoddescriptor(self.object): - # can never get arguments of a C function or method + # cannot introspect arguments of a C function or method return None try: - argspec = inspect.getargspec(self.object) + argspec = getargspec(self.object) except TypeError: # if a class should be documented as function (yay duck # typing) we try to use the constructor signature as function # signature without the first argument. try: - argspec = inspect.getargspec(self.object.__new__) + argspec = getargspec(self.object.__new__) except TypeError: - argspec = inspect.getargspec(self.object.__init__) + argspec = getargspec(self.object.__init__) if argspec[0]: del argspec[0][0] args = inspect.formatargspec(*argspec) @@ -881,11 +945,12 @@ class ClassDocumenter(ModuleLevelDocumenter): 'noindex': bool_option, 'inherited-members': bool_option, 'show-inheritance': bool_option, 'member-order': identity, 'exclude-members': members_set_option, + 'private-members': bool_option, 'special-members': bool_option, } @classmethod def can_document_member(cls, member, membername, isattr, parent): - return isinstance(member, (type, ClassType)) + return isinstance(member, class_types) def import_object(self): ret = ModuleLevelDocumenter.import_object(self) @@ -907,7 +972,7 @@ class ClassDocumenter(ModuleLevelDocumenter): (inspect.ismethod(initmeth) or inspect.isfunction(initmeth)): return None try: - argspec = inspect.getargspec(initmeth) + argspec = getargspec(initmeth) except TypeError: # still not possible: happens e.g. for old-style classes # with __init__ in C @@ -937,7 +1002,7 @@ class ClassDocumenter(ModuleLevelDocumenter): self.add_line(_(u' Bases: %s') % ', '.join(bases), '<autodoc>') - def get_doc(self, encoding=None): + def get_doc(self, encoding=None, ignore=1): content = self.env.config.autoclass_content docstrings = [] @@ -958,9 +1023,12 @@ class ClassDocumenter(ModuleLevelDocumenter): docstrings = [initdocstring] else: docstrings.append(initdocstring) - - return [prepare_docstring(force_decode(docstring, encoding)) - for docstring in docstrings] + doc = [] + for docstring in docstrings: + if not isinstance(docstring, unicode): + docstring = force_decode(docstring, encoding) + doc.append(prepare_docstring(docstring)) + return doc def add_content(self, more_content, no_docstring=False): if self.doc_as_attr: @@ -991,7 +1059,7 @@ class ExceptionDocumenter(ClassDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): - return isinstance(member, (type, ClassType)) and \ + return isinstance(member, class_types) and \ issubclass(member, base_exception) @@ -1001,16 +1069,26 @@ class DataDocumenter(ModuleLevelDocumenter): """ objtype = 'data' member_order = 40 + priority = -10 @classmethod def can_document_member(cls, member, membername, isattr, parent): return isinstance(parent, ModuleDocumenter) and isattr + def add_directive_header(self, sig): + ModuleLevelDocumenter.add_directive_header(self, sig) + try: + objrepr = safe_repr(self.object) + except ValueError: + pass + else: + self.add_line(u' :annotation: = ' + objrepr, '<autodoc>') + def document_members(self, all_members=False): pass -class MethodDocumenter(ClassLevelDocumenter): +class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): """ Specialized Documenter subclass for methods (normal, static and class). """ @@ -1023,31 +1101,45 @@ class MethodDocumenter(ClassLevelDocumenter): return inspect.isroutine(member) and \ not isinstance(parent, ModuleDocumenter) - def import_object(self): - ret = ClassLevelDocumenter.import_object(self) - 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' - return ret + if sys.version_info >= (3, 0): + def import_object(self): + ret = ClassLevelDocumenter.import_object(self) + 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 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' + return ret def format_args(self): if inspect.isbuiltin(self.object) or \ inspect.ismethoddescriptor(self.object): # can never get arguments of a C function or method return None - argspec = inspect.getargspec(self.object) + argspec = getargspec(self.object) if argspec[0] and argspec[0][0] in ('cls', 'self'): del argspec[0][0] return inspect.formatargspec(*argspec) @@ -1074,16 +1166,44 @@ class AttributeDocumenter(ClassLevelDocumenter): isdatadesc = isdescriptor(member) and not \ isinstance(member, cls.method_types) and not \ type(member).__name__ == "method_descriptor" - return isdatadesc or \ - (isattr and not isinstance(parent, ModuleDocumenter)) + return isdatadesc or (not isinstance(parent, ModuleDocumenter) + and not inspect.isroutine(member) + and not isinstance(member, class_types)) def document_members(self, all_members=False): pass + def import_object(self): + ret = ClassLevelDocumenter.import_object(self) + if isdescriptor(self.object) and \ + not isinstance(self.object, self.method_types): + self._datadescriptor = True + else: + # if it's not a data descriptor + self._datadescriptor = False + return ret + def get_real_modname(self): return self.get_attr(self.parent or self.object, '__module__', None) \ or self.modname + def add_directive_header(self, sig): + ClassLevelDocumenter.add_directive_header(self, sig) + if not self._datadescriptor: + try: + objrepr = safe_repr(self.object) + except ValueError: + pass + else: + self.add_line(u' :annotation: = ' + objrepr, '<autodoc>') + + def add_content(self, more_content, no_docstring=False): + if not self._datadescriptor: + # if it's not a data descriptor, its docstring is very probably the + # wrong thing to display + no_docstring = True + ClassLevelDocumenter.add_content(self, more_content, no_docstring) + class InstanceAttributeDocumenter(AttributeDocumenter): """ @@ -1106,6 +1226,7 @@ class InstanceAttributeDocumenter(AttributeDocumenter): """Never import anything.""" # disguise as an attribute self.objtype = 'attribute' + self._datadescriptor = False return True def add_content(self, more_content, no_docstring=False): @@ -1135,8 +1256,10 @@ class AutoDirective(Directive): _special_attrgetters = {} # flags that can be given in autodoc_default_flags - _default_flags = set(['members', 'undoc-members', 'inherited-members', - 'show-inheritance']) + _default_flags = set([ + 'members', 'undoc-members', 'inherited-members', 'show-inheritance', + 'private-members', 'special-members', + ]) # standard docutils directive settings has_content = True @@ -1226,6 +1349,17 @@ def setup(app): app.add_config_value('autoclass_content', 'class', True) 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_event('autodoc-process-docstring') app.add_event('autodoc-process-signature') app.add_event('autodoc-skip-member') + + +class testcls: + """test doc string""" + + def __getattr__(self, x): + return x + + def __setattr__(self, x, y): + """Attr setter.""" diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index a194fba8..0f5c2640 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -73,8 +73,7 @@ class autosummary_toc(nodes.comment): pass def process_autosummary_toc(app, doctree): - """ - Insert items described in autosummary:: to the TOC tree, but do + """Insert items described in autosummary:: to the TOC tree, but do not generate the toctree:: list. """ env = app.builder.env @@ -126,22 +125,39 @@ def autosummary_table_visit_html(self, node): # -- autodoc integration ------------------------------------------------------- -try: - ismemberdescriptor = inspect.ismemberdescriptor - isgetsetdescriptor = inspect.isgetsetdescriptor -except AttributeError: - def ismemberdescriptor(obj): - return False - isgetsetdescriptor = ismemberdescriptor +class FakeDirective: + env = {} + genopt = {} def get_documenter(obj, parent): + """Get an autodoc.Documenter class suitable for documenting the given + object. + + *obj* is the Python object to be documented, and *parent* is an + another Python object (e.g. a module or a class) to which *obj* + belongs to. """ - Get an autodoc.Documenter class suitable for documenting the given object - """ - from sphinx.ext.autodoc import AutoDirective, DataDocumenter + from sphinx.ext.autodoc import AutoDirective, DataDocumenter, \ + ModuleDocumenter + + if inspect.ismodule(obj): + # ModuleDocumenter.can_document_member always returns False + return ModuleDocumenter + # Construct a fake documenter for *parent* + if parent is not None: + parent_doc_cls = get_documenter(parent, None) + else: + parent_doc_cls = ModuleDocumenter + + if hasattr(parent, '__name__'): + parent_doc = parent_doc_cls(FakeDirective(), parent.__name__) + else: + parent_doc = parent_doc_cls(FakeDirective(), "") + + # Get the corrent documenter class for *obj* classes = [cls for cls in AutoDirective._registry.values() - if cls.can_document_member(obj, '', False, parent)] + if cls.can_document_member(obj, '', False, parent_doc)] if classes: classes.sort(key=lambda cls: cls.priority) return classes[-1] @@ -155,7 +171,7 @@ class Autosummary(Directive): """ Pretty table containing short signatures and summaries of functions etc. - autosummary also generates a (hidden) toctree:: node. + autosummary can also optionally generate a hidden toctree:: node. """ required_arguments = 0 @@ -210,16 +226,12 @@ class Autosummary(Directive): return self.warnings + nodes def get_items(self, names): - """ - Try to import the given names, and return a list of + """Try to import the given names, and return a list of ``[(name, signature, summary_string, real_name), ...]``. """ env = self.state.document.settings.env - prefixes = [''] - currmodule = env.temp_data.get('py:module') - if currmodule: - prefixes.insert(0, currmodule) + prefixes = get_import_prefixes_from_env(env) items = [] @@ -279,8 +291,7 @@ class Autosummary(Directive): return items def get_table(self, items): - """ - Generate a proper list of table nodes for autosummary:: directive. + """Generate a proper list of table nodes for autosummary:: directive. *items* is a list produced by :meth:`get_items`. """ @@ -325,13 +336,29 @@ class Autosummary(Directive): def mangle_signature(sig, max_chars=30): """Reformat a function signature to a more compact form.""" - sig = re.sub(r"^\((.*)\)$", r"\1", sig) + ", " - r = re.compile(r"(?P<name>[a-zA-Z0-9_*]+)(?P<default>=.*?)?, ") - items = r.findall(sig) + s = re.sub(r"^\((.*)\)$", r"\1", sig).strip() + + # Strip strings (which can contain things that confuse the code below) + s = re.sub(r"\\\\", "", s) + s = re.sub(r"\\'", "", s) + s = re.sub(r"'[^']*'", "", s) + + # Parse the signature to arguments + options + args = [] + opts = [] + + opt_re = re.compile(r"^(.*, |)([a-zA-Z0-9_*]+)=") + while s: + m = opt_re.search(s) + if not m: + # The rest are arguments + args = s.split(', ') + break - args = [name for name, default in items if not default] - opts = [name for name, default in items if default] + opts.insert(0, m.group(2)) + s = m.group(1)[:-2] + # Produce a more compact signature sig = limited_join(", ", args, max_chars=max_chars-2) if opts: if not sig: @@ -343,8 +370,7 @@ def mangle_signature(sig, max_chars=30): return u"(%s)" % sig def limited_join(sep, items, max_chars=30, overflow_marker="..."): - """ - Join a number of strings to one, limiting the length to *max_chars*. + """Join a number of strings to one, limiting the length to *max_chars*. If the string overflows this limit, replace the last fitting item by *overflow_marker*. @@ -368,9 +394,28 @@ def limited_join(sep, items, max_chars=30, overflow_marker="..."): # -- Importing items ----------------------------------------------------------- -def import_by_name(name, prefixes=[None]): +def get_import_prefixes_from_env(env): + """ + Obtain current Python import prefixes (for `import_by_name`) + from ``document.env`` """ - Import a Python object that has the given *name*, under one of the + prefixes = [None] + + currmodule = env.temp_data.get('py:module') + if currmodule: + prefixes.insert(0, currmodule) + + currclass = env.temp_data.get('py:class') + if currclass: + if currmodule: + prefixes.insert(0, currmodule + "." + currclass) + else: + prefixes.insert(0, currclass) + + return prefixes + +def import_by_name(name, prefixes=[None]): + """Import a Python object that has the given *name*, under one of the *prefixes*. The first name that succeeds is used. """ tried = [] @@ -431,8 +476,7 @@ def _import_by_name(name): def autolink_role(typ, rawtext, etext, lineno, inliner, options={}, content=[]): - """ - Smart linking role. + """Smart linking role. Expands to ':obj:`text`' if `text` is an object that can be imported; otherwise expands to '*text*'. @@ -442,8 +486,7 @@ def autolink_role(typ, rawtext, etext, lineno, inliner, 'obj', rawtext, etext, lineno, inliner, options, content) pnode = r[0][0] - prefixes = [None] - #prefixes.insert(0, inliner.document.settings.env.currmodule) + prefixes = get_import_prefixes_from_env(env) try: name, obj, parent = import_by_name(pnode['reftarget'], prefixes) except ImportError: @@ -483,12 +526,14 @@ def setup(app): html=(autosummary_toc_visit_html, autosummary_noop), latex=(autosummary_noop, autosummary_noop), text=(autosummary_noop, autosummary_noop), - man=(autosummary_noop, autosummary_noop)) + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) app.add_node(autosummary_table, html=(autosummary_table_visit_html, autosummary_noop), latex=(autosummary_noop, autosummary_noop), text=(autosummary_noop, autosummary_noop), - man=(autosummary_noop, autosummary_noop)) + man=(autosummary_noop, autosummary_noop), + texinfo=(autosummary_noop, autosummary_noop)) app.add_directive('autosummary', Autosummary) app.add_role('autolink', autolink_role) app.connect('doctree-read', process_autosummary_toc) diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index b5ff3f4c..089d181f 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -12,11 +12,12 @@ Example Makefile rule:: generate: - sphinx-autogen source/*.rst source/generated + sphinx-autogen -o source/generated source/*.rst :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + import os import re import sys @@ -193,8 +194,8 @@ def generate_autosummary_docs(sources, output_dir=None, suffix='.rst', # -- Finding documented entries in files --------------------------------------- def find_autosummary_in_files(filenames): - """ - Find out what items are documented in source/*.rst. + """Find out what items are documented in source/*.rst. + See `find_autosummary_in_lines`. """ documented = [] @@ -206,8 +207,8 @@ def find_autosummary_in_files(filenames): return documented def find_autosummary_in_docstring(name, module=None, filename=None): - """ - Find out what items are documented in the given object's docstring. + """Find out what items are documented in the given object's docstring. + See `find_autosummary_in_lines`. """ try: @@ -221,8 +222,8 @@ def find_autosummary_in_docstring(name, module=None, filename=None): return [] def find_autosummary_in_lines(lines, module=None, filename=None): - """ - Find out what items appear in autosummary:: directives in the given lines. + """Find out what items appear in autosummary:: directives in the + given lines. Returns a list of (name, toctree, template) where *name* is a name of an object and *toctree* the :toctree: path of the corresponding @@ -231,7 +232,7 @@ def find_autosummary_in_lines(lines, module=None, filename=None): *template* ``None`` if the directive does not have the corresponding options set. """ - autosummary_re = re.compile(r'^\s*\.\.\s+autosummary::\s*') + autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*') automodule_re = re.compile( r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$') module_re = re.compile( @@ -246,6 +247,7 @@ def find_autosummary_in_lines(lines, module=None, filename=None): template = None current_module = module in_autosummary = False + base_indent = "" for line in lines: if in_autosummary: @@ -276,7 +278,7 @@ def find_autosummary_in_lines(lines, module=None, filename=None): documented.append((name, toctree, template)) continue - if not line.strip(): + if not line.strip() or line.startswith(base_indent + " "): continue in_autosummary = False @@ -284,6 +286,7 @@ def find_autosummary_in_lines(lines, module=None, filename=None): m = autosummary_re.match(line) if m: in_autosummary = True + base_indent = m.group(1) toctree = None template = None continue diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index cfd4265e..af1b4026 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -105,7 +105,8 @@ class CoverageBuilder(Builder): output_file = path.join(self.outdir, 'c.txt') op = open(output_file, 'w') try: - write_header(op, 'Undocumented C API elements', '=') + if self.config.coverage_write_headline: + write_header(op, 'Undocumented C API elements', '=') op.write('\n') for filename, undoc in self.c_undoc.iteritems(): @@ -120,6 +121,8 @@ class CoverageBuilder(Builder): objects = self.env.domaindata['py']['objects'] modules = self.env.domaindata['py']['modules'] + skip_undoc = self.config.coverage_skip_undoc_in_source + for mod_name in modules: ignore = False for exp in self.mod_ignorexps: @@ -160,6 +163,8 @@ class CoverageBuilder(Builder): if exp.match(name): break else: + if skip_undoc and not obj.__doc__: + continue funcs.append(name) elif inspect.isclass(obj): for exp in self.cls_ignorexps: @@ -167,17 +172,27 @@ class CoverageBuilder(Builder): break else: if full_name not in objects: + if skip_undoc and not obj.__doc__: + continue # not documented at all classes[name] = [] continue attrs = [] - for attr_name, attr in inspect.getmembers( - obj, inspect.ismethod): + for attr_name in dir(obj): + if attr_name not in obj.__dict__: + continue + attr = getattr(obj, attr_name) + if not (inspect.ismethod(attr) or + inspect.isfunction(attr)): + continue if attr_name[0] == '_': # starts with an underscore, ignore it continue + if skip_undoc and not attr.__doc__: + # skip methods without docstring if wished + continue full_attr_name = '%s.%s' % (full_name, attr_name) if full_attr_name not in objects: @@ -194,8 +209,8 @@ class CoverageBuilder(Builder): op = open(output_file, 'w') failed = [] try: - write_header(op, 'Undocumented Python objects', '=') - + if self.config.coverage_write_headline: + write_header(op, 'Undocumented Python objects', '=') keys = self.py_undoc.keys() keys.sort() for name in keys: @@ -217,7 +232,7 @@ class CoverageBuilder(Builder): if not methods: op.write(' * %s\n' % name) else: - op.write(' * %s -- missing methods:\n' % name) + op.write(' * %s -- missing methods:\n\n' % name) op.writelines(' - %s\n' % x for x in methods) op.write('\n') @@ -245,3 +260,5 @@ def setup(app): app.add_config_value('coverage_c_path', [], False) app.add_config_value('coverage_c_regexes', {}, False) 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) diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index dcee09f5..2952388f 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -56,7 +56,7 @@ class TestDirective(Directive): test = code code = doctestopt_re.sub('', code) nodetype = nodes.literal_block - if self.name == 'testsetup' or 'hide' in self.options: + if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options: nodetype = nodes.comment if self.arguments: groups = [x.strip() for x in self.arguments[0].split(',')] @@ -86,6 +86,9 @@ class TestDirective(Directive): class TestsetupDirective(TestDirective): option_spec = {} +class TestcleanupDirective(TestDirective): + option_spec = {} + class DoctestDirective(TestDirective): option_spec = { 'hide': directives.flag, @@ -113,6 +116,7 @@ class TestGroup(object): self.name = name self.setup = [] self.tests = [] + self.cleanup = [] def add_code(self, code, prepend=False): if code.type == 'testsetup': @@ -120,6 +124,8 @@ class TestGroup(object): self.setup.insert(0, code) else: self.setup.append(code) + elif code.type == 'testcleanup': + self.cleanup.append(code) elif code.type == 'doctest': self.tests.append([code]) elif code.type == 'testcode': @@ -131,8 +137,8 @@ class TestGroup(object): raise RuntimeError('invalid TestCode type') def __repr__(self): - return 'TestGroup(name=%r, setup=%r, tests=%r)' % ( - self.name, self.setup, self.tests) + return 'TestGroup(name=%r, setup=%r, cleanup=%r, tests=%r)' % ( + self.name, self.setup, self.cleanup, self.tests) class TestCode(object): @@ -149,14 +155,14 @@ class TestCode(object): class SphinxDocTestRunner(doctest.DocTestRunner): def summarize(self, out, verbose=None): - io = StringIO.StringIO() + string_io = StringIO.StringIO() old_stdout = sys.stdout - sys.stdout = io + sys.stdout = string_io try: res = doctest.DocTestRunner.summarize(self, verbose) finally: sys.stdout = old_stdout - out(io.getvalue()) + out(string_io.getvalue()) return res def _DocTestRunner__patched_linecache_getlines(self, filename, @@ -204,6 +210,8 @@ class DocTestBuilder(Builder): self.total_tries = 0 self.setup_failures = 0 self.setup_tries = 0 + self.cleanup_failures = 0 + self.cleanup_tries = 0 date = time.strftime('%Y-%m-%d %H:%M:%S') @@ -240,12 +248,14 @@ Doctest summary %5d test%s %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.setup_failures, s(self.setup_failures), + self.cleanup_failures, s(self.cleanup_failures))) self.outfile.close() - if self.total_failures or self.setup_failures: + if self.total_failures or self.setup_failures or self.cleanup_failures: self.app.statuscode = 1 def write(self, build_docnames, updated_docnames, method='update'): @@ -265,6 +275,12 @@ Doctest summary optionflags=self.opt) self.test_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt) + self.cleanup_runner = SphinxDocTestRunner(verbose=False, + optionflags=self.opt) + + self.test_runner._fakeout = self.setup_runner._fakeout + self.cleanup_runner._fakeout = self.setup_runner._fakeout + if self.config.doctest_test_doctest_blocks: def condition(node): return (isinstance(node, (nodes.literal_block, nodes.comment)) @@ -298,6 +314,11 @@ Doctest summary 'testsetup', lineno=0) for group in groups.itervalues(): 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(): + group.add_code(code) if not groups: return @@ -313,29 +334,43 @@ Doctest summary res_f, res_t = self.test_runner.summarize(self._out, verbose=True) self.total_failures += res_f self.total_tries += res_t + if self.cleanup_runner.tries: + res_f, res_t = self.cleanup_runner.summarize(self._out, + verbose=True) + self.cleanup_failures += res_f + self.cleanup_tries += res_t def compile(self, code, name, type, flags, dont_inherit): return compile(code, name, self.type, flags, dont_inherit) def test_group(self, group, filename): ns = {} - setup_examples = [] - for setup in group.setup: - setup_examples.append(doctest.Example(setup.code, '', - lineno=setup.lineno)) - if setup_examples: - # simulate a doctest with the setup code - setup_doctest = doctest.DocTest(setup_examples, {}, - '%s (setup code)' % group.name, - filename, 0, None) - setup_doctest.globs = ns - old_f = self.setup_runner.failures + + def run_setup_cleanup(runner, testcodes, what): + examples = [] + for testcode in testcodes: + examples.append(doctest.Example(testcode.code, '', + lineno=testcode.lineno)) + if not examples: + return True + # simulate a doctest with the code + sim_doctest = doctest.DocTest(examples, {}, + '%s (%s code)' % (group.name, what), + filename, 0, None) + sim_doctest.globs = ns + old_f = runner.failures self.type = 'exec' # the snippet may contain multiple statements - self.setup_runner.run(setup_doctest, out=self._warn_out, - clear_globs=False) - if self.setup_runner.failures > old_f: - # don't run the group - return + runner.run(sim_doctest, out=self._warn_out, clear_globs=False) + if runner.failures > old_f: + return False + return True + + # run the setup code + if not run_setup_cleanup(self.setup_runner, group.setup, 'setup'): + # if setup failed, don't run the group + return + + # run the tests for code in group.tests: if len(code) == 1: # ordinary doctests (code/output interleaved) @@ -373,9 +408,13 @@ Doctest summary # also don't clear the globs namespace after running the doctest self.test_runner.run(test, out=self._warn_out, clear_globs=False) + # run the cleanup + run_setup_cleanup(self.cleanup_runner, group.cleanup, 'cleanup') + def setup(app): app.add_directive('testsetup', TestsetupDirective) + app.add_directive('testcleanup', TestcleanupDirective) app.add_directive('doctest', DoctestDirective) app.add_directive('testcode', TestcodeDirective) app.add_directive('testoutput', TestoutputDirective) @@ -384,3 +423,4 @@ def setup(app): app.add_config_value('doctest_path', [], False) 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) diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index bf62c063..ee935945 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.py @@ -11,6 +11,7 @@ """ import re +import codecs import posixpath from os import path from math import ceil @@ -25,12 +26,11 @@ from docutils.parsers.rst import directives from sphinx.errors import SphinxError from sphinx.locale import _ -from sphinx.util.osutil import ensuredir, ENOENT, EPIPE +from sphinx.util.osutil import ensuredir, ENOENT, EPIPE, EINVAL from sphinx.util.compat import Directive mapname_re = re.compile(r'<map id="(.*?)"') -svg_dim_re = re.compile(r'<svg\swidth="(\d+)pt"\sheight="(\d+)pt"', re.M) class GraphvizError(SphinxError): @@ -47,23 +47,48 @@ class Graphviz(Directive): """ has_content = True required_arguments = 0 - optional_arguments = 0 + optional_arguments = 1 final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, + 'inline': directives.flag, + 'caption': directives.unchanged, } def run(self): - dotcode = '\n'.join(self.content) - if not dotcode.strip(): - return [self.state_machine.reporter.warning( - 'Ignoring "graphviz" directive without content.', - line=self.lineno)] + if self.arguments: + document = self.state.document + if self.content: + return [document.reporter.warning( + 'Graphviz directive cannot have both content and ' + 'a filename argument', line=self.lineno)] + env = self.state.document.settings.env + rel_filename, filename = env.relfn2path(self.arguments[0]) + env.note_dependency(rel_filename) + try: + fp = codecs.open(filename, 'r', 'utf-8') + try: + dotcode = fp.read() + finally: + fp.close() + except (IOError, OSError): + return [document.reporter.warning( + 'External Graphviz file %r not found or reading ' + 'it failed' % filename, line=self.lineno)] + else: + dotcode = '\n'.join(self.content) + if not dotcode.strip(): + return [self.state_machine.reporter.warning( + 'Ignoring "graphviz" directive without content.', + line=self.lineno)] node = graphviz() node['code'] = dotcode 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 return [node] @@ -77,6 +102,8 @@ class GraphvizSimple(Directive): final_argument_whitespace = False option_spec = { 'alt': directives.unchanged, + 'inline': directives.flag, + 'caption': directives.unchanged, } def run(self): @@ -86,14 +113,16 @@ 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 return [node] def render_dot(self, code, options, format, prefix='graphviz'): - """ - Render graphviz code into a PNG or PDF output file. - """ + """Render graphviz code into a PNG or PDF output file.""" hashkey = code.encode('utf-8') + str(options) + \ + str(self.builder.config.graphviz_dot) + \ str(self.builder.config.graphviz_dot_args) fname = '%s-%s.%s' % (prefix, sha(hashkey).hexdigest(), format) if hasattr(self.builder, 'imgpath'): @@ -134,6 +163,7 @@ def render_dot(self, code, options, format, prefix='graphviz'): self.builder.config.graphviz_dot) self.builder._graphviz_warned_dot = True return None, None + wentWrong = False try: # Graphviz may close standard input when an error occurs, # resulting in a broken pipe on communicate() @@ -141,6 +171,12 @@ def render_dot(self, code, options, format, prefix='graphviz'): except (OSError, IOError), err: if err.errno != EPIPE: raise + wentWrong = True + except IOError, err: + if err.errno != EINVAL: + raise + wentWrong = True + if wentWrong: # in this case, read the standard output and standard error streams # directly, to get the error message(s) stdout, stderr = p.stdout.read(), p.stderr.read() @@ -151,37 +187,6 @@ def render_dot(self, code, options, format, prefix='graphviz'): return relfn, outfn -def get_svg_tag(svgref, svgfile, imgcls=None): - # Webkit can't figure out svg dimensions when using object tag - # so we need to get it from the svg file - fp = open(svgfile, 'r') - try: - for line in fp: - match = svg_dim_re.match(line) - if match: - dimensions = match.groups() - break - else: - dimensions = None - finally: - fp.close() - - # We need this hack to make WebKit show our object tag properly - def pt2px(x): - return int(ceil((96.0/72.0) * float(x))) - - if dimensions: - style = ' width="%s" height="%s"' % tuple(map(pt2px, dimensions)) - else: - style = '' - - # The object tag works fine on Firefox and WebKit - # Besides it's a hack, this strategy does not mess with templates. - imgcss = imgcls and ' class="%s"' % imgcls or '' - return '<object type="image/svg+xml" data="%s"%s%s></object>\n' % \ - (svgref, imgcss, style) - - def render_dot_html(self, node, code, options, prefix='graphviz', imgcls=None, alt=None): format = self.builder.config.graphviz_output_format @@ -194,14 +199,21 @@ def render_dot_html(self, node, code, options, prefix='graphviz', self.builder.warn('dot code %r: ' % code + str(exc)) raise nodes.SkipNode - self.body.append(self.starttag(node, 'p', CLASS='graphviz')) + inline = node.get('inline', False) + if inline: + wrapper = 'span' + else: + wrapper = 'p' + + self.body.append(self.starttag(node, wrapper, CLASS='graphviz')) if fname is None: self.body.append(self.encode(code)) else: if alt is None: alt = node.get('alt', self.encode(code).strip()) + imgcss = imgcls and 'class="%s"' % imgcls or '' if format == 'svg': - svgtag = get_svg_tag(fname, outfn, imgcls) + svgtag = '<img src="%s" alt="%s" %s/>\n' % (fname, alt, imgcss) self.body.append(svgtag) else: mapfile = open(outfn + '.map', 'rb') @@ -209,7 +221,6 @@ def render_dot_html(self, node, code, options, prefix='graphviz', imgmap = mapfile.readlines() finally: mapfile.close() - imgcss = imgcls and 'class="%s"' % imgcls or '' if len(imgmap) == 2: # nothing in image map (the lines are <map> and </map>) self.body.append('<img src="%s" alt="%s" %s/>\n' % @@ -220,8 +231,11 @@ 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(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('</p>\n') + self.body.append('</%s>\n' % wrapper) raise nodes.SkipNode @@ -236,8 +250,25 @@ def render_dot_latex(self, node, code, options, prefix='graphviz'): self.builder.warn('dot code %r: ' % code + str(exc)) raise nodes.SkipNode + inline = node.get('inline', False) + if inline: + para_separator = '' + else: + para_separator = '\n' + if fname is not None: - self.body.append('\n\\includegraphics{%s}\n' % fname) + 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)) raise nodes.SkipNode @@ -245,11 +276,29 @@ def latex_visit_graphviz(self, node): render_dot_latex(self, node, node['code'], node['options']) +def render_dot_texinfo(self, node, code, options, prefix='graphviz'): + try: + fname, outfn = render_dot(self, code, options, 'png', prefix) + except GraphvizError, 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]) + raise nodes.SkipNode + +def texinfo_visit_graphviz(self, node): + render_dot_texinfo(self, node, node['code'], node['options']) + + def text_visit_graphviz(self, node): if 'alt' in node.attributes: self.add_text(_('[graph: %s]') % node['alt']) self.add_text(_('[graph]')) - raise nodes.SkipNode def man_visit_graphviz(self, node): @@ -263,6 +312,7 @@ def setup(app): app.add_node(graphviz, html=(html_visit_graphviz, None), latex=(latex_visit_graphviz, None), + texinfo=(texinfo_visit_graphviz, None), text=(text_visit_graphviz, None), man=(man_visit_graphviz, None)) app.add_directive('graphviz', Graphviz) diff --git a/sphinx/ext/inheritance_diagram.py b/sphinx/ext/inheritance_diagram.py index 1b49720f..a2490486 100644 --- a/sphinx/ext/inheritance_diagram.py +++ b/sphinx/ext/inheritance_diagram.py @@ -47,7 +47,8 @@ except ImportError: from docutils import nodes from docutils.parsers.rst import directives -from sphinx.ext.graphviz import render_dot_html, render_dot_latex +from sphinx.ext.graphviz import render_dot_html, render_dot_latex, \ + render_dot_texinfo from sphinx.util.compat import Directive @@ -66,24 +67,23 @@ class InheritanceGraph(object): from all the way to the root "object", and then is able to generate a graphviz dot graph from them. """ - def __init__(self, class_names, currmodule, show_builtins=False, parts=0): - """ - *class_names* is a list of child classes to show bases from. + def __init__(self, class_names, currmodule, show_builtins=False, + private_bases=False, parts=0): + """*class_names* is a list of child classes to show bases from. If *show_builtins* is True, then Python builtins will be shown in the graph. """ self.class_names = class_names classes = self._import_classes(class_names, currmodule) - self.class_info = self._class_info(classes, show_builtins, parts) + self.class_info = self._class_info(classes, show_builtins, + private_bases, parts) if not self.class_info: raise InheritanceException('No classes found for ' 'inheritance diagram') def _import_class_or_module(self, name, currmodule): - """ - Import a class using its fully-qualified *name*. - """ + """Import a class using its fully-qualified *name*.""" try: path, base = class_sig_re.match(name).groups() except (AttributeError, ValueError): @@ -134,7 +134,7 @@ class InheritanceGraph(object): classes.extend(self._import_class_or_module(name, currmodule)) return classes - def _class_info(self, classes, show_builtins, parts): + def _class_info(self, classes, show_builtins, private_bases, parts): """Return name and bases for all classes that are ancestors of *classes*. @@ -147,6 +147,8 @@ class InheritanceGraph(object): def recurse(cls): if not show_builtins and cls in builtins: return + if not private_bases and cls.__name__.startswith('_'): + return nodename = self.class_name(cls, parts) fullname = self.class_name(cls, 0) @@ -156,6 +158,8 @@ class InheritanceGraph(object): for base in cls.__bases__: if not show_builtins and base in builtins: continue + if not private_bases and base.__name__.startswith('_'): + continue baselist.append(self.class_name(base, parts)) if base not in all_classes: recurse(base) @@ -182,9 +186,7 @@ class InheritanceGraph(object): return '.'.join(name_parts[-parts:]) def get_all_class_names(self): - """ - Get all of the class names involved in the graph. - """ + """Get all of the class names involved in the graph.""" return [fullname for (_, fullname, _) in self.class_info] # These are the default attrs for graphviz @@ -213,9 +215,8 @@ class InheritanceGraph(object): def generate_dot(self, name, urls={}, env=None, graph_attrs={}, node_attrs={}, edge_attrs={}): - """ - Generate a graphviz dot graph from the classes that - were passed in to __init__. + """Generate a graphviz dot graph from the classes that were passed in + to __init__. *name* is the name of the graph. @@ -274,6 +275,7 @@ class InheritanceDiagram(Directive): final_argument_whitespace = True option_spec = { 'parts': directives.nonnegative_int, + 'private-bases': directives.flag, } def run(self): @@ -290,7 +292,8 @@ class InheritanceDiagram(Directive): try: graph = InheritanceGraph( class_names, env.temp_data.get('py:module'), - parts=node['parts']) + parts=node['parts'], + private_bases='private-bases' in self.options) except InheritanceException, err: return [node.document.reporter.warning(err.args[0], line=self.lineno)] @@ -352,6 +355,21 @@ def latex_visit_inheritance_diagram(self, node): raise nodes.SkipNode +def texinfo_visit_inheritance_diagram(self, node): + """ + Output the graph for Texinfo. This will insert a PNG. + """ + graph = node['graph'] + + graph_hash = get_graph_hash(node) + name = 'inheritance%s' % graph_hash + + dotcode = graph.generate_dot(name, env=self.builder.env, + graph_attrs={'size': '"6.0,6.0"'}) + render_dot_texinfo(self, node, dotcode, [], 'inheritance') + raise nodes.SkipNode + + def skip(self, node): raise nodes.SkipNode @@ -363,7 +381,8 @@ def setup(app): latex=(latex_visit_inheritance_diagram, None), html=(html_visit_inheritance_diagram, None), text=(skip, None), - man=(skip, None)) + man=(skip, None), + texinfo=(texinfo_visit_inheritance_diagram, None)) app.add_directive('inheritance-diagram', InheritanceDiagram) app.add_config_value('inheritance_graph_attrs', {}, False), app.add_config_value('inheritance_node_attrs', {}, False), diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 578dddee..9bfd53fd 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -26,6 +26,7 @@ import time import zlib +import codecs import urllib2 import posixpath from os import path @@ -33,19 +34,26 @@ from os import path from docutils import nodes from sphinx.builders.html import INVENTORY_FILENAME +from sphinx.util.pycompat import b + handlers = [urllib2.ProxyHandler(), urllib2.HTTPRedirectHandler(), urllib2.HTTPHandler()] -if hasattr(urllib2, 'HTTPSHandler'): +try: handlers.append(urllib2.HTTPSHandler) +except NameError: + pass urllib2.install_opener(urllib2.build_opener(*handlers)) +UTF8StreamReader = codecs.lookup('utf-8')[2] + def read_inventory_v1(f, uri, join): + f = UTF8StreamReader(f) invdata = {} line = f.next() - projname = line.rstrip()[11:].decode('utf-8') + projname = line.rstrip()[11:] line = f.next() version = line.rstrip()[11:] for line in f: @@ -68,25 +76,25 @@ def read_inventory_v2(f, uri, join, bufsize=16*1024): projname = line.rstrip()[11:].decode('utf-8') line = f.readline() version = line.rstrip()[11:].decode('utf-8') - line = f.readline() + line = f.readline().decode('utf-8') if 'zlib' not in line: raise ValueError def read_chunks(): decompressor = zlib.decompressobj() - for chunk in iter(lambda: f.read(bufsize), ''): + for chunk in iter(lambda: f.read(bufsize), b('')): yield decompressor.decompress(chunk) yield decompressor.flush() def split_lines(iter): - buf = '' + buf = b('') for chunk in iter: buf += chunk - lineend = buf.find('\n') + lineend = buf.find(b('\n')) while lineend != -1: yield buf[:lineend].decode('utf-8') buf = buf[lineend+1:] - lineend = buf.find('\n') + lineend = buf.find(b('\n')) assert not buf for line in split_lines(read_chunks()): @@ -115,7 +123,7 @@ def fetch_inventory(app, uri, inv): '%s: %s' % (inv, err.__class__, err)) return try: - line = f.readline().rstrip() + line = f.readline().rstrip().decode('utf-8') try: if line == '# Sphinx inventory version 1': invdata = read_inventory_v1(f, uri, join) @@ -150,7 +158,7 @@ def load_mappings(app): # new format name, (uri, inv) = key, value if not name.isalnum(): - env.warn('intersphinx identifier %r is not alphanumeric' % name) + env.warn(docname=None, msg='intersphinx identifier %r is not alphanumeric' % name) else: # old format, no name name, uri, inv = None, key, value diff --git a/sphinx/ext/mathbase.py b/sphinx/ext/mathbase.py index e7ea82d7..1a2ca6af 100644 --- a/sphinx/ext/mathbase.py +++ b/sphinx/ext/mathbase.py @@ -56,6 +56,7 @@ class MathDirective(Directive): final_argument_whitespace = True option_spec = { 'label': directives.unchanged, + 'name': directives.unchanged, 'nowrap': directives.flag, } @@ -65,7 +66,9 @@ class MathDirective(Directive): latex = self.arguments[0] + '\n\n' + latex node = displaymath() node['latex'] = latex - node['label'] = self.options.get('label', None) + node['label'] = self.options.get('name', None) + if node['label'] is None: + node['label'] = self.options.get('label', None) node['nowrap'] = 'nowrap' in self.options node['docname'] = self.state.document.settings.env.docname ret = [node] @@ -125,6 +128,24 @@ def man_visit_eqref(self, node): raise nodes.SkipNode +def texinfo_visit_math(self, node): + self.body.append('@math{' + self.escape_arg(node['latex']) + '}') + raise nodes.SkipNode + +def texinfo_visit_displaymath(self, node): + if node.get('label'): + self.add_anchor(node['label'], node) + self.body.append('\n\n@example\n%s\n@end example\n\n' % + self.escape_arg(node['latex'])) +def texinfo_depart_displaymath(self, node): + pass + +def texinfo_visit_eqref(self, node): + self.add_xref(node['docname'] + ':' + node['target'], + node['target'], node) + raise nodes.SkipNode + + def html_visit_eqref(self, node): self.body.append('<a href="#equation-%s">' % node['target']) @@ -151,20 +172,23 @@ def number_equations(app, doctree, docname): def setup_math(app, htmlinlinevisitors, htmldisplayvisitors): app.add_node(math, - latex=(latex_visit_math, None), - text=(text_visit_math, None), - man=(man_visit_math, None), - html=htmlinlinevisitors) + latex=(latex_visit_math, None), + text=(text_visit_math, None), + man=(man_visit_math, None), + texinfo=(texinfo_visit_math, None), + html=htmlinlinevisitors) app.add_node(displaymath, - latex=(latex_visit_displaymath, None), - text=(text_visit_displaymath, None), - man=(man_visit_displaymath, man_depart_displaymath), - html=htmldisplayvisitors) + latex=(latex_visit_displaymath, None), + text=(text_visit_displaymath, None), + man=(man_visit_displaymath, man_depart_displaymath), + texinfo=(texinfo_visit_displaymath, texinfo_depart_displaymath), + html=htmldisplayvisitors) app.add_node(eqref, - latex=(latex_visit_eqref, None), - text=(text_visit_eqref, None), - man=(man_visit_eqref, None), - html=(html_visit_eqref, html_depart_eqref)) + latex=(latex_visit_eqref, None), + text=(text_visit_eqref, None), + man=(man_visit_eqref, None), + texinfo=(texinfo_visit_eqref, None), + html=(html_visit_eqref, html_depart_eqref)) app.add_role('math', math_role) app.add_role('eq', eq_role) app.add_directive('math', MathDirective) diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py new file mode 100644 index 00000000..7a552364 --- /dev/null +++ b/sphinx/ext/mathjax.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.mathjax + ~~~~~~~~~~~~~~~~~~ + + Allow `MathJax <http://mathjax.org/>`_ to be used to display math in + Sphinx's HTML writer -- requires the MathJax JavaScript library on your + webserver/computer. + + :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from docutils import nodes + +from sphinx.application import ExtensionError +from sphinx.ext.mathbase import setup_math as mathbase_setup + + +def html_visit_math(self, node): + self.body.append(self.starttag(node, 'span', '', CLASS='math')) + self.body.append(self.builder.config.mathjax_inline[0] + + self.encode(node['latex']) + + self.builder.config.mathjax_inline[1] + '</span>') + raise nodes.SkipNode + +def html_visit_displaymath(self, node): + self.body.append(self.starttag(node, 'div', CLASS='math')) + if node['nowrap']: + self.body.append(self.builder.config.mathjax_display[0] + + node['latex'] + + self.builder.config.mathjax_display[1]) + self.body.append('</div>') + raise nodes.SkipNode + + parts = [prt for prt in node['latex'].split('\n\n') if prt.strip()] + for i, part in enumerate(parts): + part = self.encode(part) + if i == 0: + # necessary to e.g. set the id property correctly + if node['number']: + self.body.append('<span class="eqno">(%s)</span>' % + node['number']) + if '&' in part or '\\\\' in part: + self.body.append(self.builder.config.mathjax_display[0] + + '\\begin{split}' + part + '\\end{split}' + + self.builder.config.mathjax_display[1]) + else: + self.body.append(self.builder.config.mathjax_display[0] + part + + self.builder.config.mathjax_display[1]) + self.body.append('</div>\n') + raise nodes.SkipNode + +def builder_inited(app): + if not app.config.mathjax_path: + raise ExtensionError('mathjax_path config value must be set for the ' + 'mathjax extension to work') + app.add_javascript(app.config.mathjax_path) + + +def setup(app): + mathbase_setup(app, (html_visit_math, None), (html_visit_displaymath, None)) + app.add_config_value('mathjax_path', + 'http://cdn.mathjax.org/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) diff --git a/sphinx/ext/oldcmarkup.py b/sphinx/ext/oldcmarkup.py index 2bf9b65d..41969c36 100644 --- a/sphinx/ext/oldcmarkup.py +++ b/sphinx/ext/oldcmarkup.py @@ -18,6 +18,7 @@ WARNING_MSG = 'using old C markup; please migrate to new-style markup ' \ '(e.g. c:function instead of cfunction), see ' \ 'http://sphinx.pocoo.org/domains.html' + class OldCDirective(Directive): has_content = True required_arguments = 1 diff --git a/sphinx/ext/pngmath.py b/sphinx/ext/pngmath.py index 6fdfbfe7..78c331a6 100644 --- a/sphinx/ext/pngmath.py +++ b/sphinx/ext/pngmath.py @@ -26,6 +26,7 @@ from docutils import nodes from sphinx.errors import SphinxError from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.osutil import ensuredir, ENOENT +from sphinx.util.pycompat import b from sphinx.ext.mathbase import setup_math as mathbase_setup, wrap_displaymath class MathExtError(SphinxError): @@ -58,11 +59,10 @@ DOC_BODY_PREVIEW = r''' \end{document} ''' -depth_re = re.compile(r'\[\d+ depth=(-?\d+)\]') +depth_re = re.compile(b(r'\[\d+ depth=(-?\d+)\]')) def render_math(self, math): - """ - Render the LaTeX math expression *math* using latex and dvipng. + """Render the LaTeX math expression *math* using latex and dvipng. Return the filename relative to the built document and the "depth", that is, the distance of image bottom and baseline in pixels, if the @@ -178,6 +178,11 @@ def cleanup_tempdir(app, exc): except Exception: pass +def get_tooltip(self, node): + if self.builder.config.pngmath_add_tooltips: + return ' alt="%s"' % self.encode(node['latex']).strip() + return '' + def html_visit_math(self, node): try: fname, depth = render_math(self, '$'+node['latex']+'$') @@ -193,15 +198,10 @@ def html_visit_math(self, node): self.body.append('<span class="math">%s</span>' % self.encode(node['latex']).strip()) else: - if depth is None: - self.body.append( - '<img class="math" src="%s" alt="%s"/>' % - (fname, self.encode(node['latex']).strip())) - else: - self.body.append( - '<img class="math" src="%s" alt="%s" ' - 'style="vertical-align: %dpx"/>' % - (fname, self.encode(node['latex']).strip(), -depth)) + c = ('<img class="math" src="%s"' % fname) + get_tooltip(self, node) + if depth is not None: + c += ' style="vertical-align: %dpx"' % (-depth) + self.body.append(c + '/>') raise nodes.SkipNode def html_visit_displaymath(self, node): @@ -226,8 +226,8 @@ def html_visit_displaymath(self, node): self.body.append('<span class="math">%s</span></p>\n</div>' % self.encode(node['latex']).strip()) else: - self.body.append('<img src="%s" alt="%s" /></p>\n</div>' % - (fname, self.encode(node['latex']).strip())) + self.body.append(('<img src="%s"' % fname) + get_tooltip(self, node) + + '/></p>\n</div>') raise nodes.SkipNode @@ -240,4 +240,5 @@ def setup(app): ['-gamma 1.5', '-D 110'], 'html') app.add_config_value('pngmath_latex_args', [], 'html') app.add_config_value('pngmath_latex_preamble', '', 'html') + app.add_config_value('pngmath_add_tooltips', True, 'html') app.connect('build-finished', cleanup_tempdir) diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 2ba9d5e1..6a44a9b4 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -159,7 +159,8 @@ def setup(app): html=(visit_todo_node, depart_todo_node), latex=(visit_todo_node, depart_todo_node), text=(visit_todo_node, depart_todo_node), - man=(visit_todo_node, depart_todo_node)) + man=(visit_todo_node, depart_todo_node), + texinfo=(visit_todo_node, depart_todo_node)) app.add_directive('todo', Todo) app.add_directive('todolist', TodoList) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 4994f125..32840f30 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -31,7 +31,11 @@ def doctree_read(app, doctree): env._viewcode_modules[modname] = False return analyzer.find_tags() - entry = analyzer.code.decode(analyzer.encoding), analyzer.tags, {} + if not isinstance(analyzer.code, unicode): + code = analyzer.code.decode(analyzer.encoding) + else: + code = analyzer.code + entry = code, analyzer.tags, {} env._viewcode_modules[modname] = entry elif entry is False: return @@ -94,7 +98,7 @@ def collect_pages(app): # construct a page name for the highlighted source pagename = '_modules/' + modname.replace('.', '/') # highlight the source using the builder's highlighter - highlighted = highlighter.highlight_block(code, 'python', False) + highlighted = highlighter.highlight_block(code, 'python', linenos=False) # split the code into lines lines = highlighted.splitlines() # split off wrap markup from the first line of the actual code |
