gnt-network(8): s/ipv6/IPv6/
[ganeti-local] / autotools / convert-constants
1 #!/usr/bin/python
2 #
3
4 # Copyright (C) 2011, 2012 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, int):
91     return ("Int", "%d" % value)
92   elif isinstance(value, long):
93     return ("Integer", "%d" % value)
94   elif isinstance(value, float):
95     return ("Double", "%f" % value)
96   else:
97     return None
98
99
100 def IdentifyOrigin(all_items, value):
101   """Tries to identify a constant name from a constant's value.
102
103   This uses a simple algorithm: is there a constant (and only one)
104   with the same value? If so, then it returns that constants' name.
105
106   @note: it is recommended to use this only for tuples/lists/sets, and
107       not for individual (top-level) values
108   @param all_items: a dictionary of name/values for the current module
109   @param value: the value for which we try to find an origin
110
111   """
112   found = [name for (name, v) in all_items.items()
113            if v is value and name not in IGNORED_DECL_NAMES]
114   if len(found) == 1:
115     return found[0]
116   else:
117     return None
118
119
120 def FormatListElems(all_items, pfx_name, ovals, tvals):
121   """Formats a list's elements.
122
123   This formats the elements as either values or, if we find all
124   origins, as names.
125
126   @param all_items: a dictionary of name/values for the current module
127   @param pfx_name: the prefix name currently used
128   @param ovals: the list of actual (Python) values
129   @param tvals: the list of values we want to format in the Haskell form
130
131   """
132   origins = [IdentifyOrigin(all_items, v) for v in ovals]
133   if compat.all(x is not None for x in origins):
134     values = [NameRules(pfx_name + origin) for origin in origins]
135   else:
136     values = tvals
137   return ", ".join(values)
138
139
140 def FormatDict(all_items, pfx_name, py_name, hs_name, mydict):
141   """Converts a dictionary to a Haskell association list ([(k, v)]),
142   if possible.
143
144   @param all_items: a dictionary of name/values for the current module
145   @param pfx_name: the prefix name currently used
146   @param py_name: the Python name
147   @param hs_name: the Haskell name
148   @param mydict: a dictonary, unknown yet if homogenous or not
149
150   """
151   # need this for ordering
152   orig_list = mydict.items()
153   list_form = [(HaskellTypeVal(k), HaskellTypeVal(v)) for k, v in orig_list]
154   if compat.any(v is None or k is None for k, v in list_form):
155     # type not known
156     return []
157   all_keys = [k for k, _ in list_form]
158   all_vals = [v for _, v in list_form]
159   key_types = set(k[0] for k in all_keys)
160   val_types = set(v[0] for v in all_vals)
161   if not(len(key_types) == 1 and len(val_types) == 1):
162     # multiple types
163     return []
164   # record the key and value Haskell types
165   key_type = key_types.pop()
166   val_type = val_types.pop()
167
168   # now try to find names for the keys, instead of raw values
169   key_origins = [IdentifyOrigin(all_items, k) for k, _ in orig_list]
170   if compat.all(x is not None for x in key_origins):
171     key_v = [NameRules(pfx_name + origin) for origin in key_origins]
172   else:
173     key_v = [k[1] for k in all_keys]
174   # ... and for values
175   val_origins = [IdentifyOrigin(all_items, v) for _, v in orig_list]
176   if compat.all(x is not None for x in val_origins):
177     val_v = [NameRules(pfx_name + origin) for origin in val_origins]
178   else:
179     val_v = [v[1] for v in all_vals]
180
181   # finally generate the output
182   kv_pairs = ["(%s, %s)" % (k, v) for k, v in zip(key_v, val_v)]
183   return ["-- | Converted from Python dictionary %s" % py_name,
184           "%s :: [(%s, %s)]" % (hs_name, key_type, val_type),
185           "%s = [%s]" % (hs_name, ", ".join(kv_pairs)),
186           ]
187
188
189 def ConvertVariable(prefix, name, value, all_items):
190   """Converts a given variable to Haskell code.
191
192   @param prefix: a prefix for the Haskell name (useful for module
193       identification)
194   @param name: the Python name
195   @param value: the value
196   @param all_items: a dictionary of name/value for the module being
197       processed
198   @return: a list of Haskell code lines
199
200   """
201   lines = []
202   if prefix:
203     pfx_name = prefix + "_"
204     fqn = prefix + "." + name
205   else:
206     pfx_name = ""
207     fqn = name
208   hs_name = NameRules(pfx_name + name)
209   hs_typeval = HaskellTypeVal(value)
210   if (isinstance(value, types.ModuleType) or callable(value) or
211       PRIVATE_RE.match(name)):
212     # no sense in marking these, as we don't _want_ to convert them; the
213     # message in the next if block is for datatypes we don't _know_
214     # (yet) how to convert
215     pass
216   elif not CONSTANT_RE.match(name):
217     lines.append("-- Skipped %s %s, not constant" % (fqn, type(value)))
218   elif hs_typeval is not None:
219     # this is a simple value
220     (hs_type, hs_val) = hs_typeval
221     lines.append("-- | Converted from Python constant %s" % fqn)
222     lines.append("%s :: %s" % (hs_name, hs_type))
223     lines.append("%s = %s" % (hs_name, hs_val))
224   elif isinstance(value, dict):
225     if value:
226       lines.append("-- Following lines come from dictionary %s" % fqn)
227       # try to build a real map here, if all keys have same type, and
228       # all values too (i.e. we have a homogeneous dictionary)
229       lines.extend(FormatDict(all_items, pfx_name, fqn, hs_name, value))
230       # and now create individual names
231       for k in sorted(value.keys()):
232         lines.extend(ConvertVariable(prefix, DictKeyName(name, k),
233                                      value[k], all_items))
234   elif isinstance(value, tuple):
235     tvs = [HaskellTypeVal(elem) for elem in value]
236     # Custom rule for special cluster verify error tuples
237     if name.startswith("CV_E") and len(value) == 3 and tvs[1][0] is not None:
238       cv_ename = hs_name + "Code"
239       lines.append("-- | Special cluster verify code %s" % name)
240       lines.append("%s :: %s" % (cv_ename, tvs[1][0]))
241       lines.append("%s = %s" % (cv_ename, tvs[1][1]))
242       lines.append("")
243     if compat.all(e is not None for e in tvs):
244       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       lines.append("%s :: (%s)" % (hs_name, ttypes))
248       lines.append("%s = (%s)" % (hs_name, tvals))
249     else:
250       lines.append("-- Skipped tuple %s, cannot convert all elements" % fqn)
251   elif isinstance(value, (list, set, frozenset)):
252     # Lists and frozensets are handled the same in Haskell: as lists,
253     # since lists are immutable and we don't need for constants the
254     # high-speed of an actual Set type. However, we can only convert
255     # them if they have the same type for all elements (which is a
256     # normal expectation for constants, our code should be well
257     # behaved); note that this is different from the tuples case,
258     # where we always (for some values of always) can convert
259     tvs = [HaskellTypeVal(elem) for elem in value]
260     if compat.all(e is not None for e in tvs):
261       ttypes, tvals = zip(*tvs)
262       uniq_types = set(ttypes)
263       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   elif isinstance(value, RE_TYPE):
273     tvs = HaskellTypeVal(value.pattern)
274     assert tvs is not None
275     lines.append("-- | Converted from Python RE object %s" % fqn)
276     lines.append("%s :: %s" % (hs_name, tvs[0]))
277     lines.append("%s = %s" % (hs_name, tvs[1]))
278   else:
279     lines.append("-- Skipped %s, %s not handled" % (fqn, type(value)))
280   return lines
281
282
283 def Convert(module, prefix):
284   """Converts the constants to Haskell.
285
286   """
287   lines = [""]
288
289   all_items = dict((name, getattr(module, name)) for name in dir(module))
290
291   for name in sorted(all_items.keys()):
292     value = all_items[name]
293     new_lines = ConvertVariable(prefix, name, value, all_items)
294     if new_lines:
295       lines.extend(new_lines)
296       lines.append("")
297
298   return "\n".join(lines)
299
300
301 def ConvertMisc():
302   """Convert some extra computed-values to Haskell.
303
304   """
305   lines = [""]
306   lines.extend(ConvertVariable("opcodes", "OP_IDS",
307                                opcodes.OP_MAPPING.keys(), {}))
308   return "\n".join(lines)
309
310
311 def main():
312   print Convert(constants, "")
313   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 if __name__ == "__main__":
322   main()