Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ a4338da2

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
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
from ganeti import constants
36
from ganeti import compat
37
from ganeti import errors
38
from ganeti import utils
39
from ganeti import opcodes
40
from ganeti import ht
41
from ganeti import rapi
42

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

    
45

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

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

    
52

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

    
56

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

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

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

    
66

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

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

74
  """
75
  result = {}
76

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

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

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

    
85
  return result
86

    
87

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

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

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

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

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

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

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

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

    
132

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

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

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

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

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

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

    
161
    return []
162

    
163

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

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

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

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

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

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

    
187
  return ([node], [])
188

    
189

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

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

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

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

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

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

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

    
219
    return []
220

    
221

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

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

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

    
234

    
235
# TODO: Implement Sphinx directive for query fields
236

    
237

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

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