File: rangetest.py
############################################################################## # the following works only for postional arguments, # and assumes they always appear at the same position # in every call; they cannot be passed by keyword name, # and we don't suport **args keywords in calls because # this can invalidate the positions declared in the # decorator; these examples all run under 2.6 and 3.0; def rangetest(*argchecks): # validate positional arg ranges def onDecorator(func): if not __debug__: # True if "python -O main.py args.." return func # no-op: call original directly else: # else wrapper while debugging def onCall(*args): for (ix, low, high) in argchecks: if args[ix] < low or args[ix] > high: errmsg = 'Argument %s not in %s..%s' % (ix, low, high) raise TypeError(errmsg) return func(*args) return onCall return onDecorator #if __name__ == '__main__': # to make this a library print(__debug__) # True if no -O python cmdline arg # False if "python -O main.py args" @rangetest((1, 0, 120)) # person = rangetest(..)(person) def person(name, age): print('%s is %s years old' % (name, age)) @rangetest([0, 1, 12], [1, 1, 31], [2, 0, 2009]) def birthday(M, D, Y): print('birthday = {0}/{1}/{2}'.format(M, D, Y)) class Person: """ revisit the Person class to validate argument """ def __init__(self, name, job, pay): self.job = job self.pay = pay # arg 0 is the self instance here @rangetest([1, 0.0, 1.0]) # giveRaise = rangetest(..)(giveRaise) def giveRaise(self, percent): self.pay = int(self.pay * (1 + percent)) # comment lines raise TypError unless "python -O" on shell command line person('Bob Smith', 45) # really runs onCall(..) with state #person('Bob Smith', 200) # or person() if -O cmd line argument birthday(5, 31, 1963) #birthday(5, 32, 1963) sue = Person('Sue Jones', 'dev', 100000) sue.giveRaise(.10) # really runs onCall(self, .10) print(sue.pay) # or giveRaise(self, .10) if -O #sue.giveRaise(1.10) #print(sue.pay) ################################################################################ # the following tests either positional or keyword args, # or both, but does not support an argument being passed # in both modes at different calls: once decorated, the # argument to test must always appear at the same position # in every call, or always be passed by keyword in every call; def rangetest(*argchecks, **kargchecks): # validate keyword arg ranges too def onDecorator(func): if not __debug__: # True if "python -O main.py args.." return func # no-op: call original directly else: # else wrapper while debugging def onCall(*args, **kargs): for (ix, low, high) in argchecks: if args[ix] < low or args[ix] > high: errmor = 'Argument #%s not in %s..%s' % (ix, low, high) raise TypeError(error) for (key, (low, high)) in kargchecks.items(): if kargs[key] < low or kargs[key] > high: error = 'Argument "%s" not in %s..%s' % (key, low, high) raise TypeError(error) return func(*args, **kargs) return onCall return onDecorator #if __name__ == '__main__': # to make this a library print(__debug__) # True if no -O python cmdline arg # False if "python -O main.py args" @rangetest((1, 0, 120)) # person = rangetest(..)(person) def person(name, age): print('%s is %s years old' % (name, age)) @rangetest(M=(1, 12), D=(1, 31), Y=(0, 2009)) def birthday(M, D, Y): print('birthday = {0}/{1}/{2}'.format(M, D, Y)) @rangetest((0, 0, 999999999), height=(0.0, 7.0)) # sss always by position def record(ssn, category, height): # height always by keyword print('record: %s, %s' % (ssn, category)) # category not checked class Person: """ revisit the Person class to validate argument """ def __init__(self, name, job, pay): self.job = job self.pay = pay # giveRaise = rangetest(..)(giveRaise) @rangetest((1, 0.0, 1.0)) # assume percent passed by position def giveRaise(self, percent): self.pay = int(self.pay * (1 + percent)) @rangetest(percent=(0.0, 1.0)) # same, but assume passed by keyword def giveRaise2(self, percent, bonus=.10): self.pay = int(self.pay * (1 + percent + bonus)) # comment lines raise TypError unless "python -O" on shell command line person('Bob Smith', 45) # really runs onCall(..) with state #person('Bob Smith', 200) # or person() if -O cmd line argument birthday(M=5, D=31, Y=1963) birthday(Y=1963, M=5, D=31) #birthday(M=5, D=32, Y=1963) record(123456789, 'spam', height=5.5) record(123456789, height=5.5, category='spam') #record(1234567899, height=5.5, category='spam') #record(123456789, height=99, category='spam') sue = Person('Sue Jones', 'dev', 100000) sue.giveRaise(.10) # really runs onCall(self, .10) print(sue.pay) # or giveRaise(self, .10) if -O sue.giveRaise2(percent=.10) print(sue.pay) #sue.giveRaise2(percent=1.10) #print(sue.pay) ############################################################################### # the following allows checked arguments to be passed either # by position or keyword name, and automatically maps non-keyword # arguments to their position by matching against the expected # argument names grabed from the function's code object; expected # arguments remaining after removing actual keyword arguments are # assumed to be passed by postion; their index in the expected # arguments list gives their position in the actual positionals # caveat: does not skip testing for arguments that are allowed # default in the call def rangetest(**argchecks): # validate ranges for both arg types def onDecorator(func): if not __debug__: # True if "python -O main.py args.." return func # no-op: call original directly else: # else wrapper while debugging import sys code = func.__code__ if sys.version_info[0] == 3 else func.func_code allargs = code.co_varnames[:code.co_argcount] def onCall(*pargs, **kargs): positionals = list(allargs) for argname in kargs: # for all passed by name, remove positionals.remove(argname) for (argname, (low, high)) in argchecks.items(): # for all args to be checked if argname in kargs: # was passed by name if kargs[argname] < low or kargs[argname] > high: errmsg = 'Argument "{0}" not in {1}..{2}' errmsg = errmsg.format(argname, low, high) raise TypeError(errmsg) else: # was passed by position position = positionals.index(argname) if pargs[position] < low or pargs[position] > high: errmsg = 'Argument "{0}" not in {1}..{2}' errmsg = errmsg.format(argname, low, high) raise TypeError(errmsg) return func(*pargs, **kargs) # okay: run original call return onCall return onDecorator #if __name__ == '__main__': # to make this a library print(__debug__) # True if no -O python cmdline arg # False if "python -O main.py args" @rangetest(age=(0, 120)) # person = rangetest(..)(person) def person(name, age): print('%s is %s years old' % (name, age)) @rangetest(M=(1, 12), D=(1, 31), Y=(0, 2009)) def birthday(M, D, Y): print('birthday = {0}/{1}/{2}'.format(M, D, Y)) @rangetest(ssn=(0, 999999999), height=(0.0, 7.0)) def record(ssn, category, height): # args by name or position print('record: %s, %s' % (ssn, category)) # category not checked class Person: """ revisit the Person class to validate argument """ def __init__(self, name, job, pay): self.job = job self.pay = pay # giveRaise = rangetest(..)(giveRaise) @rangetest(percent=(0.0, 1.0)) # percent passed by name or position def giveRaise(self, percent): self.pay = int(self.pay * (1 + percent)) # comment lines raise TypError unless "python -O" on shell command line person('Bob Smith', 45) # really runs onCall(..) with state person(age=45, name='Bob Smith') #person('Bob Smith', 200) # or person() if -O cmd line argument #person('Bob Smith', age=200) #person(age=200, name='Bob Smith') birthday(M=5, D=31, Y=1963) birthday(Y=1963, M=5, D=31) birthday(5, 31, Y=1963) birthday(5, D=31, Y=1963) birthday(5, Y=1963, D=31) #birthday(5, 32, 1963) #birthday(5, 32, Y=1963) #birthday(5, D=32, Y=1963) #birthday(5, Y=1963, D=32) #birthday(M=5, D=32, Y=1963) record(123456789, 'spam', height=5.5) record(123456789, height=5.5, category='spam') #record(1234567899, height=5.5, category='spam') #record(123456789, height=99, category='spam') sue = Person('Sue Jones', 'dev', 100000) sue.giveRaise(.10) # really runs onCall(self, .10) print(sue.pay) # or giveRaise(self, .10) if -O sue.giveRaise(percent=.10) print(sue.pay) #sue.giveRaise(1.10) #sue.giveRaise(percent=1.10) #print(sue.pay) # caveat: does not skip testing for arguments that are allowed # default in the call: @rangetest(a=(1,10), b=(1,10), c=(1, 10), d=(1, 10)) def omitargs(a, b=7, c=8, d=9): print(a, b, c, d) omitargs(1, 2, 3, 4) #omitargs(1, 2, 3) # FAILS! - index out of range, pargs[position] #omitargs(1, 2) #omitargs(1) omitargs(1, 2, 3, d=4) #omitargs(1, 2, d=4) #omitargs(1, d=4) #omitargs(a=1, d=4) #omitargs(d=4, a=1) #omitargs(1, b=2) #omitargs(1, b=2, d=4) ################################################################################ # final: skip tests for arguments that were defaulted (omitted) in call, # by assuming the the first N actual arguments in *pargs must match the # first N argument names in the list of all expected arguments trace = True def rangetest(**argchecks): # validate ranges for both+defaults def onDecorator(func): if not __debug__: # True if "python -O main.py args.." return func # no-op: call original directly else: # else wrapper while debugging import sys code = func.__code__ if sys.version_info[0] == 3 else func.func_code allargs = code.co_varnames[:code.co_argcount] funcname = func.__name__ def onCall(*pargs, **kargs): # all pargs match first N args by position # the rest must be in kargs or omitted defaults positionals = list(allargs) positionals = positionals[:len(pargs)] for (argname, (low, high)) in argchecks.items(): # for all args to be checked if argname in kargs: # was passed by name if kargs[argname] < low or kargs[argname] > high: errmsg = '{0} argument "{1}" not in {2}..{3}' errmsg = errmsg.format(funcname, argname, low, high) raise TypeError(errmsg) elif argname in positionals: # was passed by position position = positionals.index(argname) if pargs[position] < low or pargs[position] > high: errmsg = '{0} argument "{1}" not in {2}..{3}' errmsg = errmsg.format(funcname, argname, low, high) raise TypeError(errmsg) else: # assume not passed: default if trace: print('Argument "{0}" defaulted'.format(argname)) return func(*pargs, **kargs) # okay: run original call return onCall return onDecorator #if __name__ == '__main__': # to make this a library print(__debug__) # True if no -O python cmdline arg # False if "python -O main.py args" @rangetest(age=(0, 120)) # person = rangetest(..)(person) def person(name, age): print('%s is %s years old' % (name, age)) @rangetest(M=(1, 12), D=(1, 31), Y=(0, 2009)) def birthday(M, D, Y): print('birthday = {0}/{1}/{2}'.format(M, D, Y)) @rangetest(ssn=(0, 999999999), height=(0.0, 7.0)) def record(ssn, category, height): # args by name or position print('record: %s, %s' % (ssn, category)) # category not checked @rangetest(a=(1, 10), b=(1, 10), c=(1, 10), d=(1, 10)) def omitargs(a, b=7, c=8, d=9): print(a, b, c, d) # skip omitted defaults class Person: """ revisit the Person class to validate argument """ def __init__(self, name, job, pay): self.job = job self.pay = pay # giveRaise = rangetest(..)(giveRaise) @rangetest(percent=(0.0, 1.0)) # percent passed by name or position def giveRaise(self, percent): self.pay = int(self.pay * (1 + percent)) # comment lines raise TypError unless "python -O" on shell command line person('Bob Smith', 45) # really runs onCall(..) with state person(age=45, name='Bob Smith') #person('Bob Smith', 200) # or person() if -O cmd line argument #person('Bob Smith', age=200) #person(age=200, name='Bob Smith') birthday(M=5, D=31, Y=1963) birthday(Y=1963, M=5, D=31) birthday(5, 31, Y=1963) birthday(5, D=31, Y=1963) birthday(5, Y=1963, D=31) #birthday(5, 32, 1963) #birthday(5, 32, Y=1963) #birthday(5, D=32, Y=1963) #birthday(5, Y=1963, D=32) #birthday(M=5, D=32, Y=1963) record(123456789, 'spam', height=5.5) record(123456789, height=5.5, category='spam') #record(1234567899, height=5.5, category='spam') #record(123456789, height=99, category='spam') sue = Person('Sue Jones', 'dev', 100000) sue.giveRaise(.10) # really runs onCall(self, .10) print(sue.pay) # or giveRaise(self, .10) if -O sue.giveRaise(percent=.10) print(sue.pay) #sue.giveRaise(1.10) #sue.giveRaise(percent=1.10) #print(sue.pay) omitargs(1, 2, 3, 4) omitargs(1, 2, 3) omitargs(1, 2) omitargs(1) omitargs(1, 2, 3, d=4) omitargs(1, 2, d=4) omitargs(1, d=4) omitargs(a=1, d=4) omitargs(d=4, a=1) omitargs(1, b=2) omitargs(1, b=2, d=4) omitargs(a=1) omitargs(d=8, c=7, b=6, a=1) omitargs(d=8, c=7, a=1) #omitargs(1, 2, 3, 11) #omitargs(1, 2, 11) #omitargs(1, 11, 3) #omitargs(11, 2, 3) #omitargs(1, 11) #omitargs(11) #omitargs(1, 2, 3, d=11) #omitargs(1, 2, 11, d=4) #omitargs(1, 2, d=11) #omitargs(1, 11, d=4) #omitargs(1, d=11) #omitargs(11, d=4) #omitargs(d=4, a=11) #omitargs(1, b=11, d=4) #omitargs(1, b=2, d=11) #omitargs(1, b=2, c=11) #omitargs(1, b=11) #omitargs(d=8, c=11, b=6, a=1) #omitargs(d=8, c=7, a=11) # caveat: invald calls still fail, but at func(*pargs, **kargs) # #omitargs() #omitargs(d=8, c=7, b=6) # caveat: still does nothing about * and ** in the # decorated function, but we probably don't need to care; def func(a, b=8, *pargs, **kargs): print('func:', pargs, kargs) # jun-2018: 3.X compatible prints code = func.__code__ print('expected:', code.co_varnames[:code.co_argcount]) def wrap(func): def onCall(*pargs, **kargs): print('onCall:', pargs, kargs) func(*pargs, **kargs) return onCall func = wrap(func) func(1, 2, 3, 4, x=4, y=5) #func(1, x=4, y=5) #==> #expected: ('a', 'b') #func: (3, 4) {'y': 5, 'x': 4} #onCall: (1, 2, 3, 4) {'y': 5, 'x': 4} #func: (3, 4) {'y': 5, 'x': 4} # caveat: could also test ranges inside the function or use # assert, but decorator coding patter supports more complex # requirements better