ht: Add strict check for dictionaries
[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
26 from ganeti import compat
27 from ganeti import utils
28
29
30 _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
31
32
33 def Parens(text):
34   """Enclose text in parens if necessary.
35
36   @param text: Text
37
38   """
39   text = str(text)
40
41   if _PAREN_RE.match(text):
42     return text
43   else:
44     return "(%s)" % text
45
46
47 def WithDesc(text):
48   """Builds wrapper class with description text.
49
50   @type text: string
51   @param text: Description text
52   @return: Callable class
53
54   """
55   assert text[0] == text[0].upper()
56
57   class wrapper(object): # pylint: disable-msg=C0103
58     __slots__ = ["__call__"]
59
60     def __init__(self, fn):
61       """Initializes this class.
62
63       @param fn: Wrapped function
64
65       """
66       self.__call__ = fn
67
68     def __str__(self):
69       return text
70
71   return wrapper
72
73
74 def CombinationDesc(op, args, fn):
75   """Build description for combinating operator.
76
77   @type op: string
78   @param op: Operator as text (e.g. "and")
79   @type args: list
80   @param args: Operator arguments
81   @type fn: callable
82   @param fn: Wrapped function
83
84   """
85   if len(args) == 1:
86     descr = str(args[0])
87   else:
88     descr = (" %s " % op).join(Parens(i) for i in args)
89
90   return WithDesc(descr)(fn)
91
92
93 # Modifiable default values; need to define these here before the
94 # actual LUs
95
96 @WithDesc(str([]))
97 def EmptyList():
98   """Returns an empty list.
99
100   """
101   return []
102
103
104 @WithDesc(str({}))
105 def EmptyDict():
106   """Returns an empty dict.
107
108   """
109   return {}
110
111
112 #: The without-default default value
113 NoDefault = object()
114
115
116 #: The no-type (value too complex to check it in the type system)
117 NoType = object()
118
119
120 # Some basic types
121 @WithDesc("NotNone")
122 def TNotNone(val):
123   """Checks if the given value is not None.
124
125   """
126   return val is not None
127
128
129 @WithDesc("None")
130 def TNone(val):
131   """Checks if the given value is None.
132
133   """
134   return val is None
135
136
137 @WithDesc("Boolean")
138 def TBool(val):
139   """Checks if the given value is a boolean.
140
141   """
142   return isinstance(val, bool)
143
144
145 @WithDesc("Integer")
146 def TInt(val):
147   """Checks if the given value is an integer.
148
149   """
150   # For backwards compatibility with older Python versions, boolean values are
151   # also integers and should be excluded in this test.
152   #
153   # >>> (isinstance(False, int), isinstance(True, int))
154   # (True, True)
155   return isinstance(val, int) and not isinstance(val, bool)
156
157
158 @WithDesc("Float")
159 def TFloat(val):
160   """Checks if the given value is a float.
161
162   """
163   return isinstance(val, float)
164
165
166 @WithDesc("String")
167 def TString(val):
168   """Checks if the given value is a string.
169
170   """
171   return isinstance(val, basestring)
172
173
174 @WithDesc("EvalToTrue")
175 def TTrue(val):
176   """Checks if a given value evaluates to a boolean True value.
177
178   """
179   return bool(val)
180
181
182 def TElemOf(target_list):
183   """Builds a function that checks if a given value is a member of a list.
184
185   """
186   def fn(val):
187     return val in target_list
188
189   return WithDesc("OneOf %s" % (utils.CommaJoin(target_list), ))(fn)
190
191
192 # Container types
193 @WithDesc("List")
194 def TList(val):
195   """Checks if the given value is a list.
196
197   """
198   return isinstance(val, list)
199
200
201 @WithDesc("Dictionary")
202 def TDict(val):
203   """Checks if the given value is a dictionary.
204
205   """
206   return isinstance(val, dict)
207
208
209 def TIsLength(size):
210   """Check is the given container is of the given size.
211
212   """
213   def fn(container):
214     return len(container) == size
215
216   return WithDesc("Length %s" % (size, ))(fn)
217
218
219 # Combinator types
220 def TAnd(*args):
221   """Combine multiple functions using an AND operation.
222
223   """
224   def fn(val):
225     return compat.all(t(val) for t in args)
226
227   return CombinationDesc("and", args, fn)
228
229
230 def TOr(*args):
231   """Combine multiple functions using an AND operation.
232
233   """
234   def fn(val):
235     return compat.any(t(val) for t in args)
236
237   return CombinationDesc("or", args, fn)
238
239
240 def TMap(fn, test):
241   """Checks that a modified version of the argument passes the given test.
242
243   """
244   return WithDesc("Result of %s must be %s" %
245                   (Parens(fn), Parens(test)))(lambda val: test(fn(val)))
246
247
248 # Type aliases
249
250 #: a non-empty string
251 TNonEmptyString = WithDesc("NonEmptyString")(TAnd(TString, TTrue))
252
253 #: a maybe non-empty string
254 TMaybeString = TOr(TNonEmptyString, TNone)
255
256 #: a maybe boolean (bool or none)
257 TMaybeBool = TOr(TBool, TNone)
258
259 #: Maybe a dictionary (dict or None)
260 TMaybeDict = TOr(TDict, TNone)
261
262 #: a positive integer
263 TPositiveInt = \
264   TAnd(TInt, WithDesc("EqualGreaterZero")(lambda v: v >= 0))
265
266 #: a strictly positive integer
267 TStrictPositiveInt = \
268   TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0))
269
270 #: a positive float
271 TPositiveFloat = \
272   TAnd(TFloat, WithDesc("EqualGreaterZero")(lambda v: v >= 0.0))
273
274
275 def TListOf(my_type):
276   """Checks if a given value is a list with all elements of the same type.
277
278   """
279   desc = WithDesc("List of %s" % (Parens(my_type), ))
280   return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst)))
281
282
283 def TDictOf(key_type, val_type):
284   """Checks a dict type for the type of its key/values.
285
286   """
287   desc = WithDesc("Dictionary with keys of %s and values of %s" %
288                   (Parens(key_type), Parens(val_type)))
289
290   def fn(container):
291     return (compat.all(key_type(v) for v in container.keys()) and
292             compat.all(val_type(v) for v in container.values()))
293
294   return desc(TAnd(TDict, fn))
295
296
297 def _TStrictDictCheck(require_all, exclusive, items, val):
298   """Helper function for L{TStrictDict}.
299
300   """
301   notfound_fn = lambda _: not exclusive
302
303   if require_all and not frozenset(val.keys()).issuperset(items.keys()):
304     # Requires items not found in value
305     return False
306
307   return compat.all(items.get(key, notfound_fn)(value)
308                     for (key, value) in val.items())
309
310
311 def TStrictDict(require_all, exclusive, items):
312   """Strict dictionary check with specific keys.
313
314   @type require_all: boolean
315   @param require_all: Whether all keys in L{items} are required
316   @type exclusive: boolean
317   @param exclusive: Whether only keys listed in L{items} should be accepted
318   @type items: dictionary
319   @param items: Mapping from key (string) to verification function
320
321   """
322   descparts = ["Dictionary containing"]
323
324   if exclusive:
325     descparts.append(" none but the")
326
327   if require_all:
328     descparts.append(" required")
329
330   if len(items) == 1:
331     descparts.append(" key ")
332   else:
333     descparts.append(" keys ")
334
335   descparts.append(utils.CommaJoin("\"%s\" (value %s)" % (key, value)
336                                    for (key, value) in items.items()))
337
338   desc = WithDesc("".join(descparts))
339
340   return desc(TAnd(TDict,
341                    compat.partial(_TStrictDictCheck, require_all, exclusive,
342                                   items)))