Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ eb62069e

History | View | Annotate | Download (6.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
import operator
27
from cStringIO import StringIO
28

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

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

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

    
44
import ganeti.rapi.rlib2 # pylint: disable-msg=W0611
45

    
46

    
47
COMMON_PARAM_NAMES = map(compat.fst, opcodes.OpCode.OP_PARAMS)
48

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

    
53

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

    
57

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

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

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

    
67

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

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

75
  """
76
  result = {}
77

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

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

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

    
86
  return result
87

    
88

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

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

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

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

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

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

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

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

    
133

    
134
class OpcodeParams(sphinx.util.compat.Directive):
135
  """Custom directive for opcode parameters.
136

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

139
  """
140
  has_content = False
141
  required_arguments = 1
142
  optional_arguments = 0
143
  final_argument_whitespace = False
144
  option_spec = dict(include=_SplitOption, exclude=_SplitOption,
145
                     alias=_ParseAlias)
146

    
147
  def run(self):
148
    op_id = self.arguments[0]
149
    include = self.options.get("include", None)
150
    exclude = self.options.get("exclude", None)
151
    alias = self.options.get("alias", {})
152

    
153
    tab_width = 2
154
    path = op_id
155
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
156

    
157
    # Inject into state machine
158
    include_lines = docutils.statemachine.string2lines(include_text, tab_width,
159
                                                       convert_whitespace=1)
160
    self.state_machine.insert_input(include_lines, path)
161

    
162
    return []
163

    
164

    
165
def PythonEvalRole(role, rawtext, text, lineno, inliner,
166
                   options={}, content=[]):
167
  """Custom role to evaluate Python expressions.
168

169
  The expression's result is included as a literal.
170

171
  """
172
  # pylint: disable-msg=W0102,W0613,W0142
173
  # W0102: Dangerous default value as argument
174
  # W0142: Used * or ** magic
175
  # W0613: Unused argument
176

    
177
  code = docutils.utils.unescape(text, restore_backslashes=True)
178

    
179
  try:
180
    result = eval(code, EVAL_NS)
181
  except Exception, err: # pylint: disable-msg=W0703
182
    msg = inliner.reporter.error("Failed to evaluate %r: %s" % (code, err),
183
                                 line=lineno)
184
    return ([inliner.problematic(rawtext, rawtext, msg)], [msg])
185

    
186
  node = docutils.nodes.literal("", unicode(result), **options)
187

    
188
  return ([node], [])
189

    
190

    
191
class PythonAssert(sphinx.util.compat.Directive):
192
  """Custom directive for writing assertions.
193

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

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

    
203
  def run(self):
204
    # Handle combinations of Sphinx and docutils not providing the wanted method
205
    if hasattr(self, "assert_has_content"):
206
      self.assert_has_content()
207
    else:
208
      assert self.content
209

    
210
    code = "\n".join(self.content)
211

    
212
    try:
213
      result = eval(code, EVAL_NS)
214
    except Exception, err:
215
      raise self.error("Failed to evaluate %r: %s" % (code, err))
216

    
217
    if not result:
218
      raise self.error("Assertion failed: %s" % (code, ))
219

    
220
    return []
221

    
222

    
223
def BuildQueryFields(fields):
224
  """Build query fields documentation.
225

226
  @type fields: dict (field name as key, field details as value)
227

228
  """
229
  for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
230
                                             key=compat.fst):
231
    assert len(fdef.doc.splitlines()) == 1
232
    yield "``%s``" % fdef.name
233
    yield "  %s" % fdef.doc
234

    
235

    
236
# TODO: Implement Sphinx directive for query fields
237

    
238

    
239
def setup(app):
240
  """Sphinx extension callback.
241

242
  """
243
  app.add_directive("opcode_params", OpcodeParams)
244
  app.add_directive("pyassert", PythonAssert)
245
  app.add_role("pyeval", PythonEvalRole)