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