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