# -*- coding: UTF-8 -*- # Copyright 2015 Red Hat, Inc. # Part of clufter project # Licensed under GPLv2+ (a copy included | http://gnu.org/licenses/gpl-2.0.txt) """Command manager""" __author__ = "Jan Pokorný " from logging import getLogger from os.path import abspath, dirname, join from textwrap import wrap from sys import modules from .command import commands, CommandAlias from .error import ClufterError, ClufterPlainError, \ EC from .filter_manager import FilterManager from .plugin_registry import PluginManager from .utils import filterdict_keep from .utils_func import apply_intercalate, bifilter from .utils_prog import make_options, set_logging log = getLogger(__name__) class CommandManagerError(ClufterError): pass class CommandNotFoundError(ClufterPlainError): def __init__(self, cmd): super(CommandNotFoundError, self).__init__("Command not found: `{0}'", cmd) class CommandManager(PluginManager): """Class responsible for commands routing to filters or other actions""" _default_registry = commands @classmethod def _init_plugins(cls, commands, flt_mgr=None, *args, **kwargs): log.debug("Commands before resolving: {0}".format(commands)) if flt_mgr is None: flts = set() for cmd in commands.itervalues(): map(lambda flt: flts.add(flt), apply_intercalate(getattr(cmd, 'filter_chain', ()))) flt_mgr = FilterManager.init_lookup(flts, **kwargs) return cls._resolve(flt_mgr.filters, commands, *args, **filterdict_keep(kwargs, 'system', 'system_extra')) @staticmethod def _resolve(filters, commands, system='', system_extra=''): # name -> (cmd obj if not alias or resolvable name) aliases = [] inverse_commands = dict((b, a) for a, b in commands.iteritems()) # first, resolve end-use commands for cmd_name, cmd_cls in commands.items(): if issubclass(cmd_cls, CommandAlias): aliases.append(cmd_name) continue ret = cmd_cls(filters) if ret is not None: commands[cmd_name] = ret else: commands.pop(cmd_name) # only then resolve the command aliases, track a string identifying # end-use command in `commands` for cmd_name in aliases: alias = commands[cmd_name] if not isinstance(alias, basestring): # not resolved yet assert issubclass(alias, CommandAlias) resolved = alias(filters, commands, inverse_commands, system, system_extra) resolved_cls = type(resolved) if resolved_cls not in inverse_commands: if resolved is not None: log.warning("Resolve at `{0}' alias: target unknown" .format(cmd_name)) commands.pop(cmd_name) else: commands[cmd_name] = inverse_commands[resolved_cls] return commands @property def commands(self): return self._plugins def __call__(self, parser, args=None): """Follow up of the entry point, facade to particular commands""" ec = EC.EXIT_SUCCESS values = parser.values try: canonical_cmd = command = cmd = getattr(values, 'help', None) \ or args[0] while isinstance(command, basestring): canonical_cmd = command command = self._plugins.get(command, None) if not command: # XXX maybe it just hasn't been resolved successfully raise CommandNotFoundError(cmd) opt_group = parser.option_groups[0] parser.description, options = command.parser_desc_opts(opt_group) opt_group.add_options(make_options(options)) args = ['--help'] if values.help else args[1:] parser.defaults.update(values.__dict__) # from global options opts, args = parser.parse_args(args) if opts.help: sep = '\n{0:^{width}}'.format('or:', width=len('Usage: ')) usage = sep.join(map( lambda c: "{0}{1} []" .format("%prog [ ...] " if parser.prog != c else '', c), sorted(set([cmd, canonical_cmd]), key=lambda i: int(i == canonical_cmd)) )) print parser.format_customized_help(usage=usage) return ec set_logging(opts) log.debug("Running command `{0}'; opts={1}, args={2}" .format(cmd, opts.__dict__, args)) ec = command(opts, args) except ClufterError as e: ec = EC.EXIT_FAILURE print e if isinstance(e, CommandNotFoundError): print "\n" + self.pretty_cmds() #except Exception as e: # print "OOPS: underlying unexpected exception:\n{0}".format(e) # ec = EC.EXIT_FAILURE return ec def pretty_cmds(self, text_width=77, linesep_width=1, ind=' ', itemsep='\n', secsep='\n', intmark=' *', cmds_intro='Commands', aliases_intro='Aliases:', ellip_yes='; only built-in ones included:', ellip_no='; those without star (\'*\') are built-in:', ref_str='alias for {0}', ellip=False): """Return string containing formatted list of commands (name + desc)""" pth = join(dirname(abspath(__file__)), self._registry.__name__) # int. cmds_aliases = [ ([(name, ref_str.format(obj) if i else obj.__doc__.splitlines()[0], i or dirname(modules[obj.__class__.__module__].__file__) == pth) for name, obj in sorted(cat) if obj], max((0, ) + tuple(len(name) for name, obj in cat if obj and (not ellip or i or dirname( modules[obj.__class__.__module__].__file__ ) == pth))) ) for i, cat in enumerate( bifilter(lambda (name, obj): not isinstance(obj, basestring), self._plugins.iteritems()) ) ] width = max(i[1] for i in cmds_aliases) + linesep_width desc_indent = ind + (width * ' ') text_width = max(text_width - len(desc_indent), 20) cmds_intro += ellip_yes if ellip else ellip_no return secsep.join( itemsep.join([header] + [ '{0}{1:{width}}{2}'.format( ind if internal else intmark + ind[len(intmark):], name, '\n'.join( wrap(desc, width=text_width, subsequent_indent=desc_indent) ), width=width ) for name, desc, internal in i[0] if not ellip or internal ]) for header, i in zip((cmds_intro, aliases_intro), cmds_aliases) )