Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ 46ab58d4

History | View | Annotate | Download (11.7 kB)

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 objects
58
from ganeti import _autoconf
59

    
60
import ganeti.rapi.rlib2 # pylint: disable=W0611
61

    
62

    
63
#: Regular expression for man page names
64
_MAN_RE = re.compile(r"^(?P<name>[-\w_]+)\((?P<section>\d+)\)$")
65

    
66

    
67
class ReSTError(Exception):
68
  """Custom class for generating errors in Sphinx.
69

70
  """
71

    
72

    
73
def _GetCommonParamNames():
74
  """Builds a list of parameters common to all opcodes.
75

76
  """
77
  names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
78

    
79
  # The "depends" attribute should be listed
80
  names.remove(opcodes.DEPEND_ATTR)
81

    
82
  return names
83

    
84

    
85
COMMON_PARAM_NAMES = _GetCommonParamNames()
86

    
87
#: Namespace for evaluating expressions
88
EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
89
               rlib2=rapi.rlib2, luxi=luxi, rapi=rapi, objects=objects)
90

    
91
# Constants documentation for man pages
92
CV_ECODES_DOC = "ecodes"
93
# We don't care about the leak of variables _, name and doc here.
94
# pylint: disable=W0621
95
CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
96
DOCUMENTED_CONSTANTS = {
97
  CV_ECODES_DOC: CV_ECODES_DOC_LIST,
98
  }
99

    
100

    
101
class OpcodeError(sphinx.errors.SphinxError):
102
  category = "Opcode error"
103

    
104

    
105
def _SplitOption(text):
106
  """Split simple option list.
107

108
  @type text: string
109
  @param text: Options, e.g. "foo, bar, baz"
110

111
  """
112
  return [i.strip(",").strip() for i in text.split()]
113

    
114

    
115
def _ParseAlias(text):
116
  """Parse simple assignment option.
117

118
  @type text: string
119
  @param text: Assignments, e.g. "foo=bar, hello=world"
120
  @rtype: dict
121

122
  """
123
  result = {}
124

    
125
  for part in _SplitOption(text):
126
    if "=" not in part:
127
      raise OpcodeError("Invalid option format, missing equal sign")
128

    
129
    (name, value) = part.split("=", 1)
130

    
131
    result[name.strip()] = value.strip()
132

    
133
  return result
134

    
135

    
136
def _BuildOpcodeParams(op_id, include, exclude, alias):
137
  """Build opcode parameter documentation.
138

139
  @type op_id: string
140
  @param op_id: Opcode ID
141

142
  """
143
  op_cls = opcodes.OP_MAPPING[op_id]
144

    
145
  params_with_alias = \
146
    utils.NiceSort([(alias.get(name, name), name, default, test, doc)
147
                    for (name, default, test, doc) in op_cls.GetAllParams()],
148
                   key=compat.fst)
149

    
150
  for (rapi_name, name, default, test, doc) in params_with_alias:
151
    # Hide common parameters if not explicitly included
152
    if (name in COMMON_PARAM_NAMES and
153
        (not include or name not in include)):
154
      continue
155
    if exclude is not None and name in exclude:
156
      continue
157
    if include is not None and name not in include:
158
      continue
159

    
160
    has_default = default is not ht.NoDefault
161
    has_test = not (test is None or test is ht.NoType)
162

    
163
    buf = StringIO()
164
    buf.write("``%s``" % rapi_name)
165
    if has_default or has_test:
166
      buf.write(" (")
167
      if has_default:
168
        buf.write("defaults to ``%s``" % default)
169
        if has_test:
170
          buf.write(", ")
171
      if has_test:
172
        buf.write("must be ``%s``" % test)
173
      buf.write(")")
174
    yield buf.getvalue()
175

    
176
    # Add text
177
    for line in doc.splitlines():
178
      yield "  %s" % line
179

    
180

    
181
def _BuildOpcodeResult(op_id):
182
  """Build opcode result documentation.
183

184
  @type op_id: string
185
  @param op_id: Opcode ID
186

187
  """
