~speijnik/python-argvalidate/devel

« back to all changes in this revision

Viewing changes to argvalidate.py

  • Committer: Stephan Peijnik
  • Date: 2010-04-23 18:10:11 UTC
  • Revision ID: debian@sp.or.at-20100423181011-u1uo3ui7do6qw7gm
Initial import from HG tree.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#   Copyright (C) 2009 Stephan Peijnik (stephan@peijnik.at)
 
2
#
 
3
#    This program is free software: you can redistribute it and/or modify
 
4
#    it under the terms of the GNU Lesser General Public License as published by
 
5
#    the Free Software Foundation, either version 3 of the License, or
 
6
#    (at your option) any later version.
 
7
#
 
8
#    This program is distributed in the hope that it will be useful,
 
9
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
#    GNU Lesser General Public License for more details.
 
12
#
 
13
#    You should have received a copy of the GNU Lesser General Public License
 
14
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
15
 
 
16
__version__ = '0.9.5-dev'
 
17
 
 
18
import inspect
 
19
import os
 
20
import warnings
 
21
 
 
22
from new import classobj
 
23
 
 
24
__all__ = ['ArgvalidateException',
 
25
    'DecoratorNonKeyLengthException', 'DecoratorKeyUnspecifiedException',
 
26
    'DecoratorStackingException', 'ArgumentTypeException',
 
27
    'func_args', 'method_args', 'return_value',
 
28
    'one_of', 'type_any', 'raises_exceptions', 'warns_kwarg_as_arg',
 
29
    'accepts', 'returns']
 
30
 
 
31
# Check for environment variables
 
32
argvalidate_warn = 0
 
33
if 'ARGVALIDATE_WARN' in os.environ:
 
34
    argvalidate_warn_str = os.environ['ARGVALIDATE_WARN']
 
35
    try:
 
36
        argvalidate_warn = int(argvalidate_warn_str)
 
37
    except ValueError:
 
38
        pass
 
39
 
 
40
argvalidate_warn_kwarg_as_arg = 0
 
41
if 'ARGVALIDATE_WARN_KWARG_AS_ARG' in os.environ:
 
42
    argvalidate_warn_kwarg_as_arg_str =\
 
43
         os.environ['ARGVALIDATE_WARN_KWARG_AS_ARG']
 
44
    try:
 
45
        argvalidate_warn_kwarg_as_arg =\
 
46
            int(argvalidate_warn_kwarg_as_arg_str)
 
47
    except ValueError:
 
48
        pass
 
49
 
 
50
class ArgvalidateException(Exception):
 
51
    """
 
52
    Base argvalidate exception.
 
53
 
 
54
    Used as base for all exceptions.
 
55
 
 
56
    """
 
57
    pass
 
58
 
 
59
 
 
60
 
 
61
class DecoratorNonKeyLengthException(ArgvalidateException):
 
62
    """
 
63
    Exception for invalid decorator non-keyword argument count.
 
64
 
 
65
    This exception provides the following attributes:
 
66
 
 
67
    * func_name
 
68
        Name of function that caused the exception to be raised
 
69
        (str, read-only).
 
70
 
 
71
    * expected_count
 
72
        Number of arguments that were expected (int, read-only).
 
73
 
 
74
    * passed_count
 
75
        Number of arguments that were passed to the function (int, read-only).
 
76
 
 
77
    """
 
78
    def __init__(self, func_name, expected_count, passed_count):
 
79
        self.func_name = func_name
 
80
        self.expected_count = expected_count
 
81
        self.passed_count = passed_count
 
82
        msg = '%s: wrong number of non-keyword arguments specified in' %\
 
83
             (func_name) 
 
84
        msg += ' decorator (expected %d, got %d).' %\
 
85
             (expected_count, passed_count)
 
86
        ArgvalidateException.__init__(self, msg)
 
87
 
 
88
class DecoratorKeyUnspecifiedException(ArgvalidateException):
 
