diff --git a/cliff/command.py b/cliff/command.py index 1c6ac48f..1867150f 100644 --- a/cliff/command.py +++ b/cliff/command.py @@ -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 @@ def get_description(self): 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`. diff --git a/cliff/help.py b/cliff/help.py index 40c48746..971e9ad6 100644 --- a/cliff/help.py +++ b/cliff/help.py @@ -29,6 +29,13 @@ 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() + + 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 @@ def __call__(self, parser, namespace, values, option_string=None): 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)