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