From 50da49fab419949106442e5eec47831d0c82d0b8 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Mon, 21 Jan 2008 20:20:37 +0000 Subject: [PATCH] Further refactorings, add extensibility API. --- sphinx/__init__.py | 73 +++-- sphinx/builder.py | 199 +++++++------- sphinx/config.py | 103 ++++++++ sphinx/directives.py | 2 +- sphinx/environment.py | 60 +++-- sphinx/extension.py | 83 ++++++ sphinx/htmlhelp.py | 4 +- sphinx/htmlwriter.py | 498 ++++++++++++++++++----------------- sphinx/latexwriter.py | 12 +- sphinx/patchlevel.py | 60 ----- sphinx/roles.py | 2 +- sphinx/templates/layout.html | 3 +- 12 files changed, 622 insertions(+), 477 deletions(-) create mode 100644 sphinx/config.py create mode 100644 sphinx/extension.py delete mode 100644 sphinx/patchlevel.py diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 82cbb55eba..cd62435d95 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -9,15 +9,51 @@ :license: BSD. """ +import os import sys import getopt from os import path from cStringIO import StringIO +from sphinx.config import Config, ConfigError from sphinx.builder import builders +from sphinx.extension import EventManager from sphinx.util.console import nocolor -__version__ = '$Revision: 5369 $' +__version__ = '$Revision: 5369 $'[11:-2] + + +def init_builder(buildername, srcdirname, outdirname, doctreedir, + confoverrides, status, warning=sys.stderr, freshenv=False): + # read config + config = Config(srcdirname, 'conf.py') + if confoverrides: + for key, val in confoverrides.items(): + setattr(config, key, val) + + # extensibility + events = EventManager() + for extension in config.extensions: + try: + mod = __import__(extension, None, None, ['setup']) + except ImportError, err: + raise ConfigError('Could not import extension %s' % module, err) + if hasattr(mod, 'setup'): + mod.setup(events, builders) + + if buildername not in builders: + print >>warning, 'Builder name %s not registered' % buildername + return None + + if buildername is None: + print >>status, 'No builder selected, using default: html' + buildername = 'html' + + builderclass = builders[buildername] + builder = builderclass(srcdirname, outdirname, doctreedir, + status_stream=status, warning_stream=warning, + events=events, config=config, freshenv=freshenv) + events.emit('builder-created', builder) def usage(argv, msg=None): @@ -42,6 +78,10 @@ modi: def main(argv): + if not sys.stdout.isatty() or sys.platform == 'win32': + # Windows' poor cmd box doesn't understand ANSI sequences + nocolor() + try: opts, args = getopt.getopt(argv[1:], 'ab:d:D:NEqP') srcdirname = path.abspath(args[0]) @@ -68,17 +108,14 @@ def main(argv): if err: return 1 - builder = all_files = None + buildername = all_files = None freshenv = use_pdb = False status = sys.stdout confoverrides = {} doctreedir = path.join(outdirname, '.doctrees') for opt, val in opts: if opt == '-b': - if val not in builders: - usage(argv, 'Invalid builder value specified.') - return 1 - builder = val + buildername = val elif opt == '-a': if filenames: usage(argv, 'Cannot combine -a option and filenames.') @@ -101,28 +138,18 @@ def main(argv): elif opt == '-P': use_pdb = True - if not sys.stdout.isatty() or sys.platform == 'win32': - # Windows' cmd box doesn't understand ANSI sequences - nocolor() - - if builder is None: - print >>status, 'No builder selected, using default: html' - builder = 'html' - - builderobj = builders[builder] + builder = init_builder(buildername, srcdirname, outdirname, doctreedir, + confoverrides, status, sys.stderr, freshenv) + if not builder: + return 1 try: - builderobj = builderobj(srcdirname, outdirname, doctreedir, - status_stream=status, - warning_stream=sys.stderr, - confoverrides=confoverrides, - freshenv=freshenv) if all_files: - builderobj.build_all() + builder.build_all() elif filenames: - builderobj.build_specific(filenames) + builder.build_specific(filenames) else: - builderobj.build_update() + builder.build_update() except: if not use_pdb: raise diff --git a/sphinx/builder.py b/sphinx/builder.py index 357875bc36..796b0720c2 100644 --- a/sphinx/builder.py +++ b/sphinx/builder.py @@ -12,7 +12,6 @@ import os import sys import time -import types import codecs import shutil import cPickle as pickle @@ -31,8 +30,9 @@ from sphinx import addnodes from sphinx.util import (get_matching_files, attrdict, status_iterator, ensuredir, relative_uri, os_path, SEP) from sphinx.htmlhelp import build_hhx +from sphinx.extension import DummyEventManager, import_object from sphinx.patchlevel import get_version_info, get_sys_version_info -from sphinx.htmlwriter import HTMLWriter +from sphinx.htmlwriter import HTMLWriter, HTMLTranslator, SmartyPantsHTMLTranslator from sphinx.latexwriter import LaTeXWriter from sphinx.environment import BuildEnvironment, NoUri from sphinx.highlighting import pygments, highlight_block, get_stylesheet @@ -45,25 +45,13 @@ from sphinx import directives ENV_PICKLE_FILENAME = 'environment.pickle' LAST_BUILD_FILENAME = 'last_build' -# Helper objects - -class relpath_to(object): - def __init__(self, builder, filename): - self.baseuri = builder.get_target_uri(filename) - self.builder = builder - def __call__(self, otheruri, resource=False): - if not resource: - otheruri = self.builder.get_target_uri(otheruri + '.rst') - return relative_uri(self.baseuri, otheruri) - - class Builder(object): """ Builds target formats from the reST sources. """ def __init__(self, srcdirname, outdirname, doctreedirname, - confoverrides=None, env=None, freshenv=False, + config, env=None, freshenv=False, events=None, status_stream=None, warning_stream=None): self.srcdir = srcdirname self.outdir = outdirname @@ -75,35 +63,11 @@ class Builder(object): self.status_stream = status_stream or sys.stdout self.warning_stream = warning_stream or sys.stderr - # probably set in load_env() + self.config = config + # if None, this is set in load_env() self.env = env - self.config = {} - olddir = os.getcwd() - try: - os.chdir(srcdirname) - execfile(path.join(srcdirname, 'conf.py'), self.config) - finally: - os.chdir(olddir) - # remove potentially pickling-problematic values - del self.config['__builtins__'] - for key, val in self.config.items(): - if isinstance(val, types.ModuleType): - del self.config[key] - if confoverrides: - self.config.update(confoverrides) - # replace version info if '' - if self.config['version'] == '' or self.config['release'] == '': - try: - version, release = get_version_info(srcdirname) - except (IOError, OSError): - version, release = get_sys_version_info() - self.warn('Can\'t get version info from Include/patchlevel.h, ' - 'using version of this interpreter (%s).' % release) - if self.config['version'] == '': - self.config['version'] = version - if self.config['release'] == '': - self.config['release'] = release + self.events = events or DummyEventManager() self.init() @@ -124,12 +88,34 @@ class Builder(object): """Load necessary templates and perform initialization.""" raise NotImplementedError + def init_templates(self): + """Call if you need Jinja templates in the builder.""" + # lazily import this, maybe other builders won't need it + from sphinx._jinja import Environment, SphinxFileSystemLoader + + # load templates + self.templates = {} + templates_path = [path.join(path.dirname(__file__), 'templates')] + templates_path.extend(self.config.templates_path) + self.jinja_env = Environment(loader=SphinxFileSystemLoader(templates_path), + # disable traceback, more likely that something + # in the application is broken than in the templates + friendly_traceback=False) + + def get_template(self, name): + if name in self.templates: + return self.templates[name] + template = self.templates[name] = self.jinja_env.get_template(name) + return template + def get_target_uri(self, source_filename, typ=None): """Return the target URI for a source filename.""" raise NotImplementedError def get_relative_uri(self, from_, to, typ=None): - """Return a relative URI between two source filenames.""" + """Return a relative URI between two source filenames. + May raise environment.NoUri if there's no way to return a + sensible URI.""" return relative_uri(self.get_target_uri(from_), self.get_target_uri(to, typ)) @@ -196,7 +182,9 @@ class Builder(object): warnings = [] self.env.set_warnfunc(warnings.append) self.msg('reading, updating environment:', nonl=1) - iterator = self.env.update(self.config) + iterator = self.env.update( + self.config, + hook=lambda doctree: self.events.emit('doctree-read', doctree)) self.msg(iterator.next(), nonl=1, nobold=1) for filename in iterator: if not updated_filenames: @@ -274,25 +262,14 @@ class StandaloneHTMLBuilder(Builder): def init(self): """Load templates.""" - # lazily import this, maybe other builders won't need it - from sphinx._jinja import Environment, SphinxFileSystemLoader - - # load templates - self.templates = {} - templates_path = path.join(path.dirname(__file__), 'templates') - self.jinja_env = Environment(loader=SphinxFileSystemLoader([templates_path]), - # disable traceback, more likely that something - # in the application is broken than in the templates - friendly_traceback=False) - # pre-load built-in templates - for fname in os.listdir(templates_path): - if fname.endswith('.html'): - self.templates[fname] = self.jinja_env.get_template(fname) - - def get_template(self, name): - if name in self.templates: - return self.templates[name] - return self.jinja_env.get_template(name) + self.init_templates() + if self.config.html_translator_class: + self.translator_class = import_object(self.config.html_translator_class, + 'html_translator_class setting') + elif self.config.html_use_smartypants: + self.translator_class = SmartyPantsHTMLTranslator + else: + self.translator_class = HTMLTranslator def render_partial(self, node): """Utility: Render a lone doctree node.""" @@ -317,17 +294,17 @@ class StandaloneHTMLBuilder(Builder): # format the "last updated on" string, only once is enough since it # typically doesn't include the time of day - lufmt = self.config.get('html_last_updated_fmt') + lufmt = self.config.html_last_updated_fmt if lufmt: self.last_updated = time.strftime(lufmt) else: self.last_updated = None self.globalcontext = dict( - project = self.config.get('project', 'Python'), - copyright = self.config.get('copyright', ''), - release = self.config['release'], - version = self.config['version'], + project = self.config.project, + copyright = self.config.copyright, + release = self.config.release, + version = self.config.version, last_updated = self.last_updated, builder = self.name, parents = [], @@ -336,6 +313,7 @@ class StandaloneHTMLBuilder(Builder): ) def write_file(self, filename, doctree): + pagename = filename[:-4] destination = StringOutput(encoding='utf-8') doctree.settings = self.docsettings @@ -366,7 +344,7 @@ class StandaloneHTMLBuilder(Builder): else: title = '' self.globalcontext['titles'][filename] = title - sourcename = filename[:-4] + '.txt' + sourcename = pagename + '.txt' context = dict( title = title, sourcename = sourcename, @@ -379,8 +357,8 @@ class StandaloneHTMLBuilder(Builder): next = next, ) - self.index_file(filename, doctree, title) - self.handle_page(filename[:-4], context) + self.index_page(pagename, doctree, title) + self.handle_page(pagename, context) def finish(self): self.msg('writing additional files...') @@ -446,12 +424,12 @@ class StandaloneHTMLBuilder(Builder): self.handle_page('search', {}, 'search.html') # additional pages from conf.py - for pagename, template in self.config.get('html_additional_pages', {}).items(): + for pagename, template in self.config.html_additional_pages.items(): template = path.join(self.srcdir, template) self.handle_page(pagename, {}, template) # the index page - indextemplate = self.config.get('html_index') + indextemplate = self.config.html_index if indextemplate: indextemplate = path.join(self.srcdir, indextemplate) self.handle_page('index', {'indextemplate': indextemplate}, 'index.html') @@ -480,7 +458,7 @@ class StandaloneHTMLBuilder(Builder): def get_outdated_files(self): for filename in get_matching_files( - self.srcdir, '*.rst', exclude=set(self.config.get('unused_files', ()))): + self.srcdir, '*.rst', exclude=set(self.config.unused_files)): try: rstname = path.join(self.outdir, os_path(filename)) targetmtime = path.getmtime(rstname[:-4] + '.html') @@ -504,21 +482,26 @@ class StandaloneHTMLBuilder(Builder): # delete all entries for files that will be rebuilt self.indexer.prune([fn[:-4] for fn in set(self.env.all_files) - set(filenames)]) - def index_file(self, filename, doctree, title): + def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: - self.indexer.feed(self.get_target_uri(filename)[:-5], # strip '.html' - title, doctree) + self.indexer.feed(pagename, title, doctree) - def handle_page(self, pagename, context, templatename='page.html'): + def handle_page(self, pagename, addctx, templatename='page.html'): ctx = self.globalcontext.copy() ctx['current_page_name'] = pagename - ctx['pathto'] = relpath_to(self, self.get_target_uri(pagename+'.rst')) + + def pathto(otheruri, resource=False, + baseuri=self.get_target_uri(pagename+'.rst')): + if not resource: + otheruri = self.get_target_uri(otheruri+'.rst') + return relative_uri(baseuri, otheruri) + ctx['pathto'] = pathto ctx['hasdoc'] = lambda name: name+'.rst' in self.env.all_files - sidebarfile = self.config.get('html_sidebars', {}).get(pagename) + sidebarfile = self.config.html_sidebars.get(pagename) if sidebarfile: ctx['customsidebar'] = path.join(self.srcdir, sidebarfile) - ctx.update(context) + ctx.update(addctx) output = self.get_template(templatename).render(ctx) outfilename = path.join(self.outdir, os_path(pagename) + '.html') @@ -531,14 +514,14 @@ class StandaloneHTMLBuilder(Builder): f.close() except (IOError, OSError), err: self.warn("Error writing file %s: %s" % (outfilename, err)) - if self.copysource and context.get('sourcename'): + if self.copysource and ctx.get('sourcename'): # copy the source file for the "show source" link shutil.copyfile(path.join(self.srcdir, os_path(pagename+'.rst')), - path.join(self.outdir, os_path(context['sourcename']))) + path.join(self.outdir, os_path(ctx['sourcename']))) def handle_finish(self): self.msg('dumping search index...') - self.indexer.prune([self.get_target_uri(fn)[:-5] for fn in self.env.all_files]) + self.indexer.prune([fn[:-4] for fn in self.env.all_files]) f = open(path.join(self.outdir, 'searchindex.json'), 'w') try: self.indexer.dump(f, 'json') @@ -558,7 +541,7 @@ class WebHTMLBuilder(StandaloneHTMLBuilder): def get_outdated_files(self): for filename in get_matching_files( - self.srcdir, '*.rst', exclude=set(self.config.get('unused_files', ()))): + self.srcdir, '*.rst', exclude=set(self.config.unused_files)): try: targetmtime = path.getmtime( path.join(self.outdir, os_path(filename)[:-4] + '.fpickle')) @@ -587,14 +570,14 @@ class WebHTMLBuilder(StandaloneHTMLBuilder): # delete all entries for files that will be rebuilt self.indexer.prune(set(self.env.all_files) - set(filenames)) - def index_file(self, filename, doctree, title): + def index_page(self, pagename, doctree, title): # only index pages with title if self.indexer is not None and title: - self.indexer.feed(filename, title, doctree) + self.indexer.feed(pagename+'.rst', title, doctree) def handle_page(self, pagename, context, templatename='page.html'): context['current_page_name'] = pagename - sidebarfile = self.config.get('html_sidebars', {}).get(pagename, '') + sidebarfile = self.confightml_sidebars.get(pagename, '') if sidebarfile: context['customsidebar'] = path.join(self.srcdir, sidebarfile) outfilename = path.join(self.outdir, os_path(pagename) + '.fpickle') @@ -654,7 +637,7 @@ class HTMLHelpBuilder(StandaloneHTMLBuilder): copysource = False def handle_finish(self): - build_hhx(self, self.outdir, self.config.get('htmlhelp_basename', 'pydoc')) + build_hhx(self, self.outdir, self.config.htmlhelp_basename) class LaTeXBuilder(Builder): @@ -665,13 +648,12 @@ class LaTeXBuilder(Builder): def init(self): self.filenames = [] - self.document_data = map(list, self.config.get('latex_documents', ())) + self.document_data = map(list, self.config.latex_documents) # assign subdirs to titles self.titles = [] for entry in self.document_data: # replace version with real version - entry[0] = entry[0].replace('', self.config['version']) sourcename = entry[0] if sourcename.endswith('/index.rst'): sourcename = sourcename[:-9] @@ -693,7 +675,7 @@ class LaTeXBuilder(Builder): def write(self, *ignored): # first, assemble the "appendix" docs that are in every PDF appendices = [] - for fname in self.config.get('latex_appendices', []): + for fname in self.config.latex_appendices: appendices.append(self.env.get_doctree(fname)) docwriter = LaTeXWriter(self) @@ -780,15 +762,10 @@ class ChangesBuilder(Builder): name = 'changes' def init(self): - from sphinx._jinja import Environment, FileSystemLoader - templates_path = path.join(path.dirname(__file__), 'templates') - jinja_env = Environment(loader=SphinxFileSystemLoader([templates_path]), - # disable traceback, more likely that something in the - # application is broken than in the templates - friendly_traceback=False) - self.ftemplate = jinja_env.get_template('changes/frameset.html') - self.vtemplate = jinja_env.get_template('changes/versionchanges.html') - self.stemplate = jinja_env.get_template('changes/rstsource.html') + self.init_templates() + self.ftemplate = self.get_template('changes/frameset.html') + self.vtemplate = self.get_template('changes/versionchanges.html') + self.stemplate = self.get_template('changes/rstsource.html') def get_outdated_files(self): return self.outdir @@ -800,13 +777,13 @@ class ChangesBuilder(Builder): } def write(self, *ignored): - ver = self.config['version'] + version = self.config.version libchanges = {} apichanges = [] otherchanges = {} self.msg('writing summary file...') for type, filename, lineno, module, descname, content in \ - self.env.versionchanges[ver]: + self.env.versionchanges[version]: ttext = self.typemap[type] context = content.replace('\n', ' ') if descname and filename.startswith('c-api'): @@ -836,8 +813,8 @@ class ChangesBuilder(Builder): (entry, filename, lineno)) ctx = { - 'project': self.config.get('project', 'Python'), - 'version': ver, + 'project': self.config.project, + 'version': version, 'libchanges': sorted(libchanges.iteritems()), 'apichanges': sorted(apichanges), 'otherchanges': sorted(otherchanges.iteritems()), @@ -853,9 +830,9 @@ class ChangesBuilder(Builder): finally: f.close() - hltext = ['.. versionadded:: %s' % ver, - '.. versionchanged:: %s' % ver, - '.. deprecated:: %s' % ver] + hltext = ['.. versionadded:: %s' % version, + '.. versionchanged:: %s' % version, + '.. deprecated:: %s' % version] def hl(no, line): line = ' ' % no + escape(line) @@ -881,11 +858,11 @@ class ChangesBuilder(Builder): shutil.copyfile(path.join(path.dirname(__file__), 'style', 'default.css'), path.join(self.outdir, 'default.css')) - def hl(self, text, ver): + def hl(self, text, version): text = escape(text) for directive in ['versionchanged', 'versionadded', 'deprecated']: - text = text.replace('.. %s:: %s' % (directive, ver), - '.. %s:: %s' % (directive, ver)) + text = text.replace('.. %s:: %s' % (directive, version), + '.. %s:: %s' % (directive, version)) return text def finish(self): diff --git a/sphinx/config.py b/sphinx/config.py new file mode 100644 index 0000000000..3d5ca65776 --- /dev/null +++ b/sphinx/config.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" + sphinx.config + ~~~~~~~~~~~~~ + + Build configuration file handling. + + :copyright: 2008 by Georg Brandl. + :license: BSD license. +""" + +import os +import sys +import types +from os import path + + +class ConfigError(Exception): + """Raised if something's wrong with the configuration.""" + + def __init__(self, message, orig_exc=None): + self.message = message + self.orig_exc = orig_exc + + def __repr__(self): + if self.orig_exc: + return 'ConfigError(%r, %r)' % (self.message, self.orig_exc) + return 'ConfigError(%r)' % self.message + + def __str__(self): + if self.orig_exc: + return '%s (exception: %s)' % (self.message, self.orig_exc) + return self.message + + +class Config(object): + """Configuration file abstraction.""" + + # the values are: (default, needs fresh doctrees if changed) + + config_values = dict( + # general substitutions + project = ('Python', True), + copyright = ('', False), + version = ('', True), + release = ('', True), + today = ('', True), + today_fmt = ('%B %d, %Y', True), + + # extensibility + templates_path = ([], False), + extensions = ([], True), + + # general reading options + unused_files = ([], True), + refcount_file = ('', True), + add_function_parentheses = (True, True), + add_module_names = (True, True), + + # HTML options + html_last_updated_fmt = ('%b %d, %Y', False), + html_use_smartypants = (True, False), + html_translator_class = (None, False), + html_index = ('', False), + html_sidebars = ({}, False), + html_additional_pages = ({}, False), + + # HTML help options + htmlhelp_basename = ('pydoc', False), + + # LaTeX options + latex_paper_size = ('letter', False), + latex_font_size = ('10pt', False), + latex_documents = ([], False), + latex_preamble = ('', False), + latex_appendices = ([], False), + ) + + def __init__(self, dirname, filename): + config = {} + olddir = os.getcwd() + try: + os.chdir(dirname) + execfile(path.join(dirname, filename), config) + finally: + os.chdir(olddir) + # remove potentially pickling-problematic values + for key, val in config.items(): + if key.startswith('_') or isinstance(val, types.ModuleType): + del config[key] + self.__dict__.update(config) + + def __getattr__(self, name): + if name in self.config_values: + defval = self.config_values[name][0] + setattr(self, name, defval) + return defval + if name[0:1] == '_': + return object.__getattr__(self, name) + raise AttributeError('no configuration value named %r' % name) + + def __getitem__(self, name): + return getattr(self, name) diff --git a/sphinx/directives.py b/sphinx/directives.py index b8d7eb514c..63c676c80d 100644 --- a/sphinx/directives.py +++ b/sphinx/directives.py @@ -147,7 +147,7 @@ def parse_py_signature(signode, sig, desctype, env): signode += addnodes.desc_classname(classname, classname) # exceptions are a special case, since they are documented in the # 'exceptions' module. - elif env.config.get('add_module_names', True) and \ + elif env.config.add_module_names and \ env.currmodule and env.currmodule != 'exceptions': nodetext = env.currmodule + '.' signode += addnodes.desc_classname(nodetext, nodetext) diff --git a/sphinx/environment.py b/sphinx/environment.py index 4d81810486..b9381d4eb8 100644 --- a/sphinx/environment.py +++ b/sphinx/environment.py @@ -22,6 +22,7 @@ try: import hashlib md5 = hashlib.md5 except: + # 2.4 compatibility import md5 md5 = md5.new @@ -102,10 +103,10 @@ class DefaultSubstitutions(Transform): for ref in self.document.traverse(nodes.substitution_reference): refname = ref['refname'] if refname in to_handle: - text = config.get(refname, '') + text = config[refname] if refname == 'today' and not text: # special handling: can also specify a strftime format - text = time.strftime(config.get('today_fmt', '%B %d, %Y')) + text = time.strftime(config.today_fmt) ref.replace_self(nodes.Text(text, text)) @@ -186,7 +187,7 @@ class BuildEnvironment: def __init__(self, srcdir, doctreedir): self.doctreedir = doctreedir self.srcdir = srcdir - self.config = {} + self.config = None # refcount data if present self.refcounts = {} @@ -203,6 +204,7 @@ class BuildEnvironment: # Build times -- to determine changed files # Also use this as an inventory of all existing and built filenames. + # All "filenames" here are /-separated and relative and include '.rst'. self.all_files = {} # filename -> (mtime, md5sum) at the time of build # File metadata @@ -279,7 +281,7 @@ class BuildEnvironment: Return (added, changed, removed) iterables. """ all_source_files = list(get_matching_files( - self.srcdir, '*.rst', exclude=set(config.get('unused_files', ())))) + self.srcdir, '*.rst', exclude=set(config.unused_files))) # clear all files no longer present removed = set(self.all_files) - set(all_source_files) @@ -312,27 +314,26 @@ class BuildEnvironment: return added, changed, removed - # If one of these config values changes, all files need to be re-read. - influential_config_values = [ - 'version', 'release', 'today', 'today_fmt', 'unused_files', - 'project', 'refcount_file', 'add_function_parentheses', 'add_module_names' - ] - - def update(self, config): - """ - (Re-)read all files new or changed since last update. - Yields a summary and then filenames as it processes them. - Store all environment filenames in the canonical format - (ie using SEP as a separator in place of os.path.sep). - """ + def update(self, config, hook=None): + """(Re-)read all files new or changed since last update. Yields a summary + and then filenames as it processes them. Store all environment filenames + in the canonical format (ie using SEP as a separator in place of + os.path.sep).""" config_changed = False - for val in self.influential_config_values: - if self.config.get(val) != config.get(val): - msg = '[config changed] ' - config_changed = True - break + if self.config is None: + msg = '[new config] ' + config_changed = True else: - msg = '' + # check if a config value was changed that affects how doctrees are read + for key, descr in config.config_values.iteritems(): + if not descr[1]: + continue + if self.config[key] != config[key]: + msg = '[config changed] ' + config_changed = True + break + else: + msg = '' added, changed, removed = self.get_outdated_files(config, config_changed) msg += '%s added, %s changed, %s removed' % (len(added), len(changed), len(removed)) @@ -341,9 +342,9 @@ class BuildEnvironment: self.config = config # read the refcounts file - if self.config.get('refcount_file'): + if self.config.refcount_file: self.refcounts = Refcounts.fromfile( - path.join(self.srcdir, self.config['refcount_file'])) + path.join(self.srcdir, self.config.refcount_file)) # clear all files no longer present for filename in removed: @@ -354,9 +355,12 @@ class BuildEnvironment: yield filename self.read_file(filename) + if 'contents.rst' not in self.all_files: + self._warnfunc('no master file contents.rst found') + # --------- SINGLE FILE BUILDING ------------------------------------------- - def read_file(self, filename, src_path=None, save_parsed=True): + def read_file(self, filename, src_path=None, save_parsed=True, hook=None): """Parse a file and add/update inventory entries for the doctree. If srcpath is given, read from a different source file.""" # remove all inventory entries for that file @@ -382,6 +386,10 @@ class BuildEnvironment: f.close() self.all_files[filename] = (path.getmtime(src_path), md5sum) + # run post-read hook + if hook: + hook(doctree) + # make it picklable doctree.reporter = None doctree.transformer = None diff --git a/sphinx/extension.py b/sphinx/extension.py new file mode 100644 index 0000000000..cb45e4042f --- /dev/null +++ b/sphinx/extension.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +""" + sphinx.extension + ~~~~~~~~~~~~~~~~ + + Gracefully adapted from the TextPress event system by Armin. + + :copyright: 2008 by Georg Brandl, Armin Ronacher. + :license: BSD. +""" + +from sphinx.config import ConfigError + + +def import_object(objname, source=None): + """Import an object from a 'module.name' string.""" + try: + module, name = objname.rsplit('.', 1) + except ValueError, err: + raise ConfigError('Invalid full object name %s' % objname + + (source and ' (needed for %s)' % source or ''), err) + try: + return getattr(__import__(module, None, None, [name]), name) + except ImportError, err: + raise ConfigError('Could not import %s' % module + + (source and ' (needed for %s)' % source or ''), err) + except AttributeError, err: + raise ConfigError('Could not find %s' % objname + + (source and ' (needed for %s)' % source or ''), err) + + +# List of all known events. Maps name to arguments description. +events = { + 'builder-created' : 'builder instance', + 'doctree-read' : 'the doctree before being pickled', +} + +class EventManager(object): + """ + Helper class that handles event listeners and events. + + This is *not* a public interface. Always use the emit_event() + functions to access it or the connect_event() / disconnect_event() + functions on the application. + """ + + def __init__(self): + self.next_listener_id = 0 + self._listeners = {} + + def _validate(self, event): + event = intern(event) + if event not in events: + raise RuntimeError('unknown event name: %s' % event) + + def connect(self, event, callback): + self._validate(event) + listener_id = self.next_listener_id + if event not in self._listeners: + self._listeners[event] = {listener_id: callback} + else: + self._listeners[event][listener_id] = callback + self.next_listener_id += 1 + return listener_id + + def remove(self, listener_id): + for event in self._listeners: + event.pop(listener_id, None) + + def emit(self, event, *args): + self._validate(event) + if event in self._listeners: + for listener_id, callback in self._listeners[event].iteritems(): + yield listener_id, callback(*args) + + +class DummyEventManager(EventManager): + def connect(self, event, callback): + self._validate(event) + def remove(self, listener_id): + pass + def emit(self, event, *args): + self._validate(event) diff --git a/sphinx/htmlhelp.py b/sphinx/htmlhelp.py index 3587757c98..53604d87d1 100644 --- a/sphinx/htmlhelp.py +++ b/sphinx/htmlhelp.py @@ -129,8 +129,8 @@ def build_hhx(builder, outdir, outname): f = open(path.join(outdir, outname+'.hhp'), 'w') try: f.write(project_template % {'outname': outname, - 'version': builder.config['version'], - 'project': builder.config['project']}) + 'version': builder.config.version, + 'project': builder.config.project}) if not outdir.endswith(os.sep): outdir += os.sep olen = len(outdir) diff --git a/sphinx/htmlwriter.py b/sphinx/htmlwriter.py index ccc2fa91b3..08dc198f91 100644 --- a/sphinx/htmlwriter.py +++ b/sphinx/htmlwriter.py @@ -18,7 +18,18 @@ from sphinx.util.smartypants import sphinx_smarty_pants class HTMLWriter(Writer): def __init__(self, builder): Writer.__init__(self) - self.translator_class = translator_class(builder) + self.builder = builder + + def translate(self): + # sadly, this is mostly copied from parent class + self.visitor = visitor = self.builder.translator_class(self.builder, + self.document) + self.document.walkabout(visitor) + self.output = visitor.astext() + for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', + 'body_pre_docinfo', 'docinfo', 'body', 'fragment', + 'body_suffix'): + setattr(self, attr, getattr(visitor, attr)) version_text = { @@ -27,283 +38,278 @@ version_text = { 'versionadded': 'New in version %s', } -def translator_class(builder): - class HTMLTranslator(BaseTranslator): - """ - Our custom HTML translator. - """ +class HTMLTranslator(BaseTranslator): + """ + Our custom HTML translator. + """ - def __init__(self, *args, **kwds): - self.no_smarty = 0 - BaseTranslator.__init__(self, *args, **kwds) - self.highlightlang = 'python' - self.language.labels['warning'] = 'Caveat' + def __init__(self, builder, *args, **kwds): + BaseTranslator.__init__(self, *args, **kwds) + self.no_smarty = 0 + self.builder = builder + self.highlightlang = 'python' + self.language.labels['warning'] = 'Caveat' - def visit_desc(self, node): - self.body.append(self.starttag(node, 'dl', CLASS=node['desctype'])) - def depart_desc(self, node): - self.body.append('\n\n') + def visit_desc(self, node): + self.body.append(self.starttag(node, 'dl', CLASS=node['desctype'])) + def depart_desc(self, node): + self.body.append('\n\n') - def visit_desc_signature(self, node): - # the id is set automatically - self.body.append(self.starttag(node, 'dt')) - # anchor for per-desc interactive data - if node.parent['desctype'] != 'describe' and node['ids'] and node['first']: - self.body.append('' % node['ids'][0]) - if node.parent['desctype'] in ('class', 'exception'): - self.body.append('%s ' % node.parent['desctype']) - def depart_desc_signature(self, node): - if node['ids'] and builder.name != 'htmlhelp': - self.body.append(u'\u00B6') - self.body.append('\n') + def visit_desc_signature(self, node): + # the id is set automatically + self.body.append(self.starttag(node, 'dt')) + # anchor for per-desc interactive data + if node.parent['desctype'] != 'describe' and node['ids'] and node['first']: + self.body.append('' % node['ids'][0]) + if node.parent['desctype'] in ('class', 'exception'): + self.body.append('%s ' % node.parent['desctype']) + def depart_desc_signature(self, node): + if node['ids'] and self.builder.name != 'htmlhelp': + self.body.append(u'\u00B6') + self.body.append('\n') - def visit_desc_classname(self, node): - self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname')) - def depart_desc_classname(self, node): - self.body.append('') + def visit_desc_classname(self, node): + self.body.append(self.starttag(node, 'tt', '', CLASS='descclassname')) + def depart_desc_classname(self, node): + self.body.append('') - def visit_desc_type(self, node): - # return type of C functions -- nothing to do here - pass - def depart_desc_type(self, node): - pass + def visit_desc_type(self, node): + # return type of C functions -- nothing to do here + pass + def depart_desc_type(self, node): + pass - def visit_desc_name(self, node): - self.body.append(self.starttag(node, 'tt', '', CLASS='descname')) - def depart_desc_name(self, node): - self.body.append('') + def visit_desc_name(self, node): + self.body.append(self.starttag(node, 'tt', '', CLASS='descname')) + def depart_desc_name(self, node): + self.body.append('') - def visit_desc_parameterlist(self, node): - self.body.append('(') - self.first_param = 1 - def depart_desc_parameterlist(self, node): - self.body.append(')') + def visit_desc_parameterlist(self, node): + self.body.append('(') + self.first_param = 1 + def depart_desc_parameterlist(self, node): + self.body.append(')') - def visit_desc_parameter(self, node): - if not self.first_param: - self.body.append(', ') - else: - self.first_param = 0 - if not node.hasattr('noemph'): - self.body.append('') - def depart_desc_parameter(self, node): - if not node.hasattr('noemph'): - self.body.append('') - - def visit_desc_optional(self, node): - self.body.append('[') - def depart_desc_optional(self, node): - self.body.append(']') - - def visit_desc_content(self, node): - self.body.append(self.starttag(node, 'dd', '')) - def depart_desc_content(self, node): - self.body.append('') - - def visit_refcount(self, node): - self.body.append(self.starttag(node, 'em', '', CLASS='refcount')) - def depart_refcount(self, node): + def visit_desc_parameter(self, node): + if not self.first_param: + self.body.append(', ') + else: + self.first_param = 0 + if not node.hasattr('noemph'): + self.body.append('') + def depart_desc_parameter(self, node): + if not node.hasattr('noemph'): self.body.append('') - def visit_versionmodified(self, node): - self.body.append(self.starttag(node, 'p')) - text = version_text[node['type']] % node['version'] - if len(node): - text += ': ' + def visit_desc_optional(self, node): + self.body.append('[') + def depart_desc_optional(self, node): + self.body.append(']') + + def visit_desc_content(self, node): + self.body.append(self.starttag(node, 'dd', '')) + def depart_desc_content(self, node): + self.body.append('') + + def visit_refcount(self, node): + self.body.append(self.starttag(node, 'em', '', CLASS='refcount')) + def depart_refcount(self, node): + self.body.append('') + + def visit_versionmodified(self, node): + self.body.append(self.starttag(node, 'p')) + text = version_text[node['type']] % node['version'] + if len(node): + text += ': ' + else: + text += '.' + self.body.append('%s' % text) + def depart_versionmodified(self, node): + self.body.append('

