Make build_chroot self-contained
[ganeti-local] / autotools / convert-constants
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2011, 2012, 2013 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 """Script for converting Python constants to Haskell code fragments.
22
23 """
24
25 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 #: Private name regex
42 PRIVATE_RE = re.compile("^__.+__$")
43
44 #: The type of regex objects
45 RE_TYPE = type(CONSTANT_RE)
46
47 #: Keys which do not declare a value (manually maintained). By adding
48 # values here, we can make more lists use the actual names; otherwise
49 # we'll have (e.g.) both DEFAULT_ENABLED_HYPERVISOR and HT_XEN_PVM
50 # declare the same value, and thus the list of valid hypervisors will
51 # have strings instead of easily looked-up names.
52 IGNORED_DECL_NAMES = ["DEFAULT_ENABLED_HYPERVISOR"]
53
54
55 def NameRules(name):
56   """Converts the upper-cased Python name to Haskell camelCase.
57
58   """
59   name = name.replace("-", "_")
60   elems = name.split("_")
61   return elems[0].lower() + "".join(e.capitalize() for e in elems[1:])
62
63
64 def StringValueRules(value):
65   """Converts a string value from Python to Haskell.
66
67   """
68   value = value.encode("string_escape") # escapes backslashes
69   value = value.replace("\"", "\\\"")
70   return value
71
72
73 def DictKeyName(dict_name, key_name):
74   """Converts a dict plus key name to a full name.
75
76   """
77   return"%s_%s" % (dict_name, str(key_name).upper())
78
79
80 def HaskellTypeVal(value):
81   """Returns the Haskell type and value for a Python value.
82
83   Note that this only work for 'plain' Python types.
84
85   @returns: (string, string) or None, if we can't determine the type.
86
87   """
88   if isinstance(value, basestring):
89     return ("String", "\"%s\"" % StringValueRules(value))
90   elif isinstance(value, bool):
91     return ("Bool", "%s" % value)
92   elif isinstance(value, int):
93     return ("Int", "%d" % value)
94   elif isinstance(value, long):
95     return ("Integer", "%d" % value)
96   elif isinstance(value, float):
97     return ("Double", "%f" % value)
98   else:
99     return None
100
101
102 def IdentifyOrigin(all_items, value):
103   """Tries to identify a constant name from a constant's value.
104
105   This uses a simple algorithm: is there a constant (and only one)
106   with the same value? If so, then it returns that constants' name.
107
108   @note: it is recommended to use this only for tuples/lists/sets, and
109       not for individual (top-level) values
110   @param all_items: a dictionary of name/values for the current module
111   @param value: the value for which we try to find an origin
112
113   """
114   found = [name for (name, v) in all_items.items()
115            if v is value and name not in IGNORED_DECL_NAMES]
116   if len(found) == 1:
117     return found[0]
118   else:
119     return None
120
121
122 def FormatListElems(all_items, pfx_name, ovals, tvals):
123   """Formats a list's elements.
124
125   This formats the elements as either values or, if we find all
126   origins, as names.
127
128   @param all_items: a dictionary of name/values for the current module
129   @param pfx_name: the prefix name currently used
130   @param ovals: the list of actual (Python) values
131   @param tvals: the list of values we want to format in the Haskell form
132
133   """
134   origins = [IdentifyOrigin(all_items, v) for v in ovals]
135   if compat.all(x is not None for x in origins):
136     values = [NameRules(pfx_name + origin) for origin in origins]
137   else:
138     values = tvals
139   return ", ".join(values)
140
141
142 def FormatDict(all_items, pfx_name, py_name, hs_name, mydict):
143   """Converts a dictionary to a Haskell association list ([(k, v)]),
144   if possible.
145
146   @param all_items: a dictionary of name/values for the current module
147   @param pfx_name: the prefix name currently used
148   @param py_name: the Python name
149   @param hs_name: the Haskell name
150   @param mydict: a dictonary, unknown yet if homogenous or not
151
152   """
153   # need this for ordering
154   orig_list = mydict.items()
155   list_form = [(HaskellTypeVal(k), HaskellTypeVal(v)) for k, v in orig_list]
156   if compat.any(v is None or k is None for k, v in list_form):
157     # type not known
158     return []
159   all_keys = [k for k, _ in list_form]
160   all_vals = [v for _, v in list_form]
161   key_types = set(k[0] for k in all_keys)
162   val_types = set(v[0] for v in all_vals)
163   if not(len(key_types) == 1 and len(val_types) == 1):
164     # multiple types
165     return []
166   # record the key and value Haskell types
167   key_type = key_types.pop()
168   val_type = val_types.pop()
169
170   # now try to find names for the keys, instead of raw values
171   key_origins = [IdentifyOrigin(all_items, k) for k, _ in orig_list]
172   if compat.all(x is not None for x in key_origins):
173     key_v = [NameRules(pfx_name + origin) for origin in key_origins]
174   else:
175     key_v = [k[1] for k in all_keys]
176   # ... and for values
177   val_origins = [IdentifyOrigin(all_items, v) for _, v in orig_list]
178   if compat.all(x is not None for x in val_origins):
179     val_v = [NameRules(pfx_name + origin) for origin in val_origins]
180   else:
181     val_v = [v[1] for v in all_vals]
182
183   # finally generate the output
184   kv_pairs = ["(%s, %s)" % (k, v) for k, v in zip(key_v, val_v)]
185   return ["-- | Converted from Python dictionary @%s@" % py_name,
186           "%s :: [(%s, %s)]" % (hs_name, key_type, val_type),
187           "%s = [%s]" % (hs_name, ", ".join(kv_pairs)),
188           ]
189
190
191 def ConvertVariable(prefix, name, value, all_items):
192   """Converts a given variable to Haskell code.
193
194   @param prefix: a prefix for the Haskell name (useful for module
195       identification)
196   @param name: the Python name
197   @param value: the value
198   @param all_items: a dictionary of name/value for the module being
199       processed
200   @return: a list of Haskell code lines
201
202   """
203   lines = []
204   if prefix:
205     pfx_name = prefix + "_"
206     fqn = prefix + "." + name
207   else:
208     pfx_name = ""
209     fqn = name
210   hs_name = NameRules(pfx_name + name)
211   hs_typeval = HaskellTypeVal(value)
212   if (isinstance(value, types.ModuleType) or callable(value) or
213       PRIVATE_RE.match(name)):
214     # no sense in marking these, as we don't _want_ to convert them; the
215     # message in the next if block is for datatypes we don't _know_
216     # (yet) how to convert
217     pass
218   elif not CONSTANT_RE.match(name):
219     lines.append("-- Skipped %s %s, not constant" % (fqn, type(value)))
220   elif hs_typeval is not None:
221     # this is a simple value
222     (hs_type, hs_val) = hs_typeval
223     lines.append("-- | Converted from Python constant @%s@" % fqn)
224     lines.append("%s :: %s" % (hs_name, hs_type))
225     lines.append("%s = %s" % (hs_name, hs_val))
226   elif isinstance(value, dict):
227     if value:
228       lines.append("-- Following lines come from dictionary %s" % fqn)
229       # try to build a real map here, if all keys have same type, and
230       # all values too (i.e. we have a homogeneous dictionary)
231       lines.extend(FormatDict(all_items, pfx_name, fqn, hs_name, value))
232       # and now create individual names
233       for k in sorted(value.keys()):
234         lines.extend(ConvertVariable(prefix, DictKeyName(name, k),
235                                      value[k], all_items))
236   elif isinstance(value, tuple):
237     tvs = [HaskellTypeVal(elem) for elem in value]
238     # Custom rule for special cluster verify error tuples
239     if name.startswith("CV_E") and len(value) == 3 and tvs[1][0] is not None:
240       cv_ename = hs_name + "Code"
241       lines.append("-- | Special cluster verify code %s" % name)
242       lines.append("%s :: %s" % (cv_ename, tvs[1][0]))
243       lines.append("%s = %s" % (cv_ename, tvs[1][1]))
244       lines.append("")
245     if compat.all(e is not None for e in tvs):
246       ttypes = ", ".join(e[0] for e in tvs)
247       tvals = FormatListElems(all_items, pfx_name, value, [e[1] for e in tvs])
248       lines.append("-- | Converted from Python tuple @%s@" % fqn)
249       lines.append("%s :: (%s)" % (hs_name, ttypes))
250       lines.append("%s = (%s)" % (hs_name, tvals))
251     else:
252       lines.append("-- Skipped tuple %s, cannot convert all elements" % fqn)
253   elif isinstance(value, (list, set, frozenset)):
254     # Lists and frozensets are handled the same in Haskell: as lists,
255     # since lists are immutable and we don't need for constants the
256     # high-speed of an actual Set type. However, we can only convert
257     # them if they have the same type for all elements (which is a
258     # normal expectation for constants, our code should be well
259     # behaved); note that this is different from the tuples case,
260     # where we always (for some values of always) can convert
261     tvs = [HaskellTypeVal(elem) for elem in value]
262     if compat.all(e is not None for e in tvs):
263       ttypes, tvals = zip(*tvs)
264       uniq_types = set(ttypes)
265       if len(uniq_types) == 1:
266         values = FormatListElems(all_items, pfx_name, value, tvals)
267         lines.append("-- | Converted from Python list or set @%s@" % fqn)
268         lines.append("%s :: [%s]" % (hs_name, uniq_types.pop()))
269         lines.append("%s = [%s]" % (hs_name, values))
270       else:
271         lines.append("-- | Skipped list/set %s, is not homogeneous" % fqn)
272     else:
273       lines.append("-- | Skipped list/set %s, cannot convert all elems" % fqn)
274   elif isinstance(value, RE_TYPE):
275     tvs = HaskellTypeVal(value.pattern)
276     assert tvs is not None
277     lines.append("-- | Converted from Python RE object @%s@" % fqn)
278     lines.append("%s :: %s" % (hs_name, tvs[0]))
279     lines.append("%s = %s" % (hs_name, tvs[1]))
280   else:
281     lines.append("-- Skipped %s, %s not handled" % (fqn, type(value)))
282   return lines
283
284
285 def Convert(module, prefix):
286   """Converts the constants to Haskell.
287
288   """
289   lines = [""]
290
291   all_items = dict((name, getattr(module, name)) for name in dir(module))
292
293   for name in sorted(all_items.keys()):
294     value = all_items[name]
295     new_lines = ConvertVariable(prefix, name, value, all_items)
296     if new_lines:
297       lines.extend(new_lines)
298       lines.append("")
299
300   return "\n".join(lines)
301
302
303 def ConvertMisc():
304   """Convert some extra computed-values to Haskell.
305
306   """
307   lines = [""]
308   lines.extend(ConvertVariable("opcodes", "OP_IDS",
309                                opcodes.OP_MAPPING.keys(), {}))
310   return "\n".join(lines)
311
312
313 def main():
314   print Convert(constants, "")
315   print Convert(luxi, "luxi")
316   print Convert(qlang, "qlang")
317   print Convert(_autoconf, "autoconf")
318   print Convert(errors, "errors")
319   print Convert(jstore, "jstore")
320   print ConvertMisc()
321
322
323 if __name__ == "__main__":
324   main()