slice page working. added temporarily core and util classes from manifold core
[unfold.git] / manifold / util / type.py
diff --git a/manifold/util/type.py b/manifold/util/type.py
new file mode 100644 (file)
index 0000000..5d7ab1e
--- /dev/null
@@ -0,0 +1,143 @@
+# http://wiki.python.org/moin/PythonDecoratorLibrary#Type_Enforcement_.28accepts.2Freturns.29
+'''
+One of three degrees of enforcement may be specified by passing
+the 'debug' keyword argument to the decorator:
+    0 -- NONE:   No type-checking. Decorators disabled.
+    1 -- MEDIUM: Print warning message to stderr. (Default)
+    2 -- STRONG: Raise TypeError with message.
+If 'debug' is not passed to the decorator, the default level is used.
+
+Example usage:
+    >>> NONE, MEDIUM, STRONG = 0, 1, 2
+    >>>
+    >>> @accepts(int, int, int)
+    ... @returns(float)
+    ... def average(x, y, z):
+    ...     return (x + y + z) / 2
+    ...
+    >>> average(5.5, 10, 15.0)
+    TypeWarning:  'average' method accepts (int, int, int), but was given
+    (float, int, float)
+    15.25
+    >>> average(5, 10, 15)
+    TypeWarning:  'average' method returns (float), but result is (int)
+    15
+
+Needed to cast params as floats in function def (or simply divide by 2.0).
+
+    >>> TYPE_CHECK = STRONG
+    >>> @accepts(int, debug=TYPE_CHECK)
+    ... @returns(int, debug=TYPE_CHECK)
+    ... def fib(n):
+    ...     if n in (0, 1): return n
+    ...     return fib(n-1) + fib(n-2)
+    ...
+    >>> fib(5.3)
+    Traceback (most recent call last):
+      ...
+    TypeError: 'fib' method accepts (int), but was given (float)
+
+'''
+import sys
+from itertools import izip
+
+def accepts(*types, **kw):
+    '''Function decorator. Checks decorated function's arguments are
+    of the expected types.
+
+    Parameters:
+    types -- The expected types of the inputs to the decorated function.
+             Must specify type for each parameter.
+    kw    -- Optional specification of 'debug' level (this is the only valid
+             keyword argument, no other should be given).
+             debug = ( 0 | 1 | 2 )
+
+    '''
+    if not kw:
+        # default level: MEDIUM
+        debug = 2
+    else:
+        debug = kw['debug']
+    try:
+        def decorator(f):
+            def newf(*args):
+                if debug is 0:
+                    return f(*args)
+                assert len(args) == len(types)
+                argtypes = tuple(map(type, args))
+                if not compare_types(types, argtypes):
+                # if argtypes != types:
+                    msg = info(f.__name__, types, argtypes, 0)
+                    if debug is 1:
+                        print >> sys.stderr, 'TypeWarning: ', msg
+                    elif debug is 2:
+                        raise TypeError, msg
+                return f(*args)
+            newf.__name__ = f.__name__
+            return newf
+        return decorator
+    except KeyError, key:
+        raise KeyError, key + "is not a valid keyword argument"
+    except TypeError, msg:
+        raise TypeError, msg
+
+def compare_types(expected, actual):
+    if isinstance(expected, tuple):
+        if isinstance(actual, tuple):
+            for x, y in izip(expected, actual):
+                if not compare_types(x ,y):
+                    return False
+            return True
+        else:
+            return actual == type(None) or actual in expected
+    else:
+        return actual == type(None) or actual == expected or issubclass(actual, expected)
+
+def returns(ret_type, **kw):
+    '''Function decorator. Checks decorated function's return value
+    is of the expected type.
+
+    Parameters:
+    ret_type -- The expected type of the decorated function's return value.
+                Must specify type for each parameter.
+    kw       -- Optional specification of 'debug' level (this is the only valid
+                keyword argument, no other should be given).
+                debug=(0 | 1 | 2)
+    '''
+    try:
+        if not kw:
+            # default level: MEDIUM
+            debug = 1
+        else:
+            debug = kw['debug']
+        def decorator(f):
+            def newf(*args):
+                result = f(*args)
+                if debug is 0:
+                    return result
+                res_type = type(result)
+                if not compare_types(ret_type, res_type): 
+                # if res_type != ret_type: # JORDAN: fix to allow for # StringTypes = (str, unicode)
+                # XXX note that this check should be recursive
+                    msg = info(f.__name__, (ret_type,), (res_type,), 1)
+                    if debug is 1:
+                        print >> sys.stderr, 'TypeWarning: ', msg
+                    elif debug is 2:
+                        raise TypeError, msg
+                return result
+            newf.__name__ = f.__name__
+            return newf
+        return decorator
+    except KeyError, key:
+        raise KeyError, key + "is not a valid keyword argument"
+    except TypeError, msg:
+        raise TypeError, msg
+
+def info(fname, expected, actual, flag):
+    '''Convenience function returns nicely formatted error/warning msg.'''
+    format = lambda types: ', '.join([str(t).split("'")[1] for t in types])
+    msg = "'{}' method ".format( fname )\
+          + ("accepts", "returns")[flag] + " ({}), but ".format(expected)\
+          + ("was given", "result is")[flag] + " ({})".format(actual)
+    return msg
+