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

Side by Side Diff: subcommand.py

Issue 22824018: Convert gclient to use subcommand.py (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: fix non-determinism in gclient_smoketest.py Created 7 years, 4 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 | « git_cl.py ('k') | tests/gclient_smoketest.py » ('j') | 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 23 matching lines...) Expand all
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 """ 40 """
41 41
42 import difflib 42 import difflib
43 import sys 43 import sys
44 import textwrap
44 45
45 46
46 def usage(more): 47 def usage(more):
47 """Adds a 'usage_more' property to a CMD function.""" 48 """Adds a 'usage_more' property to a CMD function."""
48 def hook(fn): 49 def hook(fn):
49 fn.usage_more = more 50 fn.usage_more = more
50 return fn 51 return fn
51 return hook 52 return hook
52 53
53 54
55 def epilog(text):
56 """Adds an 'epilog' property to a CMD function.
57
58 It will be shown in the epilog. Usually useful for examples.
59 """
60 def hook(fn):
61 fn.epilog = text
62 return fn
63 return hook
64
65
54 def CMDhelp(parser, args): 66 def CMDhelp(parser, args):
55 """Prints list of commands or help for a specific command.""" 67 """Prints list of commands or help for a specific command."""
56 # This is the default help implementation. It can be disabled or overriden if 68 # This is the default help implementation. It can be disabled or overriden if
57 # wanted. 69 # wanted.
58 if not any(i in ('-h', '--help') for i in args): 70 if not any(i in ('-h', '--help') for i in args):
59 args = args + ['--help'] 71 args = args + ['--help']
60 _, args = parser.parse_args(args) 72 _, args = parser.parse_args(args)
61 # Never gets there. 73 # Never gets there.
62 assert False 74 assert False
63 75
64 76
77 def _get_color_module():
78 """Returns the colorama module if available.
79
80 If so, assumes colors are supported and return the module handle.
81 """
82 return sys.modules.get('colorama') or sys.modules.get('third_party.colorama')
83
84
65 class CommandDispatcher(object): 85 class CommandDispatcher(object):
66 def __init__(self, module): 86 def __init__(self, module):
67 """module is the name of the main python module where to look for commands. 87 """module is the name of the main python module where to look for commands.
68 88
69 The python builtin variable __name__ MUST be used for |module|. If the 89 The python builtin variable __name__ MUST be used for |module|. If the
70 script is executed in the form 'python script.py', __name__ == '__main__' 90 script is executed in the form 'python script.py', __name__ == '__main__'
71 and sys.modules['script'] doesn't exist. On the other hand if it is unit 91 and sys.modules['script'] doesn't exist. On the other hand if it is unit
72 tested, __main__ will be the unit test's module so it has to reference to 92 tested, __main__ will be the unit test's module so it has to reference to
73 itself with 'script'. __name__ always match the right value. 93 itself with 'script'. __name__ always match the right value.
74 """ 94 """
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
119 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3: 139 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
120 # Too ambiguous. 140 # Too ambiguous.
121 return 141 return
122 142
123 if hamming_commands[0][0] < 0.8: 143 if hamming_commands[0][0] < 0.8:
124 # Not similar enough. Don't be a fool and run a random command. 144 # Not similar enough. Don't be a fool and run a random command.
125 return 145 return
126 146
127 return commands[hamming_commands[0][1]] 147 return commands[hamming_commands[0][1]]
128 148
149 def _gen_commands_list(self):
150 """Generates the short list of supported commands."""
151 commands = self.enumerate_commands()
152 docs = sorted(
153 (name, self._create_command_summary(name, handler))
154 for name, handler in commands.iteritems())
155 # Skip commands without a docstring.
156 docs = [i for i in docs if i[1]]
157 # Then calculate maximum length for alignment:
158 length = max(len(c) for c in commands)
159
160 # Look if color is supported.
161 colors = _get_color_module()
162 green = reset = ''
163 if colors:
164 green = colors.Fore.GREEN
165 reset = colors.Fore.RESET
166 return (
167 'Commands are:\n' +
168 ''.join(
169 ' %s%-*s%s %s\n' % (green, length, name, reset, doc)
170 for name, doc in docs))
171
129 def _add_command_usage(self, parser, command): 172 def _add_command_usage(self, parser, command):
130 """Modifies an OptionParser object with the function's documentation.""" 173 """Modifies an OptionParser object with the function's documentation."""
131 name = command.__name__[3:] 174 name = command.__name__[3:]
132 more = getattr(command, 'usage_more', '')
133 if name == 'help': 175 if name == 'help':
134 name = '<command>' 176 name = '<command>'
135 # Use the module's docstring as the description for the 'help' command if 177 # Use the module's docstring as the description for the 'help' command if
136 # available. 178 # available.
137 parser.description = self.module.__doc__ 179 parser.description = (self.module.__doc__ or '').rstrip()
180 if parser.description:
181 parser.description += '\n\n'
182 parser.description += self._gen_commands_list()
183 # Do not touch epilog.
138 else: 184 else:
139 # Use the command's docstring if available. 185 # Use the command's docstring if available. For commands, unlike module
140 parser.description = command.__doc__ 186 # docstring, realign.
141 parser.description = (parser.description or '').strip() 187 lines = (command.__doc__ or '').rstrip().splitlines()
142 if parser.description: 188 if lines[:1]:
143 parser.description += '\n' 189 rest = textwrap.dedent('\n'.join(lines[1:]))
190 parser.description = '\n'.join((lines[0], rest))
191 else:
192 parser.description = lines[0]
193 if parser.description:
194 parser.description += '\n'
195 parser.epilog = getattr(command, 'epilog', None)
196 if parser.epilog:
197 parser.epilog = '\n' + parser.epilog.strip() + '\n'
198
199 more = getattr(command, 'usage_more', '')
144 parser.set_usage( 200 parser.set_usage(
145 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more)) 201 'usage: %%prog %s [options]%s' % (name, '' if not more else ' ' + more))
146 202
147 @staticmethod 203 @staticmethod
148 def _create_command_summary(name, command): 204 def _create_command_summary(name, command):
149 """Creates a oneline summary from the command's docstring.""" 205 """Creates a oneline summary from the command's docstring."""
150 if name != command.__name__[3:]: 206 if name != command.__name__[3:]:
151 # Skip aliases. 207 # Skip aliases.
152 return '' 208 return ''
153 doc = command.__doc__ or '' 209 doc = command.__doc__ or ''
154 line = doc.split('\n', 1)[0].rstrip('.') 210 line = doc.split('\n', 1)[0].rstrip('.')
155 if not line: 211 if not line:
156 return line 212 return line
157 return (line[0].lower() + line[1:]).strip() 213 return (line[0].lower() + line[1:]).strip()
158 214
159 def execute(self, parser, args): 215 def execute(self, parser, args):
160 """Dispatches execution to the right command. 216 """Dispatches execution to the right command.
161 217
162 Fallbacks to 'help' if not disabled. 218 Fallbacks to 'help' if not disabled.
163 """ 219 """
164 commands = self.enumerate_commands() 220 # Unconditionally disable format_description() and format_epilog().
165 length = max(len(c) for c in commands) 221 # Technically, a formatter should be used but it's not worth (yet) the
166 222 # trouble.
167 # Lists all the commands in 'help'. 223 parser.format_description = lambda _: parser.description or ''
168 if commands['help']: 224 parser.format_epilog = lambda _: parser.epilog or ''
169 docs = sorted(
170 (name, self._create_command_summary(name, handler))
171 for name, handler in commands.iteritems())
172 # Skip commands without a docstring.
173 commands['help'].usage_more = (
174 '\n\nCommands are:\n' + '\n'.join(
175 ' %-*s %s' % (length, name, doc) for name, doc in docs if doc))
176 225
177 if args: 226 if args:
178 if args[0] in ('-h', '--help') and len(args) > 1: 227 if args[0] in ('-h', '--help') and len(args) > 1:
179 # Inverse the argument order so 'tool --help cmd' is rewritten to 228 # Inverse the argument order so 'tool --help cmd' is rewritten to
180 # 'tool cmd --help'. 229 # 'tool cmd --help'.
181 args = [args[1], args[0]] + args[2:] 230 args = [args[1], args[0]] + args[2:]
182 command = self.find_nearest_command(args[0]) 231 command = self.find_nearest_command(args[0])
183 if command: 232 if command:
184 if command.__name__ == 'CMDhelp' and len(args) > 1: 233 if command.__name__ == 'CMDhelp' and len(args) > 1:
185 # Inverse the arguments order so 'tool help cmd' is rewritten to 234 # Inverse the arguments order so 'tool help cmd' is rewritten to
186 # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work 235 # 'tool cmd --help'. Do it here since we want 'tool hel cmd' to work
187 # too. 236 # too.
188 args = [args[1], '--help'] + args[2:] 237 args = [args[1], '--help'] + args[2:]
189 command = self.find_nearest_command(args[0]) or command 238 command = self.find_nearest_command(args[0]) or command
190 239
191 # "fix" the usage and the description now that we know the subcommand. 240 # "fix" the usage and the description now that we know the subcommand.
192 self._add_command_usage(parser, command) 241 self._add_command_usage(parser, command)
193 return command(parser, args[1:]) 242 return command(parser, args[1:])
194 243
195 if commands['help']: 244 cmdhelp = self.enumerate_commands().get('help')
245 if cmdhelp:
196 # Not a known command. Default to help. 246 # Not a known command. Default to help.
197 self._add_command_usage(parser, commands['help']) 247 self._add_command_usage(parser, cmdhelp)
198 return commands['help'](parser, args) 248 return cmdhelp(parser, args)
199 249
200 # Nothing can be done. 250 # Nothing can be done.
201 return 2 251 return 2
OLDNEW
« no previous file with comments | « git_cl.py ('k') | tests/gclient_smoketest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698