4 # Copyright (C) 2011 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22 """Sphinx extension for building opcode documentation.
27 from cStringIO import StringIO
29 import docutils.statemachine
34 import sphinx.util.compat
36 from ganeti import constants
37 from ganeti import compat
38 from ganeti import errors
39 from ganeti import utils
40 from ganeti import opcodes
42 from ganeti import rapi
44 import ganeti.rapi.rlib2 # pylint: disable-msg=W0611
47 COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
49 #: Namespace for evaluating expressions
50 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
54 class OpcodeError(sphinx.errors.SphinxError):
55 category = "Opcode error"
58 def _SplitOption(text):
59 """Split simple option list.
62 @param text: Options, e.g. "foo, bar, baz"
65 return [i.strip(",").strip() for i in text.split()]
68 def _ParseAlias(text):
69 """Parse simple assignment option.
72 @param text: Assignments, e.g. "foo=bar, hello=world"
78 for part in _SplitOption(text):
80 raise OpcodeError("Invalid option format, missing equal sign")
82 (name, value) = part.split("=", 1)
84 result[name.strip()] = value.strip()
89 def _BuildOpcodeParams(op_id, include, exclude, alias):
90 """Build opcode parameter documentation.
93 @param op_id: Opcode ID
96 op_cls = opcodes.OP_MAPPING[op_id]
99 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
100 for (name, default, test, doc) in op_cls.GetAllParams()],
101 key=operator.itemgetter(0))
103 for (rapi_name, name, default, test, doc) in params_with_alias:
104 # Hide common parameters if not explicitely included
105 if (name in COMMON_PARAM_NAMES and
106 (not include or name not in include)):
108 if exclude is not None and name in exclude:
110 if include is not None and name not in include:
113 has_default = default is not ht.NoDefault
114 has_test = not (test is None or test is ht.NoType)
117 buf.write("``%s``" % rapi_name)
118 if has_default or has_test:
121 buf.write("defaults to ``%s``" % default)
125 buf.write("must be ``%s``" % test)
130 for line in doc.splitlines():
134 class OpcodeParams(sphinx.util.compat.Directive):
135 """Custom directive for opcode parameters.
137 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
141 required_arguments = 1
142 optional_arguments = 0
143 final_argument_whitespace = False
144 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
148 op_id = self.arguments[0]
149 include = self.options.get("include", None)
150 exclude = self.options.get("exclude", None)
151 alias = self.options.get("alias", {})
155 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
157 # Inject into state machine
158 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
159 convert_whitespace=1)
160 self.state_machine.insert_input(include_lines, path)
165 def PythonEvalRole(role, rawtext, text, lineno, inliner,
166 options={}, content=[]):
167 """Custom role to evaluate Python expressions.
169 The expression's result is included as a literal.
172 # pylint: disable-msg=W0102,W0613,W0142
173 # W0102: Dangerous default value as argument
174 # W0142: Used * or ** magic
175 # W0613: Unused argument
177 code = docutils.utils.unescape(text, restore_backslashes=True)
180 result = eval(code, EVAL_NS)
181 except Exception, err: # pylint: disable-msg=W0703
182 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
184 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
186 node = docutils.nodes.literal("", unicode(result), **options)
191 class PythonAssert(sphinx.util.compat.Directive):
192 """Custom directive for writing assertions.
194 The content must be a valid Python expression. If its result does not
195 evaluate to C{True}, the assertion fails.
199 required_arguments = 0
200 optional_arguments = 0
201 final_argument_whitespace = False
204 # Handle combinations of Sphinx and docutils not providing the wanted method
205 if hasattr(self, "assert_has_content"):
206 self.assert_has_content()
210 code = "\n".join(self.content)
213 result = eval(code, EVAL_NS)
214 except Exception, err:
215 raise self.error("Failed to evaluate %r: %s" % (code, err))
218 raise self.error("Assertion failed: %s" % (code, ))
223 def BuildQueryFields(fields):
224 """Build query fields documentation.
226 @type fields: dict (field name as key, field details as value)
229 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
230 key=operator.itemgetter(0)):
231 assert len(fdef.doc.splitlines()) == 1
232 yield "``%s``" % fdef.name
233 yield " %s" % fdef.doc
236 # TODO: Implement Sphinx directive for query fields
240 """Sphinx extension callback.
243 app.add_directive("opcode_params", OpcodeParams)
244 app.add_directive("pyassert", PythonAssert)
245 app.add_role("pyeval", PythonEvalRole)