sphinx / sphinx / builders /

# -*- coding: utf-8 -*-

    Builder superclass for all builders.

    :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.

import os
from os import path

from docutils import nodes

from sphinx.util.osutil import SEP, relative_uri
from sphinx.util.console import bold, purple, darkgreen, term_width_line

# side effect: registers roles and directives
from sphinx import roles
from sphinx import directives

class Builder(object):
    Builds target formats from the reST sources.

    # builder's name, for the -b command line options
    name = ''
    # builder's output format, or '' if no document output is produced
    format = ''

    def __init__(self, app):
        self.env = app.env
        self.srcdir = app.srcdir
        self.confdir = app.confdir
        self.outdir = app.outdir
        self.doctreedir = app.doctreedir
        if not path.isdir(self.doctreedir):
            os.makedirs(self.doctreedir) = app
        self.warn = app.warn =
        self.config = app.config
        self.tags = app.tags

        # images that need to be copied over (source -> dest)
        self.images = {}


    # helper methods
    def init(self):
        Load necessary templates and perform initialization.  The default
        implementation does nothing.

    def create_template_bridge(self):
        Return the template bridge configured.
        if self.config.template_bridge:
            self.templates =
                self.config.template_bridge, 'template_bridge setting')()
            from sphinx.jinja2glue import BuiltinTemplateLoader
            self.templates = BuiltinTemplateLoader()

    def get_target_uri(self, docname, typ=None):
        Return the target URI for a document name (*typ* can be used to qualify
        the link characteristic for individual builders).
        raise NotImplementedError

    def get_relative_uri(self, from_, to, typ=None):
        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))

    def get_outdated_docs(self):
        Return an iterable of output files that are outdated, or a string
        describing what an update build will build.

        If the builder does not output individual files corresponding to
        source files, return a string here.  If it does, return an iterable
        of those files that need to be written.
        raise NotImplementedError

    def old_status_iterator(self, iterable, summary, colorfunc=darkgreen):
        l = 0
        for item in iterable:
            if l == 0:
      , nonl=1)
                l = 1
   + ' ', nonl=1)
            yield item
        if l == 1:

    # new version with progress info
    def status_iterator(self, iterable, summary, colorfunc=darkgreen, length=0):
        if length == 0:
            for item in self.old_status_iterator(iterable, summary, colorfunc):
                yield item
        l = 0
        summary = bold(summary)
        for item in iterable:
            l += 1
  '%s[%3d%%] %s' %
                                      (summary, 100*l/length,
                                       colorfunc(item))), nonl=1)
            yield item
        if l > 0:

    supported_image_types = []

    def post_process_images(self, doctree):
        Pick the best candidate for all image URIs.
        for node in doctree.traverse(nodes.image):
            if '?' in node['candidates']:
                # don't rewrite nonlocal image URIs
            if '*' not in node['candidates']:
                for imgtype in self.supported_image_types:
                    candidate = node['candidates'].get(imgtype, None)
                    if candidate:
                        'no matching candidate for image URI %r' % node['uri'],
                        '%s:%s' % (node.source, getattr(node, 'line', '')))
                node['uri'] = candidate
                candidate = node['uri']
            if candidate not in self.env.images:
                # non-existing URI; let it alone
            self.images[candidate] = self.env.images[candidate][1]

    # build methods

    def build_all(self):
        """Build all source files.""", summary='all source files', method='all')

    def build_specific(self, filenames):
        """Only rebuild as much as needed for changes in the *filenames*."""
        # bring the filenames to the canonical format, that is,
        # relative to the source directory and without source_suffix.
        dirlen = len(self.srcdir) + 1
        to_write = []
        suffix = self.config.source_suffix
        for filename in filenames:
            filename = path.normpath(path.abspath(filename))
            if not filename.startswith(self.srcdir):
                self.warn('file %r given on command line is not under the '
                          'source directory, ignoring' % filename)
            if not (path.isfile(filename) or path.isfile(filename + suffix)):
                self.warn('file %r given on command line does not exist, '
                          'ignoring' % filename)
            filename = filename[dirlen:]
            if filename.endswith(suffix):
                filename = filename[:-len(suffix)]
            filename = filename.replace(path.sep, SEP)
            to_write.append(filename), method='specific',
                   summary='%d source files given on command '
                   'line' % len(to_write))

    def build_update(self):
        """Only rebuild what was changed or added since last build."""
        to_build = self.get_outdated_docs()
        if isinstance(to_build, str):
  ['__all__'], to_build)
            to_build = list(to_build)
                       summary='targets for %d source files that are '
                       'out of date' % len(to_build))

    def build(self, docnames, summary=None, method='update'):
        Main build method.  First updates the environment, and then
        calls :meth:`write`.
        if summary:
  'building [%s]: ' %, nonl=1)

        updated_docnames = set()
        # while reading, collect all warnings from docutils
        warnings = []
        self.env.set_warnfunc(lambda *args: warnings.append(args))'updating environment: '), nonl=1)
        msg, length, iterator = self.env.update(self.config, self.srcdir,
        for docname in self.status_iterator(iterator, 'reading sources... ',
                                            purple, length):
            # nothing further to do, the environment has already
            # done the reading
        for warning in warnings:

        doccount = len(updated_docnames)'looking for now-outdated files... '), nonl=1)
        for docname in self.env.check_dependents(updated_docnames):
        outdated = len(updated_docnames) - doccount
        if outdated:
  '%d found' % outdated)
  'none found')

        if updated_docnames:
            # save the environment
            from sphinx.application import ENV_PICKLE_FILENAME
  'pickling environment... '), nonl=True)
            self.env.topickle(path.join(self.doctreedir, ENV_PICKLE_FILENAME))

            # global actions
  'checking consistency... '), nonl=True)
            if method == 'update' and not docnames:
      'no targets are out of date.'))

        # another indirection to support builders that don't build
        # files individually
        self.write(docnames, list(updated_docnames), method)

        # finish (write static files etc.)
        status = ( == 0 and 'succeeded'
                                           or 'finished with problems')
  'build %s, %s warning%s.' %
                   != 1 and 's' or '')))
  'build %s.' % status))

    def write(self, build_docnames, updated_docnames, method='update'):
        if build_docnames is None or build_docnames == ['__all__']:
            # build_all
            build_docnames = self.env.found_docs
        if method == 'update':
            # build updated ones as well
            docnames = set(build_docnames) | set(updated_docnames)
            docnames = set(build_docnames)

        # add all toctree-containing files that may have changed
        for docname in list(docnames):
            for tocdocname in self.env.files_to_rebuild.get(docname, []):
        docnames.add(self.config.master_doc)'preparing documents... '), nonl=True)

        # write target files
        warnings = []
        self.env.set_warnfunc(lambda *args: warnings.append(args))
        for docname in self.status_iterator(
            sorted(docnames), 'writing output... ', darkgreen, len(docnames)):
            doctree = self.env.get_and_resolve_doctree(docname, self)
            self.write_doc(docname, doctree)
        for warning in warnings:

    def prepare_writing(self, docnames):
        raise NotImplementedError

    def write_doc(self, docname, doctree):
        raise NotImplementedError

    def finish(self):
        Finish the building process.  The default implementation does nothing.

    def cleanup(self):
        Cleanup any resources.  The default implementation does nothing.

    'html':       ('html', 'StandaloneHTMLBuilder'),
    'dirhtml':    ('html', 'DirectoryHTMLBuilder'),
    'singlehtml': ('html', 'SingleFileHTMLBuilder'),
    'pickle':     ('html', 'PickleHTMLBuilder'),
    'json':       ('html', 'JSONHTMLBuilder'),
    'web':        ('html', 'PickleHTMLBuilder'),
    'htmlhelp':   ('htmlhelp', 'HTMLHelpBuilder'),
    'devhelp':    ('devhelp', 'DevhelpBuilder'),
    'qthelp':     ('qthelp', 'QtHelpBuilder'),
    'epub':       ('epub', 'EpubBuilder'),
    'latex':      ('latex', 'LaTeXBuilder'),
    'text':       ('text', 'TextBuilder'),
    'man':        ('manpage', 'ManualPageBuilder'),
    'changes':    ('changes', 'ChangesBuilder'),
    'linkcheck':  ('linkcheck', 'CheckExternalLinksBuilder'),