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