diff options
Diffstat (limited to 'sphinx/environment.py')
-rw-r--r-- | sphinx/environment.py | 463 |
1 files changed, 169 insertions, 294 deletions
diff --git a/sphinx/environment.py b/sphinx/environment.py index 0c83bb4f6..d82ce4f74 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -26,7 +26,7 @@ from docutils.io import FileInput, NullOutput from docutils.core import Publisher from docutils.utils import Reporter, relative_path from docutils.readers import standalone -from docutils.parsers.rst import roles +from docutils.parsers.rst import roles, directives from docutils.parsers.rst.languages import en as english from docutils.parsers.rst.directives.html import MetaBody from docutils.writers import UnfilteredWriter @@ -36,11 +36,43 @@ from docutils.transforms.parts import ContentsFilter from sphinx import addnodes from sphinx.util import url_re, get_matching_docs, docname_join, \ FilenameUniqDict -from sphinx.util.nodes import clean_astext +from sphinx.util.nodes import clean_astext, make_refnode from sphinx.util.osutil import movefile, SEP, ustrftime from sphinx.util.matching import compile_matchers -from sphinx.errors import SphinxError -from sphinx.directives import additional_xref_types +from sphinx.errors import SphinxError, ExtensionError + + +orig_role_function = roles.role +orig_directive_function = directives.directive + +class ElementLookupError(Exception): pass + +# XXX why isn't this a method of env? +def lookup_domain_element(env, type, name): + """Lookup a markup element (directive or role), given its name which can + be a full name (with domain). + """ + name = name.lower() + # explicit domain given? + if ':' in name: + domain_name, name = name.split(':', 1) + if domain_name in env.domains: + domain = env.domains[domain_name] + element = getattr(domain, type)(name) + if element is not None: + return element, [] + # else look in the default domain + else: + def_domain = env.doc_read_data.get('default_domain') + if def_domain is not None: + element = getattr(def_domain, type)(name) + if element is not None: + return element, [] + # always look in the std domain + element = getattr(env.domains['std'], type)(name) + if element is not None: + return element, [] + raise ElementLookupError default_settings = { 'embed_stylesheet': False, @@ -54,7 +86,7 @@ default_settings = { # This is increased every time an environment attribute is added # or changed to properly invalidate pickle files. -ENV_VERSION = 31 +ENV_VERSION = 32 default_substitutions = set([ @@ -205,9 +237,9 @@ class BuildEnvironment: env = pickle.load(picklefile) finally: picklefile.close() - env.config.values = config.values if env.version != ENV_VERSION: raise IOError('env version not current') + env.config.values = config.values return env def topickle(self, filename): @@ -216,6 +248,8 @@ class BuildEnvironment: self.set_warnfunc(None) values = self.config.values del self.config.values + domains = self.domains + del self.domains # first write to a temporary file, so that if dumping fails, # the existing environment won't be overwritten picklefile = open(filename + '.tmp', 'wb') @@ -232,6 +266,7 @@ class BuildEnvironment: picklefile.close() movefile(filename + '.tmp', filename) # reset attributes + self.domains = domains self.config.values = values self.set_warnfunc(warnfunc) @@ -245,6 +280,9 @@ class BuildEnvironment: # the application object; only set while update() runs self.app = None + # all the registered domains, set by the application + self.domains = {} + # the docutils settings for building self.settings = default_settings.copy() self.settings['env'] = self @@ -283,16 +321,13 @@ class BuildEnvironment: self.glob_toctrees = set() # docnames that have :glob: toctrees self.numbered_toctrees = set() # docnames that have :numbered: toctrees + # domain-specific inventories, here to be pickled + self.domaindata = {} # domainname -> domain-specific dict + # X-ref target inventory - self.descrefs = {} # fullname -> docname, desctype - self.filemodules = {} # docname -> [modules] - self.modules = {} # modname -> docname, synopsis, - # platform, deprecated self.labels = {} # labelname -> docname, labelid, sectionname self.anonlabels = {} # labelname -> docname, labelid - self.progoptions = {} # (program, name) -> docname, labelid - self.reftargets = {} # (type, name) -> docname, labelid - # type: term, token, envvar, citation + self.citations = {} # citation name -> docname, labelid # Other inventories self.indexentries = {} # docname -> list of @@ -304,14 +339,9 @@ class BuildEnvironment: self.images = FilenameUniqDict() self.dlfiles = FilenameUniqDict() - # These are set while parsing a file - self.docname = None # current document name - self.currmodule = None # current module name - self.currclass = None # current class name - self.currdesc = None # current descref name - self.currprogram = None # current program name - self.index_num = 0 # autonumber for index targets - self.gloss_entries = set() # existing definition labels + # temporary data storage while reading a document + # XXX find a better name + self.doc_read_data = {} # Some magically present labels def add_magic_label(name, description): @@ -345,7 +375,6 @@ class BuildEnvironment: self.toc_secnumbers.pop(docname, None) self.toc_num_entries.pop(docname, None) self.toctree_includes.pop(docname, None) - self.filemodules.pop(docname, None) self.indexentries.pop(docname, None) self.glob_toctrees.discard(docname) self.numbered_toctrees.discard(docname) @@ -356,25 +385,20 @@ class BuildEnvironment: fnset.discard(docname) if not fnset: del self.files_to_rebuild[subfn] - for fullname, (fn, _) in self.descrefs.items(): - if fn == docname: - del self.descrefs[fullname] - for modname, (fn, _, _, _) in self.modules.items(): - if fn == docname: - del self.modules[modname] for labelname, (fn, _, _) in self.labels.items(): if fn == docname: del self.labels[labelname] - for key, (fn, _) in self.reftargets.items(): + for key, (fn, _) in self.citations.items(): if fn == docname: - del self.reftargets[key] - for key, (fn, _) in self.progoptions.items(): - if fn == docname: - del self.progoptions[key] + del self.citations[key] for version, changes in self.versionchanges.items(): new = [change for change in changes if change[1] != docname] changes[:] = new + # XXX why does this not work inside the if? + for domain in self.domains.values(): + domain.clear_doc(docname) + def doc2path(self, docname, base=True, suffix=None): """ Return the filename for the document name. @@ -547,6 +571,26 @@ class BuildEnvironment: error.object[error.end:lineend]), lineno) return (u'?', error.end) + def patch_lookup_functions(self): + """ + Monkey-patch directive and role dispatch, so that domain-specific + markup takes precedence. + """ + def directive(name, lang_module, document): + try: + return lookup_domain_element(self, 'directive', name) + except ElementLookupError: + return orig_directive_function(name, lang_module, document) + + def role(name, lang_module, lineno, reporter): + try: + return lookup_domain_element(self, 'role', name) + except ElementLookupError: + return orig_role_function(name, lang_module, lineno, reporter) + + directives.directive = directive + roles.role = role + def read_doc(self, docname, src_path=None, save_parsed=True, app=None): """ Parse a file and add/update inventory entries for the doctree. @@ -555,6 +599,7 @@ class BuildEnvironment: # remove all inventory entries for that file if app: app.emit('env-purge-doc', self, docname) + self.clear_doc(docname) if src_path is None: @@ -569,12 +614,16 @@ class BuildEnvironment: self.warn(docname, 'default role %s not found' % self.config.default_role) - self.docname = docname + self.doc_read_data['docname'] = docname + # defaults to the global default, but can be re-set in a document + self.doc_read_data['default_domain'] = \ + self.domains.get(self.config.default_domain) + self.settings['input_encoding'] = self.config.source_encoding self.settings['trim_footnote_reference_space'] = \ self.config.trim_footnote_reference_space - codecs.register_error('sphinx', self.warn_and_replace) + self.patch_lookup_functions() codecs.register_error('sphinx', self.warn_and_replace) @@ -607,6 +656,8 @@ class BuildEnvironment: doctree = pub.document except UnicodeError, err: raise SphinxError(str(err)) + + # post-processing self.filter_messages(doctree) self.process_dependencies(docname, doctree) self.process_images(docname, doctree) @@ -618,12 +669,13 @@ class BuildEnvironment: self.note_citations_from(docname, doctree) self.build_toc_from(docname, doctree) - # store time of reading, used to find outdated files - self.all_docs[docname] = time.time() - + # allow extension-specific post-processing if app: app.emit('doctree-read', doctree) + # store time of reading, used to find outdated files + self.all_docs[docname] = time.time() + # make it picklable doctree.reporter = None doctree.transformer = None @@ -635,10 +687,7 @@ class BuildEnvironment: metanode.__class__ = addnodes.meta # cleanup - self.docname = None - self.currmodule = None - self.currclass = None - self.gloss_entries = set() + self.doc_read_data.clear() if save_parsed: # save the parsed doctree @@ -655,6 +704,35 @@ class BuildEnvironment: else: return doctree + # utilities to use while reading a document + + @property + def docname(self): + """Backwards compatible alias.""" + return self.doc_read_data['docname'] + + @property + def currmodule(self): + """Backwards compatible alias.""" + return self.doc_read_data.get('py:module') + + @property + def currclass(self): + """Backwards compatible alias.""" + return self.doc_read_data.get('py:class') + + def new_serialno(self, category=''): + """Return a serial number, e.g. for index entry targets.""" + key = category + 'serialno' + cur = self.doc_read_data.get(key, 0) + self.doc_read_data[key] = cur + 1 + return cur + + def note_dependency(self, filename): + self.dependencies.setdefault(self.docname, set()).add(filename) + + # post-processing of read doctrees + def filter_messages(self, doctree): """ Filter system messages from a doctree. @@ -816,7 +894,7 @@ class BuildEnvironment: if name.isdigit() or node.has_key('refuri') or \ node.tagname.startswith('desc_'): # ignore footnote labels, labels automatically generated from a - # link and description units + # link and object descriptions continue if name in self.labels: self.warn(docname, 'duplicate label %s, ' % name + @@ -846,11 +924,11 @@ class BuildEnvironment: def note_citations_from(self, docname, document): for node in document.traverse(nodes.citation): label = node[0].astext() - if ('citation', label) in self.reftargets: + if label in self.citations: self.warn(docname, 'duplicate citation %s, ' % label + 'other instance in %s' % self.doc2path( - self.reftargets['citation', label][0]), node.line) - self.reftargets['citation', label] = (docname, node['ids'][0]) + self.citations[label][0]), node.line) + self.citations[label] = (docname, node['ids'][0]) def note_toctree(self, docname, toctreenode): """Note a TOC tree directive in a document and gather information about @@ -938,45 +1016,23 @@ class BuildEnvironment: node['refuri'] = node['anchorname'] or '#' return toc - def get_toctree_for(self, docname, builder, collapse): + def get_toctree_for(self, docname, builder, collapse, maxdepth=0): """Return the global TOC nodetree.""" doctree = self.get_doctree(self.config.master_doc) for toctreenode in doctree.traverse(addnodes.toctree): result = self.resolve_toctree(docname, builder, toctreenode, - prune=True, collapse=collapse) + prune=True, collapse=collapse, + maxdepth=maxdepth) if result is not None: return result - # ------- - # these are called from docutils directives and therefore use self.docname - # - def note_descref(self, fullname, desctype, line): - if fullname in self.descrefs: - self.warn(self.docname, - 'duplicate canonical description name %s, ' % fullname + - 'other instance in ' + - self.doc2path(self.descrefs[fullname][0]), - line) - self.descrefs[fullname] = (self.docname, desctype) - - def note_module(self, modname, synopsis, platform, deprecated): - self.modules[modname] = (self.docname, synopsis, platform, deprecated) - self.filemodules.setdefault(self.docname, []).append(modname) - - def note_progoption(self, optname, labelid): - self.progoptions[self.currprogram, optname] = (self.docname, labelid) - - def note_reftarget(self, type, name, labelid): - self.reftargets[type, name] = (self.docname, labelid) - - def note_versionchange(self, type, version, node, lineno): - self.versionchanges.setdefault(version, []).append( - (type, self.docname, lineno, self.currmodule, self.currdesc, - node.astext())) - - def note_dependency(self, filename): - self.dependencies.setdefault(self.docname, set()).add(filename) - # ------- + def get_domain(self, domainname): + """Return the domain instance with the specified name. + Raises an ExtensionError if the domain is not registered.""" + try: + return self.domains[domainname] + except KeyError: + raise ExtensionError('Domain %r is not registered' % domainname) # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------ @@ -1160,15 +1216,7 @@ class BuildEnvironment: docname, refnode['refuri']) + refnode['anchorname'] return newnode - descroles = frozenset(('data', 'exc', 'func', 'class', 'const', - 'attr', 'obj', 'meth', 'cfunc', 'cmember', - 'cdata', 'ctype', 'cmacro')) - def resolve_references(self, doctree, fromdocname, builder): - reftarget_roles = set(('token', 'term', 'citation')) - # add all custom xref types too - reftarget_roles.update(i[0] for i in additional_xref_types.values()) - for node in doctree.traverse(addnodes.pending_xref): contnode = node[0].deepcopy() newnode = None @@ -1178,8 +1226,17 @@ class BuildEnvironment: refdoc = node.get('refdoc', fromdocname) try: - if typ == 'ref': - if node['refcaption']: + if node.has_key('refdomain') and node['refdomain']: + # let the domain try to resolve the reference + try: + domain = self.domains[node['refdomain']] + except KeyError: + raise NoUri + newnode = domain.resolve_xref(self, fromdocname, builder, + typ, target, node, contnode) + # really hardwired reference types + elif typ == 'ref': + if node['refexplicit']: # reference to anonymous label; the reference uses # the supplied link caption docname, labelid = self.anonlabels.get(target, ('','')) @@ -1188,7 +1245,7 @@ class BuildEnvironment: self.warn(refdoc, 'undefined label: %s' % target, node.line) else: - # reference to the named label; the final node will + # reference to named label; the final node will # contain the section name after the label docname, labelid, sectname = self.labels.get(target, ('','','')) @@ -1214,18 +1271,15 @@ class BuildEnvironment: if labelid: newnode['refuri'] += '#' + labelid newnode.append(innernode) - else: - newnode = contnode elif typ == 'doc': # directly reference to document by source name; # can be absolute or relative docname = docname_join(refdoc, target) if docname not in self.all_docs: - self.warn(refdoc, 'unknown document: %s' % docname, - node.line) - newnode = contnode + self.warn(refdoc, + 'unknown document: %s' % docname, node.line) else: - if node['refcaption']: + if node['refexplicit']: # reference with explicit title caption = node.astext() else: @@ -1235,100 +1289,34 @@ class BuildEnvironment: newnode['refuri'] = builder.get_relative_uri( fromdocname, docname) newnode.append(innernode) + elif typ == 'citation': + docname, labelid = self.citations.get(target, ('', '')) + if not docname: + self.warn(node['refdoc'], + 'citation not found: %s' % target, node.line) + else: + newnode = make_refnode(builder, fromdocname, docname, + labelid, contnode) elif typ == 'keyword': - # keywords are referenced by named labels + # keywords are oddballs: they are referenced by named labels docname, labelid, _ = self.labels.get(target, ('','','')) if not docname: #self.warn(refdoc, 'unknown keyword: %s' % target) - newnode = contnode - else: - newnode = nodes.reference('', '') - if docname == fromdocname: - newnode['refid'] = labelid - else: - newnode['refuri'] = builder.get_relative_uri( - fromdocname, docname) + '#' + labelid - newnode.append(contnode) - elif typ == 'option': - progname = node['refprogram'] - docname, labelid = self.progoptions.get((progname, target), - ('', '')) - if not docname: - newnode = contnode + pass else: - newnode = nodes.reference('', '') - if docname == fromdocname: - newnode['refid'] = labelid - else: - newnode['refuri'] = builder.get_relative_uri( - fromdocname, docname) + '#' + labelid - newnode.append(contnode) - elif typ in reftarget_roles: - docname, labelid = self.reftargets.get((typ, target), - ('', '')) - if not docname: - if typ == 'term': - self.warn(refdoc, - 'term not in glossary: %s' % target, - node.line) - elif typ == 'citation': - self.warn(refdoc, 'citation not found: %s' % target, - node.line) - newnode = contnode - else: - newnode = nodes.reference('', '') - if docname == fromdocname: - newnode['refid'] = labelid - else: - newnode['refuri'] = builder.get_relative_uri( - fromdocname, docname, typ) + '#' + labelid - newnode.append(contnode) - elif typ == 'mod' or \ - typ == 'obj' and target in self.modules: - docname, synopsis, platform, deprecated = \ - self.modules.get(target, ('','','', '')) - if not docname: - newnode = builder.app.emit_firstresult( - 'missing-reference', self, node, contnode) - if not newnode: - newnode = contnode - else: - newnode = nodes.reference('', '') - newnode['refuri'] = builder.get_relative_uri( - fromdocname, docname) + '#module-' + target - newnode['reftitle'] = '%s%s%s' % ( - (platform and '(%s) ' % platform), - synopsis, (deprecated and ' (deprecated)' or '')) - newnode.append(contnode) - elif typ in self.descroles: - # "descrefs" - modname = node['modname'] - clsname = node['classname'] - searchorder = node.hasattr('refspecific') and 1 or 0 - name, desc = self.find_desc(modname, clsname, - target, typ, searchorder) - if not desc: - newnode = builder.app.emit_firstresult( - 'missing-reference', self, node, contnode) - if not newnode: - newnode = contnode - else: - newnode = nodes.reference('', '') - if desc[0] == fromdocname: - newnode['refid'] = name - else: - newnode['refuri'] = ( - builder.get_relative_uri(fromdocname, desc[0]) - + '#' + name) - newnode['reftitle'] = name - newnode.append(contnode) + newnode = make_refnode(builder, fromdocname, docname, + labelid, contnode) else: raise RuntimeError('unknown xfileref node encountered: %s' % node) + + # no new node found? try the missing-reference event + if newnode is None: + newnode = builder.app.emit_firstresult( + 'missing-reference', self, node, contnode) except NoUri: newnode = contnode - if newnode: - node.replace_self(newnode) + node.replace_self(newnode or contnode) for node in doctree.traverse(addnodes.only): try: @@ -1492,7 +1480,7 @@ class BuildEnvironment: i += 1 # group the entries by letter - def keyfunc((k, v), letters=string.ascii_uppercase + '_'): + def keyfunc2((k, v), letters=string.ascii_uppercase + '_'): # hack: mutating the subitems dicts to a list in the keyfunc v[1] = sorted((si, se) for (si, (se, void)) in v[1].iteritems()) # now calculate the key @@ -1503,7 +1491,7 @@ class BuildEnvironment: # get all other symbols under one heading return 'Symbols' return [(key, list(group)) - for (key, group) in groupby(newlist, keyfunc)] + for (key, group) in groupby(newlist, keyfunc2)] def collect_relations(self): relations = {} @@ -1559,116 +1547,3 @@ class BuildEnvironment: # the master file is not included anywhere ;) continue self.warn(docname, 'document isn\'t included in any toctree') - - # --------- QUERYING ------------------------------------------------------- - - def find_desc(self, modname, classname, name, type, searchorder=0): - """Find a description node matching "name", perhaps using - the given module and/or classname.""" - # skip parens - if name[-2:] == '()': - name = name[:-2] - - if not name: - return None, None - - # don't add module and class names for C things - if type[0] == 'c' and type not in ('class', 'const'): - # skip trailing star and whitespace - name = name.rstrip(' *') - if name in self.descrefs and self.descrefs[name][1][0] == 'c': - return name, self.descrefs[name] - return None, None - - newname = None - if searchorder == 1: - if modname and classname and \ - modname + '.' + classname + '.' + name in self.descrefs: - newname = modname + '.' + classname + '.' + name - elif modname and modname + '.' + name in self.descrefs: - newname = modname + '.' + name - elif name in self.descrefs: - newname = name - else: - if name in self.descrefs: - newname = name - elif modname and modname + '.' + name in self.descrefs: - newname = modname + '.' + name - elif modname and classname and \ - modname + '.' + classname + '.' + name in self.descrefs: - newname = modname + '.' + classname + '.' + name - # special case: builtin exceptions have module "exceptions" set - elif type == 'exc' and '.' not in name and \ - 'exceptions.' + name in self.descrefs: - newname = 'exceptions.' + name - # special case: object methods - elif type in ('func', 'meth') and '.' not in name and \ - 'object.' + name in self.descrefs: - newname = 'object.' + name - if newname is None: - return None, None - return newname, self.descrefs[newname] - - def find_keyword(self, keyword, avoid_fuzzy=False, cutoff=0.6, n=20): - """ - Find keyword matches for a keyword. If there's an exact match, - just return it, else return a list of fuzzy matches if avoid_fuzzy - isn't True. - - Keywords searched are: first modules, then descrefs. - - Returns: None if nothing found - (type, docname, anchorname) if exact match found - list of (quality, type, docname, anchorname, description) - if fuzzy - """ - - if keyword in self.modules: - docname, title, system, deprecated = self.modules[keyword] - return 'module', docname, 'module-' + keyword - if keyword in self.descrefs: - docname, ref_type = self.descrefs[keyword] - return ref_type, docname, keyword - # special cases - if '.' not in keyword: - # exceptions are documented in the exceptions module - if 'exceptions.'+keyword in self.descrefs: - docname, ref_type = self.descrefs['exceptions.'+keyword] - return ref_type, docname, 'exceptions.'+keyword - # special methods are documented as object methods - if 'object.'+keyword in self.descrefs: - docname, ref_type = self.descrefs['object.'+keyword] - return ref_type, docname, 'object.'+keyword - - if avoid_fuzzy: - return - - # find fuzzy matches - s = difflib.SequenceMatcher() - s.set_seq2(keyword.lower()) - - def possibilities(): - for title, (fn, desc, _, _) in self.modules.iteritems(): - yield ('module', fn, 'module-'+title, desc) - for title, (fn, desctype) in self.descrefs.iteritems(): - yield (desctype, fn, title, '') - - def dotsearch(string): - parts = string.lower().split('.') - for idx in xrange(0, len(parts)): - yield '.'.join(parts[idx:]) - - result = [] - for type, docname, title, desc in possibilities(): - best_res = 0 - for part in dotsearch(title): - s.set_seq1(part) - if s.real_quick_ratio() >= cutoff and \ - s.quick_ratio() >= cutoff and \ - s.ratio() >= cutoff and \ - s.ratio() > best_res: - best_res = s.ratio() - if best_res: - result.append((best_res, type, docname, title, desc)) - - return heapq.nlargest(n, result) |