1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Sphinx extension for building opcode documentation.
23
24 """
25
26 import re
27 from cStringIO import StringIO
28
29 import docutils.statemachine
30 import docutils.nodes
31 import docutils.utils
32 import docutils.parsers.rst
33
34 import sphinx.errors
35 import sphinx.util.compat
36 import sphinx.roles
37 import sphinx.addnodes
38
39 s_compat = sphinx.util.compat
40
41 try:
42
43
44 orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
45 except (AttributeError, ValueError, KeyError), err:
46
47 raise Exception("Can't find reST role named 'manpage': %s" % err)
48
49 from ganeti import constants
50 from ganeti import compat
51 from ganeti import errors
52 from ganeti import utils
53 from ganeti import opcodes
54 from ganeti import ht
55 from ganeti import rapi
56 from ganeti import luxi
57 from ganeti import _autoconf
58
59 import ganeti.rapi.rlib2
60
61
62
63 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
64
65
67 """Custom class for generating errors in Sphinx.
68
69 """
70
71
82
83
84 COMMON_PARAM_NAMES = _GetCommonParamNames()
85
86
87 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
88 rlib2=rapi.rlib2, luxi=luxi, rapi=rapi)
89
90
91 CV_ECODES_DOC = "ecodes"
92
93
94 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
95 DOCUMENTED_CONSTANTS = {
96 CV_ECODES_DOC: CV_ECODES_DOC_LIST,
97 }
98
99
102
103
105 """Split simple option list.
106
107 @type text: string
108 @param text: Options, e.g. "foo, bar, baz"
109
110 """
111 return [i.strip(",").strip() for i in text.split()]
112
113
115 """Parse simple assignment option.
116
117 @type text: string
118 @param text: Assignments, e.g. "foo=bar, hello=world"
119 @rtype: dict
120
121 """
122 result = {}
123
124 for part in _SplitOption(text):
125 if "=" not in part:
126 raise OpcodeError("Invalid option format, missing equal sign")
127
128 (name, value) = part.split("=", 1)
129
130 result[name.strip()] = value.strip()
131
132 return result
133
134
136 """Build opcode parameter documentation.
137
138 @type op_id: string
139 @param op_id: Opcode ID
140
141 """
142 op_cls = opcodes.OP_MAPPING[op_id]
143
144 params_with_alias = \
145 utils.NiceSort([(alias.get(name, name), name, default, test, doc)
146 for (name, default, test, doc) in op_cls.GetAllParams()],
147 key=compat.fst)
148
149 for (rapi_name, name, default, test, doc) in params_with_alias:
150
151 if (name in COMMON_PARAM_NAMES and
152 (not include or name not in include)):
153 continue
154 if exclude is not None and name in exclude:
155 continue
156 if include is not None and name not in include:
157 continue
158
159 has_default = default is not ht.NoDefault
160 has_test = not (test is None or test is ht.NoType)
161
162 buf = StringIO()
163 buf.write("``%s``" % (rapi_name,))
164 if has_default or has_test:
165 buf.write(" (")
166 if has_default:
167 buf.write("defaults to ``%s``" % (default,))
168 if has_test:
169 buf.write(", ")
170 if has_test:
171 buf.write("must be ``%s``" % (test,))
172 buf.write(")")
173 yield buf.getvalue()
174
175
176 for line in doc.splitlines():
177 yield " %s" % line
178
179
181 """Build opcode result documentation.
182
183 @type op_id: string
184 @param op_id: Opcode ID
185
186 """
187 op_cls = opcodes.OP_MAPPING[op_id]
188
189 result_fn = getattr(op_cls, "OP_RESULT", None)
190
191 if not result_fn:
192 raise OpcodeError("Opcode '%s' has no result description" % op_id)
193
194 return "``%s``" % result_fn
195
196
198 """Custom directive for opcode parameters.
199
200 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
201
202 """
203 has_content = False
204 required_arguments = 1
205 optional_arguments = 0
206 final_argument_whitespace = False
207 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
208 alias=_ParseAlias)
209
211 op_id = self.arguments[0]
212 include = self.options.get("include", None)
213 exclude = self.options.get("exclude", None)
214 alias = self.options.get("alias", {})
215
216 tab_width = 2
217 path = op_id
218 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
219
220
221 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
222 convert_whitespace=1)
223 self.state_machine.insert_input(include_lines, path)
224
225 return []
226
227
229 """Custom directive for opcode result.
230
231 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
232
233 """
234 has_content = False
235 required_arguments = 1
236 optional_arguments = 0
237 final_argument_whitespace = False
238
240 op_id = self.arguments[0]
241
242 tab_width = 2
243 path = op_id
244 include_text = _BuildOpcodeResult(op_id)
245
246
247 include_lines = docutils.statemachine.string2lines(include_text, tab_width,
248 convert_whitespace=1)
249 self.state_machine.insert_input(include_lines, path)
250
251 return []
252
253
254 -def PythonEvalRole(role, rawtext, text, lineno, inliner,
255 options={}, content=[]):
256 """Custom role to evaluate Python expressions.
257
258 The expression's result is included as a literal.
259
260 """
261
262
263
264
265
266 code = docutils.utils.unescape(text, restore_backslashes=True)
267
268 try:
269 result = eval(code, EVAL_NS)
270 except Exception, err:
271 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
272 line=lineno)
273 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
274
275 node = docutils.nodes.literal("", unicode(result), **options)
276
277 return ([node], [])
278
279
281 """Custom directive for writing assertions.
282
283 The content must be a valid Python expression. If its result does not
284 evaluate to C{True}, the assertion fails.
285
286 """
287 has_content = True
288 required_arguments = 0
289 optional_arguments = 0
290 final_argument_whitespace = False
291
293
294 if hasattr(self, "assert_has_content"):
295 self.assert_has_content()
296 else:
297 assert self.content
298
299 code = "\n".join(self.content)
300
301 try:
302 result = eval(code, EVAL_NS)
303 except Exception, err:
304 raise self.error("Failed to evaluate %r: %s" % (code, err))
305
306 if not result:
307 raise self.error("Assertion failed: %s" % (code, ))
308
309 return []
310
311
313 """Build query fields documentation.
314
315 @type fields: dict (field name as key, field details as value)
316
317 """
318 defs = [(fdef.name, fdef.doc)
319 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
320 key=compat.fst)]
321 return BuildValuesDoc(defs)
322
323
325 """Builds documentation for a list of values
326
327 @type values: list of tuples in the form (value, documentation)
328
329 """
330 for name, doc in values:
331 assert len(doc.splitlines()) == 1
332 yield "``%s``" % (name,)
333 yield " %s" % (doc,)
334
335
336 -def _ManPageNodeClass(*args, **kwargs):
337 """Generates a pending XRef like a ":doc:`...`" reference.
338
339 """
340
341 kwargs["reftype"] = "doc"
342
343
344 kwargs["refexplicit"] = True
345
346 return sphinx.addnodes.pending_xref(*args, **kwargs)
347
348
349 -class _ManPageXRefRole(sphinx.roles.XRefRole):
350 - def __init__(self):
351 """Initializes this class.
352
353 """
354 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
355 warn_dangling=True)
356
357 assert not hasattr(self, "converted"), \
358 "Sphinx base class gained an attribute named 'converted'"
359
360 self.converted = None
361
362 - def process_link(self, env, refnode, has_explicit_title, title, target):
363 """Specialization for man page links.
364
365 """
366 if has_explicit_title:
367 raise ReSTError("Setting explicit title is not allowed for man pages")
368
369
370 m = _MAN_RE.match(title)
371 if not m:
372 raise ReSTError("Man page reference '%s' does not match regular"
373 " expression '%s'" % (title, _MAN_RE.pattern))
374
375 name = m.group("name")
376 section = int(m.group("section"))
377
378 wanted_section = _autoconf.MAN_PAGES.get(name, None)
379
380 if not (wanted_section is None or wanted_section == section):
381 raise ReSTError("Referenced man page '%s' has section number %s, but the"
382 " reference uses section %s" %
383 (name, wanted_section, section))
384
385 self.converted = bool(wanted_section is not None and
386 env.app.config.enable_manpages)
387
388 if self.converted:
389
390 return (title, "man-%s" % name)
391 else:
392
393 return (title, target)
394
395
398 """Custom role for man page references.
399
400 Converts man pages to links if enabled during the build.
401
402 """
403 xref = _ManPageXRefRole()
404
405 assert ht.TNone(xref.converted)
406
407
408 try:
409 result = xref(typ, rawtext, text, lineno, inliner,
410 options=options, content=content)
411 except ReSTError, err:
412 msg = inliner.reporter.error(str(err), line=lineno)
413 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
414
415 assert ht.TBool(xref.converted)
416
417
418
419 if xref.converted:
420 return result
421
422
423 return orig_manpage_role(typ, rawtext, text, lineno, inliner,
424 options=options, content=content)
425
426
428 """Sphinx extension callback.
429
430 """
431
432 app.add_directive("opcode_params", OpcodeParams)
433 app.add_directive("opcode_result", OpcodeResult)
434 app.add_directive("pyassert", PythonAssert)
435 app.add_role("pyeval", PythonEvalRole)
436
437 app.add_config_value("enable_manpages", False, True)
438 app.add_role("manpage", _ManPageRole)
439