Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ b8291e00

History | View | Annotate | Download (8.2 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
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
from ganeti import luxi
45

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

    
48

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

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

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

    
58
  return names
59

    
60

    
61
COMMON_PARAM_NAMES = _GetCommonParamNames()
62

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

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

    
76

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

    
80

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

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

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

    
90

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

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

98
  """
99
  result = {}
100

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

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

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

    
109
  return result
110

    
111

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

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

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

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

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

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

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

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

    
156

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

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

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

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

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

    
171
  return "``%s``" % result_fn
172

    
173

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

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

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

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

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

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

    
202
    return []
203

    
204

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

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

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

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

    
219
    tab_width = 2
220
    path = op_id
221
    include_text = _BuildOpcodeResult(op_id)
222

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

    
228
    return []
229

    
230

    
231
def PythonEvalRole(role, rawtext, text, lineno, inliner,
232
                   options={}, content=[]):
233
  """Custom role to evaluate Python expressions.
234

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

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

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

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

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

    
254
  return ([node], [])
255

    
256

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

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

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

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

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

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

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

    
286
    return []
287

    
288

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

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

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

    
300

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

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

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

    
312

    
313
# TODO: Implement Sphinx directive for query fields
314

    
315

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

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