Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ c6793656

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
_TAB_WIDTH = 2
67

    
68

    
69
class ReSTError(Exception):
70
  """Custom class for generating errors in Sphinx.
71

72
  """
73

    
74

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

78
  """
79
  names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
80

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

    
84
  return names
85

    
86

    
87
COMMON_PARAM_NAMES = _GetCommonParamNames()
88

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

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

    
102

    
103
class OpcodeError(sphinx.errors.SphinxError):
104
  category = "Opcode error"
105

    
106

    
107
def _SplitOption(text):
108
  """Split simple option list.
109

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

113
  """
114
  return [i.strip(",").strip() for i in text.split()]
115

    
116

    
117
def _ParseAlias(text):
118
  """Parse simple assignment option.
119

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

124
  """
125
  result = {}
126

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

    
131
    (name, value) = part.split("=", 1)
132

    
133
    result[name.strip()] = value.strip()
134

    
135
  return result
136

    
137

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

141
  @type op_id: string
142
  @param op_id: Opcode ID
143

144
  """
145
  op_cls = opcodes.OP_MAPPING[op_id]
146

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

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

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

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

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

    
182

    
183
def _BuildOpcodeResult(op_id):
184
  """Build opcode result documentation.
185

186
  @type op_id: string
187
  @param op_id: Opcode ID
188

189
  """
190
  op_cls = opcodes.OP_MAPPING[op_id]
191

    
192
  result_fn = getattr(op_cls, "OP_RESULT", None)
193

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

    
197
  return "``%s``" % result_fn
198

    
199

    
200
class OpcodeParams(s_compat.Directive):
201
  """Custom directive for opcode parameters.
202

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

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

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

    
219
    path = op_id
220
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
221

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

    
227
    return []
228

    
229

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

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

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

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

    
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)