convert-constants 10.7 KB
Newer Older
1
2
3
#!/usr/bin/python
#

4
# Copyright (C) 2011, 2012 Google Inc.
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

"""Script for converting Python constants to Haskell code fragments.

"""

import re
26
import types
27

28
from ganeti import _autoconf
29
from ganeti import compat
30
from ganeti import constants
31
from ganeti import errors
32
from ganeti import luxi
33
from ganeti import opcodes
34
from ganeti import qlang
35
from ganeti import jstore
36

37

38
#: Constant name regex
39
CONSTANT_RE = re.compile("^[A-Z][A-Z0-9_-]+$")
40

41
42
43
#: Private name regex
PRIVATE_RE = re.compile("^__.+__$")

44
45
46
#: The type of regex objects
RE_TYPE = type(CONSTANT_RE)

47
48
49
50
51
52
#: Keys which do not declare a value (manually maintained). By adding
# values here, we can make more lists use the actual names; otherwise
# we'll have (e.g.) both DEFAULT_ENABLED_HYPERVISOR and HT_XEN_PVM
# declare the same value, and thus the list of valid hypervisors will
# have strings instead of easily looked-up names.
IGNORED_DECL_NAMES = ["DEFAULT_ENABLED_HYPERVISOR"]
53

54

55
56
57
58
def NameRules(name):
  """Converts the upper-cased Python name to Haskell camelCase.

  """
59
  name = name.replace("-", "_")
60
61
62
63
64
65
66
67
68
69
70
71
72
  elems = name.split("_")
  return elems[0].lower() + "".join(e.capitalize() for e in elems[1:])


def StringValueRules(value):
  """Converts a string value from Python to Haskell.

  """
  value = value.encode("string_escape") # escapes backslashes
  value = value.replace("\"", "\\\"")
  return value


73
74
75
76
77
78
79
def DictKeyName(dict_name, key_name):
  """Converts a dict plus key name to a full name.

  """
  return"%s_%s" % (dict_name, str(key_name).upper())


80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def HaskellTypeVal(value):
  """Returns the Haskell type and value for a Python value.

  Note that this only work for 'plain' Python types.

  @returns: (string, string) or None, if we can't determine the type.

  """
  if isinstance(value, basestring):
    return ("String", "\"%s\"" % StringValueRules(value))
  elif isinstance(value, int):
    return ("Int", "%d" % value)
  elif isinstance(value, long):
    return ("Integer", "%d" % value)
  elif isinstance(value, float):
    return ("Double", "%f" % value)
  else:
    return None


100
101
102
103
104
105
106
107
108
109
110
111
def IdentifyOrigin(all_items, value):
  """Tries to identify a constant name from a constant's value.

  This uses a simple algorithm: is there a constant (and only one)
  with the same value? If so, then it returns that constants' name.

  @note: it is recommended to use this only for tuples/lists/sets, and
      not for individual (top-level) values
  @param all_items: a dictionary of name/values for the current module
  @param value: the value for which we try to find an origin

  """
112
113
  found = [name for (name, v) in all_items.items()
           if v is value and name not in IGNORED_DECL_NAMES]
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
  if len(found) == 1:
    return found[0]
  else:
    return None


