File: LP6E/Chapter39/_QuizAnswers/Question3/argtest.py
"""
A function decorator that performs arbitrary passed-in validations
for arguments passed to any function method. Range and type tests
are provided as two example uses; valuetest handles more arbitrary
tests on an argument's value.
Arguments are specified by keyword to the decorator. In the actual
call, arguments may be passed by position or keyword, and defaults
may be omitted. See self-test scripts for example use cases.
Caveats: doesn't fully support nesting because call-proxy args
differ; doesn't validate extra args passed to a decoratee's *args;
and may be no easier than an assert except for provided use cases.
"""
trace = False
def rangetest(**argchecks):
return argtest(argchecks, lambda arg, vals: arg < vals[0] or arg > vals[1])
def typetest(**argchecks):
return argtest(argchecks, lambda arg, type: not isinstance(arg, type))
def valuetest(**argchecks):
return argtest(argchecks, lambda arg, tester: not tester(arg))
def argtest(argchecks, failif): # Validate args per failif + criteria
def onDecorator(func): # onCall retains func, argchecks, failif
if not __debug__: # No-op if "python -O main.py args..."
return func
else:
code = func.__code__
expected = code.co_varnames[:code.co_argcount]
def onError(argname, criteria):
errmsg = f'{func.__name__} argument "{argname}" not {criteria}'
raise TypeError(errmsg)
def onCall(*pargs, **kargs):
positionals = expected[:len(pargs)]
for (argname, criteria) in argchecks.items(): # For all to test
if argname in kargs: # Passed by name
if failif(kargs[argname], criteria):
onError(argname, criteria)
elif argname in positionals: # Passed by posit
position = positionals.index(argname)
if failif(pargs[position], criteria):
onError(argname, criteria)
else: # Not passed-dflt
if trace:
print('Argument "%s" defaulted' % argname)
return func(*pargs, **kargs) # OK: run original call
return onCall
return onDecorator