Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ c83c0410

History | View | Annotate | Download (11.7 kB)

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

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

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

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

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

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

    
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))
384

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

    
388
    if self.converted:
389
      # Create link to known man page
390
      return (title, "man-%s" % name)
391
    else:
392
      # No changes
393
      return (title, target)
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)