cmdlib changes to support nic.network as uuid
[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 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   # Access to a protected member of a client class
43   # pylint: disable=W0212
44   orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"]
45 except (AttributeError, ValueError, KeyError), err:
46   # Normally the "manpage" role is registered by sphinx/roles.py
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 # pylint: disable=W0611
60
61
62 #: Regular expression for man page names
63 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
64
65
66 class ReSTError(Exception):
67   """Custom class for generating errors in Sphinx.
68
69   """
70
71
72 def _GetCommonParamNames():
73   """Builds a list of parameters common to all opcodes.
74
75   """
76   names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
77
78   # The "depends" attribute should be listed
79   names.remove(opcodes.DEPEND_ATTR)
80
81   return names
82
83
84 COMMON_PARAM_NAMES = _GetCommonParamNames()
85
86 #: Namespace for evaluating expressions
87 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
88                rlib2=rapi.rlib2, luxi=luxi, rapi=rapi)
89
90 # Constants documentation for man pages
91 CV_ECODES_DOC = "ecodes"
92 # We don't care about the leak of variables _, name and doc here.
93 # pylint: disable=W0621
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
100 class OpcodeError(sphinx.errors.SphinxError):
101   category = "Opcode error"
102
103
104 def _SplitOption(text):
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
114 def _ParseAlias(text):
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
135 def _BuildOpcodeParams(op_id, include, exclude, alias):
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     # Hide common parameters if not explicitly included
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     # Add text
176     for line in doc.splitlines():
177       yield "  %s" % line
178
179
180 def _BuildOpcodeResult(op_id):
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
197 class OpcodeParams(s_compat.Directive):
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
210   def run(self):
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     # Inject into state machine
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
228 class OpcodeResult(s_compat.Directive):
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
239   def run(self):
240     op_id = self.arguments[0]
241
242     tab_width = 2
243     path = op_id
244     include_text = _BuildOpcodeResult(op_id)
245
246     # Inject into state machine
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   # pylint: disable=W0102,W0613,W0142
262   # W0102: Dangerous default value as argument
263   # W0142: Used * or ** magic
264   # W0613: Unused argument
265
266   code = docutils.utils.unescape(text, restore_backslashes=True)
267
268   try:
269     result = eval(code, EVAL_NS)
270   except Exception, err: # pylint: disable=W0703
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
280 class PythonAssert(s_compat.Directive):
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
292   def run(self):
293     # Handle combinations of Sphinx and docutils not providing the wanted method
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
312 def BuildQueryFields(fields):
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
324 def BuildValuesDoc(values):
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   # Type for sphinx/environment.py:BuildEnvironment.resolve_references
341   kwargs["reftype"] = "doc"
342
343   # Force custom title
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     # Check format and extract name and section
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       # Create link to known man page
390       return (title, "man-%s" % name)
391     else:
392       # No changes
393       return (title, target)
394
395
396 def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
397                  options={}, content=[]):
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   # Check if it's a known man page
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   # Return if the conversion was successful (i.e. the man page was known and
418   # conversion was enabled)
419   if xref.converted:
420     return result
421
422   # Fallback if man page links are disabled or an unknown page is referenced
423   return orig_manpage_role(typ, rawtext, text, lineno, inliner,
424                            options=options, content=content)
425
426
427 def setup(app):
428   """Sphinx extension callback.
429
430   """
431   # TODO: Implement Sphinx directive for query fields
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)