188
  op_cls = opcodes.OP_MAPPING[op_id]
189

    
190
  result_fn = getattr(op_cls, "OP_RESULT", None)
191

    
192
  if not result_fn:
193
    raise OpcodeError("Opcode '%s' has no result description" % op_id)
194

    
195
  return "``%s``" % result_fn
196

    
197

    
198
class OpcodeParams(s_compat.Directive):
199
  """Custom directive for opcode parameters.
200

201
  See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
202

203
  """
204
  has_content = False
205
  required_arguments = 1
206
  optional_arguments = 0
207
  final_argument_whitespace = False
208
  option_spec = dict(include=_SplitOption, exclude=_SplitOption,
209
                     alias=_ParseAlias)
210

    
211
  def run(self):
212
    op_id = self.arguments[0]
213
    include = self.options.get("include", None)
214
    exclude = self.options.get("exclude", None)
215
    alias = self.options.get("alias", {})
216

    
217
    tab_width = 2
218
    path = op_id
219
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
220

    
221
    # Inject into state machine
222
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
223
                                                       convert_whitespace=1)
224
    self.state_machine.insert_input(include_lines, path)
225

    
226
    return []
227

    
228

    
229
class OpcodeResult(s_compat.Directive):
230
  """Custom directive for opcode result.
231

232
  See also <http://docutils.sourceforge.net/docs/howto/rst-directives.html>.
233

234
  """
235
  has_content = False
236
  required_arguments = 1
237
  optional_arguments = 0
238
  final_argument_whitespace = False
239

    
240
  def run(self):
241
    op_id = self.arguments[0]
242

    
243
    tab_width = 2
244
    path = op_id
245
    include_text = _BuildOpcodeResult(op_id)
246

    
247
    # Inject into state machine
248
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
249
                                                       convert_whitespace=1)
250
    self.state_machine.insert_input(include_lines, path)
251

    
252
    return []
253

    
254

    
255
def PythonEvalRole(role, rawtext, text, lineno, inliner,
256
                   options={}, content=[]):
257
  """Custom role to evaluate Python expressions.
258

259
  The expression's result is included as a literal.
260

261
  """
262
  # pylint: disable=W0102,W0613,W0142
263
  # W0102: Dangerous default value as argument
264
  # W0142: Used * or ** magic
265
  # W0613: Unused argument
266

    
267
  code = docutils.utils.unescape(text, restore_backslashes=True)
268

    
269
  try:
270
    result = eval(code, EVAL_NS)
271
  except Exception, err: # pylint: disable=W0703
272
    msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
273
                                 line=lineno)
274
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
275

    
276
  node = docutils.nodes.literal("", unicode(result), **options)
277

    
278
  return ([node], [])
279

    
280

    
281
class PythonAssert(s_compat.Directive):
282
  """Custom directive for writing assertions.
283

284
  The content must be a valid Python expression. If its result does not
285
  evaluate to C{True}, the assertion fails.
286

287
  """
288
  has_content = True
289
  required_arguments = 0
290
  optional_arguments = 0
291
  final_argument_whitespace = False
292

    
293
  def run(self):
294
    # Handle combinations of Sphinx and docutils not providing the wanted method
295
    if hasattr(self, "assert_has_content"):
296
      self.assert_has_content()
297
    else:
298
      assert self.content
299

    
300
    code = "\n".join(self.content)
301

    
302
    try:
303
      result = eval(code, EVAL_NS)
304
    except Exception, err:
305
      raise self.error("Failed to evaluate %r: %s" % (code, err))
306

    
307
    if not result:
308
      raise self.error("Assertion failed: %s" % (code, ))
309

    
310
    return []
311

    
312

    
313
def BuildQueryFields(fields):
314
  """Build query fields documentation.
315

316
  @type fields: dict (field name as key, field details as value)
317

318
  """
319
  defs = [(fdef.name, fdef.doc)
320
           for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
321
                                                      key=compat.fst)]
322
  return BuildValuesDoc(defs)
