##// END OF EJS Templates
restore '-V' cmdline option for printing version...
Yaroslav Halchenko -
Show More
@@ -1,485 +1,485 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 * Min RK
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2011 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 import logging
23 23 import os
24 24 import re
25 25 import sys
26 26 from copy import deepcopy
27 27 from collections import defaultdict
28 28
29 29 from IPython.config.configurable import SingletonConfigurable
30 30 from IPython.config.loader import (
31 31 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound,
32 32 )
33 33
34 34 from IPython.utils.traitlets import (
35 35 Unicode, List, Int, Enum, Dict, Instance, TraitError
36 36 )
37 37 from IPython.utils.importstring import import_item
38 38 from IPython.utils.text import indent, wrap_paragraphs, dedent
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # function for re-wrapping a helpstring
42 42 #-----------------------------------------------------------------------------
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # Descriptions for the various sections
46 46 #-----------------------------------------------------------------------------
47 47
48 48 # merge flags&aliases into options
49 49 option_description = """
50 50 Arguments that take values are actually convenience aliases to full
51 51 Configurables, whose aliases are listed on the help line. For more information
52 52 on full configurables, see '--help-all'.
53 53 """.strip() # trim newlines of front and back
54 54
55 55 keyvalue_description = """
56 56 Parameters are set from command-line arguments of the form:
57 57 `--Class.trait=value`.
58 58 This line is evaluated in Python, so simple expressions are allowed, e.g.::
59 59 `--C.a='range(3)'` For setting C.a=[0,1,2].
60 60 """.strip() # trim newlines of front and back
61 61
62 62 subcommand_description = """
63 63 Subcommands are launched as `{app} cmd [args]`. For information on using
64 64 subcommand 'cmd', do: `{app} cmd -h`.
65 65 """.strip().format(app=os.path.basename(sys.argv[0]))
66 66 # get running program name
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Application class
70 70 #-----------------------------------------------------------------------------
71 71
72 72
73 73 class ApplicationError(Exception):
74 74 pass
75 75
76 76
77 77 class Application(SingletonConfigurable):
78 78 """A singleton application with full configuration support."""
79 79
80 80 # The name of the application, will usually match the name of the command
81 81 # line application
82 82 name = Unicode(u'application')
83 83
84 84 # The description of the application that is printed at the beginning
85 85 # of the help.
86 86 description = Unicode(u'This is an application.')
87 87 # default section descriptions
88 88 option_description = Unicode(option_description)
89 89 keyvalue_description = Unicode(keyvalue_description)
90 90 subcommand_description = Unicode(subcommand_description)
91 91
92 92 # The usage and example string that goes at the end of the help string.
93 93 examples = Unicode()
94 94
95 95 # A sequence of Configurable subclasses whose config=True attributes will
96 96 # be exposed at the command line.
97 97 classes = List([])
98 98
99 99 # The version string of this application.
100 100 version = Unicode(u'0.0')
101 101
102 102 # The log level for the application
103 103 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
104 104 default_value=logging.WARN,
105 105 config=True,
106 106 help="Set the log level by value or name.")
107 107 def _log_level_changed(self, name, old, new):
108 108 """Adjust the log level when log_level is set."""
109 109 if isinstance(new, basestring):
110 110 new = getattr(logging, new)
111 111 self.log_level = new
112 112 self.log.setLevel(new)
113 113
114 114 # the alias map for configurables
115 115 aliases = Dict({'log-level' : 'Application.log_level'})
116 116
117 117 # flags for loading Configurables or store_const style flags
118 118 # flags are loaded from this dict by '--key' flags
119 119 # this must be a dict of two-tuples, the first element being the Config/dict
120 120 # and the second being the help string for the flag
121 121 flags = Dict()
122 122 def _flags_changed(self, name, old, new):
123 123 """ensure flags dict is valid"""
124 124 for key,value in new.iteritems():
125 125 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
126 126 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
127 127 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
128 128
129 129
130 130 # subcommands for launching other applications
131 131 # if this is not empty, this will be a parent Application
132 132 # this must be a dict of two-tuples,
133 133 # the first element being the application class/import string
134 134 # and the second being the help string for the subcommand
135 135 subcommands = Dict()
136 136 # parse_command_line will initialize a subapp, if requested
137 137 subapp = Instance('IPython.config.application.Application', allow_none=True)
138 138
139 139 # extra command-line arguments that don't set config values
140 140 extra_args = List(Unicode)
141 141
142 142
143 143 def __init__(self, **kwargs):
144 144 SingletonConfigurable.__init__(self, **kwargs)
145 145 # Ensure my class is in self.classes, so my attributes appear in command line
146 146 # options and config files.
147 147 if self.__class__ not in self.classes:
148 148 self.classes.insert(0, self.__class__)
149 149
150 150 self.init_logging()
151 151
152 152 def _config_changed(self, name, old, new):
153 153 SingletonConfigurable._config_changed(self, name, old, new)
154 154 self.log.debug('Config changed:')
155 155 self.log.debug(repr(new))
156 156
157 157 def init_logging(self):
158 158 """Start logging for this application.
159 159
160 160 The default is to log to stdout using a StreaHandler. The log level
161 161 starts at loggin.WARN, but this can be adjusted by setting the
162 162 ``log_level`` attribute.
163 163 """
164 164 self.log = logging.getLogger(self.__class__.__name__)
165 165 self.log.setLevel(self.log_level)
166 166 if sys.executable.endswith('pythonw.exe'):
167 167 # this should really go to a file, but file-logging is only
168 168 # hooked up in parallel applications
169 169 self._log_handler = logging.StreamHandler(open(os.devnull, 'w'))
170 170 else:
171 171 self._log_handler = logging.StreamHandler()
172 172 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
173 173 self._log_handler.setFormatter(self._log_formatter)
174 174 self.log.addHandler(self._log_handler)
175 175
176 176 def initialize(self, argv=None):
177 177 """Do the basic steps to configure me.
178 178
179 179 Override in subclasses.
180 180 """
181 181 self.parse_command_line(argv)
182 182
183 183
184 184 def start(self):
185 185 """Start the app mainloop.
186 186
187 187 Override in subclasses.
188 188 """
189 189 if self.subapp is not None:
190 190 return self.subapp.start()
191 191
192 192 def print_alias_help(self):
193 193 """Print the alias part of the help."""
194 194 if not self.aliases:
195 195 return
196 196
197 197 lines = []
198 198 classdict = {}
199 199 for cls in self.classes:
200 200 # include all parents (up to, but excluding Configurable) in available names
201 201 for c in cls.mro()[:-3]:
202 202 classdict[c.__name__] = c
203 203
204 204 for alias, longname in self.aliases.iteritems():
205 205 classname, traitname = longname.split('.',1)
206 206 cls = classdict[classname]
207 207
208 208 trait = cls.class_traits(config=True)[traitname]
209 209 help = cls.class_get_trait_help(trait).splitlines()
210 210 # reformat first line
211 211 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
212 212 if len(alias) == 1:
213 213 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
214 214 lines.extend(help)
215 215 # lines.append('')
216 216 print os.linesep.join(lines)
217 217
218 218 def print_flag_help(self):
219 219 """Print the flag part of the help."""
220 220 if not self.flags:
221 221 return
222 222
223 223 lines = []
224 224 for m, (cfg,help) in self.flags.iteritems():
225 225 prefix = '--' if len(m) > 1 else '-'
226 226 lines.append(prefix+m)
227 227 lines.append(indent(dedent(help.strip())))
228 228 # lines.append('')
229 229 print os.linesep.join(lines)
230 230
231 231 def print_options(self):
232 232 if not self.flags and not self.aliases:
233 233 return
234 234 lines = ['Options']
235 235 lines.append('-'*len(lines[0]))
236 236 lines.append('')
237 237 for p in wrap_paragraphs(self.option_description):
238 238 lines.append(p)
239 239 lines.append('')
240 240 print os.linesep.join(lines)
241 241 self.print_flag_help()
242 242 self.print_alias_help()
243 243 print
244 244
245 245 def print_subcommands(self):
246 246 """Print the subcommand part of the help."""
247 247 if not self.subcommands:
248 248 return
249 249
250 250 lines = ["Subcommands"]
251 251 lines.append('-'*len(lines[0]))
252 252 lines.append('')
253 253 for p in wrap_paragraphs(self.subcommand_description):
254 254 lines.append(p)
255 255 lines.append('')
256 256 for subc, (cls, help) in self.subcommands.iteritems():
257 257 lines.append(subc)
258 258 if help:
259 259 lines.append(indent(dedent(help.strip())))
260 260 lines.append('')
261 261 print os.linesep.join(lines)
262 262
263 263 def print_help(self, classes=False):
264 264 """Print the help for each Configurable class in self.classes.
265 265
266 266 If classes=False (the default), only flags and aliases are printed.
267 267 """
268 268 self.print_subcommands()
269 269 self.print_options()
270 270
271 271 if classes:
272 272 if self.classes:
273 273 print "Class parameters"
274 274 print "----------------"
275 275 print
276 276 for p in wrap_paragraphs(self.keyvalue_description):
277 277 print p
278 278 print
279 279
280 280 for cls in self.classes:
281 281 cls.class_print_help()
282 282 print
283 283 else:
284 284 print "To see all available configurables, use `--help-all`"
285 285 print
286 286
287 287 def print_description(self):
288 288 """Print the application description."""
289 289 for p in wrap_paragraphs(self.description):
290 290 print p
291 291 print
292 292
293 293 def print_examples(self):
294 294 """Print usage and examples.
295 295
296 296 This usage string goes at the end of the command line help string
297 297 and should contain examples of the application's usage.
298 298 """
299 299 if self.examples:
300 300 print "Examples"
301 301 print "--------"
302 302 print
303 303 print indent(dedent(self.examples.strip()))
304 304 print
305 305
306 306 def print_version(self):
307 307 """Print the version string."""
308 308 print self.version
309 309
310 310 def update_config(self, config):
311 311 """Fire the traits events when the config is updated."""
312 312 # Save a copy of the current config.
313 313 newconfig = deepcopy(self.config)
314 314 # Merge the new config into the current one.
315 315 newconfig._merge(config)
316 316 # Save the combined config as self.config, which triggers the traits
317 317 # events.
318 318 self.config = newconfig
319 319
320 320 def initialize_subcommand(self, subc, argv=None):
321 321 """Initialize a subcommand with argv."""
322 322 subapp,help = self.subcommands.get(subc)
323 323
324 324 if isinstance(subapp, basestring):
325 325 subapp = import_item(subapp)
326 326
327 327 # clear existing instances
328 328 self.__class__.clear_instance()
329 329 # instantiate
330 330 self.subapp = subapp.instance()
331 331 # and initialize subapp
332 332 self.subapp.initialize(argv)
333 333
334 334 def flatten_flags(self):
335 335 """flatten flags and aliases, so cl-args override as expected.
336 336
337 337 This prevents issues such as an alias pointing to InteractiveShell,
338 338 but a config file setting the same trait in TerminalInteraciveShell
339 339 getting inappropriate priority over the command-line arg.
340 340
341 341 Only aliases with exactly one descendent in the class list
342 342 will be promoted.
343 343
344 344 """
345 345 # build a tree of classes in our list that inherit from a particular
346 346 # it will be a dict by parent classname of classes in our list
347 347 # that are descendents
348 348 mro_tree = defaultdict(list)
349 349 for cls in self.classes:
350 350 clsname = cls.__name__
351 351 for parent in cls.mro()[1:-3]:
352 352 # exclude cls itself and Configurable,HasTraits,object
353 353 mro_tree[parent.__name__].append(clsname)
354 354 # flatten aliases, which have the form:
355 355 # { 'alias' : 'Class.trait' }
356 356 aliases = {}
357 357 for alias, cls_trait in self.aliases.iteritems():
358 358 cls,trait = cls_trait.split('.',1)
359 359 children = mro_tree[cls]
360 360 if len(children) == 1:
361 361 # exactly one descendent, promote alias
362 362 cls = children[0]
363 363 aliases[alias] = '.'.join([cls,trait])
364 364
365 365 # flatten flags, which are of the form:
366 366 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
367 367 flags = {}
368 368 for key, (flagdict, help) in self.flags.iteritems():
369 369 newflag = {}
370 370 for cls, subdict in flagdict.iteritems():
371 371 children = mro_tree[cls]
372 372 # exactly one descendent, promote flag section
373 373 if len(children) == 1:
374 374 cls = children[0]
375 375 newflag[cls] = subdict
376 376 flags[key] = (newflag, help)
377 377 return flags, aliases
378 378
379 379 def parse_command_line(self, argv=None):
380 380 """Parse the command line arguments."""
381 381 argv = sys.argv[1:] if argv is None else argv
382 382
383 383 if self.subcommands and len(argv) > 0:
384 384 # we have subcommands, and one may have been specified
385 385 subc, subargv = argv[0], argv[1:]
386 386 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
387 387 # it's a subcommand, and *not* a flag or class parameter
388 388 return self.initialize_subcommand(subc, subargv)
389 389
390 390 if '-h' in argv or '--help' in argv or '--help-all' in argv:
391 391 self.print_description()
392 392 self.print_help('--help-all' in argv)
393 393 self.print_examples()
394 394 self.exit(0)
395 395
396 if '--version' in argv:
396 if '--version' in argv or '-V' in argv:
397 397 self.print_version()
398 398 self.exit(0)
399 399
400 400 # flatten flags&aliases, so cl-args get appropriate priority:
401 401 flags,aliases = self.flatten_flags()
402 402
403 403 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
404 404 flags=flags)
405 405 try:
406 406 config = loader.load_config()
407 407 self.update_config(config)
408 408 except (TraitError, ArgumentError) as e:
409 409 self.print_description()
410 410 self.print_help()
411 411 self.print_examples()
412 412 self.log.fatal(str(e))
413 413 self.exit(1)
414 414 # store unparsed args in extra_args
415 415 self.extra_args = loader.extra_args
416 416
417 417 def load_config_file(self, filename, path=None):
418 418 """Load a .py based config file by filename and path."""
419 419 loader = PyFileConfigLoader(filename, path=path)
420 420 try:
421 421 config = loader.load_config()
422 422 except ConfigFileNotFound:
423 423 # problem finding the file, raise
424 424 raise
425 425 except Exception:
426 426 # try to get the full filename, but it will be empty in the
427 427 # unlikely event that the error raised before filefind finished
428 428 filename = loader.full_filename or filename
429 429 # problem while running the file
430 430 self.log.error("Exception while loading config file %s",
431 431 filename, exc_info=True)
432 432 else:
433 433 self.log.debug("Loaded config file: %s", loader.full_filename)
434 434 self.update_config(config)
435 435
436 436 def generate_config_file(self):
437 437 """generate default config file from Configurables"""
438 438 lines = ["# Configuration file for %s."%self.name]
439 439 lines.append('')
440 440 lines.append('c = get_config()')
441 441 lines.append('')
442 442 for cls in self.classes:
443 443 lines.append(cls.class_config_section())
444 444 return '\n'.join(lines)
445 445
446 446 def exit(self, exit_status=0):
447 447 self.log.debug("Exiting application: %s" % self.name)
448 448 sys.exit(exit_status)
449 449
450 450 #-----------------------------------------------------------------------------
451 451 # utility functions, for convenience
452 452 #-----------------------------------------------------------------------------
453 453
454 454 def boolean_flag(name, configurable, set_help='', unset_help=''):
455 455 """Helper for building basic --trait, --no-trait flags.
456 456
457 457 Parameters
458 458 ----------
459 459
460 460 name : str
461 461 The name of the flag.
462 462 configurable : str
463 463 The 'Class.trait' string of the trait to be set/unset with the flag
464 464 set_help : unicode
465 465 help string for --name flag
466 466 unset_help : unicode
467 467 help string for --no-name flag
468 468
469 469 Returns
470 470 -------
471 471
472 472 cfg : dict
473 473 A dict with two keys: 'name', and 'no-name', for setting and unsetting
474 474 the trait, respectively.
475 475 """
476 476 # default helpstrings
477 477 set_help = set_help or "set %s=True"%configurable
478 478 unset_help = unset_help or "set %s=False"%configurable
479 479
480 480 cls,trait = configurable.split('.')
481 481
482 482 setter = {cls : {trait : True}}
483 483 unsetter = {cls : {trait : False}}
484 484 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
485 485
General Comments 0
You need to be logged in to leave comments. Login now