Add betterhelp epilogue feature, flatten some logic.
This commit is contained in:
parent
40410496fc
commit
8cb50d8cac
3 changed files with 104 additions and 24 deletions
|
@ -4,9 +4,32 @@ import functools
|
|||
from voussoirkit import pipeable
|
||||
from voussoirkit import vlogging
|
||||
|
||||
log = vlogging.getLogger(__name__)
|
||||
log = vlogging.get_logger(__name__)
|
||||
|
||||
HELPSTRINGS = {'', 'help', '-h', '--help'}
|
||||
# When using a single parser or a command of a subparser, the presence of any
|
||||
# of these strings in argv will trigger the helptext.
|
||||
# > application.py --help
|
||||
# > application.py command --help
|
||||
HELP_ARGS = {'-h', '--help'}
|
||||
|
||||
# When using a subparser, the command name can be any of these to trigger
|
||||
# the helptext.
|
||||
# > application.py help
|
||||
# > application.py --help
|
||||
# This does not apply to single-parser applications because the user might try
|
||||
# to pass the word "help" as the actual argument to the program, but in a
|
||||
# subparser application it's very unlikely that there is an actual command
|
||||
# called help.
|
||||
HELP_COMMANDS = {'help', '-h', '--help'}
|
||||
|
||||
# Modules can add additional helptexts to this set, and they will appear
|
||||
# after the program's main docstring is shown. This is used when the module
|
||||
# intercepts sys.argv to change program behavior beyond the options provided
|
||||
# by the program's argparse. For example, voussoirkit.vlogging.main_decorator
|
||||
# adds command-line arguments like --debug which the application's argparse
|
||||
# is not aware of. vlogging registers an epilogue here so that all vlogging-
|
||||
# enabled applications gain the relevant helptext for free.
|
||||
HELPTEXT_EPILOGUES = set()
|
||||
|
||||
# INTERNALS
|
||||
################################################################################
|
||||
|
@ -85,19 +108,32 @@ def listget(li, index, fallback=None):
|
|||
except IndexError:
|
||||
return fallback
|
||||
|
||||
def print_helptext(text) -> None:
|
||||
'''
|
||||
Print the given text to stderr, along with any epilogues added by
|
||||
other modules.
|
||||
'''
|
||||
fulltext = [text.strip()] + sorted(epi.strip() for epi in HELPTEXT_EPILOGUES)
|
||||
fulltext = '\n\n'.join(fulltext)
|
||||
pipeable.stderr()
|
||||
pipeable.stderr(fulltext)
|
||||
|
||||
def set_alias_docstrings(sub_docstrings, subparser_action) -> dict:
|
||||
'''
|
||||
When using subparser aliases:
|
||||
When using subparser aliases like the following:
|
||||
|
||||
subp = parser.add_subparser('command', aliases=['comm'])
|
||||
subp = parser.add_subparser('do_this', aliases=['do-this'])
|
||||
|
||||
The _SubParsersAction will contain a dictionary `choices` of
|
||||
{'command': ArgumentParser, 'comm': ArgumentParser}.
|
||||
{'do_this': ArgumentParser, 'do-this': ArgumentParser}.
|
||||
This choices dict does not indicate which one was the original name;
|
||||
all aliases are equal. So, we'll identify which names are aliases because
|
||||
their ArgumentParsers will have the same ID in memory. And, as long as one
|
||||
of those aliases is in the provided docstrings, all the other aliases will
|
||||
get that docstring too.
|
||||
|
||||
This function modifies the given sub_docstrings so that all aliases are
|
||||
present as keys.
|
||||
'''
|
||||
sub_docstrings = {name.lower(): docstring for (name, docstring) in sub_docstrings.items()}
|
||||
# aliases is a map of {action object's id(): [list of alias name strings]}.
|
||||
|
@ -140,12 +176,15 @@ def single_betterhelp(parser, docstring):
|
|||
def wrapper(main):
|
||||
@functools.wraps(main)
|
||||
def wrapped(argv):
|
||||
argument = listget(argv, 0, '').lower()
|
||||
if len(argv) == 0 and can_bare:
|
||||
return main(argv)
|
||||
|
||||
if argument == '' and can_bare:
|
||||
pass
|
||||
elif argument in HELPSTRINGS:
|
||||
pipeable.stderr(docstring)
|
||||
if len(argv) == 0:
|
||||
print_helptext(docstring)
|
||||
return 1
|
||||
|
||||
if any(arg.lower() in HELP_ARGS for arg in argv):
|
||||
print_helptext(docstring)
|
||||
return 1
|
||||
|
||||
return main(argv)
|
||||
|
@ -166,21 +205,33 @@ def subparser_betterhelp(parser, main_docstring, sub_docstrings):
|
|||
if command == '' and can_bare:
|
||||
return main(argv)
|
||||
|
||||
if command not in sub_docstrings:
|
||||
pipeable.stderr(main_docstring)
|
||||
if command == '':
|
||||
because = 'you did not choose a command'
|
||||
pipeable.stderr(f'You are seeing the default help text because {because}.')
|
||||
elif command not in HELPSTRINGS:
|
||||
because = f'"{command}" was not recognized'
|
||||
pipeable.stderr(f'You are seeing the default help text because {because}.')
|
||||
if command == '':
|
||||
print_helptext(main_docstring)
|
||||
because = 'you did not choose a command'
|
||||
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
||||
return 1
|
||||
|
||||
argument = listget(argv, 1, '').lower()
|
||||
if argument == '' and command in can_bares:
|
||||
pass
|
||||
elif argument in HELPSTRINGS:
|
||||
pipeable.stderr(sub_docstrings[command])
|
||||
if command in HELP_COMMANDS:
|
||||
print_helptext(main_docstring)
|
||||
return 1
|
||||
|
||||
if command not in sub_docstrings:
|
||||
print_helptext(main_docstring)
|
||||
because = f'"{command}" was not recognized'
|
||||
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
||||
return 1
|
||||
|
||||
arguments = argv[1:]
|
||||
|
||||
if len(arguments) == 0 and command in can_bares:
|
||||
return main(argv)
|
||||
|
||||
if len(arguments) == 0:
|
||||
print_helptext(sub_docstrings[command])
|
||||
return 1
|
||||
|
||||
if any(arg.lower() in HELP_ARGS for arg in arguments):
|
||||
print_helptext(sub_docstrings[command])
|
||||
return 1
|
||||
|
||||
return main(argv)
|
||||
|
|
|
@ -54,12 +54,25 @@ import io
|
|||
import sys
|
||||
import traceback
|
||||
|
||||
from voussoirkit import betterhelp
|
||||
from voussoirkit import pipeable
|
||||
from voussoirkit import vlogging
|
||||
|
||||
log = vlogging.getLogger(__name__, 'operatornotify')
|
||||
|
||||
BETTERHELP_EPILOGUE = '''
|
||||
This program uses voussoirkit.operatornotify to allow the program operator to
|
||||
receive messages about important events. See operatornotify.py's docstring to
|
||||
learn how to create your own my_operatornotify file. Then, you can call this
|
||||
program with the following arguments:
|
||||
|
||||
--operatornotify
|
||||
opts in to notifications and will capture logging at the WARNING level.
|
||||
|
||||
--operatornotify-level X
|
||||
opts in to notifications and will capture logging at level X, where X is
|
||||
e.g. debug, info, warning, error, critical.
|
||||
'''
|
||||
|
||||
####################################################################################################
|
||||
|
||||
def default_notify(subject, body=''):
|
||||
|
@ -239,6 +252,8 @@ def main_decorator(subject, *, log_return_value=True, **kwargs):
|
|||
2. Remove those args from argv so your argparse doesn't know the difference.
|
||||
3. Wrap main call with main_log_context.
|
||||
'''
|
||||
from voussoirkit import betterhelp
|
||||
betterhelp.HELPTEXT_EPILOGUES.add(BETTERHELP_EPILOGUE)
|
||||
def wrapper(main):
|
||||
@functools.wraps(main)
|
||||
def wrapped(argv, *args, **kwargs):
|
||||
|
|
|
@ -14,6 +14,18 @@ import sys
|
|||
|
||||
_getLogger = getLogger
|
||||
|
||||
BETTERHELP_EPILOGUE = '''
|
||||
This program uses voussoirkit.vlogging to make logging controls accessible via
|
||||
command line arguments. You can add the following arguments to change the
|
||||
logging level:
|
||||
|
||||
--loud
|
||||
--debug
|
||||
--warning
|
||||
--quiet
|
||||
--silent
|
||||
'''
|
||||
|
||||
# Python gives the root logger a level of WARNING. The problem is that prevents
|
||||
# any handlers you add to it from receiving lower level messages. WARNING might
|
||||
# be fine for the stderr handler, but you might like to have a log file
|
||||
|
@ -183,6 +195,8 @@ def main_decorator(main):
|
|||
to use --debug, --quiet, etc. on the command line without making any
|
||||
changes to your argparser.
|
||||
'''
|
||||
from voussoirkit import betterhelp
|
||||
betterhelp.HELPTEXT_EPILOGUES.add(BETTERHELP_EPILOGUE)
|
||||
@functools.wraps(main)
|
||||
def wrapped(argv, *args, **kwargs):
|
||||
(level, argv) = get_level_by_argv(argv)
|
||||
|
|
Loading…
Reference in a new issue