Python argparse custom action and custom type
Package argparse is widely used to parse arguments. It fits the needs nicely in most cases. Sometimes we might want to customize it. In this case, argparse provides custom action and custom type feature.
Custom type without additional arguments
Let us say you want a command line argument that it is a directory with the writable permission. With custom type, it can be done as this:
# test_type.py
import os
import argparsedef ExistingWritableDir(dir_name):
if os.path.exists(dir_name) and os.path.isdir(dir_name) and os.access(dir_name, os.W_OK):
return dir_name # you can return an updated value if needed.
else:
raise argparse.ArgumentTypeError(f"{dir_name} is not an existing writable dir")parser = argparse.ArgumentParser(description="Custom type example")
parser.add_argument('--writable_dir_type', type=ExistingWritableDir)
args = parser.parse_args()
print(f"writable_dir_type: {args.writable_dir_type}")
Running the script you will get something like the following if tt1 is a writable directory:
$ python test_type.py --writable_dir_type tt1
writable_dir_type: tt1
Otherwise, you will get:
$ python test_type.py --writable_dir_type non-dir
usage: test_type.py [-h] [--writable_dir_type WRITABLE_DIR_TYPE]
test_type.py: error: argument --writable_dir_type: non-dir is not an existing writable dir
Custom type with additional arguments
For example, you’d like a script parameter following certain pattern. The pattern is an additional argument passed to the custom type.
import os
import reclass PatternedName(object):
def __init__(self, pattern):
self._pattern = pattern def __call__(self, name):
if re.match(self._pattern, name):
return name
else:
raise argparse.ArgumentTypeError(f"{name} does not follow the expected pattern.")parser = argparse.ArgumentParser(description="Custom type example")
parser.add_argument('--patterned_name', type=PatternedName(r'^[a-f0-9]+$'))
args = parser.parse_args()
Custom action
We can implement the same feature with custom action.
import os
import re
import argparsedef check_existing_writable_dir(parser, dir_name):
if os.path.exists(dir_name) and os.path.isdir(dir_name) and os.access(dir_name, os.W_OK):
return dir_name # you can return an updated value if needed.
else:
parser.error(f"{dir_name} is not an existing writable dir")def check_patterned_name(pattern): # you can also implment this as a class similar to PatternedName
def internal_func(parser, name):
if re.match(pattern, name):
return name
else:
parser.error(f"{name} does not follow the expected pattern.")
return internal_funcclass CustomAction(argparse.Action):
def __init__(self, check_func, *args, **kwargs):
"""
argparse custom action.
:param check_func: callable to do the real check.
"""
self._check_func = check_func
super(CustomAction, self).__init__(*args, **kwargs)def __call__(self, parser, namespace, values, option_string):
if isinstance(values, list):
values = [self._check_func(parser, v) for v in values]
else:
values = self._check_func(parser, values)
setattr(namespace, self.dest, values)parser = argparse.ArgumentParser(description="Custom action example")
parser.add_argument('--writable_dir_action', action=CustomAction, check_func=check_existing_writable_dir)
parser.add_argument('--patterned_name_action', action=CustomAction, check_func=check_patterned_name(r'^[a-f0-9]+'))
args = parser.parse_args()
print(f"""
writable_dir_action: {args.writable_dir_action},
patterned_name_action: {args.patterned_name_action}""")
If the command line argument is a list, simply pass nargs='+'
to add_argument
statement.
Happy coding!