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