##// END OF EJS Templates
don't crash on bad config files
MinRK -
Show More
@@ -1,421 +1,426 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
28 28 from IPython.config.configurable import SingletonConfigurable
29 29 from IPython.config.loader import (
30 30 KeyValueConfigLoader, PyFileConfigLoader, Config, ArgumentError
31 31 )
32 32
33 33 from IPython.utils.traitlets import (
34 34 Unicode, List, Int, Enum, Dict, Instance, TraitError
35 35 )
36 36 from IPython.utils.importstring import import_item
37 37 from IPython.utils.text import indent, wrap_paragraphs, dedent
38 38
39 39 #-----------------------------------------------------------------------------
40 40 # function for re-wrapping a helpstring
41 41 #-----------------------------------------------------------------------------
42 42
43 43 #-----------------------------------------------------------------------------
44 44 # Descriptions for the various sections
45 45 #-----------------------------------------------------------------------------
46 46
47 47 # merge flags&aliases into options
48 48 option_description = """
49 49 IPython command-line arguments are passed as '--<flag>', or '--<name>=<value>'.
50 50
51 51 Arguments that take values are actually convenience aliases to full
52 52 Configurables, whose aliases are listed on the help line. For more information
53 53 on full configurables, see '--help-all'.
54 54 """.strip() # trim newlines of front and back
55 55
56 56 keyvalue_description = """
57 57 Parameters are set from command-line arguments of the form:
58 58 `--Class.trait=value`.
59 59 This line is evaluated in Python, so simple expressions are allowed, e.g.::
60 60 `--C.a='range(3)'` For setting C.a=[0,1,2].
61 61 """.strip() # trim newlines of front and back
62 62
63 63 subcommand_description = """
64 64 Subcommands are launched as `{app} cmd [args]`. For information on using
65 65 subcommand 'cmd', do: `{app} cmd -h`.
66 66 """.strip().format(app=os.path.basename(sys.argv[0]))
67 67 # get running program name
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Application class
71 71 #-----------------------------------------------------------------------------
72 72
73 73
74 74 class ApplicationError(Exception):
75 75 pass
76 76
77 77
78 78 class Application(SingletonConfigurable):
79 79 """A singleton application with full configuration support."""
80 80
81 81 # The name of the application, will usually match the name of the command
82 82 # line application
83 83 name = Unicode(u'application')
84 84
85 85 # The description of the application that is printed at the beginning
86 86 # of the help.
87 87 description = Unicode(u'This is an application.')
88 88 # default section descriptions
89 89 option_description = Unicode(option_description)
90 90 keyvalue_description = Unicode(keyvalue_description)
91 91 subcommand_description = Unicode(subcommand_description)
92 92
93 93 # The usage and example string that goes at the end of the help string.
94 94 examples = Unicode()
95 95
96 96 # A sequence of Configurable subclasses whose config=True attributes will
97 97 # be exposed at the command line.
98 98 classes = List([])
99 99
100 100 # The version string of this application.
101 101 version = Unicode(u'0.0')
102 102
103 103 # The log level for the application
104 104 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
105 105 default_value=logging.WARN,
106 106 config=True,
107 107 help="Set the log level by value or name.")
108 108 def _log_level_changed(self, name, old, new):
109 109 """Adjust the log level when log_level is set."""
110 110 if isinstance(new, basestring):
111 111 new = getattr(logging, new)
112 112 self.log_level = new
113 113 self.log.setLevel(new)
114 114
115 115 # the alias map for configurables
116 116 aliases = Dict({'log-level' : 'Application.log_level'})
117 117
118 118 # flags for loading Configurables or store_const style flags
119 119 # flags are loaded from this dict by '--key' flags
120 120 # this must be a dict of two-tuples, the first element being the Config/dict
121 121 # and the second being the help string for the flag
122 122 flags = Dict()
123 123 def _flags_changed(self, name, old, new):
124 124 """ensure flags dict is valid"""
125 125 for key,value in new.iteritems():
126 126 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
127 127 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
128 128 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
129 129
130 130
131 131 # subcommands for launching other applications
132 132 # if this is not empty, this will be a parent Application
133 133 # this must be a dict of two-tuples,
134 134 # the first element being the application class/import string
135 135 # and the second being the help string for the subcommand
136 136 subcommands = Dict()
137 137 # parse_command_line will initialize a subapp, if requested
138 138 subapp = Instance('IPython.config.application.Application', allow_none=True)
139 139
140 140 # extra command-line arguments that don't set config values
141 141 extra_args = List(Unicode)
142 142
143 143
144 144 def __init__(self, **kwargs):
145 145 SingletonConfigurable.__init__(self, **kwargs)
146 146 # Add my class to self.classes so my attributes appear in command line
147 147 # options.
148 148 self.classes.insert(0, self.__class__)
149 149
150 150 self.init_logging()
151 151
152 152 def _config_changed(self, name, old, new):
153 153 SingletonConfigurable._config_changed(self, name, old, new)
154 154 self.log.debug('Config changed:')
155 155 self.log.debug(repr(new))
156 156
157 157 def init_logging(self):
158 158 """Start logging for this application.
159 159
160 160 The default is to log to stdout using a StreaHandler. The log level
161 161 starts at loggin.WARN, but this can be adjusted by setting the
162 162 ``log_level`` attribute.
163 163 """
164 164 self.log = logging.getLogger(self.__class__.__name__)
165 165 self.log.setLevel(self.log_level)
166 166 if sys.executable.endswith('pythonw.exe'):
167 167 # this should really go to a file, but file-logging is only
168 168 # hooked up in parallel applications
169 169 self._log_handler = logging.StreamHandler(open(os.devnull, 'w'))
170 170 else:
171 171 self._log_handler = logging.StreamHandler()
172 172 self._log_formatter = logging.Formatter("[%(name)s] %(message)s")
173 173 self._log_handler.setFormatter(self._log_formatter)
174 174 self.log.addHandler(self._log_handler)
175 175
176 176 def initialize(self, argv=None):
177 177 """Do the basic steps to configure me.
178 178
179 179 Override in subclasses.
180 180 """
181 181 self.parse_command_line(argv)
182 182
183 183
184 184 def start(self):
185 185 """Start the app mainloop.
186 186
187 187 Override in subclasses.
188 188 """
189 189 if self.subapp is not None:
190 190 return self.subapp.start()
191 191
192 192 def print_alias_help(self):
193 193 """Print the alias part of the help."""
194 194 if not self.aliases:
195 195 return
196 196
197 197 lines = []
198 198 classdict = {}
199 199 for cls in self.classes:
200 200 # include all parents (up to, but excluding Configurable) in available names
201 201 for c in cls.mro()[:-3]:
202 202 classdict[c.__name__] = c
203 203
204 204 for alias, longname in self.aliases.iteritems():
205 205 classname, traitname = longname.split('.',1)
206 206 cls = classdict[classname]
207 207
208 208 trait = cls.class_traits(config=True)[traitname]
209 209 help = cls.class_get_trait_help(trait).splitlines()
210 210 # reformat first line
211 211 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
212 212 lines.extend(help)
213 213 # lines.append('')
214 214 print os.linesep.join(lines)
215 215
216 216 def print_flag_help(self):
217 217 """Print the flag part of the help."""
218 218 if not self.flags:
219 219 return
220 220
221 221 lines = []
222 222 for m, (cfg,help) in self.flags.iteritems():
223 223 lines.append('--'+m)
224 224 lines.append(indent(dedent(help.strip())))
225 225 # lines.append('')
226 226 print os.linesep.join(lines)
227 227
228 228 def print_options(self):
229 229 if not self.flags and not self.aliases:
230 230 return
231 231 lines = ['Options']
232 232 lines.append('-'*len(lines[0]))
233 233 lines.append('')
234 234 for p in wrap_paragraphs(self.option_description):
235 235 lines.append(p)
236 236 lines.append('')
237 237 print os.linesep.join(lines)
238 238 self.print_flag_help()
239 239 self.print_alias_help()
240 240 print
241 241
242 242 def print_subcommands(self):
243 243 """Print the subcommand part of the help."""
244 244 if not self.subcommands:
245 245 return
246 246
247 247 lines = ["Subcommands"]
248 248 lines.append('-'*len(lines[0]))
249 249 lines.append('')
250 250 for p in wrap_paragraphs(self.subcommand_description):
251 251 lines.append(p)
252 252 lines.append('')
253 253 for subc, (cls, help) in self.subcommands.iteritems():
254 254 lines.append(subc)
255 255 if help:
256 256 lines.append(indent(dedent(help.strip())))
257 257 lines.append('')
258 258 print os.linesep.join(lines)
259 259
260 260 def print_help(self, classes=False):
261 261 """Print the help for each Configurable class in self.classes.
262 262
263 263 If classes=False (the default), only flags and aliases are printed.
264 264 """
265 265 self.print_subcommands()
266 266 self.print_options()
267 267
268 268 if classes:
269 269 if self.classes:
270 270 print "Class parameters"
271 271 print "----------------"
272 272 print
273 273 for p in wrap_paragraphs(self.keyvalue_description):
274 274 print p
275 275 print
276 276
277 277 for cls in self.classes:
278 278 cls.class_print_help()
279 279 print
280 280 else:
281 281 print "To see all available configurables, use `--help-all`"
282 282 print
283 283
284 284 def print_description(self):
285 285 """Print the application description."""
286 286 for p in wrap_paragraphs(self.description):
287 287 print p
288 288 print
289 289
290 290 def print_examples(self):
291 291 """Print usage and examples.
292 292
293 293 This usage string goes at the end of the command line help string
294 294 and should contain examples of the application's usage.
295 295 """
296 296 if self.examples:
297 297 print "Examples"
298 298 print "--------"
299 299 print
300 300 print indent(dedent(self.examples.strip()))
301 301 print
302 302
303 303 def print_version(self):
304 304 """Print the version string."""
305 305 print self.version
306 306
307 307 def update_config(self, config):
308 308 """Fire the traits events when the config is updated."""
309 309 # Save a copy of the current config.
310 310 newconfig = deepcopy(self.config)
311 311 # Merge the new config into the current one.
312 312 newconfig._merge(config)
313 313 # Save the combined config as self.config, which triggers the traits
314 314 # events.
315 315 self.config = newconfig
316 316
317 317 def initialize_subcommand(self, subc, argv=None):
318 318 """Initialize a subcommand with argv."""
319 319 subapp,help = self.subcommands.get(subc)
320 320
321 321 if isinstance(subapp, basestring):
322 322 subapp = import_item(subapp)
323 323
324 324 # clear existing instances
325 325 self.__class__.clear_instance()
326 326 # instantiate
327 327 self.subapp = subapp.instance()
328 328 # and initialize subapp
329 329 self.subapp.initialize(argv)
330 330
331 331 def parse_command_line(self, argv=None):
332 332 """Parse the command line arguments."""
333 333 argv = sys.argv[1:] if argv is None else argv
334 334
335 335 if self.subcommands and len(argv) > 0:
336 336 # we have subcommands, and one may have been specified
337 337 subc, subargv = argv[0], argv[1:]
338 338 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
339 339 # it's a subcommand, and *not* a flag or class parameter
340 340 return self.initialize_subcommand(subc, subargv)
341 341
342 342 if '-h' in argv or '--help' in argv or '--help-all' in argv:
343 343 self.print_description()
344 344 self.print_help('--help-all' in argv)
345 345 self.print_examples()
346 346 self.exit(0)
347 347
348 348 if '--version' in argv:
349 349 self.print_version()
350 350 self.exit(0)
351 351
352 352 loader = KeyValueConfigLoader(argv=argv, aliases=self.aliases,
353 353 flags=self.flags)
354 354 try:
355 355 config = loader.load_config()
356 356 self.update_config(config)
357 357 except (TraitError, ArgumentError) as e:
358 358 self.print_description()
359 359 self.print_help()
360 360 self.print_examples()
361 361 self.log.fatal(str(e))
362 362 self.exit(1)
363 363 # store unparsed args in extra_args
364 364 self.extra_args = loader.extra_args
365 365
366 366 def load_config_file(self, filename, path=None):
367 367 """Load a .py based config file by filename and path."""
368 368 loader = PyFileConfigLoader(filename, path=path)
369 config = loader.load_config()
370 self.update_config(config)
369 try:
370 config = loader.load_config()
371 except Exception:
372 self.log.error("Exception while loading config file %s [path=%s]"%
373 (filename, path), exc_info=True)
374 else:
375 self.update_config(config)
371 376
372 377 def generate_config_file(self):
373 378 """generate default config file from Configurables"""
374 379 lines = ["# Configuration file for %s."%self.name]
375 380 lines.append('')
376 381 lines.append('c = get_config()')
377 382 lines.append('')
378 383 for cls in self.classes:
379 384 lines.append(cls.class_config_section())
380 385 return '\n'.join(lines)
381 386
382 387 def exit(self, exit_status=0):
383 388 self.log.debug("Exiting application: %s" % self.name)
384 389 sys.exit(exit_status)
385 390
386 391 #-----------------------------------------------------------------------------
387 392 # utility functions, for convenience
388 393 #-----------------------------------------------------------------------------
389 394
390 395 def boolean_flag(name, configurable, set_help='', unset_help=''):
391 396 """Helper for building basic --trait, --no-trait flags.
392 397
393 398 Parameters
394 399 ----------
395 400
396 401 name : str
397 402 The name of the flag.
398 403 configurable : str
399 404 The 'Class.trait' string of the trait to be set/unset with the flag
400 405 set_help : unicode
401 406 help string for --name flag
402 407 unset_help : unicode
403 408 help string for --no-name flag
404 409
405 410 Returns
406 411 -------
407 412
408 413 cfg : dict
409 414 A dict with two keys: 'name', and 'no-name', for setting and unsetting
410 415 the trait, respectively.
411 416 """
412 417 # default helpstrings
413 418 set_help = set_help or "set %s=True"%configurable
414 419 unset_help = unset_help or "set %s=False"%configurable
415 420
416 421 cls,trait = configurable.split('.')
417 422
418 423 setter = {cls : {trait : True}}
419 424 unsetter = {cls : {trait : False}}
420 425 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
421 426
General Comments 0
You need to be logged in to leave comments. Login now