1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13"""Discover and lookup command plugins. 14""" 15 16import logging 17 18import pkg_resources 19 20from . import utils 21 22 23LOG = logging.getLogger(__name__) 24 25 26def _get_commands_by_partial_name(args, commands): 27 n = len(args) 28 candidates = [] 29 for command_name in commands: 30 command_parts = command_name.split() 31 if len(command_parts) != n: 32 continue 33 if all(command_parts[i].startswith(args[i]) for i in range(n)): 34 candidates.append(command_name) 35 return candidates 36 37 38class EntryPointWrapper(object): 39 """Wrap up a command class already imported to make it look like a plugin. 40 """ 41 42 def __init__(self, name, command_class): 43 self.name = name 44 self.command_class = command_class 45 46 def load(self, require=False): 47 return self.command_class 48 49 50class CommandManager(object): 51 """Discovers commands and handles lookup based on argv data. 52 53 :param namespace: String containing the setuptools entrypoint namespace 54 for the plugins to be loaded. For example, 55 ``'cliff.formatter.list'``. 56 :param convert_underscores: Whether cliff should convert underscores to 57 spaces in entry_point commands. 58 """ 59 def __init__(self, namespace, convert_underscores=True): 60 self.commands = {} 61 self._legacy = {} 62 self.namespace = namespace 63 self.convert_underscores = convert_underscores 64 self.group_list = [] 65 self._load_commands() 66 67 def _load_commands(self): 68 # NOTE(jamielennox): kept for compatibility. 69 if self.namespace: 70 self.load_commands(self.namespace) 71 72 def load_commands(self, namespace): 73 """Load all the commands from an entrypoint""" 74 self.group_list.append(namespace) 75 for ep in pkg_resources.iter_entry_points(namespace): 76 LOG.debug('found command %r', ep.name) 77 cmd_name = (ep.name.replace('_', ' ') 78 if self.convert_underscores 79 else ep.name) 80 self.commands[cmd_name] = ep 81 return 82 83 def __iter__(self): 84 return iter(self.commands.items()) 85 86 def add_command(self, name, command_class): 87 self.commands[name] = EntryPointWrapper(name, command_class) 88 89 def add_legacy_command(self, old_name, new_name): 90 """Map an old command name to the new name. 91 92 :param old_name: The old command name. 93 :type old_name: str 94 :param new_name: The new command name. 95 :type new_name: str 96 97 """ 98 self._legacy[old_name] = new_name 99 100 def find_command(self, argv): 101 """Given an argument list, find a command and 102 return the processor and any remaining arguments. 103 """ 104 start = self._get_last_possible_command_index(argv) 105 for i in range(start, 0, -1): 106 name = ' '.join(argv[:i]) 107 search_args = argv[i:] 108 # The legacy command handling may modify name, so remember 109 # the value we actually found in argv so we can return it. 110 return_name = name 111 # Convert the legacy command name to its new name. 112 if name in self._legacy: 113 name = self._legacy[name] 114 115 found = None 116 if name in self.commands: 117 found = name 118 else: 119 candidates = _get_commands_by_partial_name( 120 argv[:i], self.commands) 121 if len(candidates) == 1: 122 found = candidates[0] 123 if found: 124 cmd_ep = self.commands[found] 125 if hasattr(cmd_ep, 'resolve'): 126 cmd_factory = cmd_ep.resolve() 127 else: 128 # NOTE(dhellmann): Some fake classes don't take 129 # require as an argument. Yay? 130 arg_spec = utils.getargspec(cmd_ep.load) 131 if 'require' in arg_spec[0]: 132 cmd_factory = cmd_ep.load(require=False) 133 else: 134 cmd_factory = cmd_ep.load() 135 return (cmd_factory, return_name, search_args) 136 else: 137 raise ValueError('Unknown command %r' % 138 (argv,)) 139 140 def _get_last_possible_command_index(self, argv): 141 """Returns the index after the last argument 142 in argv that can be a command word 143 """ 144 for i, arg in enumerate(argv): 145 if arg.startswith('-'): 146 return i 147 return len(argv) 148 149 def add_command_group(self, group=None): 150 """Adds another group of command entrypoints""" 151 if group: 152 self.load_commands(group) 153 154 def get_command_groups(self): 155 """Returns a list of the loaded command groups""" 156 return self.group_list 157 158 def get_command_names(self, group=None): 159 """Returns a list of commands loaded for the specified group""" 160 group_list = [] 161 if group is not None: 162 for ep in pkg_resources.iter_entry_points(group): 163 cmd_name = ( 164 ep.name.replace('_', ' ') 165 if self.convert_underscores 166 else ep.name 167 ) 168 group_list.append(cmd_name) 169 return group_list 170 return list(self.commands.keys()) 171