def FormatListElems(all_items, pfx_name, ovals, tvals):
  """Formats a list's elements.

  This formats the elements as either values or, if we find all
  origins, as names.

  @param all_items: a dictionary of name/values for the current module
  @param pfx_name: the prefix name currently used
  @param ovals: the list of actual (Python) values
  @param tvals: the list of values we want to format in the Haskell form

  """
  origins = [IdentifyOrigin(all_items, v) for v in ovals]
  if compat.all(x is not None for x in origins):
    values = [NameRules(pfx_name + origin) for origin in origins]
  else:
    values = tvals
  return ", ".join(values)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def FormatDict(all_items, pfx_name, py_name, hs_name, mydict):
  """Converts a dictionary to a Haskell association list ([(k, v)]),
  if possible.

  @param all_items: a dictionary of name/values for the current module
  @param pfx_name: the prefix name currently used
  @param py_name: the Python name
  @param hs_name: the Haskell name
  @param mydict: a dictonary, unknown yet if homogenous or not

  """
  # need this for ordering
  orig_list = mydict.items()
  list_form = [(HaskellTypeVal(k), HaskellTypeVal(v)) for k, v in orig_list]
  if compat.any(v is None or k is None for k, v in list_form):
    # type not known
    return []
  all_keys = [k for k, _ in list_form]
  all_vals = [v for _, v in list_form]
  key_types = set(k[0] for k in all_keys)
  val_types = set(v[0] for v in all_vals)
  if not(len(key_types) == 1 and len(val_types) == 1):
    # multiple types
    return []
  # record the key and value Haskell types
  key_type = key_types.pop()
  val_type = val_types.pop()

  # now try to find names for the keys, instead of raw values
  key_origins = [IdentifyOrigin(all_items, k) for k, _ in orig_list]
  if compat.all(x is not None for x in key_origins):
    key_v = [NameRules(pfx_name + origin) for origin in key_origins]
  else:
    key_v = [k[1] for k in all_keys]
  # ... and for values
  val_origins = [IdentifyOrigin(all_items, v) for _, v in orig_list]
  if compat.all(x is not None for x in val_origins):
    val_v = [NameRules(pfx_name + origin) for origin in val_origins]
  else:
    val_v = [v[1] for v in all_vals]

  # finally generate the output
  kv_pairs = ["(%s, %s)" % (k, v) for k, v in zip(key_v, val_v)]
  return ["-- | Converted from Python dictionary %s" % py_name,
          "%s :: [(%s, %s)]" % (hs_name, key_type, val_type),
          "%s = [%s]" % (hs_name, ", ".join(kv_pairs)),
          ]


189
def ConvertVariable(prefix, name, value, all_items):
190
191
  """Converts a given variable to Haskell code.

192
193
  @param prefix: a prefix for the Haskell name (useful for module
      identification)
194
195
  @param name: the Python name
  @param value: the value
196
197
  @param all_items: a dictionary of name/value for the module being
      processed
198
199
200
201
  @return: a list of Haskell code lines

  """
  lines = []
202
203
204
205
206
207
208
  if prefix:
    pfx_name = prefix + "_"
    fqn = prefix + "." + name
  else:
    pfx_name = ""
    fqn = name
  hs_name = NameRules(pfx_name + name)
209
  hs_typeval = HaskellTypeVal(value)
210
211
212
213
214
215
216
217
  if (isinstance(value, types.ModuleType) or callable(value) or
      PRIVATE_RE.match(name)):
    # no sense in marking these, as we don't _want_ to convert them; the
    # message in the next if block is for datatypes we don't _know_
    # (yet) how to convert
    pass
  elif not CONSTANT_RE.match(name):
    lines.append("-- Skipped %s %s, not constant" % (fqn, type(value)))
218
219
220
  elif hs_typeval is not None:
    # this is a simple value
    (hs_type, hs_val) = hs_typeval
221
    lines.append("-- | Converted from Python constant %s" % fqn)
222
223
    lines.append("%s :: %s" % (hs_name, hs_type))
    lines.append("%s = %s" % (hs_name, hs_val))
224
225
  elif isinstance(value, dict):
    if value:
226
      lines.append("-- Following lines come from dictionary %s" % fqn)
227
228
229
230
      # try to build a real map here, if all keys have same type, and
      # all values too (i.e. we have a homogeneous dictionary)
      lines.extend(FormatDict(all_items, pfx_name, fqn, hs_name, value))
      # and now create individual names
231
      for k in sorted(value.keys()):
232
233
        lines.extend(ConvertVariable(prefix, DictKeyName(name, k),
                                     value[k], all_items))
234
235
  elif isinstance(value, tuple):
    tvs = [HaskellTypeVal(elem) for elem in value]
