# -*- coding: utf-8 -*- """ sphinx.domains.python ~~~~~~~~~~~~~~~~~~~~~ The Python domain. :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re from docutils import nodes from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.roles import XRefRole from sphinx.locale import l_ from sphinx.domains import Domain, ObjType from sphinx.directives import ObjectDescription from sphinx.util.nodes import make_refnode from sphinx.util.compat import Directive from sphinx.util.docfields import Field, GroupedField, TypedField # REs for Python signatures py_sig_re = re.compile( r'''^ ([\w.]*\.)? # class name(s) (\w+) \s* # thing name (?: \((.*)\) # optional: arguments (?:\s* -> \s* (.*))? # return annotation )? $ # and nothing more ''', re.VERBOSE) py_paramlist_re = re.compile(r'([\[\],])') # split at '[', ']' and ',' class PyObject(ObjectDescription): """ Description of a general Python object. """ doc_field_types = [ TypedField('parameter', label=l_('Parameters'), names=('param', 'parameter', 'arg', 'argument', 'keyword', 'kwarg', 'kwparam'), typerolename='obj', typenames=('paramtype', 'type')), TypedField('variable', label=l_('Variables'), rolename='obj', names=('var', 'ivar', 'cvar'), typerolename='obj', typenames=('vartype',)), GroupedField('exceptions', label=l_('Raises'), rolename='exc', names=('raises', 'raise', 'exception', 'except'), can_collapse=True), Field('returnvalue', label=l_('Returns'), has_arg=False, names=('returns', 'return')), Field('returntype', label=l_('Return type'), has_arg=False, names=('rtype',)), ] def get_signature_prefix(self, sig): """ May return a prefix to put before the object name in the signature. """ return '' def needs_arglist(self): """ May return true if an empty argument list is to be generated even if the document contains none. """ return False def handle_signature(self, sig, signode): """ Transform a Python signature into RST nodes. Returns (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 name_prefix, name, arglist, retann = m.groups() # determine module and class name (if applicable), as well as full name modname = self.options.get( 'module', self.env.doc_read_data.get('py:module')) classname = self.env.doc_read_data.get('py:class') if classname: add_module = False if name_prefix and name_prefix.startswith(classname): fullname = name_prefix + name # class name is given again in the signature name_prefix = name_prefix[len(classname):].lstrip('.') elif name_prefix: # class name is given in the signature, but different # (shouldn't happen) fullname = classname + '.' + name_prefix + name else: # class name is not given in the signature fullname = classname + '.' + name else: add_module = True if name_prefix: classname = name_prefix.rstrip('.') fullname = name_prefix + name else: classname = '' fullname = name signode['module'] = modname signode['class'] = classname signode['fullname'] = fullname sig_prefix = self.get_signature_prefix(sig) if sig_prefix: signode += addnodes.desc_annotation(sig_prefix, sig_prefix) if name_prefix: signode += addnodes.desc_addname(name_prefix, name_prefix) # 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.doc_read_data.get('py:module')) if modname and modname != 'exceptions': nodetext = modname + '.' signode += addnodes.desc_addname(nodetext, nodetext) signode += addnodes.desc_name(name, name) if not arglist: if self.needs_arglist(): # for callables, add an empty parameter list signode += addnodes.desc_parameterlist() if retann: signode += addnodes.desc_returns(retann, retann) return fullname, name_prefix 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, name_prefix def get_index_text(self, modname, name): """ Return the text for the index entry of the object. """ raise NotImplementedError('must be implemented in subclasses') def add_target_and_index(self, name_cls, sig, signode): modname = self.options.get( 'module', self.env.doc_read_data.get('py:module')) 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) objects = self.env.domaindata['py']['objects'] if fullname in objects: self.env.warn( self.env.docname, 'duplicate object description of %s, ' % fullname + 'other instance in ' + self.env.doc2path(objects[fullname][0]), self.lineno) objects[fullname] = (self.env.docname, self.objtype) 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.doc_read_data['py:class'] = None class PyModulelevel(PyObject): """ Description of an object on module level (functions, data). """ def needs_arglist(self): return self.objtype == 'function' def get_index_text(self, modname, name_cls): if self.objtype == 'function': if not modname: return _('%s() (built-in function)') % name_cls[0] return _('%s() (in module %s)') % (name_cls[0], modname) elif self.objtype == 'data': if not modname: return _('%s (built-in variable)') % name_cls[0] return _('%s (in module %s)') % (name_cls[0], modname) else: return '' class PyClasslike(PyObject): """ Description of a class-like object (classes, interfaces, exceptions). """ def get_signature_prefix(self, sig): return self.objtype + ' ' def get_index_text(self, modname, name_cls): if self.objtype == 'class': if not modname: return _('%s (built-in class)') % name_cls[0] return _('%s (class in %s)') % (name_cls[0], modname) elif self.objtype == 'exception': return name_cls[0] else: return '' def before_content(self): PyObject.before_content(self) if self.names: self.env.doc_read_data['py:class'] = self.names[0][0] self.clsname_set = True class PyClassmember(PyObject): """ Description of a class member (methods, attributes). """ def needs_arglist(self): return self.objtype.endswith('method') def get_signature_prefix(self, sig): if self.objtype == 'staticmethod': return 'static ' elif self.objtype == 'classmethod': return 'classmethod ' return '' def get_index_text(self, modname, name_cls): name, cls = name_cls add_modules = self.env.config.add_module_names if self.objtype == 'method': 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 method)') % (methname, modname, clsname) else: return _('%s() (%s method)') % (methname, clsname) elif self.objtype == '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.objtype == '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.objtype == '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): PyObject.before_content(self) lastname = self.names and self.names[-1][1] if lastname and not self.env.doc_read_data.get('py:class'): self.env.doc_read_data['py:class'] = lastname.strip('.') self.clsname_set = True class PyModule(Directive): """ Directive to mark description of a new module. """ 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, } def run(self): env = self.state.document.settings.env modname = self.arguments[0].strip() noindex = 'noindex' in self.options env.doc_read_data['py:module'] = modname env.domaindata['py']['modules'][modname] = \ (env.docname, 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], ismod=True) self.state.document.note_explicit_target(targetnode) ret = [modulenode, targetnode] # XXX this behavior of the module directive is a mess... 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 class PyCurrentModule(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. """ has_content = False required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False option_spec = {} def run(self): env = self.state.document.settings.env modname = self.arguments[0].strip() if modname == 'None': env.doc_read_data['py:module'] = None else: env.doc_read_data['py:module'] = modname return [] class PyXRefRole(XRefRole): def process_link(self, env, refnode, has_explicit_title, title, target): refnode['py:module'] = env.doc_read_data.get('py:module') refnode['py:class'] = env.doc_read_data.get('py:class') if not has_explicit_title: title = title.lstrip('.') # only has a meaning for the target target = target.lstrip('~') # only has a meaning for the title # if the first character is a tilde, don't display the module/class # parts of the contents if title[0:1] == '~': title = title[1:] dot = title.rfind('.') if dot != -1: title = title[dot+1:] # if the first character is a dot, search more specific namespaces first # else search builtins first if target[0:1] == '.': target = target[1:] refnode['refspecific'] = True return title, target class PythonDomain(Domain): """Python language domain.""" name = 'py' label = 'Python' object_types = { 'function': ObjType(l_('function'), 'func', 'obj'), 'data': ObjType(l_('data'), 'data', 'obj'), 'class': ObjType(l_('class'), 'class', 'obj'), 'exception': ObjType(l_('exception'), 'exc', 'obj'), 'method': ObjType(l_('method'), 'meth', 'obj'), 'attribute': ObjType(l_('attribute'), 'attr', 'obj'), 'module': ObjType(l_('module'), 'mod', 'obj'), } directives = { 'function': PyModulelevel, 'data': PyModulelevel, 'class': PyClasslike, 'exception': PyClasslike, 'method': PyClassmember, 'classmethod': PyClassmember, 'staticmethod': PyClassmember, 'attribute': PyClassmember, 'module': PyModule, 'currentmodule': PyCurrentModule, } roles = { 'data': PyXRefRole(), 'exc': PyXRefRole(), 'func': PyXRefRole(fix_parens=True), 'class': PyXRefRole(), 'const': PyXRefRole(), 'attr': PyXRefRole(), 'meth': PyXRefRole(fix_parens=True), 'mod': PyXRefRole(), 'obj': PyXRefRole(), } initial_data = { 'objects': {}, # fullname -> docname, objtype 'modules': {}, # modname -> docname, synopsis, platform, deprecated } def clear_doc(self, docname): for fullname, (fn, _) in self.data['objects'].items(): if fn == docname: del self.data['objects'][fullname] for modname, (fn, _, _, _) in self.data['modules'].items(): if fn == docname: del self.data['modules'][modname] def find_obj(self, env, modname, classname, name, type, searchorder=0): """ Find a Python object for "name", perhaps using the given module and/or classname. """ # skip parens if name[-2:] == '()': name = name[:-2] if not name: return None, None objects = self.data['objects'] newname = None if searchorder == 1: if modname and classname and \ modname + '.' + classname + '.' + name in objects: newname = modname + '.' + classname + '.' + name elif modname and modname + '.' + name in objects: newname = modname + '.' + name elif name in objects: newname = name else: if name in objects: newname = name elif modname and modname + '.' + name in objects: newname = modname + '.' + name elif modname and classname and \ modname + '.' + classname + '.' + name in objects: newname = modname + '.' + classname + '.' + name # special case: builtin exceptions have module "exceptions" set elif type == 'exc' and '.' not in name and \ 'exceptions.' + name in objects: newname = 'exceptions.' + name # special case: object methods elif type in ('func', 'meth') and '.' not in name and \ 'object.' + name in objects: newname = 'object.' + name if newname is None: return None, None return newname, objects[newname] def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): if (typ == 'mod' or typ == 'obj' and target in self.data['modules']): docname, synopsis, platform, deprecated = \ self.data['modules'].get(target, ('','','', '')) if not docname: return None else: title = '%s%s%s' % ((platform and '(%s) ' % platform), synopsis, (deprecated and ' (deprecated)' or '')) return make_refnode(builder, fromdocname, docname, 'module-' + target, contnode, title) else: modname = node.get('py:module') clsname = node.get('py:class') searchorder = node.hasattr('refspecific') and 1 or 0 name, obj = self.find_obj(env, modname, clsname, target, typ, searchorder) if not obj: return None else: return make_refnode(builder, fromdocname, obj[0], name, contnode, name) def get_objects(self): for modname, info in self.data['modules'].iteritems(): yield (modname, 'module', info[0], 'module-' + modname, 0) for refname, (docname, type) in self.data['objects'].iteritems(): yield (refname, type, docname, refname, 1)