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
44 COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
46 #: Namespace for evaluating expressions
47 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors)
50 class OpcodeError(sphinx.errors.SphinxError):
51 category = "Opcode error"
54 def _SplitOption(text):
55 """Split simple option list.
58 @param text: Options, e.g. "foo, bar, baz"
61 return [i.strip(",").strip() for i in text.split()]
64 def _ParseAlias(text):
65 """Parse simple assignment option.
68 @param text: Assignments, e.g. "foo=bar, hello=world"
74 for part in _SplitOption(text):
76 raise OpcodeError("Invalid option format, missing equal sign")
78 (name, value) = part.split("=", 1)
80 result[name.strip()] = value.strip()
85 def _BuildOpcodeParams(op_id, include, exclude, alias):
86 """Build opcode parameter documentation.
89 @param op_id: Opcode ID
92 op_cls = opcodes.OP_MAPPING[op_id]
95 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
96 for (name, default, test, doc) in op_cls.GetAllParams()],
97 key=operator.itemgetter(0))
99 for (rapi_name, name, default, test, doc) in params_with_alias:
100 # Hide common parameters if not explicitely included
101 if (name in COMMON_PARAM_NAMES and
102 (not include or name not in include)):
104 if exclude is not None and name in exclude:
106 if include is not None and name not in include:
109 has_default = default is not ht.NoDefault
110 has_test = not (test is None or test is ht.NoType)
113 buf.write("``%s``" % rapi_name)
114 if has_default or has_test:
117 buf.write("defaults to ``%s``" % default)
121 buf.write("must be ``%s``" % test)
126 for line in doc.splitlines():
130 class OpcodeParams(sphinx.util.compat.Directive):
131 """Custom directive for opcode parameters.
133 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
137 required_arguments = 1
138 optional_arguments = 0
139 final_argument_whitespace = False
140 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
144 op_id = self.arguments[0]
145 include = self.options.get("include", None)
146 exclude = self.options.get("exclude", None)
147 alias = self.options.get("alias", {})
151 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
153 # Inject into state machine
154 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
155 convert_whitespace=1)
156 self.state_machine.insert_input(include_lines, path)
161 def PythonEvalRole(role, rawtext, text, lineno, inliner,
162 options={}, content=[]):
163 """Custom role to evaluate Python expressions.
165 The expression's result is included as a literal.
168 # pylint: disable-msg=W0102,W0613,W0142
169 # W0102: Dangerous default value as argument
170 # W0142: Used * or ** magic
171 # W0613: Unused argument
173 code = docutils.utils.unescape(text, restore_backslashes=True)
176 result = eval(code, EVAL_NS)
177 except Exception, err: # pylint: disable-msg=W0703
178 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
180 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
182 node = docutils.nodes.literal("", unicode(result), **options)
187 class PythonAssert(sphinx.util.compat.Directive):
188 """Custom directive for writing assertions.
190 The content must be a valid Python expression. If its result does not
191 evaluate to C{True}, the assertion fails.
195 required_arguments = 0
196 optional_arguments = 0
197 final_argument_whitespace = False
200 # Handle combinations of Sphinx and docutils not providing the wanted method
201 if hasattr(self, "assert_has_content"):
202 self.assert_has_content()
206 code = "\n".join(self.content)
209 result = eval(code, EVAL_NS)
210 except Exception, err:
211 raise self.error("Failed to evaluate %r: %s" % (code, err))
214 raise self.error("Assertion failed: %s" % (code, ))
219 def BuildQueryFields(fields):
220 """Build query fields documentation.
222 @type fields: dict (field name as key, field details as value)
225 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
226 key=operator.itemgetter(0)):
227 assert len(fdef.doc.splitlines()) == 1
228 yield "``%s``" % fdef.name
229 yield " %s" % fdef.doc
232 # TODO: Implement Sphinx directive for query fields
236 """Sphinx extension callback.
239 app.add_directive("opcode_params", OpcodeParams)
240 app.add_directive("pyassert", PythonAssert)
241 app.add_role("pyeval", PythonEvalRole)