Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(145)

Side by Side Diff: subcommand.py

Issue 1040503003: Formalizes support for '-' in command names. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: Fix regression in new code which broke aliases Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2013 The Chromium Authors. All rights reserved. 1 # Copyright 2013 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """Manages subcommands in a script. 5 """Manages subcommands in a script.
6 6
7 Each subcommand should look like this: 7 Each subcommand should look like this:
8 @usage('[pet name]') 8 @usage('[pet name]')
9 def CMDpet(parser, args): 9 def CMDpet(parser, args):
10 '''Prints a pet. 10 '''Prints a pet.
(...skipping 19 matching lines...) Expand all
30 - Every function in the specified module with a name starting with 'CMD' will 30 - Every function in the specified module with a name starting with 'CMD' will
31 be a subcommand. 31 be a subcommand.
32 - The module's docstring will be used in the default 'help' page. 32 - The module's docstring will be used in the default 'help' page.
33 - If a command has no docstring, it will not be listed in the 'help' page. 33 - If a command has no docstring, it will not be listed in the 'help' page.
34 Useful to keep compatibility commands around or aliases. 34 Useful to keep compatibility commands around or aliases.
35 - If a command is an alias to another one, it won't be documented. E.g.: 35 - If a command is an alias to another one, it won't be documented. E.g.:
36 CMDoldname = CMDnewcmd 36 CMDoldname = CMDnewcmd
37 will result in oldname not being documented but supported and redirecting to 37 will result in oldname not being documented but supported and redirecting to
38 newcmd. Make it a real function that calls the old function if you want it 38 newcmd. Make it a real function that calls the old function if you want it
39 to be documented. 39 to be documented.
40 - CMDfoo_bar will be command 'foo-bar'.
40 """ 41 """
41 42
42 import difflib 43 import difflib
43 import sys 44 import sys
44 import textwrap 45 import textwrap
45 46
46 47
47 def usage(more): 48 def usage(more):
48 """Adds a 'usage_more' property to a CMD function.""" 49 """Adds a 'usage_more' property to a CMD function."""
49 def hook(fn): 50 def hook(fn):
(...skipping 25 matching lines...) Expand all
75 76
76 77
77 def _get_color_module(): 78 def _get_color_module():
78 """Returns the colorama module if available. 79 """Returns the colorama module if available.
79 80
80 If so, assumes colors are supported and return the module handle. 81 If so, assumes colors are supported and return the module handle.
81 """ 82 """
82 return sys.modules.get('colorama') or sys.modules.get('third_party.colorama') 83 return sys.modules.get('colorama') or sys.modules.get('third_party.colorama')
83 84
84 85
86 def _function_to_name(name):
87 """Returns the name of a CMD function."""
88 return name[3:].replace('_', '-')
89
90
85 class CommandDispatcher(object): 91 class CommandDispatcher(object):
86 def __init__(self, module): 92 def __init__(self, module):
87 """module is the name of the main python module where to look for commands. 93 """module is the name of the main python module where to look for commands.
88 94
89 The python builtin variable __name__ MUST be used for |module|. If the 95 The python builtin variable __name__ MUST be used for |module|. If the
90 script is executed in the form 'python script.py', __name__ == '__main__' 96 script is executed in the form 'python script.py', __name__ == '__main__'
91 and sys.modules['script'] doesn't exist. On the other hand if it is unit 97 and sys.modules['script'] doesn't exist. On the other hand if it is unit
92 tested, __main__ will be the unit test's module so it has to reference to 98 tested, __main__ will be the unit test's module so it has to reference to
93 itself with 'script'. __name__ always match the right value. 99 itself with 'script'. __name__ always match the right value.
94 """ 100 """
95 self.module = sys.modules[module] 101 self.module = sys.modules[module]
96 102
97 def enumerate_commands(self): 103 def enumerate_commands(self):
98 """Returns a dict of command and their handling function. 104 """Returns a dict of command and their handling function.
99 105
100 The commands must be in the '__main__' modules. To import a command from a 106 The commands must be in the '__main__' modules. To import a command from a
101 submodule, use: 107 submodule, use:
102 from mysubcommand import CMDfoo 108 from mysubcommand import CMDfoo
103 109
104 Automatically adds 'help' if not already defined. 110 Automatically adds 'help' if not already defined.
105 111
112 Normalizes '_' in the commands to '-'.
113
106 A command can be effectively disabled by defining a global variable to None, 114 A command can be effectively disabled by defining a global variable to None,
107 e.g.: 115 e.g.:
108 CMDhelp = None 116 CMDhelp = None
109 """ 117 """
110 cmds = dict( 118 cmds = dict(
111 (fn[3:], getattr(self.module, fn)) 119 (_function_to_name(name), getattr(self.module, name))
112 for fn in dir(self.module) if fn.startswith('CMD')) 120 for name in dir(self.module) if name.startswith('CMD'))
113 cmds.setdefault('help', CMDhelp) 121 cmds.setdefault('help', CMDhelp)
114 return cmds 122 return cmds
115 123
116 def find_nearest_command(self, name): 124 def find_nearest_command(self, name_asked):
117 """Retrieves the function to handle a command. 125 """Retrieves the function to handle a command as supplied by the user.
118 126
119 It automatically tries to guess the intended command by handling typos or 127 It automatically tries to guess the _intended command_ by handling typos
120 incomplete names. 128 and/or incomplete names.
121 """ 129 """
122 # Implicitly replace foo-bar to foo_bar since foo-bar is not a valid python
123 # symbol but it's faster to type.
124 name = name.replace('-', '_')
125 commands = self.enumerate_commands() 130 commands = self.enumerate_commands()
126 if name in commands: 131 if name_asked in commands:
127 return commands[name] 132 return commands[name_asked]
128 133
129 # An exact match was not found. Try to be smart and look if there's 134 # An exact match was not found. Try to be smart and look if there's
130 # something similar. 135 # something similar.
131 commands_with_prefix = [c for c in commands if c.startswith(name)] 136 commands_with_prefix = [c for c in commands if c.startswith(name_asked)]
132 if len(commands_with_prefix) == 1: 137 if len(commands_with_prefix) == 1:
133 return commands[commands_with_prefix[0]] 138 return commands[commands_with_prefix[0]]
134 139
135 # A #closeenough approximation of levenshtein distance. 140 # A #closeenough approximation of levenshtein distance.
136 def close_enough(a, b): 141 def close_enough(a, b):
137 return difflib.SequenceMatcher(a=a, b=b).ratio() 142 return difflib.SequenceMatcher(a=a, b=b).ratio()
138 143
139 hamming_commands = sorted( 144 hamming_commands = sorted(
140 ((close_enough(c, name), c) for c in commands), 145 ((close_enough(c, name_asked), c) for c in commands),
141 reverse=True) 146 reverse=True)
142 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3: 147 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
143 # Too ambiguous. 148 # Too ambiguous.
144 return 149 return
145 150
146 if hamming_commands[0][0] < 0.8: 151 if hamming_commands[0][0] < 0.8:
147 # Not similar enough. Don't be a fool and run a random command. 152 # Not similar enough. Don't be a fool and run a random command.
148 return 153 return
149 154
150 return commands[hamming_commands[0][1]] 155 return commands[hamming_commands[0][1]]
151 156
152 def _gen_commands_list(self): 157 def _gen_commands_list(self):
153 """Generates the short list of supported commands.""" 158 """Generates the short list of supported commands."""
154 commands = self.enumerate_commands() 159 commands = self.enumerate_commands()
155 docs = sorted( 160 docs = sorted(
156 (name, self._create_command_summary(name, handler)) 161 (cmd_name, self._create_command_summary(cmd_name, handler))
157 for name, handler in commands.iteritems()) 162 for cmd_name, handler in commands.iteritems())
158 # Skip commands without a docstring. 163 # Skip commands without a docstring.
159 docs = [i for i in docs if i[1]] 164 docs = [i for i in docs if i[1]]
160 # Then calculate maximum length for alignment: 165 # Then calculate maximum length for alignment:
161 length = max(len(c) for c in commands) 166 length = max(len(c) for c in commands)
162 167
163 # Look if color is supported. 168 # Look if color is supported.
164 colors = _get_color_module() 169 colors = _get_color_module()
165 green = reset = '' 170 green = reset = ''
166 if colors: 171 if colors:
167 green = colors.Fore.GREEN 172 green = colors.Fore.GREEN
168 reset = colors.Fore.RESET 173 reset = colors.Fore.RESET
169 return ( 174 return (
170 'Commands are:\n' + 175 'Commands are:\n' +
171 ''.join( 176 ''.join(
172 ' %s%-*s%s %s\n' % (green, length, name, reset, doc) 177 ' %s%-*s%s %s\n' % (green, length, cmd_name, reset, doc)
173 for name, doc in docs)) 178 for cmd_name, doc in docs))
174 179
175 def _add_command_usage(self, parser, command): 180 def _add_command_usage(self, parser, command):
176 """Modifies an OptionParser object with the function's documentation.""" 181 """Modifies an OptionParser object with the function's documentation."""
177 name = command.__name__[3:] 182 cmd_name = _function_to_name(command.__name__)
178 if name == 'help': 183 if cmd_name == 'help':
179 name = '<command>' 184 cmd_name = '<command>'
180 # Use the module's docstring as the description for the 'help' command if 185 # Use the module's docstring as the description for the 'help' command if
181 # available. 186 # available.
182 parser.description = (self.module.__doc__ or '').rstrip() 187 parser.description = (self.module.__doc__ or '').rstrip()
183 if parser.description: 188 if parser.description:
184 parser.description += '\n\n' 189 parser.description += '\n\n'
185 parser.description += self._gen_commands_list() 190 parser.description += self._gen_commands_list()
186 # Do not touch epilog. 191 # Do not touch epilog.
187 else: 192 else:
188 # Use the command's docstring if available. For commands, unlike module 193 # Use the command's docstring if available. For commands, unlike module
189 # docstring, realign. 194 # docstring, realign.
190 lines = (command.__doc__ or '').rstrip().splitlines() 195 lines = (command.__doc__ or '').rstrip().splitlines()
191 if lines[:1]: 196 if lines[:1]:
192 rest = textwrap.dedent('\n'.join(lines[1:])) 197 rest = textwrap.dedent('\n'.join(lines[1:]))
193 parser.description = '\n'.join((lines[0], rest)) 198 parser.description = '\n'.join((lines[0], rest))
194 else: 199 else:
195 parser.description = lines[0] if lines else '' 200 parser.description = lines[0] if lines else ''
196 if parser.description: 201 if parser.description:
197 parser.description += '\n' 202 parser.description += '\n'
198 parser.epilog = getattr(command, 'epilog', None) 203 parser.epilog = getattr(command, 'epilog', None)
199 if parser.epilog: 204 if parser.epilog:
200 parser.epilog = '\n' + parser.epilog.strip() + '\n' 205 parser.epilog = '\n' + parser.epilog.strip() + '\n'
201 206
202 more = getattr(command, 'usage_more', '') 207 more = getattr(command, 'usage_more', '')
203 parser.set_usage( 208 extra = '' if not more else ' ' + more
204 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more)) 209 parser.set_usage('usage: %%prog %s [options]%s' % (cmd_name, extra))
205 210
206 @staticmethod 211 @staticmethod
207 def _create_command_summary(name, command): 212 def _create_command_summary(cmd_name, command):
208 """Creates a oneline summary from the command's docstring.""" 213 """Creates a oneliner summary from the command's docstring."""
209 if name != command.__name__[3:]: 214 if cmd_name != _function_to_name(command.__name__):
210 # Skip aliases. 215 # Skip aliases. For example using at module level:
216 # CMDfoo = CMDbar
211 return '' 217 return ''
212 doc = command.__doc__ or '' 218 doc = command.__doc__ or ''
213 line = doc.split('\n', 1)[0].rstrip('.') 219 line = doc.split('\n', 1)[0].rstrip('.')
214 if not line: 220 if not line:
215 return line 221 return line
216 return (line[0].lower() + line[1:]).strip() 222 return (line[0].lower() + line[1:]).strip()
217 223
218 def execute(self, parser, args): 224 def execute(self, parser, args):
219 """Dispatches execution to the right command. 225 """Dispatches execution to the right command.
220 226
(...skipping 24 matching lines...) Expand all
245 return command(parser, args[1:]) 251 return command(parser, args[1:])
246 252
247 cmdhelp = self.enumerate_commands().get('help') 253 cmdhelp = self.enumerate_commands().get('help')
248 if cmdhelp: 254 if cmdhelp:
249 # Not a known command. Default to help. 255 # Not a known command. Default to help.
250 self._add_command_usage(parser, cmdhelp) 256 self._add_command_usage(parser, cmdhelp)
251 return cmdhelp(parser, args) 257 return cmdhelp(parser, args)
252 258
253 # Nothing can be done. 259 # Nothing can be done.
254 return 2 260 return 2
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698