4 # Copyright (C) 2011, 2012 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
32 import docutils.parsers.rst
35 import sphinx.util.compat
37 import sphinx.addnodes
39 s_compat = sphinx.util.compat
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)
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
55 from ganeti import rapi
56 from ganeti import luxi
57 from ganeti import _autoconf
59 import ganeti.rapi.rlib2 # pylint: disable=W0611
62 #: Regular expression for man page names
63 _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
66 class ReSTError(Exception):
67 """Custom class for generating errors in Sphinx.
72 def _GetCommonParamNames():
73 """Builds a list of parameters common to all opcodes.
76 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
78 # The "depends" attribute should be listed
79 names.remove(opcodes.DEPEND_ATTR)
84 COMMON_PARAM_NAMES = _GetCommonParamNames()
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)
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,
100 class OpcodeError(sphinx.errors.SphinxError):
101 category = "Opcode error"
104 def _SplitOption(text):
105 """Split simple option list.
108 @param text: Options, e.g. "foo, bar, baz"
111 return [i.strip(",").strip() for i in text.split()]
114 def _ParseAlias(text):
115 """Parse simple assignment option.
118 @param text: Assignments, e.g. "foo=bar, hello=world"
124 for part in _SplitOption(text):
126 raise OpcodeError("Invalid option format, missing equal sign")
128 (name, value) = part.split("=", 1)
130 result[name.strip()] = value.strip()
135 def _BuildOpcodeParams(op_id, include, exclude, alias):
136 """Build opcode parameter documentation.
139 @param op_id: Opcode ID
142 op_cls = opcodes.OP_MAPPING[op_id]
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()],
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)):
154 if exclude is not None and name in exclude:
156 if include is not None and name not in include:
159 has_default = default is not ht.NoDefault
160 has_test = not (test is None or test is ht.NoType)
163 buf.write("``%s``" % rapi_name)
164 if has_default or has_test:
167 buf.write("defaults to ``%s``" % default)
171 buf.write("must be ``%s``" % test)
176 for line in doc.splitlines():
180 def _BuildOpcodeResult(op_id):
181 """Build opcode result documentation.
184 @param op_id: Opcode ID
187 op_cls = opcodes.OP_MAPPING[op_id]
189 result_fn = getattr(op_cls, "OP_RESULT", None)
192 raise OpcodeError("Opcode '%s' has no result description" % op_id)
194 return "``%s``" % result_fn
197 class OpcodeParams(s_compat.Directive):
198 """Custom directive for opcode parameters.
200 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
204 required_arguments = 1
205 optional_arguments = 0
206 final_argument_whitespace = False
207 option_spec = dict(include=_SplitOption, exclude=_SplitOption,
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", {})
218 include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
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)
228 class OpcodeResult(s_compat.Directive):
229 """Custom directive for opcode result.
231 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
235 required_arguments = 1
236 optional_arguments = 0
237 final_argument_whitespace = False
240 op_id = self.arguments[0]
244 include_text = _BuildOpcodeResult(op_id)
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)
254 def PythonEvalRole(role, rawtext, text, lineno, inliner,
255 options={}, content=[]):
256 """Custom role to evaluate Python expressions.
258 The expression's result is included as a literal.
261 # pylint: disable=W0102,W0613,W0142
262 # W0102: Dangerous default value as argument
263 # W0142: Used * or ** magic
264 # W0613: Unused argument
266 code = docutils.utils.unescape(text, restore_backslashes=True)
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),
273 return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
275 node = docutils.nodes.literal("", unicode(result), **options)
280 class PythonAssert(s_compat.Directive):
281 """Custom directive for writing assertions.
283 The content must be a valid Python expression. If its result does not
284 evaluate to C{True}, the assertion fails.
288 required_arguments = 0
289 optional_arguments = 0
290 final_argument_whitespace = False
293 # Handle combinations of Sphinx and docutils not providing the wanted method
294 if hasattr(self, "assert_has_content"):
295 self.assert_has_content()
299 code = "\n".join(self.content)
302 result = eval(code, EVAL_NS)
303 except Exception, err:
304 raise self.error("Failed to evaluate %r: %s" % (code, err))
307 raise self.error("Assertion failed: %s" % (code, ))
312 def BuildQueryFields(fields):
313 """Build query fields documentation.
315 @type fields: dict (field name as key, field details as value)
318 defs = [(fdef.name, fdef.doc)
319 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
321 return BuildValuesDoc(defs)
324 def BuildValuesDoc(values):
325 """Builds documentation for a list of values
327 @type values: list of tuples in the form (value, documentation)
330 for name, doc in values:
331 assert len(doc.splitlines()) == 1
332 yield "``%s``" % name
336 def _ManPageNodeClass(*args, **kwargs):
337 """Generates a pending XRef like a ":doc:`...`" reference.
340 # Type for sphinx/environment.py:BuildEnvironment.resolve_references
341 kwargs["reftype"] = "doc"
344 kwargs["refexplicit"] = True
346 return sphinx.addnodes.pending_xref(*args, **kwargs)
349 class _ManPageXRefRole(sphinx.roles.XRefRole):
351 """Initializes this class.
354 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
357 assert not hasattr(self, "converted"), \
358 "Sphinx base class gained an attribute named 'converted'"
360 self.converted = None
362 def process_link(self, env, refnode, has_explicit_title, title, target):
363 """Specialization for man page links.
366 if has_explicit_title:
367 raise ReSTError("Setting explicit title is not allowed for man pages")
369 # Check format and extract name and section
370 m = _MAN_RE.match(title)
372 raise ReSTError("Man page reference '%s' does not match regular"
373 " expression '%s'" % (title, _MAN_RE.pattern))
375 name = m.group("name")
376 section = int(m.group("section"))
378 wanted_section = _autoconf.MAN_PAGES.get(name, None)
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))
385 self.converted = bool(wanted_section is not None and
386 env.app.config.enable_manpages)
389 # Create link to known man page
390 return (title, "man-%s" % name)
393 return (title, target)
396 def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
397 options={}, content=[]):
398 """Custom role for man page references.
400 Converts man pages to links if enabled during the build.
403 xref = _ManPageXRefRole()
405 assert ht.TNone(xref.converted)
407 # Check if it's a known man page
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])
415 assert ht.TBool(xref.converted)
417 # Return if the conversion was successful (i.e. the man page was known and
418 # conversion was enabled)
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)
428 """Sphinx extension callback.
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)
437 app.add_config_value("enable_manpages", False, True)
438 app.add_role("manpage", _ManPageRole)