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