89
    """
 
90
    Exception for unspecified decorator keyword argument.
 
91
 
 
92
    This exception provides the following attributes:
 
93
 
 
94
    * func_name
 
95
        Name of function that caused the exception to be raised
 
96
        (str, read-only).
 
97
 
 
98
    * arg_name
 
99
        Name of the keyword argument passed to the function, but not specified
 
100
        in the decorator (str, read-only).
 
101
        
 
102
    """
 
103
    def __init__(self, func_name, arg_name):
 
104
        self.func_name = func_name
 
105
        self.arg_name = arg_name
 
106
        msg = '%s: keyword argument %s not specified in decorator.' %\
 
107
            (func_name, arg_name)
 
108
        ArgvalidateException.__init__(self, msg)
 
109
 
 
110
class DecoratorStackingException(ArgvalidateException):
 
111
    """
 
112
    Exception for stacking a decorator with itself.
 
113
 
 
114
    This exception provides the following attributes:
 
115
 
 
116
    * func_name
 
117
        Name of function that caused the exception to be raised
 
118
        (str, read-only).
 
119
 
 
120
    * decorator_name
 
121
        Name of the decorator that was stacked with itself (str, read-only).
 
122
 
 
123
    """
 
124
    def __init__(self, func_name, decorator_name):
 
125
        self.func_name = func_name
 
126
        self.decorator_name = decorator_name
 
127
        msg = '%s: decorator %s stacked with itself.' %\
 
128
            (func_name, decorator_name)
 
129
        ArgvalidateException.__init__(self, msg)
 
130
 
 
131
class ArgumentTypeException(ArgvalidateException):
 
132
    """
 
133
    Exception for invalid argument type.
 
134
 
 
135
    This exception provides the following attributes:
 
136
 
 
137
    * func_name
 
138
        Name of function that caused the exception to be raised
 
139
        (str, read-only).
 
140
 
 
141
    * arg_name
 
142
        Name of the keyword argument passed to the function, but not specified
 
143
        in the decorator (str, read-only).
 
144
 
 
145
    * expected_type
 
146
        Argument type that was expected (type, read-only).
 
147
 
 
148
    * passed_type
 
149
        Argument type that was passed to the function (type, read-only).
 
150
 
 
151
    """
 
152
    def __init__(self, func_name, arg_name, expected_type, passed_type):
 
153
        self.func_name = func_name
 
154
        self.arg_name = arg_name
 
155
        self.expected_type = expected_type
 
156
        self.passed_type = passed_type
 
157
        msg = '%s: invalid argument type for %r (expected %r, got %r).' %\
 
158
            (func_name, arg_name, expected_type, passed_type)
 
159
        ArgvalidateException.__init__(self, msg)
 
160
 
 
161
class ReturnValueTypeException(ArgvalidateException):
 
162
    """
 
163
    Exception for invalid return value type.
 
164
 
 
165
    This exception provides the following attributes:
 
166
 
 
167
    * func_name
 
168
        Name of function that caused the exception to be raised
 
169
        (string, read-only).
 
170
 
 
171
    * expected_type
 
172
        Argument type that was expected (type, read-only).
 
173
 
 
174
    * passed_type
 
175
        Type of value returned by the function (type, read-only).
 
176
 
 
177
    """
 
178
    def __init__(self, func_name, expected_type, passed_type):
 
179
        self.func_name = func_name
 
180
        self.expected_type = expected_type
 
181
        self.passed_type = passed_type
 
182
        msg = '%s: invalid type for return value (expected %r, got %r).' %\
 
183
            (func_name, expected_type, passed_type)
 
184
        ArgvalidateException.__init__(self, msg)
 
185
 
 
186
class KWArgAsArgWarning(ArgvalidateException):
 
187
    def __init__(self, func_name, arg_name):
 
188
        msg = '%s: argument %s is a keyword argument and was passed as a '\
 
189
            'non-keyword argument.' % (func_name, arg_name)
 
