Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ 3ac3f5e4

History | View | Annotate | Download (7.9 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
# Constants documentation for man pages
55
CV_ECODES_DOC = "ecodes"
56
# We don't care about the leak of variables _, name and doc here.
57
# pylint: disable=W0621
58
CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
59
DOCUMENTED_CONSTANTS = {
60
  CV_ECODES_DOC: CV_ECODES_DOC_LIST,
61
  }
62

    
63

    
64
class OpcodeError(sphinx.errors.SphinxError):
65
  category = "Opcode error"
66

    
67

    
68
def _SplitOption(text):
69
  """Split simple option list.
70

71
  @type text: string
72
  @param text: Options, e.g. "foo, bar, baz"
73

74
  """
75
  return [i.strip(",").strip() for i in text.split()]
76

    
77

    
78
def _ParseAlias(text):
79
  """Parse simple assignment option.
80

81
  @type text: string
82
  @param text: Assignments, e.g. "foo=bar, hello=world"
83
  @rtype: dict
84

85
  """
86
  result = {}
87

    
88
  for part in _SplitOption(text):
89
    if "=" not in part:
90
      raise OpcodeError("Invalid option format, missing equal sign")
91

    
92
    (name, value) = part.split("=", 1)
93

    
94
    result[name.strip()] = value.strip()
95

    
96
  return result
97

    
98

    
99
def _BuildOpcodeParams(op_id, include, exclude, alias):
100
  """Build opcode parameter documentation.
101

102
  @type op_id: string
103
  @param op_id: Opcode ID
104

105
  """
106
  op_cls = opcodes.OP_MAPPING[op_id]
107

    
108
  params_with_alias = \
109
    utils.NiceSort([(alias.get(name, name), name, default, test, doc)
110
                    for (name, default, test, doc) in op_cls.GetAllParams()],
111
                   key=compat.fst)
112

    
113
  for (rapi_name, name, default, test, doc) in params_with_alias:
114
    # Hide common parameters if not explicitely included
115
    if (name in COMMON_PARAM_NAMES and
116
        (not include or name not in include)):
117
      continue
118
    if exclude is not None and name in exclude:
119
      continue
120
    if include is not None and name not in include:
121
      continue
122

    
123
    has_default = default is not ht.NoDefault
124
    has_test = not (test is None or test is ht.NoType)
125

    
126
    buf = StringIO()
127
    buf.write("``%s``" % rapi_name)
128
    if has_default or has_test:
129
      buf.write(" (")
130
      if has_default:
131
        buf.write("defaults to ``%s``" % default)
132
        if has_test:
133
          buf.write(", ")
134
      if has_test:
135
        buf.write("must be ``%s``" % test)
136
      buf.write(")")
137
    yield buf.getvalue()
138

    
139
    # Add text
140
    for line in doc.splitlines():
141
      yield "  %s" % line
142

    
143

    
144
def _BuildOpcodeResult(op_id):
145
  """Build opcode result documentation.
146

147
  @type op_id: string
148
  @param op_id: Opcode ID
149

150
  """
151
  op_cls = opcodes.OP_MAPPING[op_id]
152

    
153
  result_fn = getattr(op_cls, "OP_RESULT", None)
154

    
155
  if not result_fn:
156
    raise OpcodeError("Opcode '%s' has no result description" % op_id)
157

    
158
  return "``%s``" % result_fn
159

    
160

    
161
class OpcodeParams(s_compat.Directive):
162
  """Custom directive for opcode parameters.
163

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

166
  """
167
  has_content = False
168
  required_arguments = 1
169
  optional_arguments = 0
170
  final_argument_whitespace = False
171
  option_spec = dict(include=_SplitOption, exclude=_SplitOption,
172
                     alias=_ParseAlias)
173

    
174
  def run(self):
175
    op_id = self.arguments[0]
176
    include = self.options.get("include", None)
177
    exclude = self.options.get("exclude", None)
178
    alias = self.options.get("alias", {})
179

    
180
    tab_width = 2
181
    path = op_id
182
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
183

    
184
    # Inject into state machine
185
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
186
                                                       convert_whitespace=1)
187
    self.state_machine.insert_input(include_lines, path)
188

    
189
    return []
190

    
191

    
192
class OpcodeResult(s_compat.Directive):
193
  """Custom directive for opcode result.
194

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

197
  """
198
  has_content = False
199
  required_arguments = 1
200
  optional_arguments = 0
201
  final_argument_whitespace = False
202

    
203
  def run(self):
204
    op_id = self.arguments[0]
205

    
206
    tab_width = 2
207
    path = op_id
208
    include_text = _BuildOpcodeResult(op_id)
209

    
210
    # Inject into state machine
211
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
212
                                                       convert_whitespace=1)
213
    self.state_machine.insert_input(include_lines, path)
214

    
215
    return []
216

    
217

    
218
def PythonEvalRole(role, rawtext, text, lineno, inliner,
219
                   options={}, content=[]):
220
  """Custom role to evaluate Python expressions.
221

222
  The expression's result is included as a literal.
223

224
  """
225
  # pylint: disable=W0102,W0613,W0142
226
  # W0102: Dangerous default value as argument
227
  # W0142: Used * or ** magic
228
  # W0613: Unused argument
229

    
230
  code = docutils.utils.unescape(text, restore_backslashes=True)
231

    
232
  try:
233
    result = eval(code, EVAL_NS)
234
  except Exception, err: # pylint: disable=W0703
235
    msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
236
                                 line=lineno)
237
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
238

    
239
  node = docutils.nodes.literal("", unicode(result), **options)
240

    
241
  return ([node], [])
242

    
243

    
244
class PythonAssert(s_compat.Directive):
245
  """Custom directive for writing assertions.
246

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

250
  """
251
  has_content = True
252
  required_arguments = 0
253
  optional_arguments = 0
254
  final_argument_whitespace = False
255

    
256
  def run(self):
257
    # Handle combinations of Sphinx and docutils not providing the wanted method
258
    if hasattr(self, "assert_has_content"):
259
      self.assert_has_content()
260
    else:
261
      assert self.content
262

    
263
    code = "\n".join(self.content)
264

    
265
    try:
266
      result = eval(code, EVAL_NS)
267
    except Exception, err:
268
      raise self.error("Failed to evaluate %r: %s" % (code, err))
269

    
270
    if not result:
271
      raise self.error("Assertion failed: %s" % (code, ))
272

    
273
    return []
274

    
275

    
276
def BuildQueryFields(fields):
277
  """Build query fields documentation.
278

279
  @type fields: dict (field name as key, field details as value)
280

281
  """
282
  defs = [(fdef.name, fdef.doc)
283
           for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
284
                                                      key=compat.fst)]
285
  yield BuildValuesDoc(defs)
286

    
287

    
288
def BuildValuesDoc(values):
289
  """Builds documentation for a list of values
290

291
  @type values: list of tuples in the form (value, documentation)
292

293
  """
294
  for name, doc in values:
295
    assert len(doc.splitlines()) == 1
296
    yield "``%s``" % name
297
    yield "  %s" % doc
298

    
299

    
300
# TODO: Implement Sphinx directive for query fields
301

    
302

    
303
def setup(app):
304
  """Sphinx extension callback.
305

306
  """
307
  app.add_directive("opcode_params", OpcodeParams)
308
  app.add_directive("opcode_result", OpcodeResult)
309
  app.add_directive("pyassert", PythonAssert)
310
  app.add_role("pyeval", PythonEvalRole)