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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | #!/usr/bin/env python
# -*- coding: iso-8859-1 -*-
################################################################################
#
# Method call parameters/return value type checking decorators.
# (c) 2006-2007, Dmitry Dvoinikov <dmitry@targeted.org>
# 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
|
What I would initially wanted such decorators to look like was: <pre> class foo: @takes(foo, int) def bar(self, i): return i </pre> so that foo().bar("happy") throws, or <pre> class foo: @returns(int) def bar(self, i): return i </pre> and foo().bar("pity") throws again (for different reason).
There appears to be a problem though - declaration such as <pre> class foo: @takes(foo) # foo is not known at this point def bar(self): ... </pre> is impossible, because foo is incomplete by the time the decorator picks it up.
I came up with the following workaround (arguably rather standard): <pre> class foo: @takes("foo") # "foo" is a name of a class, not a class itself def bar(self): ... </pre>
Keyword arguments can be checked as well:
<pre> @takes(foo = str, bar = optional(int)) def foo(**kwargs): return len(kwargs["foo"]) + kwargs.get("bar", 0) </pre>
One more thing that's nice to have is an ability to check protocols, rather that types. This can be done as <pre> @takes(with_attr("write", "flush") def foo(stream): stream.write() stream.flush() </pre>
Any callable can be used as a checker predicate as soon as it returns True/False as appropriate:
<pre> @takes(callable) def foo(c): ... </pre>
<pre> @takes(lambda x: x < 0) def foo(x): ... </pre>
Multiple "or" checks can be applied from a tuple (similar to isinstance() semantics): <pre> @takes((int, long, callable), (str, unicode)) def f(a, b): ... </pre>
Optional parameters can be checked like: <pre> @takes(optional(int, long)) def foo(i = None): ... </pre> where optional(x) is essentially an alias for (x, NoneType)
Likewise, in <pre> @takes(anything) @returns(nothing) def foo(): ... </pre> "anything" is an alias for lambda: True, while "nothing" is an alias for NoneType.
Finally, a few shortcut checkers exist:
<pre> @takes(by_regex("^foo$")) def foo(s): ... </pre>
<pre> @takes(with_attr("write", "flush")) def foo(stream): ... </pre>
<pre> @takes(one_of(1, 2)) def foo(stream): ... </pre>
Checkers can be nested, using the following structural checkers:
<pre> @takes(list_of(int)) def foo(lstint): return lstint[0] + 1 </pre>
<pre> @takes(tuple_of(by_regex("^[0-9]+$"))) def foo(tupstr): return int(tupstr[0]) </pre>
<pre> @takes(dict_of(str, callable)) def foo(callables): return callables"foo" </pre>
<pre> @takes(set_of(str)) def foo(attrs): return "good" in attrs </pre>
preview is your friend. You should reformat your code snippets in the discussion via <pre>, and verify it looks the way you want it to with the 'preview' button.
Good point. Yes, now it looks much better indeed.
Added predicates. Added a few useful predicates:
@takes(list_of(int))
@takes(tuple_of(int))
@takes(dict_of(str, str))
@takes(by_regex("^[0-9]{1,8}$"))
Added predicate. Added another predicate useful for checking protocol conformance:
@takes(with_attr("read", "write"))
Added one_of checker and support for checking kwargs
You could simplify:
to:
or perhaps:
Similarly for other shortcuts.
Thank you, I have incorporated the simplification you suggest.