190
        ArgvalidateException.__init__(self, msg)
 
191
 
 
192
def __raise(exception, stacklevel=3):
 
193
    if argvalidate_warn:
 
194
        warnings.warn(exception, stacklevel=stacklevel)
 
195
    else:
 
196
        raise exception
 
197
 
 
198
def __check_return_value(func_name, expected_type, return_value):
 
199
    return_value_type = type(return_value)
 
200
    error = False
 
201
 
 
202
    if expected_type is None:
 
203
        error = False
 
204
 
 
205
    elif isinstance(return_value, classobj):
 
206
        if not isinstance(return_value, expected_type) and\
 
207
            not issubclass(return_value.__class__, expected_type):
 
208
                error=True
 
209
    else:
 
210
        if not isinstance(return_value, expected_type):
 
211
            error=True
 
212
 
 
213
    if error:
 
214
        __raise(ReturnValueTypeException(func_name, expected_type,\
 
215
             return_value_type), stacklevel=3)
 
216
 
 
217
def __check_type(func_name, arg_name, expected_type, passed_value,\
 
218
    stacklevel=4):
 
219
    passed_type = type(passed_value)
 
220
    error=False
 
221
 
 
222
    # None means the type is not checked
 
223
    if expected_type is None:
 
224
        error=False
 
225
 
 
226
    # Check a class
 
227
    elif isinstance(passed_value, classobj):
 
228
        if not isinstance(passed_value, expected_type) and\
 
229
            not issubclass(passed_value.__class__, expected_type):
 
230
            error=True
 
231
    
 
232
    # Check a type
 
233
    else:
 
234
        if not isinstance(passed_value, expected_type):
 
235
            error=True
 
236
 
 
237
    if error:
 
238
        __raise(ArgumentTypeException(func_name, arg_name, expected_type,\
 
239
            passed_type), stacklevel=stacklevel)
 
240
 
 
241
def __check_args(type_args, type_kwargs, start=-1):
 
242
    type_nonkey_argcount = len(type_args)
 
243
    type_key_argcount = len(type_kwargs)
 
244
 
 
245
    def validate(f):
 
246
        accepts_func = getattr(f, 'argvalidate_accepts_stacked_func', None)
 
247
        
 
248
        if accepts_func:
 
249
            if start == -1:
 
250
                raise DecoratorStackingException(accepts_func.func_name,\
 
251
                    'accepts')
 
252
            if start == 0:
 
253
                raise DecoratorStackingException(accepts_func.func_name,\
 
254
                     'function_accepts')
 
255
            elif start == 1:
 
256
                raise DecoratorStackingException(accepts_func.func_name,\
 
257
                     'method_accepts')
 
258
            else:
 
259
                raise DecoratorStackingException(accepts_func.func_name,\
 
260
                     'unknown; start=%d' % (start))
 
261
 
 
262
        func = getattr(f, 'argvalidate_returns_stacked_func', f)
 
263
        f_name = func.__name__
 
264
        (func_args, func_varargs, func_varkw, func_defaults) =\
 
265
             inspect.getargspec(func)
 
266
 
 
267
        func_argcount = len(func_args)
 
268
        is_method = True
 
269
 
 
270
        # The original idea was to use inspect.ismethod here,
 
271
        # but it seems as the decorator is called before the
 
272
        # method is bound to a class, so this will always
 
273
        # return False.
 
274
        # The new method follows the original idea of checking
 
275
        # tha name of the first argument passed.
 
276
        # self and cls indicate methods, everything else indicates
 
277
        # a function.
 
278
        if start < 0 and func_argcount > 0 and func_args[0] in ['self', 'cls']:
 
279
            func_argcount -= 1
 
280
            func_args = func_args[1:]
 
281
        elif start == 1:
 
282
            func_argcount -=1
 
283
            func_args = func_args[1:]
 
284
        else:
 
285
            is_method = False
 
