-
Michael Hanselmann authored
Assert that the documented fields are equal to those in the actual object. Signed-off-by:
Michael Hanselmann <hansmi@google.com> Reviewed-by:
Guido Trotter <ultrotter@google.com>
46ab58d4
sphinx_ext.py 11.70 KiB
#
#
# Copyright (C) 2011, 2012 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""Sphinx extension for building opcode documentation.
"""
import re
from cStringIO import StringIO
import docutils.statemachine
import docutils.nodes
import docutils.utils
import docutils.parsers.rst
import sphinx.errors
import sphinx.util.compat
import sphinx.roles
import sphinx.addnodes
s_compat = sphinx.util.compat
try:
# Access to a protected member of a client class
# pylint: disable=W0212
orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
except (AttributeError, ValueError, KeyError), err:
# Normally the "manpage" role is registered by sphinx/roles.py
raise Exception("Can't find reST role named 'manpage': %s" % err)
from ganeti import constants
from ganeti import compat
from ganeti import errors
from ganeti import utils
from ganeti import opcodes
from ganeti import ht
from ganeti import rapi
from ganeti import luxi
from ganeti import objects
from ganeti import _autoconf
import ganeti.rapi.rlib2 # pylint: disable=W0611
#: Regular expression for man page names
_MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
class ReSTError(Exception):
"""Custom class for generating errors in Sphinx.
"""
def _GetCommonParamNames():
"""Builds a list of parameters common to all opcodes.
"""
names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
# The "depends" attribute should be listed
names.remove(opcodes.DEPEND_ATTR)
return names
COMMON_PARAM_NAMES = _GetCommonParamNames()
#: Namespace for evaluating expressions
EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects)
# Constants documentation for man pages
CV_ECODES_DOC = "ecodes"
# We don't care about the leak of variables _, name and doc here.
# pylint: disable=W0621
CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
DOCUMENTED_CONSTANTS = {
CV_ECODES_DOC: CV_ECODES_DOC_LIST,
}
class OpcodeError(sphinx.errors.SphinxError):
category = "Opcode error"
def _SplitOption(text):
"""Split simple option list.
@type text: string
@param text: Options, e.g. "foo, bar, baz"
"""
return [i.strip(",").strip() for i in text.split()]
def _ParseAlias(text):
"""Parse simple assignment option.
@type text: string
@param text: Assignments, e.g. "foo=bar, hello=world"
@rtype: dict
"""
result = {}
for part in _SplitOption(text):
if "=" not in part:
raise OpcodeError("Invalid option format, missing equal sign")
(name, value) = part.split("=", 1)
result[name.strip()] = value.strip()
return result
def _BuildOpcodeParams(op_id, include, exclude, alias):
"""Build opcode parameter documentation.
@type op_id: string
@param op_id: Opcode ID
"""
op_cls = opcodes.OP_MAPPING[op_id]
params_with_alias = \
utils.NiceSort([(alias.get(name, name), name, default, test, doc)
for (name, default, test, doc) in op_cls.GetAllParams()],
key=compat.fst)
for (rapi_name, name, default, test, doc) in params_with_alias:
# Hide common parameters if not explicitly included
if (name in COMMON_PARAM_NAMES and
(not include or name not in include)):
continue
if exclude is not None and name in exclude:
continue
if include is not None and name not in include:
continue
has_default = default is not ht.NoDefault
has_test = not (test is None or test is ht.NoType)
buf = StringIO()
buf.write("``%s``" % rapi_name)
if has_default or has_test:
buf.write(" (")
if has_default:
buf.write("defaults to ``%s``" % default)
if has_test:
buf.write(", ")
if has_test:
buf.write("must be ``%s``" % test)
buf.write(")")
yield buf.getvalue()
# Add text
for line in doc.splitlines():
yield " %s" % line
def _BuildOpcodeResult(op_id):
"""Build opcode result documentation.
@type op_id: string
@param op_id: Opcode ID
"""
op_cls = opcodes.OP_MAPPING[op_id]
result_fn = getattr(op_cls, "OP_RESULT", None)
if not result_fn:
raise OpcodeError("Opcode '%s' has no result description" % op_id)
return "``%s``" % result_fn
class OpcodeParams(s_compat.Directive):
"""Custom directive for opcode parameters.
See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
option_spec = dict(include=_SplitOption, exclude=_SplitOption,
alias=_ParseAlias)
def run(self):
op_id = self.arguments[0]
include = self.options.get("include", None)
exclude = self.options.get("exclude", None)
alias = self.options.get("alias", {})
tab_width = 2
path = op_id
include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
# Inject into state machine
include_lines = docutils.statemachine.string2lines(include_text, tab_width,
convert_whitespace=1)
self.state_machine.insert_input(include_lines, path)
return []
class OpcodeResult(s_compat.Directive):
"""Custom directive for opcode result.
See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
"""
has_content = False
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = False
def run(self):
op_id = self.arguments[0]
tab_width = 2
path = op_id
include_text = _BuildOpcodeResult(op_id)
# Inject into state machine
include_lines = docutils.statemachine.string2lines(include_text, tab_width,
convert_whitespace=1)
self.state_machine.insert_input(include_lines, path)
return []
def PythonEvalRole(role, rawtext, text, lineno, inliner,
options={}, content=[]):
"""Custom role to evaluate Python expressions.
The expression's result is included as a literal.
"""
# pylint: disable=W0102,W0613,W0142
# W0102: Dangerous default value as argument
# W0142: Used * or ** magic
# W0613: Unused argument
code = docutils.utils.unescape(text, restore_backslashes=True)
try:
result = eval(code, EVAL_NS)
except Exception, err: # pylint: disable=W0703
msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
line=lineno)
return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
node = docutils.nodes.literal("", unicode(result), **options)
return ([node], [])
class PythonAssert(s_compat.Directive):
"""Custom directive for writing assertions.
The content must be a valid Python expression. If its result does not
evaluate to C{True}, the assertion fails.
"""
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = False
def run(self):
# Handle combinations of Sphinx and docutils not providing the wanted method
if hasattr(self, "assert_has_content"):
self.assert_has_content()
else:
assert self.content
code = "\n".join(self.content)
try:
result = eval(code, EVAL_NS)
except Exception, err:
raise self.error("Failed to evaluate %r: %s" % (code, err))
if not result:
raise self.error("Assertion failed: %s" % (code, ))
return []
def BuildQueryFields(fields):
"""Build query fields documentation.
@type fields: dict (field name as key, field details as value)
"""
defs = [(fdef.name, fdef.doc)
for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
key=compat.fst)]
return BuildValuesDoc(defs)
def BuildValuesDoc(values):
"""Builds documentation for a list of values
@type values: list of tuples in the form (value, documentation)
"""
for name, doc in values:
assert len(doc.splitlines()) == 1
yield "``%s``" % name
yield " %s" % doc
def _ManPageNodeClass(*args, **kwargs):
"""Generates a pending XRef like a ":doc:`...`" reference.
"""
# Type for sphinx/environment.py:BuildEnvironment.resolve_references
kwargs["reftype"] = "doc"
# Force custom title
kwargs["refexplicit"] = True
return sphinx.addnodes.pending_xref(*args, **kwargs)
class _ManPageXRefRole(sphinx.roles.XRefRole):
def __init__(self):
"""Initializes this class.
"""
sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
warn_dangling=True)
assert not hasattr(self, "converted"), \
"Sphinx base class gained an attribute named 'converted'"
self.converted = None
def process_link(self, env, refnode, has_explicit_title, title, target):
"""Specialization for man page links.
"""
if has_explicit_title:
raise ReSTError("Setting explicit title is not allowed for man pages")
# Check format and extract name and section
m = _MAN_RE.match(title)
if not m:
raise ReSTError("Man page reference '%s' does not match regular"
" expression '%s'" % (title, _MAN_RE.pattern))
name = m.group("name")
section = int(m.group("section"))
wanted_section = _autoconf.MAN_PAGES.get(name, None)
if not (wanted_section is None or wanted_section == section):
raise ReSTError("Referenced man page '%s' has section number %s, but the"
" reference uses section %s" %
(name, wanted_section, section))
self.converted = bool(wanted_section is not None and
env.app.config.enable_manpages)
if self.converted:
# Create link to known man page
return (title, "man-%s" % name)
else:
# No changes
return (title, target)
def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
options={}, content=[]):
"""Custom role for man page references.
Converts man pages to links if enabled during the build.
"""
xref = _ManPageXRefRole()
assert ht.TNone(xref.converted)
# Check if it's a known man page
try:
result = xref(typ, rawtext, text, lineno, inliner,
options=options, content=content)
except ReSTError, err:
msg = inliner.reporter.error(str(err), line=lineno)
return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
assert ht.TBool(xref.converted)
# Return if the conversion was successful (i.e. the man page was known and
# conversion was enabled)
if xref.converted:
return result
# Fallback if man page links are disabled or an unknown page is referenced
return orig_manpage_role(typ, rawtext, text, lineno, inliner,
options=options, content=content)
def setup(app):
"""Sphinx extension callback.
"""
# TODO: Implement Sphinx directive for query fields
app.add_directive("opcode_params", OpcodeParams)
app.add_directive("opcode_result", OpcodeResult)
app.add_directive("pyassert", PythonAssert)
app.add_role("pyeval", PythonEvalRole)
app.add_config_value("enable_manpages", False, True)
app.add_role("manpage", _ManPageRole)