Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ a19a6326

History | View | Annotate | Download (8.2 kB)

1
#
2
#
3

    
4
# Copyright (C) 2011 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
from cStringIO import StringIO
27

    
28
import docutils.statemachine
29
import docutils.nodes
30
import docutils.utils
31

    
32
import sphinx.errors
33
import sphinx.util.compat
34

    
35
s_compat = sphinx.util.compat
36

    
37
from ganeti import constants
38
from ganeti import compat
39
from ganeti import errors
40
from ganeti import utils
41
from ganeti import opcodes
42
from ganeti import ht
43
from ganeti import rapi
44

    
45
import ganeti.rapi.rlib2 # pylint: disable=W0611
46

    
47

    
48
def _GetCommonParamNames():
49
  """Builds a list of parameters common to all opcodes.
50

51
  """
52
  names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
53

    
54
  # The "depends" attribute should be listed
55
  names.remove(opcodes.DEPEND_ATTR)
56

    
57
  return names
58

    
59

    
60
COMMON_PARAM_NAMES = _GetCommonParamNames()
61

    
62
#: Namespace for evaluating expressions
63
EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
64
               rlib2=rapi.rlib2)
65

    
66
# Constants documentation for man pages
67
CV_ECODES_DOC = "ecodes"
68
# We don't care about the leak of variables _, name and doc here.
69
# pylint: disable=W0621
70
CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
71
DOCUMENTED_CONSTANTS = {
72
  CV_ECODES_DOC: CV_ECODES_DOC_LIST,
73
  }
74

    
75

    
76
class OpcodeError(sphinx.errors.SphinxError):
77
  category = "Opcode error"
78

    
79

    
80
def _SplitOption(text):
81
  """Split simple option list.
82

83
  @type text: string
84
  @param text: Options, e.g. "foo, bar, baz"
85

86
  """
87
  return [i.strip(",").strip() for i in text.split()]
88

    
89

    
90
def _ParseAlias(text):
91
  """Parse simple assignment option.
92

93
  @type text: string
94
  @param text: Assignments, e.g. "foo=bar, hello=world"
95
  @rtype: dict
96

97
  """
98
  result = {}
99

    
100
  for part in _SplitOption(text):
101
    if "=" not in part:
102
      raise OpcodeError("Invalid option format, missing equal sign")
103

    
104
    (name, value) = part.split("=", 1)
105

    
106
    result[name.strip()] = value.strip()
107

    
108
  return result
109

    
110

    
111
def _BuildOpcodeParams(op_id, include, exclude, alias):
112
  """Build opcode parameter documentation.
113

114
  @type op_id: string
115
  @param op_id: Opcode ID
116

117
  """
118
  op_cls = opcodes.OP_MAPPING[op_id]
119

    
120
  params_with_alias = \
121
    utils.NiceSort([(alias.get(name, name), name, default, test, doc)
122
                    for (name, default, test, doc) in op_cls.GetAllParams()],
123
                   key=compat.fst)
124

    
125
  for (rapi_name, name, default, test, doc) in params_with_alias:
126
    # Hide common parameters if not explicitely included
127
    if (name in COMMON_PARAM_NAMES and
128
        (not include or name not in include)):
129
      continue
130
    if exclude is not None and name in exclude:
131
      continue
132
    if include is not None and name not in include:
133
      continue
134

    
135
    has_default = default is not ht.NoDefault
136
    has_test = not (test is None or test is ht.NoType)
137

    
138
    buf = StringIO()
139
    buf.write("``%s``" % rapi_name)
140
    if has_default or has_test:
141
      buf.write(" (")
142
      if has_default:
143
        buf.write("defaults to ``%s``" % default)
144
        if has_test:
145
          buf.write(", ")
146
      if has_test:
147
        buf.write("must be ``%s``" % test)
148
      buf.write(")")
149
    yield buf.getvalue()
150

    
151
    # Add text
152
    for line in doc.splitlines():
153
      yield "  %s" % line
154

    
155

    
156
def _BuildOpcodeResult(op_id):
157
  """Build opcode result documentation.
158

159
  @type op_id: string
160
  @param op_id: Opcode ID
161

162
  """
163
  op_cls = opcodes.OP_MAPPING[op_id]
