Add more detailed help message
This commit is contained in:
parent
46139e21d8
commit
06a2ca2ab1
127
src/magicli.py
127
src/magicli.py
@ -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=[]))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user