"""
File devtools.py: function decorator that performs range-test
validation for passed arguments. 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 devtools_test.py for example use cases.
"""

trace = True

def rangetest(**argchecks):                 # Validate ranges for both+defaults
	def onDecorator(func):                  # onCall remembers func and argchecks
		if not __debug__:                   # True if "python -O main.py args..."
			return func                     # Wrap if debugging; else use original
		else:
			import sys
			code     = func.__code__
			allargs  = code.co_varnames[:code.co_argcount]
			funcname = func.__name__

			def onCall(*pargs, **kargs):
				# All pargs match first N expected args by position
				# The rest must be in kargs or be 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)    # OK: run original call
			return onCall
	return onDecorator