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