Added an `only` directive that can selectively include text

based on enabled "tags".  Tags can be given on the command
line.  Also, the current builder output format (e.g. "html" or
"latex") is always a defined tag.
This commit is contained in:
Georg Brandl
2009-02-19 21:56:34 +01:00
parent 8e4ef0d585
commit 4c81b055c8
20 changed files with 242 additions and 27 deletions

View File

@@ -36,6 +36,11 @@ New features added
- #4: Added a ``:download:`` role that marks a non-document file
for inclusion into the HTML output and links to it.
- Added an ``only`` directive that can selectively include text
based on enabled "tags". Tags can be given on the command
line. Also, the current builder output format (e.g. "html" or
"latex") is always a defined tag.
- The ``literalinclude`` directive now supports several more
options, to include only parts of a file.

View File

@@ -38,6 +38,11 @@ Important points to note:
delete them from the namespace with ``del`` if appropriate. Modules are
removed automatically, so you don't need to ``del`` your imports after use.
* There is a special object named ``tags`` available in the config file.
It can be used to query and change the tags (see :ref:`tags`). Use
``tags.has('tag')`` to query, ``tags.add('tag')`` and ``tags.remove('tag')``
to change.
General configuration
---------------------

View File

@@ -89,6 +89,12 @@ The :program:`sphinx-build` script has several more options:
cross-references), but rebuild it completely. The default is to only read
and parse source files that are new or have changed since the last run.
**-t** *tag*
Define the tag *tag*. This is relevant for :dir:`only` directives that only
include their content if this tag is set.
.. versionadded:: 0.6
**-d** *path*
Since Sphinx has to read and parse all source files before it can write an
output file, the parsed source files are cached as "doctree pickles".

View File

