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