Add more detailed help message
This commit is contained in:
parent
46139e21d8
commit
06a2ca2ab1
131
src/magicli.py
131
src/magicli.py
@ -1,10 +1,10 @@
|
||||
import inspect
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
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.
|
||||
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
|
||||
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:
|
||||
print_help_and_exit(app_name, functions)
|
||||
if re.search('\.pyc?$', app_name):
|
||||
app_name = sys.orig_argv[0] + ' ' + app_name
|
||||
|
||||
possible_commands = [f.__name__ for f in functions]
|
||||
function_name = None
|
||||
function_to_call, *commands = [f for f in filter_functions(glbls) if f.__name__ not in exclude]
|
||||
command_names = [f.__name__ for f in commands]
|
||||
|
||||
if len(args) and args[0] in possible_commands:
|
||||
function_name = args[0]
|
||||
# Call command if specified
|
||||
if args and args[0] in command_names:
|
||||
function_to_call = glbls.get(args[0])
|
||||
app_name += f" {args[0]}"
|
||||
commands = []
|
||||
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:
|
||||
function_to_call(*args, **kwargs)
|
||||
except TypeError:
|
||||
pass
|
||||
print(help_message(app_name, function_to_call, commands))
|
||||
exit()
|
||||
|
||||
|
||||
def format_args(argv):
|
||||
args, kwargs = parse_args(argv)
|
||||
app_name = os.path.basename(args[0])
|
||||
args = args[1:]
|
||||
app_name, *args = args
|
||||
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.
|
||||
Exits after displaying the help message.
|
||||
Automatically create a help message.
|
||||
"""
|
||||
print('Usage:')
|
||||
for f in functions:
|
||||
words = []
|
||||
if app_name != f.__name__:
|
||||
words.append(app_name)
|
||||
words.append(f.__name__)
|
||||
specs = inspect.getfullargspec(f)
|
||||
words += ['--' + arg for arg in specs.args]
|
||||
print(' '*4 + ' '.join(words))
|
||||
exit()
|
||||
specs = inspect.getfullargspec(function)
|
||||
|
||||
arguments = specs.args
|
||||
defaults = list(specs.defaults)
|
||||
options = list(reversed([arguments.pop() for _ in defaults])) if defaults else []
|
||||
|
||||
arguments_docs = [extract_annotation(specs.annotations[arg]) if arg in specs.annotations else '' for arg in arguments]
|
||||
options_docs = [extract_annotation(specs.annotations[opt]) if opt in specs.annotations else '' for opt in options]
|
||||
commands_docs = [c.__doc__ if c.__doc__ else '' for c in commands]
|
||||
|
||||
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():
|
||||
for s in reversed(inspect.stack()):
|
||||
if s.code_context == None:
|
||||
continue
|
||||
if s.code_context[0].startswith('import magicli'):
|
||||
if s.code_context[0].lstrip().startswith('import magicli'):
|
||||
return s.frame
|
||||
return None
|
||||
|
||||
|
||||
frame = first_calling_frame()
|
||||
if frame != None:
|
||||
magicli(glbls=frame.f_globals)
|
||||
sys.exit(magicli(glbls=frame.f_globals, exclude=[]))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user