236
237
238
239
240
241
242
    # Custom rule for special cluster verify error tuples
    if name.startswith("CV_E") and len(value) == 3 and tvs[1][0] is not None:
      cv_ename = hs_name + "Code"
      lines.append("-- | Special cluster verify code %s" % name)
      lines.append("%s :: %s" % (cv_ename, tvs[1][0]))
      lines.append("%s = %s" % (cv_ename, tvs[1][1]))
      lines.append("")
243
244
    if compat.all(e is not None for e in tvs):
      ttypes = ", ".join(e[0] for e in tvs)
245
      tvals = FormatListElems(all_items, pfx_name, value, [e[1] for e in tvs])
246
      lines.append("-- | Converted from Python tuple %s" % fqn)
247
248
249
      lines.append("%s :: (%s)" % (hs_name, ttypes))
      lines.append("%s = (%s)" % (hs_name, tvals))
    else:
250
      lines.append("-- Skipped tuple %s, cannot convert all elements" % fqn)
251
252
253
254
255
256
257
258
259
260
261
262
263
  elif isinstance(value, (list, set, frozenset)):
    # Lists and frozensets are handled the same in Haskell: as lists,
    # since lists are immutable and we don't need for constants the
    # high-speed of an actual Set type. However, we can only convert
    # them if they have the same type for all elements (which is a
    # normal expectation for constants, our code should be well
    # behaved); note that this is different from the tuples case,
    # where we always (for some values of always) can convert
    tvs = [HaskellTypeVal(elem) for elem in value]
    if compat.all(e is not None for e in tvs):
      ttypes, tvals = zip(*tvs)
      uniq_types = set(ttypes)
      if len(uniq_types) == 1:
264
        values = FormatListElems(all_items, pfx_name, value, tvals)
265
        lines.append("-- | Converted from Python list or set %s" % fqn)
266
        lines.append("%s :: [%s]" % (hs_name, uniq_types.pop()))
267
        lines.append("%s = [%s]" % (hs_name, values))
268
      else:
269
        lines.append("-- | Skipped list/set %s, is not homogeneous" % fqn)
270
    else:
271
      lines.append("-- | Skipped list/set %s, cannot convert all elems" % fqn)
272
273
274
  elif isinstance(value, RE_TYPE):
    tvs = HaskellTypeVal(value.pattern)
    assert tvs is not None
275
    lines.append("-- | Converted from Python RE object %s" % fqn)
276
277
    lines.append("%s :: %s" % (hs_name, tvs[0]))
    lines.append("%s = %s" % (hs_name, tvs[1]))
278
  else:
279
    lines.append("-- Skipped %s, %s not handled" % (fqn, type(value)))
280
281
282
  return lines


283
def Convert(module, prefix):
284
285
286
287
288
  """Converts the constants to Haskell.

  """
  lines = [""]

289
  all_items = dict((name, getattr(module, name)) for name in dir(module))
290

291
292
293
  for name in sorted(all_items.keys()):
    value = all_items[name]
    new_lines = ConvertVariable(prefix, name, value, all_items)
294
295
296
    if new_lines:
      lines.extend(new_lines)
      lines.append("")
297
298
299
300

  return "\n".join(lines)


301
302
303
304
305
306
307
308
309
310
def ConvertMisc():
  """Convert some extra computed-values to Haskell.

  """
  lines = [""]
  lines.extend(ConvertVariable("opcodes", "OP_IDS",
                               opcodes.OP_MAPPING.keys(), {}))
  return "\n".join(lines)


311
def main():
312
313
  print Convert(constants, "")
  print Convert(luxi, "luxi")
314
  print Convert(qlang, "qlang")
315
  print Convert(_autoconf, "autoconf")
316
  print Convert(errors, "errors")
317
  print Convert(jstore, "jstore")
318
  print ConvertMisc()
319
320
321
322


if __name__ == "__main__":
  main()