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  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Sphinx extension for building opcode documentation. 
 32   
 33  """ 
 34   
 35  import re 
 36  from cStringIO import StringIO 
 37   
 38  import docutils.statemachine 
 39  import docutils.nodes 
 40  import docutils.utils 
 41  import docutils.parsers.rst 
 42   
 43  import sphinx.errors 
 44  import sphinx.util.compat 
 45  import sphinx.roles 
 46  import sphinx.addnodes 
 47   
 48  s_compat = sphinx.util.compat 
 49   
 50  try: 
 51    # Access to a protected member of a client class 
 52    # pylint: disable=W0212 
 53    orig_manpage_role = docutils.parsers.rst.roles._roles["manpage"] 
 54  except (AttributeError, ValueError, KeyError), err: 
 55    # Normally the "manpage" role is registered by sphinx/roles.py 
 56    raise Exception("Can't find reST role named 'manpage': %s" % err) 
 57   
 58  from ganeti import _constants 
 59  from ganeti import constants 
 60  from ganeti import compat 
 61  from ganeti import errors 
 62  from ganeti import utils 
 63  from ganeti import opcodes 
 64  from ganeti import opcodes_base 
 65  from ganeti import ht 
 66  from ganeti import rapi 
 67  from ganeti import luxi 
 68  from ganeti import objects 
 69  from ganeti import http 
 70   
 71  import ganeti.rapi.rlib2 # pylint: disable=W0611 
 72  import ganeti.rapi.connector # pylint: disable=W0611 
 73   
 74   
 75  #: Regular expression for man page names 
 76  _MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$") 
 77   
 78  _TAB_WIDTH = 2 
 79   
 80  RAPI_URI_ENCODE_RE = re.compile("[^_a-z0-9]+", re.I) 
81 82 83 -class ReSTError(Exception):
84 """Custom class for generating errors in Sphinx. 85 86 """
87
88 89 -def _GetCommonParamNames():
90 """Builds a list of parameters common to all opcodes. 91 92 """ 93 names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS)) 94 95 # The "depends" attribute should be listed 96 names.remove(opcodes_base.DEPEND_ATTR) 97 98 return names
99 100 101 COMMON_PARAM_NAMES = _GetCommonParamNames() 102 103 #: Namespace for evaluating expressions 104 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors, 105 rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects, 106 http=http) 107 108 # Constants documentation for man pages 109 CV_ECODES_DOC = "ecodes" 110 # We don't care about the leak of variables _, name and doc here. 111 # pylint: disable=W0621 112 CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES] 113 DOCUMENTED_CONSTANTS = { 114 CV_ECODES_DOC: CV_ECODES_DOC_LIST, 115 }
116 117 118 -class OpcodeError(sphinx.errors.SphinxError):
119 category = "Opcode error"
120
121 122 -def _SplitOption(text):
123 """Split simple option list. 124 125 @type text: string 126 @param text: Options, e.g. "foo, bar, baz" 127 128 """ 129 return [i.strip(",").strip() for i in text.split()]
130
131 132 -def _ParseAlias(text):
133 """Parse simple assignment option. 134 135 @type text: string 136 @param text: Assignments, e.g. "foo=bar, hello=world" 137 @rtype: dict 138 139 """ 140 result = {} 141 142 for part in _SplitOption(text): 143 if "=" not in part: 144 raise OpcodeError("Invalid option format, missing equal sign") 145 146 (name, value) = part.split("=", 1) 147 148 result[name.strip()] = value.strip() 149 150 return result
151
152 153 -def _BuildOpcodeParams(op_id, include, exclude, alias):
154 """Build opcode parameter documentation. 155 156 @type op_id: string 157 @param op_id: Opcode ID 158 159 """ 160 op_cls = opcodes.OP_MAPPING[op_id] 161 162 params_with_alias = \ 163 utils.NiceSort([(alias.get(name, name), name, default, test, doc) 164 for (name, default, test, doc) in op_cls.GetAllParams()], 165 key=compat.fst) 166 167 for (rapi_name, name, default, test, doc) in params_with_alias: 168 # Hide common parameters if not explicitly included 169 if (name in COMMON_PARAM_NAMES and 170 (not include or name not in include)): 171 continue 172 if exclude is not None and name in exclude: 173 continue 174 if include is not None and name not in include: 175 continue 176 177 has_default = default is not None or default is not ht.NoDefault 178 has_test = test is not None 179 180 buf = StringIO() 181 buf.write("``%s``" % (rapi_name,)) 182 if has_default or has_test: 183 buf.write(" (") 184 if has_default: 185 if default == "": 186 buf.write("defaults to the empty string") 187 else: 188 buf.write("defaults to ``%s``" % (default,)) 189 if has_test: 190 buf.write(", ") 191 if has_test: 192 buf.write("must be ``%s``" % (test,)) 193 buf.write(")") 194 yield buf.getvalue() 195 196 # Add text 197 for line in doc.splitlines(): 198 yield " %s" % line
199
200 201 -def _BuildOpcodeResult(op_id):
202 """Build opcode result documentation. 203 204 @type op_id: string 205 @param op_id: Opcode ID 206 207 """ 208 op_cls = opcodes.OP_MAPPING[op_id] 209 210 result_fn = getattr(op_cls, "OP_RESULT", None) 211 212 if not result_fn: 213 raise OpcodeError("Opcode '%s' has no result description" % op_id) 214 215 return "``%s``" % result_fn
216
217 218 -class OpcodeParams(s_compat.Directive):
219 """Custom directive for opcode parameters. 220 221 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 222 223 """ 224 has_content = False 225 required_arguments = 1 226 optional_arguments = 0 227 final_argument_whitespace = False 228 option_spec = dict(include=_SplitOption, exclude=_SplitOption, 229 alias=_ParseAlias) 230
231 - def run(self):
232 op_id = self.arguments[0] 233 include = self.options.get("include", None) 234 exclude = self.options.get("exclude", None) 235 alias = self.options.get("alias", {}) 236 237 path = op_id 238 include_text = "\n\n".join(_BuildOpcodeParams(op_id, 239 include, 240 exclude, 241 alias)) 242 243 # Inject into state machine 244 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH, 245 convert_whitespace=1) 246 self.state_machine.insert_input(include_lines, path) 247 248 return []
249
250 251 -class OpcodeResult(s_compat.Directive):
252 """Custom directive for opcode result. 253 254 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 255 256 """ 257 has_content = False 258 required_arguments = 1 259 optional_arguments = 0 260 final_argument_whitespace = False 261
262 - def run(self):
263 op_id = self.arguments[0] 264 265 path = op_id 266 include_text = _BuildOpcodeResult(op_id) 267 268 # Inject into state machine 269 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH, 270 convert_whitespace=1) 271 self.state_machine.insert_input(include_lines, path) 272 273 return []
274
275 276 -def PythonEvalRole(role, rawtext, text, lineno, inliner, 277 options={}, content=[]):
278 """Custom role to evaluate Python expressions. 279 280 The expression's result is included as a literal. 281 282 """ 283 # pylint: disable=W0102,W0613,W0142 284 # W0102: Dangerous default value as argument 285 # W0142: Used * or ** magic 286 # W0613: Unused argument 287 288 code = docutils.utils.unescape(text, restore_backslashes=True) 289 290 try: 291 result = eval(code, EVAL_NS) 292 except Exception, err: # pylint: disable=W0703 293 msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err), 294 line=lineno) 295 return ([inliner.problematic(rawtext, rawtext, msg)], [msg]) 296 297 node = docutils.nodes.literal("", unicode(result), **options) 298 299 return ([node], [])
300
301 302 -class PythonAssert(s_compat.Directive):
303 """Custom directive for writing assertions. 304 305 The content must be a valid Python expression. If its result does not 306 evaluate to C{True}, the assertion fails. 307 308 """ 309 has_content = True 310 required_arguments = 0 311 optional_arguments = 0 312 final_argument_whitespace = False 313
314 - def run(self):
315 # Handle combinations of Sphinx and docutils not providing the wanted method 316 if hasattr(self, "assert_has_content"): 317 self.assert_has_content() 318 else: 319 assert self.content 320 321 code = "\n".join(self.content) 322 323 try: 324 result = eval(code, EVAL_NS) 325 except Exception, err: 326 raise self.error("Failed to evaluate %r: %s" % (code, err)) 327 328 if not result: 329 raise self.error("Assertion failed: %s" % (code, )) 330 331 return []
332
333 334 -def BuildQueryFields(fields):
335 """Build query fields documentation. 336 337 @type fields: dict (field name as key, field details as value) 338 339 """ 340 defs = [(fdef.name, fdef.doc) 341 for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(), 342 key=compat.fst)] 343 return BuildValuesDoc(defs)
344
345 346 -def BuildValuesDoc(values):
347 """Builds documentation for a list of values 348 349 @type values: list of tuples in the form (value, documentation) 350 351 """ 352 for name, doc in values: 353 assert len(doc.splitlines()) == 1 354 yield "``%s``" % (name,) 355 yield " %s" % (doc,)
356
357 358 -def _ManPageNodeClass(*args, **kwargs):
359 """Generates a pending XRef like a ":doc:`...`" reference. 360 361 """ 362 # Type for sphinx/environment.py:BuildEnvironment.resolve_references 363 kwargs["reftype"] = "doc" 364 365 # Force custom title 366 kwargs["refexplicit"] = True 367 368 return sphinx.addnodes.pending_xref(*args, **kwargs)
369
370 371 -class _ManPageXRefRole(sphinx.roles.XRefRole):
372 - def __init__(self):
373 """Initializes this class. 374 375 """ 376 sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass, 377 warn_dangling=True) 378 379 assert not hasattr(self, "converted"), \ 380 "Sphinx base class gained an attribute named 'converted'" 381 382 self.converted = None
383
416
417 418 -def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102 419 options={}, content=[]):
420 """Custom role for man page references. 421 422 Converts man pages to links if enabled during the build. 423 424 """ 425 xref = _ManPageXRefRole() 426 427 assert ht.TNone(xref.converted) 428 429 # Check if it's a known man page 430 try: 431 result = xref(typ, rawtext, text, lineno, inliner, 432 options=options, content=content) 433 except ReSTError, err: 434 msg = inliner.reporter.error(str(err), line=lineno) 435 return ([inliner.problematic(rawtext, rawtext, msg)], [msg]) 436 437 assert ht.TBool(xref.converted) 438 439 # Return if the conversion was successful (i.e. the man page was known and 440 # conversion was enabled) 441 if xref.converted: 442 return result 443 444 # Fallback if man page links are disabled or an unknown page is referenced 445 return orig_manpage_role(typ, rawtext, text, lineno, inliner, 446 options=options, content=content)
447 459 477
478 479 -def _GetHandlerMethods(handler):
480 """Returns list of HTTP methods supported by handler class. 481 482 @type handler: L{rapi.baserlib.ResourceBase} 483 @param handler: Handler class 484 @rtype: list of strings 485 486 """ 487 return sorted(method 488 for (method, op_attr, _, _, _) in rapi.baserlib.OPCODE_ATTRS 489 # Only if handler supports method 490 if hasattr(handler, method) or hasattr(handler, op_attr))
491
492 493 -def _DescribeHandlerAccess(handler, method):
494 """Returns textual description of required RAPI permissions. 495 496 @type handler: L{rapi.baserlib.ResourceBase} 497 @param handler: Handler class 498 @type method: string 499 @param method: HTTP method (e.g. L{http.HTTP_GET}) 500 @rtype: string 501 502 """ 503 access = rapi.baserlib.GetHandlerAccess(handler, method) 504 505 if access: 506 return utils.CommaJoin(sorted(access)) 507 else: 508 return "*(none)*"
509
510 511 -class _RapiHandlersForDocsHelper(object):
512 @classmethod
513 - def Build(cls):
514 """Returns dictionary of resource handlers. 515 516 """ 517 resources = \ 518 rapi.connector.GetHandlers("[node_name]", "[instance_name]", 519 "[group_name]", "[network_name]", "[job_id]", 520 "[disk_index]", "[resource]", 521 translate=cls._TranslateResourceUri) 522 523 return resources
524 525 @classmethod
526 - def _TranslateResourceUri(cls, *args):
527 """Translates a resource URI for use in documentation. 528 529 @see: L{rapi.connector.GetHandlers} 530 531 """ 532 return "".join(map(cls._UriPatternToString, args))
533 534 @staticmethod
535 - def _UriPatternToString(value):
536 """Converts L{rapi.connector.UriPattern} to strings. 537 538 """ 539 if isinstance(value, rapi.connector.UriPattern): 540 return value.content 541 else: 542 return value
543 544 545 _RAPI_RESOURCES_FOR_DOCS = _RapiHandlersForDocsHelper.Build()
546 547 548 -def _BuildRapiAccessTable(res):
549 """Build a table with access permissions needed for all RAPI resources. 550 551 """ 552 for (uri, handler) in utils.NiceSort(res.items(), key=compat.fst): 553 reslink = _MakeRapiResourceLink(None, uri) 554 if not reslink: 555 # No link was generated 556 continue 557 558 yield ":ref:`%s <%s>`" % (uri, reslink) 559 560 for method in _GetHandlerMethods(handler): 561 yield (" | :ref:`%s <%s>`: %s" % 562 (method, _MakeRapiResourceLink(method, uri), 563 _DescribeHandlerAccess(handler, method)))
564
565 566 -class RapiAccessTable(s_compat.Directive):
567 """Custom directive to generate table of all RAPI resources. 568 569 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 570 571 """ 572 has_content = False 573 required_arguments = 0 574 optional_arguments = 0 575 final_argument_whitespace = False 576 option_spec = {} 577
578 - def run(self):
579 include_text = "\n".join(_BuildRapiAccessTable(_RAPI_RESOURCES_FOR_DOCS)) 580 581 # Inject into state machine 582 include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH, 583 convert_whitespace=1) 584 self.state_machine.insert_input(include_lines, self.__class__.__name__) 585 586 return []
587
588 589 -class RapiResourceDetails(s_compat.Directive):
590 """Custom directive for RAPI resource details. 591 592 See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>. 593 594 """ 595 has_content = False 596 required_arguments = 1 597 optional_arguments = 0 598 final_argument_whitespace = False 599
600 - def run(self):
601 uri = self.arguments[0] 602 603 try: 604 handler = _RAPI_RESOURCES_FOR_DOCS[uri] 605 except KeyError: 606 raise self.error("Unknown resource URI '%s'" % uri) 607 608 lines = [ 609 ".. list-table::", 610 " :widths: 1 4", 611 " :header-rows: 1", 612 "", 613 " * - Method", 614 " - :ref:`Required permissions <rapi-users>`", 615 ] 616 617 for method in _GetHandlerMethods(handler): 618 lines.extend([ 619 " * - :ref:`%s <%s>`" % (method, _MakeRapiResourceLink(method, uri)), 620 " - %s" % _DescribeHandlerAccess(handler, method), 621 ]) 622 623 # Inject into state machine 624 include_lines = \ 625 docutils.statemachine.string2lines("\n".join(lines), _TAB_WIDTH, 626 convert_whitespace=1) 627 self.state_machine.insert_input(include_lines, self.__class__.__name__) 628 629 return []
630
631 632 -def setup(app):
633 """Sphinx extension callback. 634 635 """ 636 # TODO: Implement Sphinx directive for query fields 637 app.add_directive("opcode_params", OpcodeParams) 638 app.add_directive("opcode_result", OpcodeResult) 639 app.add_directive("pyassert", PythonAssert) 640 app.add_role("pyeval", PythonEvalRole) 641 app.add_directive("rapi_access_table", RapiAccessTable) 642 app.add_directive("rapi_resource_details", RapiResourceDetails) 643 644 app.add_config_value("enable_manpages", False, True) 645 app.add_role("manpage", _ManPageRole)
646