d1a8df93bd378d1e257f4cd2fd4ad2a5d4028f91
[ganeti-local] / lib / ht.py
1 #
2 #
3
4 # Copyright (C) 2010, 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 """Module implementing the parameter types code."""
23
24 import re
25 import operator
26
27 from ganeti import compat
28 from ganeti import utils
29 from ganeti import constants
30
31
32 _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
33
34
35 def Parens(text):
36   """Enclose text in parens if necessary.
37
38   @param text: Text
39
40   """
41   text = str(text)
42
43   if _PAREN_RE.match(text):
44     return text
45   else:
46     return "(%s)" % text
47
48
49 class _DescWrapper(object):
50   __slots__ = [
51     "_fn",
52     "_text",
53     ]
54
55   def __init__(self, text, fn):
56     """Initializes this class.
57
58     @param text: Description
59     @param fn: Wrapped function
60
61     """
62     self._text = text
63     self._fn = fn
64
65   def __call__(self, *args):
66     return self._fn(*args)
67
68   def __str__(self):
69     return self._text
70
71
72 def WithDesc(text):
73   """Builds wrapper class with description text.
74
75   @type text: string
76   @param text: Description text
77   @return: Callable class
78
79   """
80   assert text[0] == text[0].upper()
81
82   return compat.partial(_DescWrapper, text)
83
84
85 def CombinationDesc(op, args, fn):
86   """Build description for combinating operator.
87
88   @type op: string
89   @param op: Operator as text (e.g. "and")
90   @type args: list
91   @param args: Operator arguments
92   @type fn: callable
93   @param fn: Wrapped function
94
95   """
96   if len(args) == 1:
97     descr = str(args[0])
98   else:
99     descr = (" %s " % op).join(Parens(i) for i in args)
100
101   return WithDesc(descr)(fn)
102
103
104 # Modifiable default values; need to define these here before the
105 # actual LUs
106
107 @WithDesc(str([]))
108 def EmptyList():
109   """Returns an empty list.
110
111   """
112   return []
113
114
115 @WithDesc(str({}))
116 def EmptyDict():
117   """Returns an empty dict.
118
119   """
120   return {}
121
122
123 #: The without-default default value
124 NoDefault = object()
125
126
127 #: The no-type (value too complex to check it in the type system)
128 NoType = object()
129
130
131 # Some basic types
132 @WithDesc("Anything")
133 def TAny(_):
134   """Accepts any value.
135
136   """
137   return True
138
139
140 @WithDesc("NotNone")
141 def TNotNone(val):
142   """Checks if the given value is not None.
143
144   """
145   return val is not None
146
147
148 @WithDesc("None")
149 def TNone(val):
150   """Checks if the given value is None.
151
152   """
153   return val is None
154
155
156 @WithDesc("Boolean")
157 def TBool(val):
158   """Checks if the given value is a boolean.
159
160   """
161   return isinstance(val, bool)
162
163
164 @WithDesc("Integer")
165 def TInt(val):
166   """Checks if the given value is an integer.
167
168   """
169   # For backwards compatibility with older Python versions, boolean values are
170   # also integers and should be excluded in this test.
171   #
172   # >>> (isinstance(False, int), isinstance(True, int))
173   # (True, True)
174   return isinstance(val, (int, long)) and not isinstance(val, bool)
175
176
177 @WithDesc("Float")
178 def TFloat(val):
179   """Checks if the given value is a float.
180
181   """
182   return isinstance(val, float)
183
184
185 @WithDesc("String")
186 def TString(val):
187   """Checks if the given value is a string.
188
189   """
190   return isinstance(val, basestring)
191
192
193 @WithDesc("EvalToTrue")
194 def TTrue(val):
195   """Checks if a given value evaluates to a boolean True value.
196
197   """
198   return bool(val)
199
200
201 def TElemOf(target_list):
202   """Builds a function that checks if a given value is a member of a list.
203
204   """
205   def fn(val):
206     return val in target_list
207
208   return WithDesc("OneOf %s" % (utils.CommaJoin(target_list), ))(fn)
209
210
211 # Container types
212 @WithDesc("List")
213 def TList(val):
214   """Checks if the given value is a list.
215
216   """
217   return isinstance(val, list)
218
219
220 @WithDesc("Dictionary")
221 def TDict(val):
222   """Checks if the given value is a dictionary.
223
224   """
225   return isinstance(val, dict)
226
227
228 def TIsLength(size):
229   """Check is the given container is of the given size.
230
231   """
232   def fn(container):
233     return len(container) == size
234
235   return WithDesc("Length %s" % (size, ))(fn)
236
237
238 # Combinator types
239 def TAnd(*args):
240   """Combine multiple functions using an AND operation.
241
242   """
243   def fn(val):
244     return compat.all(t(val) for t in args)
245
246   return CombinationDesc("and", args, fn)
247
248
249 def TOr(*args):
250   """Combine multiple functions using an AND operation.
251
252   """
253   def fn(val):
254     return compat.any(t(val) for t in args)
255
256   return CombinationDesc("or", args, fn)
257
258
259 def TMap(fn, test):
260   """Checks that a modified version of the argument passes the given test.
261
262   """
263   return WithDesc("Result of %s must be %s" %
264                   (Parens(fn), Parens(test)))(lambda val: test(fn(val)))
265
266
267 def TRegex(pobj):
268   """Checks whether a string matches a specific regular expression.
269
270   @param pobj: Compiled regular expression as returned by C{re.compile}
271
272   """
273   desc = WithDesc("String matching regex \"%s\"" %
274                   pobj.pattern.encode("string_escape"))
275
276   return desc(TAnd(TString, pobj.match))
277
278
279 # Type aliases
280
281 #: a non-empty string
282 TNonEmptyString = WithDesc("NonEmptyString")(TAnd(TString, TTrue))
283
284 #: a maybe non-empty string
285 TMaybeString = TOr(TNonEmptyString, TNone)
286
287 #: a maybe boolean (bool or none)
288 TMaybeBool = TOr(TBool, TNone)
289
290 #: Maybe a dictionary (dict or None)
291 TMaybeDict = TOr(TDict, TNone)
292
293 #: a positive integer
294 TPositiveInt = \
295   TAnd(TInt, WithDesc("EqualGreaterZero")(lambda v: v >= 0))
296
297 #: a strictly positive integer
298 TStrictPositiveInt = \
299   TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0))
300
301 #: a strictly negative integer (0 > value)
302 TStrictNegativeInt = \
303   TAnd(TInt, WithDesc("LessThanZero")(compat.partial(operator.gt, 0)))
304
305 #: a positive float
306 TPositiveFloat = \
307   TAnd(TFloat, WithDesc("EqualGreaterZero")(lambda v: v >= 0.0))
308
309 #: Job ID
310 TJobId = WithDesc("JobId")(TOr(TPositiveInt,
311                                TRegex(re.compile("^%s$" %
312                                                  constants.JOB_ID_TEMPLATE))))
313
314 #: Number
315 TNumber = TOr(TInt, TFloat)
316
317 #: Relative job ID
318 TRelativeJobId = WithDesc("RelativeJobId")(TStrictNegativeInt)
319
320
321 def TListOf(my_type):
322   """Checks if a given value is a list with all elements of the same type.
323
324   """
325   desc = WithDesc("List of %s" % (Parens(my_type), ))
326   return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst)))
327
328
329 def TDictOf(key_type, val_type):
330   """Checks a dict type for the type of its key/values.
331
332   """
333   desc = WithDesc("Dictionary with keys of %s and values of %s" %
334                   (Parens(key_type), Parens(val_type)))
335
336   def fn(container):
337     return (compat.all(key_type(v) for v in container.keys()) and
338             compat.all(val_type(v) for v in container.values()))
339
340   return desc(TAnd(TDict, fn))
341
342
343 def _TStrictDictCheck(require_all, exclusive, items, val):
344   """Helper function for L{TStrictDict}.
345
346   """
347   notfound_fn = lambda _: not exclusive
348
349   if require_all and not frozenset(val.keys()).issuperset(items.keys()):
350     # Requires items not found in value
351     return False
352
353   return compat.all(items.get(key, notfound_fn)(value)
354                     for (key, value) in val.items())
355
356
357 def TStrictDict(require_all, exclusive, items):
358   """Strict dictionary check with specific keys.
359
360   @type require_all: boolean
361   @param require_all: Whether all keys in L{items} are required
362   @type exclusive: boolean
363   @param exclusive: Whether only keys listed in L{items} should be accepted
364   @type items: dictionary
365   @param items: Mapping from key (string) to verification function
366
367   """
368   descparts = ["Dictionary containing"]
369
370   if exclusive:
371     descparts.append(" none but the")
372
373   if require_all:
374     descparts.append(" required")
375
376   if len(items) == 1:
377     descparts.append(" key ")
378   else:
379     descparts.append(" keys ")
380
381   descparts.append(utils.CommaJoin("\"%s\" (value %s)" % (key, value)
382                                    for (key, value) in items.items()))
383
384   desc = WithDesc("".join(descparts))
385
386   return desc(TAnd(TDict,
387                    compat.partial(_TStrictDictCheck, require_all, exclusive,
388                                   items)))
389
390
391 def TItems(items):
392   """Checks individual items of a container.
393
394   If the verified value and the list of expected items differ in length, this
395   check considers only as many items as are contained in the shorter list. Use
396   L{TIsLength} to enforce a certain length.
397
398   @type items: list
399   @param items: List of checks
400
401   """
402   assert items, "Need items"
403
404   text = ["Item", "item"]
405   desc = WithDesc(utils.CommaJoin("%s %s is %s" %
406                                   (text[int(idx > 0)], idx, Parens(check))
407                                   for (idx, check) in enumerate(items)))
408
409   return desc(lambda value: compat.all(check(i)
410                                        for (check, i) in zip(items, value)))