show the distribution providing the command in help output

When listing all available commands, include the distribution name if
it does not match the distribution of the main application.

When showing the verbose help for a single command, include the
distribution name if it does not match the distribution of the main
application.

Change-Id: I4ddb3327c62cfd0f82167c15e9513ece6a3689c4
Signed-off-by: Doug Hellmann <doug@doughellmann.com>
This commit is contained in:
Doug Hellmann 2017-09-07 16:15:36 -04:00 committed by Dean Troyer
parent dacbd601d8
commit 31ec27dc61
2 changed files with 62 additions and 8 deletions

View File

@ -13,11 +13,47 @@
import abc
import inspect
import pkg_resources
import six
from stevedore import extension
from cliff import _argparse
_dists_by_mods = None
def _get_distributions_by_modules():
"""Return dict mapping module name to distribution names.
The python package name (the name used for importing) and the
distribution name (the name used with pip and PyPI) do not
always match. We want to report which distribution caused the
command to be installed, so we need to look up the values.
"""
global _dists_by_mods
if _dists_by_mods is None:
results = {}
for dist in pkg_resources.working_set:
try:
mod_name = dist.get_metadata('top_level.txt').strip()
except KeyError:
# Could not retrieve metadata. Ignore.
pass
else:
results[mod_name] = dist.project_name
_dists_by_mods = results
return _dists_by_mods
def _get_distribution_for_module(module):
"Return the distribution containing the module."
dist_name = None
if module:
pkg_name = module.__name__.partition('.')[0]
dist_name = _get_distributions_by_modules().get(pkg_name)
return dist_name
@six.add_metaclass(abc.ABCMeta)
class Command(object):
@ -89,17 +125,23 @@ class Command(object):
def get_epilog(self):
"""Return the command epilog."""
# replace a None in self._epilog with an empty string
parts = [self._epilog or '']
hook_epilogs = filter(
None,
(h.obj.get_epilog() for h in self._hooks),
)
if hook_epilogs:
# combine them, replacing a None in self._epilog with an
# empty string
parts = [self._epilog or '']
parts.extend(hook_epilogs)
return '\n\n'.join(parts)
return self._epilog
parts.extend(hook_epilogs)
app_dist_name = _get_distribution_for_module(
inspect.getmodule(self.app)
)
dist_name = _get_distribution_for_module(inspect.getmodule(self))
if dist_name and dist_name != app_dist_name:
parts.append(
'This command is provided by the %s plugin.' %
(dist_name,)
)
return '\n\n'.join(parts)
def get_parser(self, prog_name):
"""Return an :class:`argparse.ArgumentParser`.

View File

@ -29,6 +29,13 @@ class HelpAction(argparse.Action):
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands:\n')
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)
app_dist = dist_for_obj(app)
command_manager = app.command_manager
for name, ep in sorted(command_manager):
try:
@ -51,7 +58,12 @@ class HelpAction(argparse.Action):
traceback.print_exc(file=app.stdout)
continue
one_liner = cmd.get_description().split('\n')[0]
app.stdout.write(' %-13s %s\n' % (name, one_liner))
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))
sys.exit(0)