Add a dependency system for handling .. include, .. literalinclude

and later .. image dependencies.
This commit is contained in:
Georg Brandl 2008-03-25 10:16:51 +00:00
parent 59a60d5e9f
commit 10e231bf12
6 changed files with 88 additions and 44 deletions

View File

@ -1,3 +1,10 @@
Changes in trunk
================
* sphinx.environment: Take dependent files into account when collecting
the set of outdated sources.
Release 0.1.61843 (Mar 24, 2008) Release 0.1.61843 (Mar 24, 2008)
================================ ================================

View File

@ -127,8 +127,7 @@ class Builder(object):
# build methods # build methods
def load_env(self): def load_env(self):
"""Set up the build environment. Return True if a pickled file could be """Set up the build environment."""
successfully loaded, False if a new environment had to be created."""
if self.env: if self.env:
return return
if not self.freshenv: if not self.freshenv:
@ -143,8 +142,10 @@ class Builder(object):
else: else:
self.info('failed: %s' % err) self.info('failed: %s' % err)
self.env = BuildEnvironment(self.srcdir, self.doctreedir, self.config) self.env = BuildEnvironment(self.srcdir, self.doctreedir, self.config)
self.env.find_files(self.config)
else: else:
self.env = BuildEnvironment(self.srcdir, self.doctreedir, self.config) self.env = BuildEnvironment(self.srcdir, self.doctreedir, self.config)
self.env.find_files(self.config)
self.env.set_warnfunc(self.warn) self.env.set_warnfunc(self.warn)
def build_all(self): def build_all(self):
@ -171,10 +172,6 @@ class Builder(object):
def build_update(self): def build_update(self):
"""Only rebuild files changed or added since last build.""" """Only rebuild files changed or added since last build."""
to_build = self.get_outdated_docs() to_build = self.get_outdated_docs()
if not to_build and self.env.all_docs:
# if there is nothing in all_docs, it's a fresh env
self.info(bold('no target files are out of date, exiting.'))
return
if isinstance(to_build, str): if isinstance(to_build, str):
self.build([], to_build) self.build([], to_build)
else: else:
@ -213,6 +210,10 @@ class Builder(object):
# global actions # global actions
self.info(bold('checking consistency...')) self.info(bold('checking consistency...'))
self.env.check_consistency() self.env.check_consistency()
else:
if not docnames:
self.info(bold('no targets are out of date.'))
return
# another indirection to support methods which don't build files # another indirection to support methods which don't build files
# individually # individually
@ -222,14 +223,15 @@ class Builder(object):
self.info(bold('finishing... ')) self.info(bold('finishing... '))
self.finish() self.finish()
if self.app._warncount: if self.app._warncount:
self.info(bold('build succeeded, %s warnings.' % self.app._warncount)) self.info(bold('build succeeded, %s warning%s.' %
(self.app._warncount, self.app._warncount != 1 and 's' or '')))
else: else:
self.info(bold('build succeeded.')) self.info(bold('build succeeded.'))
def write(self, build_docnames, updated_docnames, method='update'): def write(self, build_docnames, updated_docnames, method='update'):
if build_docnames is None: if build_docnames is None:
# build_all # build_all
build_docnames = self.env.all_docs build_docnames = self.env.found_docs
if method == 'update': if method == 'update':
# build updated ones as well # build updated ones as well
docnames = set(build_docnames) | set(updated_docnames) docnames = set(build_docnames) | set(updated_docnames)
@ -383,7 +385,7 @@ class StandaloneHTMLBuilder(Builder):
self.handle_page(docname, ctx) self.handle_page(docname, ctx)
def finish(self): def finish(self):
self.info(bold('writing additional files...')) self.info(bold('writing additional files...'), nonl=1)
# the global general index # the global general index
@ -397,6 +399,7 @@ class StandaloneHTMLBuilder(Builder):
genindexentries = self.env.index, genindexentries = self.env.index,
genindexcounts = indexcounts, genindexcounts = indexcounts,
) )
self.info(' genindex', nonl=1)
self.handle_page('genindex', genindexcontext, 'genindex.html') self.handle_page('genindex', genindexcontext, 'genindex.html')
# the global module index # the global module index
@ -442,21 +445,26 @@ class StandaloneHTMLBuilder(Builder):
modindexentries = modindexentries, modindexentries = modindexentries,
platforms = platforms, platforms = platforms,
) )
self.info(' modindex', nonl=1)
self.handle_page('modindex', modindexcontext, 'modindex.html') self.handle_page('modindex', modindexcontext, 'modindex.html')
# the search page # the search page
self.info(' search', nonl=1)
self.handle_page('search', {}, 'search.html') self.handle_page('search', {}, 'search.html')
# additional pages from conf.py # additional pages from conf.py
for pagename, template in self.config.html_additional_pages.items(): for pagename, template in self.config.html_additional_pages.items():
self.info(' '+pagename, nonl=1)
self.handle_page(pagename, {}, template) self.handle_page(pagename, {}, template)
# the index page # the index page
indextemplate = self.config.html_index indextemplate = self.config.html_index
if indextemplate: if indextemplate:
self.info(' index', nonl=1)
self.handle_page('index', {'indextemplate': indextemplate}, 'index.html') self.handle_page('index', {'indextemplate': indextemplate}, 'index.html')
# copy static files # copy static files
self.info()
self.info(bold('copying static files...')) self.info(bold('copying static files...'))
ensuredir(path.join(self.outdir, 'static')) ensuredir(path.join(self.outdir, 'static'))
staticdirnames = [path.join(path.dirname(__file__), 'static')] + \ staticdirnames = [path.join(path.dirname(__file__), 'static')] + \
@ -481,10 +489,7 @@ class StandaloneHTMLBuilder(Builder):
return docname + '.html' return docname + '.html'
def get_outdated_docs(self): def get_outdated_docs(self):
for docname in get_matching_docs( for docname in self.env.found_docs:
self.srcdir, self.config.source_suffix,
exclude=set(self.config.unused_docs),
prune=['_sources']):
targetname = self.env.doc2path(docname, self.outdir, '.html') targetname = self.env.doc2path(docname, self.outdir, '.html')
try: try:
targetmtime = path.getmtime(targetname) targetmtime = path.getmtime(targetname)
@ -566,10 +571,7 @@ class PickleHTMLBuilder(StandaloneHTMLBuilder):
self.init_translator_class() self.init_translator_class()
def get_outdated_docs(self): def get_outdated_docs(self):
for docname in get_matching_docs( for docname in self.env.found_docs:
self.srcdir, self.config.source_suffix,
exclude=set(self.config.unused_docs),
prune=['_sources']):
targetname = self.env.doc2path(docname, self.outdir, '.fpickle') targetname = self.env.doc2path(docname, self.outdir, '.fpickle')
try: try:
targetmtime = path.getmtime(targetname) targetmtime = path.getmtime(targetname)

