I have spent some time coding in python and ran accross the problem of parsing' comand line parameteras and validating them. The python argparse library proved to be great at parsing but at first glance did not provide obvious means for validating the parameters.
It turns out however that there is a feature in the argparse library we can exploit to easily add that.
Consider the following example from the argparse documentation:
import argparse
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='an integer for the accumulator')
parser.add_argument('--sum', dest='accumulate', action='store_const',
const=sum, default=max, help='sum the integers (default: find the max)')
args = parser.parse_args()
print(args.accumulate(args.integers))
Consider especially the parameter type=int to the first argument.
On the surface what this does is passing a function to convert the argument to the desired type which was not what I was trying to do. But considering that this is an arbitrary function call this is a great place for injecting my own validation code.
The naive approach would write a validation function for each parameter and then passing it to the type argument and then do the checks there. This is quite good but it can be improved.
Consider this function
def create_checked(predicate,error="ERROR: value does not meet constraints"):
def fun(value):
if predicate(value):
return value
raise Exception(error)
return fun
which is then used like this
parser.add_argument('integers', metavar='N', type=create_checked(int), nargs='+',
help='an integer for the accumulator')
In this example the situation is actually not improving, we are checking if the argument can be converted to string and if a false value is returned we are trowing an exception. This is a bit redundant as the int function already throws.
But lets consider another example
parser.add_argument('config', metavar='FILENAME', type=create_checked(os.path.isfile), help='a config file')
This is more useful, now we can use standard function to check if input files exists and any other function that already exists and can return a boolean based on a string like a pathname (os.path.isdir comes to mind).
We are still not done thou.
What if we want to check more than one thing about a parameter, or we want to check that the file does not exists.
Enter the following functions
def And(*predicates):
def inner(obj):
for p in predicates:
if not p(obj):
return False
return True
return inner
def Or(*predicates):
def inner(obj):
for p in predicates:
if p(obj):
return True
return False
return inner
def Not(predicate):
def inner(obj):
return not predicate(obj)
return inner
These functions allows us to combine several things to check regarding the same parameter or to negate the value of a checking function.
parser.add_argument('output', metavar='FILENAME', type=create_checked(Not(os.path.isfile)), help='an output file')
parser.add_argument('input-zip', metavar='FILENAME', type=create_checked(And(os.path.isfile,valid_zip)), help='an input zip file')
The code above first checks that the output file does not exists and then for the input parameter checks that the file exists and is a valid zip file (assuming the valid_zip function exists).
That's all for this post, hope you find it useful
Source code for this: create_checked.py
Feel free to use this code for any and all purposes, consider it in the public domain or if that is not workable for you you can use it under the terms of the MIT License