##// END OF EJS Templates
make subcommands optional
MinRK -
Show More
@@ -1,368 +1,362 b''
1 1 # encoding: utf-8
2 2 """
3 3 A base class for a configurable application.
4 4
5 5 Authors:
6 6
7 7 * Brian Granger
8 8 """
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Copyright (C) 2008-2011 The IPython Development Team
12 12 #
13 13 # Distributed under the terms of the BSD License. The full license is in
14 14 # the file COPYING, distributed as part of this software.
15 15 #-----------------------------------------------------------------------------
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 from copy import deepcopy
22 22 import logging
23 import re
23 24 import sys
24 25
25 26 from IPython.config.configurable import SingletonConfigurable
26 27 from IPython.config.loader import (
27 28 KeyValueConfigLoader, PyFileConfigLoader, Config, ArgumentError
28 29 )
29 30
30 31 from IPython.utils.traitlets import (
31 32 Unicode, List, Int, Enum, Dict, Instance
32 33 )
33 34 from IPython.utils.importstring import import_item
34 35 from IPython.utils.text import indent
35 36
36 37 #-----------------------------------------------------------------------------
37 38 # Descriptions for the various sections
38 39 #-----------------------------------------------------------------------------
39 40
40 41 flag_description = """
41 42 Flags are command-line arguments passed as '--<flag>'.
42 43 These take no parameters, unlike regular key-value arguments.
43 44 They are typically used for setting boolean flags, or enabling
44 45 modes that involve setting multiple options together.
45 46 """.strip() # trim newlines of front and back
46 47
47 48 alias_description = """
48 49 These are commonly set parameters, given abbreviated aliases for convenience.
49 50 They are set in the same `name=value` way as class parameters, where
50 51 <name> is replaced by the real parameter for which it is an alias.
51 52 """.strip() # trim newlines of front and back
52 53
53 54 keyvalue_description = """
54 55 Parameters are set from command-line arguments of the form:
55 56 `Class.trait=value`. Parameters will *never* be prefixed with '-'.
56 57 This line is evaluated in Python, so simple expressions are allowed, e.g.
57 58 `C.a='range(3)'` For setting C.a=[0,1,2]
58 59 """.strip() # trim newlines of front and back
59 60
60 61 #-----------------------------------------------------------------------------
61 62 # Application class
62 63 #-----------------------------------------------------------------------------
63 64
64 65
65 66 class ApplicationError(Exception):
66 67 pass
67 68
68 69
69 70 class Application(SingletonConfigurable):
70 71 """A singleton application with full configuration support."""
71 72
72 73 # The name of the application, will usually match the name of the command
73 74 # line application
74 75 name = Unicode(u'application')
75 76
76 77 # The description of the application that is printed at the beginning
77 78 # of the help.
78 79 description = Unicode(u'This is an application.')
79 80 # default section descriptions
80 81 flag_description = Unicode(flag_description)
81 82 alias_description = Unicode(alias_description)
82 83 keyvalue_description = Unicode(keyvalue_description)
83 84
84 85
85 86 # A sequence of Configurable subclasses whose config=True attributes will
86 87 # be exposed at the command line.
87 88 classes = List([])
88 89
89 90 # The version string of this application.
90 91 version = Unicode(u'0.0')
91 92
92 93 # The log level for the application
93 94 log_level = Enum((0,10,20,30,40,50), default_value=logging.WARN,
94 95 config=True,
95 96 help="Set the log level.")
96 97
97 98 # the alias map for configurables
98 99 aliases = Dict(dict(log_level='Application.log_level'))
99 100
100 101 # flags for loading Configurables or store_const style flags
101 102 # flags are loaded from this dict by '--key' flags
102 103 # this must be a dict of two-tuples, the first element being the Config/dict
103 104 # and the second being the help string for the flag
104 105 flags = Dict()
105 106
106 107 # subcommands for launching other applications
107 108 # if this is not empty, this will be a parent Application
108 109 # this must be a dict of two-tuples, the first element being the application class/import string
109 110 # and the second being the help string for the subcommand
110 111 subcommands = Dict()
111 112 # parse_command_line will initialize a subapp, if requested
112 113 subapp = Instance('IPython.config.application.Application', allow_none=True)
113 114
114 115
115 116 def __init__(self, **kwargs):
116 117 SingletonConfigurable.__init__(self, **kwargs)
117 118 # Add my class to self.classes so my attributes appear in command line
118 119 # options.
119 120 self.classes.insert(0, self.__class__)
120 121
121 122 # ensure self.flags dict is valid
122 123 for key,value in self.flags.iteritems():
123 124 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
124 125 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
125 126 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
126 127 self.init_logging()
127 128
128 129 def _config_changed(self, name, old, new):
129 130 SingletonConfigurable._config_changed(self, name, old, new)
130 131 self.log.debug('Config changed:')
131 132 self.log.debug(repr(new))
132 133
133 134 def init_logging(self):
134 135 """Start logging for this application.
135 136
136 137 The default is to log to stdout using a StreaHandler. The log level
137 138 starts at loggin.WARN, but this can be adjusted by setting the
138 139 ``log_level`` attribute.
139 140 """
140 141 self.log = logging.getLogger(self.__class__.__name__)
141 142 self.log.setLevel(self.log_level)
142 143 self._log_handler = logging.StreamHandler()
143 144 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
144 145 self._log_handler.setFormatter(self._log_formatter)
145 146 self.log.addHandler(self._log_handler)
146 147
147 148 def initialize(self, argv=None):
148 149 """Do the basic steps to configure me.
149 150
150 151 Override in subclasses.
151 152 """
152 153 self.parse_command_line(argv)
153 154
154 155
155 156 def start(self):
156 157 """Start the app mainloop.
157 158
158 159 Override in subclasses.
159 160 """
160 161 if self.subapp is not None:
161 162 return self.subapp.start()
162 163
163 164 def _log_level_changed(self, name, old, new):
164 165 """Adjust the log level when log_level is set."""
165 166 self.log.setLevel(new)
166 167
167 168 def print_alias_help(self):
168 169 """print the alias part of the help"""
169 170 if not self.aliases:
170 171 return
171 172
172 173 lines = ['Aliases']
173 174 lines.append('-'*len(lines[0]))
174 175 lines.append(self.alias_description)
175 176 lines.append('')
176 177
177 178 classdict = {}
178 179 for cls in self.classes:
179 # include all parents in available names
180 for c in cls.mro():
180 # include all parents (up to, but excluding Configurable) in available names
181 for c in cls.mro()[:-3]:
181 182 classdict[c.__name__] = c
182 183
183 184 for alias, longname in self.aliases.iteritems():
184 185 classname, traitname = longname.split('.',1)
185 186 cls = classdict[classname]
186 187
187 188 trait = cls.class_traits(config=True)[traitname]
188 189 help = cls.class_get_trait_help(trait)
189 190 help = help.replace(longname, "%s (%s)"%(alias, longname), 1)
190 191 lines.append(help)
191 192 lines.append('')
192 193 print '\n'.join(lines)
193 194
194 195 def print_flag_help(self):
195 196 """print the flag part of the help"""
196 197 if not self.flags:
197 198 return
198 199
199 200 lines = ['Flags']
200 201 lines.append('-'*len(lines[0]))
201 202 lines.append(self.flag_description)
202 203 lines.append('')
203 204
204 205 for m, (cfg,help) in self.flags.iteritems():
205 206 lines.append('--'+m)
206 207 lines.append(indent(help, flatten=True))
207 208 lines.append('')
208 209 print '\n'.join(lines)
209 210
210 211 def print_subcommands(self):
211 212 """print the subcommand part of the help"""
212 213 if not self.subcommands:
213 214 return
214 215
215 216 lines = ["Subcommands"]
216 217 lines.append('-'*len(lines[0]))
217 218 for subc, (cls,help) in self.subcommands.iteritems():
218 219 lines.append("%s : %s"%(subc, cls))
219 220 if help:
220 221 lines.append(indent(help, flatten=True))
221 222 lines.append('')
222 223 print '\n'.join(lines)
223 224
224 225 def print_help(self, classes=False):
225 226 """Print the help for each Configurable class in self.classes.
226 227
227 228 If classes=False (the default), only flags and aliases are printed
228 229 """
230 self.print_subcommands()
229 231 self.print_flag_help()
230 232 self.print_alias_help()
231 233
232 234 if classes:
233 235 if self.classes:
234 236 print "Class parameters"
235 237 print "----------------"
236 238 print self.keyvalue_description
237 239 print
238 240
239 241 for cls in self.classes:
240 242 cls.class_print_help()
241 243 print
242 244 else:
243 245 print "To see all available configurables, use `--help-all`"
244 246 print
245 247
246 248 def print_description(self):
247 249 """Print the application description."""
248 250 print self.description
249 251 print
250 252
251 253 def print_version(self):
252 254 """Print the version string."""
253 255 print self.version
254 256
255 257 def update_config(self, config):
256 258 """Fire the traits events when the config is updated."""
257 259 # Save a copy of the current config.
258 260 newconfig = deepcopy(self.config)
259 261 # Merge the new config into the current one.
260 262 newconfig._merge(config)
261 263 # Save the combined config as self.config, which triggers the traits
262 264 # events.
263 265 self.config = newconfig
264 266
265 267 def initialize_subcommand(self, subc, argv=None):
266 268 """Initialize a subcommand with argv"""
267 if '-h' in subc:
268 # requested help
269 self.print_description()
270 self.print_subcommands()
271 self.exit(0)
272 269 subapp,help = self.subcommands.get(subc, (None,None))
273 270 if subapp is None:
274 271 self.print_description()
275 272 print "No such subcommand: %r"%subc
276 273 print
277 274 self.print_subcommands()
278 275 self.exit(1)
279 276
280 277 if isinstance(subapp, basestring):
281 278 subapp = import_item(subapp)
282 279
283 280 # instantiate
284 281 self.subapp = subapp()
285 282 # and initialize subapp
286 283 self.subapp.initialize(argv)
287 284
288 285 def parse_command_line(self, argv=None):
289 286 """Parse the command line arguments."""
290 287 argv = sys.argv[1:] if argv is None else argv
291 288
292 if self.subcommands:
293 # we have subcommands
294 if len(argv) == 0:
295 # none specified
296 self.print_description()
297 self.print_subcommands()
298 self.exit(1)
299
300 return self.initialize_subcommand(argv[0], argv[1:])
289 if self.subcommands and len(argv) > 0:
290 # we have subcommands, and one may have been specified
291 subc, subargv = argv[0], argv[1:]
292 if re.match(r'^\w(\-?\w)*$', subc):
293 # it's a subcommand, and *not* a flag or class parameter
294 return self.initialize_subcommand(subc, subargv)
301 295
302 296 if '-h' in argv or '--help' in argv or '--help-all' in argv:
303 297 self.print_description()
304 298 self.print_help('--help-all' in argv)
305 299 self.exit(0)
306 300
307 301 if '--version' in argv:
308 302 self.print_version()
309 303 self.exit(0)
310 304
311 305 loader = KeyValueConfigLoader(argv=argv, aliases=self.aliases,
312 306 flags=self.flags)
313 307 try:
314 308 config = loader.load_config()
315 309 except ArgumentError as e:
316 310 self.log.fatal(str(e))
317 311 self.print_description()
318 312 self.print_help()
319 313 self.exit(1)
320 314 self.update_config(config)
321 315
322 316 def load_config_file(self, filename, path=None):
323 317 """Load a .py based config file by filename and path."""
324 318 loader = PyFileConfigLoader(filename, path=path)
325 319 config = loader.load_config()
326 320 self.update_config(config)
327 321
328 322 def exit(self, exit_status=0):
329 323 self.log.debug("Exiting application: %s" % self.name)
330 324 sys.exit(exit_status)
331 325
332 326 #-----------------------------------------------------------------------------
333 327 # utility functions, for convenience
334 328 #-----------------------------------------------------------------------------
335 329
336 330 def boolean_flag(name, configurable, set_help='', unset_help=''):
337 331 """helper for building basic --trait, --no-trait flags
338 332
339 333 Parameters
340 334 ----------
341 335
342 336 name : str
343 337 The name of the flag.
344 338 configurable : str
345 339 The 'Class.trait' string of the trait to be set/unset with the flag
346 340 set_help : unicode
347 341 help string for --name flag
348 342 unset_help : unicode
349 343 help string for --no-name flag
350 344
351 345 Returns
352 346 -------
353 347
354 348 cfg : dict
355 349 A dict with two keys: 'name', and 'no-name', for setting and unsetting
356 350 the trait, respectively.
357 351 """
358 352 # default helpstrings
359 353 set_help = set_help or "set %s=True"%configurable
360 354 unset_help = unset_help or "set %s=False"%configurable
361 355
362 356 cls,trait = configurable.split('.')
363 357
364 358 setter = Config()
365 359 setter[cls][trait] = True
366 360 unsetter = Config()
367 361 unsetter[cls][trait] = False
368 362 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
General Comments 0
You need to be logged in to leave comments. Login now