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