286
 
 
287
        if func_varargs:
 
288
            func_args.remove(func_varargs)
 
289
            
 
290
        if func_varkw:
 
291
            func_args.remove(func_varkw)
 
292
 
 
293
        func_key_args = {}
 
294
        func_key_argcount = 0
 
295
 
 
296
        if func_defaults:
 
297
            func_key_argcount = len(func_defaults)
 
298
            tmp_key_args = zip(func_args[-func_key_argcount:], func_defaults)
 
299
 
 
300
            for tmp_key_name, tmp_key_default in tmp_key_args:
 
301
                func_key_args.update({tmp_key_name: tmp_key_default})
 
302
 
 
303
            # Get rid of unused variables
 
304
            del tmp_key_args
 
305
            del tmp_key_name
 
306
            del tmp_key_default
 
307
 
 
308
        func_nonkey_args = []
 
309
        if func_key_argcount < func_argcount:
 
310
            func_nonkey_args = func_args[:func_argcount-func_key_argcount]
 
311
        func_nonkey_argcount = len(func_nonkey_args)
 
312
 
 
313
        # Static check #0:
 
314
        #
 
315
        # Checking the lengths of type_args vs. func_args and type_kwargs vs.
 
316
        # func_key_args should be done here.
 
317
        #
 
318
        # This means that the check is only performed when the decorator
 
319
        # is actually invoked, not every time the target function is called.
 
320
        if func_nonkey_argcount != type_nonkey_argcount:
 
321
            __raise(DecoratorNonKeyLengthException(f_name,\
 
322
                func_nonkey_argcount, type_nonkey_argcount))
 
323
                
 
324
        if func_key_argcount != type_key_argcount:
 
325
            __raise(DecoratorKeyLengthException(f_name,\
 
326
                func_key_argcount, type_key_argcount))
 
327
 
 
328
        # Static check #1:
 
329
        #
 
330
        # kwarg default value types.
 
331
        if func_defaults:
 
332
            tmp_kw_zip = zip(func_key_args, func_defaults)
 
333
            for tmp_kw_name, tmp_kw_default in tmp_kw_zip:
 
334
                if not tmp_kw_name in type_kwargs:
 
335
                    __raise(DecoratorKeyUnspecifiedException(f_name,\
 
336
                         tmp_kw_name))
 
337
                
 
338
                tmp_kw_type = type_kwargs[tmp_kw_name]
 
339
                __check_type(f_name, tmp_kw_name, tmp_kw_type, tmp_kw_default)
 
340
            
 
341
            del tmp_kw_type
 
342
            del tmp_kw_name
 
343
            del tmp_kw_default
 
344
            del tmp_kw_zip
 
345
 
 
346
        def __wrapper_func(*call_args, **call_kwargs):
 
347
            call_nonkey_argcount = len(call_args)
 
348
            call_key_argcount = len(call_kwargs)
 
349
            call_nonkey_args = []
 
350
 
 
351
            if is_method:
 
352
                call_nonkey_args = call_args[1:]
 
353
            else:
 
354
                call_nonkey_args = call_args[:]
 
355
 
 
356
            # Dynamic check #1:
 
357
            #
 
358
            #
 
359
            # Non-keyword argument types.
 
360
            if type_nonkey_argcount > 0:
 
361
                tmp_zip = zip(call_nonkey_args, type_args,\
 
362
                     func_nonkey_args)
 
363
                for tmp_call_value, tmp_type, tmp_arg_name in tmp_zip:
 
364
                    __check_type(f_name, tmp_arg_name, tmp_type, tmp_call_value)
 
365
 
 
366
 
 
367
            # Dynamic check #2:
 
368
            #
 
369
            # Keyword arguments passed as non-keyword arguments.
 
370
            if type_nonkey_argcount < call_nonkey_argcount:
 
371
                tmp_kwargs_as_args = zip(call_nonkey_args[type_nonkey_argcount:],\
 
372
                    func_key_args.keys())
 
