X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/8c9ee749f4e827e74d4bc16a44a3137d6e99cd63..ce0248c6633a15ee525a91cd0bea01011a4eb467:/lib/ht.py?ds=sidebyside diff --git a/lib/ht.py b/lib/ht.py index ffc6b44..a452239 100644 --- a/lib/ht.py +++ b/lib/ht.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2010, 2011 Google Inc. +# Copyright (C) 2010, 2011, 2012 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,9 +22,11 @@ """Module implementing the parameter types code.""" import re +import operator from ganeti import compat from ganeti import utils +from ganeti import constants _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$") @@ -44,6 +46,44 @@ def Parens(text): return "(%s)" % text +class _WrapperBase(object): + __slots__ = [ + "_fn", + "_text", + ] + + def __init__(self, text, fn): + """Initializes this class. + + @param text: Description + @param fn: Wrapped function + + """ + assert text.strip() + + self._text = text + self._fn = fn + + def __call__(self, *args): + return self._fn(*args) + + +class _DescWrapper(_WrapperBase): + """Wrapper class for description text. + + """ + def __str__(self): + return self._text + + +class _CommentWrapper(_WrapperBase): + """Wrapper class for comment. + + """ + def __str__(self): + return "%s [%s]" % (self._fn, self._text) + + def WithDesc(text): """Builds wrapper class with description text. @@ -54,21 +94,20 @@ def WithDesc(text): """ assert text[0] == text[0].upper() - class wrapper(object): # pylint: disable-msg=C0103 - __slots__ = ["__call__"] + return compat.partial(_DescWrapper, text) - def __init__(self, fn): - """Initializes this class. - @param fn: Wrapped function +def Comment(text): + """Builds wrapper for adding comment to description text. - """ - self.__call__ = fn + @type text: string + @param text: Comment text + @return: Callable class - def __str__(self): - return text + """ + assert not frozenset(text).intersection("[]") - return wrapper + return compat.partial(_CommentWrapper, text) def CombinationDesc(op, args, fn): @@ -82,6 +121,12 @@ def CombinationDesc(op, args, fn): @param fn: Wrapped function """ + # Some type descriptions are rather long. If "None" is listed at the + # end or somewhere in between it is easily missed. Therefore it should + # be at the beginning, e.g. "None or (long description)". + if __debug__ and TNone in args and args.index(TNone) > 0: + raise Exception("TNone must be listed first") + if len(args) == 1: descr = str(args[0]) else: @@ -118,6 +163,14 @@ NoType = object() # Some basic types +@WithDesc("Anything") +def TAny(_): + """Accepts any value. + + """ + return True + + @WithDesc("NotNone") def TNotNone(val): """Checks if the given value is not None. @@ -134,6 +187,14 @@ def TNone(val): return val is None +@WithDesc("ValueNone") +def TValueNone(val): + """Checks if the given value is L{constants.VALUE_NONE}. + + """ + return val == constants.VALUE_NONE + + @WithDesc("Boolean") def TBool(val): """Checks if the given value is a boolean. @@ -152,7 +213,7 @@ def TInt(val): # # >>> (isinstance(False, int), isinstance(True, int)) # (True, True) - return isinstance(val, int) and not isinstance(val, bool) + return isinstance(val, (int, long)) and not isinstance(val, bool) @WithDesc("Float") @@ -198,6 +259,14 @@ def TList(val): return isinstance(val, list) +@WithDesc("Tuple") +def TTuple(val): + """Checks if the given value is a tuple. + + """ + return isinstance(val, tuple) + + @WithDesc("Dictionary") def TDict(val): """Checks if the given value is a dictionary. @@ -228,7 +297,7 @@ def TAnd(*args): def TOr(*args): - """Combine multiple functions using an AND operation. + """Combine multiple functions using an OR operation. """ def fn(val): @@ -245,28 +314,95 @@ def TMap(fn, test): (Parens(fn), Parens(test)))(lambda val: test(fn(val))) +def TRegex(pobj): + """Checks whether a string matches a specific regular expression. + + @param pobj: Compiled regular expression as returned by C{re.compile} + + """ + desc = WithDesc("String matching regex \"%s\"" % + pobj.pattern.encode("string_escape")) + + return desc(TAnd(TString, pobj.match)) + + +def TMaybe(test): + """Wrap a test in a TOr(TNone, test). + + This makes it easier to define TMaybe* types. + + """ + return TOr(TNone, test) + + +def TMaybeValueNone(test): + """Used for unsetting values. + + """ + return TMaybe(TOr(TValueNone, test)) + + # Type aliases #: a non-empty string TNonEmptyString = WithDesc("NonEmptyString")(TAnd(TString, TTrue)) #: a maybe non-empty string -TMaybeString = TOr(TNonEmptyString, TNone) +TMaybeString = TMaybe(TNonEmptyString) #: a maybe boolean (bool or none) -TMaybeBool = TOr(TBool, TNone) +TMaybeBool = TMaybe(TBool) #: Maybe a dictionary (dict or None) -TMaybeDict = TOr(TDict, TNone) +TMaybeDict = TMaybe(TDict) -#: a positive integer -TPositiveInt = \ - TAnd(TInt, WithDesc("EqualGreaterZero")(lambda v: v >= 0)) +#: Maybe a list (list or None) +TMaybeList = TMaybe(TList) + +#: a non-negative integer (value >= 0) +TNonNegativeInt = \ + TAnd(TInt, WithDesc("EqualOrGreaterThanZero")(lambda v: v >= 0)) -#: a strictly positive integer -TStrictPositiveInt = \ +#: a positive integer (value > 0) +TPositiveInt = \ TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0)) +#: a maybe positive integer (positive integer or None) +TMaybePositiveInt = TMaybe(TPositiveInt) + +#: a negative integer (value < 0) +TNegativeInt = \ + TAnd(TInt, WithDesc("LessThanZero")(compat.partial(operator.gt, 0))) + +#: a positive float +TNonNegativeFloat = \ + TAnd(TFloat, WithDesc("EqualOrGreaterThanZero")(lambda v: v >= 0.0)) + +#: Job ID +TJobId = WithDesc("JobId")(TOr(TNonNegativeInt, + TRegex(re.compile("^%s$" % + constants.JOB_ID_TEMPLATE)))) + +#: Number +TNumber = TOr(TInt, TFloat) + +#: Relative job ID +TRelativeJobId = WithDesc("RelativeJobId")(TNegativeInt) + + +def TInstanceOf(cls): + """Checks if a given value is an instance of C{cls}. + + @type cls: class + @param cls: Class object + + """ + name = "%s.%s" % (cls.__module__, cls.__name__) + + desc = WithDesc("Instance of %s" % (Parens(name), )) + + return desc(lambda val: isinstance(val, cls)) + def TListOf(my_type): """Checks if a given value is a list with all elements of the same type. @@ -276,6 +412,9 @@ def TListOf(my_type): return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst))) +TMaybeListOf = lambda item_type: TMaybe(TListOf(item_type)) + + def TDictOf(key_type, val_type): """Checks a dict type for the type of its key/values. @@ -288,3 +427,73 @@ def TDictOf(key_type, val_type): compat.all(val_type(v) for v in container.values())) return desc(TAnd(TDict, fn)) + + +def _TStrictDictCheck(require_all, exclusive, items, val): + """Helper function for L{TStrictDict}. + + """ + notfound_fn = lambda _: not exclusive + + if require_all and not frozenset(val.keys()).issuperset(items.keys()): + # Requires items not found in value + return False + + return compat.all(items.get(key, notfound_fn)(value) + for (key, value) in val.items()) + + +def TStrictDict(require_all, exclusive, items): + """Strict dictionary check with specific keys. + + @type require_all: boolean + @param require_all: Whether all keys in L{items} are required + @type exclusive: boolean + @param exclusive: Whether only keys listed in L{items} should be accepted + @type items: dictionary + @param items: Mapping from key (string) to verification function + + """ + descparts = ["Dictionary containing"] + + if exclusive: + descparts.append(" none but the") + + if require_all: + descparts.append(" required") + + if len(items) == 1: + descparts.append(" key ") + else: + descparts.append(" keys ") + + descparts.append(utils.CommaJoin("\"%s\" (value %s)" % (key, value) + for (key, value) in items.items())) + + desc = WithDesc("".join(descparts)) + + return desc(TAnd(TDict, + compat.partial(_TStrictDictCheck, require_all, exclusive, + items))) + + +def TItems(items): + """Checks individual items of a container. + + If the verified value and the list of expected items differ in length, this + check considers only as many items as are contained in the shorter list. Use + L{TIsLength} to enforce a certain length. + + @type items: list + @param items: List of checks + + """ + assert items, "Need items" + + text = ["Item", "item"] + desc = WithDesc(utils.CommaJoin("%s %s is %s" % + (text[int(idx > 0)], idx, Parens(check)) + for (idx, check) in enumerate(items))) + + return desc(lambda value: compat.all(check(i) + for (check, i) in zip(items, value)))