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 pipeable
|
||||||
from voussoirkit import vlogging
|
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
|
# INTERNALS
|
||||||
################################################################################
|
################################################################################
|
||||||
|
@ -85,19 +108,32 @@ def listget(li, index, fallback=None):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return fallback
|
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:
|
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
|
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;
|
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
|
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
|
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
|
of those aliases is in the provided docstrings, all the other aliases will
|
||||||
get that docstring too.
|
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()}
|
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]}.
|
# 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):
|
def wrapper(main):
|
||||||
@functools.wraps(main)
|
@functools.wraps(main)
|
||||||
def wrapped(argv):
|
def wrapped(argv):
|
||||||
argument = listget(argv, 0, '').lower()
|
if len(argv) == 0 and can_bare:
|
||||||
|
return main(argv)
|
||||||
|
|
||||||
if argument == '' and can_bare:
|
if len(argv) == 0:
|
||||||
pass
|
print_helptext(docstring)
|
||||||
elif argument in HELPSTRINGS:
|
return 1
|
||||||
pipeable.stderr(docstring)
|
|
||||||
|
if any(arg.lower() in HELP_ARGS for arg in argv):
|
||||||
|
print_helptext(docstring)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
return main(argv)
|
return main(argv)
|
||||||
|
@ -166,21 +205,33 @@ def subparser_betterhelp(parser, main_docstring, sub_docstrings):
|
||||||
if command == '' and can_bare:
|
if command == '' and can_bare:
|
||||||
return main(argv)
|
return main(argv)
|
||||||
|
|
||||||
if command not in sub_docstrings:
|
if command == '':
|
||||||
pipeable.stderr(main_docstring)
|
print_helptext(main_docstring)
|
||||||
if command == '':
|
because = 'you did not choose a command'
|
||||||
because = 'you did not choose a command'
|
pipeable.stderr(f'\nYou are seeing the default help text because {because}.')
|
||||||
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}.')
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
argument = listget(argv, 1, '').lower()
|
if command in HELP_COMMANDS:
|
||||||
if argument == '' and command in can_bares:
|
print_helptext(main_docstring)
|
||||||
pass
|
return 1
|
||||||
elif argument in HELPSTRINGS:
|
|
||||||
pipeable.stderr(sub_docstrings[command])
|
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 1
|
||||||
|
|
||||||
return main(argv)
|
return main(argv)
|
||||||
|
|
|
@ -54,12 +54,25 @@ import io
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from voussoirkit import betterhelp
|
|
||||||
from voussoirkit import pipeable
|
from voussoirkit import pipeable
|
||||||
from voussoirkit import vlogging
|
from voussoirkit import vlogging
|
||||||
|
|
||||||
log = vlogging.getLogger(__name__, 'operatornotify')
|
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=''):
|
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.
|
2. Remove those args from argv so your argparse doesn't know the difference.
|
||||||
3. Wrap main call with main_log_context.
|
3. Wrap main call with main_log_context.
|
||||||
'''
|
'''
|
||||||
|
from voussoirkit import betterhelp
|
||||||
|
betterhelp.HELPTEXT_EPILOGUES.add(BETTERHELP_EPILOGUE)
|
||||||
def wrapper(main):
|
def wrapper(main):
|
||||||
@functools.wraps(main)
|
@functools.wraps(main)
|
||||||
def wrapped(argv, *args, **kwargs):
|
def wrapped(argv, *args, **kwargs):
|
||||||
|
|
|
@ -14,6 +14,18 @@ import sys
|
||||||
|
|
||||||
_getLogger = getLogger
|
_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
|
# 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
|
# 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
|
# 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
|
to use --debug, --quiet, etc. on the command line without making any
|
||||||
changes to your argparser.
|
changes to your argparser.
|
||||||
'''
|
'''
|
||||||
|
from voussoirkit import betterhelp
|
||||||
|
betterhelp.HELPTEXT_EPILOGUES.add(BETTERHELP_EPILOGUE)
|
||||||
@functools.wraps(main)
|
@functools.wraps(main)
|
||||||
def wrapped(argv, *args, **kwargs):
|
def wrapped(argv, *args, **kwargs):
|
||||||
(level, argv) = get_level_by_argv(argv)
|
(level, argv) = get_level_by_argv(argv)
|
||||||
|
|
Loading…
Reference in a new issue