pytils/third/aspn426123.py
author pythy <the.pythy@gmail.com>
Mon Sep 22 23:42:30 2008 +0700 (5 weeks ago)
changeset 105 1ff4355c8413
permissions -rw-r--r--
Make .hgignore
     1 #!/usr/bin/env python
     2 # -*- coding: iso-8859-1 -*-
     3 ################################################################################
     4 #
     5 # Method call parameters/return value type checking decorators.
     6 # (c) 2006-2007, Dmitry Dvoinikov <[email protected]>
     7 # Distributed under BSD license.
     8 #
     9 # Samples:
    10 #
    11 # from typecheck import *
    12 #
    13 # @takes(int, str) # takes int, str, upon a problem throws InputParameterError
    14 # @returns(int)    # returns int, upon a problem throws ReturnValueError
    15 # def foo(i, s): 
    16 #     return i + len(s)
    17 #
    18 # @takes((int, long), by_regex("^[0-9]+$")) # int or long, numerical string
    19 # def foo(i, s, anything):                  # and the third parameter is not checked
    20 #     ...
    21 #
    22 # @takes(int, int, foo = int, bar = optional(int)) # keyword argument foo must be int
    23 # def foo(a, b, **kwargs):                         # bar may be int or missing
    24 #     ...
    25 #
    26 # Note: @takes for positional arguments, @takes for keyword arguments and @returns
    27 # all support the same checker syntax, for example for the following declaration
    28 # 
    29 # @takes(C)
    30 # def foo(x):
    31 #     ...
    32 #
    33 # then C may be one of the simple checkers:
    34 #
    35 # --------- C ---------     ------------- semantics -------------
    36 # typename              ==> ok if x is is an instance of typename
    37 # "typename"            ==> ok if x is is an instance of typename
    38 # with_attr("a", "b")   ==> ok if x has specific attributes
    39 # some_callable         ==> ok if some_callable(x) is True
    40 # one_of(1, "2")        ==> ok if x is one of the literal values
    41 # by_regex("^foo$")     ==> ok if x is a matching basestring
    42 # nothing               ==> ok if x is None
    43 # anything              ==> always ok
    44 #
    45 # simple checkers can further be combined with OR semantics using tuples:
    46 #
    47 # --------- C ---------     ------------- semantics -------------
    48 # (checker1, checker2)  ==> ok if x conforms with either checker
    49 #
    50 # be optional:
    51 #
    52 # --------- C ---------     ------------- semantics -------------
    53 # optional(checker)     ==> ok if x is checker-conformant or None
    54 #
    55 # or nested recursively into one of the following checkers
    56 #
    57 # --------- C ---------     ------------- semantics -------------
    58 # list_of(checker)      ==> ok if x is a list of checker-conformant values
    59 # tuple_of(checker)     ==> ok if x is a tuple of checker-conformant values
    60 # dict_of(key_checker, value_checker) ==> ok if x is a dict mapping key_checker-
    61 #                           conformant keys to value_checker-conformant values
    62 #
    63 # More samples:
    64 #
    65 # class foo(object):
    66 #     @takes("foo", optional(int))  # foo, maybe int, but foo is yet incomplete
    67 #     def __init__(self, i = None): # and is thus specified by name
    68 #         ...
    69 #     @takes("foo", int)            # foo, and int if presents in args,
    70 #     def bar(self, *args):         # if args is empty, the check passes ok
    71 #         ...
    72 #     @takes("foo")                 
    73 #     @returns(object)              # returns foo which is fine, because
    74 #     def biz(self):                # foo is an object
    75 #         return self
    76 #     @classmethod                  # classmethod's and staticmethod's
    77 #     @takes(type)                  # go same way
    78 #     def baz(cls):
    79 #         ...
    80 #    
    81 # @takes(int)
    82 # @returns(optional("int", foo))    # returns either int, foo or NoneType
    83 # def bar(i):                       # "int" (rather than just int) is for fun
    84 #     if i > 0: 
    85 #         return i
    86 #     elif i == 0:
    87 #         return foo()              # otherwise returns NoneType
    88 #
    89 # @takes(callable)                  # built-in functions are treated as predicates
    90 # @returns(lambda x: x == 123)      # and so do user-defined functions or lambdas
    91 # def execute(f, *args, **kwargs):
    92 #     return f(*args, **kwargs)
    93 #
    94 # assert execute(execute, execute, execute, lambda x: x, 123) == 123
    95 #
    96 # def readable(x):                  # user-defined type-checking predicate
    97 #     return hasattr(x, "read")
    98 #
    99 # anything is an alias for predicate lambda: True,
   100 # nothing is an alias for NoneType, as in:
   101 #
   102 # @takes(callable, readable, optional(anything), optional(int))
   103 # @returns(nothing)
   104 # def foo(f, r, x = None, i = None):
   105 #     ...
   106 #
   107 # @takes(with_attr("read", "write")) # another way of protocol checking
   108 # def foo(pipe):
   109 #     ...
   110 #
   111 # @takes(list_of(int))              # list of ints
   112 # def foo(x):
   113 #     print x[0]
   114 #
   115 # @takes(tuple_of(callable))        # tuple of callables
   116 # def foo(x):
   117 #     print x[0]()
   118 #
   119 # @takes(dict_of(str, list_of(int))) # dict mapping strs to lists of int
   120 # def foo(x):
   121 #     print sum(x["foo"])
   122 #
   123 # @takes(by_regex("^[0-9]{1,8}$"))  # integer-as-a-string regex
   124 # def foo(x):
   125 #     i = int(x)
   126 #
   127 # @takes(one_of(1, 2))              # must be equal to either one
   128 # def set_version(version):
   129 #     ...
   130 #
   131 # The (3 times longer) source code with self-tests is available from:
   132 # http://www.targeted.org/python/recipes/typecheck.py
   133 #
   134 ################################################################################
   135 
   136 __all__ = [ "takes", "InputParameterError", "returns", "ReturnValueError", 
   137             "optional", "nothing", "anything", "list_of", "tuple_of", "dict_of",
   138             "by_regex", "with_attr", "one_of" ]
   139 
   140 no_check = False # set this to True to turn all checks off
   141 
   142 ################################################################################
   143 
   144 from inspect import getargspec, isfunction, isbuiltin, isclass
   145 from types import NoneType
   146 from re import compile as regex
   147 
   148 ################################################################################
   149 
   150 def base_names(C):
   151     "Returns list of base class names for a given class"
   152     return [ x.__name__ for x in C.__mro__ ]
   153     
   154 ################################################################################
   155 
   156 def type_name(v):
   157     "Returns the name of the passed value's type"
   158     return type(v).__name__
   159 
   160 ################################################################################
   161 
   162 class Checker(object):
   163 
   164     def __init__(self, reference):
   165         self.reference = reference
   166 
   167     def check(self, value): # abstract
   168         pass
   169 
   170     _registered = [] # a list of registered descendant class factories
   171 
   172     @staticmethod
   173     def create(value): # static factory method
   174         for f, t in Checker._registered:
   175             if f(value):
   176                 return t(value)
   177         else:
   178             return None
   179 
   180 ################################################################################
   181 
   182 class TypeChecker(Checker):
   183 
   184     def check(self, value):
   185         return isinstance(value, self.reference)
   186 
   187 Checker._registered.append((isclass, TypeChecker))
   188 
   189 nothing = NoneType
   190 
   191 ################################################################################
   192 
   193 class StrChecker(Checker):
   194 
   195     def check(self, value):
   196         value_base_names = base_names(type(value))
   197         return self.reference in value_base_names or "instance" in value_base_names
   198    
   199 Checker._registered.append((lambda x: isinstance(x, str), StrChecker))
   200 
   201 ################################################################################
   202 
   203 class TupleChecker(Checker):
   204 
   205     def __init__(self, reference):
   206         self.reference = map(Checker.create, reference)
   207 
   208     def check(self, value):
   209         return reduce(lambda r, c: r or c.check(value), self.reference, False)
   210 
   211 Checker._registered.append((lambda x: isinstance(x, tuple) and not
   212                                       filter(lambda y: Checker.create(y) is None,
   213                                              x), 
   214                             TupleChecker))
   215 
   216 optional = lambda *args: args + (NoneType, )
   217 
   218 ################################################################################
   219 
   220 class FunctionChecker(Checker):
   221 
   222     def check(self, value):
   223         return self.reference(value)
   224 
   225 Checker._registered.append((lambda x: isfunction(x) or isbuiltin(x), 
   226                             FunctionChecker))
   227 
   228 anything = lambda *args: True
   229 
   230 ################################################################################
   231 
   232 class ListOfChecker(Checker):
   233 
   234     def __init__(self, reference):
   235         self.reference = Checker.create(reference)
   236 
   237     def check(self, value):
   238         return isinstance(value, list) and \
   239                not filter(lambda e: not self.reference.check(e), value)
   240 
   241 list_of = lambda *args: lambda value: ListOfChecker(*args).check(value)
   242 
   243 ################################################################################
   244 
   245 class TupleOfChecker(Checker):
   246 
   247     def __init__(self, reference):
   248         self.reference = Checker.create(reference)
   249 
   250     def check(self, value):
   251         return isinstance(value, tuple) and \
   252                not filter(lambda e: not self.reference.check(e), value)
   253 
   254 tuple_of = lambda *args: lambda value: TupleOfChecker(*args).check(value)
   255 
   256 ################################################################################
   257 
   258 class DictOfChecker(Checker):
   259 
   260     def __init__(self, key_reference, value_reference):
   261         self.key_reference = Checker.create(key_reference)
   262         self.value_reference = Checker.create(value_reference)
   263 
   264     def check(self, value):
   265         return isinstance(value, dict) and \
   266                not filter(lambda e: not self.key_reference.check(e), value.iterkeys()) and \
   267                not filter(lambda e: not self.value_reference.check(e), value.itervalues())
   268 
   269 dict_of = lambda *args: lambda value: DictOfChecker(*args).check(value)
   270 
   271 ################################################################################
   272 
   273 class RegexChecker(Checker):
   274 
   275     def __init__(self, reference):
   276         self.reference = regex(reference)
   277 
   278     def check(self, value):
   279         return isinstance(value, basestring) and self.reference.match(value)
   280 
   281 by_regex = lambda *args: lambda value: RegexChecker(*args).check(value)
   282 
   283 ################################################################################
   284 
   285 class AttrChecker(Checker):
   286 
   287     def __init__(self, *attrs):
   288         self.attrs = attrs
   289 
   290     def check(self, value):
   291         return reduce(lambda r, c: r and c, map(lambda a: hasattr(value, a), self.attrs), True)
   292 
   293 with_attr = lambda *args: lambda value: AttrChecker(*args).check(value)
   294 
   295 ################################################################################
   296 
   297 class OneOfChecker(Checker):
   298 
   299     def __init__(self, *values):
   300         self.values = values
   301 
   302     def check(self, value):
   303         return value in self.values
   304 
   305 one_of = lambda *args: lambda value: OneOfChecker(*args).check(value)
   306 
   307 ################################################################################
   308 
   309 def takes(*args, **kwargs):
   310     "Method signature checking decorator"
   311 
   312     # convert decorator arguments into a list of checkers
   313 
   314     checkers = []
   315     for i, arg in enumerate(args):
   316         checker = Checker.create(arg)
   317         if checker is None:
   318             raise TypeError("@takes decorator got parameter %d of unsupported "
   319                             "type %s" % (i + 1, type_name(arg)))
   320         checkers.append(checker)
   321 
   322     kwcheckers = {}
   323     for kwname, kwarg in kwargs.iteritems():
   324         checker = Checker.create(kwarg)
   325         if checker is None:
   326             raise TypeError("@takes decorator got parameter %s of unsupported "
   327                             "type %s" % (kwname, type_name(kwarg)))
   328         kwcheckers[kwname] = checker
   329 
   330     if no_check: # no type checking is performed, return decorated method itself
   331 
   332         def takes_proxy(method):
   333             return method        
   334 
   335     else:
   336 
   337         def takes_proxy(method):
   338             
   339             method_args, method_defaults = getargspec(method)[0::3]
   340 
   341             def takes_invocation_proxy(*args, **kwargs):
   342                 ## do not append the default parameters // 'DONT' by Pythy
   343                 # if method_defaults is not None and len(method_defaults) > 0 \
   344                 # and len(method_args) - len(method_defaults) <= len(args) < len(method_args):
   345                 #    args += method_defaults[len(args) - len(method_args):]
   346 
   347                 # check the types of the actual call parameters
   348 
   349                 for i, (arg, checker) in enumerate(zip(args, checkers)):
   350                     if not checker.check(arg):
   351                         raise InputParameterError("%s() got invalid parameter "
   352                                                   "%d of type %s" %
   353                                                   (method.__name__, i + 1, 
   354                                                    type_name(arg)))
   355 
   356                 for kwname, checker in kwcheckers.iteritems():
   357                     if not checker.check(kwargs.get(kwname, None)):
   358                         raise InputParameterError("%s() got invalid parameter "
   359                                                   "%s of type %s" %
   360                                                   (method.__name__, kwname, 
   361                                                    type_name(kwargs.get(kwname, None))))
   362                 return method(*args, **kwargs)
   363 
   364             takes_invocation_proxy.__name__ = method.__name__
   365             return takes_invocation_proxy
   366     
   367     return takes_proxy
   368 
   369 class InputParameterError(TypeError): pass
   370 
   371 ################################################################################
   372 
   373 def returns(sometype):
   374     "Return type checking decorator"
   375 
   376     # convert decorator argument into a checker
   377 
   378     checker = Checker.create(sometype)
   379     if checker is None:
   380         raise TypeError("@returns decorator got parameter of unsupported "
   381                         "type %s" % type_name(sometype))
   382 
   383     if no_check: # no type checking is performed, return decorated method itself
   384 
   385         def returns_proxy(method):
   386             return method
   387 
   388     else:
   389 
   390         def returns_proxy(method):
   391             
   392             def returns_invocation_proxy(*args, **kwargs):
   393                 
   394                 result = method(*args, **kwargs)
   395                 
   396                 if not checker.check(result):
   397                     raise ReturnValueError("%s() has returned an invalid "
   398                                            "value of type %s" % 
   399                                            (method.__name__, type_name(result)))
   400 
   401                 return result
   402     
   403             returns_invocation_proxy.__name__ = method.__name__
   404             return returns_invocation_proxy
   405         
   406     return returns_proxy
   407 
   408 class ReturnValueError(TypeError): pass
   409 
   410 ################################################################################
   411 # EOF
   412