373
 
 
374
                for tmp_call_value, tmp_kwarg_name in tmp_kwargs_as_args:
 
375
                    tmp_type = type_kwargs[tmp_kwarg_name]
 
376
 
 
377
                    if argvalidate_warn_kwarg_as_arg:
 
378
                        warnings.warn(KWArgAsArgWarning(f_name, tmp_kwarg_name))
 
379
 
 
380
                    __check_type(f_name, tmp_kwarg_name, tmp_type,\
 
381
                         tmp_call_value)
 
382
 
 
383
            # Dynamic check #3:
 
384
            #
 
385
            # Keyword argument types.
 
386
            if call_key_argcount > 0:
 
387
                for tmp_kwarg_name in call_kwargs:
 
388
                    if tmp_kwarg_name not in type_kwargs:
 
389
                        continue
 
390
 
 
391
                    tmp_call_value = call_kwargs[tmp_kwarg_name]
 
392
                    tmp_type = type_kwargs[tmp_kwarg_name]
 
393
                    __check_type(f_name, tmp_kwarg_name, tmp_type,\
 
394
                         tmp_call_value)
 
395
            
 
396
            return func(*call_args, **call_kwargs)
 
397
 
 
398
        
 
399
        __wrapper_func.func_name = func.__name__
 
400
        __wrapper_func.__doc__ = func.__doc__
 
401
        __wrapper_func.__dict__.update(func.__dict__)
 
402
 
 
403
        __wrapper_func.argvalidate_accepts_stacked_func = func
 
404
        return __wrapper_func
 
405
    
 
406
    return validate
 
407
 
 
408
def accepts(*type_args, **type_kwargs):
 
409
    """
 
410
    Decorator used for checking arguments passed to a function or method.
 
411
 
 
412
    :param start: method/function-detection override. The number of arguments
 
413
                  defined with start are ignored in all checks.
 
414
 
 
415
    :param type_args: type definitions of non-keyword arguments.
 
416
    :param type_kwargs: type definitions of keyword arguments.
 
417
 
 
418
    :raises DecoratorNonKeyLengthException: Raised if the number of non-keyword
 
419
        arguments specified in the decorator does not match the number of
 
420
        non-keyword arguments the function accepts.
 
421
 
 
422
    :raises DecoratorKeyLengthException: Raised if the number of keyword
 
423
        arguments specified in the decorator does not match the number of
 
424
        non-keyword arguments the function accepts.
 
425
 
 
426
    :raises DecoratorKeyUnspecifiedException: Raised if a keyword argument's
 
427
        type has not been specified in the decorator.
 
428
 
 
429
    :raises ArgumentTypeException: Raised if an argument type passed to the
 
430
        function does not match the type specified in the decorator.
 
431
 
 
432
    Example::
 
433
 
 
434
        class MyClass:
 
435
            @accepts(int, str)
 
436
            def my_method(self, x_is_int, y_is_str):
 
437
                [...]
 
438
 
 
439
        @accepts(int, str)
 
440
        def my_function(x_is_int, y_is_str):
 
441
            [....]
 
442
 
 
443
    """
 
444
    return __check_args(type_args, type_kwargs, start=-1)
 
445
 
 
446
def returns(expected_type):
 
447
    """
 
448
    Decorator used for checking the return value of a function or method.
 
449
 
 
450
    :param expected_type: expected type or return value
 
451
 
 
452
    :raises ReturnValueTypeException: Raised if the return value's type does not
 
453
        match the definition in the decorator's `expected_type` parameter.
 
454
 
 
455
    Example::
 
456
    
 
457
        @return_value(int)
 
458
        def my_func():
 
459
            return 5
 
460
            
 
461
    """
 
462
    def validate(f):
 
463
 
 
464
        returns_func = getattr(f, 'argvalidate_returns_stacked_func', None)
 
465
        if returns_func:
 
466
            raise DecoratorStackingException(returns_func.func_name,'returns')
 
