#!/usr/bin/env python # -*- coding: iso-8859-1 -*- ################################################################################ # # Method call parameters/return value type checking decorators. # (c) 2006-2007, Dmitry Dvoinikov # Distributed under BSD license. # # Samples: # # from typecheck import * # # @takes(int, str) # takes int, str, upon a problem throws InputParameterError # @returns(int) # returns int, upon a problem throws ReturnValueError # def foo(i, s): # return i + len(s) # # @takes((int, long), by_regex("^[0-9]+$")) # int or long, numerical string # def foo(i, s, anything): # and the third parameter is not checked # ... # # @takes(int, int, foo = int, bar = optional(int)) # keyword argument foo must be int # def foo(a, b, **kwargs): # bar may be int or missing # ... # # Note: @takes for positional arguments, @takes for keyword arguments and @returns # all support the same checker syntax, for example for the following declaration # # @takes(C) # def foo(x): # ... # # then C may be one of the simple checkers: # # --------- C --------- ------------- semantics ------------- # typename ==> ok if x is is an instance of typename # "typename" ==> ok if x is is an instance of typename # with_attr("a", "b") ==> ok if x has specific attributes # some_callable ==> ok if some_callable(x) is True # one_of(1, "2") ==> ok if x is one of the literal values # by_regex("^foo$") ==> ok if x is a matching basestring # nothing ==> ok if x is None # anything ==> always ok # # simple checkers can further be combined with OR semantics using tuples: # # --------- C --------- ------------- semantics ------------- # (checker1, checker2) ==> ok if x conforms with either checker # # be optional: # # --------- C --------- ------------- semantics ------------- # optional(checker) ==> ok if x is checker-conformant or None # # or nested recursively into one of the following checkers # # --------- C --------- ------------- semantics ------------- # list_of(checker) ==> ok if x is a list of checker-conformant values # tuple_of(checker) ==> ok if x is a tuple of checker-conformant values # set_of(checker) ==> ok if x is a set of checker-conformant values # dict_of(key_checker, value_checker) ==> ok if x is a dict mapping key_checker- # conformant keys to value_checker-conformant values # # More samples: # # class foo(object): # @takes("foo", optional(int)) # foo, maybe int, but foo is yet incomplete # def __init__(self, i = None): # and is thus specified by name # ... # @takes("foo", int) # foo, and int if presents in args, # def bar(self, *args): # if args is empty, the check passes ok # ... # @takes("foo") # @returns(object) # returns foo which is fine, because # def biz(self): # foo is an object # return self # @classmethod # classmethod's and staticmethod's # @takes(type) # go same way # def baz(cls): # ... # # @takes(int) # @returns(optional("int", foo)) # returns either int, foo or NoneType # def bar(i): # "int" (rather than just int) is for fun # if i > 0: # return i # elif i == 0: # return foo() # otherwise returns NoneType # # @takes(callable) # built-in functions are treated as predicates # @returns(lambda x: x == 123) # and so do user-defined functions or lambdas # def execute(f, *args, **kwargs): # return f(*args, **kwargs) # # assert execute(execute, execute, execute, lambda x: x, 123) == 123 # # def readable(x): # user-defined type-checking predicate # return hasattr(x, "read") # # anything is an alias for predicate lambda: True, # nothing is an alias for NoneType, as in: # # @takes(callable, readable, optional(anything), optional(int)) # @returns(nothing) # def foo(f, r, x = None, i = None): # ... # # @takes(with_attr("read", "write")) # another way of protocol checking # def foo(pipe): # ... # # @takes(list_of(int)) # list of ints # def foo(x): # print x[0] # # @takes(tuple_of(callable)) # tuple of callables # def foo(x): # print x[0]() # # @takes(dict_of(str, list_of(int))) # dict mapping strs to lists of int # def foo(x): # print sum(x["foo"]) # # @takes(by_regex("^[0-9]{1,8}$")) # integer-as-a-string regex # def foo(x): # i = int(x) # # @takes(one_of(1, 2)) # must be equal to either one # def set_version(version): # ... # # The (3 times longer) source code with self-tests is available from: # http://www.targeted.org/python/recipes/typecheck.py # ################################################################################ __all__ = [ "takes", "InputParameterError", "returns", "ReturnValueError", "optional", "nothing", "anything", "list_of", "tuple_of", "dict_of", "by_regex", "with_attr", "one_of", "set_of" ] no_check = False # set this to True to turn all checks off ################################################################################ from inspect import getargspec, isfunction, isbuiltin, isclass from types import NoneType from re import compile as regex ################################################################################ def base_names(C): "Returns list of base class names for a given class" return [ x.__name__ for x in C.__mro__ ] ################################################################################ def type_name(v): "Returns the name of the passed value's type" return type(v).__name__ ################################################################################ class Checker(object): def __init__(self, reference): self.reference = reference def check(self, value): # abstract pass _registered = [] # a list of registered descendant class factories @staticmethod def create(value): # static factory method for f, t in Checker._registered: if f(value): return t(value) else: return None ################################################################################ class TypeChecker(Checker): def check(self, value): return isinstance(value, self.reference) Checker._registered.append((isclass, TypeChecker)) nothing = NoneType ################################################################################ class StrChecker(Checker): def check(self, value): value_base_names = base_names(type(value)) return self.reference in value_base_names or "instance" in value_base_names Checker._registered.append((lambda x: isinstance(x, str), StrChecker)) ################################################################################ class TupleChecker(Checker): def __init__(self, reference): self.reference = map(Checker.create, reference) def check(self, value): return reduce(lambda r, c: r or c.check(value), self.reference, False) Checker._registered.append((lambda x: isinstance(x, tuple) and not filter(lambda y: Checker.create(y) is None, x), TupleChecker)) optional = lambda *args: args + (NoneType, ) ################################################################################ class CallableChecker(Checker): def check(self, value): return self.reference(value) # note that the callable check is the most relaxed of all, therefore it should # be registered last, after all the more specific cases have been registered Checker._registered.append((callable, CallableChecker)) anything = lambda *args: True ################################################################################ class ListOfChecker(Checker): def __init__(self, reference): self.reference = Checker.create(reference) def check(self, value): return isinstance(value, list) and \ not filter(lambda e: not self.reference.check(e), value) list_of = lambda *args: ListOfChecker(*args).check ################################################################################ class TupleOfChecker(Checker): def __init__(self, reference): self.reference = Checker.create(reference) def check(self, value): return isinstance(value, tuple) and \ not filter(lambda e: not self.reference.check(e), value) tuple_of = lambda *args: TupleOfChecker(*args).check ################################################################################ class SetOfChecker(Checker): def __init__(self, reference): self.reference = Checker.create(reference) def check(self, value): return isinstance(value, set) and \ not filter(lambda e: not self.reference.check(e), value) set_of = lambda *args: SetOfChecker(*args).check ################################################################################ class DictOfChecker(Checker): def __init__(self, key_reference, value_reference): self.key_reference = Checker.create(key_reference) self.value_reference = Checker.create(value_reference) def check(self, value): return isinstance(value, dict) and \ not filter(lambda e: not self.key_reference.check(e), value.iterkeys()) and \ not filter(lambda e: not self.value_reference.check(e), value.itervalues()) dict_of = lambda *args: DictOfChecker(*args).check ################################################################################ class RegexChecker(Checker): def __init__(self, reference): self.reference = regex(reference) def check(self, value): return isinstance(value, basestring) and self.reference.match(value) by_regex = lambda *args: RegexChecker(*args).check ################################################################################ class AttrChecker(Checker): def __init__(self, *attrs): self.attrs = attrs def check(self, value): return reduce(lambda r, c: r and c, map(lambda a: hasattr(value, a), self.attrs), True) with_attr = lambda *args: AttrChecker(*args).check ################################################################################ class OneOfChecker(Checker): def __init__(self, *values): self.values = values def check(self, value): return value in self.values one_of = lambda *args: OneOfChecker(*args).check ################################################################################ def takes(*args, **kwargs): "Method signature checking decorator" # convert decorator arguments into a list of checkers checkers = [] for i, arg in enumerate(args): checker = Checker.create(arg) if checker is None: raise TypeError("@takes decorator got parameter %d of unsupported " "type %s" % (i + 1, type_name(arg))) checkers.append(checker) kwcheckers = {} for kwname, kwarg in kwargs.iteritems(): checker = Checker.create(kwarg) if checker is None: raise TypeError("@takes decorator got parameter %s of unsupported " "type %s" % (kwname, type_name(kwarg))) kwcheckers[kwname] = checker if no_check: # no type checking is performed, return decorated method itself def takes_proxy(method): return method else: def takes_proxy(method): method_args, method_defaults = getargspec(method)[0::3] def takes_invocation_proxy(*args, **kwargs): # append the default parameters if method_defaults is not None and len(method_defaults) > 0 \ and len(method_args) - len(method_defaults) <= len(args) < len(method_args): args += method_defaults[len(args) - len(method_args):] # check the types of the actual call parameters for i, (arg, checker) in enumerate(zip(args, checkers)): if not checker.check(arg): raise InputParameterError("%s() got invalid parameter " "%d of type %s" % (method.__name__, i + 1, type_name(arg))) for kwname, checker in kwcheckers.iteritems(): if not checker.check(kwargs.get(kwname, None)): raise InputParameterError("%s() got invalid parameter " "%s of type %s" % (method.__name__, kwname, type_name(kwargs.get(kwname, None)))) return method(*args, **kwargs) takes_invocation_proxy.__name__ = method.__name__ return takes_invocation_proxy return takes_proxy class InputParameterError(TypeError): pass ################################################################################ def returns(sometype): "Return type checking decorator" # convert decorator argument into a checker checker = Checker.create(sometype) if checker is None: raise TypeError("@returns decorator got parameter of unsupported " "type %s" % type_name(sometype)) if no_check: # no type checking is performed, return decorated method itself def returns_proxy(method): return method else: def returns_proxy(method): def returns_invocation_proxy(*args, **kwargs): result = method(*args, **kwargs) if not checker.check(result): raise ReturnValueError("%s() has returned an invalid " "value of type %s" % (method.__name__, type_name(result))) return result returns_invocation_proxy.__name__ = method.__name__ return returns_invocation_proxy return returns_proxy class ReturnValueError(TypeError): pass ################################################################################ # EOF