Source code for taurus.core.util.decorator.typecheck

#!/usr/bin/env python

#############################################################################
##
# This file is part of Taurus
##
# http://taurus-scada.org
##
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
##
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
##
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
##
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
##
#############################################################################

"""
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)

"""

from __future__ import print_function
import sys

__all__ = ["accepts", "returns"]

__docformat__ = "restructuredtext"

[docs]def accepts(*types, **kw): """ Function decorator. Checks that inputs given to decorated function are of the expected type. :param types: The expected type of the decorated function's return value :param debug: Optional specification of 'debug' level (0 | 1 | 2) """ if not kw: # default level: MEDIUM debug = 1 else: debug = kw['debug'] try: def decorator(f): def newf(*args): if debug == 0: return f(*args) assert len(args) == len(types) argtypes = tuple(map(type, args)) if argtypes != types: msg = info(f.__name__, types, argtypes, 0) if debug == 1: print('TypeWarning: ', msg, file=sys.stderr) elif debug == 2: raise TypeError(msg) return f(*args) newf.__name__ = f.__name__ return newf return decorator except KeyError as key: raise KeyError(key + "is not a valid keyword argument") except TypeError as msg: raise TypeError(msg)
[docs]def returns(ret_type, **kw): """ Function decorator. Checks that return value of decorated function is of the expected type. :param ret_type: The expected type of the decorated function's return value. :param debug: Optional specification of 'debug' level (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 == 0: return result res_type = type(result) if res_type != ret_type: msg = info(f.__name__, (ret_type,), (res_type,), 1) if debug == 1: print('TypeWarning: ', msg, file=sys.stderr) elif debug == 2: raise TypeError(msg) return result newf.__name__ = f.__name__ return newf return decorator except KeyError as key: raise KeyError(key + "is not a valid keyword argument") except TypeError as msg: raise TypeError(msg)
[docs]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]) expected, actual = format(expected), format(actual) msg = "'%s' method " % fname \ + ("accepts", "returns")[flag] + " (%s), but " % expected\ + ("was given", "result is")[flag] + " (%s)" % actual return msg