Package ganeti :: Package build :: Module sphinx_ext
[hide private]
[frames] | no frames]

Source Code for Module ganeti.build.sphinx_ext

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2011, 2012, 2013 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
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)
439