File: rangetest.py

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



AltStyle によって変換されたページ (->オリジナル) /