Welcome, guest | Sign In | My Account | Store | Cart

Watchdog((Param_Type1, Param_Type2, ...), Result_Type) returns a function decorator which can easily be applied to functions in oder to check parameter / result integrity.

Python, 115 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
from sets import Set, ImmutableSet
from types import TypeType, ClassType, NoneType, InstanceType


class Error(ValueError):
   """Error class for Watchdog"""

   def __init__(self, func, value, exp_type, name, number = None):
      try:
         prefix = "value '%s' of " % value
      except:
         prefix = ''
      if number != None:
         number_str = ' #%s' % number
      else:
         number_str = ''
      msg = "mismatch in file '%s', line %s: %s%s%s should be %s, got %s" % (
            func.func_code.co_filename, func.func_code.co_firstlineno,
            prefix, name, number_str, exp_type, type(value))
      ValueError.__init__(self, msg)


class Watchdog(object):

   # cache for parameters / result types:
   signatures = Set()

   def __new__(cls, para_types, res_type, strict = False):
      if not strict and not __debug__:
         # decorator: identity, does nothing
         return lambda func: func
      # create watchdog instance:
      watchdog = object.__new__(cls)
      # assign function signature to instance & add to cache if needed:
      signature = para_types, res_type
      if signature not in Watchdog.signatures:
         # unknown signature: check and insert
         if not isinstance(para_types, tuple):
            raise ValueError('Parameter #1 must be a tuple')
         allow_types = [TypeType, ClassType]
         for entry in para_types:
            if type(entry) not in allow_types:
               msg = 'Parameter #1 must contain only Types and/or Classes'
               raise ValueError(msg)
         if type(res_type) not in allow_types:
            raise ValueError('Parameter #2 must be a Type or Class')
         Watchdog.signatures |= ImmutableSet([signature])
      watchdog.signature = signature
      # returns the decorator(not the watchdog class-instance):
      return watchdog.check


   def check(self, func):

      def caller(*args):
         # load function signature:
         para_types, res_type = self.signature
         normal_len = func.func_code.co_argcount
         submit_len = len(args)
         if normal_len != len(para_types):
            raise ValueError('wrong number of parameter types')
         # check if there are conflicts with user arguments:
         pairs = zip(args, para_types, range(1, submit_len + 1))
         for pair in pairs:
            if not isinstance(pair[0], pair[1]):
               raise Error(func, pair[0], pair[1], 'parameter', pair[2])
         try:
            # check default parameter integrity:
            default_args = func.func_defaults
            default_len = len(default_args)
            default_types = para_types[0: default_len]
            count_range = range(normal_len - default_len + 1, normal_len + 1)
            pairs = zip(default_args, default_types, count_range)
            for pair in pairs:
               if not isinstance(pair[0], pair[1]):
                  raise Error(func, pair[0], pair[1],
                              'default parameter', pair[2])
         except TypeError:
            # no default arguments: ignore
            pass
         # we actually call the function here:
         res = func(*args)
         # check result integrity:
         if not isinstance(res, res_type):
            raise Error(func, res, res_type, 'result')
         return res
 
      return caller



if __name__ == "__main__":

   # a small test:
   # try to run "python watchdog.py" and "python -O watchdog.py"

   @Watchdog((int, int), int, strict = True)
   def add(a, b):
      return a + b

   @Watchdog((int, int), int) # strict is implicitly set to False
   def sub(a, b):
      return a - b

   # strict checking(not affected by __debug__):
   try:
      add(1, '3')
   except Error, err:
      print 'strict check failed: %s' % err

   # non-strict checking(affected by __debug__):
   try:
      sub([1,2,3], 7)
   except Error, err:
      print 'non-strict check failed: %s' % err

The idea behind Watchdog is the following: Normally, the __new__ constructor would return a class instance(which cannot be used for function decoration). So the solution is: We first create an instance with a method "check", which takes an arbitrary function as a parameter. Then, we do NOT return the instance, but the "check" method reference, which fits our needs as a decorator.

You can imagine that the instance serves the parameter type / result type configuration to that returned method. To speed things up there is a signatures cache at class-level.

Hope you'll have fun with it Tobi

3 comments

Peter Fein 18 years ago  # | flag

Documentation? Some docstrings and comments would be nice.

Denis Barmenkov 18 years ago  # | flag

debug/production flag. It will be nice to have debug/production flag:

debug: create decorators as specified production: no checks/empty decorators for speed optimisations

Tobias Simon (author) 18 years ago  # | flag

strict flag. I added a 'strict' flag(default = False) for the constructor, which means:

If 'strict' is set, there is no way to to suppress the evaluation - but if it is not set, the evaluation depends from the __debug__ state of the interpreter. When __debug__ is set, there will be an evaluation. Otherwise, the decorator is replaced by "lambda func: func" - should be fast enough.

Best Regards, Tobi

Created by Tobias Simon on Wed, 15 Mar 2006 (PSF)
Python recipes (4591)
Tobias Simon's recipes (1)

Required Modules

Other Information and Tasks