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