File indexing completed on 2024-07-14 15:05:09

0001 # -*- coding: utf-8 -*-
0002 #   Copyright (C) 2009 Stephan Peijnik (stephan@peijnik.at)
0003 #
0004 #    This program is free software: you can redistribute it and/or modify
0005 #    it under the terms of the GNU Lesser General Public License as published by
0006 #    the Free Software Foundation, either version 3 of the License, or
0007 #    (at your option) any later version.
0008 #
0009 #    This program is distributed in the hope that it will be useful,
0010 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
0011 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012 #    GNU Lesser General Public License for more details.
0013 #
0014 #    You should have received a copy of the GNU Lesser General Public License
0015 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0016 
0017 __version__ = '0.9.0'
0018 
0019 import inspect
0020 import os
0021 import warnings
0022 
0023 from new import classobj
0024 
0025 __all__ = ['ArgvalidateException',
0026     'DecoratorNonKeyLengthException', 'DecoratorKeyUnspecifiedException',
0027     'DecoratorStackingException', 'ArgumentTypeException',
0028     'func_args', 'method_args', 'return_value',
0029     'one_of', 'type_any', 'raises_exceptions', 'warns_kwarg_as_arg',
0030     'accepts', 'returns']
0031 
0032 # Check for environment variables
0033 argvalidate_warn = 0
0034 if 'ARGVALIDATE_WARN' in os.environ:
0035     argvalidate_warn_str = os.environ['ARGVALIDATE_WARN']
0036     try:
0037         argvalidate_warn = int(argvalidate_warn_str)
0038     except ValueError:
0039         pass
0040 
0041 argvalidate_warn_kwarg_as_arg = 0
0042 if 'ARGVALIDATE_WARN_KWARG_AS_ARG' in os.environ:
0043     argvalidate_warn_kwarg_as_arg_str =\
0044          os.environ['ARGVALIDATE_WARN_KWARG_AS_ARG']
0045     try:
0046         argvalidate_warn_kwarg_as_arg =\
0047             int(argvalidate_warn_kwarg_as_arg_str)
0048     except ValueError:
0049         pass
0050 
0051 class ArgvalidateException(Exception):
0052     """
0053     Base argvalidate exception.
0054 
0055     Used as base for all exceptions.
0056 
0057     """
0058     pass
0059 
0060 
0061 
0062 class DecoratorNonKeyLengthException(ArgvalidateException):
0063     """
0064     Exception for invalid decorator non-keyword argument count.
0065 
0066     This exception provides the following attributes:
0067 
0068     * func_name
0069         Name of function that caused the exception to be raised
0070         (str, read-only).
0071 
0072     * expected_count
0073         Number of arguments that were expected (int, read-only).
0074 
0075     * passed_count
0076         Number of arguments that were passed to the function (int, read-only).
0077 
0078     """
0079     def __init__(self, func_name, expected_count, passed_count):
0080         self.func_name = func_name
0081         self.expected_count = expected_count
0082         self.passed_count = passed_count
0083         msg = '%s: wrong number of non-keyword arguments specified in' %\
0084              (func_name) 
0085         msg += ' decorator (expected %d, got %d).' %\
0086              (expected_count, passed_count)
0087         ArgvalidateException.__init__(self, msg)
0088 
0089 class DecoratorKeyUnspecifiedException(ArgvalidateException):
0090     """
0091     Exception for unspecified decorator keyword argument.
0092 
0093     This exception provides the following attributes:
0094 
0095     * func_name
0096         Name of function that caused the exception to be raised
0097         (str, read-only).
0098 
0099     * arg_name
0100         Name of the keyword argument passed to the function, but not specified
0101         in the decorator (str, read-only).
0102         
0103     """
0104     def __init__(self, func_name, arg_name):
0105         self.func_name = func_name
0106         self.arg_name = arg_name
0107         msg = '%s: keyword argument %s not specified in decorator.' %\
0108             (func_name, arg_name)
0109         ArgvalidateException.__init__(self, msg)
0110 
0111 class DecoratorStackingException(ArgvalidateException):
0112     """
0113     Exception for stacking a decorator with itself.
0114 
0115     This exception provides the following attributes:
0116 
0117     * func_name
0118         Name of function that caused the exception to be raised
0119         (str, read-only).
0120 
0121     * decorator_name
0122         Name of the decorator that was stacked with itself (str, read-only).
0123 
0124     """
0125     def __init__(self, func_name, decorator_name):
0126         self.func_name = func_name
0127         self.decorator_name = decorator_name
0128         msg = '%s: decorator %s stacked with itself.' %\
0129             (func_name, decorator_name)
0130         ArgvalidateException.__init__(self, msg)
0131 
0132 class ArgumentTypeException(ArgvalidateException):
0133     """
0134     Exception for invalid argument type.
0135 
0136     This exception provides the following attributes:
0137 
0138     * func_name
0139         Name of function that caused the exception to be raised
0140         (str, read-only).
0141 
0142     * arg_name
0143         Name of the keyword argument passed to the function, but not specified
0144         in the decorator (str, read-only).
0145 
0146     * expected_type
0147         Argument type that was expected (type, read-only).
0148 
0149     * passed_type
0150         Argument type that was passed to the function (type, read-only).
0151 
0152     """
0153     def __init__(self, func_name, arg_name, expected_type, passed_type):
0154         self.func_name = func_name
0155         self.arg_name = arg_name
0156         self.expected_type = expected_type
0157         self.passed_type = passed_type
0158         msg = '%s: invalid argument type for %r (expected %r, got %r).' %\
0159             (func_name, arg_name, expected_type, passed_type)
0160         ArgvalidateException.__init__(self, msg)
0161 
0162 class ReturnValueTypeException(ArgvalidateException):
0163     """
0164     Exception for invalid return value type.
0165 
0166     This exception provides the following attributes:
0167 
0168     * func_name
0169         Name of function that caused the exception to be raised
0170         (string, read-only).
0171 
0172     * expected_type
0173         Argument type that was expected (type, read-only).
0174 
0175     * passed_type
0176         Type of value returned by the function (type, read-only).
0177 
0178     """
0179     def __init__(self, func_name, expected_type, passed_type):
0180         self.func_name = func_name
0181         self.expected_type = expected_type
0182         self.passed_type = passed_type
0183         msg = '%s: invalid type for return value (expected %r, got %r).' %\
0184             (func_name, expected_type, passed_type)
0185         ArgvalidateException.__init__(self, msg)
0186 
0187 class KWArgAsArgWarning(ArgvalidateException):
0188     def __init__(self, func_name, arg_name):
0189         msg = '%s: argument %s is a keyword argument and was passed as a '\
0190             'non-keyword argument.' % (func_name, arg_name)
0191         ArgvalidateException.__init__(self, msg)
0192 
0193 def __raise(exception, stacklevel=3):
0194     if argvalidate_warn:
0195         warnings.warn(exception, stacklevel=stacklevel)
0196     else:
0197         raise exception
0198 
0199 def __check_return_value(func_name, expected_type, return_value):
0200     return_value_type = type(return_value)
0201     error = False
0202 
0203     if expected_type is None:
0204         error = False
0205 
0206     elif isinstance(return_value, classobj):
0207         if not isinstance(return_value, expected_type) and\
0208             not issubclass(return_value.__class__, expected_type):
0209                 error=True
0210     else:
0211         if not isinstance(return_value, expected_type):
0212             error=True
0213 
0214     if error:
0215         __raise(ReturnValueTypeException(func_name, expected_type,\
0216              return_value_type), stacklevel=3)
0217 
0218 def __check_type(func_name, arg_name, expected_type, passed_value,\
0219     stacklevel=4):
0220     passed_type = type(passed_value)
0221     error=False
0222 
0223     # None means the type is not checked
0224     if expected_type is None:
0225         error=False
0226 
0227     # Check a class
0228     elif isinstance(passed_value, classobj):
0229         if not isinstance(passed_value, expected_type) and\
0230             not issubclass(passed_value.__class__, expected_type):
0231             error=True
0232     
0233     # Check a type
0234     else:
0235         if not isinstance(passed_value, expected_type):
0236             error=True
0237 
0238     if error:
0239         __raise(ArgumentTypeException(func_name, arg_name, expected_type,\
0240             passed_type), stacklevel=stacklevel)
0241 
0242 def __check_args(type_args, type_kwargs, start=-1):
0243     type_nonkey_argcount = len(type_args)
0244     type_key_argcount = len(type_kwargs)
0245 
0246     def validate(f):
0247         accepts_func = getattr(f, 'argvalidate_accepts_stacked_func', None)
0248         
0249         if accepts_func:
0250             if start == -1:
0251                 raise DecoratorStackingException(accepts_func.func_name,\
0252                     'accepts')
0253             if start == 0:
0254                 raise DecoratorStackingException(accepts_func.func_name,\
0255                      'function_accepts')
0256             elif start == 1:
0257                 raise DecoratorStackingException(accepts_func.func_name,\
0258                      'method_accepts')
0259             else:
0260                 raise DecoratorStackingException(accepts_func.func_name,\
0261                      'unknown; start=%d' % (start))
0262 
0263         func = getattr(f, 'argvalidate_returns_stacked_func', f)
0264         f_name = func.__name__
0265         (func_args, func_varargs, func_varkw, func_defaults) =\
0266              inspect.getargspec(func)
0267 
0268         func_argcount = len(func_args)
0269         is_method = True
0270 
0271         # The original idea was to use inspect.ismethod here,
0272         # but it seems as the decorator is called before the
0273         # method is bound to a class, so this will always
0274         # return False.
0275         # The new method follows the original idea of checking
0276         # tha name of the first argument passed.
0277         # self and cls indicate methods, everything else indicates
0278         # a function.
0279         if start < 0 and func_argcount > 0 and func_args[0] in ['self', 'cls']:
0280             func_argcount -= 1
0281             func_args = func_args[1:]
0282         elif start == 1:
0283             func_argcount -=1
0284             func_args = func_args[1:]
0285         else:
0286             is_method = False
0287 
0288         if func_varargs:
0289             func_args.remove(func_varargs)
0290             
0291         if func_varkw:
0292             func_args.remove(func_varkw)
0293 
0294         func_key_args = {}
0295         func_key_argcount = 0
0296 
0297         if func_defaults:
0298             func_key_argcount = len(func_defaults)
0299             tmp_key_args = zip(func_args[-func_key_argcount:], func_defaults)
0300 
0301             for tmp_key_name, tmp_key_default in tmp_key_args:
0302                 func_key_args.update({tmp_key_name: tmp_key_default})
0303 
0304             # Get rid of unused variables
0305             del tmp_key_args
0306             del tmp_key_name
0307             del tmp_key_default
0308 
0309         func_nonkey_args = []
0310         if func_key_argcount < func_argcount:
0311             func_nonkey_args = func_args[:func_argcount-func_key_argcount]
0312         func_nonkey_argcount = len(func_nonkey_args)
0313 
0314         # Static check #0:
0315         #
0316         # Checking the lengths of type_args vs. func_args and type_kwargs vs.
0317         # func_key_args should be done here.
0318         #
0319         # This means that the check is only performed when the decorator
0320         # is actually invoked, not every time the target function is called.
0321         if func_nonkey_argcount != type_nonkey_argcount:
0322             __raise(DecoratorNonKeyLengthException(f_name,\
0323                 func_nonkey_argcount, type_nonkey_argcount))
0324                 
0325         if func_key_argcount != type_key_argcount:
0326             __raise(DecoratorKeyLengthException(f_name,\
0327                 func_key_argcount, type_key_argcount))
0328 
0329         # Static check #1:
0330         #
0331         # kwarg default value types.
0332         if func_defaults:
0333             tmp_kw_zip = zip(func_args[-func_key_argcount:], func_defaults)
0334             for tmp_kw_name, tmp_kw_default in tmp_kw_zip:
0335                 if not tmp_kw_name in type_kwargs:
0336                     __raise(DecoratorKeyUnspecifiedException(f_name,\
0337                          tmp_kw_name))
0338                 
0339                 tmp_kw_type = type_kwargs[tmp_kw_name]
0340                 __check_type(f_name, tmp_kw_name, tmp_kw_type, tmp_kw_default)
0341             
0342             del tmp_kw_type
0343             del tmp_kw_name
0344             del tmp_kw_default
0345             del tmp_kw_zip
0346 
0347         def __wrapper_func(*call_args, **call_kwargs):
0348             call_nonkey_argcount = len(call_args)
0349             call_key_argcount = len(call_kwargs)
0350             call_nonkey_args = []
0351 
0352             if is_method:
0353                 call_nonkey_args = call_args[1:]
0354             else:
0355                 call_nonkey_args = call_args[:]
0356 
0357             # Dynamic check #1:
0358             #
0359             #
0360             # Non-keyword argument types.
0361             if type_nonkey_argcount > 0:
0362                 tmp_zip = zip(call_nonkey_args, type_args,\
0363                      func_nonkey_args)
0364                 for tmp_call_value, tmp_type, tmp_arg_name in tmp_zip:
0365                     __check_type(f_name, tmp_arg_name, tmp_type, tmp_call_value)
0366 
0367 
0368             # Dynamic check #2:
0369             #
0370             # Keyword arguments passed as non-keyword arguments.
0371             if type_nonkey_argcount < call_nonkey_argcount:
0372                 tmp_kwargs_as_args = zip(call_nonkey_args[type_nonkey_argcount:],\
0373                     func_args[-func_key_argcount:])
0374 
0375                 for tmp_call_value, tmp_kwarg_name in tmp_kwargs_as_args:
0376                     tmp_type = type_kwargs[tmp_kwarg_name]
0377 
0378                     if argvalidate_warn_kwarg_as_arg:
0379                         warnings.warn(KWArgAsArgWarning(f_name, tmp_kwarg_name))
0380 
0381                     __check_type(f_name, tmp_kwarg_name, tmp_type,\
0382                          tmp_call_value)
0383 
0384             # Dynamic check #3:
0385             #
0386             # Keyword argument types.
0387             if call_key_argcount > 0:
0388                 for tmp_kwarg_name in call_kwargs:
0389                     if tmp_kwarg_name not in type_kwargs:
0390                         continue
0391 
0392                     tmp_call_value = call_kwargs[tmp_kwarg_name]
0393                     tmp_type = type_kwargs[tmp_kwarg_name]
0394                     __check_type(f_name, tmp_kwarg_name, tmp_type,\
0395                          tmp_call_value)
0396             
0397             return func(*call_args, **call_kwargs)
0398 
0399         
0400         __wrapper_func.func_name = func.__name__
0401         __wrapper_func.__doc__ = func.__doc__
0402         __wrapper_func.__dict__.update(func.__dict__)
0403 
0404         __wrapper_func.argvalidate_accepts_stacked_func = func
0405         return __wrapper_func
0406     
0407     return validate
0408 
0409 def accepts(*type_args, **type_kwargs):
0410     """
0411     Decorator used for checking arguments passed to a function or method.
0412 
0413     :param start: method/function-detection override. The number of arguments
0414                   defined with start are ignored in all checks.
0415 
0416     :param type_args: type definitions of non-keyword arguments.
0417     :param type_kwargs: type definitions of keyword arguments.
0418 
0419     :raises DecoratorNonKeyLengthException: Raised if the number of non-keyword
0420         arguments specified in the decorator does not match the number of
0421         non-keyword arguments the function accepts.
0422 
0423     :raises DecoratorKeyLengthException: Raised if the number of keyword
0424         arguments specified in the decorator does not match the number of
0425         non-keyword arguments the function accepts.
0426 
0427     :raises DecoratorKeyUnspecifiedException: Raised if a keyword argument's
0428         type has not been specified in the decorator.
0429 
0430     :raises ArgumentTypeException: Raised if an argument type passed to the
0431         function does not match the type specified in the decorator.
0432 
0433     Example::
0434 
0435         class MyClass:
0436             @accepts(int, str)
0437             def my_method(self, x_is_int, y_is_str):
0438                 [...]
0439 
0440         @accepts(int, str)
0441         def my_function(x_is_int, y_is_str):
0442             [....]
0443 
0444     """
0445     return __check_args(type_args, type_kwargs, start=-1)
0446 
0447 def returns(expected_type):
0448     """
0449     Decorator used for checking the return value of a function or method.
0450 
0451     :param expected_type: expected type or return value
0452 
0453     :raises ReturnValueTypeException: Raised if the return value's type does not
0454         match the definition in the decorator's `expected_type` parameter.
0455 
0456     Example::
0457     
0458         @return_value(int)
0459         def my_func():
0460             return 5
0461             
0462     """
0463     def validate(f):
0464 
0465         returns_func = getattr(f, 'argvalidate_returns_stacked_func', None)
0466         if returns_func:
0467             raise DecoratorStackingException(returns_func.func_name,'returns')
0468 
0469         func = getattr(f, 'argvalidate_accepts_stacked_func', f)
0470 
0471         def __wrapper_func(*args, **kwargs):
0472             result = func(*args, **kwargs)
0473             __check_return_value(func.func_name, expected_type, result)
0474             return result
0475 
0476         __wrapper_func.func_name = func.__name__
0477         __wrapper_func.__doc__ = func.__doc__
0478         __wrapper_func.__dict__.update(func.__dict__)
0479             
0480         __wrapper_func.argvalidate_returns_stacked_func = func
0481         return __wrapper_func
0482     
0483     return validate
0484 
0485 # Wrappers for old decorators
0486 def return_value(expected_type):
0487     """
0488     Wrapper for backwards-compatibility.
0489 
0490     :deprecated: This decorator has been replaced with :func:`returns`.
0491 
0492     """
0493     warnings.warn(DeprecationWarning('The return_value decorator has been '\
0494         'deprecated. Please use the returns decorator instead.'))
0495     return returns(expected_type)
0496 
0497 
0498 def method_args(*type_args, **type_kwargs):
0499     """
0500     Wrapper for backwards-compatibility.
0501 
0502     :deprecated: This decorator has been replaced with :func:`accepts`.
0503 
0504     """
0505     warnings.warn(DeprecationWarning('The method_args decorator has been '\
0506         'deprecated. Please use the accepts decorator instead.'))
0507     return __check_args(type_args, type_kwargs, start=1)
0508 
0509 def func_args(*type_args, **type_kwargs):
0510     """
0511     Wrapper for backwards-compatibility.
0512 
0513     :deprecated: This decorator has been replaced with :func:`accepts`.
0514     """
0515     warnings.warn(DeprecationWarning('The func_args decorator has been '\
0516         'deprecated. Please use the accepts decorator instead.'))
0517     return __check_args(type_args, type_kwargs, start=0)
0518 
0519 
0520 class __OneOfTuple(tuple):
0521     def __repr__(self):
0522         return 'one of %r' % (tuple.__repr__(self))
0523 
0524 # Used for readability, using a tuple alone would be sufficient.
0525 def one_of(*args):
0526     """
0527     Simple helper function to create a tuple from every argument passed to it.
0528 
0529     :param args: type definitions
0530 
0531     A tuple can be used instead of calling this function, however, the tuple
0532     returned by this function contains a customized __repr__ method, which
0533     makes Exceptions easier to read.
0534 
0535     Example::
0536 
0537         @func_check_args(one_of(int, str, float))
0538         def my_func(x):
0539             pass
0540             
0541     """
0542     return __OneOfTuple(args)
0543 
0544 def raises_exceptions():
0545     """
0546     Returns True if argvalidate raises exceptions, False if argvalidate
0547     creates warnings instead.
0548 
0549     This behaviour can be controlled via the environment variable
0550     :envvar:`ARGVALIDATE_WARN`.
0551     """
0552     return not argvalidate_warn
0553 
0554 def warns_kwarg_as_arg():
0555     """
0556     Returns True if argvalidate generates warnings for keyword arguments
0557     passed as arguments.
0558 
0559     This behaviour can be controlled via the environment variable
0560     :envvar:`ARGVALIDATE_WARN_KWARG_AS_ARG`.
0561     """
0562     return argvalidate_kwarg_as_arg
0563 
0564 # Used for readbility, using None alone would be sufficient
0565 type_any = None