Rename watcher's constant for instance status file
[ganeti-local] / lib / build / sphinx_ext.py
1 #
2 #
3
4 # Copyright (C) 2011 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 import operator
27 from cStringIO import StringIO
28
29 import docutils.statemachine
30 import docutils.nodes
31 import docutils.utils
32
33 import sphinx.errors
34 import sphinx.util.compat
35
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
41 from ganeti import ht
42 from ganeti import rapi
43
44 import ganeti.rapi.rlib2 # pylint: disable-msg=W0611
45
46
47 COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
48
49 #: Namespace for evaluating expressions
50 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
51                rlib2=rapi.rlib2)
52
53
54 class OpcodeError(sphinx.errors.SphinxError):
55   category = "Opcode error"
56
57
58 def _SplitOption(text):
59   """Split simple option list.
60
61   @type text: string
62   @param text: Options, e.g. "foo, bar, baz"
63
64   """
65   return [i.strip(",").strip() for i in text.split()]
66
67
68 def _ParseAlias(text):
69   """Parse simple assignment option.
70
71   @type text: string
72   @param text: Assignments, e.g. "foo=bar, hello=world"
73   @rtype: dict
74
75   """
76   result = {}
77
78   for part in _SplitOption(text):
79     if "=" not in part:
80       raise OpcodeError("Invalid option format, missing equal sign")
81
82     (name, value) = part.split("=", 1)
83
84     result[name.strip()] = value.strip()
85
86   return result
87
88
89 def _BuildOpcodeParams(op_id, include, exclude, alias):
90   """Build opcode parameter documentation.
91
92   @type op_id: string
93   @param op_id: Opcode ID
94
95   """
96   op_cls = opcodes.OP_MAPPING[op_id]
97
98   params_with_alias = \
99     utils.NiceSort([(alias.get(name, name), name, default, test, doc)
100                     for (name, default, test, doc) in op_cls.GetAllParams()],
101                    key=operator.itemgetter(0))
102
103   for (rapi_name, name, default, test, doc) in params_with_alias:
104     # Hide common parameters if not explicitely included
105     if (name in COMMON_PARAM_NAMES and
106         (not include or name not in include)):
107       continue
108     if exclude is not None and name in exclude:
109       continue
110     if include is not None and name not in include:
111       continue
112
113     has_default = default is not ht.NoDefault
114     has_test = not (test is None or test is ht.NoType)
115
116     buf = StringIO()
117     buf.write("``%s``" % rapi_name)
118     if has_default or has_test:
119       buf.write(" (")
120       if has_default:
121         buf.write("defaults to ``%s``" % default)
122         if has_test:
123           buf.write(", ")
124       if has_test:
125         buf.write("must be ``%s``" % test)
126       buf.write(")")
127     yield buf.getvalue()
128
129     # Add text
130     for line in doc.splitlines():
131       yield "  %s" % line
132
133
134 class OpcodeParams(sphinx.util.compat.Directive):
135   """Custom directive for opcode parameters.
136
137   See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
138
139   """
140   has_content = False
141   required_arguments = 1
142   optional_arguments = 0
143   final_argument_whitespace = False
144   option_spec = dict(include=_SplitOption, exclude=_SplitOption,
145                      alias=_ParseAlias)
146
147   def run(self):
148     op_id = self.arguments[0]
149     include = self.options.get("include", None)
150     exclude = self.options.get("exclude", None)
151     alias = self.options.get("alias", {})
152
153     tab_width = 2
154     path = op_id
155     include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
156
157     # Inject into state machine
158     include_lines = docutils.statemachine.string2lines(include_text, tab_width,
159                                                        convert_whitespace=1)
160     self.state_machine.insert_input(include_lines, path)
161
162     return []
163
164
165 def PythonEvalRole(role, rawtext, text, lineno, inliner,
166                    options={}, content=[]):
167   """Custom role to evaluate Python expressions.
168
169   The expression's result is included as a literal.
170
171   """
172   # pylint: disable-msg=W0102,W0613,W0142
173   # W0102: Dangerous default value as argument
174   # W0142: Used * or ** magic
175   # W0613: Unused argument
176
177   code = docutils.utils.unescape(text, restore_backslashes=True)
178
179   try:
180     result = eval(code, EVAL_NS)
181   except Exception, err: # pylint: disable-msg=W0703
182     msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
183                                  line=lineno)
184     return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
185
186   node = docutils.nodes.literal("", unicode(result), **options)
187
188   return ([node], [])
189
190
191 class PythonAssert(sphinx.util.compat.Directive):
192   """Custom directive for writing assertions.
193
194   The content must be a valid Python expression. If its result does not
195   evaluate to C{True}, the assertion fails.
196
197   """
198   has_content = True
199   required_arguments = 0
200   optional_arguments = 0
201   final_argument_whitespace = False
202
203   def run(self):
204     # Handle combinations of Sphinx and docutils not providing the wanted method
205     if hasattr(self, "assert_has_content"):
206       self.assert_has_content()
207     else:
208       assert self.content
209
210     code = "\n".join(self.content)
211
212     try:
213       result = eval(code, EVAL_NS)
214     except Exception, err:
215       raise self.error("Failed to evaluate %r: %s" % (code, err))
216
217     if not result:
218       raise self.error("Assertion failed: %s" % (code, ))
219
220     return []
221
222
223 def BuildQueryFields(fields):
224   """Build query fields documentation.
225
226   @type fields: dict (field name as key, field details as value)
227
228   """
229   for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
230                                              key=operator.itemgetter(0)):
231     assert len(fdef.doc.splitlines()) == 1
232     yield "``%s``" % fdef.name
233     yield "  %s" % fdef.doc
234
235
236 # TODO: Implement Sphinx directive for query fields
237
238
239 def setup(app):
240   """Sphinx extension callback.
241
242   """
243   app.add_directive("opcode_params", OpcodeParams)
244   app.add_directive("pyassert", PythonAssert)
245   app.add_role("pyeval", PythonEvalRole)