|
Description:
This recipe adds parameter type checking to each method or function invocation. Not a replacement for static typing, but it makes a nice pair of shackles to me.
Source: Text Source
__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
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):
pass
_registered = []
@staticmethod
def create(value):
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)
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"
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:
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):
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):]
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"
checker = Checker.create(sometype)
if checker is None:
raise TypeError("@returns decorator got parameter of unsupported "
"type %s" % type_name(sometype))
if no_check:
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
Discussion:
What I would initially wanted such decorators to look like was:
class foo:
@takes(foo, int)
def bar(self, i):
return i
so that foo().bar("happy") throws, or
class foo:
@returns(int)
def bar(self, i):
return i
and foo().bar("pity") throws again (for different reason).
There appears to be a problem though - declaration such as
class foo:
@takes(foo) # foo is not known at this point
def bar(self):
...
is impossible, because foo is incomplete by the time the decorator picks it up.
I came up with the following workaround (arguably rather standard):
class foo:
@takes("foo") # "foo" is a name of a class, not a class itself
def bar(self):
...
Keyword arguments can be checked as well:
@takes(foo = str, bar = optional(int))
def foo(**kwargs):
return len(kwargs["foo"]) + kwargs.get("bar", 0)
One more thing that's nice to have is an ability to check protocols, rather that types. This can be done as
@takes(with_attr("write", "flush")
def foo(stream):
stream.write()
stream.flush()
Any callable can be used as a checker predicate as soon as it returns True/False as appropriate:
@takes(callable)
def foo(c):
...
@takes(lambda x: x < 0)
def foo(x):
...
Multiple "or" checks can be applied from a tuple (similar to isinstance() semantics):
@takes((int, long, callable), (str, unicode))
def f(a, b):
...
Optional parameters can be checked like:
@takes(optional(int, long))
def foo(i = None):
...
where optional(x) is essentially an alias for (x, NoneType)
Likewise, in
@takes(anything)
@returns(nothing)
def foo():
...
"anything" is an alias for lambda: True, while "nothing" is an alias for NoneType.
Finally, a few shortcut checkers exist:
@takes(by_regex("^foo$"))
def foo(s):
...
@takes(with_attr("write", "flush"))
def foo(stream):
...
@takes(one_of(1, 2))
def foo(stream):
...
Checkers can be nested, using the following structural checkers:
@takes(list_of(int))
def foo(lstint):
return lstint[0] + 1
@takes(tuple_of(by_regex("^[0-9]+$")))
def foo(tupstr):
return int(tupstr[0])
@takes(dict_of(str, callable))
def foo(callables):
return callables["foo"]()
@takes(set_of(str))
def foo(attrs):
return "good" in attrs
|