Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ b459a848

History | View | Annotate | Download (7.4 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
COMMON_PARAM_NAMES = map(compat.fst, opcodes.OpCode.OP_PARAMS)
49

    
50
#: Namespace for evaluating expressions
51
EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
52
               rlib2=rapi.rlib2)
53

    
54

    
55
class OpcodeError(sphinx.errors.SphinxError):
56
  category = "Opcode error"
57

    
58

    
59
def _SplitOption(text):
60
  """Split simple option list.
61

62
  @type text: string
63
  @param text: Options, e.g. "foo, bar, baz"
64

65
  """
66
  return [i.strip(",").strip() for i in text.split()]
67

    
68

    
69
def _ParseAlias(text):
70
  """Parse simple assignment option.
71

72
  @type text: string
73
  @param text: Assignments, e.g. "foo=bar, hello=world"
74
  @rtype: dict
75

76
  """
77
  result = {}
78

    
79
  for part in _SplitOption(text):
80
    if "=" not in part:
81
      raise OpcodeError("Invalid option format, missing equal sign")
82

    
83
    (name, value) = part.split("=", 1)
84

    
85
    result[name.strip()] = value.strip()
86

    
87
  return result
88

    
89

    
90
def _BuildOpcodeParams(op_id, include, exclude, alias):
91
  """Build opcode parameter documentation.
92

93
  @type op_id: string
94
  @param op_id: Opcode ID
95

96
  """
97
  op_cls = opcodes.OP_MAPPING[op_id]
98

    
99
  params_with_alias = \
100
    utils.NiceSort([(alias.get(name, name), name, default, test, doc)
101
                    for (name, default, test, doc) in op_cls.GetAllParams()],
102
                   key=compat.fst)
103

    
104
  for (rapi_name, name, default, test, doc) in params_with_alias:
105
    # Hide common parameters if not explicitely included
106
    if (name in COMMON_PARAM_NAMES and
107
        (not include or name not in include)):
108
      continue
109
    if exclude is not None and name in exclude:
110
      continue
111
    if include is not None and name not in include:
112
      continue
113

    
114
    has_default = default is not ht.NoDefault
115
    has_test = not (test is None or test is ht.NoType)
116

    
117
    buf = StringIO()
118
    buf.write("``%s``" % rapi_name)
119
    if has_default or has_test:
120
      buf.write(" (")
121
      if has_default:
122
        buf.write("defaults to ``%s``" % default)
123
        if has_test:
124
          buf.write(", ")
125
      if has_test:
126
        buf.write("must be ``%s``" % test)
127
      buf.write(")")
128
    yield buf.getvalue()
129

    
130
    # Add text
131
    for line in doc.splitlines():
132
      yield "  %s" % line
133

    
134

    
135
def _BuildOpcodeResult(op_id):
136
  """Build opcode result 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
  result_fn = getattr(op_cls, "OP_RESULT", None)
145

    
146
  if not result_fn:
147
    raise OpcodeError("Opcode '%s' has no result description" % op_id)
148

    
149
  return "``%s``" % result_fn
150

    
151

    
152
class OpcodeParams(s_compat.Directive):
153
  """Custom directive for opcode parameters.
154

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

157
  """
158
  has_content = False
159
  required_arguments = 1
160
  optional_arguments = 0
161
  final_argument_whitespace = False
162
  option_spec = dict(include=_SplitOption, exclude=_SplitOption,
163
                     alias=_ParseAlias)
164

    
165
  def run(self):
166
    op_id = self.arguments[0]
167
    include = self.options.get("include", None)
168
    exclude = self.options.get("exclude", None)
169
    alias = self.options.get("alias", {})
170

    
171
    tab_width = 2
172
    path = op_id
173
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
174

    
175
    # Inject into state machine
176
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
177
                                                       convert_whitespace=1)
178
    self.state_machine.insert_input(include_lines, path)
179

    
180
    return []
181

    
182

    
183
class OpcodeResult(s_compat.Directive):
184
  """Custom directive for opcode result.
185

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

188
  """
189
  has_content = False
190
  required_arguments = 1
191
  optional_arguments = 0
192
  final_argument_whitespace = False
193

    
194
  def run(self):
195
    op_id = self.arguments[0]
196

    
197
    tab_width = 2
198
    path = op_id
199
    include_text = _BuildOpcodeResult(op_id)
200

    
201
    # Inject into state machine
202
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
203
                                                       convert_whitespace=1)
204
    self.state_machine.insert_input(include_lines, path)
205

    
206
    return []
207

    
208

    
209
def PythonEvalRole(role, rawtext, text, lineno, inliner,
210
                   options={}, content=[]):
211
  """Custom role to evaluate Python expressions.
212

213
  The expression's result is included as a literal.
214

215
  """
216
  # pylint: disable=W0102,W0613,W0142
217
  # W0102: Dangerous default value as argument
218
  # W0142: Used * or ** magic
219
  # W0613: Unused argument
220

    
221
  code = docutils.utils.unescape(text, restore_backslashes=True)
222

    
223
  try:
224
    result = eval(code, EVAL_NS)
225
  except Exception, err: # pylint: disable=W0703
226
    msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
227
                                 line=lineno)
228
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
229

    
230
  node = docutils.nodes.literal("", unicode(result), **options)
231

    
232
  return ([node], [])
233

    
234

    
235
class PythonAssert(s_compat.Directive):
236
  """Custom directive for writing assertions.
237

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

241
  """
242
  has_content = True
243
  required_arguments = 0
244
  optional_arguments = 0
245
  final_argument_whitespace = False
246

    
247
  def run(self):
248
    # Handle combinations of Sphinx and docutils not providing the wanted method
249
    if hasattr(self, "assert_has_content"):
250
      self.assert_has_content()
251
    else:
252
      assert self.content
253

    
254
    code = "\n".join(self.content)
255

    
256
    try:
257
      result = eval(code, EVAL_NS)
258
    except Exception, err:
259
      raise self.error("Failed to evaluate %r: %s" % (code, err))
260

    
261
    if not result:
262
      raise self.error("Assertion failed: %s" % (code, ))
263

    
264
    return []
265

    
266

    
267
def BuildQueryFields(fields):
268
  """Build query fields documentation.
269

270
  @type fields: dict (field name as key, field details as value)
271

272
  """
273
  for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
274
                                             key=compat.fst):
275
    assert len(fdef.doc.splitlines()) == 1
276
    yield "``%s``" % fdef.name
277
    yield "  %s" % fdef.doc
278

    
279

    
280
# TODO: Implement Sphinx directive for query fields
281

    
282

    
283
def setup(app):
284
  """Sphinx extension callback.
285

286
  """
287
  app.add_directive("opcode_params", OpcodeParams)
288
  app.add_directive("opcode_result", OpcodeResult)
289
  app.add_directive("pyassert", PythonAssert)
290
  app.add_role("pyeval", PythonEvalRole)