Colourise and automatically page help output

Using the autopage library we can automatically send the help output to
a pager (less, by default), git-style. The pager is configured to not
reset the terminal on exit, avoiding the problem when piping to less
manually that the help text you want to refer to disappears off the
screen when you go to use it. The pager is only invoked when the output
is to the terminal.

Since we invoke the pager, we can ensure that it is correctly set up to
interpret ANSI escape codes, so it is safe to use colour to make the
output easier to read. The autopage library provides light styling of
the default argparse help output, and some additional colour
highlighting is added here for the command list (which is generated by
cliff, not using argparse's formatting code).

Change-Id: If9e1aa5166da32c58cc0fa617f4f81eaa9b2c470
Depends-On: https://review.opendev.org/c/openstack/requirements/+/799343
This commit is contained in:
Zane Bitter 2021-06-04 22:44:13 -04:00
parent 392f3b2e7c
commit 8fa916e916
3 changed files with 57 additions and 38 deletions

View File

@ -12,9 +12,11 @@
"""Overrides of standard argparse behavior."""
import argparse
import argparse as orig_argparse
import warnings
from autopage import argparse
class _ArgumentContainerMixIn(object):
@ -75,12 +77,12 @@ def _handle_conflict_ignore(container, option_string_actions,
)
class _ArgumentGroup(_ArgumentContainerMixIn, argparse._ArgumentGroup):
class _ArgumentGroup(_ArgumentContainerMixIn, orig_argparse._ArgumentGroup):
pass
class _MutuallyExclusiveGroup(_ArgumentContainerMixIn,
argparse._MutuallyExclusiveGroup):
orig_argparse._MutuallyExclusiveGroup):
pass

View File

@ -14,6 +14,8 @@ import argparse
import inspect
import traceback
import autopage.argparse
from . import command
@ -37,43 +39,53 @@ class HelpAction(argparse.Action):
"""
def __call__(self, parser, namespace, values, option_string=None):
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands:\n')
dists_by_module = command._get_distributions_by_modules()
pager = autopage.argparse.help_pager(app.stdout)
color = pager.to_terminal()
autopage.argparse.use_color_for_parser(parser, color)
with pager as out:
parser.print_help(out)
title_hl = ('\033[4m', '\033[0m') if color else ('', '')
out.write('\n%sCommands%s:\n' % title_hl)
dists_by_module = command._get_distributions_by_modules()
def dist_for_obj(obj):
name = inspect.getmodule(obj).__name__.partition('.')[0]
return dists_by_module.get(name)
def dist_for_obj(obj):
name = inspect.getmodule(obj).__name__.partition('.')[0]
return dists_by_module.get(name)
app_dist = dist_for_obj(app)
command_manager = app.command_manager
for name, ep in sorted(command_manager):
try:
factory = ep.load()
except Exception:
app.stdout.write('Could not load %r\n' % ep)
if namespace.debug:
traceback.print_exc(file=app.stdout)
continue
try:
kwargs = {}
if 'cmd_name' in inspect.getfullargspec(factory.__init__).args:
kwargs['cmd_name'] = name
cmd = factory(app, None, **kwargs)
if cmd.deprecated:
app_dist = dist_for_obj(app)
command_manager = app.command_manager
for name, ep in sorted(command_manager):
try:
factory = ep.load()
except Exception:
out.write('Could not load %r\n' % ep)
if namespace.debug:
traceback.print_exc(file=out)
continue
except Exception as err:
app.stdout.write('Could not instantiate %r: %s\n' % (ep, err))
if namespace.debug:
traceback.print_exc(file=app.stdout)
continue
one_liner = cmd.get_description().split('\n')[0]
dist_name = dist_for_obj(factory)
if dist_name and dist_name != app_dist:
dist_info = ' (' + dist_name + ')'
else:
dist_info = ''
app.stdout.write(' %-13s %s%s\n' % (name, one_liner, dist_info))
try:
kwargs = {}
fact_args = inspect.getfullargspec(factory.__init__).args
if 'cmd_name' in fact_args:
kwargs['cmd_name'] = name
cmd = factory(app, None, **kwargs)
if cmd.deprecated:
continue
except Exception as err:
out.write('Could not instantiate %r: %s\n' % (ep, err))
if namespace.debug:
traceback.print_exc(file=out)
continue
one_liner = cmd.get_description().split('\n')[0]
dist_name = dist_for_obj(factory)
if dist_name and dist_name != app_dist:
dist_info = ' (' + dist_name + ')'
if color:
dist_info = '\033[90m%s\033[39m' % dist_info
else:
dist_info = ''
if color:
name = '\033[36m%s\033[39m' % name
out.write(' %-13s %s%s\n' % (name, one_liner, dist_info))
raise HelpExit()
@ -118,7 +130,11 @@ class HelpCommand(command.Command):
else ' '.join([self.app.NAME, cmd_name])
)
cmd_parser = cmd.get_parser(full_name)
cmd_parser.print_help(self.app.stdout)
pager = autopage.argparse.help_pager(self.app.stdout)
with pager as out:
autopage.argparse.use_color_for_parser(cmd_parser,
pager.to_terminal())
cmd_parser.print_help(out)
else:
action = HelpAction(None, None, default=self.app)
action(self.app.parser, self.app.options, None, None)

View File

@ -2,6 +2,7 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr!=2.1.0,>=2.0.0 # Apache-2.0
autopage>=0.4.0 # Apache 2.0
cmd2>=1.0.0 # MIT
PrettyTable>=0.7.2 # BSD
pyparsing>=2.1.0 # MIT