467
 
 
468
        func = getattr(f, 'argvalidate_accepts_stacked_func', f)
 
469
 
 
470
        def __wrapper_func(*args, **kwargs):
 
471
            result = func(*args, **kwargs)
 
472
            __check_return_value(func.func_name, expected_type, result)
 
473
            return result
 
474
 
 
475
        __wrapper_func.func_name = func.__name__
 
476
        __wrapper_func.__doc__ = func.__doc__
 
477
        __wrapper_func.__dict__.update(func.__dict__)
 
478
            
 
479
        __wrapper_func.argvalidate_returns_stacked_func = func
 
480
        return __wrapper_func
 
481
    
 
482
    return validate
 
483
 
 
484
# Wrappers for old decorators
 
485
def return_value(expected_type):
 
486
    """
 
487
    Wrapper for backwards-compatibility.
 
488
 
 
489
    :deprecated: This decorator has been replaced with :func:`returns`.
 
490
 
 
491
    """
 
492
    warnings.warn(DeprecationWarning('The return_value decorator has been '\
 
493
        'deprecated. Please use the returns decorator instead.'))
 
494
    return returns(expected_type)
 
495
 
 
496
 
 
497
def method_args(*type_args, **type_kwargs):
 
498
    """
 
499
    Wrapper for backwards-compatibility.
 
500
 
 
501
    :deprecated: This decorator has been replaced with :func:`accepts`.
 
502
 
 
503
    """
 
504
    warnings.warn(DeprecationWarning('The method_args decorator has been '\
 
505
        'deprecated. Please use the accepts decorator instead.'))
 
506
    return __check_args(type_args, type_kwargs, start=1)
 
507
 
 
508
def func_args(*type_args, **type_kwargs):
 
509
    """
 
510
    Wrapper for backwards-compatibility.
 
511
 
 
512
    :deprecated: This decorator has been replaced with :func:`accepts`.
 
513
    """
 
514
    warnings.warn(DeprecationWarning('The func_args decorator has been '\
 
515
        'deprecated. Please use the accepts decorator instead.'))
 
516
    return __check_args(type_args, type_kwargs, start=0)
 
517
 
 
518
 
 
519
class __OneOfTuple(tuple):
 
520
    def __repr__(self):
 
521
        return 'one of %r' % (tuple.__repr__(self))
 
522
 
 
523
# Used for readability, using a tuple alone would be sufficient.
 
524
def one_of(*args):
 
525
    """
 
526
    Simple helper function to create a tuple from every argument passed to it.
 
527
 
 
528
    :param args: type definitions
 
529
 
 
530
    A tuple can be used instead of calling this function, however, the tuple
 
531
    returned by this function contains a customized __repr__ method, which
 
532
    makes Exceptions easier to read.
 
533
 
 
534
    Example::
 
535
 
 
536
        @func_check_args(one_of(int, str, float))
 
537
        def my_func(x):
 
538
            pass
 
539
            
 
540
    """
 
541
    return __OneOfTuple(args)
 
542
 
 
543
def raises_exceptions():
 
544
    """
 
545
    Returns True if argvalidate raises exceptions, False if argvalidate
 
546
    creates warnings instead.
 
547
 
 
548
    This behaviour can be controlled via the environment variable
 
549
    :envvar:`ARGVALIDATE_WARN`.
 
550
    """
 
551
    return not argvalidate_warn
 
552
 
 
553
def warns_kwarg_as_arg():
 
554
    """
 
555
    Returns True if argvalidate generates warnings for keyword arguments
 
556
    passed as arguments.
 
557
 
 
558
    This behaviour can be controlled via the environment variable
 
559
    :envvar:`ARGVALIDATE_WARN_KWARG_AS_ARG`.
 
560
    """
 
561
    return argvalidate_kwarg_as_arg
 
562
 
 
563
# Used for readbility, using None alone would be sufficient
 
564
type_any = None