diff --git a/sphinx/application.py b/sphinx/application.py index 5e34232c0..7d9ee49db 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -14,19 +14,21 @@ import sys import types import posixpath +from os import path from cStringIO import StringIO from docutils import nodes from docutils.parsers.rst import directives, roles import sphinx +from sphinx import package_dir, locale from sphinx.roles import XRefRole from sphinx.config import Config from sphinx.errors import SphinxError, SphinxWarning, ExtensionError -from sphinx.domains import domains +from sphinx.domains import all_domains from sphinx.builders import BUILTIN_BUILDERS from sphinx.directives import GenericDesc, Target, additional_xref_types -from sphinx.environment import SphinxStandaloneReader +from sphinx.environment import BuildEnvironment, SphinxStandaloneReader from sphinx.util import pycompat from sphinx.util.tags import Tags from sphinx.util.compat import Directive, directive_dwim @@ -50,6 +52,7 @@ events = { } CONFIG_FILENAME = 'conf.py' +ENV_PICKLE_FILENAME = 'environment.pickle' class Sphinx(object): @@ -62,6 +65,7 @@ class Sphinx(object): self._listeners = {} self.builderclasses = BUILTIN_BUILDERS.copy() self.builder = None + self.env = None self.srcdir = srcdir self.confdir = confdir @@ -104,8 +108,62 @@ class Sphinx(object): # now that we know all config values, collect them from conf.py self.config.init_values() + # set up translation infrastructure + self._init_i18n() + # set up the build environment + self._init_env(freshenv) + # set up the builder + self._init_builder(buildername) + + def _init_i18n(self): + """ + Load translated strings from the configured localedirs if + enabled in the configuration. + """ + if self.config.language is not None: + self.info(bold('loading translations [%s]... ' % + self.config.language), nonl=True) + locale_dirs = [None, path.join(package_dir, 'locale')] + \ + [path.join(self.srcdir, x) for x in self.config.locale_dirs] + else: + locale_dirs = [] + self.translator, has_translation = locale.init(locale_dirs, + self.config.language) + if self.config.language is not None: + if has_translation: + self.info('done') + else: + self.info('locale not available') + + def _init_env(self, freshenv): + if freshenv: + self.env = BuildEnvironment(self.srcdir, self.doctreedir, + self.config) + self.env.find_files(self.config) + for domain in all_domains.keys(): + self.env.domains[domain] = all_domains[domain](self.env) + else: + try: + self.info(bold('loading pickled environment... '), nonl=True) + self.env = BuildEnvironment.frompickle(self.config, + path.join(self.doctreedir, ENV_PICKLE_FILENAME)) + self.env.domains = {} + for domain in all_domains.keys(): + # this can raise if the data version doesn't fit + self.env.domains[domain] = all_domains[domain](self.env) + self.info('done') + except Exception, err: + if type(err) is IOError and err.errno == 2: + self.info('not yet created') + else: + self.info('failed: %s' % err) + return self._init_env(freshenv=True) + + self.env.set_warnfunc(self.warn) + + def _init_builder(self, buildername): if buildername is None: - print >>status, 'No builder selected, using default: html' + print >>self._status, 'No builder selected, using default: html' buildername = 'html' if buildername not in self.builderclasses: raise SphinxError('Builder name %s not registered' % buildername) @@ -116,9 +174,7 @@ class Sphinx(object): mod, cls = builderclass builderclass = getattr( __import__('sphinx.builders.' + mod, None, None, [cls]), cls) - self.builder = builderclass(self, freshenv=freshenv) - self.builder.tags = self.tags - self.builder.tags.add(self.builder.format) + self.builder = builderclass(self) self.emit('builder-inited') def build(self, all_files, filenames): @@ -300,9 +356,9 @@ class Sphinx(object): roles.register_local_role(name, role) def add_domain(self, domain): - if domain.name in domains: + if domain.name in all_domains: raise ExtensionError('domain %s already registered' % domain.name) - domains[domain.name] = domain + all_domains[domain.name] = domain def add_description_unit(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None): @@ -311,7 +367,7 @@ class Sphinx(object): directives.register_directive(directivename, directive_dwim(GenericDesc)) # XXX support more options? - role_func = XRefRole('', innernodeclass=ref_nodeclass) + role_func = XRefRole(innernodeclass=ref_nodeclass) roles.register_local_role(rolename, role_func) def add_crossref_type(self, directivename, rolename, indextemplate='', @@ -319,7 +375,7 @@ class Sphinx(object): additional_xref_types[directivename] = (rolename, indextemplate, None) directives.register_directive(directivename, directive_dwim(Target)) # XXX support more options - role_func = XRefRole('', innernodeclass=ref_nodeclass) + role_func = XRefRole(innernodeclass=ref_nodeclass) roles.register_local_role(rolename, role_func) def add_transform(self, transform): diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 94b16b0a9..ca3c11473 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -15,9 +15,7 @@ from os import path from docutils import nodes -from sphinx import package_dir, locale from sphinx.util import SEP, relative_uri -from sphinx.environment import BuildEnvironment from sphinx.util.console import bold, purple, darkgreen, term_width_line # side effect: registers roles and directives @@ -25,9 +23,6 @@ from sphinx import roles from sphinx import directives -ENV_PICKLE_FILENAME = 'environment.pickle' - - class Builder(object): """ Builds target formats from the reST sources. @@ -38,7 +33,8 @@ class Builder(object): # builder's output format, or '' if no document output is produced format = '' - def __init__(self, app, env=None, freshenv=False): + def __init__(self, app): + self.env = app.env self.srcdir = app.srcdir self.confdir = app.confdir self.outdir = app.outdir @@ -50,18 +46,13 @@ class Builder(object): self.warn = app.warn self.info = app.info self.config = app.config - - self.load_i18n() + self.tags = app.tags + self.tags.add(self.format) # images that need to be copied over (source -> dest) self.images = {} - # if None, this is set in load_env() - self.env = env - self.freshenv = freshenv - self.init() - self.load_env() # helper methods @@ -167,50 +158,6 @@ class Builder(object): # build methods - def load_i18n(self): - """ - Load translated strings from the configured localedirs if - enabled in the configuration. - """ - if self.config.language is not None: - self.info(bold('loading translations [%s]... ' % - self.config.language), nonl=True) - locale_dirs = [None, path.join(package_dir, 'locale')] + \ - [path.join(self.srcdir, x) for x in self.config.locale_dirs] - else: - locale_dirs = [] - self.translator, has_translation = locale.init(locale_dirs, - self.config.language) - if self.config.language is not None: - if has_translation: - self.info('done') - else: - self.info('locale not available') - - def load_env(self): - """Set up the build environment.""" - if self.env: - return - if not self.freshenv: - try: - self.info(bold('loading pickled environment... '), nonl=True) - self.env = BuildEnvironment.frompickle(self.config, - path.join(self.doctreedir, ENV_PICKLE_FILENAME)) - self.info('done') - except Exception, err: - if type(err) is IOError and err.errno == 2: - self.info('not found') - else: - self.info('failed: %s' % err) - self.env = BuildEnvironment(self.srcdir, self.doctreedir, - self.config) - self.env.find_files(self.config) - else: - self.env = BuildEnvironment(self.srcdir, self.doctreedir, - self.config) - self.env.find_files(self.config) - self.env.set_warnfunc(self.warn) - def build_all(self): """Build all source files.""" self.build(None, summary='all source files', method='all') @@ -290,6 +237,7 @@ class Builder(object): if updated_docnames: # save the environment + from sphinx.application import ENV_PICKLE_FILENAME self.info(bold('pickling environment... '), nonl=True) self.env.topickle(path.join(self.doctreedir, ENV_PICKLE_FILENAME)) self.info('done') diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 54327fa1e..02f621640 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -33,7 +33,8 @@ from sphinx.util import SEP, os_path, relative_uri, ensuredir, \ from sphinx.errors import SphinxError from sphinx.search import js_index from sphinx.theming import Theme -from sphinx.builders import Builder, ENV_PICKLE_FILENAME +from sphinx.builders import Builder +from sphinx.application import ENV_PICKLE_FILENAME from sphinx.highlighting import PygmentsBridge from sphinx.util.console import bold from sphinx.writers.html import HTMLWriter, HTMLTranslator, \ @@ -239,7 +240,9 @@ class StandaloneHTMLBuilder(Builder): rellinks = [] if self.config.html_use_index: rellinks.append(('genindex', _('General Index'), 'I', _('index'))) - if self.config.html_use_modindex and self.env.modules: + # XXX generalization of modindex? + if self.config.html_use_modindex and \ + self.env.domaindata['py']['modules']: rellinks.append(('modindex', _('Global Module Index'), 'M', _('modules'))) @@ -404,12 +407,13 @@ class StandaloneHTMLBuilder(Builder): # the global module index - if self.config.html_use_modindex and self.env.modules: + moduleindex = self.env.domaindata['py']['modules'] + if self.config.html_use_modindex and moduleindex: # the sorted list of all modules, for the global module index modules = sorted(((mn, (self.get_relative_uri('modindex', fn) + '#module-' + mn, sy, pl, dep)) for (mn, (fn, sy, pl, dep)) in - self.env.modules.iteritems()), + moduleindex.iteritems()), key=lambda x: x[0].lower()) # collect all platforms platforms = set() @@ -709,14 +713,15 @@ class StandaloneHTMLBuilder(Builder): self.info(bold('dumping object inventory... '), nonl=True) f = open(path.join(self.outdir, INVENTORY_FILENAME), 'w') try: + # XXX inventory version 2 f.write('# Sphinx inventory version 1\n') f.write('# Project: %s\n' % self.config.project.encode('utf-8')) f.write('# Version: %s\n' % self.config.version) - for modname, info in self.env.modules.iteritems(): - f.write('%s mod %s\n' % (modname, self.get_target_uri(info[0]))) - for refname, (docname, desctype) in self.env.descrefs.iteritems(): - f.write('%s %s %s\n' % (refname, desctype, - self.get_target_uri(docname))) + #for modname, info in self.env.modules.iteritems(): + # f.write('%s mod %s\n' % (modname, self.get_target_uri(info[0]))) + #for refname, (docname, desctype) in self.env.descrefs.iteritems(): + # f.write('%s %s %s\n' % (refname, desctype, + # self.get_target_uri(docname))) finally: f.close() self.info('done') diff --git a/sphinx/directives/desc.py b/sphinx/directives/desc.py index dfd713821..06fc4c983 100644 --- a/sphinx/directives/desc.py +++ b/sphinx/directives/desc.py @@ -204,7 +204,10 @@ class DescDirective(Directive): pass def run(self): - self.desctype = self.name + if ':' in self.name: + self.domain, self.desctype = self.name.split(':', 1) + else: + self.domain, self.desctype = '', self.name self.env = self.state.document.settings.env self.indexnode = addnodes.index(entries=[]) @@ -366,7 +369,7 @@ class DefaultDomain(Directive): def run(self): env = self.state.document.settings.env domain_name = arguments[0] - env.default_domain = domains.get(domain_name) + env.default_domain = env.domains.get(domain_name) # Note: the target directive is not registered here, it is used by the diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index d056f3dce..ee938cfd9 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -103,75 +103,6 @@ class TocTree(Directive): return ret -class Module(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.currmodule = modname - 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], ismod=True) - 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 - - -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. - """ - - 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.currmodule = None - else: - env.currmodule = modname - return [] - - class Author(Directive): """ Directive to give the name of the author of the current document @@ -559,8 +490,6 @@ class Only(Directive): directives.register_directive('toctree', directive_dwim(TocTree)) -directives.register_directive('module', directive_dwim(Module)) -directives.register_directive('currentmodule', directive_dwim(CurrentModule)) directives.register_directive('sectionauthor', directive_dwim(Author)) directives.register_directive('moduleauthor', directive_dwim(Author)) directives.register_directive('program', directive_dwim(Program)) diff --git a/sphinx/domains.py b/sphinx/domains.py index 9fa41090f..d40915472 100644 --- a/sphinx/domains.py +++ b/sphinx/domains.py @@ -4,17 +4,23 @@ ~~~~~~~~~~~~~~ Support for domains, which are groupings of description directives - describing e.g. constructs of one programming language. + and roles describing e.g. constructs of one programming language. :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ import re +import string + +from docutils import nodes +from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.roles import XRefRole from sphinx.directives import DescDirective +from sphinx.util import make_refnode +from sphinx.util.compat import Directive class Domain(object): @@ -23,6 +29,68 @@ class Domain(object): roles = {} label = '' + # data value for a fresh environment + initial_data = {} + # data version + data_version = 0 + + def __init__(self, env): + self.env = env + if self.name not in env.domaindata: + new_data = self.initial_data.copy() + new_data['version'] = self.data_version + self.data = env.domaindata[self.name] = new_data + else: + self.data = env.domaindata[self.name] + if self.data['version'] < self.data_version: + raise IOError('data of %r domain out of date' % self.label) + self._role_cache = {} + self._directive_cache = {} + + def clear_doc(self, docname): + """ + Remove traces of a document in the domain-specific inventories. + """ + pass + + def role(self, name): + """ + Return a role adapter function that always gives the registered + role its full name ('domain:name') as the first argument. + """ + if name in self._role_cache: + return self._role_cache[name] + if name not in self.roles: + return None + fullname = '%s:%s' % (self.name, name) + def role_adapter(typ, rawtext, text, lineno, inliner, + options={}, content=[]): + return self.roles[name](fullname, rawtext, text, lineno, + inliner, options, content) + self._role_cache[name] = role_adapter + return role_adapter + + def directive(self, name): + """ + Return a directive adapter class that always gives the registered + directive its full name ('domain:name') as ``self.name``. + """ + if name in self._directive_cache: + return self._directive_cache[name] + if name not in self.directives: + return None + fullname = '%s:%s' % (self.name, name) + BaseDirective = self.directives[name] + class DirectiveAdapter(BaseDirective): + def run(self): + self.name = fullname + return BaseDirective.run(self) + self._directive_cache[name] = DirectiveAdapter + return DirectiveAdapter + + def resolve_xref(self, typ, target, node, contnode): + pass + # REs for Python signatures py_sig_re = re.compile( @@ -146,7 +214,15 @@ class PythonDesc(DescDirective): 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) + 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.desctype) indextext = self.get_index_text(modname, name_cls) if indextext: @@ -286,6 +362,75 @@ class ClassmemberDesc(PythonDesc): 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.currmodule = 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] + 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.currmodule = None + else: + env.currmodule = modname + return [] + + class PyXRefRole(XRefRole): def process_link(self, env, pnode, has_explicit_title, title, target): pnode['modname'] = env.currmodule @@ -321,18 +466,105 @@ class PythonDomain(Domain): 'classmethod': ClassmemberDesc, 'staticmethod': ClassmemberDesc, 'attribute': ClassmemberDesc, + 'module': PyModule, + 'currentmodule': PyCurrentModule, } roles = { - 'data': PyXRefRole('py'), - 'exc': PyXRefRole('py'), - 'func': PyXRefRole('py', True), - 'class': PyXRefRole('py'), - 'const': PyXRefRole('py'), - 'attr': PyXRefRole('py'), - 'meth': PyXRefRole('py', True), - 'mod': PyXRefRole('py'), - 'obj': PyXRefRole('py'), + '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, desctype + '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_desc(self, env, modname, classname, name, type, searchorder=0): + """ + Find a Python object description 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 + elif docname == fromdocname: + # don't link to self + return contnode + 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['modname'] + clsname = node['classname'] + searchorder = node.hasattr('refspecific') and 1 or 0 + name, desc = self.find_desc(env, modname, clsname, + target, typ, searchorder) + if not desc: + return None + else: + return make_refnode(builder, fromdocname, desc[0], name, + contnode, name) + # RE to split at word boundaries @@ -370,7 +602,7 @@ class CDesc(DescDirective): if part[0] in string.ascii_letters+'_' and \ part not in self.stopwords: pnode = addnodes.pending_xref( - '', reftype='ctype', reftarget=part, + '', refdomain='c', reftype='type', reftarget=part, modname=None, classname=None) pnode += tnode node += pnode @@ -451,7 +683,14 @@ class CDesc(DescDirective): signode['ids'].append(name) signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) - self.env.note_descref(name, self.desctype, self.lineno) + inv = self.env.domaindata['c']['objects'] + if name in inv: + self.env.warn( + self.env.docname, + 'duplicate C object description of %s, ' % name + + 'other instance in ' + self.env.doc2path(inv[name][0]), + self.lineno) + inv[name] = (self.env.docname, self.desctype) indextext = self.get_index_text(name) if indextext: @@ -470,16 +709,33 @@ class CDomain(Domain): 'var': CDesc, } roles = { - 'member': XRefRole('c'), - 'macro': XRefRole('c'), - 'func' : XRefRole('c', True), - 'data': XRefRole('c'), - 'type': XRefRole('c'), + 'member': XRefRole(), + 'macro': XRefRole(), + 'func' : XRefRole(fix_parens=True), + 'data': XRefRole(), + 'type': XRefRole(), } + initial_data = { + 'objects': {}, # fullname -> docname, desctype + } + + def clear_doc(self, docname): + for fullname, (fn, _) in self.data['objects'].items(): + if fn == docname: + del self.data['objects'][fullname] + + def resolve_xref(self, env, fromdocname, builder, + typ, target, node, contnode): + # strip pointer asterisk + target = target.rstrip(' *') + if target not in self.data: + return None + desc = self.data[target] + return make_refnode(builder, fromdocname, desc[0], contnode, target) # this contains all registered domains -domains = { +all_domains = { 'py': PythonDomain, 'c': CDomain, } diff --git a/sphinx/environment.py b/sphinx/environment.py index 9393b6ff9..2f64b147b 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -43,9 +43,8 @@ from docutils.transforms.parts import ContentsFilter from sphinx import addnodes from sphinx.util import movefile, get_matching_docs, SEP, ustrftime, \ - docname_join, FilenameUniqDict, url_re + docname_join, FilenameUniqDict, url_re, make_refnode from sphinx.errors import SphinxError -from sphinx.domains import domains from sphinx.directives import additional_xref_types orig_role_function = roles.role @@ -214,9 +213,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): @@ -225,6 +224,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') @@ -241,6 +242,7 @@ class BuildEnvironment: picklefile.close() movefile(filename + '.tmp', filename) # reset attributes + self.domains = domains self.config.values = values self.set_warnfunc(warnfunc) @@ -254,6 +256,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 @@ -292,10 +297,10 @@ 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 object + # X-ref target inventory - self.descrefs = {} # fullname -> docname, desctype - self.modules = {} # modname -> docname, synopsis, - # platform, deprecated self.labels = {} # labelname -> docname, labelid, sectionname self.anonlabels = {} # labelname -> docname, labelid self.progoptions = {} # (program, name) -> docname, labelid @@ -363,12 +368,6 @@ 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] @@ -382,6 +381,10 @@ class BuildEnvironment: 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. @@ -561,6 +564,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: @@ -600,18 +604,20 @@ class BuildEnvironment: return data # defaults to the global default, but can be re-set in a document - self.default_domain = domains.get(self.config.default_domain) + self.default_domain = self.domains.get(self.config.default_domain) # monkey-patch, so that domain directives take precedence def directive(directive_name, language_module, document): + directive_name = directive_name.lower() if ':' in directive_name: domain_name, directive_name = directive_name.split(':', 1) - if domain_name in domains: - domain = domains[domain_name] - if directive_name in domain.directives: - return domain.directives[directive_name], [] + if domain_name in self.domains: + domain = self.domains[domain_name] + directive = domain.directive(directive_name) + if directive is not None: + return directive, [] elif self.default_domain is not None: - directive = self.default_domain.directives.get(directive_name) + directive = self.default_domain.directive(directive_name) if directive is not None: return directive, [] return orig_directive_function(directive_name, language_module, @@ -619,14 +625,16 @@ class BuildEnvironment: directives.directive = directive def role(role_name, language_module, lineno, reporter): + role_name = role_name.lower() if ':' in role_name: domain_name, role_name = role_name.split(':', 1) - if domain_name in domains: - domain = domains[domain_name] - if role_name in domain.roles: - return domain.roles[role_name], [] + if domain_name in self.domains: + domain = self.domains[domain_name] + role = domain.role(role_name) + if role is not None: + return role, [] elif self.default_domain is not None: - role = self.default_domain.roles.get(role_name) + role = self.default_domain.role(role_name) if role is not None: return role, [] return orig_role_function(role_name, language_module, @@ -678,6 +686,7 @@ class BuildEnvironment: self.docname = None self.currmodule = None self.currclass = None + self.default_domain = None self.gloss_entries = set() if save_parsed: @@ -987,18 +996,6 @@ class BuildEnvironment: # ------- # 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) - def note_progoption(self, optname, labelid): self.progoptions[self.currprogram, optname] = (self.docname, labelid) @@ -1196,11 +1193,8 @@ 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): + # XXX remove this reftarget_roles = set(('token', 'term', 'citation')) # add all custom xref types too reftarget_roles.update(i[0] for i in additional_xref_types.values()) @@ -1213,7 +1207,15 @@ class BuildEnvironment: target = node['reftarget'] try: - if typ == 'ref': + if node.has_key('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) + elif typ == 'ref': if node['refcaption']: # reference to anonymous label; the reference uses # the supplied link caption @@ -1278,13 +1280,8 @@ class BuildEnvironment: #self.warn(fromdocname, '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) + newnode = make_refnode(builder, fromdocname, docname, + labelid, contnode) elif typ == 'option': progname = node['refprogram'] docname, labelid = self.progoptions.get((progname, target), @@ -1292,13 +1289,8 @@ class BuildEnvironment: if not docname: 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) + newnode = make_refnode(builder, fromdocname, docname, + labelid, contnode) elif typ in reftarget_roles: docname, labelid = self.reftargets.get((typ, target), ('', '')) @@ -1313,62 +1305,19 @@ class BuildEnvironment: 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 - elif docname == fromdocname: - # don't link to self - 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: @@ -1592,52 +1541,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] diff --git a/sphinx/ext/coverage.py b/sphinx/ext/coverage.py index 964e58eec..ed5d92a4f 100644 --- a/sphinx/ext/coverage.py +++ b/sphinx/ext/coverage.py @@ -79,6 +79,7 @@ class CoverageBuilder(Builder): def build_c_coverage(self): # Fetch all the info from the header files + c_objects = self.env.domaindata['c']['objects'] for filename in self.c_sourcefiles: undoc = [] f = open(filename, 'r') @@ -88,7 +89,7 @@ class CoverageBuilder(Builder): match = regex.match(line) if match: name = match.groups()[0] - if name not in self.env.descrefs: + if name not in c_objects: for exp in self.c_ignorexps.get(key, ()): if exp.match(name): break @@ -116,7 +117,10 @@ class CoverageBuilder(Builder): op.close() def build_py_coverage(self): - for mod_name in self.env.modules: + objects = self.env.domaindata['py']['objects'] + modules = self.env.domaindata['py']['modules'] + + for mod_name in modules: ignore = False for exp in self.mod_ignorexps: if exp.match(mod_name): @@ -151,7 +155,7 @@ class CoverageBuilder(Builder): full_name = '%s.%s' % (mod_name, name) if inspect.isfunction(obj): - if full_name not in self.env.descrefs: + if full_name not in objects: for exp in self.fun_ignorexps: if exp.match(name): break @@ -162,7 +166,7 @@ class CoverageBuilder(Builder): if exp.match(name): break else: - if full_name not in self.env.descrefs: + if full_name not in objects: # not documented at all classes[name] = [] continue @@ -176,7 +180,7 @@ class CoverageBuilder(Builder): continue full_attr_name = '%s.%s' % (full_name, attr_name) - if full_attr_name not in self.env.descrefs: + if full_attr_name not in objects: attrs.append(attr_name) if attrs: diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index c584a0cde..626e7f8a8 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -94,7 +94,7 @@ class BuiltinTemplateLoader(TemplateBridge, BaseLoader): # make the paths into loaders self.loaders = map(SphinxFileSystemLoader, chain) - use_i18n = builder.translator is not None + use_i18n = builder.app.translator is not None extensions = use_i18n and ['jinja2.ext.i18n'] or [] self.environment = SandboxedEnvironment(loader=self, extensions=extensions) @@ -102,7 +102,7 @@ class BuiltinTemplateLoader(TemplateBridge, BaseLoader): self.environment.globals['debug'] = contextfunction(pformat) self.environment.globals['accesskey'] = contextfunction(accesskey) if use_i18n: - self.environment.install_gettext_translations(builder.translator) + self.environment.install_gettext_translations(builder.app.translator) def render(self, template, context): return self.environment.get_template(template).render(context) diff --git a/sphinx/roles.py b/sphinx/roles.py index 66729ca92..8833c275d 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -137,9 +137,8 @@ class XRefRole(object): nodeclass = addnodes.pending_xref innernodeclass = nodes.literal - def __init__(self, domain_name, fix_parens=False, lowercase=False, + def __init__(self, fix_parens=False, lowercase=False, nodeclass=None, innernodeclass=None): - self.domain_name = domain_name self.fix_parens = fix_parens if nodeclass is not None: self.nodeclass = nodeclass @@ -169,10 +168,10 @@ class XRefRole(object): typ = env.config.default_role else: typ = typ.lower() - if ":" in typ: - domain, role = typ.split(":", 1) + if ':' not in typ: + domain, role = '', typ else: - domain, role = self.domain_name, typ + domain, role = typ.split(':', 1) text = utils.unescape(text) # if the first character is a bang, don't cross-reference at all if text[0:1] == '!': @@ -212,13 +211,13 @@ class OptionXRefRole(XRefRole): specific_docroles = { - 'keyword': XRefRole(''), - 'ref': XRefRole('', lowercase=True, innernodeclass=nodes.emphasis), - 'token': XRefRole(''), - 'term': XRefRole('', lowercase=True, innernodeclass=nodes.emphasis), - 'option': OptionXRefRole('', innernodeclass=addnodes.literal_emphasis), - 'doc': XRefRole(''), - 'download': XRefRole('', nodeclass=addnodes.download_reference), + 'keyword': XRefRole(), + 'ref': XRefRole(lowercase=True, innernodeclass=nodes.emphasis), + 'token': XRefRole(), + 'term': XRefRole(lowercase=True, innernodeclass=nodes.emphasis), + 'option': OptionXRefRole(innernodeclass=addnodes.literal_emphasis), + 'doc': XRefRole(), + 'download': XRefRole(nodeclass=addnodes.download_reference), 'menuselection': menusel_role, 'file': emph_literal_role, diff --git a/sphinx/search.py b/sphinx/search.py index 499c4aa9e..f283881fa 100644 --- a/sphinx/search.py +++ b/sphinx/search.py @@ -149,6 +149,8 @@ class IndexBuilder(object): def get_modules(self, fn2index): rv = {} + # XXX implement search capability + return rv for name, (doc, _, _, _) in self.env.modules.iteritems(): if doc in fn2index: rv[name] = fn2index[doc] @@ -157,6 +159,8 @@ class IndexBuilder(object): def get_descrefs(self, fn2index): rv = {} dt = self._desctypes + # XXX implement search capability + return rv for fullname, (doc, desctype) in self.env.descrefs.iteritems(): if doc not in fn2index: continue diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 3465403df..c238fd47e 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -396,8 +396,10 @@ def movefile(source, dest): def copyfile(source, dest): """Copy a file and its modification times, if possible.""" shutil.copyfile(source, dest) - try: shutil.copystat(source, dest) - except shutil.Error: pass + try: + shutil.copystat(source, dest) + except shutil.Error: + pass def copy_static_entry(source, target, builder, context={}): @@ -435,6 +437,23 @@ def split_explicit_title(text): else: return False, text, text + +from docutils import nodes + +def make_refnode(builder, fromdocname, todocname, targetid, child, title=None): + """Shortcut to create a reference node.""" + node = nodes.reference('', '') + if fromdocname == todocname: + node['refid'] = targetid + else: + node['refuri'] = (builder.get_relative_uri(fromdocname, todocname) + + '#' + targetid) + if title: + node['reftitle'] = title + node.append(child) + return node + + # monkey-patch Node.traverse to get more speed # traverse() is called so many times during a build that it saves # on average 20-25% overall build time! @@ -464,8 +483,7 @@ def _new_traverse(self, condition=None, return self._old_traverse(condition, include_self, descend, siblings, ascend) -import docutils.nodes -docutils.nodes.Node._old_traverse = docutils.nodes.Node.traverse -docutils.nodes.Node._all_traverse = _all_traverse -docutils.nodes.Node._fast_traverse = _fast_traverse -docutils.nodes.Node.traverse = _new_traverse +nodes.Node._old_traverse = nodes.Node.traverse +nodes.Node._all_traverse = _all_traverse +nodes.Node._fast_traverse = _fast_traverse +nodes.Node.traverse = _new_traverse diff --git a/tests/root/desc.txt b/tests/root/desc.txt index d6915dc27..bdceafc0a 100644 --- a/tests/root/desc.txt +++ b/tests/root/desc.txt @@ -43,15 +43,15 @@ Testing description units C items ======= -.. cfunction:: Sphinx_DoSomething() +.. c:function:: Sphinx_DoSomething() -.. cmember:: SphinxStruct.member +.. c:member:: SphinxStruct.member -.. cmacro:: SPHINX_USE_PYTHON +.. c:macro:: SPHINX_USE_PYTHON -.. ctype:: SphinxType +.. c:type:: SphinxType -.. cvar:: sphinx_global +.. c:var:: sphinx_global Testing references diff --git a/tests/test_env.py b/tests/test_env.py index a06656d69..e9118a633 100644 --- a/tests/test_env.py +++ b/tests/test_env.py @@ -20,8 +20,8 @@ warnings = [] def setup_module(): global app, env - app = TestApp(srcdir='(temp)') - env = BuildEnvironment(app.srcdir, app.doctreedir, app.config) + app = TestApp(srcdir='(temp)', freshenv=True) + env = app.env env.set_warnfunc(lambda *args: warnings.append(args)) def teardown_module(): @@ -51,7 +51,7 @@ def test_images(): tree = env.get_doctree('images') app._warning.reset() - htmlbuilder = StandaloneHTMLBuilder(app, env) + htmlbuilder = StandaloneHTMLBuilder(app) htmlbuilder.post_process_images(tree) assert "no matching candidate for image URI u'foo.*'" in \ app._warning.content[-1] @@ -61,7 +61,7 @@ def test_images(): set(['img.png', 'img1.png', 'simg.png', 'svgimg.svg']) app._warning.reset() - latexbuilder = LaTeXBuilder(app, env) + latexbuilder = LaTeXBuilder(app) latexbuilder.post_process_images(tree) assert "no matching candidate for image URI u'foo.*'" in \ app._warning.content[-1] @@ -92,7 +92,7 @@ def test_second_update(): assert 'autodoc' not in env.found_docs def test_object_inventory(): - refs = env.descrefs + refs = env.domaindata['py']['objects'] assert 'func_without_module' in refs assert refs['func_without_module'] == ('desc', 'function') @@ -109,5 +109,8 @@ def test_object_inventory(): assert 'func_in_module' not in refs assert 'func_noindex' not in refs - assert 'mod' in env.modules - assert env.modules['mod'] == ('desc', 'Module synopsis.', 'UNIX', False) + assert env.domaindata['py']['modules']['mod'] == \ + ('desc', 'Module synopsis.', 'UNIX', False) + + assert env.domains['py'].data is env.domaindata['py'] + assert env.domains['c'].data is env.domaindata['c']