View File

@ -664,10 +664,10 @@ def literalinclude_directive(name, arguments, options, content, lineno,
if not state.document.settings.file_insertion_enabled: if not state.document.settings.file_insertion_enabled:
return [state.document.reporter.warning('File insertion disabled', line=lineno)] return [state.document.reporter.warning('File insertion disabled', line=lineno)]
env = state.document.settings.env env = state.document.settings.env
fn = arguments[0] rel_fn = arguments[0]
source_dir = path.dirname(path.abspath(state_machine.input_lines.source( source_dir = path.dirname(path.abspath(state_machine.input_lines.source(
lineno - state_machine.input_offset - 1))) lineno - state_machine.input_offset - 1)))
fn = path.normpath(path.join(source_dir, fn)) fn = path.normpath(path.join(source_dir, rel_fn))
try: try:
f = open(fn) f = open(fn)
@ -683,6 +683,7 @@ def literalinclude_directive(name, arguments, options, content, lineno,
retnode['language'] = options['language'] retnode['language'] = options['language']
if 'linenos' in options: if 'linenos' in options:
retnode['linenos'] = True retnode['linenos'] = True
state.document.settings.env.note_dependency(rel_fn)
return [retnode] return [retnode]
literalinclude_directive.options = {'linenos': directives.flag, literalinclude_directive.options = {'linenos': directives.flag,

View File

@ -57,7 +57,7 @@ default_settings = {
# This is increased every time a new environment attribute is added # This is increased every time a new environment attribute is added
# to properly invalidate pickle files. # to properly invalidate pickle files.
ENV_VERSION = 18 ENV_VERSION = 19
def walk_depth(node, depth, maxdepth): def walk_depth(node, depth, maxdepth):
@ -218,8 +218,10 @@ class BuildEnvironment:
# All "docnames" here are /-separated and relative and exclude the source suffix. # All "docnames" here are /-separated and relative and exclude the source suffix.
self.found_docs = set() # contains all existing docnames self.found_docs = set() # contains all existing docnames
self.all_docs = {} # docname -> (mtime, md5sum) at the time of build self.all_docs = {} # docname -> mtime at the time of build
# contains all built docnames # contains all built docnames
self.dependencies = {} # docname -> set of dependent file names, relative to
# documentation root
# File metadata # File metadata
self.metadata = {} # docname -> dict of metadata items self.metadata = {} # docname -> dict of metadata items
@ -278,6 +280,7 @@ class BuildEnvironment:
if docname in self.all_docs: if docname in self.all_docs:
self.all_docs.pop(docname, None) self.all_docs.pop(docname, None)
self.metadata.pop(docname, None) self.metadata.pop(docname, None)
self.dependencies.pop(docname, None)
self.titles.pop(docname, None) self.titles.pop(docname, None)
self.tocs.pop(docname, None) self.tocs.pop(docname, None)
self.toc_num_entries.pop(docname, None) self.toc_num_entries.pop(docname, None)
@ -318,14 +321,18 @@ class BuildEnvironment:
else: else:
return path.join(base, docname.replace(SEP, path.sep)) + suffix return path.join(base, docname.replace(SEP, path.sep)) + suffix
def get_outdated_files(self, config, config_changed): def find_files(self, config):
""" """
Return (added, changed, removed) sets. Find all source files in the source dir and put them in self.found_docs.
""" """
self.found_docs = set(get_matching_docs(self.srcdir, config.source_suffix, self.found_docs = set(get_matching_docs(self.srcdir, config.source_suffix,
exclude=set(config.unused_docs), exclude=set(config.unused_docs),
prune=['_sources'])) prune=['_sources']))
def get_outdated_files(self, config_changed):
"""
Return (added, changed, removed) sets.
"""
# clear all files no longer present # clear all files no longer present
removed = set(self.all_docs) - self.found_docs removed = set(self.all_docs) - self.found_docs
@ -339,17 +346,28 @@ class BuildEnvironment:
for docname in self.found_docs: for docname in self.found_docs:
if docname not in self.all_docs: if docname not in self.all_docs:
added.add(docname) added.add(docname)
else: continue
# if the doctree file is not there, rebuild # if the doctree file is not there, rebuild
if not path.isfile(self.doc2path(docname, self.doctreedir, if not path.isfile(self.doc2path(docname, self.doctreedir,
'.doctree')): '.doctree')):
changed.add(docname)
continue
mtime, md5sum = self.all_docs[docname]
newmtime = path.getmtime(self.doc2path(docname))
if newmtime == mtime:
continue
changed.add(docname) changed.add(docname)
continue
# check the mtime of the document
mtime = self.all_docs[docname]
newmtime = path.getmtime(self.doc2path(docname))
if newmtime > mtime:
changed.add(docname)
continue
# finally, check the mtime of dependencies
for dep in self.dependencies.get(docname, ()):
deppath = path.join(self.srcdir, dep)
if not path.isfile(deppath):
changed.add(docname)
break
depmtime = path.getmtime(deppath)
if depmtime > mtime:
changed.add(docname)
break
return added, changed, removed return added, changed, removed
@ -369,12 +387,14 @@ class BuildEnvironment:
continue continue
if not hasattr(self.config, key) or \ if not hasattr(self.config, key) or \
self.config[key] != config[key]: self.config[key] != config[key]:
msg = '[config changed] ' msg = '[config changed] '
config_changed = True config_changed = True
break break
else: else:
msg = '' msg = ''
added, changed, removed = self.get_outdated_files(config, config_changed) self.find_files(config)
added, changed, removed = self.get_outdated_files(config_changed)
msg += '%s added, %s changed, %s removed' % (len(added), len(changed), msg += '%s added, %s changed, %s removed' % (len(added), len(changed),
len(removed)) len(removed))
yield msg yield msg
@ -409,18 +429,14 @@ class BuildEnvironment:
doctree = publish_doctree(None, src_path, FileInput, doctree = publish_doctree(None, src_path, FileInput,
settings_overrides=self.settings, settings_overrides=self.settings,
reader=MyStandaloneReader()) reader=MyStandaloneReader())
self.process_dependencies(docname, doctree)
self.process_metadata(docname, doctree) self.process_metadata(docname, doctree)
self.create_title_from(docname, doctree) self.create_title_from(docname, doctree)
self.note_labels_from(docname, doctree) self.note_labels_from(docname, doctree)
self.build_toc_from(docname, doctree) self.build_toc_from(docname, doctree)
# calculate the MD5 of the file at time of build # store time of reading, used to find outdated files
f = open(src_path, 'rb') self.all_docs[docname] = time.time()
try:
md5sum = md5(f.read()).digest()
finally:
f.close()
self.all_docs[docname] = (path.getmtime(src_path), md5sum)
if app: if app:
app.emit('doctree-read', doctree) app.emit('doctree-read', doctree)
@ -430,6 +446,7 @@ class BuildEnvironment:
doctree.transformer = None doctree.transformer = None
doctree.settings.warning_stream = None doctree.settings.warning_stream = None
doctree.settings.env = None doctree.settings.env = None
doctree.settings.record_dependencies = None
# cleanup # cleanup
self.docname = None self.docname = None
@ -452,6 +469,18 @@ class BuildEnvironment:
else: else:
return doctree return doctree
def process_dependencies(self, docname, doctree):
"""
Process docutils-generated dependency info.
"""
deps = doctree.settings.record_dependencies
if not deps:
return
basename = path.dirname(self.doc2path(docname, base=None))
for dep in deps.list:
dep = path.join(basename, dep)
self.dependencies.setdefault(docname, set()).add(dep)
def process_metadata(self, docname, doctree): def process_metadata(self, docname, doctree):
""" """
Process the docinfo part of the doctree as metadata. Process the docinfo part of the doctree as metadata.
@ -602,6 +631,11 @@ class BuildEnvironment:
def note_versionchange(self, type, version, node, lineno): def note_versionchange(self, type, version, node, lineno):
self.versionchanges.setdefault(version, []).append( self.versionchanges.setdefault(version, []).append(
(type, self.docname, lineno, self.currmodule, self.currdesc, node.astext())) (type, self.docname, lineno, self.currmodule, self.currdesc, node.astext()))
def note_dependency(self, filename):
basename = path.dirname(self.doc2path(self.docname, base=None))
filename = path.join(basename, filename)
self.dependencies.setdefault(self.docname, set()).add(filename)
# ------- # -------
# --------- RESOLVING REFERENCES AND TOCTREES ------------------------------ # --------- RESOLVING REFERENCES AND TOCTREES ------------------------------

View File

@ -183,7 +183,7 @@ Results of doctest builder run on %s
return '' return ''
def get_outdated_docs(self): def get_outdated_docs(self):
return self.env.all_docs return self.env.found_docs
def finish(self): def finish(self):
# write executive summary # write executive summary
@ -204,7 +204,7 @@ Doctest summary
def write(self, build_docnames, updated_docnames, method='update'): def write(self, build_docnames, updated_docnames, method='update'):
if build_docnames is None: if build_docnames is None:
build_docnames = self.env.all_docs build_docnames = sorted(self.env.all_docs)
self.info(bold('running tests...')) self.info(bold('running tests...'))
for docname in build_docnames: for docname in build_docnames:

View File

@ -42,7 +42,7 @@ class CheckExternalLinksBuilder(Builder):
return '' return ''
def get_outdated_docs(self): def get_outdated_docs(self):
return self.env.all_docs return self.env.found_docs
def prepare_writing(self, docnames): def prepare_writing(self, docnames):
return return