323

    
324

    
325
def BuildValuesDoc(values):
326
  """Builds documentation for a list of values
327

328
  @type values: list of tuples in the form (value, documentation)
329

330
  """
331
  for name, doc in values:
332
    assert len(doc.splitlines()) == 1
333
    yield "``%s``" % name
334
    yield "  %s" % doc
335

    
336

    
337
def _ManPageNodeClass(*args, **kwargs):
338
  """Generates a pending XRef like a ":doc:`...`" reference.
339

340
  """
341
  # Type for sphinx/environment.py:BuildEnvironment.resolve_references
342
  kwargs["reftype"] = "doc"
343

    
344
  # Force custom title
345
  kwargs["refexplicit"] = True
346

    
347
  return sphinx.addnodes.pending_xref(*args, **kwargs)
348

    
349

    
350
class _ManPageXRefRole(sphinx.roles.XRefRole):
351
  def __init__(self):
352
    """Initializes this class.
353

354
    """
355
    sphinx.roles.XRefRole.__init__(self, nodeclass=_ManPageNodeClass,
356
                                   warn_dangling=True)
357

    
358
    assert not hasattr(self, "converted"), \
359
      "Sphinx base class gained an attribute named 'converted'"
360

    
361
    self.converted = None
362

    
363
  def process_link(self, env, refnode, has_explicit_title, title, target):
364
    """Specialization for man page links.
365

366
    """
367
    if has_explicit_title:
368
      raise ReSTError("Setting explicit title is not allowed for man pages")
369

    
370
    # Check format and extract name and section
371
    m = _MAN_RE.match(title)
372
    if not m:
373
      raise ReSTError("Man page reference '%s' does not match regular"
374
                      " expression '%s'" % (title, _MAN_RE.pattern))
375

    
376
    name = m.group("name")
377
    section = int(m.group("section"))
378

    
379
    wanted_section = _autoconf.MAN_PAGES.get(name, None)
380

    
381
    if not (wanted_section is None or wanted_section == section):
382
      raise ReSTError("Referenced man page '%s' has section number %s, but the"
383
                      " reference uses section %s" %
384
                      (name, wanted_section, section))
385

    
386
    self.converted = bool(wanted_section is not None and
387
                          env.app.config.enable_manpages)
388

    
389
    if self.converted:
390
      # Create link to known man page
391
      return (title, "man-%s" % name)
392
    else:
393
      # No changes
394
      return (title, target)
395

    
396

    
397
def _ManPageRole(typ, rawtext, text, lineno, inliner, # pylint: disable=W0102
398
                 options={}, content=[]):
399
  """Custom role for man page references.
400

401
  Converts man pages to links if enabled during the build.
402

403
  """
404
  xref = _ManPageXRefRole()
405

    
406
  assert ht.TNone(xref.converted)
407

    
408
  # Check if it's a known man page
409
  try:
410
    result = xref(typ, rawtext, text, lineno, inliner,
411
                  options=options, content=content)
412
  except ReSTError, err:
413
    msg = inliner.reporter.error(str(err), line=lineno)
414
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
415

    
416
  assert ht.TBool(xref.converted)
417

    
418
  # Return if the conversion was successful (i.e. the man page was known and
419
  # conversion was enabled)
420
  if xref.converted:
421
    return result
422

    
423
  # Fallback if man page links are disabled or an unknown page is referenced
424
  return orig_manpage_role(typ, rawtext, text, lineno, inliner,
425
                           options=options, content=content)
426

    
427

    
428
def setup(app):
429
  """Sphinx extension callback.
430

431
  """
432
  # TODO: Implement Sphinx directive for query fields
433
  app.add_directive("opcode_params", OpcodeParams)
434
  app.add_directive("opcode_result", OpcodeResult)
435
  app.add_directive("pyassert", PythonAssert)
436
  app.add_role("pyeval", PythonEvalRole)
437

    
438
  app.add_config_value("enable_manpages", False, True)
439
  app.add_role("manpage", _ManPageRole)