##// END OF EJS Templates
don't validate Application.argv type...
MinRK -
Show More
@@ -1,579 +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 145
146 146 # the argv used to initialize the application
147 argv = List(Unicode)
147 argv = List()
148 148
149 149 # The log level for the application
150 150 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
151 151 default_value=logging.WARN,
152 152 config=True,
153 153 help="Set the log level by value or name.")
154 154 def _log_level_changed(self, name, old, new):
155 155 """Adjust the log level when log_level is set."""
156 156 if isinstance(new, basestring):
157 157 new = getattr(logging, new)
158 158 self.log_level = new
159 159 self.log.setLevel(new)
160 160
161 161 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
162 162 help="The date format used by logging formatters for %(asctime)s"
163 163 )
164 164 def _log_datefmt_changed(self, name, old, new):
165 165 self._log_format_changed()
166 166
167 167 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
168 168 help="The Logging format template",
169 169 )
170 170 def _log_format_changed(self, name, old, new):
171 171 """Change the log formatter when log_format is set."""
172 172 _log_handler = self.log.handlers[0]
173 173 _log_formatter = LevelFormatter(new, datefmt=self.log_datefmt)
174 174 _log_handler.setFormatter(_log_formatter)
175 175
176 176 log = Instance(logging.Logger)
177 177 def _log_default(self):
178 178 """Start logging for this application.
179 179
180 180 The default is to log to stderr using a StreamHandler, if no default
181 181 handler already exists. The log level starts at logging.WARN, but this
182 182 can be adjusted by setting the ``log_level`` attribute.
183 183 """
184 184 log = logging.getLogger(self.__class__.__name__)
185 185 log.setLevel(self.log_level)
186 186 log.propagate = False
187 187 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
188 188 while _log:
189 189 if _log.handlers:
190 190 return log
191 191 if not _log.propagate:
192 192 break
193 193 else:
194 194 _log = _log.parent
195 195 if sys.executable.endswith('pythonw.exe'):
196 196 # this should really go to a file, but file-logging is only
197 197 # hooked up in parallel applications
198 198 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
199 199 else:
200 200 _log_handler = logging.StreamHandler()
201 201 _log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt)
202 202 _log_handler.setFormatter(_log_formatter)
203 203 log.addHandler(_log_handler)
204 204 return log
205 205
206 206 # the alias map for configurables
207 207 aliases = Dict({'log-level' : 'Application.log_level'})
208 208
209 209 # flags for loading Configurables or store_const style flags
210 210 # flags are loaded from this dict by '--key' flags
211 211 # this must be a dict of two-tuples, the first element being the Config/dict
212 212 # and the second being the help string for the flag
213 213 flags = Dict()
214 214 def _flags_changed(self, name, old, new):
215 215 """ensure flags dict is valid"""
216 216 for key,value in new.iteritems():
217 217 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
218 218 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
219 219 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
220 220
221 221
222 222 # subcommands for launching other applications
223 223 # if this is not empty, this will be a parent Application
224 224 # this must be a dict of two-tuples,
225 225 # the first element being the application class/import string
226 226 # and the second being the help string for the subcommand
227 227 subcommands = Dict()
228 228 # parse_command_line will initialize a subapp, if requested
229 229 subapp = Instance('IPython.config.application.Application', allow_none=True)
230 230
231 231 # extra command-line arguments that don't set config values
232 232 extra_args = List(Unicode)
233 233
234 234
235 235 def __init__(self, **kwargs):
236 236 SingletonConfigurable.__init__(self, **kwargs)
237 237 # Ensure my class is in self.classes, so my attributes appear in command line
238 238 # options and config files.
239 239 if self.__class__ not in self.classes:
240 240 self.classes.insert(0, self.__class__)
241 241
242 242 def _config_changed(self, name, old, new):
243 243 SingletonConfigurable._config_changed(self, name, old, new)
244 244 self.log.debug('Config changed:')
245 245 self.log.debug(repr(new))
246 246
247 247 @catch_config_error
248 248 def initialize(self, argv=None):
249 249 """Do the basic steps to configure me.
250 250
251 251 Override in subclasses.
252 252 """
253 253 self.parse_command_line(argv)
254 254
255 255
256 256 def start(self):
257 257 """Start the app mainloop.
258 258
259 259 Override in subclasses.
260 260 """
261 261 if self.subapp is not None:
262 262 return self.subapp.start()
263 263
264 264 def print_alias_help(self):
265 265 """Print the alias part of the help."""
266 266 if not self.aliases:
267 267 return
268 268
269 269 lines = []
270 270 classdict = {}
271 271 for cls in self.classes:
272 272 # include all parents (up to, but excluding Configurable) in available names
273 273 for c in cls.mro()[:-3]:
274 274 classdict[c.__name__] = c
275 275
276 276 for alias, longname in self.aliases.iteritems():
277 277 classname, traitname = longname.split('.',1)
278 278 cls = classdict[classname]
279 279
280 280 trait = cls.class_traits(config=True)[traitname]
281 281 help = cls.class_get_trait_help(trait).splitlines()
282 282 # reformat first line
283 283 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
284 284 if len(alias) == 1:
285 285 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
286 286 lines.extend(help)
287 287 # lines.append('')
288 288 print os.linesep.join(lines)
289 289
290 290 def print_flag_help(self):
291 291 """Print the flag part of the help."""
292 292 if not self.flags:
293 293 return
294 294
295 295 lines = []
296 296 for m, (cfg,help) in self.flags.iteritems():
297 297 prefix = '--' if len(m) > 1 else '-'
298 298 lines.append(prefix+m)
299 299 lines.append(indent(dedent(help.strip())))
300 300 # lines.append('')
301 301 print os.linesep.join(lines)
302 302
303 303 def print_options(self):
304 304 if not self.flags and not self.aliases:
305 305 return
306 306 lines = ['Options']
307 307 lines.append('-'*len(lines[0]))
308 308 lines.append('')
309 309 for p in wrap_paragraphs(self.option_description):
310 310 lines.append(p)
311 311 lines.append('')
312 312 print os.linesep.join(lines)
313 313 self.print_flag_help()
314 314 self.print_alias_help()
315 315 print
316 316
317 317 def print_subcommands(self):
318 318 """Print the subcommand part of the help."""
319 319 if not self.subcommands:
320 320 return
321 321
322 322 lines = ["Subcommands"]
323 323 lines.append('-'*len(lines[0]))
324 324 lines.append('')
325 325 for p in wrap_paragraphs(self.subcommand_description):
326 326 lines.append(p)
327 327 lines.append('')
328 328 for subc, (cls, help) in self.subcommands.iteritems():
329 329 lines.append(subc)
330 330 if help:
331 331 lines.append(indent(dedent(help.strip())))
332 332 lines.append('')
333 333 print os.linesep.join(lines)
334 334
335 335 def print_help(self, classes=False):
336 336 """Print the help for each Configurable class in self.classes.
337 337
338 338 If classes=False (the default), only flags and aliases are printed.
339 339 """
340 340 self.print_description()
341 341 self.print_subcommands()
342 342 self.print_options()
343 343
344 344 if classes:
345 345 if self.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 self.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, basestring):
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.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 self.aliases.iteritems():
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 self.flags.iteritems():
446 446 newflag = {}
447 447 for cls, subdict in flagdict.iteritems():
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 = list(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
493 493 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
494 494 flags=flags)
495 495 config = loader.load_config()
496 496 self.update_config(config)
497 497 # store unparsed args in extra_args
498 498 self.extra_args = loader.extra_args
499 499
500 500 @catch_config_error
501 501 def load_config_file(self, filename, path=None):
502 502 """Load a .py based config file by filename and path."""
503 503 loader = PyFileConfigLoader(filename, path=path)
504 504 try:
505 505 config = loader.load_config()
506 506 except ConfigFileNotFound:
507 507 # problem finding the file, raise
508 508 raise
509 509 except Exception:
510 510 # try to get the full filename, but it will be empty in the
511 511 # unlikely event that the error raised before filefind finished
512 512 filename = loader.full_filename or filename
513 513 # problem while running the file
514 514 self.log.error("Exception while loading config file %s",
515 515 filename, exc_info=True)
516 516 else:
517 517 self.log.debug("Loaded config file: %s", loader.full_filename)
518 518 self.update_config(config)
519 519
520 520 def generate_config_file(self):
521 521 """generate default config file from Configurables"""
522 522 lines = ["# Configuration file for %s."%self.name]
523 523 lines.append('')
524 524 lines.append('c = get_config()')
525 525 lines.append('')
526 526 for cls in self.classes:
527 527 lines.append(cls.class_config_section())
528 528 return '\n'.join(lines)
529 529
530 530 def exit(self, exit_status=0):
531 531 self.log.debug("Exiting application: %s" % self.name)
532 532 sys.exit(exit_status)
533 533
534 534 @classmethod
535 535 def launch_instance(cls, argv=None, **kwargs):
536 536 """Launch a global instance of this Application
537 537
538 538 If a global instance already exists, this reinitializes and starts it
539 539 """
540 540 app = cls.instance(**kwargs)
541 541 app.initialize(argv)
542 542 app.start()
543 543
544 544 #-----------------------------------------------------------------------------
545 545 # utility functions, for convenience
546 546 #-----------------------------------------------------------------------------
547 547
548 548 def boolean_flag(name, configurable, set_help='', unset_help=''):
549 549 """Helper for building basic --trait, --no-trait flags.
550 550
551 551 Parameters
552 552 ----------
553 553
554 554 name : str
555 555 The name of the flag.
556 556 configurable : str
557 557 The 'Class.trait' string of the trait to be set/unset with the flag
558 558 set_help : unicode
559 559 help string for --name flag
560 560 unset_help : unicode
561 561 help string for --no-name flag
562 562
563 563 Returns
564 564 -------
565 565
566 566 cfg : dict
567 567 A dict with two keys: 'name', and 'no-name', for setting and unsetting
568 568 the trait, respectively.
569 569 """
570 570 # default helpstrings
571 571 set_help = set_help or "set %s=True"%configurable
572 572 unset_help = unset_help or "set %s=False"%configurable
573 573
574 574 cls,trait = configurable.split('.')
575 575
576 576 setter = {cls : {trait : True}}
577 577 unsetter = {cls : {trait : False}}
578 578 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
579 579
General Comments 0
You need to be logged in to leave comments. Login now