Statistics
| Branch: | Tag: | Revision:

root / lib / build / sphinx_ext.py @ 95eb4188

History | View | Annotate | Download (6.3 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

    
43

    
44
COMMON_PARAM_NAMES = map(operator.itemgetter(0), opcodes.OpCode.OP_PARAMS)
45

    
46
#: Namespace for evaluating expressions
47
EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors)
48

    
49

    
50
class OpcodeError(sphinx.errors.SphinxError):
51
  category = "Opcode error"
52

    
53

    
54
def _SplitOption(text):
55
  """Split simple option list.
56

57
  @type text: string
58
  @param text: Options, e.g. "foo, bar, baz"
59

60
  """
61
  return [i.strip(",").strip() for i in text.split()]
62

    
63

    
64
def _ParseAlias(text):
65
  """Parse simple assignment option.
66

67
  @type text: string
68
  @param text: Assignments, e.g. "foo=bar, hello=world"
69
  @rtype: dict
70

71
  """
72
  result = {}
73

    
74
  for part in _SplitOption(text):
75
    if "=" not in part:
76
      raise OpcodeError("Invalid option format, missing equal sign")
77

    
78
    (name, value) = part.split("=", 1)
79

    
80
    result[name.strip()] = value.strip()
81

    
82
  return result
83

    
84

    
85
def _BuildOpcodeParams(op_id, include, exclude, alias):
86
  """Build opcode parameter documentation.
87

88
  @type op_id: string
89
  @param op_id: Opcode ID
90

91
  """
92
  op_cls = opcodes.OP_MAPPING[op_id]
93

    
94
  params_with_alias = \
95
    utils.NiceSort([(alias.get(name, name), name, default, test, doc)
96
                    for (name, default, test, doc) in op_cls.GetAllParams()],
97
                   key=operator.itemgetter(0))
98

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

    
109
    has_default = default is not ht.NoDefault
110
    has_test = not (test is None or test is ht.NoType)
111

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

    
125
    # Add text
126
    for line in doc.splitlines():
127
      yield "  %s" % line
128

    
129

    
130
class OpcodeParams(sphinx.util.compat.Directive):
131
  """Custom directive for opcode parameters.
132

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

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

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

    
149
    tab_width = 2
150
    path = op_id
151
    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
152

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

    
158
    return []
159

    
160

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

165
  The expression's result is included as a literal.
166

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

    
173
  code = docutils.utils.unescape(text, restore_backslashes=True)
174

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

    
182
  node = docutils.nodes.literal("", unicode(result), **options)
183

    
184
  return ([node], [])
185

    
186

    
187
class PythonAssert(sphinx.util.compat.Directive):
188
  """Custom directive for writing assertions.
189

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

193
  """
194
  has_content = True
195
  required_arguments = 0
196
  optional_arguments = 0
197
  final_argument_whitespace = False
198

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

    
206
    code = "\n".join(self.content)
207

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

    
213
    if not result:
214
      raise self.error("Assertion failed: %s" % (code, ))
215

    
216
    return []
217

    
218

    
219
def BuildQueryFields(fields):
220
  """Build query fields documentation.
221

222
  @type fields: dict (field name as key, field details as value)
223

224
  """
225
  for (_, (fdef, _, _)) in utils.NiceSort(fields.items(),
226
                                          key=operator.itemgetter(0)):
227
    assert len(fdef.doc.splitlines()) == 1
228
    yield "``%s``" % fdef.name
229
    yield "  %s" % fdef.doc
230

    
231

    
232
# TODO: Implement Sphinx directive for query fields
233

    
234

    
235
def setup(app):
236
  """Sphinx extension callback.
237

238
  """
239
  app.add_directive("opcode_params", OpcodeParams)
240
  app.add_directive("pyassert", PythonAssert)
241
  app.add_role("pyeval", PythonEvalRole)