4 # Copyright (C) 2011, 2012 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.
26 from cStringIO import StringIO
28 import docutils.statemachine
33 import sphinx.util.compat
35 s_compat = sphinx.util.compat
37 from ganeti import constants
38 from ganeti import compat
39 from ganeti import errors
40 from ganeti import utils
41 from ganeti import opcodes
43 from ganeti import rapi
44 from ganeti import luxi
46 import ganeti.rapi.rlib2 # pylint: disable=W0611
49 def _GetCommonParamNames():
50 """Builds a list of parameters common to all opcodes.
53 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
55 # The "depends" attribute should be listed
56 names.remove(opcodes.DEPEND_ATTR)
61 COMMON_PARAM_NAMES = _GetCommonParamNames()
63 #: Namespace for evaluating expressions
64 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
65 rlib2=rapi.rlib2, luxi=luxi)
67 # Constants documentation for man pages
68 CV_ECODES_DOC = "ecodes"
69 # We don't care about the leak of variables _, name and doc here.
70 # pylint: disable=W0621
71 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
72 DOCUMENTED_CONSTANTS = {
73 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
77 class OpcodeError(sphinx.errors.SphinxError):
78 category = "Opcode error"
81 def _SplitOption(text):
82 """Split simple option list.
85 @param text: Options, e.g. "foo, bar, baz"
88 return [i.strip(",").strip() for i in text.split()]
91 def _ParseAlias(text):
92 """Parse simple assignment option.
95 @param text: Assignments, e.g. "foo=bar, hello=world"
101 for part in _SplitOption(text):
103 raise OpcodeError("Invalid option format, missing equal sign")
105 (name, value) = part.split("=", 1)
107 result[name.strip()] = value.strip()
112 def _BuildOpcodeParams(op_id, include, exclude, alias):
113 """Build opcode parameter documentation.
116 @param op_id: Opcode ID
119 op_cls = opcodes.OP_MAPPING[op_id]
121 params_with_alias = \
122 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
123 for (name, default, test, doc) in op_cls.GetAllParams()],
126 for (rapi_name, name, default, test, doc) in params_with_alias:
127 # Hide common parameters if not explicitly included
128 if (name in COMMON_PARAM_NAMES and
129 (not include or name not in include)):
131 if exclude is not None and name in exclude:
133 if include is not None and name not in include:
136 has_default = default is not ht.NoDefault
137 has_test = not (test is None or test is ht.NoType)
140 buf.write("``%s``" % rapi_name)
141 if has_default or has_test:
144 buf.write("defaults to ``%s``" % default)
148 buf.write("must be ``%s``" % test)
153 for line in doc.splitlines():
157 def _BuildOpcodeResult(op_id):
158 """Build opcode result documentation.
161 @param op_id: Opcode ID
164 op_cls = opcodes.OP_MAPPING[op_id]
166 result_fn = getattr(op_cls, "OP_RESULT", None)
169 raise OpcodeError("Opcode '%s' has no result description" % op_id)
171 return "``%s``" % result_fn
174 class OpcodeParams(s_compat.Directive):
175 """Custom directive for opcode parameters.
177 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
181 required_arguments = 1
182 optional_arguments = 0
183 final_argument_whitespace = False
184 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
188 op_id = self.arguments[0]
189 include = self.options.get("include", None)
190 exclude = self.options.get("exclude", None)
191 alias = self.options.get("alias", {})
195 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
197 # Inject into state machine
198 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
199 convert_whitespace=1)
200 self.state_machine.insert_input(include_lines, path)
205 class OpcodeResult(s_compat.Directive):
206 """Custom directive for opcode result.
208 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
212 required_arguments = 1
213 optional_arguments = 0
214 final_argument_whitespace = False
217 op_id = self.arguments[0]
221 include_text = _BuildOpcodeResult(op_id)
223 # Inject into state machine
224 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
225 convert_whitespace=1)
226 self.state_machine.insert_input(include_lines, path)
231 def PythonEvalRole(role, rawtext, text, lineno, inliner,
232 options={}, content=[]):
233 """Custom role to evaluate Python expressions.
235 The expression's result is included as a literal.
238 # pylint: disable=W0102,W0613,W0142
239 # W0102: Dangerous default value as argument
240 # W0142: Used * or ** magic
241 # W0613: Unused argument
243 code = docutils.utils.unescape(text, restore_backslashes=True)
246 result = eval(code, EVAL_NS)
247 except Exception, err: # pylint: disable=W0703
248 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
250 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
252 node = docutils.nodes.literal("", unicode(result), **options)
257 class PythonAssert(s_compat.Directive):
258 """Custom directive for writing assertions.
260 The content must be a valid Python expression. If its result does not
261 evaluate to C{True}, the assertion fails.
265 required_arguments = 0
266 optional_arguments = 0
267 final_argument_whitespace = False
270 # Handle combinations of Sphinx and docutils not providing the wanted method
271 if hasattr(self, "assert_has_content"):
272 self.assert_has_content()
276 code = "\n".join(self.content)
279 result = eval(code, EVAL_NS)
280 except Exception, err:
281 raise self.error("Failed to evaluate %r: %s" % (code, err))
284 raise self.error("Assertion failed: %s" % (code, ))
289 def BuildQueryFields(fields):
290 """Build query fields documentation.
292 @type fields: dict (field name as key, field details as value)
295 defs = [(fdef.name, fdef.doc)
296 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
298 return BuildValuesDoc(defs)
301 def BuildValuesDoc(values):
302 """Builds documentation for a list of values
304 @type values: list of tuples in the form (value, documentation)
307 for name, doc in values:
308 assert len(doc.splitlines()) == 1
309 yield "``%s``" % name
313 # TODO: Implement Sphinx directive for query fields
317 """Sphinx extension callback.
320 app.add_directive("opcode_params", OpcodeParams)
321 app.add_directive("opcode_result", OpcodeResult)
322 app.add_directive("pyassert", PythonAssert)
323 app.add_role("pyeval", PythonEvalRole)