\n') + + # overwritten + def visit_reference(self, node): + BaseTranslator.visit_reference(self, node) + if node.hasattr('reftitle'): + # ugly hack to add a title attribute + starttag = self.body[-1] + if not starttag.startswith('%s' % text) - def depart_versionmodified(self, node): - self.body.append('

\n') + attrs = {} + self.body.append(self.starttag(node, 'h%d' % h_level, '', **attrs)) + self.context.append('\n' % h_level) + else: + BaseTranslator.visit_title(self, node, move_ids) - # overwritten - def visit_reference(self, node): - BaseTranslator.visit_reference(self, node) - if node.hasattr('reftitle'): - # ugly hack to add a title attribute - starttag = self.body[-1] - if not starttag.startswith('
\n' % h_level) + def visit_productionlist(self, node): + self.body.append(self.starttag(node, 'pre')) + names = [] + for production in node: + names.append(production['tokenname']) + maxlen = max(len(name) for name in names) + for production in node: + if production['tokenname']: + self.body.append(self.starttag(production, 'strong', '')) + self.body.append(production['tokenname'].ljust(maxlen) + + ' ::= ') + lastname = production['tokenname'] else: - BaseTranslator.visit_title(self, node, move_ids) + self.body.append('%s ' % (' '*len(lastname))) + production.walkabout(self) + self.body.append('\n') + self.body.append('\n') + raise nodes.SkipNode + def depart_productionlist(self, node): + pass - # overwritten - def visit_literal_block(self, node): - from sphinx.highlighting import highlight_block - self.body.append(highlight_block(node.rawsource, self.highlightlang)) - raise nodes.SkipNode + def visit_production(self, node): + pass + def depart_production(self, node): + pass - # overwritten - def visit_literal(self, node): - if len(node.children) == 1 and \ - node.children[0] in ('None', 'True', 'False'): - node['classes'].append('xref') - BaseTranslator.visit_literal(self, node) + def visit_centered(self, node): + self.body.append(self.starttag(node, 'p', CLASS="centered") + '') + def depart_centered(self, node): + self.body.append('

