-
-
Save amarao/36327a6f77b86b90c2bca72ba03c9d3a to your computer and use it in GitHub Desktop.
#!/usr/bin/env python | |
import argparse | |
def main(command_line=None): | |
parser = argparse.ArgumentParser('Blame Praise app') | |
parser.add_argument( | |
'--debug', | |
action='store_true', | |
help='Print debug info' | |
) | |
subparsers = parser.add_subparsers(dest='command') | |
blame = subparsers.add_parser('blame', help='blame people') | |
blame.add_argument( | |
'--dry-run', | |
help='do not blame, just pretend', | |
action='store_true' | |
) | |
blame.add_argument('name', nargs='+', help='name(s) to blame') | |
praise = subparsers.add_parser('praise', help='praise someone') | |
praise.add_argument('name', help='name of person to praise') | |
praise.add_argument( | |
'reason', | |
help='what to praise for (optional)', | |
default="no reason", | |
nargs='?' | |
) | |
args = parser.parse_args(command_line) | |
if args.debug: | |
print("debug: " + str(args)) | |
if args.command == 'blame': | |
if args.dry_run: | |
print("Not for real") | |
print("blaming " + ", ".join(args.name)) | |
elif args.command == 'praise': | |
print('praising ' + args.name + ' for ' + args.reason) | |
if __name__ == '__main__': | |
main() |
This is great - was trying to figure out a way to make a general command-line tool for work and subparser commands fit what I was wanting to do.
Question though - command_line is defined in the function arguments? I'm unsure how this actually works so I'm going to write as I try to think through it. The main() function argument is =None, but this is setting its default value right? And command_line is actually getting set to the arguments given on the command line? How does that work?
Thanks in advance! I just want to solidify my understanding here as much as possible.
There might be a more elegant solution for subparsers by using set_defaults to directly run the desired function:
import argparse
class Test:
def __init__(self) -> None:
pass
def foo(self, args):
print("foo")
def bar(self, args):
print("bar {}".format(args.context))
def main():
test = Test()
parser = argparse.ArgumentParser(description='Foo Bar')
subparsers = parser.add_subparsers(dest='command', help='Commands to run', required=True)
foo = subparsers.add_parser('foo', help='foo some Foos')
foo.set_defaults(func=test.foo)
bar = subparsers.add_parser('bar', help='bar some Bars')
bar.add_argument('context', help='context for bar')
bar.set_defaults(func=test.bar)
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()
edited to include @Cinndy s comment.
Also added required=True to subparser, since there is a known python3 argparse bug, which gives a misleading error message, if you don't provide any arguments.
Traceback (most recent call last):
File "/home/rdo/workspace/repos/test.py", line 30, in <module>
main()
File "/home/rdo/workspace/repos/test.py", line 27, in main
args.func(args)
AttributeError: 'Namespace' object has no attribute 'func'
@luhahn this is great, thank you! One small thing: should the foo definition be foo(self, args)? Thanks again!
@Cinndy indeed, i missed that :)
@luhahn a small thing I don't quite like about this pattern is that it makes all the actual arguments hidden inside Namespace args
instead being explicit. Which also makes it kinda difficult to "retrofit" argparse into a script that has batch of utility functions.
I prefer to keep my actual functions in format similar def func(url, headers=None)
.
This is what I come up with, I know it's not as pretty but works for me. Any input would be appreciated.
import argparse
def minus_one(x):
print(int(x) - 1)
def time(x, y):
print(int(x) * int(y))
def main():
parser = argparse.ArgumentParser(description='Foo Bar')
subparsers = parser.add_subparsers(dest='command', help='Commands to run', required=True)
parser_minus_one = subparsers.add_parser('minusone')
parser_minus_one.add_argument('x', help='X')
parser_minus_one.set_defaults(func=minus_one)
parser_time = subparsers.add_parser('time', help='X times Y')
parser_time.add_argument('x', help='X')
parser_time.add_argument('y', help='Y')
parser_time.set_defaults(func=time)
args = parser.parse_args()
args_ = vars(args).copy()
args_.pop('command', None)
args_.pop('func', None)
args.func(**args_)
if __name__ == '__main__':
main()
@luhahn a small thing I don't quite like about this pattern is that it makes all the actual arguments hidden inside Namespace
args
instead being explicit. Which also makes it kinda difficult to "retrofit" argparse into a script that has batch of utility functions.I prefer to keep my actual functions in format similar
def func(url, headers=None)
.This is what I come up with, I know it's not as pretty but works for me. Any input would be appreciated.
import argparse def minus_one(x): print(int(x) - 1) def time(x, y): print(int(x) * int(y)) def main(): parser = argparse.ArgumentParser(description='Foo Bar') subparsers = parser.add_subparsers(dest='command', help='Commands to run', required=True) parser_minus_one = subparsers.add_parser('minusone') parser_minus_one.add_argument('x', help='X') parser_minus_one.set_defaults(func=minus_one) parser_time = subparsers.add_parser('time', help='X times Y') parser_time.add_argument('x', help='X') parser_time.add_argument('y', help='Y') parser_time.set_defaults(func=time) args = parser.parse_args() args_ = vars(args).copy() args_.pop('command', None) args_.pop('func', None) args.func(**args_) if __name__ == '__main__': main()
Good evening @fireattack
Maybe something more elegant like:
import argparse
import inspect
from beartype.typing import Callable, Any
def minus_one(x: int) -> None:
"""
Print the result of subtracting one from x.
Args:
x (int): The number to subtract
Returns:
None
"""
print(int(x) - 1)
def time(x: int, y: int) -> None:
"""
Print the product of x and y.
Args:
x (int): The first number
y (int): The second number
Returns:
None
"""
print(int(x) * int(y))
def call_function_with_args(func: Callable, args: argparse.Namespace) -> Any:
"""
Call the function with the arguments extracted from the argparse.Namespace object.
Args:
func: The function to call.
args: The argparse.Namespace object containing the arguments.
Returns:
Any: The result of the function call.
Author:
Laurent DECLERCQ, AGON PARTNERS INNOVATION <[email protected]>
"""
# Let's inspect the signature of the function so that we can call it with the correct arguments.
# We make use of the inspect module to get the function signature.
signature = inspect.signature(func)
# Get the parameters of the function using a dictionary comprehension.
# Note: Could be enhanced to handle edge cases (default values, *args, **kwargs, etc.)
args = {parameter: getattr(args, parameter) for parameter in signature.parameters}
# Type cast the arguments to the correct type according to the function signature. We use the annotation of the
# parameter to cast the argument. If the annotation is empty, we keep the argument as is. We only process the
# arguments that are in the function signature.
args = {
parameter: (
signature.parameters[parameter].annotation(args[parameter])
if signature.parameters[parameter].annotation is not inspect.Parameter.empty else args[parameter]
) for parameter in args if parameter in signature.parameters
}
# Call the function with the arguments and return the result if any.
return func(**arguments)
def cli():
parser = argparse.ArgumentParser(description='Foo Bar')
subparsers = parser.add_subparsers(dest='command', help='Commands to run', required=True)
# Define the minusone sub-command.
parser_minus_one = subparsers.add_parser('minusone')
parser_minus_one.add_argument('x', help='X')
parser_minus_one.set_defaults(func=minus_one)
# Define the time sub-command.
parser_time = subparsers.add_parser('time', help='X times Y')
parser_time.add_argument('x', help='X')
parser_time.add_argument('y', help='Y')
parser_time.set_defaults(func=time)
# Parse the arguments from the command line.
args = parser.parse_args()
# Call the function with the arguments.
call_function_with_args(args.func, args)
if __name__ == '__main__':
cli()
Thank you! This example was amazing.