Merge branch 'stable-2.6' into devel-2.6
[ganeti-local] / lib / build / sphinx_ext.py
1 #
2 #
3
4 # Copyright (C) 2011, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Sphinx extension for building opcode documentation.
23
24 """
25
26 from cStringIO import StringIO
27
28 import docutils.statemachine
29 import docutils.nodes
30 import docutils.utils
31
32 import sphinx.errors
33 import sphinx.util.compat
34
35 s_compat = sphinx.util.compat
36
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
42 from ganeti import ht
43 from ganeti import rapi
44 from ganeti import luxi
45
46 import ganeti.rapi.rlib2 # pylint: disable=W0611
47
48
49 def _GetCommonParamNames():
50   """Builds a list of parameters common to all opcodes.
51
52   """
53   names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
54
55   # The "depends" attribute should be listed
56   names.remove(opcodes.DEPEND_ATTR)
57
58   return names
59
60
61 COMMON_PARAM_NAMES = _GetCommonParamNames()
62
63 #: Namespace for evaluating expressions
64 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
65                rlib2=rapi.rlib2, luxi=luxi)
66
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,
74   }
75
76
77 class OpcodeError(sphinx.errors.SphinxError):
78   category = "Opcode error"
79
80
81 def _SplitOption(text):
82   """Split simple option list.
83
84   @type text: string
85   @param text: Options, e.g. "foo, bar, baz"
86
87   """
88   return [i.strip(",").strip() for i in text.split()]
89
90
91 def _ParseAlias(text):
92   """Parse simple assignment option.
93
94   @type text: string
95   @param text: Assignments, e.g. "foo=bar, hello=world"
96   @rtype: dict
97
98   """
99   result = {}
100
101   for part in _SplitOption(text):
102     if "=" not in part:
103       raise OpcodeError("Invalid option format, missing equal sign")
104
105     (name, value) = part.split("=", 1)
106
107     result[name.strip()] = value.strip()
108
109   return result
110
111
112 def _BuildOpcodeParams(op_id, include, exclude, alias):
113   """Build opcode parameter documentation.
114
115   @type op_id: string
116   @param op_id: Opcode ID
117
118   """
119   op_cls = opcodes.OP_MAPPING[op_id]
120
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()],
124                    key=compat.fst)
125
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)):
130       continue
131     if exclude is not None and name in exclude:
132       continue
133     if include is not None and name not in include:
134       continue
135
136     has_default = default is not ht.NoDefault
137     has_test = not (test is None or test is ht.NoType)
138
139     buf = StringIO()
140     buf.write("``%s``" % rapi_name)
141     if has_default or has_test:
142       buf.write(" (")
143       if has_default:
144         buf.write("defaults to ``%s``" % default)
145         if has_test:
146           buf.write(", ")
147       if has_test:
148         buf.write("must be ``%s``" % test)
149       buf.write(")")
150     yield buf.getvalue()
151
152     # Add text
153     for line in doc.splitlines():
154       yield "  %s" % line
155
156
157 def _BuildOpcodeResult(op_id):
158   """Build opcode result documentation.
159
160   @type op_id: string
161   @param op_id: Opcode ID
162
163   """
164   op_cls = opcodes.OP_MAPPING[op_id]
165
166   result_fn = getattr(op_cls, "OP_RESULT", None)
167
168   if not result_fn:
169     raise OpcodeError("Opcode '%s' has no result description" % op_id)
170
171   return "``%s``" % result_fn
172
173
174 class OpcodeParams(s_compat.Directive):
175   """Custom directive for opcode parameters.
176
177   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
178
179   """
180   has_content = False
181   required_arguments = 1
182   optional_arguments = 0
183   final_argument_whitespace = False
184   option_spec = dict(include=_SplitOption, exclude=_SplitOption,
185                      alias=_ParseAlias)
186
187   def run(self):
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", {})
192
193     tab_width = 2
194     path = op_id
195     include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
196
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)
201
202     return []
203
204
205 class OpcodeResult(s_compat.Directive):
206   """Custom directive for opcode result.
207
208   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
209
210   """
211   has_content = False
212   required_arguments = 1
213   optional_arguments = 0
214   final_argument_whitespace = False
215
216   def run(self):
217     op_id = self.arguments[0]
218
219     tab_width = 2
220     path = op_id
221     include_text = _BuildOpcodeResult(op_id)
222
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)
227
228     return []
229
230
231 def PythonEvalRole(role, rawtext, text, lineno, inliner,
232                    options={}, content=[]):
233   """Custom role to evaluate Python expressions.
234
235   The expression's result is included as a literal.
236
237   """
238   # pylint: disable=W0102,W0613,W0142
239   # W0102: Dangerous default value as argument
240   # W0142: Used * or ** magic
241   # W0613: Unused argument
242
243   code = docutils.utils.unescape(text, restore_backslashes=True)
244
245   try:
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),
249                                  line=lineno)
250     return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
251
252   node = docutils.nodes.literal("", unicode(result), **options)
253
254   return ([node], [])
255
256
257 class PythonAssert(s_compat.Directive):
258   """Custom directive for writing assertions.
259
260   The content must be a valid Python expression. If its result does not
261   evaluate to C{True}, the assertion fails.
262
263   """
264   has_content = True
265   required_arguments = 0
266   optional_arguments = 0
267   final_argument_whitespace = False
268
269   def run(self):
270     # Handle combinations of Sphinx and docutils not providing the wanted method
271     if hasattr(self, "assert_has_content"):
272       self.assert_has_content()
273     else:
274       assert self.content
275
276     code = "\n".join(self.content)
277
278     try:
279       result = eval(code, EVAL_NS)
280     except Exception, err:
281       raise self.error("Failed to evaluate %r: %s" % (code, err))
282
283     if not result:
284       raise self.error("Assertion failed: %s" % (code, ))
285
286     return []
287
288
289 def BuildQueryFields(fields):
290   """Build query fields documentation.
291
292   @type fields: dict (field name as key, field details as value)
293
294   """
295   defs = [(fdef.name, fdef.doc)
296            for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
297                                                       key=compat.fst)]
298   return BuildValuesDoc(defs)
299
300
301 def BuildValuesDoc(values):
302   """Builds documentation for a list of values
303
304   @type values: list of tuples in the form (value, documentation)
305
306   """
307   for name, doc in values:
308     assert len(doc.splitlines()) == 1
309     yield "``%s``" % name
310     yield "  %s" % doc
311
312
313 # TODO: Implement Sphinx directive for query fields
314
315
316 def setup(app):
317   """Sphinx extension callback.
318
319   """
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)