164

    
165
  result_fn = getattr(op_cls, "OP_RESULT", None)
166

    
167
  if not result_fn:
168
    raise OpcodeError("Opcode '%s' has no result description" % op_id)
169

    
170
  return "``%s``" % result_fn
171

    
172

    
173
class OpcodeParams(s_compat.Directive):
174
  """Custom directive for opcode parameters.
175

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

178
  """
179
  has_content = False
180
  required_arguments = 1
181
  optional_arguments = 0
182
  final_argument_whitespace = False
183
  option_spec = dict(include=_SplitOption, exclude=_SplitOption,
184
                     alias=_ParseAlias)
185

    
186
  def run(self):
187
    op_id = self.arguments[0]
188
    include = self.options.get("include", None)
189
    exclude = self.options.get("exclude", None)
190
    alias = self.options.get("alias", {})
191

    
192
    tab_width = 2
193
    path = op_id
194
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
195

    
196
    # Inject into state machine
197
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
198
                                                       convert_whitespace=1)
199
    self.state_machine.insert_input(include_lines, path)
200

    
201
    return []
202

    
203

    
204
class OpcodeResult(s_compat.Directive):
205
  """Custom directive for opcode result.
206

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

209
  """
210
  has_content = False
211
  required_arguments = 1
212
  optional_arguments = 0
213
  final_argument_whitespace = False
214

    
215
  def run(self):
216
    op_id = self.arguments[0]
217

    
218
    tab_width = 2
219
    path = op_id
220
    include_text = _BuildOpcodeResult(op_id)
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
def PythonEvalRole(role, rawtext, text, lineno, inliner,
231
                   options={}, content=[]):
232
  """Custom role to evaluate Python expressions.
233

234
  The expression's result is included as a literal.
235

236
  """
237
  # pylint: disable=W0102,W0613,W0142
238
  # W0102: Dangerous default value as argument
239
  # W0142: Used * or ** magic
240
  # W0613: Unused argument
241

    
242
  code = docutils.utils.unescape(text, restore_backslashes=True)
243

    
244
  try:
245
    result = eval(code, EVAL_NS)
246
  except Exception, err: # pylint: disable=W0703
247
    msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
248
                                 line=lineno)
249
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
250

    
251
  node = docutils.nodes.literal("", unicode(result), **options)
252

    
253
  return ([node], [])
254

    
255

    
256
class PythonAssert(s_compat.Directive):
257
  """Custom directive for writing assertions.
258

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

262
  """
263
  has_content = True
264
  required_arguments = 0
265
  optional_arguments = 0
266
  final_argument_whitespace = False
267

    
268
  def run(self):
269
    # Handle combinations of Sphinx and docutils not providing the wanted method
270
    if hasattr(self, "assert_has_content"):
271
      self.assert_has_content()
272
    else:
273
      assert self.content
274

    
275
    code = "\n".join(self.content)
276

    
277
    try:
278
      result = eval(code, EVAL_NS)
279
    except Exception, err:
280
      raise self.error("Failed to evaluate %r: %s" % (code, err))
281

    
282
    if not result:
283
      raise self.error("Assertion failed: %s" % (code, ))
284

    
285
    return []
286

    
287

    
288
def BuildQueryFields(fields):
289
  """Build query fields documentation.
290

291
  @type fields: dict (field name as key, field details as value)
292

293
  """
294
  defs = [(fdef.name, fdef.doc)
295
           for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
296
                                                      key=compat.fst)]
297
  yield BuildValuesDoc(defs)
298

    
299

    
300
def BuildValuesDoc(values):
301
  """Builds documentation for a list of values
302

303
  @type values: list of tuples in the form (value, documentation)
304

305
  """
306
  for name, doc in values:
307
    assert len(doc.splitlines()) == 1
308
    yield "``%s``" % name
309
    yield "  %s" % doc
310

    
311

    
312
# TODO: Implement Sphinx directive for query fields
313

    
314

    
315
def setup(app):
316
  """Sphinx extension callback.
317

318
  """
319
  app.add_directive("opcode_params", OpcodeParams)
320
  app.add_directive("opcode_result", OpcodeResult)
321
  app.add_directive("pyassert", PythonAssert)
322
  app.add_role("pyeval", PythonEvalRole)