@@ -48,6 +48,28 @@ Meta-information markup
output.
.. _tags:
Including content based on tags
-------------------------------
.. directive:: .. only:: <expression>
Include the content of the directive only if the *expression* is true. The
expression should consist of tags, like this::
.. only:: html and draft
Undefined tags are false, defined tags (via the ``-t`` command-line option or
within :file:`conf.py`) are true. Boolean expressions, also using
parentheses (like ``html and (latex or draft)`` are supported.
The format of the current builder (``html``, ``latex`` or ``text``) is always
set as a tag.
.. versionadded:: 0.6
Tables
------

View File

@@ -36,7 +36,7 @@ are already present, work fine and can be seen "in action" in the Python docs:
and inclusion of appropriately formatted docstrings.
'''
requires = ['Pygments>=0.8', 'Jinja2>=2.0', 'docutils>=0.4']
requires = ['Pygments>=0.8', 'Jinja2>=2.1', 'docutils>=0.4']
if sys.version_info < (2, 4):
print 'ERROR: Sphinx requires at least Python 2.4 to run.'

View File

@@ -96,6 +96,9 @@ class start_of_file(nodes.Element): pass
# tabular column specification, used for the LaTeX writer
class tabular_col_spec(nodes.Element): pass
# only (in/exclusion based on tags)
class only(nodes.Element): pass
# meta directive -- same as docutils' standard meta node, but pickleable
class meta(nodes.Special, nodes.PreBibliographic, nodes.Element): pass

View File

@@ -59,6 +59,7 @@ from sphinx.config import Config
from sphinx.builders import BUILTIN_BUILDERS
from sphinx.directives import GenericDesc, Target, additional_xref_types
from sphinx.environment import SphinxStandaloneReader
from sphinx.util.tags import Tags
from sphinx.util.compat import Directive, directive_dwim
from sphinx.util.console import bold
@@ -82,7 +83,7 @@ class Sphinx(object):
def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
confoverrides, status, warning=sys.stderr, freshenv=False,
warningiserror=False):
warningiserror=False, tags=None):
self.next_listener_id = 0
self._listeners = {}
self.builderclasses = BUILTIN_BUILDERS.copy()
@@ -113,7 +114,8 @@ class Sphinx(object):
self.statuscode = 0
# read config
self.config = Config(confdir, CONFIG_FILENAME, confoverrides)
self.tags = Tags(tags)
self.config = Config(confdir, CONFIG_FILENAME, confoverrides, self.tags)
# load all extension modules
for extension in self.config.extensions:
@@ -141,6 +143,8 @@ class Sphinx(object):
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.emit('builder-inited')
def build(self, all_files, filenames):

View File

@@ -35,6 +35,8 @@ class Builder(object):
# 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, env=None, freshenv=False):
self.srcdir = app.srcdir

View File

@@ -60,12 +60,13 @@ class StandaloneHTMLBuilder(Builder):
Builds standalone HTML docs.
"""
name = 'html'
format = 'html'
copysource = True
out_suffix = '.html'
link_suffix = '.html' # defaults to matching out_suffix
indexer_format = js_index
supported_image_types = ['image/svg+xml',
'image/png', 'image/gif', 'image/jpeg']
supported_image_types = ['image/svg+xml', 'image/png',
'image/gif', 'image/jpeg']
searchindex_filename = 'searchindex.js'
add_permalinks = True
embedded = False # for things like HTML help or Qt help: suppresses sidebar
@@ -76,6 +77,7 @@ class StandaloneHTMLBuilder(Builder):
def init(self):
# a hash of all config values that, if changed, cause a full rebuild
self.config_hash = ''
self.tags_hash = ''
self.init_templates()
self.init_highlighter()
self.init_translator_class()
@@ -116,23 +118,28 @@ class StandaloneHTMLBuilder(Builder):
for (name, desc) in self.config.values.iteritems()
if desc[1] == 'html')
self.config_hash = md5(str(cfgdict)).hexdigest()
self.tags_hash = md5(str(sorted(self.tags))).hexdigest()
old_config_hash = old_tags_hash = ''
try:
fp = open(path.join(self.outdir, '.buildinfo'))
version = fp.readline()
if version.rstrip() != '# Sphinx build info version 1':
raise ValueError
fp.readline() # skip commentary
cfg, old_hash = fp.readline().strip().split(': ')
cfg, old_config_hash = fp.readline().strip().split(': ')
if cfg != 'config':
raise ValueError
tag, old_tags_hash = fp.readline().strip().split(': ')
if tag != 'tags':
raise ValueError
fp.close()
except ValueError:
self.warn('unsupported build info format in %r, building all' %
path.join(self.outdir, '.buildinfo'))
old_hash = ''
except Exception:
old_hash = ''
if old_hash != self.config_hash:
pass
if old_config_hash != self.config_hash or \
old_tags_hash != self.tags_hash:
for docname in self.env.found_docs:
yield docname
return
@@ -544,7 +551,8 @@ class StandaloneHTMLBuilder(Builder):
fp.write('# Sphinx build info version 1\n'
'# This file hashes the configuration used when building'
' these files. When it is not found, a full rebuild will'
' be done.\nconfig: %s\n' % self.config_hash)
' be done.\nconfig: %s\ntags: %s\n' %
(self.config_hash, self.tags_hash))
finally:
fp.close()
@@ -721,8 +729,8 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder):
#: the filename for the global context file
globalcontext_filename = None
supported_image_types = ('image/svg+xml', 'image/png', 'image/gif',
'image/jpeg')
supported_image_types = ['image/svg+xml', 'image/png',
'image/gif', 'image/jpeg']
def init(self):
self.init_translator_class()

View File

@@ -31,8 +31,9 @@ class LaTeXBuilder(Builder):
Builds LaTeX output to create PDF.
"""
name = 'latex'
supported_image_types = ['application/pdf', 'image/png', 'image/gif',
'image/jpeg']
format = 'latex'
supported_image_types = ['application/pdf', 'image/png',
'image/gif', 'image/jpeg']
def init(self):
self.docnames = []

View File

@@ -21,6 +21,7 @@ from sphinx.writers.text import TextWriter
class TextBuilder(Builder):
name = 'text'
format = 'text'
out_suffix = '.txt'
def init(self):

View File

@@ -34,6 +34,7 @@ Options: -b <builder> -- builder to use; default is html
-a -- write all files; default is to only write \
new and changed files
-E -- don't use a saved environment, always read all files
-t <tag> -- include "only" blocks with <tag>
-d <path> -- path for the cached environment and doctree files
(default: outdir/.doctrees)
-c <path> -- path where configuration file (conf.py) is located
@@ -58,7 +59,7 @@ def main(argv):
nocolor()
try:
opts, args = getopt.getopt(argv[1:], 'ab:d:c:CD:A:NEqQWP')
opts, args = getopt.getopt(argv[1:], 'ab:t:d:c:CD:A:NEqQWP')
allopts = set(opt[0] for opt in opts)
srcdir = confdir = path.abspath(args[0])
if not path.isdir(srcdir):
@@ -92,6 +93,7 @@ def main(argv):
warning = sys.stderr
confoverrides = {}
htmlcontext = {}
tags = []
doctreedir = path.join(outdir, '.doctrees')
for opt, val in opts:
if opt == '-b':
@@ -101,6 +103,8 @@ def main(argv):
usage(argv, 'Cannot combine -a option and filenames.')
return 1
all_files = True
elif opt == '-t':
tags.append(val)
elif opt == '-d':
doctreedir = path.abspath(val)
elif opt == '-c':
@@ -152,7 +156,8 @@ def main(argv):
try:
app = Sphinx(srcdir, confdir, outdir, doctreedir, buildername,
confoverrides, status, warning, freshenv, warningiserror)
confoverrides, status, warning, freshenv,
warningiserror, tags)
app.build(all_files, filenames)
return app.statuscode
except KeyboardInterrupt:

View File

@@ -106,12 +106,13 @@ class Config(object):
latex_preamble = ('', None),
)
def __init__(self, dirname, filename, overrides):
def __init__(self, dirname, filename, overrides, tags):
self.overrides = overrides
self.values = Config.config_values.copy()
config = {}
if dirname is not None:
config['__file__'] = path.join(dirname, filename)
config['tags'] = tags
olddir = os.getcwd()
try:
os.chdir(dirname)

View File

@@ -383,6 +383,23 @@ class ProductionList(Directive):
return [node] + messages
class TabularColumns(Directive):
"""
Directive to give an explicit tabulary column definition to LaTeX.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {}
def run(self):
node = addnodes.tabular_col_spec()
node['spec'] = self.arguments[0]
return [node]
class Glossary(Directive):
"""
Directive to create a glossary with cross-reference targets
@@ -506,20 +523,23 @@ class HList(Directive):
return [newnode]
class TabularColumns(Directive):
class Only(Directive):
"""
Directive to give an explicit tabulary column definition to LaTeX.
Directive to only include text if the given tag(s) are enabled.
"""
has_content = False
has_content = True
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {}
def run(self):
node = addnodes.tabular_col_spec()
node['spec'] = self.arguments[0]
node = addnodes.only()
node.document = self.state.document
node.line = self.lineno
node['expr'] = self.arguments[0]
self.state.nested_parse(self.content, self.content_offset, node)
return [node]
@@ -535,11 +555,12 @@ directives.register_directive('versionadded', VersionChange)
directives.register_directive('versionchanged', VersionChange)
directives.register_directive('seealso', SeeAlso)
directives.register_directive('productionlist', ProductionList)
directives.register_directive('tabularcolumns', TabularColumns)
directives.register_directive('glossary', Glossary)
directives.register_directive('centered', Centered)
directives.register_directive('acks', Acks)
directives.register_directive('hlist', HList)
directives.register_directive('tabularcolumns', TabularColumns)
directives.register_directive('only', Only)
# register the standard rst class directive under a different name

View File

@@ -1270,6 +1270,19 @@ class BuildEnvironment:
if newnode:
node.replace_self(newnode)
for node in doctree.traverse(addnodes.only):
try:
ret = builder.tags.eval_condition(node['expr'])
except Exception, err:
self.warn(fromdocname, 'exception while evaluating only '
'directive expression: %s' % err, node.line)
node.replace_self(node.children)
else:
if ret:
node.replace_self(node.children)
else:
node.replace_self([])
# allow custom references to be resolved
builder.app.emit('doctree-resolved', doctree, fromdocname)

90
sphinx/util/tags.py Normal file
View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
"""
sphinx.util.tags
~~~~~~~~~~~~~~~~
:copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import warnings
# jinja2.sandbox imports the sets module on purpose
warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning,
module='jinja2.sandbox')
# (ab)use the Jinja parser for parsing our boolean expressions
from jinja2 import nodes
from jinja2.parser import Parser
from jinja2.environment import Environment
env = Environment()
class BooleanParser(Parser):
"""
Only allow condition exprs and/or/not operations.
"""
def parse_compare(self):
token = self.stream.current
if token.type == 'name':
if token.value in ('true', 'false', 'True', 'False'):
node = nodes.Const(token.value in ('true', 'True'),
lineno=token.lineno)
elif token.value in ('none', 'None'):
node = nodes.Const(None, lineno=token.lineno)
else:
node = nodes.Name(token.value, 'load', lineno=token.lineno)
self.stream.next()
elif token.type == 'lparen':
self.stream.next()
node = self.parse_expression()
self.stream.expect('rparen')
else:
self.fail("unexpected token '%s'" % (token,), token.lineno)
return node
class Tags(object):
def __init__(self, tags=None):
self.tags = dict.fromkeys(tags or [], True)
def has(self, tag):
return tag in self.tags
__contains__ = has
def __iter__(self):
return iter(self.tags)
def add(self, tag):
self.tags[tag] = True
def remove(self, tag):
self.tags.pop(tag, None)
def eval_condition(self, condition):
# exceptions are handled by the caller
parser = BooleanParser(env, condition, state='variable')
expr = parser.parse_expression()
if not parser.stream.eos:
raise ValueError('chunk after expression')
def eval_node(node):
if isinstance(node, nodes.CondExpr):
if eval_node(node.test):
return eval_node(node.expr1)
else:
return eval_node(node.expr2)
elif isinstance(node, nodes.And):
return eval_node(node.left) and eval_node(node.right)
elif isinstance(node, nodes.Or):
return eval_node(node.left) or eval_node(node.right)
elif isinstance(node, nodes.Not):
return not eval_node(node.node)
elif isinstance(node, nodes.Name):
return self.tags.get(node.name, False)
else:
raise ValueError('invalid node, check parsing')
return eval_node(expr)

View File

@@ -47,6 +47,8 @@ value_from_conf_py = 84
coverage_c_path = ['special/*.h']
coverage_c_regexes = {'cfunction': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
# modify tags from conf.py
tags.add('confpytag')
from sphinx import addnodes

View File

@@ -176,6 +176,26 @@ Invalid index markup...
Testing öäü...
Only directive
--------------
.. only:: html
In HTML.
.. only:: latex
In LaTeX.
.. only:: html or latex
In both.
.. only:: confpytag and (testtag or nonexisting_tag)
Always present, because set through conf.py/command line.
.. rubric:: Footnotes
.. [#] Like footnotes.

View File

@@ -83,6 +83,9 @@ HTML_XPATH = {
".//div[@id='label']": '',
".//span[@class='option']": '--help',
".//p": 'A global substitution.',
".//p": 'In HTML.',
".//p": 'In both.',
".//p": 'Always present',
},
'desc.html': {
".//dt[@id='mod.Cls.meth1']": '',
@@ -135,7 +138,7 @@ class NslessParser(ET.XMLParser):
return name
@with_app(buildername='html', warning=html_warnfile)
@with_app(buildername='html', warning=html_warnfile, tags=['testtag'])
def test_html(app):
app.builder.build_all()
html_warnings = html_warnfile.getvalue().replace(os.sep, '/')

View File

@@ -99,8 +99,9 @@ class TestApp(application.Sphinx):
def __init__(self, srcdir=None, confdir=None, outdir=None, doctreedir=None,
buildername='html', confoverrides=None,
status=None, warning=None,
freshenv=None, confname='conf.py', cleanenv=False):
status=None, warning=None, freshenv=None,
warningiserror=None, tags=None,
confname='conf.py', cleanenv=False):
application.CONFIG_FILENAME = confname
@@ -136,10 +137,12 @@ class TestApp(application.Sphinx):
warning = ListOutput('stderr')
if freshenv is None:
freshenv = False
if warningiserror is None:
warningiserror = False
application.Sphinx.__init__(self, srcdir, confdir, outdir, doctreedir,
buildername, confoverrides, status, warning,
freshenv)
freshenv, warningiserror, tags)
def cleanup(self, doctrees=False):
AutoDirective._registry.clear()