#!/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
# 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" ]

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 FunctionChecker(Checker):

    def check(self, value):
        return self.reference(value)

Checker._registered.append((lambda x: isfunction(x) or isbuiltin(x), 
                            FunctionChecker))

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: lambda value: ListOfChecker(*args).check(value)

################################################################################

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: lambda value: TupleOfChecker(*args).check(value)

################################################################################

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: lambda value: DictOfChecker(*args).check(value)

################################################################################

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: lambda value: RegexChecker(*args).check(value)

################################################################################

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: lambda value: AttrChecker(*args).check(value)

################################################################################

class OneOfChecker(Checker):

    def __init__(self, *values):
        self.values = values

    def check(self, value):
        return value in self.values

one_of = lambda *args: lambda value: OneOfChecker(*args).check(value)

################################################################################

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):
                ## do not append the default parameters // 'DONT' by Pythy
                # 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

