Add more detailed help message

This commit is contained in:
Patrick Elmer 2023-08-13 13:25:09 +09:00
parent 46139e21d8
commit 06a2ca2ab1

View File

@ -1,10 +1,10 @@
import inspect import inspect
import sys import sys
import os import re
from pargv import parse_args from pargv import parse_args
def magicli(exclude=['main'], help_message=True, glbls=None, argv=None): def magicli(exclude=['main'], glbls=None, argv=None):
""" """
Get all functions from calling file and interprets them as CLI commands. Get all functions from calling file and interprets them as CLI commands.
Parses command line arguments for function to call Parses command line arguments for function to call
@ -17,70 +17,127 @@ def magicli(exclude=['main'], help_message=True, glbls=None, argv=None):
argv = argv if argv else sys.argv argv = argv if argv else sys.argv
app_name, args, kwargs = format_args(argv) app_name, args, kwargs = format_args(argv)
functions = [f for f in filter_functions(glbls) if f.__name__ not in exclude] if 'version' in kwargs:
print(glbls['__version__']) if '__version__' in glbls else print('Unknown version.')
exit()
if help_message and 'help' in kwargs: if re.search('\.pyc?$', app_name):
print_help_and_exit(app_name, functions) app_name = sys.orig_argv[0] + ' ' + app_name
possible_commands = [f.__name__ for f in functions] function_to_call, *commands = [f for f in filter_functions(glbls) if f.__name__ not in exclude]
function_name = None command_names = [f.__name__ for f in commands]
if len(args) and args[0] in possible_commands: # Call command if specified
function_name = args[0] if args and args[0] in command_names:
function_to_call = glbls.get(args[0])
app_name += f" {args[0]}"
commands = []
args = args[1:] args = args[1:]
elif app_name in possible_commands:
function_name = app_name
else:
print_help_and_exit(app_name, functions)
function_to_call = glbls.get(function_name) if 'help' in kwargs:
print(help_message(app_name, function_to_call, commands))
exit()
try: try:
function_to_call(*args, **kwargs) function_to_call(*args, **kwargs)
except TypeError: except TypeError:
pass print(help_message(app_name, function_to_call, commands))
exit()
def format_args(argv): def format_args(argv):
args, kwargs = parse_args(argv) args, kwargs = parse_args(argv)
app_name = os.path.basename(args[0]) app_name, *args = args
args = args[1:]
return app_name, args, kwargs return app_name, args, kwargs
def filter_functions(args): def filter_functions(glbls):
""" """
Gets list of functions from the globals variable of a specific module (default: __main__). Gets list of functions from the globals variable of a specific module.
""" """
return [v for v in args.values() if inspect.isfunction(v) and v.__module__ == args['__name__']] return [v for v in glbls.values() if inspect.isfunction(v) and v.__module__ == glbls['__name__']]
def print_help_and_exit(app_name, functions): def help_message(app_name, function, commands=[]):
""" """
Print the help message based on functions contained in the calling file. Automatically create a help message.
Exits after displaying the help message.
""" """
print('Usage:') specs = inspect.getfullargspec(function)
for f in functions:
words = [] arguments = specs.args
if app_name != f.__name__: defaults = list(specs.defaults)
words.append(app_name) options = list(reversed([arguments.pop() for _ in defaults])) if defaults else []
words.append(f.__name__)
specs = inspect.getfullargspec(f) arguments_docs = [extract_annotation(specs.annotations[arg]) if arg in specs.annotations else '' for arg in arguments]
words += ['--' + arg for arg in specs.args] options_docs = [extract_annotation(specs.annotations[opt]) if opt in specs.annotations else '' for opt in options]
print(' '*4 + ' '.join(words)) commands_docs = [c.__doc__ if c.__doc__ else '' for c in commands]
exit()
options += ['help', 'version']
options_docs += ['Show the help message.', 'Show version information.']
defaults += ['', '']
help_text = []
usage = [app_name]
if arguments:
usage.append(' '.join(arguments))
help_text.append({'lines': [[argument, doc] for argument, doc in zip(arguments, arguments_docs)], 'heading': 'Arguments:'})
if options:
usage.append('[options]')
help_text.append({'lines': [[f"--{option}", f"{doc} ".lstrip() + f"(default: {default})"*(default!='')] for option, doc, default in zip(options, options_docs, defaults)], 'heading': 'Options:'})
usage = [' '.join(usage)]
if commands:
usage.append(f"{app_name} command [...]")
help_text.append({'lines': [[c.__name__, doc] for c, doc in zip(commands, commands_docs)], 'heading': 'Commands:'})
if function.__doc__:
help_text = [{'lines': [line.strip() for line in function.__doc__.strip().split('\n')]}] + help_text
return format_help_message([{'lines': usage, 'heading': 'Usage:'}] + help_text)
def extract_annotation(spec):
return '' if isinstance(spec, type) else spec.__metadata__[0]
def type_to_str(text):
found = re.findall("'(.*)'", str(text))
return found[0] if found else ''
def format_help_message(list_of_dicts):
"""Usage example:
format_help_message([
{'lines': ['--all', 'All files.'], 'heading': 'Options:'},
{'lines': ['run', 'Run script.'], 'heading': 'Commands:'},
{'lines': 'Copyright 2023'},
])
"""
return '\n\n'.join(format_block(**kwargs) for kwargs in list_of_dicts)
def format_block(lines, heading='', indent=2):
if isinstance(lines[0], list):
max_length = len(max([l[0] for l in lines], key=len))
lines = [f"{line[0]:{max_length}} {line[1]}" for line in lines]
elif isinstance(lines, str):
lines = [lines]
if heading:
return '\n'.join([heading]+['\n'.join([indent*' '+l for l in lines])])
return '\n'.join(['\n'.join([indent*' '+l for l in lines])])
def first_calling_frame(): def first_calling_frame():
for s in reversed(inspect.stack()): for s in reversed(inspect.stack()):
if s.code_context == None: if s.code_context == None:
continue continue
if s.code_context[0].startswith('import magicli'): if s.code_context[0].lstrip().startswith('import magicli'):
return s.frame return s.frame
return None return None
frame = first_calling_frame() frame = first_calling_frame()
if frame != None: if frame != None:
magicli(glbls=frame.f_globals) sys.exit(magicli(glbls=frame.f_globals, exclude=[]))