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.
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 COMMON_PARAM_NAMES = map(compat.fst, opcodes.OpCode.OP_PARAMS)
50 #: Namespace for evaluating expressions
51 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
54 # Constants documentation for man pages
55 CV_ECODES_DOC = "ecodes"
56 # We don't care about the leak of variables _, name and doc here.
57 # pylint: disable=W0621
58 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
59 DOCUMENTED_CONSTANTS = {
60 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
64 class OpcodeError(sphinx.errors.SphinxError):
65 category = "Opcode error"
68 def _SplitOption(text):
69 """Split simple option list.
72 @param text: Options, e.g. "foo, bar, baz"
75 return [i.strip(",").strip() for i in text.split()]
78 def _ParseAlias(text):
79 """Parse simple assignment option.
82 @param text: Assignments, e.g. "foo=bar, hello=world"
88 for part in _SplitOption(text):
90 raise OpcodeError("Invalid option format, missing equal sign")
92 (name, value) = part.split("=", 1)
94 result[name.strip()] = value.strip()
99 def _BuildOpcodeParams(op_id, include, exclude, alias):
100 """Build opcode parameter documentation.
103 @param op_id: Opcode ID
106 op_cls = opcodes.OP_MAPPING[op_id]
108 params_with_alias = \
109 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
110 for (name, default, test, doc) in op_cls.GetAllParams()],
113 for (rapi_name, name, default, test, doc) in params_with_alias:
114 # Hide common parameters if not explicitely included
115 if (name in COMMON_PARAM_NAMES and
116 (not include or name not in include)):
118 if exclude is not None and name in exclude:
120 if include is not None and name not in include:
123 has_default = default is not ht.NoDefault
124 has_test = not (test is None or test is ht.NoType)
127 buf.write("``%s``" % rapi_name)
128 if has_default or has_test:
131 buf.write("defaults to ``%s``" % default)
135 buf.write("must be ``%s``" % test)
140 for line in doc.splitlines():
144 def _BuildOpcodeResult(op_id):
145 """Build opcode result documentation.
148 @param op_id: Opcode ID
151 op_cls = opcodes.OP_MAPPING[op_id]
153 result_fn = getattr(op_cls, "OP_RESULT", None)
156 raise OpcodeError("Opcode '%s' has no result description" % op_id)
158 return "``%s``" % result_fn
161 class OpcodeParams(s_compat.Directive):
162 """Custom directive for opcode parameters.
164 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
168 required_arguments = 1
169 optional_arguments = 0
170 final_argument_whitespace = False
171 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
175 op_id = self.arguments[0]
176 include = self.options.get("include", None)
177 exclude = self.options.get("exclude", None)
178 alias = self.options.get("alias", {})
182 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
184 # Inject into state machine
185 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
186 convert_whitespace=1)
187 self.state_machine.insert_input(include_lines, path)
192 class OpcodeResult(s_compat.Directive):
193 """Custom directive for opcode result.
195 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
199 required_arguments = 1
200 optional_arguments = 0
201 final_argument_whitespace = False
204 op_id = self.arguments[0]
208 include_text = _BuildOpcodeResult(op_id)
210 # Inject into state machine
211 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
212 convert_whitespace=1)
213 self.state_machine.insert_input(include_lines, path)
218 def PythonEvalRole(role, rawtext, text, lineno, inliner,
219 options={}, content=[]):
220 """Custom role to evaluate Python expressions.
222 The expression's result is included as a literal.
225 # pylint: disable=W0102,W0613,W0142
226 # W0102: Dangerous default value as argument
227 # W0142: Used * or ** magic
228 # W0613: Unused argument
230 code = docutils.utils.unescape(text, restore_backslashes=True)
233 result = eval(code, EVAL_NS)
234 except Exception, err: # pylint: disable=W0703
235 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
237 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
239 node = docutils.nodes.literal("", unicode(result), **options)
244 class PythonAssert(s_compat.Directive):
245 """Custom directive for writing assertions.
247 The content must be a valid Python expression. If its result does not
248 evaluate to C{True}, the assertion fails.
252 required_arguments = 0
253 optional_arguments = 0
254 final_argument_whitespace = False
257 # Handle combinations of Sphinx and docutils not providing the wanted method
258 if hasattr(self, "assert_has_content"):
259 self.assert_has_content()
263 code = "\n".join(self.content)
266 result = eval(code, EVAL_NS)
267 except Exception, err:
268 raise self.error("Failed to evaluate %r: %s" % (code, err))
271 raise self.error("Assertion failed: %s" % (code, ))
276 def BuildQueryFields(fields):
277 """Build query fields documentation.
279 @type fields: dict (field name as key, field details as value)
282 defs = [(fdef.name, fdef.doc)
283 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
285 yield BuildValuesDoc(defs)
288 def BuildValuesDoc(values):
289 """Builds documentation for a list of values
291 @type values: list of tuples in the form (value, documentation)
294 for name, doc in values:
295 assert len(doc.splitlines()) == 1
296 yield "``%s``" % name
300 # TODO: Implement Sphinx directive for query fields
304 """Sphinx extension callback.
307 app.add_directive("opcode_params", OpcodeParams)
308 app.add_directive("opcode_result", OpcodeResult)
309 app.add_directive("pyassert", PythonAssert)
310 app.add_role("pyeval", PythonEvalRole)