From 044d42953db35999f68ff8d3f6f8a60d76cbc980 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 18 Feb 2009 19:55:57 +0100 Subject: [PATCH] Convert builtin directives to classes. Refactor desc_directive so that it is easier to subclass. --- sphinx/application.py | 7 +- sphinx/directives/code.py | 268 +++++---- sphinx/directives/desc.py | 1132 +++++++++++++++++++----------------- sphinx/directives/other.py | 820 ++++++++++++++------------ 4 files changed, 1208 insertions(+), 1019 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 70a0a8232..f221e1c71 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -57,8 +57,7 @@ import sphinx from sphinx.roles import xfileref_role, innernodetypes from sphinx.config import Config from sphinx.builders import BUILTIN_BUILDERS -from sphinx.directives import desc_directive, target_directive, \ - additional_xref_types +from sphinx.directives import GenericDesc, Target, additional_xref_types from sphinx.environment import SphinxStandaloneReader from sphinx.util.compat import Directive, directive_dwim from sphinx.util.console import bold @@ -314,7 +313,7 @@ class Sphinx(object): parse_node=None, ref_nodeclass=None): additional_xref_types[directivename] = (rolename, indextemplate, parse_node) - directives.register_directive(directivename, desc_directive) + directives.register_directive(directivename, GenericDesc) roles.register_canonical_role(rolename, xfileref_role) if ref_nodeclass is not None: innernodetypes[rolename] = ref_nodeclass @@ -322,7 +321,7 @@ class Sphinx(object): def add_crossref_type(self, directivename, rolename, indextemplate='', ref_nodeclass=None): additional_xref_types[directivename] = (rolename, indextemplate, None) - directives.register_directive(directivename, target_directive) + directives.register_directive(directivename, Target) roles.register_canonical_role(rolename, xfileref_role) if ref_nodeclass is not None: innernodetypes[rolename] = ref_nodeclass diff --git a/sphinx/directives/code.py b/sphinx/directives/code.py index 3162d5575..a66940857 100644 --- a/sphinx/directives/code.py +++ b/sphinx/directives/code.py @@ -16,135 +16,159 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.util import parselinenos +from sphinx.util.compat import Directive -# ------ highlight directive --------------------------------------------------- +class Highlight(Directive): + """ + Directive to set the highlighting language for code blocks, as well + as the threshold for line numbers. + """ -def highlightlang_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - if 'linenothreshold' in options: - try: - linenothreshold = int(options['linenothreshold']) - except Exception: - linenothreshold = 10 - else: - linenothreshold = sys.maxint - return [addnodes.highlightlang(lang=arguments[0].strip(), - linenothreshold=linenothreshold)] + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'linenothreshold': directives.unchanged, + } -highlightlang_directive.content = 0 -highlightlang_directive.arguments = (1, 0, 0) -highlightlang_directive.options = {'linenothreshold': directives.unchanged} -directives.register_directive('highlight', highlightlang_directive) -# old name -directives.register_directive('highlightlang', highlightlang_directive) - - -# ------ code-block directive -------------------------------------------------- - -def codeblock_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - code = u'\n'.join(content) - literal = nodes.literal_block(code, code) - literal['language'] = arguments[0] - literal['linenos'] = 'linenos' in options - return [literal] - -codeblock_directive.content = 1 -codeblock_directive.arguments = (1, 0, 0) -codeblock_directive.options = {'linenos': directives.flag} -directives.register_directive('code-block', codeblock_directive) -directives.register_directive('sourcecode', codeblock_directive) - - -# ------ literalinclude directive ---------------------------------------------- - -def literalinclude_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - """Like .. include:: :literal:, but only warns if the include file is - not found.""" - if not state.document.settings.file_insertion_enabled: - return [state.document.reporter.warning('File insertion disabled', - line=lineno)] - env = state.document.settings.env - rel_fn = arguments[0] - source_dir = path.dirname(path.abspath(state_machine.input_lines.source( - lineno - state_machine.input_offset - 1))) - fn = path.normpath(path.join(source_dir, rel_fn)) - - if 'pyobject' in options and 'lines' in options: - return [state.document.reporter.warning( - 'Cannot use both "pyobject" and "lines" options', line=lineno)] - - encoding = options.get('encoding', env.config.source_encoding) - try: - f = codecs.open(fn, 'rU', encoding) - lines = f.readlines() - f.close() - except (IOError, OSError): - return [state.document.reporter.warning( - 'Include file %r not found or reading it failed' % arguments[0], - line=lineno)] - except UnicodeError: - return [state.document.reporter.warning( - 'Encoding %r used for reading included file %r seems to ' - 'be wrong, try giving an :encoding: option' % - (encoding, arguments[0]))] - - objectname = options.get('pyobject') - if objectname is not None: - from sphinx.pycode import ModuleAnalyzer - analyzer = ModuleAnalyzer.for_file(fn, '') - tags = analyzer.find_tags() - if objectname not in tags: - return [state.document.reporter.warning( - 'Object named %r not found in include file %r' % - (objectname, arguments[0]), line=lineno)] + def run(self): + if 'linenothreshold' in self.options: + try: + linenothreshold = int(self.options['linenothreshold']) + except Exception: + linenothreshold = 10 else: - lines = lines[tags[objectname][1] - 1 : tags[objectname][2] - 1] + linenothreshold = sys.maxint + return [addnodes.highlightlang(lang=self.arguments[0].strip(), + linenothreshold=linenothreshold)] - linespec = options.get('lines') - if linespec is not None: + +class CodeBlock(Directive): + """ + Directive for a code block with special highlighting or line numbering + settings. + """ + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'linenos': directives.flag, + } + + def run(self): + code = u'\n'.join(self.content) + literal = nodes.literal_block(code, code) + literal['language'] = self.arguments[0] + literal['linenos'] = 'linenos' in self.options + return [literal] + + +class LiteralInclude(Directive): + """ + Like ``.. include:: :literal:``, but only warns if the include file is + not found, and does not raise errors. Also has several options for + selecting what to include. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'linenos': directives.flag, + 'language': directives.unchanged_required, + 'encoding': directives.encoding, + 'pyobject': directives.unchanged_required, + 'lines': directives.unchanged_required, + 'start-after': directives.unchanged_required, + 'end-before': directives.unchanged_required, + } + + def run(self): + document = self.state.document + filename = self.arguments[0] + if not document.settings.file_insertion_enabled: + return [document.reporter.warning('File insertion disabled', + line=self.lineno)] + env = document.settings.env + rel_fn = filename + sourcename = self.state_machine.input_lines.source( + self.lineno - self.state_machine.input_offset - 1) + source_dir = path.dirname(path.abspath(sourcename)) + fn = path.normpath(path.join(source_dir, rel_fn)) + + if 'pyobject' in self.options and 'lines' in self.options: + return [document.reporter.warning( + 'Cannot use both "pyobject" and "lines" options', + line=self.lineno)] + + encoding = self.options.get('encoding', env.config.source_encoding) try: - linelist = parselinenos(linespec, len(lines)) - except ValueError, err: - return [state.document.reporter.warning(str(err), line=lineno)] - lines = [lines[i] for i in linelist] + f = codecs.open(fn, 'rU', encoding) + lines = f.readlines() + f.close() + except (IOError, OSError): + return [document.reporter.warning( + 'Include file %r not found or reading it failed' % filename, + line=self.lineno)] + except UnicodeError: + return [document.reporter.warning( + 'Encoding %r used for reading included file %r seems to ' + 'be wrong, try giving an :encoding: option' % + (encoding, filename))] - startafter = options.get('start-after') - endbefore = options.get('end-before') - if startafter is not None or endbefore is not None: - use = not startafter - res = [] - for line in lines: - if not use and startafter in line: - use = True - elif use and endbefore in line: - use = False - break - elif use: - res.append(line) - lines = res + objectname = self.options.get('pyobject') + if objectname is not None: + from sphinx.pycode import ModuleAnalyzer + analyzer = ModuleAnalyzer.for_file(fn, '') + tags = analyzer.find_tags() + if objectname not in tags: + return [document.reporter.warning( + 'Object named %r not found in include file %r' % + (objectname, filename), line=self.lineno)] + else: + lines = lines[tags[objectname][1]-1 : tags[objectname][2]-1] - text = ''.join(lines) - retnode = nodes.literal_block(text, text, source=fn) - retnode.line = 1 - if options.get('language', ''): - retnode['language'] = options['language'] - if 'linenos' in options: - retnode['linenos'] = True - state.document.settings.env.note_dependency(rel_fn) - return [retnode] + linespec = self.options.get('lines') + if linespec is not None: + try: + linelist = parselinenos(linespec, len(lines)) + except ValueError, err: + return [document.reporter.warning(str(err), line=self.lineno)] + lines = [lines[i] for i in linelist] -literalinclude_directive.options = { - 'linenos': directives.flag, - 'language': directives.unchanged_required, - 'encoding': directives.encoding, - 'pyobject': directives.unchanged_required, - 'lines': directives.unchanged_required, - 'start-after': directives.unchanged_required, - 'end-before': directives.unchanged_required, -} -literalinclude_directive.content = 0 -literalinclude_directive.arguments = (1, 0, 0) -directives.register_directive('literalinclude', literalinclude_directive) + startafter = self.options.get('start-after') + endbefore = self.options.get('end-before') + if startafter is not None or endbefore is not None: + use = not startafter + res = [] + for line in lines: + if not use and startafter in line: + use = True + elif use and endbefore in line: + use = False + break + elif use: + res.append(line) + lines = res + + text = ''.join(lines) + retnode = nodes.literal_block(text, text, source=fn) + retnode.line = 1 + if self.options.get('language', ''): + retnode['language'] = self.options['language'] + if 'linenos' in self.options: + retnode['linenos'] = True + document.settings.env.note_dependency(rel_fn) + return [retnode] + + +directives.register_directive('highlight', Highlight) +directives.register_directive('highlightlang', Highlight) # old name +directives.register_directive('code-block', CodeBlock) +directives.register_directive('sourcecode', CodeBlock) +directives.register_directive('literalinclude', LiteralInclude) diff --git a/sphinx/directives/desc.py b/sphinx/directives/desc.py index dd275b741..e45042b96 100644 --- a/sphinx/directives/desc.py +++ b/sphinx/directives/desc.py @@ -15,124 +15,11 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.util import ws_re - - -# ------ information units ----------------------------------------------------- - -def desc_index_text(desctype, module, name, add_modules): - if desctype == 'function': - if not module: - return _('%s() (built-in function)') % name - return _('%s() (in module %s)') % (name, module) - elif desctype == 'data': - if not module: - return _('%s (built-in variable)') % name - return _('%s (in module %s)') % (name, module) - elif desctype == 'class': - if not module: - return _('%s (built-in class)') % name - return _('%s (class in %s)') % (name, module) - elif desctype == 'exception': - return name - elif desctype == 'method': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if module: - return _('%s() (in module %s)') % (name, module) - else: - return '%s()' % name - if module and add_modules: - return _('%s() (%s.%s method)') % (methname, module, clsname) - else: - return _('%s() (%s method)') % (methname, clsname) - elif desctype == 'staticmethod': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if module: - return _('%s() (in module %s)') % (name, module) - else: - return '%s()' % name - if module and add_modules: - return _('%s() (%s.%s static method)') % (methname, module, clsname) - else: - return _('%s() (%s static method)') % (methname, clsname) - elif desctype == 'classmethod': - try: - clsname, methname = name.rsplit('.', 1) - except ValueError: - if module: - return '%s() (in module %s)' % (name, module) - else: - return '%s()' % name - if module: - return '%s() (%s.%s class method)' % (methname, module, clsname) - else: - return '%s() (%s class method)' % (methname, clsname) - elif desctype == 'attribute': - try: - clsname, attrname = name.rsplit('.', 1) - except ValueError: - if module: - return _('%s (in module %s)') % (name, module) - else: - return name - if module and add_modules: - return _('%s (%s.%s attribute)') % (attrname, module, clsname) - else: - return _('%s (%s attribute)') % (attrname, clsname) - elif desctype == 'cfunction': - return _('%s (C function)') % name - elif desctype == 'cmember': - return _('%s (C member)') % name - elif desctype == 'cmacro': - return _('%s (C macro)') % name - elif desctype == 'ctype': - return _('%s (C type)') % name - elif desctype == 'cvar': - return _('%s (C variable)') % name - else: - raise ValueError('unhandled descenv: %s' % desctype) - - -# ------ make field lists (like :param foo:) in desc bodies prettier - -_ = lambda x: x # make gettext extraction in constants possible - -doc_fields_with_arg = { - 'param': '%param', - 'parameter': '%param', - 'arg': '%param', - 'argument': '%param', - 'keyword': '%param', - 'kwarg': '%param', - 'kwparam': '%param', - 'type': '%type', - 'raises': _('Raises'), - 'raise': 'Raises', - 'exception': 'Raises', - 'except': 'Raises', - 'var': _('Variable'), - 'ivar': 'Variable', - 'cvar': 'Variable', - 'returns': _('Returns'), - 'return': 'Returns', -} - -doc_fields_with_linked_arg = ('raises', 'raise', 'exception', 'except') - -doc_fields_without_arg = { - 'returns': 'Returns', - 'return': 'Returns', - 'rtype': _('Return type'), -} - -del _ +from sphinx.util.compat import Directive def _is_only_paragraph(node): - # determine if the node only contains one paragraph (and system messages) + """True if the node only contains one paragraph (and system messages).""" if len(node) == 0: return False elif len(node) > 1: @@ -144,89 +31,7 @@ def _is_only_paragraph(node): return False -def handle_doc_fields(node, env): - # don't traverse, only handle field lists that are immediate children - for child in node.children: - if not isinstance(child, nodes.field_list): - continue - params = [] - pfield = None - param_nodes = {} - param_types = {} - new_list = nodes.field_list() - for field in child: - fname, fbody = field - try: - typ, obj = fname.astext().split(None, 1) - typdesc = _(doc_fields_with_arg[typ]) - if _is_only_paragraph(fbody): - children = fbody.children[0].children - else: - children = fbody.children - if typdesc == '%param': - if not params: - # add the field that later gets all the parameters - pfield = nodes.field() - new_list += pfield - dlitem = nodes.list_item() - dlpar = nodes.paragraph() - dlpar += nodes.emphasis(obj, obj) - dlpar += nodes.Text(' -- ', ' -- ') - dlpar += children - param_nodes[obj] = dlpar - dlitem += dlpar - params.append(dlitem) - elif typdesc == '%type': - typenodes = fbody.children - if _is_only_paragraph(fbody): - typenodes = [nodes.Text(' (')] + \ - typenodes[0].children + [nodes.Text(')')] - param_types[obj] = typenodes - else: - fieldname = typdesc + ' ' - nfield = nodes.field() - nfieldname = nodes.field_name(fieldname, fieldname) - nfield += nfieldname - node = nfieldname - if typ in doc_fields_with_linked_arg: - node = addnodes.pending_xref(obj, reftype='obj', - refcaption=False, - reftarget=obj, - modname=env.currmodule, - classname=env.currclass) - nfieldname += node - node += nodes.Text(obj, obj) - nfield += nodes.field_body() - nfield[1] += fbody.children - new_list += nfield - except (KeyError, ValueError): - fnametext = fname.astext() - try: - typ = _(doc_fields_without_arg[fnametext]) - except KeyError: - # at least capitalize the field name - typ = fnametext.capitalize() - fname[0] = nodes.Text(typ) - new_list += field - if params: - if len(params) == 1: - pfield += nodes.field_name('', _('Parameter')) - pfield += nodes.field_body() - pfield[1] += params[0][0] - else: - pfield += nodes.field_name('', _('Parameters')) - pfield += nodes.field_body() - pfield[1] += nodes.bullet_list() - pfield[1][0].extend(params) - - for param, type in param_types.iteritems(): - if param in param_nodes: - param_nodes[param][1:1] = type - child.replace_self(new_list) - - -# ------ functions to parse a Python or C signature and create desc_* nodes. - +# REs for Python signatures py_sig_re = re.compile( r'''^ ([\w.]*\.)? # class name(s) (\w+) \s* # thing name @@ -237,84 +42,7 @@ py_sig_re = re.compile( py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' -def parse_py_signature(signode, sig, desctype, module, env): - """ - Transform a python signature into RST nodes. - Return (fully qualified name of the thing, classname if any). - - If inside a class, the current class name is handled intelligently: - * it is stripped from the displayed name if present - * it is added to the full name (return value) if not present - """ - m = py_sig_re.match(sig) - if m is None: - raise ValueError - classname, name, arglist, retann = m.groups() - - if env.currclass: - add_module = False - if classname and classname.startswith(env.currclass): - fullname = classname + name - # class name is given again in the signature - classname = classname[len(env.currclass):].lstrip('.') - elif classname: - # class name is given in the signature, but different - # (shouldn't happen) - fullname = env.currclass + '.' + classname + name - else: - # class name is not given in the signature - fullname = env.currclass + '.' + name - else: - add_module = True - fullname = classname and classname + name or name - - if desctype == 'staticmethod': - signode += addnodes.desc_annotation('static ', 'static ') - elif desctype == 'classmethod': - signode += addnodes.desc_annotation('classmethod ', 'classmethod ') - - if classname: - signode += addnodes.desc_addname(classname, classname) - # exceptions are a special case, since they are documented in the - # 'exceptions' module. - elif add_module and env.config.add_module_names and \ - module and module != 'exceptions': - nodetext = module + '.' - signode += addnodes.desc_addname(nodetext, nodetext) - - signode += addnodes.desc_name(name, name) - if not arglist: - if desctype in ('function', 'method', 'staticmethod', 'classmethod'): - # for callables, add an empty parameter list - signode += addnodes.desc_parameterlist() - if retann: - signode += addnodes.desc_returns(retann, retann) - return fullname, classname - signode += addnodes.desc_parameterlist() - - stack = [signode[-1]] - for token in py_paramlist_re.split(arglist): - if token == '[': - opt = addnodes.desc_optional() - stack[-1] += opt - stack.append(opt) - elif token == ']': - try: - stack.pop() - except IndexError: - raise ValueError - elif not token or token == ',' or token.isspace(): - pass - else: - token = token.strip() - stack[-1] += addnodes.desc_parameter(token, token) - if len(stack) != 1: - raise ValueError - if retann: - signode += addnodes.desc_returns(retann, retann) - return fullname, classname - - +# REs for C signatures c_sig_re = re.compile( r'''^([^(]*?) # return type ([\w:]+) \s* # thing name (colon allowed for C++ class names) @@ -329,258 +57,608 @@ c_funcptr_sig_re = re.compile( ''', re.VERBOSE) c_funcptr_name_re = re.compile(r'^\(\s*\*\s*(.*?)\s*\)$') -# RE to split at word boundaries -wsplit_re = re.compile(r'(\W+)') - -# These C types aren't described in the reference, so don't try to create -# a cross-reference to them -stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct')) - -def parse_c_type(node, ctype): - # add cross-ref nodes for all words - for part in filter(None, wsplit_re.split(ctype)): - tnode = nodes.Text(part, part) - if part[0] in string.ascii_letters+'_' and part not in stopwords: - pnode = addnodes.pending_xref( - '', reftype='ctype', reftarget=part, - modname=None, classname=None) - pnode += tnode - node += pnode - else: - node += tnode - -def parse_c_signature(signode, sig, desctype): - """Transform a C (or C++) signature into RST nodes.""" - # first try the function pointer signature regex, it's more specific - m = c_funcptr_sig_re.match(sig) - if m is None: - m = c_sig_re.match(sig) - if m is None: - raise ValueError('no match') - rettype, name, arglist, const = m.groups() - - signode += addnodes.desc_type('', '') - parse_c_type(signode[-1], rettype) - try: - classname, funcname = name.split('::', 1) - classname += '::' - signode += addnodes.desc_addname(classname, classname) - signode += addnodes.desc_name(funcname, funcname) - # name (the full name) is still both parts - except ValueError: - signode += addnodes.desc_name(name, name) - # clean up parentheses from canonical name - m = c_funcptr_name_re.match(name) - if m: - name = m.group(1) - if not arglist: - if desctype == 'cfunction': - # for functions, add an empty parameter list - signode += addnodes.desc_parameterlist() - return name - - paramlist = addnodes.desc_parameterlist() - arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup - # this messes up function pointer types, but not too badly ;) - args = arglist.split(',') - for arg in args: - arg = arg.strip() - param = addnodes.desc_parameter('', '', noemph=True) - try: - ctype, argname = arg.rsplit(' ', 1) - except ValueError: - # no argument name given, only the type - parse_c_type(param, arg) - else: - parse_c_type(param, ctype) - param += nodes.emphasis(' '+argname, ' '+argname) - paramlist += param - signode += paramlist - if const: - signode += addnodes.desc_addname(const, const) - return name - - +# RE for option descriptions option_desc_re = re.compile( r'((?:/|-|--)[-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') -def parse_option_desc(signode, sig): - """Transform an option description into RST nodes.""" - count = 0 - firstname = '' - for m in option_desc_re.finditer(sig): - optname, args = m.groups() - if count: - signode += addnodes.desc_addname(', ', ', ') - signode += addnodes.desc_name(optname, optname) - signode += addnodes.desc_addname(args, args) - if not count: - firstname = optname - count += 1 - if not firstname: - raise ValueError - return firstname - +# RE to split at word boundaries +wsplit_re = re.compile(r'(\W+)') +# RE to strip backslash escapes strip_backslash_re = re.compile(r'\\(?=[^\\])') -def desc_directive(desctype, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - inode = addnodes.index(entries=[]) - node = addnodes.desc() - node.document = state.document - node['desctype'] = desctype - noindex = ('noindex' in options) - node['noindex'] = noindex - # remove backslashes to support (dummy) escapes; helps Vim's highlighting - signatures = map(lambda s: strip_backslash_re.sub('', s.strip()), - arguments[0].split('\n')) - names = [] - clsname = None - module = options.get('module', env.currmodule) - for i, sig in enumerate(signatures): - # add a signature node for each signature in the current unit - # and add a reference target for it - sig = sig.strip() - signode = addnodes.desc_signature(sig, '') - signode['first'] = False - node.append(signode) - try: - if desctype in ('function', 'data', 'class', 'exception', - 'method', 'staticmethod', 'classmethod', - 'attribute'): - name, clsname = parse_py_signature(signode, sig, - desctype, module, env) - elif desctype in ('cfunction', 'cmember', 'cmacro', - 'ctype', 'cvar'): - name = parse_c_signature(signode, sig, desctype) - elif desctype == 'cmdoption': - optname = parse_option_desc(signode, sig) - if not noindex: - targetname = optname.replace('/', '-') - if env.currprogram: - targetname = '-' + env.currprogram + targetname - targetname = 'cmdoption' + targetname - signode['ids'].append(targetname) - state.document.note_explicit_target(signode) - inode['entries'].append( - ('pair', _('%scommand line option; %s') % - ((env.currprogram and env.currprogram + ' ' or ''), - sig), - targetname, targetname)) - env.note_progoption(optname, targetname) +class DescDirective(Directive): + """ + Directive to describe a class, function or similar object. + """ + + has_content = True + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = { + 'noindex': directives.flag, + 'module': directives.unchanged, + } + + _ = lambda x: x # make gettext extraction in constants possible + + doc_fields_with_arg = { + 'param': '%param', + 'parameter': '%param', + 'arg': '%param', + 'argument': '%param', + 'keyword': '%param', + 'kwarg': '%param', + 'kwparam': '%param', + 'type': '%type', + 'raises': _('Raises'), + 'raise': 'Raises', + 'exception': 'Raises', + 'except': 'Raises', + 'var': _('Variable'), + 'ivar': 'Variable', + 'cvar': 'Variable', + 'returns': _('Returns'), + 'return': 'Returns', + } + + doc_fields_with_linked_arg = ('raises', 'raise', 'exception', 'except') + + doc_fields_without_arg = { + 'returns': 'Returns', + 'return': 'Returns', + 'rtype': _('Return type'), + } + + def handle_doc_fields(self, node): + # don't traverse, only handle field lists that are immediate children + for child in node.children: + if not isinstance(child, nodes.field_list): continue - elif desctype == 'describe': + params = [] + pfield = None + param_nodes = {} + param_types = {} + new_list = nodes.field_list() + for field in child: + fname, fbody = field + try: + typ, obj = fname.astext().split(None, 1) + typdesc = _(self.doc_fields_with_arg[typ]) + if _is_only_paragraph(fbody): + children = fbody.children[0].children + else: + children = fbody.children + if typdesc == '%param': + if not params: + # add the field that later gets all the parameters + pfield = nodes.field() + new_list += pfield + dlitem = nodes.list_item() + dlpar = nodes.paragraph() + dlpar += nodes.emphasis(obj, obj) + dlpar += nodes.Text(' -- ', ' -- ') + dlpar += children + param_nodes[obj] = dlpar + dlitem += dlpar + params.append(dlitem) + elif typdesc == '%type': + typenodes = fbody.children + if _is_only_paragraph(fbody): + typenodes = ([nodes.Text(' (')] + + typenodes[0].children + + [nodes.Text(')')]) + param_types[obj] = typenodes + else: + fieldname = typdesc + ' ' + nfield = nodes.field() + nfieldname = nodes.field_name(fieldname, fieldname) + nfield += nfieldname + node = nfieldname + if typ in self.doc_fields_with_linked_arg: + node = addnodes.pending_xref( + obj, reftype='obj', refcaption=False, + reftarget=obj, modname=self.env.currmodule, + classname=self.env.currclass) + nfieldname += node + node += nodes.Text(obj, obj) + nfield += nodes.field_body() + nfield[1] += fbody.children + new_list += nfield + except (KeyError, ValueError): + fnametext = fname.astext() + try: + typ = _(self.doc_fields_without_arg[fnametext]) + except KeyError: + # at least capitalize the field name + typ = fnametext.capitalize() + fname[0] = nodes.Text(typ) + new_list += field + if params: + if len(params) == 1: + pfield += nodes.field_name('', _('Parameter')) + pfield += nodes.field_body() + pfield[1] += params[0][0] + else: + pfield += nodes.field_name('', _('Parameters')) + pfield += nodes.field_body() + pfield[1] += nodes.bullet_list() + pfield[1][0].extend(params) + + for param, type in param_types.iteritems(): + if param in param_nodes: + param_nodes[param][1:1] = type + child.replace_self(new_list) + + def get_signatures(self): + # remove backslashes to support (dummy) escapes; helps Vim highlighting + return [strip_backslash_re.sub('', sig.strip()) + for sig in self.arguments[0].split('\n')] + + def parse_signature(self, sig, signode): + raise ValueError # must be implemented in subclasses + + def add_target_and_index(self, name, sig, signode): + return # do nothing by default + + def before_content(self): + pass + + def after_content(self): + pass + + def run(self): + self.desctype = self.name + self.env = self.state.document.settings.env + self.indexnode = addnodes.index(entries=[]) + + node = addnodes.desc() + node.document = self.state.document + node['desctype'] = self.desctype + node['noindex'] = noindex = ('noindex' in self.options) + + self.names = [] + signatures = self.get_signatures() + for i, sig in enumerate(signatures): + # add a signature node for each signature in the current unit + # and add a reference target for it + signode = addnodes.desc_signature(sig, '') + signode['first'] = False + node.append(signode) + try: + # name can also be a tuple, e.g. (classname, objname) + name = self.parse_signature(sig, signode) + except ValueError, err: + # signature parsing failed signode.clear() signode += addnodes.desc_name(sig, sig) - continue + continue # we don't want an index entry here + if not noindex and name not in self.names: + # only add target and index entry if this is the first + # description of the object with this name in this desc block + self.names.append(name) + self.add_target_and_index(name, sig, signode) + + contentnode = addnodes.desc_content() + node.append(contentnode) + if self.names: + # needed for association of version{added,changed} directives + self.env.currdesc = self.names[0] + self.before_content() + self.state.nested_parse(self.content, self.content_offset, contentnode) + self.handle_doc_fields(contentnode) + self.env.currdesc = None + self.after_content() + return [self.indexnode, node] + + +class PythonDesc(DescDirective): + def parse_signature(self, sig, signode): + """ + Transform a python signature into RST nodes. + Return (fully qualified name of the thing, classname if any). + + If inside a class, the current class name is handled intelligently: + * it is stripped from the displayed name if present + * it is added to the full name (return value) if not present + """ + m = py_sig_re.match(sig) + if m is None: + raise ValueError + classname, name, arglist, retann = m.groups() + + if self.env.currclass: + add_module = False + if classname and classname.startswith(self.env.currclass): + fullname = classname + name + # class name is given again in the signature + classname = classname[len(self.env.currclass):].lstrip('.') + elif classname: + # class name is given in the signature, but different + # (shouldn't happen) + fullname = self.env.currclass + '.' + classname + name else: - # another registered generic x-ref directive - rolename, indextemplate, parse_node = \ - additional_xref_types[desctype] - if parse_node: - fullname = parse_node(env, sig, signode) + # class name is not given in the signature + fullname = self.env.currclass + '.' + name + else: + add_module = True + fullname = classname and classname + name or name + + # XXX! + if self.desctype == 'staticmethod': + signode += addnodes.desc_annotation('static ', 'static ') + elif self.desctype == 'classmethod': + signode += addnodes.desc_annotation('classmethod ', 'classmethod ') + + if classname: + signode += addnodes.desc_addname(classname, classname) + # exceptions are a special case, since they are documented in the + # 'exceptions' module. + elif add_module and self.env.config.add_module_names: + modname = self.options.get('module', self.env.currmodule) + if modname and modname != 'exceptions': + nodetext = modname + '.' + signode += addnodes.desc_addname(nodetext, nodetext) + + signode += addnodes.desc_name(name, name) + if not arglist: + # XXX! + if self.desctype in ('function', 'method', + 'staticmethod', 'classmethod'): + # for callables, add an empty parameter list + signode += addnodes.desc_parameterlist() + if retann: + signode += addnodes.desc_returns(retann, retann) + return fullname, classname + signode += addnodes.desc_parameterlist() + + stack = [signode[-1]] + for token in py_paramlist_re.split(arglist): + if token == '[': + opt = addnodes.desc_optional() + stack[-1] += opt + stack.append(opt) + elif token == ']': + try: + stack.pop() + except IndexError: + raise ValueError + elif not token or token == ',' or token.isspace(): + pass + else: + token = token.strip() + stack[-1] += addnodes.desc_parameter(token, token) + if len(stack) != 1: + raise ValueError + if retann: + signode += addnodes.desc_returns(retann, retann) + return fullname, classname + + def get_index_text(self, modname, name): + raise NotImplementedError('must be implemented in subclasses') + + def add_target_and_index(self, name_cls, sig, signode): + modname = self.options.get('module', self.env.currmodule) + fullname = (modname and modname + '.' or '') + name_cls[0] + # note target + if fullname not in self.state.document.ids: + signode['names'].append(fullname) + signode['ids'].append(fullname) + signode['first'] = (not self.names) + self.state.document.note_explicit_target(signode) + self.env.note_descref(fullname, self.desctype, self.lineno) + + indextext = self.get_index_text(modname, name_cls) + if indextext: + self.indexnode['entries'].append(('single', indextext, + fullname, fullname)) + + def before_content(self): + # needed for automatic qualification of members (reset in subclasses) + self.clsname_set = False + + def after_content(self): + if self.clsname_set: + self.env.currclass = None + + +class ModulelevelDesc(PythonDesc): + def get_index_text(self, modname, name_cls): + if self.desctype == 'function': + if not modname: + return _('%s() (built-in function)') % name_cls[0] + return _('%s() (in module %s)') % (name_cls[0], modname) + elif self.desctype == 'data': + if not modname: + return _('%s (built-in variable)') % name_cls[0] + return _('%s (in module %s)') % (name_cls[0], modname) + else: + return '' + + +class ClasslikeDesc(PythonDesc): + def get_index_text(self, modname, name_cls): + if self.desctype == 'class': + if not modname: + return _('%s (built-in class)') % name_cls[0] + return _('%s (class in %s)') % (name_cls[0], modname) + elif self.desctype == 'exception': + return name_cls[0] + else: + return '' + + def before_content(self): + PythonDesc.before_content(self) + if self.names: + self.env.currclass = self.names[0][0] + self.clsname_set = True + + +class ClassmemberDesc(PythonDesc): + def get_index_text(self, modname, name_cls): + name, cls = name_cls + add_modules = self.env.config.add_module_names + if self.desctype == 'method': + try: + clsname, methname = name.rsplit('.', 1) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) else: - signode.clear() - signode += addnodes.desc_name(sig, sig) - # normalize whitespace like xfileref_role does - fullname = ws_re.sub('', sig) - if not noindex: - targetname = '%s-%s' % (rolename, fullname) - signode['ids'].append(targetname) - state.document.note_explicit_target(signode) - if indextemplate: - indexentry = _(indextemplate) % (fullname,) - indextype = 'single' - colon = indexentry.find(':') - if colon != -1: - indextype = indexentry[:colon].strip() - indexentry = indexentry[colon+1:].strip() - inode['entries'].append((indextype, indexentry, - targetname, targetname)) - env.note_reftarget(rolename, fullname, targetname) - # don't use object indexing below - continue - except ValueError, err: - # signature parsing failed + return '%s()' % name + if modname and add_modules: + return _('%s() (%s.%s method)') % (methname, modname, clsname) + else: + return _('%s() (%s method)') % (methname, clsname) + elif self.desctype == 'staticmethod': + try: + clsname, methname = name.rsplit('.', 1) + except ValueError: + if modname: + return _('%s() (in module %s)') % (name, modname) + else: + return '%s()' % name + if modname and add_modules: + return _('%s() (%s.%s static method)') % (methname, modname, + clsname) + else: + return _('%s() (%s static method)') % (methname, clsname) + elif self.desctype == 'classmethod': + try: + clsname, methname = name.rsplit('.', 1) + except ValueError: + if modname: + return '%s() (in module %s)' % (name, modname) + else: + return '%s()' % name + if modname: + return '%s() (%s.%s class method)' % (methname, modname, + clsname) + else: + return '%s() (%s class method)' % (methname, clsname) + elif self.desctype == 'attribute': + try: + clsname, attrname = name.rsplit('.', 1) + except ValueError: + if modname: + return _('%s (in module %s)') % (name, modname) + else: + return name + if modname and add_modules: + return _('%s (%s.%s attribute)') % (attrname, modname, clsname) + else: + return _('%s (%s attribute)') % (attrname, clsname) + else: + return '' + + def before_content(self): + PythonDesc.before_content(self) + if self.names and self.names[-1][1] and not self.env.currclass: + self.env.currclass = self.names[-1][1].strip('.') + self.clsname_set = True + + +class CDesc(DescDirective): + + # These C types aren't described anywhere, so don't try to create + # a cross-reference to them + stopwords = set(('const', 'void', 'char', 'int', 'long', 'FILE', 'struct')) + + def _parse_type(self, node, ctype): + # add cross-ref nodes for all words + for part in filter(None, wsplit_re.split(ctype)): + tnode = nodes.Text(part, part) + if part[0] in string.ascii_letters+'_' and \ + part not in self.stopwords: + pnode = addnodes.pending_xref( + '', reftype='ctype', reftarget=part, + modname=None, classname=None) + pnode += tnode + node += pnode + else: + node += tnode + + def parse_signature(self, sig, signode): + """Transform a C (or C++) signature into RST nodes.""" + # first try the function pointer signature regex, it's more specific + m = c_funcptr_sig_re.match(sig) + if m is None: + m = c_sig_re.match(sig) + if m is None: + raise ValueError('no match') + rettype, name, arglist, const = m.groups() + + signode += addnodes.desc_type('', '') + self._parse_type(signode[-1], rettype) + try: + classname, funcname = name.split('::', 1) + classname += '::' + signode += addnodes.desc_addname(classname, classname) + signode += addnodes.desc_name(funcname, funcname) + # name (the full name) is still both parts + except ValueError: + signode += addnodes.desc_name(name, name) + # clean up parentheses from canonical name + m = c_funcptr_name_re.match(name) + if m: + name = m.group(1) + if not arglist: + if self.desctype == 'cfunction': + # for functions, add an empty parameter list + signode += addnodes.desc_parameterlist() + return name + + paramlist = addnodes.desc_parameterlist() + arglist = arglist.replace('`', '').replace('\\ ', '') # remove markup + # this messes up function pointer types, but not too badly ;) + args = arglist.split(',') + for arg in args: + arg = arg.strip() + param = addnodes.desc_parameter('', '', noemph=True) + try: + ctype, argname = arg.rsplit(' ', 1) + except ValueError: + # no argument name given, only the type + self._parse_type(param, arg) + else: + self._parse_type(param, ctype) + param += nodes.emphasis(' '+argname, ' '+argname) + paramlist += param + signode += paramlist + if const: + signode += addnodes.desc_addname(const, const) + return name + + def get_index_text(self, modname, name): + if self.desctype == 'cfunction': + return _('%s (C function)') % name + elif self.desctype == 'cmember': + return _('%s (C member)') % name + elif self.desctype == 'cmacro': + return _('%s (C macro)') % name + elif self.desctype == 'ctype': + return _('%s (C type)') % name + elif self.desctype == 'cvar': + return _('%s (C variable)') % name + else: + return '' + + # just copy that one + add_target_and_index = PythonDesc.__dict__['add_target_and_index'] + + +class CmdoptionDesc(DescDirective): + """ + A command-line option (.. cmdoption). + """ + + def parse_signature(self, sig, signode): + """Transform an option description into RST nodes.""" + count = 0 + firstname = '' + for m in option_desc_re.finditer(sig): + optname, args = m.groups() + if count: + signode += addnodes.desc_addname(', ', ', ') + signode += addnodes.desc_name(optname, optname) + signode += addnodes.desc_addname(args, args) + if not count: + firstname = optname + count += 1 + if not firstname: + raise ValueError + return firstname + + def add_target_and_index(self, name, sig, signode): + targetname = name.replace('/', '-') + if self.env.currprogram: + targetname = '-' + self.env.currprogram + targetname + targetname = 'cmdoption' + targetname + signode['ids'].append(targetname) + self.state.document.note_explicit_target(signode) + self.indexnode['entries'].append( + ('pair', _('%scommand line option; %s') % + ((self.env.currprogram and + self.env.currprogram + ' ' or ''), sig), + targetname, targetname)) + self.env.note_progoption(name, targetname) + + +class GenericDesc(DescDirective): + """ + A generic x-ref directive registered with Sphinx.add_description_unit(). + """ + + def parse_signature(self, sig, signode): + parse_node = additional_xref_types[self.desctype][2] + if parse_node: + name = parse_node(self.env, sig, signode) + else: signode.clear() signode += addnodes.desc_name(sig, sig) - continue # we don't want an index entry here - # only add target and index entry if this is the first description - # of the function name in this desc block - if not noindex and name not in names: - fullname = (module and module + '.' or '') + name - # note target - if fullname not in state.document.ids: - signode['names'].append(fullname) - signode['ids'].append(fullname) - signode['first'] = (not names) - state.document.note_explicit_target(signode) - env.note_descref(fullname, desctype, lineno) - names.append(name) + # normalize whitespace like xfileref_role does + name = ws_re.sub('', sig) + return name - indextext = desc_index_text(desctype, module, name, - env.config.add_module_names) - inode['entries'].append(('single', indextext, fullname, fullname)) + def add_target_and_index(self, name, sig, signode): + rolename, indextemplate, _ = additional_xref_types[self.desctype] + targetname = '%s-%s' % (rolename, name) + signode['ids'].append(targetname) + self.state.document.note_explicit_target(signode) + if indextemplate: + indexentry = _(indextemplate) % (name,) + indextype = 'single' + colon = indexentry.find(':') + if colon != -1: + indextype = indexentry[:colon].strip() + indexentry = indexentry[colon+1:].strip() + self.indexnode['entries'].append((indextype, indexentry, + targetname, targetname)) + self.env.note_reftarget(rolename, name, targetname) - subnode = addnodes.desc_content() - node.append(subnode) - # needed for automatic qualification of members - clsname_set = False - if desctype in ('class', 'exception') and names: - env.currclass = names[0] - clsname_set = True - elif desctype in ('method', 'staticmethod', 'classmethod', - 'attribute') and clsname and not env.currclass: - env.currclass = clsname.strip('.') - clsname_set = True - # needed for association of version{added,changed} directives - if names: - env.currdesc = names[0] - state.nested_parse(content, content_offset, subnode) - handle_doc_fields(subnode, env) - if clsname_set: - env.currclass = None - env.currdesc = None - return [inode, node] -desc_directive.content = 1 -desc_directive.arguments = (1, 0, 1) -desc_directive.options = {'noindex': directives.flag, - 'module': directives.unchanged} +class Target(Directive): + """ + Generic target for user-defined cross-reference types. + """ -desctypes = [ - # the Python ones - 'function', - 'data', - 'class', - 'method', - 'classmethod', - 'staticmethod', - 'attribute', - 'exception', - # the C ones - 'cfunction', - 'cmember', - 'cmacro', - 'ctype', - 'cvar', - # for command line options - 'cmdoption', - # the generic one - 'describe', - 'envvar', -] + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} -for _name in desctypes: - directives.register_directive(_name, desc_directive) + def run(self): + env = self.state.document.settings.env + rolename, indextemplate, foo = additional_xref_types[self.name] + # normalize whitespace in fullname like xfileref_role does + fullname = ws_re.sub('', self.arguments[0].strip()) + targetname = '%s-%s' % (rolename, fullname) + node = nodes.target('', '', ids=[targetname]) + self.state.document.note_explicit_target(node) + ret = [node] + if indextemplate: + indexentry = indextemplate % (fullname,) + indextype = 'single' + colon = indexentry.find(':') + if colon != -1: + indextype = indexentry[:colon].strip() + indexentry = indexentry[colon+1:].strip() + inode = addnodes.index(entries=[(indextype, indexentry, + targetname, targetname)]) + ret.insert(0, inode) + env.note_reftarget(rolename, fullname, targetname) + return ret + +# Note: the target directive is not registered here, it is used by the +# application when registering additional xref types. _ = lambda x: x # Generic cross-reference types; they can be registered in the application; -# the directives are either desc_directive or target_directive +# the directives are either desc_directive or target_directive. additional_xref_types = { # directive name: (role name, index text, function to parse the desc node) 'envvar': ('envvar', _('environment variable; %s'), None), @@ -589,34 +667,22 @@ additional_xref_types = { del _ -# ------ target ---------------------------------------------------------------- +directives.register_directive('describe', DescDirective) -def target_directive(targettype, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - """Generic target for user-defined cross-reference types.""" - env = state.document.settings.env - rolename, indextemplate, foo = additional_xref_types[targettype] - # normalize whitespace in fullname like xfileref_role does - fullname = ws_re.sub('', arguments[0].strip()) - targetname = '%s-%s' % (rolename, fullname) - node = nodes.target('', '', ids=[targetname]) - state.document.note_explicit_target(node) - ret = [node] - if indextemplate: - indexentry = indextemplate % (fullname,) - indextype = 'single' - colon = indexentry.find(':') - if colon != -1: - indextype = indexentry[:colon].strip() - indexentry = indexentry[colon+1:].strip() - inode = addnodes.index(entries=[(indextype, indexentry, - targetname, targetname)]) - ret.insert(0, inode) - env.note_reftarget(rolename, fullname, targetname) - return ret +directives.register_directive('function', ModulelevelDesc) +directives.register_directive('data', ModulelevelDesc) +directives.register_directive('class', ClasslikeDesc) +directives.register_directive('exception', ClasslikeDesc) +directives.register_directive('method', ClassmemberDesc) +directives.register_directive('classmethod', ClassmemberDesc) +directives.register_directive('staticmethod', ClassmemberDesc) +directives.register_directive('attribute', ClassmemberDesc) -target_directive.content = 0 -target_directive.arguments = (1, 0, 1) +directives.register_directive('cfunction', CDesc) +directives.register_directive('cmember', CDesc) +directives.register_directive('cmacro', CDesc) +directives.register_directive('ctype', CDesc) +directives.register_directive('cvar', CDesc) -# note, the target directive is not registered here, it is used by the -# application when registering additional xref types +directives.register_directive('cmdoption', CmdoptionDesc) +directives.register_directive('envvar', GenericDesc) diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index 3d060b02b..7601be1f8 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -15,266 +15,313 @@ from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.locale import pairindextypes from sphinx.util import patfilter, ws_re, caption_ref_re, url_re, docname_join -from sphinx.util.compat import make_admonition +from sphinx.util.compat import Directive, make_admonition -# ------ the TOC tree ---------------------------------------------------------- +class TocTree(Directive): + """ + Directive to notify Sphinx about the hierarchical structure of the docs, + and to include a table-of-contents like tree in the current document. + """ -def toctree_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - suffix = env.config.source_suffix - glob = 'glob' in options + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'maxdepth': int, + 'glob': directives.flag, + 'hidden': directives.flag, + } - ret = [] - # (title, ref) pairs, where ref may be a document, or an external link, - # and title may be None if the document's title is to be used - entries = [] - includefiles = [] - includetitles = {} - all_docnames = env.found_docs.copy() - # don't add the currently visited file in catch-all patterns - all_docnames.remove(env.docname) - for entry in content: - if not entry: - continue - if not glob: - # look for explicit titles and documents ("Some Title "). - m = caption_ref_re.match(entry) - if m: - ref = m.group(2) - title = m.group(1) - docname = ref + def run(self): + env = self.state.document.settings.env + suffix = env.config.source_suffix + glob = 'glob' in self.options + + ret = [] + # (title, ref) pairs, where ref may be a document, or an external link, + # and title may be None if the document's title is to be used + entries = [] + includefiles = [] + includetitles = {} + all_docnames = env.found_docs.copy() + # don't add the currently visited file in catch-all patterns + all_docnames.remove(env.docname) + for entry in self.content: + if not entry: + continue + if not glob: + # look for explicit titles ("Some Title ") + m = caption_ref_re.match(entry) + if m: + ref = m.group(2) + title = m.group(1) + docname = ref + else: + ref = docname = entry + title = None + # remove suffixes (backwards compatibility) + if docname.endswith(suffix): + docname = docname[:-len(suffix)] + # absolutize filenames + docname = docname_join(env.docname, docname) + if url_re.match(ref) or ref == 'self': + entries.append((title, ref)) + elif docname not in env.found_docs: + ret.append(self.state.document.reporter.warning( + 'toctree references unknown document %r' % docname, + line=self.lineno)) + else: + entries.append((title, docname)) + includefiles.append(docname) else: - ref = docname = entry - title = None - # remove suffixes (backwards compatibility) - if docname.endswith(suffix): - docname = docname[:-len(suffix)] - # absolutize filenames - docname = docname_join(env.docname, docname) - if url_re.match(ref) or ref == 'self': - entries.append((title, ref)) - elif docname not in env.found_docs: - ret.append(state.document.reporter.warning( - 'toctree references unknown document %r' % docname, - line=lineno)) - else: - entries.append((title, docname)) - includefiles.append(docname) - else: - patname = docname_join(env.docname, entry) - docnames = sorted(patfilter(all_docnames, patname)) - for docname in docnames: - all_docnames.remove(docname) # don't include it again - entries.append((None, docname)) - includefiles.append(docname) - if not docnames: - ret.append(state.document.reporter.warning( - 'toctree glob pattern %r didn\'t match any documents' - % entry, line=lineno)) - subnode = addnodes.toctree() - subnode['parent'] = env.docname - subnode['entries'] = entries - subnode['includefiles'] = includefiles - subnode['maxdepth'] = options.get('maxdepth', -1) - subnode['glob'] = glob - subnode['hidden'] = 'hidden' in options - ret.append(subnode) - return ret - -toctree_directive.content = 1 -toctree_directive.options = {'maxdepth': int, 'glob': directives.flag, - 'hidden': directives.flag} -directives.register_directive('toctree', toctree_directive) + patname = docname_join(env.docname, entry) + docnames = sorted(patfilter(all_docnames, patname)) + for docname in docnames: + all_docnames.remove(docname) # don't include it again + entries.append((None, docname)) + includefiles.append(docname) + if not docnames: + ret.append(self.state.document.reporter.warning( + 'toctree glob pattern %r didn\'t match any documents' + % entry, line=self.lineno)) + subnode = addnodes.toctree() + subnode['parent'] = env.docname + subnode['entries'] = entries + subnode['includefiles'] = includefiles + subnode['maxdepth'] = self.options.get('maxdepth', -1) + subnode['glob'] = glob + subnode['hidden'] = 'hidden' in self.options + ret.append(subnode) + return ret -# ------ section metadata ------------------------------------------------------ +class Module(Directive): + """ + Directive to mark description of a new module. + """ -def module_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - modname = arguments[0].strip() - noindex = 'noindex' in options - env.currmodule = modname - env.note_module(modname, options.get('synopsis', ''), - options.get('platform', ''), - 'deprecated' in options) - modulenode = addnodes.module() - modulenode['modname'] = modname - modulenode['synopsis'] = options.get('synopsis', '') - targetnode = nodes.target('', '', ids=['module-' + modname]) - state.document.note_explicit_target(targetnode) - ret = [modulenode, targetnode] - if 'platform' in options: - modulenode['platform'] = options['platform'] - node = nodes.paragraph() - node += nodes.emphasis('', _('Platforms: ')) - node += nodes.Text(options['platform'], options['platform']) - ret.append(node) - # the synopsis isn't printed; in fact, it is only used in the - # modindex currently - if not noindex: - indextext = _('%s (module)') % modname - inode = addnodes.index(entries=[('single', indextext, - 'module-' + modname, modname)]) - ret.insert(0, inode) - return ret + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'platform': lambda x: x, + 'synopsis': lambda x: x, + 'noindex': directives.flag, + 'deprecated': directives.flag, + } -module_directive.arguments = (1, 0, 0) -module_directive.options = {'platform': lambda x: x, - 'synopsis': lambda x: x, - 'noindex': directives.flag, - 'deprecated': directives.flag} -directives.register_directive('module', module_directive) - - -def currentmodule_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - # This directive is just to tell people that we're documenting - # stuff in module foo, but links to module foo won't lead here. - env = state.document.settings.env - modname = arguments[0].strip() - if modname == 'None': - env.currmodule = None - else: + def run(self): + env = self.state.document.settings.env + modname = self.arguments[0].strip() + noindex = 'noindex' in self.options env.currmodule = modname - return [] - -currentmodule_directive.arguments = (1, 0, 0) -directives.register_directive('currentmodule', currentmodule_directive) + env.note_module(modname, self.options.get('synopsis', ''), + self.options.get('platform', ''), + 'deprecated' in self.options) + modulenode = addnodes.module() + modulenode['modname'] = modname + modulenode['synopsis'] = self.options.get('synopsis', '') + targetnode = nodes.target('', '', ids=['module-' + modname]) + self.state.document.note_explicit_target(targetnode) + ret = [modulenode, targetnode] + if 'platform' in self.options: + platform = self.options['platform'] + modulenode['platform'] = platform + node = nodes.paragraph() + node += nodes.emphasis('', _('Platforms: ')) + node += nodes.Text(platform, platform) + ret.append(node) + # the synopsis isn't printed; in fact, it is only used in the + # modindex currently + if not noindex: + indextext = _('%s (module)') % modname + inode = addnodes.index(entries=[('single', indextext, + 'module-' + modname, modname)]) + ret.insert(0, inode) + return ret -def author_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - # Show authors only if the show_authors option is on - env = state.document.settings.env - if not env.config.show_authors: - return [] - para = nodes.paragraph() - emph = nodes.emphasis() - para += emph - if name == 'sectionauthor': - text = _('Section author: ') - elif name == 'moduleauthor': - text = _('Module author: ') - else: - text = _('Author: ') - emph += nodes.Text(text, text) - inodes, messages = state.inline_text(arguments[0], lineno) - emph.extend(inodes) - return [para] + messages +class CurrentModule(Directive): + """ + This directive is just to tell Sphinx that we're documenting + stuff in module foo, but links to module foo won't lead here. + """ -author_directive.arguments = (1, 0, 1) -directives.register_directive('sectionauthor', author_directive) -directives.register_directive('moduleauthor', author_directive) + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} - -def program_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - program = ws_re.sub('-', arguments[0].strip()) - if program == 'None': - env.currprogram = None - else: - env.currprogram = program - return [] - -program_directive.arguments = (1, 0, 1) -directives.register_directive('program', program_directive) - - -# ------ index markup ---------------------------------------------------------- - -indextypes = [ - 'single', 'pair', 'triple', -] - -def index_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - arguments = arguments[0].split('\n') - env = state.document.settings.env - targetid = 'index-%s' % env.index_num - env.index_num += 1 - targetnode = nodes.target('', '', ids=[targetid]) - state.document.note_explicit_target(targetnode) - indexnode = addnodes.index() - indexnode['entries'] = ne = [] - for entry in arguments: - entry = entry.strip() - for type in pairindextypes: - if entry.startswith(type+':'): - value = entry[len(type)+1:].strip() - value = pairindextypes[type] + '; ' + value - ne.append(('pair', value, targetid, value)) - break + def run(self): + env = self.state.document.settings.env + modname = self.arguments[0].strip() + if modname == 'None': + env.currmodule = None else: - for type in indextypes: + env.currmodule = modname + return [] + + +class Author(Directive): + """ + Directive to give the name of the author of the current document + or section. Shown in the output only if the show_authors option is on. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + def run(self): + env = self.state.document.settings.env + if not env.config.show_authors: + return [] + para = nodes.paragraph() + emph = nodes.emphasis() + para += emph + if self.name == 'sectionauthor': + text = _('Section author: ') + elif self.name == 'moduleauthor': + text = _('Module author: ') + else: + text = _('Author: ') + emph += nodes.Text(text, text) + inodes, messages = self.state.inline_text(self.arguments[0], + self.lineno) + emph.extend(inodes) + return [para] + messages + + +class Program(Directive): + """ + Directive to name the program for which options are documented. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + def run(self): + env = self.state.document.settings.env + program = ws_re.sub('-', self.arguments[0].strip()) + if program == 'None': + env.currprogram = None + else: + env.currprogram = program + return [] + + +class Index(Directive): + """ + Directive to add entries to the index. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + indextypes = [ + 'single', 'pair', 'triple', + ] + + def run(self): + arguments = self.arguments[0].split('\n') + env = self.state.document.settings.env + targetid = 'index-%s' % env.index_num + env.index_num += 1 + targetnode = nodes.target('', '', ids=[targetid]) + self.state.document.note_explicit_target(targetnode) + indexnode = addnodes.index() + indexnode['entries'] = ne = [] + for entry in arguments: + entry = entry.strip() + for type in pairindextypes: if entry.startswith(type+':'): value = entry[len(type)+1:].strip() - ne.append((type, value, targetid, value)) + value = pairindextypes[type] + '; ' + value + ne.append(('pair', value, targetid, value)) break - # shorthand notation for single entries else: - for value in entry.split(','): - value = value.strip() - if not value: - continue - ne.append(('single', value, targetid, value)) - return [indexnode, targetnode] - -index_directive.arguments = (1, 0, 1) -directives.register_directive('index', index_directive) - -# ------ versionadded/versionchanged ------------------------------------------- - -def version_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - node = addnodes.versionmodified() - node.document = state.document - node['type'] = name - node['version'] = arguments[0] - if len(arguments) == 2: - inodes, messages = state.inline_text(arguments[1], lineno+1) - node.extend(inodes) - if content: - state.nested_parse(content, content_offset, node) - ret = [node] + messages - else: - ret = [node] - env = state.document.settings.env - env.note_versionchange(node['type'], node['version'], node, lineno) - return ret - -version_directive.arguments = (1, 1, 1) -version_directive.content = 1 - -directives.register_directive('deprecated', version_directive) -directives.register_directive('versionadded', version_directive) -directives.register_directive('versionchanged', version_directive) + for type in self.indextypes: + if entry.startswith(type+':'): + value = entry[len(type)+1:].strip() + ne.append((type, value, targetid, value)) + break + # shorthand notation for single entries + else: + for value in entry.split(','): + value = value.strip() + if not value: + continue + ne.append(('single', value, targetid, value)) + return [indexnode, targetnode] -# ------ see also -------------------------------------------------------------- +class VersionChange(Directive): + """ + Directive to describe a change/addition/deprecation in a specific version. + """ -def seealso_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - ret = make_admonition( - addnodes.seealso, name, [_('See also')], options, content, - lineno, content_offset, block_text, state, state_machine) - if arguments: - argnodes, msgs = state.inline_text(arguments[0], lineno) - para = nodes.paragraph() - para += argnodes - para += msgs - ret[0].insert(1, para) - return ret + has_content = True + required_arguments = 1 + optional_arguments = 1 + final_argument_whitespace = True + option_spec = {} -seealso_directive.content = 1 -seealso_directive.arguments = (0, 1, 1) -directives.register_directive('seealso', seealso_directive) + def run(self): + node = addnodes.versionmodified() + node.document = self.state.document + node['type'] = self.name + node['version'] = self.arguments[0] + if len(self.arguments) == 2: + inodes, messages = self.state.inline_text(self.arguments[1], + self.lineno+1) + node.extend(inodes) + if self.content: + self.state.nested_parse(self.content, self.content_offset, node) + ret = [node] + messages + else: + ret = [node] + env = self.state.document.settings.env + env.note_versionchange(node['type'], node['version'], node, self.lineno) + return ret -# ------ production list (for the reference) ----------------------------------- +class SeeAlso(Directive): + """ + An admonition mentioning things to look at as reference. + """ + + has_content = True + required_arguments = 0 + optional_arguments = 1 + final_argument_whitespace = True + option_spec = {} + + def run(self): + ret = make_admonition( + addnodes.seealso, self.name, [_('See also')], self.options, + self.content, self.lineno, self.content_offset, self.block_text, + self.state, self.state_machine) + if self.arguments: + argnodes, msgs = self.state.inline_text(self.arguments[0], + self.lineno) + para = nodes.paragraph() + para += argnodes + para += msgs + ret[0].insert(1, para) + return ret + token_re = re.compile('`([a-z_]+)`') @@ -297,149 +344,202 @@ def token_xrefs(text, env): retnodes.append(nodes.Text(text[pos:], text[pos:])) return retnodes -def productionlist_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - env = state.document.settings.env - node = addnodes.productionlist() - messages = [] - i = 0 +class ProductionList(Directive): + """ + Directive to list grammar productions. + """ - for rule in arguments[0].split('\n'): - if i == 0 and ':' not in rule: - # production group - continue - i += 1 - try: - name, tokens = rule.split(':', 1) - except ValueError: - break - subnode = addnodes.production() - subnode['tokenname'] = name.strip() - if subnode['tokenname']: - idname = 'grammar-token-%s' % subnode['tokenname'] - if idname not in state.document.ids: - subnode['ids'].append(idname) - state.document.note_implicit_target(subnode, subnode) - env.note_reftarget('token', subnode['tokenname'], idname) - subnode.extend(token_xrefs(tokens, env)) - node.append(subnode) - return [node] + messages + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} -productionlist_directive.content = 0 -productionlist_directive.arguments = (1, 0, 1) -directives.register_directive('productionlist', productionlist_directive) + def run(self): + env = self.state.document.settings.env + node = addnodes.productionlist() + messages = [] + i = 0 - -# ------ glossary directive ---------------------------------------------------- - -def glossary_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - """Glossary with cross-reference targets for :term: roles.""" - env = state.document.settings.env - node = addnodes.glossary() - node.document = state.document - state.nested_parse(content, content_offset, node) - - # the content should be definition lists - dls = [child for child in node if isinstance(child, nodes.definition_list)] - # now, extract definition terms to enable cross-reference creation - for dl in dls: - dl['classes'].append('glossary') - for li in dl.children: - if not li.children or not isinstance(li[0], nodes.term): + for rule in self.arguments[0].split('\n'): + if i == 0 and ':' not in rule: + # production group continue - termtext = li.children[0].astext() - new_id = 'term-' + nodes.make_id(termtext) - if new_id in env.gloss_entries: - new_id = 'term-' + str(len(env.gloss_entries)) - env.gloss_entries.add(new_id) - li[0]['names'].append(new_id) - li[0]['ids'].append(new_id) - state.document.settings.env.note_reftarget('term', termtext.lower(), - new_id) - # add an index entry too - indexnode = addnodes.index() - indexnode['entries'] = [('single', termtext, new_id, termtext)] - li.insert(0, indexnode) - return [node] - -glossary_directive.content = 1 -glossary_directive.arguments = (0, 0, 0) -directives.register_directive('glossary', glossary_directive) + i += 1 + try: + name, tokens = rule.split(':', 1) + except ValueError: + break + subnode = addnodes.production() + subnode['tokenname'] = name.strip() + if subnode['tokenname']: + idname = 'grammar-token-%s' % subnode['tokenname'] + if idname not in self.state.document.ids: + subnode['ids'].append(idname) + self.state.document.note_implicit_target(subnode, subnode) + env.note_reftarget('token', subnode['tokenname'], idname) + subnode.extend(token_xrefs(tokens, env)) + node.append(subnode) + return [node] + messages -# ------ miscellaneous markup -------------------------------------------------- +class Glossary(Directive): + """ + Directive to create a glossary with cross-reference targets + for :term: roles. + """ -def centered_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - if not arguments: - return [] - subnode = addnodes.centered() - inodes, messages = state.inline_text(arguments[0], lineno) - subnode.extend(inodes) - return [subnode] + messages + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} -centered_directive.arguments = (1, 0, 1) -directives.register_directive('centered', centered_directive) + def run(self): + env = self.state.document.settings.env + node = addnodes.glossary() + node.document = self.state.document + self.state.nested_parse(self.content, self.content_offset, node) + + # the content should be definition lists + dls = [child for child in node + if isinstance(child, nodes.definition_list)] + # now, extract definition terms to enable cross-reference creation + for dl in dls: + dl['classes'].append('glossary') + for li in dl.children: + if not li.children or not isinstance(li[0], nodes.term): + continue + termtext = li.children[0].astext() + new_id = 'term-' + nodes.make_id(termtext) + if new_id in env.gloss_entries: + new_id = 'term-' + str(len(env.gloss_entries)) + env.gloss_entries.add(new_id) + li[0]['names'].append(new_id) + li[0]['ids'].append(new_id) + env.note_reftarget('term', termtext.lower(), new_id) + # add an index entry too + indexnode = addnodes.index() + indexnode['entries'] = [('single', termtext, new_id, termtext)] + li.insert(0, indexnode) + return [node] -def acks_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - node = addnodes.acks() - node.document = state.document - state.nested_parse(content, content_offset, node) - if len(node.children) != 1 or not isinstance(node.children[0], - nodes.bullet_list): - return [state.document.reporter.warning('.. acks content is not a list', - line=lineno)] - return [node] +class Centered(Directive): + """ + Directive to create a centered line of bold text. + """ -acks_directive.content = 1 -acks_directive.arguments = (0, 0, 0) -directives.register_directive('acks', acks_directive) + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + def run(self): + if not self.arguments: + return [] + subnode = addnodes.centered() + inodes, messages = self.state.inline_text(self.arguments[0], + self.lineno) + subnode.extend(inodes) + return [subnode] + messages -def hlist_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - ncolumns = options.get('columns', 2) - node = nodes.paragraph() - node.document = state.document - state.nested_parse(content, content_offset, node) - if len(node.children) != 1 or not isinstance(node.children[0], - nodes.bullet_list): - return [state.document.reporter.warning( - '.. hlist content is not a list', line=lineno)] - fulllist = node.children[0] - # create a hlist node where the items are distributed - npercol, nmore = divmod(len(fulllist), ncolumns) - index = 0 - newnode = addnodes.hlist() - for column in range(ncolumns): - endindex = index + (column < nmore and (npercol+1) or npercol) - col = addnodes.hlistcol() - col += nodes.bullet_list() - col[0] += fulllist.children[index:endindex] - index = endindex - newnode += col - return [newnode] -hlist_directive.content = 1 -hlist_directive.arguments = (0, 0, 0) -hlist_directive.options = {'columns': int} -directives.register_directive('hlist', hlist_directive) +class Acks(Directive): + """ + Directive for a list of names. + """ + + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = {} + + def run(self): + node = addnodes.acks() + node.document = self.state.document + self.state.nested_parse(self.content, self.content_offset, node) + if len(node.children) != 1 or not isinstance(node.children[0], + nodes.bullet_list): + return [self.state.document.reporter.warning( + '.. acks content is not a list', line=self.lineno)] + return [node] -def tabularcolumns_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - # support giving explicit tabulary column definition to latex - node = addnodes.tabular_col_spec() - node['spec'] = arguments[0] - return [node] +class HList(Directive): + """ + Directive for a list that gets compacted horizontally. + """ -tabularcolumns_directive.content = 0 -tabularcolumns_directive.arguments = (1, 0, 1) -directives.register_directive('tabularcolumns', tabularcolumns_directive) + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = False + option_spec = { + 'columns': int, + } + def run(self): + ncolumns = self.options.get('columns', 2) + node = nodes.paragraph() + node.document = self.state.document + self.state.nested_parse(self.content, self.content_offset, node) + if len(node.children) != 1 or not isinstance(node.children[0], + nodes.bullet_list): + return [self.state.document.reporter.warning( + '.. hlist content is not a list', line=self.lineno)] + fulllist = node.children[0] + # create a hlist node where the items are distributed + npercol, nmore = divmod(len(fulllist), ncolumns) + index = 0 + newnode = addnodes.hlist() + for column in range(ncolumns): + endindex = index + (column < nmore and (npercol+1) or npercol) + col = addnodes.hlistcol() + col += nodes.bullet_list() + col[0] += fulllist.children[index:endindex] + index = endindex + newnode += col + return [newnode] + + +class TabularColumns(Directive): + """ + Directive to give an explicit tabulary column definition to LaTeX. + """ + + has_content = False + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + def run(self): + node = addnodes.tabular_col_spec() + node['spec'] = self.arguments[0] + return [node] + + +directives.register_directive('toctree', TocTree) +directives.register_directive('module', Module) +directives.register_directive('currentmodule', CurrentModule) +directives.register_directive('sectionauthor', Author) +directives.register_directive('moduleauthor', Author) +directives.register_directive('program', Program) +directives.register_directive('index', Index) +directives.register_directive('deprecated', VersionChange) +directives.register_directive('versionadded', VersionChange) +directives.register_directive('versionchanged', VersionChange) +directives.register_directive('seealso', SeeAlso) +directives.register_directive('productionlist', ProductionList) +directives.register_directive('glossary', Glossary) +directives.register_directive('centered', Centered) +directives.register_directive('acks', Acks) +directives.register_directive('hlist', HList) +directives.register_directive('tabularcolumns', TabularColumns) # register the standard rst class directive under a different name