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
45 import ganeti.rapi.rlib2 # pylint: disable=W0611
48 def _GetCommonParamNames():
49 """Builds a list of parameters common to all opcodes.
52 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
54 # The "depends" attribute should be listed
55 names.remove(opcodes.DEPEND_ATTR)
60 COMMON_PARAM_NAMES = _GetCommonParamNames()
62 #: Namespace for evaluating expressions
63 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
66 # Constants documentation for man pages
67 CV_ECODES_DOC = "ecodes"
68 # We don't care about the leak of variables _, name and doc here.
69 # pylint: disable=W0621
70 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
71 DOCUMENTED_CONSTANTS = {
72 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
76 class OpcodeError(sphinx.errors.SphinxError):
77 category = "Opcode error"
80 def _SplitOption(text):
81 """Split simple option list.
84 @param text: Options, e.g. "foo, bar, baz"
87 return [i.strip(",").strip() for i in text.split()]
90 def _ParseAlias(text):
91 """Parse simple assignment option.
94 @param text: Assignments, e.g. "foo=bar, hello=world"
100 for part in _SplitOption(text):
102 raise OpcodeError("Invalid option format, missing equal sign")
104 (name, value) = part.split("=", 1)
106 result[name.strip()] = value.strip()
111 def _BuildOpcodeParams(op_id, include, exclude, alias):
112 """Build opcode parameter documentation.
115 @param op_id: Opcode ID
118 op_cls = opcodes.OP_MAPPING[op_id]
120 params_with_alias = \
121 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
122 for (name, default, test, doc) in op_cls.GetAllParams()],
125 for (rapi_name, name, default, test, doc) in params_with_alias:
126 # Hide common parameters if not explicitely included
127 if (name in COMMON_PARAM_NAMES and
128 (not include or name not in include)):
130 if exclude is not None and name in exclude:
132 if include is not None and name not in include:
135 has_default = default is not ht.NoDefault
136 has_test = not (test is None or test is ht.NoType)
139 buf.write("``%s``" % rapi_name)
140 if has_default or has_test:
143 buf.write("defaults to ``%s``" % default)
147 buf.write("must be ``%s``" % test)
152 for line in doc.splitlines():
156 def _BuildOpcodeResult(op_id):
157 """Build opcode result documentation.
160 @param op_id: Opcode ID
163 op_cls = opcodes.OP_MAPPING[op_id]
165 result_fn = getattr(op_cls, "OP_RESULT", None)
168 raise OpcodeError("Opcode '%s' has no result description" % op_id)
170 return "``%s``" % result_fn
173 class OpcodeParams(s_compat.Directive):
174 """Custom directive for opcode parameters.
176 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
180 required_arguments = 1
181 optional_arguments = 0
182 final_argument_whitespace = False
183 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
187 op_id = self.arguments[0]
188 include = self.options.get("include", None)
189 exclude = self.options.get("exclude", None)
190 alias = self.options.get("alias", {})
194 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
196 # Inject into state machine
197 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
198 convert_whitespace=1)
199 self.state_machine.insert_input(include_lines, path)
204 class OpcodeResult(s_compat.Directive):
205 """Custom directive for opcode result.
207 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
211 required_arguments = 1
212 optional_arguments = 0
213 final_argument_whitespace = False
216 op_id = self.arguments[0]
220 include_text = _BuildOpcodeResult(op_id)
222 # Inject into state machine
223 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
224 convert_whitespace=1)
225 self.state_machine.insert_input(include_lines, path)
230 def PythonEvalRole(role, rawtext, text, lineno, inliner,
231 options={}, content=[]):
232 """Custom role to evaluate Python expressions.
234 The expression's result is included as a literal.
237 # pylint: disable=W0102,W0613,W0142
238 # W0102: Dangerous default value as argument
239 # W0142: Used * or ** magic
240 # W0613: Unused argument
242 code = docutils.utils.unescape(text, restore_backslashes=True)
245 result = eval(code, EVAL_NS)
246 except Exception, err: # pylint: disable=W0703
247 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
249 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
251 node = docutils.nodes.literal("", unicode(result), **options)
256 class PythonAssert(s_compat.Directive):
257 """Custom directive for writing assertions.
259 The content must be a valid Python expression. If its result does not
260 evaluate to C{True}, the assertion fails.
264 required_arguments = 0
265 optional_arguments = 0
266 final_argument_whitespace = False
269 # Handle combinations of Sphinx and docutils not providing the wanted method
270 if hasattr(self, "assert_has_content"):
271 self.assert_has_content()
275 code = "\n".join(self.content)
278 result = eval(code, EVAL_NS)
279 except Exception, err:
280 raise self.error("Failed to evaluate %r: %s" % (code, err))
283 raise self.error("Assertion failed: %s" % (code, ))
288 def BuildQueryFields(fields):
289 """Build query fields documentation.
291 @type fields: dict (field name as key, field details as value)
294 defs = [(fdef.name, fdef.doc)
295 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
297 return BuildValuesDoc(defs)
300 def BuildValuesDoc(values):
301 """Builds documentation for a list of values
303 @type values: list of tuples in the form (value, documentation)
306 for name, doc in values:
307 assert len(doc.splitlines()) == 1
308 yield "``%s``" % name
312 # TODO: Implement Sphinx directive for query fields
316 """Sphinx extension callback.
319 app.add_directive("opcode_params", OpcodeParams)
320 app.add_directive("opcode_result", OpcodeResult)
321 app.add_directive("pyassert", PythonAssert)
322 app.add_role("pyeval", PythonEvalRole)