# -*- coding: utf-8 -*- """ sphinx.directives ~~~~~~~~~~~~~~~~~ Handlers for additional ReST directives. :copyright: 2007 by Georg Brandl. :license: Python license. """ import re import string from os import path from docutils import nodes from docutils.parsers.rst import directives, roles from docutils.parsers.rst.directives import admonitions from . import addnodes # ------ index markup -------------------------------------------------------------- entrytypes = [ 'single', 'pair', 'triple', 'quadruple', 'module', 'keyword', 'operator', 'object', 'exception', 'statement', 'builtin', ] 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'] = arguments for entry in arguments: try: type, string = entry.split(':', 1) env.note_index_entry(type.strip(), string.strip(), targetid, string.strip()) except ValueError: continue return [indexnode, targetnode] index_directive.arguments = (1, 0, 1) directives.register_directive('index', index_directive) # ------ information units --------------------------------------------------------- def desc_index_text(desctype, currmodule, name): if desctype == 'function': if not currmodule: return '%s() (built-in function)' % name return '%s() (in module %s)' % (name, currmodule) elif desctype == 'data': if not currmodule: return '%s (built-in variable)' % name return '%s (in module %s)' % (name, currmodule) elif desctype == 'class': return '%s (class in %s)' % (name, currmodule) elif desctype == 'exception': return name elif desctype == 'method': try: clsname, methname = name.rsplit('.', 1) except: if currmodule: return '%s() (in module %s)' % (name, currmodule) else: return '%s()' % name if currmodule: return '%s() (%s.%s method)' % (methname, currmodule, clsname) else: return '%s() (%s method)' % (methname, clsname) elif desctype == 'attribute': try: clsname, attrname = name.rsplit('.', 1) except: if currmodule: return '%s (in module %s)' % (name, currmodule) else: return name if currmodule: return '%s (%s.%s attribute)' % (attrname, currmodule, clsname) else: return '%s (%s attribute)' % (attrname, clsname) elif desctype == 'opcode': return '%s (opcode)' % name 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) # ------ functions to parse a Python or C signature and create desc_* nodes. py_sig_re = re.compile(r'''^([\w.]*\.)? # class names (\w+) \s* # thing name (?: \((.*)\) )? $ # optionally arguments ''', re.VERBOSE) py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' def parse_py_signature(signode, sig, desctype, currclass): """ Transform a python signature into RST nodes. Returns (signode, fullname). Return the fully qualified name of the thing. 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 = m.groups() if currclass: if classname and classname.startswith(currclass): fullname = classname + name classname = classname[len(currclass):].lstrip('.') elif classname: fullname = currclass + '.' + classname + name else: fullname = currclass + '.' + name else: fullname = classname + name if classname else name if classname: signode += addnodes.desc_classname(classname, classname) signode += addnodes.desc_name(name, name) if not arglist: if desctype in ('function', 'method'): # for callables, add an empty parameter list signode += addnodes.desc_parameterlist() return fullname signode += addnodes.desc_parameterlist() stack = [signode[-1]] arglist = arglist.replace('`', '').replace(r'\ ', '') # remove markup 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 return fullname c_sig_re = re.compile( r'''^([^(]*?) # return type (\w+) \s* # thing name (?: \((.*)\) )? $ # optionally arguments ''', re.VERBOSE) c_funcptr_sig_re = re.compile( r'''^([^(]+?) # return type (\( [^()]+ \)) \s* # name in parentheses \( (.*) \) $ # arguments ''', re.VERBOSE) # 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.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-language 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 = m.groups() parse_c_type(signode, rettype) signode += addnodes.desc_name(name, name) 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 return name opcode_sig_re = re.compile(r'(\w+(?:\+\d)?)\s*\((.*)\)') def parse_opcode_signature(signode, sig, desctype): """Transform an opcode signature into RST nodes.""" m = opcode_sig_re.match(sig) if m is None: raise ValueError opname, arglist = m.groups() signode += addnodes.desc_name(opname, opname) paramlist = addnodes.desc_parameterlist() signode += paramlist paramlist += addnodes.desc_parameter(arglist, arglist) return opname.strip() def add_refcount_annotation(env, node, name): """Add a reference count annotation. Return None.""" entry = env.refcounts.get(name) if not entry: return elif entry.result_type not in ("PyObject*", "PyVarObject*"): return rc = 'Return value: ' if entry.result_refs is None: rc += "Always NULL." else: rc += ("New" if entry.result_refs else "Borrowed") + " reference." node += addnodes.refcount(rc, rc) def desc_directive(desctype, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env node = addnodes.desc() node['desctype'] = desctype noindex = ('noindex' in options) signatures = map(lambda s: s.strip(), arguments[0].split('\n')) names = [] 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', 'attribute'): name = parse_py_signature(signode, sig, desctype, env.currclass) elif desctype in ('cfunction', 'cmember', 'cmacro', 'ctype', 'cvar'): name = parse_c_signature(signode, sig, desctype) elif desctype == 'opcode': name = parse_opcode_signature(signode, sig, desctype) else: # describe: use generic fallback raise ValueError except ValueError, err: 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 = (env.currmodule + '.' if env.currmodule else '') + 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) names.append(name) env.note_index_entry('single', desc_index_text(desctype, env.currmodule, name), fullname, fullname) subnode = addnodes.desc_content() if desctype == 'cfunction': add_refcount_annotation(env, subnode, name) # needed for automatic qualification of members if desctype == 'class' and names: env.currclass = names[0] # needed for association of version{added,changed} directives if names: env.currdesc = names[0] state.nested_parse(content, content_offset, subnode) if desctype == 'class': env.currclass = None env.currdesc = None node.append(subnode) return [node] desc_directive.content = 1 desc_directive.arguments = (1, 0, 1) desc_directive.options = {'noindex': directives.flag} desctypes = [ # the Python ones 'function', 'data', 'class', 'method', 'attribute', 'exception', # the C ones 'cfunction', 'cmember', 'cmacro', 'ctype', 'cvar', # the odd one 'opcode', # the generic one 'describe', ] for name in desctypes: directives.register_directive(name, desc_directive) # ------ versionadded/versionchanged ----------------------------------------------- def version_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): node = addnodes.versionmodified() 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) 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) # ------ see also ------------------------------------------------------------------ def seealso_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): rv = admonitions.make_admonition( addnodes.seealso, name, ['See also:'], options, content, lineno, content_offset, block_text, state, state_machine) return rv seealso_directive.content = 1 seealso_directive.arguments = (0, 0, 0) directives.register_directive('seealso', seealso_directive) # ------ production list (for the reference) --------------------------------------- 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 # use token as the default role while in production list roles._roles[''] = roles._role_registry['token'] 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_token(subnode['tokenname']) inodes, imessages = state.inline_text(tokens, lineno+i) subnode.extend(inodes) messages.extend(imessages) node.append(subnode) del roles._roles[''] return [node] + messages productionlist_directive.content = 0 productionlist_directive.arguments = (1, 0, 1) directives.register_directive('productionlist', productionlist_directive) # ------ section metadata ---------------------------------------------------------- def module_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env modname = arguments[0].strip() env.currmodule = modname env.note_module(modname, options.get('synopsis', ''), options.get('platform', '')) ret = [] targetnode = nodes.target('', '', ids=['module-' + modname]) state.document.note_explicit_target(targetnode) ret.append(targetnode) if 'platform' in options: node = nodes.paragraph() node += nodes.emphasis('Platforms: ', '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 env.note_index_entry('single', '%s (module)' % modname, 'module-' + modname, modname) return ret module_directive.arguments = (1, 0, 0) module_directive.options = {'platform': lambda x: x, 'synopsis': lambda x: x} directives.register_directive('module', module_directive) def author_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): # The author directives aren't included in the built document return [] author_directive.arguments = (1, 0, 1) directives.register_directive('sectionauthor', author_directive) directives.register_directive('moduleauthor', author_directive) # ------ toctree directive --------------------------------------------------------- def toctree_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): env = state.document.settings.env dirname = path.dirname(env.filename) subnode = addnodes.toctree() includefiles = filter(None, content) # absolutize filenames includefiles = map(lambda x: path.normpath(path.join(dirname, x)), includefiles) subnode['includefiles'] = includefiles subnode['maxdepth'] = options.get('maxdepth', -1) return [subnode] toctree_directive.content = 1 toctree_directive.options = {'maxdepth': int} directives.register_directive('toctree', toctree_directive) # ------ centered directive --------------------------------------------------------- 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 centered_directive.arguments = (1, 0, 1) directives.register_directive('centered', centered_directive) # ------ highlightlanguage directive ------------------------------------------------ def highlightlang_directive(name, arguments, options, content, lineno, content_offset, block_text, state, state_machine): return [addnodes.highlightlang(lang=arguments[0].strip())] highlightlang_directive.content = 0 highlightlang_directive.arguments = (1, 0, 0) directives.register_directive('highlightlang', highlightlang_directive)