Add betterhelp epilogue feature, flatten some logic.

This commit is contained in:
voussoir 2021-11-07 19:25:37 -08:00
parent 40410496fc
commit 8cb50d8cac
No known key found for this signature in database
GPG key ID: 5F7554F8C26DACCB
3 changed files with 104 additions and 24 deletions

View file

@ -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)

View file

@ -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):

View file

@ -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)