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