') - def visit_productionlist(self, node): - self.body.append(self.starttag(node, 'pre')) - names = [] - for production in node: - names.append(production['tokenname']) - maxlen = max(len(name) for name in names) - for production in node: - if production['tokenname']: - self.body.append(self.starttag(production, 'strong', '')) - self.body.append(production['tokenname'].ljust(maxlen) + - ' ::= ') - lastname = production['tokenname'] - else: - self.body.append('%s ' % (' '*len(lastname))) - production.walkabout(self) - self.body.append('\n') - self.body.append('\n') - raise nodes.SkipNode - def depart_productionlist(self, node): - pass + def visit_compact_paragraph(self, node): + pass + def depart_compact_paragraph(self, node): + pass - def visit_production(self, node): - pass - def depart_production(self, node): - pass + def visit_highlightlang(self, node): + self.highlightlang = node['lang'] + def depart_highlightlang(self, node): + pass - def visit_centered(self, node): - self.body.append(self.starttag(node, 'p', CLASS="centered") + '') - def depart_centered(self, node): - self.body.append('

') + def visit_toctree(self, node): + # this only happens when formatting a toc from env.tocs -- in this + # case we don't want to include the subtree + raise nodes.SkipNode - def visit_compact_paragraph(self, node): - pass - def depart_compact_paragraph(self, node): - pass + def visit_index(self, node): + raise nodes.SkipNode - def visit_highlightlang(self, node): - self.highlightlang = node['lang'] - def depart_highlightlang(self, node): - pass + def visit_glossary(self, node): + pass + def depart_glossary(self, node): + pass - def visit_toctree(self, node): - # this only happens when formatting a toc from env.tocs -- in this - # case we don't want to include the subtree - raise nodes.SkipNode + def visit_acks(self, node): + pass + def depart_acks(self, node): + pass - def visit_index(self, node): - raise nodes.SkipNode + def visit_module(self, node): + pass + def depart_module(self, node): + pass - def visit_glossary(self, node): - pass - def depart_glossary(self, node): - pass + # these are only handled specially in the SmartyPantsHTMLTranslator + def visit_literal_emphasis(self, node): + return self.visit_emphasis(node) + def depart_literal_emphasis(self, node): + return self.depart_emphasis(node) - def visit_acks(self, node): - pass - def depart_acks(self, node): - pass - - def visit_module(self, node): - pass - def depart_module(self, node): - pass - - # these are only handled specially in the SmartyPantsHTMLTranslator - def visit_literal_emphasis(self, node): - return self.visit_emphasis(node) - def depart_literal_emphasis(self, node): - return self.depart_emphasis(node) - - def depart_title(self, node): - close_tag = self.context[-1] - if builder.name != 'htmlhelp' and \ - (close_tag.startswith('\u00B6
') - BaseTranslator.depart_title(self, node) + def depart_title(self, node): + close_tag = self.context[-1] + if self.builder.name != 'htmlhelp' and \ + (close_tag.startswith('\u00B6') + BaseTranslator.depart_title(self, node) - class SmartyPantsHTMLTranslator(HTMLTranslator): - """ - Handle ordinary text via smartypants, converting quotes and dashes - to the correct entities. - """ +class SmartyPantsHTMLTranslator(HTMLTranslator): + """ + Handle ordinary text via smartypants, converting quotes and dashes + to the correct entities. + """ - def __init__(self, *args, **kwds): - self.no_smarty = 0 - HTMLTranslator.__init__(self, *args, **kwds) + def __init__(self, *args, **kwds): + self.no_smarty = 0 + HTMLTranslator.__init__(self, *args, **kwds) - def visit_literal(self, node): - self.no_smarty += 1 - try: - # this raises SkipNode - HTMLTranslator.visit_literal(self, node) - finally: - self.no_smarty -= 1 - - def visit_literal_emphasis(self, node): - self.no_smarty += 1 - self.visit_emphasis(node) - - def depart_literal_emphasis(self, node): - self.depart_emphasis(node) + def visit_literal(self, node): + self.no_smarty += 1 + try: + # this raises SkipNode + HTMLTranslator.visit_literal(self, node) + finally: self.no_smarty -= 1 - def visit_desc_signature(self, node): - self.no_smarty += 1 - HTMLTranslator.visit_desc_signature(self, node) + def visit_literal_emphasis(self, node): + self.no_smarty += 1 + self.visit_emphasis(node) - def depart_desc_signature(self, node): + def depart_literal_emphasis(self, node): + self.depart_emphasis(node) + self.no_smarty -= 1 + + def visit_desc_signature(self, node): + self.no_smarty += 1 + HTMLTranslator.visit_desc_signature(self, node) + + def depart_desc_signature(self, node): + self.no_smarty -= 1 + HTMLTranslator.depart_desc_signature(self, node) + + def visit_productionlist(self, node): + self.no_smarty += 1 + try: + HTMLTranslator.visit_productionlist(self, node) + finally: self.no_smarty -= 1 - HTMLTranslator.depart_desc_signature(self, node) - def visit_productionlist(self, node): - self.no_smarty += 1 - try: - HTMLTranslator.visit_productionlist(self, node) - finally: - self.no_smarty -= 1 - - def encode(self, text): - text = HTMLTranslator.encode(self, text) - if self.no_smarty <= 0: - text = sphinx_smarty_pants(text) - return text - - if builder.config.get('html_use_smartypants', False): - return SmartyPantsHTMLTranslator - else: - return HTMLTranslator + def encode(self, text): + text = HTMLTranslator.encode(self, text) + if self.no_smarty <= 0: + text = sphinx_smarty_pants(text) + return text diff --git a/sphinx/latexwriter.py b/sphinx/latexwriter.py index 3524f2c55d..e7a64d10c7 100644 --- a/sphinx/latexwriter.py +++ b/sphinx/latexwriter.py @@ -21,7 +21,7 @@ from docutils import frontend, nodes, languages, writers, utils from sphinx import addnodes from sphinx import highlighting - +# Move to a template? HEADER = r'''%% Generated by Sphinx. \documentclass[%(papersize)s,%(pointsize)s]{%(docclass)s} \usepackage[utf8]{inputenc} @@ -92,18 +92,18 @@ class LaTeXTranslator(nodes.NodeVisitor): self.builder = builder self.body = [] docclass = document.settings.docclass - paper = builder.config.get('latex_paper_size', 'letter') + 'paper' + paper = builder.config.latex_paper_size + 'paper' if paper == 'paper': # e.g. command line "-D latex_paper_size=" paper = 'letterpaper' - date = time.strftime(builder.config.get('today_fmt', '%B %d, %Y')) + date = time.strftime(builder.config.today_fmt) self.options = {'docclass': docclass, 'papersize': paper, - 'pointsize': builder.config.get('latex_font_size', '10pt'), - 'preamble': builder.config['latex_preamble'], + 'pointsize': builder.config.latex_font_size, + 'preamble': builder.config.latex_preamble, 'author': document.settings.author, 'filename': document.settings.filename, 'title': None, # is determined later - 'release': builder.config['release'], + 'release': builder.config.release, 'date': date, } self.context = [] diff --git a/sphinx/patchlevel.py b/sphinx/patchlevel.py deleted file mode 100644 index efd1ab73d7..0000000000 --- a/sphinx/patchlevel.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -""" - sphinx.patchlevel - ~~~~~~~~~~~~~~~~~ - - Extract version info from Include/patchlevel.h. - Adapted from Doc/tools/getversioninfo. - - XXX Python specific - - :copyright: 2007-2008 by Georg Brandl. - :license: BSD. -""" - -import os -import re -import sys - -def get_version_info(srcdir): - patchlevel_h = os.path.join(srcdir, '..', "Include", "patchlevel.h") - - # This won't pick out all #defines, but it will pick up the ones we - # care about. - rx = re.compile(r"\s*#define\s+([a-zA-Z][a-zA-Z_0-9]*)\s+([a-zA-Z_0-9]+)") - - d = {} - f = open(patchlevel_h) - try: - for line in f: - m = rx.match(line) - if m is not None: - name, value = m.group(1, 2) - d[name] = value - finally: - f.close() - - release = version = "%s.%s" % (d["PY_MAJOR_VERSION"], d["PY_MINOR_VERSION"]) - micro = int(d["PY_MICRO_VERSION"]) - if micro != 0: - release += "." + str(micro) - - level = d["PY_RELEASE_LEVEL"] - suffixes = { - "PY_RELEASE_LEVEL_ALPHA": "a", - "PY_RELEASE_LEVEL_BETA": "b", - "PY_RELEASE_LEVEL_GAMMA": "c", - } - if level != "PY_RELEASE_LEVEL_FINAL": - release += suffixes[level] + str(int(d["PY_RELEASE_SERIAL"])) - return version, release - - -def get_sys_version_info(): - major, minor, micro, level, serial = sys.version_info - release = version = '%s.%s' % (major, minor) - if micro: - release += '.%s' % micro - if level != 'final': - release += '%s%s' % (level[0], serial) - return version, release diff --git a/sphinx/roles.py b/sphinx/roles.py index 73b65abc98..4570fbc7b5 100644 --- a/sphinx/roles.py +++ b/sphinx/roles.py @@ -109,7 +109,7 @@ def xfileref_role(typ, rawtext, text, lineno, inliner, options={}, content=[]): if text.endswith('()'): # remove parentheses text = text[:-2] - if env.config.get('add_function_parentheses', True): + if env.config.add_function_parentheses: # add them back to all occurrences if configured text += '()' # if the first character is a bang, don't cross-reference at all diff --git a/sphinx/templates/layout.html b/sphinx/templates/layout.html index 0f518d9118..68f253067d 100644 --- a/sphinx/templates/layout.html +++ b/sphinx/templates/layout.html @@ -113,7 +113,8 @@ {% if customsidebar %} {{ rendertemplate(customsidebar) }} {% endif %} - {% if current_page_name != "search" %} + {% if current_page_name != "search" and builder == 'web' %} + {# HTML builder search is disabled for now #}

{{ builder == 'web' and 'Keyword' or 'Quick' }} search