##// END OF EJS Templates
make Config.merge a public method...
MinRK -
Show More
@@ -1,561 +1,561 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 A base class for a configurable application.
3 A base class for a configurable application.
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 * Min RK
8 * Min RK
9 """
9 """
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2011 The IPython Development Team
12 # Copyright (C) 2008-2011 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 import logging
22 import logging
23 import os
23 import os
24 import re
24 import re
25 import sys
25 import sys
26 from copy import deepcopy
26 from copy import deepcopy
27 from collections import defaultdict
27 from collections import defaultdict
28
28
29 from IPython.external.decorator import decorator
29 from IPython.external.decorator import decorator
30
30
31 from IPython.config.configurable import SingletonConfigurable
31 from IPython.config.configurable import SingletonConfigurable
32 from IPython.config.loader import (
32 from IPython.config.loader import (
33 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound,
33 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound,
34 )
34 )
35
35
36 from IPython.utils.traitlets import (
36 from IPython.utils.traitlets import (
37 Unicode, List, Enum, Dict, Instance, TraitError
37 Unicode, List, Enum, Dict, Instance, TraitError
38 )
38 )
39 from IPython.utils.importstring import import_item
39 from IPython.utils.importstring import import_item
40 from IPython.utils.text import indent, wrap_paragraphs, dedent
40 from IPython.utils.text import indent, wrap_paragraphs, dedent
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # function for re-wrapping a helpstring
43 # function for re-wrapping a helpstring
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # Descriptions for the various sections
47 # Descriptions for the various sections
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 # merge flags&aliases into options
50 # merge flags&aliases into options
51 option_description = """
51 option_description = """
52 Arguments that take values are actually convenience aliases to full
52 Arguments that take values are actually convenience aliases to full
53 Configurables, whose aliases are listed on the help line. For more information
53 Configurables, whose aliases are listed on the help line. For more information
54 on full configurables, see '--help-all'.
54 on full configurables, see '--help-all'.
55 """.strip() # trim newlines of front and back
55 """.strip() # trim newlines of front and back
56
56
57 keyvalue_description = """
57 keyvalue_description = """
58 Parameters are set from command-line arguments of the form:
58 Parameters are set from command-line arguments of the form:
59 `--Class.trait=value`.
59 `--Class.trait=value`.
60 This line is evaluated in Python, so simple expressions are allowed, e.g.::
60 This line is evaluated in Python, so simple expressions are allowed, e.g.::
61 `--C.a='range(3)'` For setting C.a=[0,1,2].
61 `--C.a='range(3)'` For setting C.a=[0,1,2].
62 """.strip() # trim newlines of front and back
62 """.strip() # trim newlines of front and back
63
63
64 subcommand_description = """
64 subcommand_description = """
65 Subcommands are launched as `{app} cmd [args]`. For information on using
65 Subcommands are launched as `{app} cmd [args]`. For information on using
66 subcommand 'cmd', do: `{app} cmd -h`.
66 subcommand 'cmd', do: `{app} cmd -h`.
67 """.strip().format(app=os.path.basename(sys.argv[0]))
67 """.strip().format(app=os.path.basename(sys.argv[0]))
68 # get running program name
68 # get running program name
69
69
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71 # Application class
71 # Application class
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73
73
74 @decorator
74 @decorator
75 def catch_config_error(method, app, *args, **kwargs):
75 def catch_config_error(method, app, *args, **kwargs):
76 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
76 """Method decorator for catching invalid config (Trait/ArgumentErrors) during init.
77
77
78 On a TraitError (generally caused by bad config), this will print the trait's
78 On a TraitError (generally caused by bad config), this will print the trait's
79 message, and exit the app.
79 message, and exit the app.
80
80
81 For use on init methods, to prevent invoking excepthook on invalid input.
81 For use on init methods, to prevent invoking excepthook on invalid input.
82 """
82 """
83 try:
83 try:
84 return method(app, *args, **kwargs)
84 return method(app, *args, **kwargs)
85 except (TraitError, ArgumentError) as e:
85 except (TraitError, ArgumentError) as e:
86 app.print_description()
86 app.print_description()
87 app.print_help()
87 app.print_help()
88 app.print_examples()
88 app.print_examples()
89 app.log.fatal("Bad config encountered during initialization:")
89 app.log.fatal("Bad config encountered during initialization:")
90 app.log.fatal(str(e))
90 app.log.fatal(str(e))
91 app.log.debug("Config at the time: %s", app.config)
91 app.log.debug("Config at the time: %s", app.config)
92 app.exit(1)
92 app.exit(1)
93
93
94
94
95 class ApplicationError(Exception):
95 class ApplicationError(Exception):
96 pass
96 pass
97
97
98 class LevelFormatter(logging.Formatter):
98 class LevelFormatter(logging.Formatter):
99 """Formatter with additional `highlevel` record
99 """Formatter with additional `highlevel` record
100
100
101 This field is empty if log level is less than highlevel_limit,
101 This field is empty if log level is less than highlevel_limit,
102 otherwise it is formatted with self.highlevel_format.
102 otherwise it is formatted with self.highlevel_format.
103
103
104 Useful for adding 'WARNING' to warning messages,
104 Useful for adding 'WARNING' to warning messages,
105 without adding 'INFO' to info, etc.
105 without adding 'INFO' to info, etc.
106 """
106 """
107 highlevel_limit = logging.WARN
107 highlevel_limit = logging.WARN
108 highlevel_format = " %(levelname)s |"
108 highlevel_format = " %(levelname)s |"
109
109
110 def format(self, record):
110 def format(self, record):
111 if record.levelno >= self.highlevel_limit:
111 if record.levelno >= self.highlevel_limit:
112 record.highlevel = self.highlevel_format % record.__dict__
112 record.highlevel = self.highlevel_format % record.__dict__
113 else:
113 else:
114 record.highlevel = ""
114 record.highlevel = ""
115
115
116 return super(LevelFormatter, self).format(record)
116 return super(LevelFormatter, self).format(record)
117
117
118
118
119 class Application(SingletonConfigurable):
119 class Application(SingletonConfigurable):
120 """A singleton application with full configuration support."""
120 """A singleton application with full configuration support."""
121
121
122 # The name of the application, will usually match the name of the command
122 # The name of the application, will usually match the name of the command
123 # line application
123 # line application
124 name = Unicode(u'application')
124 name = Unicode(u'application')
125
125
126 # The description of the application that is printed at the beginning
126 # The description of the application that is printed at the beginning
127 # of the help.
127 # of the help.
128 description = Unicode(u'This is an application.')
128 description = Unicode(u'This is an application.')
129 # default section descriptions
129 # default section descriptions
130 option_description = Unicode(option_description)
130 option_description = Unicode(option_description)
131 keyvalue_description = Unicode(keyvalue_description)
131 keyvalue_description = Unicode(keyvalue_description)
132 subcommand_description = Unicode(subcommand_description)
132 subcommand_description = Unicode(subcommand_description)
133
133
134 # The usage and example string that goes at the end of the help string.
134 # The usage and example string that goes at the end of the help string.
135 examples = Unicode()
135 examples = Unicode()
136
136
137 # A sequence of Configurable subclasses whose config=True attributes will
137 # A sequence of Configurable subclasses whose config=True attributes will
138 # be exposed at the command line.
138 # be exposed at the command line.
139 classes = List([])
139 classes = List([])
140
140
141 # The version string of this application.
141 # The version string of this application.
142 version = Unicode(u'0.0')
142 version = Unicode(u'0.0')
143
143
144 # The log level for the application
144 # The log level for the application
145 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
145 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
146 default_value=logging.WARN,
146 default_value=logging.WARN,
147 config=True,
147 config=True,
148 help="Set the log level by value or name.")
148 help="Set the log level by value or name.")
149 def _log_level_changed(self, name, old, new):
149 def _log_level_changed(self, name, old, new):
150 """Adjust the log level when log_level is set."""
150 """Adjust the log level when log_level is set."""
151 if isinstance(new, basestring):
151 if isinstance(new, basestring):
152 new = getattr(logging, new)
152 new = getattr(logging, new)
153 self.log_level = new
153 self.log_level = new
154 self.log.setLevel(new)
154 self.log.setLevel(new)
155
155
156 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
156 log_datefmt = Unicode("%Y-%m-%d %H:%M:%S", config=True,
157 help="The date format used by logging formatters for %(asctime)s"
157 help="The date format used by logging formatters for %(asctime)s"
158 )
158 )
159 def _log_datefmt_changed(self, name, old, new):
159 def _log_datefmt_changed(self, name, old, new):
160 self._log_format_changed()
160 self._log_format_changed()
161
161
162 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
162 log_format = Unicode("[%(name)s]%(highlevel)s %(message)s", config=True,
163 help="The Logging format template",
163 help="The Logging format template",
164 )
164 )
165 def _log_format_changed(self, name, old, new):
165 def _log_format_changed(self, name, old, new):
166 """Change the log formatter when log_format is set."""
166 """Change the log formatter when log_format is set."""
167 _log_handler = self.log.handlers[0]
167 _log_handler = self.log.handlers[0]
168 _log_formatter = LevelFormatter(new, datefmt=self._log_datefmt)
168 _log_formatter = LevelFormatter(new, datefmt=self._log_datefmt)
169 _log_handler.setFormatter(_log_formatter)
169 _log_handler.setFormatter(_log_formatter)
170
170
171 log = Instance(logging.Logger)
171 log = Instance(logging.Logger)
172 def _log_default(self):
172 def _log_default(self):
173 """Start logging for this application.
173 """Start logging for this application.
174
174
175 The default is to log to stderr using a StreamHandler, if no default
175 The default is to log to stderr using a StreamHandler, if no default
176 handler already exists. The log level starts at logging.WARN, but this
176 handler already exists. The log level starts at logging.WARN, but this
177 can be adjusted by setting the ``log_level`` attribute.
177 can be adjusted by setting the ``log_level`` attribute.
178 """
178 """
179 log = logging.getLogger(self.__class__.__name__)
179 log = logging.getLogger(self.__class__.__name__)
180 log.setLevel(self.log_level)
180 log.setLevel(self.log_level)
181 log.propagate = False
181 log.propagate = False
182 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
182 _log = log # copied from Logger.hasHandlers() (new in Python 3.2)
183 while _log:
183 while _log:
184 if _log.handlers:
184 if _log.handlers:
185 return log
185 return log
186 if not _log.propagate:
186 if not _log.propagate:
187 break
187 break
188 else:
188 else:
189 _log = _log.parent
189 _log = _log.parent
190 if sys.executable.endswith('pythonw.exe'):
190 if sys.executable.endswith('pythonw.exe'):
191 # this should really go to a file, but file-logging is only
191 # this should really go to a file, but file-logging is only
192 # hooked up in parallel applications
192 # hooked up in parallel applications
193 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
193 _log_handler = logging.StreamHandler(open(os.devnull, 'w'))
194 else:
194 else:
195 _log_handler = logging.StreamHandler()
195 _log_handler = logging.StreamHandler()
196 _log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt)
196 _log_formatter = LevelFormatter(self.log_format, datefmt=self.log_datefmt)
197 _log_handler.setFormatter(_log_formatter)
197 _log_handler.setFormatter(_log_formatter)
198 log.addHandler(_log_handler)
198 log.addHandler(_log_handler)
199 return log
199 return log
200
200
201 # the alias map for configurables
201 # the alias map for configurables
202 aliases = Dict({'log-level' : 'Application.log_level'})
202 aliases = Dict({'log-level' : 'Application.log_level'})
203
203
204 # flags for loading Configurables or store_const style flags
204 # flags for loading Configurables or store_const style flags
205 # flags are loaded from this dict by '--key' flags
205 # flags are loaded from this dict by '--key' flags
206 # this must be a dict of two-tuples, the first element being the Config/dict
206 # this must be a dict of two-tuples, the first element being the Config/dict
207 # and the second being the help string for the flag
207 # and the second being the help string for the flag
208 flags = Dict()
208 flags = Dict()
209 def _flags_changed(self, name, old, new):
209 def _flags_changed(self, name, old, new):
210 """ensure flags dict is valid"""
210 """ensure flags dict is valid"""
211 for key,value in new.iteritems():
211 for key,value in new.iteritems():
212 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
212 assert len(value) == 2, "Bad flag: %r:%s"%(key,value)
213 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
213 assert isinstance(value[0], (dict, Config)), "Bad flag: %r:%s"%(key,value)
214 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
214 assert isinstance(value[1], basestring), "Bad flag: %r:%s"%(key,value)
215
215
216
216
217 # subcommands for launching other applications
217 # subcommands for launching other applications
218 # if this is not empty, this will be a parent Application
218 # if this is not empty, this will be a parent Application
219 # this must be a dict of two-tuples,
219 # this must be a dict of two-tuples,
220 # the first element being the application class/import string
220 # the first element being the application class/import string
221 # and the second being the help string for the subcommand
221 # and the second being the help string for the subcommand
222 subcommands = Dict()
222 subcommands = Dict()
223 # parse_command_line will initialize a subapp, if requested
223 # parse_command_line will initialize a subapp, if requested
224 subapp = Instance('IPython.config.application.Application', allow_none=True)
224 subapp = Instance('IPython.config.application.Application', allow_none=True)
225
225
226 # extra command-line arguments that don't set config values
226 # extra command-line arguments that don't set config values
227 extra_args = List(Unicode)
227 extra_args = List(Unicode)
228
228
229
229
230 def __init__(self, **kwargs):
230 def __init__(self, **kwargs):
231 SingletonConfigurable.__init__(self, **kwargs)
231 SingletonConfigurable.__init__(self, **kwargs)
232 # Ensure my class is in self.classes, so my attributes appear in command line
232 # Ensure my class is in self.classes, so my attributes appear in command line
233 # options and config files.
233 # options and config files.
234 if self.__class__ not in self.classes:
234 if self.__class__ not in self.classes:
235 self.classes.insert(0, self.__class__)
235 self.classes.insert(0, self.__class__)
236
236
237 def _config_changed(self, name, old, new):
237 def _config_changed(self, name, old, new):
238 SingletonConfigurable._config_changed(self, name, old, new)
238 SingletonConfigurable._config_changed(self, name, old, new)
239 self.log.debug('Config changed:')
239 self.log.debug('Config changed:')
240 self.log.debug(repr(new))
240 self.log.debug(repr(new))
241
241
242 @catch_config_error
242 @catch_config_error
243 def initialize(self, argv=None):
243 def initialize(self, argv=None):
244 """Do the basic steps to configure me.
244 """Do the basic steps to configure me.
245
245
246 Override in subclasses.
246 Override in subclasses.
247 """
247 """
248 self.parse_command_line(argv)
248 self.parse_command_line(argv)
249
249
250
250
251 def start(self):
251 def start(self):
252 """Start the app mainloop.
252 """Start the app mainloop.
253
253
254 Override in subclasses.
254 Override in subclasses.
255 """
255 """
256 if self.subapp is not None:
256 if self.subapp is not None:
257 return self.subapp.start()
257 return self.subapp.start()
258
258
259 def print_alias_help(self):
259 def print_alias_help(self):
260 """Print the alias part of the help."""
260 """Print the alias part of the help."""
261 if not self.aliases:
261 if not self.aliases:
262 return
262 return
263
263
264 lines = []
264 lines = []
265 classdict = {}
265 classdict = {}
266 for cls in self.classes:
266 for cls in self.classes:
267 # include all parents (up to, but excluding Configurable) in available names
267 # include all parents (up to, but excluding Configurable) in available names
268 for c in cls.mro()[:-3]:
268 for c in cls.mro()[:-3]:
269 classdict[c.__name__] = c
269 classdict[c.__name__] = c
270
270
271 for alias, longname in self.aliases.iteritems():
271 for alias, longname in self.aliases.iteritems():
272 classname, traitname = longname.split('.',1)
272 classname, traitname = longname.split('.',1)
273 cls = classdict[classname]
273 cls = classdict[classname]
274
274
275 trait = cls.class_traits(config=True)[traitname]
275 trait = cls.class_traits(config=True)[traitname]
276 help = cls.class_get_trait_help(trait).splitlines()
276 help = cls.class_get_trait_help(trait).splitlines()
277 # reformat first line
277 # reformat first line
278 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
278 help[0] = help[0].replace(longname, alias) + ' (%s)'%longname
279 if len(alias) == 1:
279 if len(alias) == 1:
280 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
280 help[0] = help[0].replace('--%s='%alias, '-%s '%alias)
281 lines.extend(help)
281 lines.extend(help)
282 # lines.append('')
282 # lines.append('')
283 print os.linesep.join(lines)
283 print os.linesep.join(lines)
284
284
285 def print_flag_help(self):
285 def print_flag_help(self):
286 """Print the flag part of the help."""
286 """Print the flag part of the help."""
287 if not self.flags:
287 if not self.flags:
288 return
288 return
289
289
290 lines = []
290 lines = []
291 for m, (cfg,help) in self.flags.iteritems():
291 for m, (cfg,help) in self.flags.iteritems():
292 prefix = '--' if len(m) > 1 else '-'
292 prefix = '--' if len(m) > 1 else '-'
293 lines.append(prefix+m)
293 lines.append(prefix+m)
294 lines.append(indent(dedent(help.strip())))
294 lines.append(indent(dedent(help.strip())))
295 # lines.append('')
295 # lines.append('')
296 print os.linesep.join(lines)
296 print os.linesep.join(lines)
297
297
298 def print_options(self):
298 def print_options(self):
299 if not self.flags and not self.aliases:
299 if not self.flags and not self.aliases:
300 return
300 return
301 lines = ['Options']
301 lines = ['Options']
302 lines.append('-'*len(lines[0]))
302 lines.append('-'*len(lines[0]))
303 lines.append('')
303 lines.append('')
304 for p in wrap_paragraphs(self.option_description):
304 for p in wrap_paragraphs(self.option_description):
305 lines.append(p)
305 lines.append(p)
306 lines.append('')
306 lines.append('')
307 print os.linesep.join(lines)
307 print os.linesep.join(lines)
308 self.print_flag_help()
308 self.print_flag_help()
309 self.print_alias_help()
309 self.print_alias_help()
310 print
310 print
311
311
312 def print_subcommands(self):
312 def print_subcommands(self):
313 """Print the subcommand part of the help."""
313 """Print the subcommand part of the help."""
314 if not self.subcommands:
314 if not self.subcommands:
315 return
315 return
316
316
317 lines = ["Subcommands"]
317 lines = ["Subcommands"]
318 lines.append('-'*len(lines[0]))
318 lines.append('-'*len(lines[0]))
319 lines.append('')
319 lines.append('')
320 for p in wrap_paragraphs(self.subcommand_description):
320 for p in wrap_paragraphs(self.subcommand_description):
321 lines.append(p)
321 lines.append(p)
322 lines.append('')
322 lines.append('')
323 for subc, (cls, help) in self.subcommands.iteritems():
323 for subc, (cls, help) in self.subcommands.iteritems():
324 lines.append(subc)
324 lines.append(subc)
325 if help:
325 if help:
326 lines.append(indent(dedent(help.strip())))
326 lines.append(indent(dedent(help.strip())))
327 lines.append('')
327 lines.append('')
328 print os.linesep.join(lines)
328 print os.linesep.join(lines)
329
329
330 def print_help(self, classes=False):
330 def print_help(self, classes=False):
331 """Print the help for each Configurable class in self.classes.
331 """Print the help for each Configurable class in self.classes.
332
332
333 If classes=False (the default), only flags and aliases are printed.
333 If classes=False (the default), only flags and aliases are printed.
334 """
334 """
335 self.print_subcommands()
335 self.print_subcommands()
336 self.print_options()
336 self.print_options()
337
337
338 if classes:
338 if classes:
339 if self.classes:
339 if self.classes:
340 print "Class parameters"
340 print "Class parameters"
341 print "----------------"
341 print "----------------"
342 print
342 print
343 for p in wrap_paragraphs(self.keyvalue_description):
343 for p in wrap_paragraphs(self.keyvalue_description):
344 print p
344 print p
345 print
345 print
346
346
347 for cls in self.classes:
347 for cls in self.classes:
348 cls.class_print_help()
348 cls.class_print_help()
349 print
349 print
350 else:
350 else:
351 print "To see all available configurables, use `--help-all`"
351 print "To see all available configurables, use `--help-all`"
352 print
352 print
353
353
354 def print_description(self):
354 def print_description(self):
355 """Print the application description."""
355 """Print the application description."""
356 for p in wrap_paragraphs(self.description):
356 for p in wrap_paragraphs(self.description):
357 print p
357 print p
358 print
358 print
359
359
360 def print_examples(self):
360 def print_examples(self):
361 """Print usage and examples.
361 """Print usage and examples.
362
362
363 This usage string goes at the end of the command line help string
363 This usage string goes at the end of the command line help string
364 and should contain examples of the application's usage.
364 and should contain examples of the application's usage.
365 """
365 """
366 if self.examples:
366 if self.examples:
367 print "Examples"
367 print "Examples"
368 print "--------"
368 print "--------"
369 print
369 print
370 print indent(dedent(self.examples.strip()))
370 print indent(dedent(self.examples.strip()))
371 print
371 print
372
372
373 def print_version(self):
373 def print_version(self):
374 """Print the version string."""
374 """Print the version string."""
375 print self.version
375 print self.version
376
376
377 def update_config(self, config):
377 def update_config(self, config):
378 """Fire the traits events when the config is updated."""
378 """Fire the traits events when the config is updated."""
379 # Save a copy of the current config.
379 # Save a copy of the current config.
380 newconfig = deepcopy(self.config)
380 newconfig = deepcopy(self.config)
381 # Merge the new config into the current one.
381 # Merge the new config into the current one.
382 newconfig._merge(config)
382 newconfig.merge(config)
383 # Save the combined config as self.config, which triggers the traits
383 # Save the combined config as self.config, which triggers the traits
384 # events.
384 # events.
385 self.config = newconfig
385 self.config = newconfig
386
386
387 @catch_config_error
387 @catch_config_error
388 def initialize_subcommand(self, subc, argv=None):
388 def initialize_subcommand(self, subc, argv=None):
389 """Initialize a subcommand with argv."""
389 """Initialize a subcommand with argv."""
390 subapp,help = self.subcommands.get(subc)
390 subapp,help = self.subcommands.get(subc)
391
391
392 if isinstance(subapp, basestring):
392 if isinstance(subapp, basestring):
393 subapp = import_item(subapp)
393 subapp = import_item(subapp)
394
394
395 # clear existing instances
395 # clear existing instances
396 self.__class__.clear_instance()
396 self.__class__.clear_instance()
397 # instantiate
397 # instantiate
398 self.subapp = subapp.instance()
398 self.subapp = subapp.instance()
399 # and initialize subapp
399 # and initialize subapp
400 self.subapp.initialize(argv)
400 self.subapp.initialize(argv)
401
401
402 def flatten_flags(self):
402 def flatten_flags(self):
403 """flatten flags and aliases, so cl-args override as expected.
403 """flatten flags and aliases, so cl-args override as expected.
404
404
405 This prevents issues such as an alias pointing to InteractiveShell,
405 This prevents issues such as an alias pointing to InteractiveShell,
406 but a config file setting the same trait in TerminalInteraciveShell
406 but a config file setting the same trait in TerminalInteraciveShell
407 getting inappropriate priority over the command-line arg.
407 getting inappropriate priority over the command-line arg.
408
408
409 Only aliases with exactly one descendent in the class list
409 Only aliases with exactly one descendent in the class list
410 will be promoted.
410 will be promoted.
411
411
412 """
412 """
413 # build a tree of classes in our list that inherit from a particular
413 # build a tree of classes in our list that inherit from a particular
414 # it will be a dict by parent classname of classes in our list
414 # it will be a dict by parent classname of classes in our list
415 # that are descendents
415 # that are descendents
416 mro_tree = defaultdict(list)
416 mro_tree = defaultdict(list)
417 for cls in self.classes:
417 for cls in self.classes:
418 clsname = cls.__name__
418 clsname = cls.__name__
419 for parent in cls.mro()[1:-3]:
419 for parent in cls.mro()[1:-3]:
420 # exclude cls itself and Configurable,HasTraits,object
420 # exclude cls itself and Configurable,HasTraits,object
421 mro_tree[parent.__name__].append(clsname)
421 mro_tree[parent.__name__].append(clsname)
422 # flatten aliases, which have the form:
422 # flatten aliases, which have the form:
423 # { 'alias' : 'Class.trait' }
423 # { 'alias' : 'Class.trait' }
424 aliases = {}
424 aliases = {}
425 for alias, cls_trait in self.aliases.iteritems():
425 for alias, cls_trait in self.aliases.iteritems():
426 cls,trait = cls_trait.split('.',1)
426 cls,trait = cls_trait.split('.',1)
427 children = mro_tree[cls]
427 children = mro_tree[cls]
428 if len(children) == 1:
428 if len(children) == 1:
429 # exactly one descendent, promote alias
429 # exactly one descendent, promote alias
430 cls = children[0]
430 cls = children[0]
431 aliases[alias] = '.'.join([cls,trait])
431 aliases[alias] = '.'.join([cls,trait])
432
432
433 # flatten flags, which are of the form:
433 # flatten flags, which are of the form:
434 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
434 # { 'key' : ({'Cls' : {'trait' : value}}, 'help')}
435 flags = {}
435 flags = {}
436 for key, (flagdict, help) in self.flags.iteritems():
436 for key, (flagdict, help) in self.flags.iteritems():
437 newflag = {}
437 newflag = {}
438 for cls, subdict in flagdict.iteritems():
438 for cls, subdict in flagdict.iteritems():
439 children = mro_tree[cls]
439 children = mro_tree[cls]
440 # exactly one descendent, promote flag section
440 # exactly one descendent, promote flag section
441 if len(children) == 1:
441 if len(children) == 1:
442 cls = children[0]
442 cls = children[0]
443 newflag[cls] = subdict
443 newflag[cls] = subdict
444 flags[key] = (newflag, help)
444 flags[key] = (newflag, help)
445 return flags, aliases
445 return flags, aliases
446
446
447 @catch_config_error
447 @catch_config_error
448 def parse_command_line(self, argv=None):
448 def parse_command_line(self, argv=None):
449 """Parse the command line arguments."""
449 """Parse the command line arguments."""
450 argv = sys.argv[1:] if argv is None else argv
450 argv = sys.argv[1:] if argv is None else argv
451
451
452 if argv and argv[0] == 'help':
452 if argv and argv[0] == 'help':
453 # turn `ipython help notebook` into `ipython notebook -h`
453 # turn `ipython help notebook` into `ipython notebook -h`
454 argv = argv[1:] + ['-h']
454 argv = argv[1:] + ['-h']
455
455
456 if self.subcommands and len(argv) > 0:
456 if self.subcommands and len(argv) > 0:
457 # we have subcommands, and one may have been specified
457 # we have subcommands, and one may have been specified
458 subc, subargv = argv[0], argv[1:]
458 subc, subargv = argv[0], argv[1:]
459 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
459 if re.match(r'^\w(\-?\w)*$', subc) and subc in self.subcommands:
460 # it's a subcommand, and *not* a flag or class parameter
460 # it's a subcommand, and *not* a flag or class parameter
461 return self.initialize_subcommand(subc, subargv)
461 return self.initialize_subcommand(subc, subargv)
462
462
463 # Arguments after a '--' argument are for the script IPython may be
463 # Arguments after a '--' argument are for the script IPython may be
464 # about to run, not IPython iteslf. For arguments parsed here (help and
464 # about to run, not IPython iteslf. For arguments parsed here (help and
465 # version), we want to only search the arguments up to the first
465 # version), we want to only search the arguments up to the first
466 # occurrence of '--', which we're calling interpreted_argv.
466 # occurrence of '--', which we're calling interpreted_argv.
467 try:
467 try:
468 interpreted_argv = argv[:argv.index('--')]
468 interpreted_argv = argv[:argv.index('--')]
469 except ValueError:
469 except ValueError:
470 interpreted_argv = argv
470 interpreted_argv = argv
471
471
472 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
472 if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')):
473 self.print_description()
473 self.print_description()
474 self.print_help('--help-all' in interpreted_argv)
474 self.print_help('--help-all' in interpreted_argv)
475 self.print_examples()
475 self.print_examples()
476 self.exit(0)
476 self.exit(0)
477
477
478 if '--version' in interpreted_argv or '-V' in interpreted_argv:
478 if '--version' in interpreted_argv or '-V' in interpreted_argv:
479 self.print_version()
479 self.print_version()
480 self.exit(0)
480 self.exit(0)
481
481
482 # flatten flags&aliases, so cl-args get appropriate priority:
482 # flatten flags&aliases, so cl-args get appropriate priority:
483 flags,aliases = self.flatten_flags()
483 flags,aliases = self.flatten_flags()
484
484
485 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
485 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
486 flags=flags)
486 flags=flags)
487 config = loader.load_config()
487 config = loader.load_config()
488 self.update_config(config)
488 self.update_config(config)
489 # store unparsed args in extra_args
489 # store unparsed args in extra_args
490 self.extra_args = loader.extra_args
490 self.extra_args = loader.extra_args
491
491
492 @catch_config_error
492 @catch_config_error
493 def load_config_file(self, filename, path=None):
493 def load_config_file(self, filename, path=None):
494 """Load a .py based config file by filename and path."""
494 """Load a .py based config file by filename and path."""
495 loader = PyFileConfigLoader(filename, path=path)
495 loader = PyFileConfigLoader(filename, path=path)
496 try:
496 try:
497 config = loader.load_config()
497 config = loader.load_config()
498 except ConfigFileNotFound:
498 except ConfigFileNotFound:
499 # problem finding the file, raise
499 # problem finding the file, raise
500 raise
500 raise
501 except Exception:
501 except Exception:
502 # try to get the full filename, but it will be empty in the
502 # try to get the full filename, but it will be empty in the
503 # unlikely event that the error raised before filefind finished
503 # unlikely event that the error raised before filefind finished
504 filename = loader.full_filename or filename
504 filename = loader.full_filename or filename
505 # problem while running the file
505 # problem while running the file
506 self.log.error("Exception while loading config file %s",
506 self.log.error("Exception while loading config file %s",
507 filename, exc_info=True)
507 filename, exc_info=True)
508 else:
508 else:
509 self.log.debug("Loaded config file: %s", loader.full_filename)
509 self.log.debug("Loaded config file: %s", loader.full_filename)
510 self.update_config(config)
510 self.update_config(config)
511
511
512 def generate_config_file(self):
512 def generate_config_file(self):
513 """generate default config file from Configurables"""
513 """generate default config file from Configurables"""
514 lines = ["# Configuration file for %s."%self.name]
514 lines = ["# Configuration file for %s."%self.name]
515 lines.append('')
515 lines.append('')
516 lines.append('c = get_config()')
516 lines.append('c = get_config()')
517 lines.append('')
517 lines.append('')
518 for cls in self.classes:
518 for cls in self.classes:
519 lines.append(cls.class_config_section())
519 lines.append(cls.class_config_section())
520 return '\n'.join(lines)
520 return '\n'.join(lines)
521
521
522 def exit(self, exit_status=0):
522 def exit(self, exit_status=0):
523 self.log.debug("Exiting application: %s" % self.name)
523 self.log.debug("Exiting application: %s" % self.name)
524 sys.exit(exit_status)
524 sys.exit(exit_status)
525
525
526 #-----------------------------------------------------------------------------
526 #-----------------------------------------------------------------------------
527 # utility functions, for convenience
527 # utility functions, for convenience
528 #-----------------------------------------------------------------------------
528 #-----------------------------------------------------------------------------
529
529
530 def boolean_flag(name, configurable, set_help='', unset_help=''):
530 def boolean_flag(name, configurable, set_help='', unset_help=''):
531 """Helper for building basic --trait, --no-trait flags.
531 """Helper for building basic --trait, --no-trait flags.
532
532
533 Parameters
533 Parameters
534 ----------
534 ----------
535
535
536 name : str
536 name : str
537 The name of the flag.
537 The name of the flag.
538 configurable : str
538 configurable : str
539 The 'Class.trait' string of the trait to be set/unset with the flag
539 The 'Class.trait' string of the trait to be set/unset with the flag
540 set_help : unicode
540 set_help : unicode
541 help string for --name flag
541 help string for --name flag
542 unset_help : unicode
542 unset_help : unicode
543 help string for --no-name flag
543 help string for --no-name flag
544
544
545 Returns
545 Returns
546 -------
546 -------
547
547
548 cfg : dict
548 cfg : dict
549 A dict with two keys: 'name', and 'no-name', for setting and unsetting
549 A dict with two keys: 'name', and 'no-name', for setting and unsetting
550 the trait, respectively.
550 the trait, respectively.
551 """
551 """
552 # default helpstrings
552 # default helpstrings
553 set_help = set_help or "set %s=True"%configurable
553 set_help = set_help or "set %s=True"%configurable
554 unset_help = unset_help or "set %s=False"%configurable
554 unset_help = unset_help or "set %s=False"%configurable
555
555
556 cls,trait = configurable.split('.')
556 cls,trait = configurable.split('.')
557
557
558 setter = {cls : {trait : True}}
558 setter = {cls : {trait : True}}
559 unsetter = {cls : {trait : False}}
559 unsetter = {cls : {trait : False}}
560 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
560 return {name : (setter, set_help), 'no-'+name : (unsetter, unset_help)}
561
561
@@ -1,356 +1,356 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 A base class for objects that are configurable.
3 A base class for objects that are configurable.
4
4
5 Inheritance diagram:
5 Inheritance diagram:
6
6
7 .. inheritance-diagram:: IPython.config.configurable
7 .. inheritance-diagram:: IPython.config.configurable
8 :parts: 3
8 :parts: 3
9
9
10 Authors:
10 Authors:
11
11
12 * Brian Granger
12 * Brian Granger
13 * Fernando Perez
13 * Fernando Perez
14 * Min RK
14 * Min RK
15 """
15 """
16
16
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18 # Copyright (C) 2008-2011 The IPython Development Team
18 # Copyright (C) 2008-2011 The IPython Development Team
19 #
19 #
20 # Distributed under the terms of the BSD License. The full license is in
20 # Distributed under the terms of the BSD License. The full license is in
21 # the file COPYING, distributed as part of this software.
21 # the file COPYING, distributed as part of this software.
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Imports
25 # Imports
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28 import datetime
28 import datetime
29 from copy import deepcopy
29 from copy import deepcopy
30
30
31 from loader import Config
31 from loader import Config
32 from IPython.utils.traitlets import HasTraits, Instance
32 from IPython.utils.traitlets import HasTraits, Instance
33 from IPython.utils.text import indent, wrap_paragraphs
33 from IPython.utils.text import indent, wrap_paragraphs
34
34
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Helper classes for Configurables
37 # Helper classes for Configurables
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40
40
41 class ConfigurableError(Exception):
41 class ConfigurableError(Exception):
42 pass
42 pass
43
43
44
44
45 class MultipleInstanceError(ConfigurableError):
45 class MultipleInstanceError(ConfigurableError):
46 pass
46 pass
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # Configurable implementation
49 # Configurable implementation
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class Configurable(HasTraits):
52 class Configurable(HasTraits):
53
53
54 config = Instance(Config, (), {})
54 config = Instance(Config, (), {})
55 created = None
55 created = None
56
56
57 def __init__(self, **kwargs):
57 def __init__(self, **kwargs):
58 """Create a configurable given a config config.
58 """Create a configurable given a config config.
59
59
60 Parameters
60 Parameters
61 ----------
61 ----------
62 config : Config
62 config : Config
63 If this is empty, default values are used. If config is a
63 If this is empty, default values are used. If config is a
64 :class:`Config` instance, it will be used to configure the
64 :class:`Config` instance, it will be used to configure the
65 instance.
65 instance.
66
66
67 Notes
67 Notes
68 -----
68 -----
69 Subclasses of Configurable must call the :meth:`__init__` method of
69 Subclasses of Configurable must call the :meth:`__init__` method of
70 :class:`Configurable` *before* doing anything else and using
70 :class:`Configurable` *before* doing anything else and using
71 :func:`super`::
71 :func:`super`::
72
72
73 class MyConfigurable(Configurable):
73 class MyConfigurable(Configurable):
74 def __init__(self, config=None):
74 def __init__(self, config=None):
75 super(MyConfigurable, self).__init__(config=config)
75 super(MyConfigurable, self).__init__(config=config)
76 # Then any other code you need to finish initialization.
76 # Then any other code you need to finish initialization.
77
77
78 This ensures that instances will be configured properly.
78 This ensures that instances will be configured properly.
79 """
79 """
80 config = kwargs.pop('config', None)
80 config = kwargs.pop('config', None)
81 if config is not None:
81 if config is not None:
82 # We used to deepcopy, but for now we are trying to just save
82 # We used to deepcopy, but for now we are trying to just save
83 # by reference. This *could* have side effects as all components
83 # by reference. This *could* have side effects as all components
84 # will share config. In fact, I did find such a side effect in
84 # will share config. In fact, I did find such a side effect in
85 # _config_changed below. If a config attribute value was a mutable type
85 # _config_changed below. If a config attribute value was a mutable type
86 # all instances of a component were getting the same copy, effectively
86 # all instances of a component were getting the same copy, effectively
87 # making that a class attribute.
87 # making that a class attribute.
88 # self.config = deepcopy(config)
88 # self.config = deepcopy(config)
89 self.config = config
89 self.config = config
90 # This should go second so individual keyword arguments override
90 # This should go second so individual keyword arguments override
91 # the values in config.
91 # the values in config.
92 super(Configurable, self).__init__(**kwargs)
92 super(Configurable, self).__init__(**kwargs)
93 self.created = datetime.datetime.now()
93 self.created = datetime.datetime.now()
94
94
95 #-------------------------------------------------------------------------
95 #-------------------------------------------------------------------------
96 # Static trait notifiations
96 # Static trait notifiations
97 #-------------------------------------------------------------------------
97 #-------------------------------------------------------------------------
98
98
99 def _config_changed(self, name, old, new):
99 def _config_changed(self, name, old, new):
100 """Update all the class traits having ``config=True`` as metadata.
100 """Update all the class traits having ``config=True`` as metadata.
101
101
102 For any class trait with a ``config`` metadata attribute that is
102 For any class trait with a ``config`` metadata attribute that is
103 ``True``, we update the trait with the value of the corresponding
103 ``True``, we update the trait with the value of the corresponding
104 config entry.
104 config entry.
105 """
105 """
106 # Get all traits with a config metadata entry that is True
106 # Get all traits with a config metadata entry that is True
107 traits = self.traits(config=True)
107 traits = self.traits(config=True)
108
108
109 # We auto-load config section for this class as well as any parent
109 # We auto-load config section for this class as well as any parent
110 # classes that are Configurable subclasses. This starts with Configurable
110 # classes that are Configurable subclasses. This starts with Configurable
111 # and works down the mro loading the config for each section.
111 # and works down the mro loading the config for each section.
112 section_names = [cls.__name__ for cls in \
112 section_names = [cls.__name__ for cls in \
113 reversed(self.__class__.__mro__) if
113 reversed(self.__class__.__mro__) if
114 issubclass(cls, Configurable) and issubclass(self.__class__, cls)]
114 issubclass(cls, Configurable) and issubclass(self.__class__, cls)]
115
115
116 for sname in section_names:
116 for sname in section_names:
117 # Don't do a blind getattr as that would cause the config to
117 # Don't do a blind getattr as that would cause the config to
118 # dynamically create the section with name self.__class__.__name__.
118 # dynamically create the section with name self.__class__.__name__.
119 if new._has_section(sname):
119 if new._has_section(sname):
120 my_config = new[sname]
120 my_config = new[sname]
121 for k, v in traits.iteritems():
121 for k, v in traits.iteritems():
122 # Don't allow traitlets with config=True to start with
122 # Don't allow traitlets with config=True to start with
123 # uppercase. Otherwise, they are confused with Config
123 # uppercase. Otherwise, they are confused with Config
124 # subsections. But, developers shouldn't have uppercase
124 # subsections. But, developers shouldn't have uppercase
125 # attributes anyways! (PEP 6)
125 # attributes anyways! (PEP 6)
126 if k[0].upper()==k[0] and not k.startswith('_'):
126 if k[0].upper()==k[0] and not k.startswith('_'):
127 raise ConfigurableError('Configurable traitlets with '
127 raise ConfigurableError('Configurable traitlets with '
128 'config=True must start with a lowercase so they are '
128 'config=True must start with a lowercase so they are '
129 'not confused with Config subsections: %s.%s' % \
129 'not confused with Config subsections: %s.%s' % \
130 (self.__class__.__name__, k))
130 (self.__class__.__name__, k))
131 try:
131 try:
132 # Here we grab the value from the config
132 # Here we grab the value from the config
133 # If k has the naming convention of a config
133 # If k has the naming convention of a config
134 # section, it will be auto created.
134 # section, it will be auto created.
135 config_value = my_config[k]
135 config_value = my_config[k]
136 except KeyError:
136 except KeyError:
137 pass
137 pass
138 else:
138 else:
139 # print "Setting %s.%s from %s.%s=%r" % \
139 # print "Setting %s.%s from %s.%s=%r" % \
140 # (self.__class__.__name__,k,sname,k,config_value)
140 # (self.__class__.__name__,k,sname,k,config_value)
141 # We have to do a deepcopy here if we don't deepcopy the entire
141 # We have to do a deepcopy here if we don't deepcopy the entire
142 # config object. If we don't, a mutable config_value will be
142 # config object. If we don't, a mutable config_value will be
143 # shared by all instances, effectively making it a class attribute.
143 # shared by all instances, effectively making it a class attribute.
144 setattr(self, k, deepcopy(config_value))
144 setattr(self, k, deepcopy(config_value))
145
145
146 def update_config(self, config):
146 def update_config(self, config):
147 """Fire the traits events when the config is updated."""
147 """Fire the traits events when the config is updated."""
148 # Save a copy of the current config.
148 # Save a copy of the current config.
149 newconfig = deepcopy(self.config)
149 newconfig = deepcopy(self.config)
150 # Merge the new config into the current one.
150 # Merge the new config into the current one.
151 newconfig._merge(config)
151 newconfig.merge(config)
152 # Save the combined config as self.config, which triggers the traits
152 # Save the combined config as self.config, which triggers the traits
153 # events.
153 # events.
154 self.config = newconfig
154 self.config = newconfig
155
155
156 @classmethod
156 @classmethod
157 def class_get_help(cls, inst=None):
157 def class_get_help(cls, inst=None):
158 """Get the help string for this class in ReST format.
158 """Get the help string for this class in ReST format.
159
159
160 If `inst` is given, it's current trait values will be used in place of
160 If `inst` is given, it's current trait values will be used in place of
161 class defaults.
161 class defaults.
162 """
162 """
163 assert inst is None or isinstance(inst, cls)
163 assert inst is None or isinstance(inst, cls)
164 cls_traits = cls.class_traits(config=True)
164 cls_traits = cls.class_traits(config=True)
165 final_help = []
165 final_help = []
166 final_help.append(u'%s options' % cls.__name__)
166 final_help.append(u'%s options' % cls.__name__)
167 final_help.append(len(final_help[0])*u'-')
167 final_help.append(len(final_help[0])*u'-')
168 for k, v in sorted(cls.class_traits(config=True).iteritems()):
168 for k, v in sorted(cls.class_traits(config=True).iteritems()):
169 help = cls.class_get_trait_help(v, inst)
169 help = cls.class_get_trait_help(v, inst)
170 final_help.append(help)
170 final_help.append(help)
171 return '\n'.join(final_help)
171 return '\n'.join(final_help)
172
172
173 @classmethod
173 @classmethod
174 def class_get_trait_help(cls, trait, inst=None):
174 def class_get_trait_help(cls, trait, inst=None):
175 """Get the help string for a single trait.
175 """Get the help string for a single trait.
176
176
177 If `inst` is given, it's current trait values will be used in place of
177 If `inst` is given, it's current trait values will be used in place of
178 the class default.
178 the class default.
179 """
179 """
180 assert inst is None or isinstance(inst, cls)
180 assert inst is None or isinstance(inst, cls)
181 lines = []
181 lines = []
182 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
182 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
183 lines.append(header)
183 lines.append(header)
184 if inst is not None:
184 if inst is not None:
185 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
185 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
186 else:
186 else:
187 try:
187 try:
188 dvr = repr(trait.get_default_value())
188 dvr = repr(trait.get_default_value())
189 except Exception:
189 except Exception:
190 dvr = None # ignore defaults we can't construct
190 dvr = None # ignore defaults we can't construct
191 if dvr is not None:
191 if dvr is not None:
192 if len(dvr) > 64:
192 if len(dvr) > 64:
193 dvr = dvr[:61]+'...'
193 dvr = dvr[:61]+'...'
194 lines.append(indent('Default: %s' % dvr, 4))
194 lines.append(indent('Default: %s' % dvr, 4))
195 if 'Enum' in trait.__class__.__name__:
195 if 'Enum' in trait.__class__.__name__:
196 # include Enum choices
196 # include Enum choices
197 lines.append(indent('Choices: %r' % (trait.values,)))
197 lines.append(indent('Choices: %r' % (trait.values,)))
198
198
199 help = trait.get_metadata('help')
199 help = trait.get_metadata('help')
200 if help is not None:
200 if help is not None:
201 help = '\n'.join(wrap_paragraphs(help, 76))
201 help = '\n'.join(wrap_paragraphs(help, 76))
202 lines.append(indent(help, 4))
202 lines.append(indent(help, 4))
203 return '\n'.join(lines)
203 return '\n'.join(lines)
204
204
205 @classmethod
205 @classmethod
206 def class_print_help(cls, inst=None):
206 def class_print_help(cls, inst=None):
207 """Get the help string for a single trait and print it."""
207 """Get the help string for a single trait and print it."""
208 print cls.class_get_help(inst)
208 print cls.class_get_help(inst)
209
209
210 @classmethod
210 @classmethod
211 def class_config_section(cls):
211 def class_config_section(cls):
212 """Get the config class config section"""
212 """Get the config class config section"""
213 def c(s):
213 def c(s):
214 """return a commented, wrapped block."""
214 """return a commented, wrapped block."""
215 s = '\n\n'.join(wrap_paragraphs(s, 78))
215 s = '\n\n'.join(wrap_paragraphs(s, 78))
216
216
217 return '# ' + s.replace('\n', '\n# ')
217 return '# ' + s.replace('\n', '\n# ')
218
218
219 # section header
219 # section header
220 breaker = '#' + '-'*78
220 breaker = '#' + '-'*78
221 s = "# %s configuration" % cls.__name__
221 s = "# %s configuration" % cls.__name__
222 lines = [breaker, s, breaker, '']
222 lines = [breaker, s, breaker, '']
223 # get the description trait
223 # get the description trait
224 desc = cls.class_traits().get('description')
224 desc = cls.class_traits().get('description')
225 if desc:
225 if desc:
226 desc = desc.default_value
226 desc = desc.default_value
227 else:
227 else:
228 # no description trait, use __doc__
228 # no description trait, use __doc__
229 desc = getattr(cls, '__doc__', '')
229 desc = getattr(cls, '__doc__', '')
230 if desc:
230 if desc:
231 lines.append(c(desc))
231 lines.append(c(desc))
232 lines.append('')
232 lines.append('')
233
233
234 parents = []
234 parents = []
235 for parent in cls.mro():
235 for parent in cls.mro():
236 # only include parents that are not base classes
236 # only include parents that are not base classes
237 # and are not the class itself
237 # and are not the class itself
238 # and have some configurable traits to inherit
238 # and have some configurable traits to inherit
239 if parent is not cls and issubclass(parent, Configurable) and \
239 if parent is not cls and issubclass(parent, Configurable) and \
240 parent.class_traits(config=True):
240 parent.class_traits(config=True):
241 parents.append(parent)
241 parents.append(parent)
242
242
243 if parents:
243 if parents:
244 pstr = ', '.join([ p.__name__ for p in parents ])
244 pstr = ', '.join([ p.__name__ for p in parents ])
245 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
245 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
246 lines.append('')
246 lines.append('')
247
247
248 for name, trait in cls.class_traits(config=True).iteritems():
248 for name, trait in cls.class_traits(config=True).iteritems():
249 help = trait.get_metadata('help') or ''
249 help = trait.get_metadata('help') or ''
250 lines.append(c(help))
250 lines.append(c(help))
251 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
251 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
252 lines.append('')
252 lines.append('')
253 return '\n'.join(lines)
253 return '\n'.join(lines)
254
254
255
255
256
256
257 class SingletonConfigurable(Configurable):
257 class SingletonConfigurable(Configurable):
258 """A configurable that only allows one instance.
258 """A configurable that only allows one instance.
259
259
260 This class is for classes that should only have one instance of itself
260 This class is for classes that should only have one instance of itself
261 or *any* subclass. To create and retrieve such a class use the
261 or *any* subclass. To create and retrieve such a class use the
262 :meth:`SingletonConfigurable.instance` method.
262 :meth:`SingletonConfigurable.instance` method.
263 """
263 """
264
264
265 _instance = None
265 _instance = None
266
266
267 @classmethod
267 @classmethod
268 def _walk_mro(cls):
268 def _walk_mro(cls):
269 """Walk the cls.mro() for parent classes that are also singletons
269 """Walk the cls.mro() for parent classes that are also singletons
270
270
271 For use in instance()
271 For use in instance()
272 """
272 """
273
273
274 for subclass in cls.mro():
274 for subclass in cls.mro():
275 if issubclass(cls, subclass) and \
275 if issubclass(cls, subclass) and \
276 issubclass(subclass, SingletonConfigurable) and \
276 issubclass(subclass, SingletonConfigurable) and \
277 subclass != SingletonConfigurable:
277 subclass != SingletonConfigurable:
278 yield subclass
278 yield subclass
279
279
280 @classmethod
280 @classmethod
281 def clear_instance(cls):
281 def clear_instance(cls):
282 """unset _instance for this class and singleton parents.
282 """unset _instance for this class and singleton parents.
283 """
283 """
284 if not cls.initialized():
284 if not cls.initialized():
285 return
285 return
286 for subclass in cls._walk_mro():
286 for subclass in cls._walk_mro():
287 if isinstance(subclass._instance, cls):
287 if isinstance(subclass._instance, cls):
288 # only clear instances that are instances
288 # only clear instances that are instances
289 # of the calling class
289 # of the calling class
290 subclass._instance = None
290 subclass._instance = None
291
291
292 @classmethod
292 @classmethod
293 def instance(cls, *args, **kwargs):
293 def instance(cls, *args, **kwargs):
294 """Returns a global instance of this class.
294 """Returns a global instance of this class.
295
295
296 This method create a new instance if none have previously been created
296 This method create a new instance if none have previously been created
297 and returns a previously created instance is one already exists.
297 and returns a previously created instance is one already exists.
298
298
299 The arguments and keyword arguments passed to this method are passed
299 The arguments and keyword arguments passed to this method are passed
300 on to the :meth:`__init__` method of the class upon instantiation.
300 on to the :meth:`__init__` method of the class upon instantiation.
301
301
302 Examples
302 Examples
303 --------
303 --------
304
304
305 Create a singleton class using instance, and retrieve it::
305 Create a singleton class using instance, and retrieve it::
306
306
307 >>> from IPython.config.configurable import SingletonConfigurable
307 >>> from IPython.config.configurable import SingletonConfigurable
308 >>> class Foo(SingletonConfigurable): pass
308 >>> class Foo(SingletonConfigurable): pass
309 >>> foo = Foo.instance()
309 >>> foo = Foo.instance()
310 >>> foo == Foo.instance()
310 >>> foo == Foo.instance()
311 True
311 True
312
312
313 Create a subclass that is retrived using the base class instance::
313 Create a subclass that is retrived using the base class instance::
314
314
315 >>> class Bar(SingletonConfigurable): pass
315 >>> class Bar(SingletonConfigurable): pass
316 >>> class Bam(Bar): pass
316 >>> class Bam(Bar): pass
317 >>> bam = Bam.instance()
317 >>> bam = Bam.instance()
318 >>> bam == Bar.instance()
318 >>> bam == Bar.instance()
319 True
319 True
320 """
320 """
321 # Create and save the instance
321 # Create and save the instance
322 if cls._instance is None:
322 if cls._instance is None:
323 inst = cls(*args, **kwargs)
323 inst = cls(*args, **kwargs)
324 # Now make sure that the instance will also be returned by
324 # Now make sure that the instance will also be returned by
325 # parent classes' _instance attribute.
325 # parent classes' _instance attribute.
326 for subclass in cls._walk_mro():
326 for subclass in cls._walk_mro():
327 subclass._instance = inst
327 subclass._instance = inst
328
328
329 if isinstance(cls._instance, cls):
329 if isinstance(cls._instance, cls):
330 return cls._instance
330 return cls._instance
331 else:
331 else:
332 raise MultipleInstanceError(
332 raise MultipleInstanceError(
333 'Multiple incompatible subclass instances of '
333 'Multiple incompatible subclass instances of '
334 '%s are being created.' % cls.__name__
334 '%s are being created.' % cls.__name__
335 )
335 )
336
336
337 @classmethod
337 @classmethod
338 def initialized(cls):
338 def initialized(cls):
339 """Has an instance been created?"""
339 """Has an instance been created?"""
340 return hasattr(cls, "_instance") and cls._instance is not None
340 return hasattr(cls, "_instance") and cls._instance is not None
341
341
342
342
343 class LoggingConfigurable(Configurable):
343 class LoggingConfigurable(Configurable):
344 """A parent class for Configurables that log.
344 """A parent class for Configurables that log.
345
345
346 Subclasses have a log trait, and the default behavior
346 Subclasses have a log trait, and the default behavior
347 is to get the logger from the currently running Application
347 is to get the logger from the currently running Application
348 via Application.instance().log.
348 via Application.instance().log.
349 """
349 """
350
350
351 log = Instance('logging.Logger')
351 log = Instance('logging.Logger')
352 def _log_default(self):
352 def _log_default(self):
353 from IPython.config.application import Application
353 from IPython.config.application import Application
354 return Application.instance().log
354 return Application.instance().log
355
355
356
356
@@ -1,719 +1,724 b''
1 """A simple configuration system.
1 """A simple configuration system.
2
2
3 Inheritance diagram:
3 Inheritance diagram:
4
4
5 .. inheritance-diagram:: IPython.config.loader
5 .. inheritance-diagram:: IPython.config.loader
6 :parts: 3
6 :parts: 3
7
7
8 Authors
8 Authors
9 -------
9 -------
10 * Brian Granger
10 * Brian Granger
11 * Fernando Perez
11 * Fernando Perez
12 * Min RK
12 * Min RK
13 """
13 """
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Copyright (C) 2008-2011 The IPython Development Team
16 # Copyright (C) 2008-2011 The IPython Development Team
17 #
17 #
18 # Distributed under the terms of the BSD License. The full license is in
18 # Distributed under the terms of the BSD License. The full license is in
19 # the file COPYING, distributed as part of this software.
19 # the file COPYING, distributed as part of this software.
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Imports
23 # Imports
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25
25
26 import __builtin__ as builtin_mod
26 import __builtin__ as builtin_mod
27 import os
27 import os
28 import re
28 import re
29 import sys
29 import sys
30
30
31 from IPython.external import argparse
31 from IPython.external import argparse
32 from IPython.utils.path import filefind, get_ipython_dir
32 from IPython.utils.path import filefind, get_ipython_dir
33 from IPython.utils import py3compat, text, warn
33 from IPython.utils import py3compat, text, warn
34 from IPython.utils.encoding import DEFAULT_ENCODING
34 from IPython.utils.encoding import DEFAULT_ENCODING
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Exceptions
37 # Exceptions
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40
40
41 class ConfigError(Exception):
41 class ConfigError(Exception):
42 pass
42 pass
43
43
44 class ConfigLoaderError(ConfigError):
44 class ConfigLoaderError(ConfigError):
45 pass
45 pass
46
46
47 class ConfigFileNotFound(ConfigError):
47 class ConfigFileNotFound(ConfigError):
48 pass
48 pass
49
49
50 class ArgumentError(ConfigLoaderError):
50 class ArgumentError(ConfigLoaderError):
51 pass
51 pass
52
52
53 #-----------------------------------------------------------------------------
53 #-----------------------------------------------------------------------------
54 # Argparse fix
54 # Argparse fix
55 #-----------------------------------------------------------------------------
55 #-----------------------------------------------------------------------------
56
56
57 # Unfortunately argparse by default prints help messages to stderr instead of
57 # Unfortunately argparse by default prints help messages to stderr instead of
58 # stdout. This makes it annoying to capture long help screens at the command
58 # stdout. This makes it annoying to capture long help screens at the command
59 # line, since one must know how to pipe stderr, which many users don't know how
59 # line, since one must know how to pipe stderr, which many users don't know how
60 # to do. So we override the print_help method with one that defaults to
60 # to do. So we override the print_help method with one that defaults to
61 # stdout and use our class instead.
61 # stdout and use our class instead.
62
62
63 class ArgumentParser(argparse.ArgumentParser):
63 class ArgumentParser(argparse.ArgumentParser):
64 """Simple argparse subclass that prints help to stdout by default."""
64 """Simple argparse subclass that prints help to stdout by default."""
65
65
66 def print_help(self, file=None):
66 def print_help(self, file=None):
67 if file is None:
67 if file is None:
68 file = sys.stdout
68 file = sys.stdout
69 return super(ArgumentParser, self).print_help(file)
69 return super(ArgumentParser, self).print_help(file)
70
70
71 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
71 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
72
72
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74 # Config class for holding config information
74 # Config class for holding config information
75 #-----------------------------------------------------------------------------
75 #-----------------------------------------------------------------------------
76
76
77
77
78 class Config(dict):
78 class Config(dict):
79 """An attribute based dict that can do smart merges."""
79 """An attribute based dict that can do smart merges."""
80
80
81 def __init__(self, *args, **kwds):
81 def __init__(self, *args, **kwds):
82 dict.__init__(self, *args, **kwds)
82 dict.__init__(self, *args, **kwds)
83 # This sets self.__dict__ = self, but it has to be done this way
83 # This sets self.__dict__ = self, but it has to be done this way
84 # because we are also overriding __setattr__.
84 # because we are also overriding __setattr__.
85 dict.__setattr__(self, '__dict__', self)
85 dict.__setattr__(self, '__dict__', self)
86 self._ensure_subconfig()
86 self._ensure_subconfig()
87
87
88 def _ensure_subconfig(self):
88 def _ensure_subconfig(self):
89 """ensure that sub-dicts that should be Config objects are
89 """ensure that sub-dicts that should be Config objects are
90
90
91 casts dicts that are under section keys to Config objects,
91 casts dicts that are under section keys to Config objects,
92 which is necessary for constructing Config objects from dict literals.
92 which is necessary for constructing Config objects from dict literals.
93 """
93 """
94 for key in self:
94 for key in self:
95 obj = self[key]
95 obj = self[key]
96 if self._is_section_key(key) \
96 if self._is_section_key(key) \
97 and isinstance(obj, dict) \
97 and isinstance(obj, dict) \
98 and not isinstance(obj, Config):
98 and not isinstance(obj, Config):
99 dict.__setattr__(self, key, Config(obj))
99 dict.__setattr__(self, key, Config(obj))
100
100
101 def _merge(self, other):
101 def _merge(self, other):
102 """deprecated alias, use Config.merge()"""
103 self.merge(other)
104
105 def merge(self, other):
106 """merge another config object into this one"""
102 to_update = {}
107 to_update = {}
103 for k, v in other.iteritems():
108 for k, v in other.iteritems():
104 if k not in self:
109 if k not in self:
105 to_update[k] = v
110 to_update[k] = v
106 else: # I have this key
111 else: # I have this key
107 if isinstance(v, Config):
112 if isinstance(v, Config) and isinstance(self[k], Config):
108 # Recursively merge common sub Configs
113 # Recursively merge common sub Configs
109 self[k]._merge(v)
114 self[k].merge(v)
110 else:
115 else:
111 # Plain updates for non-Configs
116 # Plain updates for non-Configs
112 to_update[k] = v
117 to_update[k] = v
113
118
114 self.update(to_update)
119 self.update(to_update)
115
120
116 def _is_section_key(self, key):
121 def _is_section_key(self, key):
117 if key[0].upper()==key[0] and not key.startswith('_'):
122 if key[0].upper()==key[0] and not key.startswith('_'):
118 return True
123 return True
119 else:
124 else:
120 return False
125 return False
121
126
122 def __contains__(self, key):
127 def __contains__(self, key):
123 if self._is_section_key(key):
128 if self._is_section_key(key):
124 return True
129 return True
125 else:
130 else:
126 return super(Config, self).__contains__(key)
131 return super(Config, self).__contains__(key)
127 # .has_key is deprecated for dictionaries.
132 # .has_key is deprecated for dictionaries.
128 has_key = __contains__
133 has_key = __contains__
129
134
130 def _has_section(self, key):
135 def _has_section(self, key):
131 if self._is_section_key(key):
136 if self._is_section_key(key):
132 if super(Config, self).__contains__(key):
137 if super(Config, self).__contains__(key):
133 return True
138 return True
134 return False
139 return False
135
140
136 def copy(self):
141 def copy(self):
137 return type(self)(dict.copy(self))
142 return type(self)(dict.copy(self))
138
143
139 def __copy__(self):
144 def __copy__(self):
140 return self.copy()
145 return self.copy()
141
146
142 def __deepcopy__(self, memo):
147 def __deepcopy__(self, memo):
143 import copy
148 import copy
144 return type(self)(copy.deepcopy(self.items()))
149 return type(self)(copy.deepcopy(self.items()))
145
150
146 def __getitem__(self, key):
151 def __getitem__(self, key):
147 # We cannot use directly self._is_section_key, because it triggers
152 # We cannot use directly self._is_section_key, because it triggers
148 # infinite recursion on top of PyPy. Instead, we manually fish the
153 # infinite recursion on top of PyPy. Instead, we manually fish the
149 # bound method.
154 # bound method.
150 is_section_key = self.__class__._is_section_key.__get__(self)
155 is_section_key = self.__class__._is_section_key.__get__(self)
151
156
152 # Because we use this for an exec namespace, we need to delegate
157 # Because we use this for an exec namespace, we need to delegate
153 # the lookup of names in __builtin__ to itself. This means
158 # the lookup of names in __builtin__ to itself. This means
154 # that you can't have section or attribute names that are
159 # that you can't have section or attribute names that are
155 # builtins.
160 # builtins.
156 try:
161 try:
157 return getattr(builtin_mod, key)
162 return getattr(builtin_mod, key)
158 except AttributeError:
163 except AttributeError:
159 pass
164 pass
160 if is_section_key(key):
165 if is_section_key(key):
161 try:
166 try:
162 return dict.__getitem__(self, key)
167 return dict.__getitem__(self, key)
163 except KeyError:
168 except KeyError:
164 c = Config()
169 c = Config()
165 dict.__setitem__(self, key, c)
170 dict.__setitem__(self, key, c)
166 return c
171 return c
167 else:
172 else:
168 return dict.__getitem__(self, key)
173 return dict.__getitem__(self, key)
169
174
170 def __setitem__(self, key, value):
175 def __setitem__(self, key, value):
171 # Don't allow names in __builtin__ to be modified.
176 # Don't allow names in __builtin__ to be modified.
172 if hasattr(builtin_mod, key):
177 if hasattr(builtin_mod, key):
173 raise ConfigError('Config variable names cannot have the same name '
178 raise ConfigError('Config variable names cannot have the same name '
174 'as a Python builtin: %s' % key)
179 'as a Python builtin: %s' % key)
175 if self._is_section_key(key):
180 if self._is_section_key(key):
176 if not isinstance(value, Config):
181 if not isinstance(value, Config):
177 raise ValueError('values whose keys begin with an uppercase '
182 raise ValueError('values whose keys begin with an uppercase '
178 'char must be Config instances: %r, %r' % (key, value))
183 'char must be Config instances: %r, %r' % (key, value))
179 else:
184 else:
180 dict.__setitem__(self, key, value)
185 dict.__setitem__(self, key, value)
181
186
182 def __getattr__(self, key):
187 def __getattr__(self, key):
183 try:
188 try:
184 return self.__getitem__(key)
189 return self.__getitem__(key)
185 except KeyError as e:
190 except KeyError as e:
186 raise AttributeError(e)
191 raise AttributeError(e)
187
192
188 def __setattr__(self, key, value):
193 def __setattr__(self, key, value):
189 try:
194 try:
190 self.__setitem__(key, value)
195 self.__setitem__(key, value)
191 except KeyError as e:
196 except KeyError as e:
192 raise AttributeError(e)
197 raise AttributeError(e)
193
198
194 def __delattr__(self, key):
199 def __delattr__(self, key):
195 try:
200 try:
196 dict.__delitem__(self, key)
201 dict.__delitem__(self, key)
197 except KeyError as e:
202 except KeyError as e:
198 raise AttributeError(e)
203 raise AttributeError(e)
199
204
200
205
201 #-----------------------------------------------------------------------------
206 #-----------------------------------------------------------------------------
202 # Config loading classes
207 # Config loading classes
203 #-----------------------------------------------------------------------------
208 #-----------------------------------------------------------------------------
204
209
205
210
206 class ConfigLoader(object):
211 class ConfigLoader(object):
207 """A object for loading configurations from just about anywhere.
212 """A object for loading configurations from just about anywhere.
208
213
209 The resulting configuration is packaged as a :class:`Struct`.
214 The resulting configuration is packaged as a :class:`Struct`.
210
215
211 Notes
216 Notes
212 -----
217 -----
213 A :class:`ConfigLoader` does one thing: load a config from a source
218 A :class:`ConfigLoader` does one thing: load a config from a source
214 (file, command line arguments) and returns the data as a :class:`Struct`.
219 (file, command line arguments) and returns the data as a :class:`Struct`.
215 There are lots of things that :class:`ConfigLoader` does not do. It does
220 There are lots of things that :class:`ConfigLoader` does not do. It does
216 not implement complex logic for finding config files. It does not handle
221 not implement complex logic for finding config files. It does not handle
217 default values or merge multiple configs. These things need to be
222 default values or merge multiple configs. These things need to be
218 handled elsewhere.
223 handled elsewhere.
219 """
224 """
220
225
221 def __init__(self):
226 def __init__(self):
222 """A base class for config loaders.
227 """A base class for config loaders.
223
228
224 Examples
229 Examples
225 --------
230 --------
226
231
227 >>> cl = ConfigLoader()
232 >>> cl = ConfigLoader()
228 >>> config = cl.load_config()
233 >>> config = cl.load_config()
229 >>> config
234 >>> config
230 {}
235 {}
231 """
236 """
232 self.clear()
237 self.clear()
233
238
234 def clear(self):
239 def clear(self):
235 self.config = Config()
240 self.config = Config()
236
241
237 def load_config(self):
242 def load_config(self):
238 """Load a config from somewhere, return a :class:`Config` instance.
243 """Load a config from somewhere, return a :class:`Config` instance.
239
244
240 Usually, this will cause self.config to be set and then returned.
245 Usually, this will cause self.config to be set and then returned.
241 However, in most cases, :meth:`ConfigLoader.clear` should be called
246 However, in most cases, :meth:`ConfigLoader.clear` should be called
242 to erase any previous state.
247 to erase any previous state.
243 """
248 """
244 self.clear()
249 self.clear()
245 return self.config
250 return self.config
246
251
247
252
248 class FileConfigLoader(ConfigLoader):
253 class FileConfigLoader(ConfigLoader):
249 """A base class for file based configurations.
254 """A base class for file based configurations.
250
255
251 As we add more file based config loaders, the common logic should go
256 As we add more file based config loaders, the common logic should go
252 here.
257 here.
253 """
258 """
254 pass
259 pass
255
260
256
261
257 class PyFileConfigLoader(FileConfigLoader):
262 class PyFileConfigLoader(FileConfigLoader):
258 """A config loader for pure python files.
263 """A config loader for pure python files.
259
264
260 This calls execfile on a plain python file and looks for attributes
265 This calls execfile on a plain python file and looks for attributes
261 that are all caps. These attribute are added to the config Struct.
266 that are all caps. These attribute are added to the config Struct.
262 """
267 """
263
268
264 def __init__(self, filename, path=None):
269 def __init__(self, filename, path=None):
265 """Build a config loader for a filename and path.
270 """Build a config loader for a filename and path.
266
271
267 Parameters
272 Parameters
268 ----------
273 ----------
269 filename : str
274 filename : str
270 The file name of the config file.
275 The file name of the config file.
271 path : str, list, tuple
276 path : str, list, tuple
272 The path to search for the config file on, or a sequence of
277 The path to search for the config file on, or a sequence of
273 paths to try in order.
278 paths to try in order.
274 """
279 """
275 super(PyFileConfigLoader, self).__init__()
280 super(PyFileConfigLoader, self).__init__()
276 self.filename = filename
281 self.filename = filename
277 self.path = path
282 self.path = path
278 self.full_filename = ''
283 self.full_filename = ''
279 self.data = None
284 self.data = None
280
285
281 def load_config(self):
286 def load_config(self):
282 """Load the config from a file and return it as a Struct."""
287 """Load the config from a file and return it as a Struct."""
283 self.clear()
288 self.clear()
284 try:
289 try:
285 self._find_file()
290 self._find_file()
286 except IOError as e:
291 except IOError as e:
287 raise ConfigFileNotFound(str(e))
292 raise ConfigFileNotFound(str(e))
288 self._read_file_as_dict()
293 self._read_file_as_dict()
289 self._convert_to_config()
294 self._convert_to_config()
290 return self.config
295 return self.config
291
296
292 def _find_file(self):
297 def _find_file(self):
293 """Try to find the file by searching the paths."""
298 """Try to find the file by searching the paths."""
294 self.full_filename = filefind(self.filename, self.path)
299 self.full_filename = filefind(self.filename, self.path)
295
300
296 def _read_file_as_dict(self):
301 def _read_file_as_dict(self):
297 """Load the config file into self.config, with recursive loading."""
302 """Load the config file into self.config, with recursive loading."""
298 # This closure is made available in the namespace that is used
303 # This closure is made available in the namespace that is used
299 # to exec the config file. It allows users to call
304 # to exec the config file. It allows users to call
300 # load_subconfig('myconfig.py') to load config files recursively.
305 # load_subconfig('myconfig.py') to load config files recursively.
301 # It needs to be a closure because it has references to self.path
306 # It needs to be a closure because it has references to self.path
302 # and self.config. The sub-config is loaded with the same path
307 # and self.config. The sub-config is loaded with the same path
303 # as the parent, but it uses an empty config which is then merged
308 # as the parent, but it uses an empty config which is then merged
304 # with the parents.
309 # with the parents.
305
310
306 # If a profile is specified, the config file will be loaded
311 # If a profile is specified, the config file will be loaded
307 # from that profile
312 # from that profile
308
313
309 def load_subconfig(fname, profile=None):
314 def load_subconfig(fname, profile=None):
310 # import here to prevent circular imports
315 # import here to prevent circular imports
311 from IPython.core.profiledir import ProfileDir, ProfileDirError
316 from IPython.core.profiledir import ProfileDir, ProfileDirError
312 if profile is not None:
317 if profile is not None:
313 try:
318 try:
314 profile_dir = ProfileDir.find_profile_dir_by_name(
319 profile_dir = ProfileDir.find_profile_dir_by_name(
315 get_ipython_dir(),
320 get_ipython_dir(),
316 profile,
321 profile,
317 )
322 )
318 except ProfileDirError:
323 except ProfileDirError:
319 return
324 return
320 path = profile_dir.location
325 path = profile_dir.location
321 else:
326 else:
322 path = self.path
327 path = self.path
323 loader = PyFileConfigLoader(fname, path)
328 loader = PyFileConfigLoader(fname, path)
324 try:
329 try:
325 sub_config = loader.load_config()
330 sub_config = loader.load_config()
326 except ConfigFileNotFound:
331 except ConfigFileNotFound:
327 # Pass silently if the sub config is not there. This happens
332 # Pass silently if the sub config is not there. This happens
328 # when a user s using a profile, but not the default config.
333 # when a user s using a profile, but not the default config.
329 pass
334 pass
330 else:
335 else:
331 self.config._merge(sub_config)
336 self.config.merge(sub_config)
332
337
333 # Again, this needs to be a closure and should be used in config
338 # Again, this needs to be a closure and should be used in config
334 # files to get the config being loaded.
339 # files to get the config being loaded.
335 def get_config():
340 def get_config():
336 return self.config
341 return self.config
337
342
338 namespace = dict(
343 namespace = dict(
339 load_subconfig=load_subconfig,
344 load_subconfig=load_subconfig,
340 get_config=get_config,
345 get_config=get_config,
341 __file__=self.full_filename,
346 __file__=self.full_filename,
342 )
347 )
343 fs_encoding = sys.getfilesystemencoding() or 'ascii'
348 fs_encoding = sys.getfilesystemencoding() or 'ascii'
344 conf_filename = self.full_filename.encode(fs_encoding)
349 conf_filename = self.full_filename.encode(fs_encoding)
345 py3compat.execfile(conf_filename, namespace)
350 py3compat.execfile(conf_filename, namespace)
346
351
347 def _convert_to_config(self):
352 def _convert_to_config(self):
348 if self.data is None:
353 if self.data is None:
349 ConfigLoaderError('self.data does not exist')
354 ConfigLoaderError('self.data does not exist')
350
355
351
356
352 class CommandLineConfigLoader(ConfigLoader):
357 class CommandLineConfigLoader(ConfigLoader):
353 """A config loader for command line arguments.
358 """A config loader for command line arguments.
354
359
355 As we add more command line based loaders, the common logic should go
360 As we add more command line based loaders, the common logic should go
356 here.
361 here.
357 """
362 """
358
363
359 def _exec_config_str(self, lhs, rhs):
364 def _exec_config_str(self, lhs, rhs):
360 """execute self.config.<lhs> = <rhs>
365 """execute self.config.<lhs> = <rhs>
361
366
362 * expands ~ with expanduser
367 * expands ~ with expanduser
363 * tries to assign with raw eval, otherwise assigns with just the string,
368 * tries to assign with raw eval, otherwise assigns with just the string,
364 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
369 allowing `--C.a=foobar` and `--C.a="foobar"` to be equivalent. *Not*
365 equivalent are `--C.a=4` and `--C.a='4'`.
370 equivalent are `--C.a=4` and `--C.a='4'`.
366 """
371 """
367 rhs = os.path.expanduser(rhs)
372 rhs = os.path.expanduser(rhs)
368 try:
373 try:
369 # Try to see if regular Python syntax will work. This
374 # Try to see if regular Python syntax will work. This
370 # won't handle strings as the quote marks are removed
375 # won't handle strings as the quote marks are removed
371 # by the system shell.
376 # by the system shell.
372 value = eval(rhs)
377 value = eval(rhs)
373 except (NameError, SyntaxError):
378 except (NameError, SyntaxError):
374 # This case happens if the rhs is a string.
379 # This case happens if the rhs is a string.
375 value = rhs
380 value = rhs
376
381
377 exec u'self.config.%s = value' % lhs
382 exec u'self.config.%s = value' % lhs
378
383
379 def _load_flag(self, cfg):
384 def _load_flag(self, cfg):
380 """update self.config from a flag, which can be a dict or Config"""
385 """update self.config from a flag, which can be a dict or Config"""
381 if isinstance(cfg, (dict, Config)):
386 if isinstance(cfg, (dict, Config)):
382 # don't clobber whole config sections, update
387 # don't clobber whole config sections, update
383 # each section from config:
388 # each section from config:
384 for sec,c in cfg.iteritems():
389 for sec,c in cfg.iteritems():
385 self.config[sec].update(c)
390 self.config[sec].update(c)
386 else:
391 else:
387 raise TypeError("Invalid flag: %r" % cfg)
392 raise TypeError("Invalid flag: %r" % cfg)
388
393
389 # raw --identifier=value pattern
394 # raw --identifier=value pattern
390 # but *also* accept '-' as wordsep, for aliases
395 # but *also* accept '-' as wordsep, for aliases
391 # accepts: --foo=a
396 # accepts: --foo=a
392 # --Class.trait=value
397 # --Class.trait=value
393 # --alias-name=value
398 # --alias-name=value
394 # rejects: -foo=value
399 # rejects: -foo=value
395 # --foo
400 # --foo
396 # --Class.trait
401 # --Class.trait
397 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
402 kv_pattern = re.compile(r'\-\-[A-Za-z][\w\-]*(\.[\w\-]+)*\=.*')
398
403
399 # just flags, no assignments, with two *or one* leading '-'
404 # just flags, no assignments, with two *or one* leading '-'
400 # accepts: --foo
405 # accepts: --foo
401 # -foo-bar-again
406 # -foo-bar-again
402 # rejects: --anything=anything
407 # rejects: --anything=anything
403 # --two.word
408 # --two.word
404
409
405 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
410 flag_pattern = re.compile(r'\-\-?\w+[\-\w]*$')
406
411
407 class KeyValueConfigLoader(CommandLineConfigLoader):
412 class KeyValueConfigLoader(CommandLineConfigLoader):
408 """A config loader that loads key value pairs from the command line.
413 """A config loader that loads key value pairs from the command line.
409
414
410 This allows command line options to be gives in the following form::
415 This allows command line options to be gives in the following form::
411
416
412 ipython --profile="foo" --InteractiveShell.autocall=False
417 ipython --profile="foo" --InteractiveShell.autocall=False
413 """
418 """
414
419
415 def __init__(self, argv=None, aliases=None, flags=None):
420 def __init__(self, argv=None, aliases=None, flags=None):
416 """Create a key value pair config loader.
421 """Create a key value pair config loader.
417
422
418 Parameters
423 Parameters
419 ----------
424 ----------
420 argv : list
425 argv : list
421 A list that has the form of sys.argv[1:] which has unicode
426 A list that has the form of sys.argv[1:] which has unicode
422 elements of the form u"key=value". If this is None (default),
427 elements of the form u"key=value". If this is None (default),
423 then sys.argv[1:] will be used.
428 then sys.argv[1:] will be used.
424 aliases : dict
429 aliases : dict
425 A dict of aliases for configurable traits.
430 A dict of aliases for configurable traits.
426 Keys are the short aliases, Values are the resolved trait.
431 Keys are the short aliases, Values are the resolved trait.
427 Of the form: `{'alias' : 'Configurable.trait'}`
432 Of the form: `{'alias' : 'Configurable.trait'}`
428 flags : dict
433 flags : dict
429 A dict of flags, keyed by str name. Vaues can be Config objects,
434 A dict of flags, keyed by str name. Vaues can be Config objects,
430 dicts, or "key=value" strings. If Config or dict, when the flag
435 dicts, or "key=value" strings. If Config or dict, when the flag
431 is triggered, The flag is loaded as `self.config.update(m)`.
436 is triggered, The flag is loaded as `self.config.update(m)`.
432
437
433 Returns
438 Returns
434 -------
439 -------
435 config : Config
440 config : Config
436 The resulting Config object.
441 The resulting Config object.
437
442
438 Examples
443 Examples
439 --------
444 --------
440
445
441 >>> from IPython.config.loader import KeyValueConfigLoader
446 >>> from IPython.config.loader import KeyValueConfigLoader
442 >>> cl = KeyValueConfigLoader()
447 >>> cl = KeyValueConfigLoader()
443 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
448 >>> d = cl.load_config(["--A.name='brian'","--B.number=0"])
444 >>> sorted(d.items())
449 >>> sorted(d.items())
445 [('A', {'name': 'brian'}), ('B', {'number': 0})]
450 [('A', {'name': 'brian'}), ('B', {'number': 0})]
446 """
451 """
447 self.clear()
452 self.clear()
448 if argv is None:
453 if argv is None:
449 argv = sys.argv[1:]
454 argv = sys.argv[1:]
450 self.argv = argv
455 self.argv = argv
451 self.aliases = aliases or {}
456 self.aliases = aliases or {}
452 self.flags = flags or {}
457 self.flags = flags or {}
453
458
454
459
455 def clear(self):
460 def clear(self):
456 super(KeyValueConfigLoader, self).clear()
461 super(KeyValueConfigLoader, self).clear()
457 self.extra_args = []
462 self.extra_args = []
458
463
459
464
460 def _decode_argv(self, argv, enc=None):
465 def _decode_argv(self, argv, enc=None):
461 """decode argv if bytes, using stin.encoding, falling back on default enc"""
466 """decode argv if bytes, using stin.encoding, falling back on default enc"""
462 uargv = []
467 uargv = []
463 if enc is None:
468 if enc is None:
464 enc = DEFAULT_ENCODING
469 enc = DEFAULT_ENCODING
465 for arg in argv:
470 for arg in argv:
466 if not isinstance(arg, unicode):
471 if not isinstance(arg, unicode):
467 # only decode if not already decoded
472 # only decode if not already decoded
468 arg = arg.decode(enc)
473 arg = arg.decode(enc)
469 uargv.append(arg)
474 uargv.append(arg)
470 return uargv
475 return uargv
471
476
472
477
473 def load_config(self, argv=None, aliases=None, flags=None):
478 def load_config(self, argv=None, aliases=None, flags=None):
474 """Parse the configuration and generate the Config object.
479 """Parse the configuration and generate the Config object.
475
480
476 After loading, any arguments that are not key-value or
481 After loading, any arguments that are not key-value or
477 flags will be stored in self.extra_args - a list of
482 flags will be stored in self.extra_args - a list of
478 unparsed command-line arguments. This is used for
483 unparsed command-line arguments. This is used for
479 arguments such as input files or subcommands.
484 arguments such as input files or subcommands.
480
485
481 Parameters
486 Parameters
482 ----------
487 ----------
483 argv : list, optional
488 argv : list, optional
484 A list that has the form of sys.argv[1:] which has unicode
489 A list that has the form of sys.argv[1:] which has unicode
485 elements of the form u"key=value". If this is None (default),
490 elements of the form u"key=value". If this is None (default),
486 then self.argv will be used.
491 then self.argv will be used.
487 aliases : dict
492 aliases : dict
488 A dict of aliases for configurable traits.
493 A dict of aliases for configurable traits.
489 Keys are the short aliases, Values are the resolved trait.
494 Keys are the short aliases, Values are the resolved trait.
490 Of the form: `{'alias' : 'Configurable.trait'}`
495 Of the form: `{'alias' : 'Configurable.trait'}`
491 flags : dict
496 flags : dict
492 A dict of flags, keyed by str name. Values can be Config objects
497 A dict of flags, keyed by str name. Values can be Config objects
493 or dicts. When the flag is triggered, The config is loaded as
498 or dicts. When the flag is triggered, The config is loaded as
494 `self.config.update(cfg)`.
499 `self.config.update(cfg)`.
495 """
500 """
496 from IPython.config.configurable import Configurable
501 from IPython.config.configurable import Configurable
497
502
498 self.clear()
503 self.clear()
499 if argv is None:
504 if argv is None:
500 argv = self.argv
505 argv = self.argv
501 if aliases is None:
506 if aliases is None:
502 aliases = self.aliases
507 aliases = self.aliases
503 if flags is None:
508 if flags is None:
504 flags = self.flags
509 flags = self.flags
505
510
506 # ensure argv is a list of unicode strings:
511 # ensure argv is a list of unicode strings:
507 uargv = self._decode_argv(argv)
512 uargv = self._decode_argv(argv)
508 for idx,raw in enumerate(uargv):
513 for idx,raw in enumerate(uargv):
509 # strip leading '-'
514 # strip leading '-'
510 item = raw.lstrip('-')
515 item = raw.lstrip('-')
511
516
512 if raw == '--':
517 if raw == '--':
513 # don't parse arguments after '--'
518 # don't parse arguments after '--'
514 # this is useful for relaying arguments to scripts, e.g.
519 # this is useful for relaying arguments to scripts, e.g.
515 # ipython -i foo.py --pylab=qt -- args after '--' go-to-foo.py
520 # ipython -i foo.py --pylab=qt -- args after '--' go-to-foo.py
516 self.extra_args.extend(uargv[idx+1:])
521 self.extra_args.extend(uargv[idx+1:])
517 break
522 break
518
523
519 if kv_pattern.match(raw):
524 if kv_pattern.match(raw):
520 lhs,rhs = item.split('=',1)
525 lhs,rhs = item.split('=',1)
521 # Substitute longnames for aliases.
526 # Substitute longnames for aliases.
522 if lhs in aliases:
527 if lhs in aliases:
523 lhs = aliases[lhs]
528 lhs = aliases[lhs]
524 if '.' not in lhs:
529 if '.' not in lhs:
525 # probably a mistyped alias, but not technically illegal
530 # probably a mistyped alias, but not technically illegal
526 warn.warn("Unrecognized alias: '%s', it will probably have no effect."%lhs)
531 warn.warn("Unrecognized alias: '%s', it will probably have no effect."%lhs)
527 try:
532 try:
528 self._exec_config_str(lhs, rhs)
533 self._exec_config_str(lhs, rhs)
529 except Exception:
534 except Exception:
530 raise ArgumentError("Invalid argument: '%s'" % raw)
535 raise ArgumentError("Invalid argument: '%s'" % raw)
531
536
532 elif flag_pattern.match(raw):
537 elif flag_pattern.match(raw):
533 if item in flags:
538 if item in flags:
534 cfg,help = flags[item]
539 cfg,help = flags[item]
535 self._load_flag(cfg)
540 self._load_flag(cfg)
536 else:
541 else:
537 raise ArgumentError("Unrecognized flag: '%s'"%raw)
542 raise ArgumentError("Unrecognized flag: '%s'"%raw)
538 elif raw.startswith('-'):
543 elif raw.startswith('-'):
539 kv = '--'+item
544 kv = '--'+item
540 if kv_pattern.match(kv):
545 if kv_pattern.match(kv):
541 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
546 raise ArgumentError("Invalid argument: '%s', did you mean '%s'?"%(raw, kv))
542 else:
547 else:
543 raise ArgumentError("Invalid argument: '%s'"%raw)
548 raise ArgumentError("Invalid argument: '%s'"%raw)
544 else:
549 else:
545 # keep all args that aren't valid in a list,
550 # keep all args that aren't valid in a list,
546 # in case our parent knows what to do with them.
551 # in case our parent knows what to do with them.
547 self.extra_args.append(item)
552 self.extra_args.append(item)
548 return self.config
553 return self.config
549
554
550 class ArgParseConfigLoader(CommandLineConfigLoader):
555 class ArgParseConfigLoader(CommandLineConfigLoader):
551 """A loader that uses the argparse module to load from the command line."""
556 """A loader that uses the argparse module to load from the command line."""
552
557
553 def __init__(self, argv=None, aliases=None, flags=None, *parser_args, **parser_kw):
558 def __init__(self, argv=None, aliases=None, flags=None, *parser_args, **parser_kw):
554 """Create a config loader for use with argparse.
559 """Create a config loader for use with argparse.
555
560
556 Parameters
561 Parameters
557 ----------
562 ----------
558
563
559 argv : optional, list
564 argv : optional, list
560 If given, used to read command-line arguments from, otherwise
565 If given, used to read command-line arguments from, otherwise
561 sys.argv[1:] is used.
566 sys.argv[1:] is used.
562
567
563 parser_args : tuple
568 parser_args : tuple
564 A tuple of positional arguments that will be passed to the
569 A tuple of positional arguments that will be passed to the
565 constructor of :class:`argparse.ArgumentParser`.
570 constructor of :class:`argparse.ArgumentParser`.
566
571
567 parser_kw : dict
572 parser_kw : dict
568 A tuple of keyword arguments that will be passed to the
573 A tuple of keyword arguments that will be passed to the
569 constructor of :class:`argparse.ArgumentParser`.
574 constructor of :class:`argparse.ArgumentParser`.
570
575
571 Returns
576 Returns
572 -------
577 -------
573 config : Config
578 config : Config
574 The resulting Config object.
579 The resulting Config object.
575 """
580 """
576 super(CommandLineConfigLoader, self).__init__()
581 super(CommandLineConfigLoader, self).__init__()
577 self.clear()
582 self.clear()
578 if argv is None:
583 if argv is None:
579 argv = sys.argv[1:]
584 argv = sys.argv[1:]
580 self.argv = argv
585 self.argv = argv
581 self.aliases = aliases or {}
586 self.aliases = aliases or {}
582 self.flags = flags or {}
587 self.flags = flags or {}
583
588
584 self.parser_args = parser_args
589 self.parser_args = parser_args
585 self.version = parser_kw.pop("version", None)
590 self.version = parser_kw.pop("version", None)
586 kwargs = dict(argument_default=argparse.SUPPRESS)
591 kwargs = dict(argument_default=argparse.SUPPRESS)
587 kwargs.update(parser_kw)
592 kwargs.update(parser_kw)
588 self.parser_kw = kwargs
593 self.parser_kw = kwargs
589
594
590 def load_config(self, argv=None, aliases=None, flags=None):
595 def load_config(self, argv=None, aliases=None, flags=None):
591 """Parse command line arguments and return as a Config object.
596 """Parse command line arguments and return as a Config object.
592
597
593 Parameters
598 Parameters
594 ----------
599 ----------
595
600
596 args : optional, list
601 args : optional, list
597 If given, a list with the structure of sys.argv[1:] to parse
602 If given, a list with the structure of sys.argv[1:] to parse
598 arguments from. If not given, the instance's self.argv attribute
603 arguments from. If not given, the instance's self.argv attribute
599 (given at construction time) is used."""
604 (given at construction time) is used."""
600 self.clear()
605 self.clear()
601 if argv is None:
606 if argv is None:
602 argv = self.argv
607 argv = self.argv
603 if aliases is None:
608 if aliases is None:
604 aliases = self.aliases
609 aliases = self.aliases
605 if flags is None:
610 if flags is None:
606 flags = self.flags
611 flags = self.flags
607 self._create_parser(aliases, flags)
612 self._create_parser(aliases, flags)
608 self._parse_args(argv)
613 self._parse_args(argv)
609 self._convert_to_config()
614 self._convert_to_config()
610 return self.config
615 return self.config
611
616
612 def get_extra_args(self):
617 def get_extra_args(self):
613 if hasattr(self, 'extra_args'):
618 if hasattr(self, 'extra_args'):
614 return self.extra_args
619 return self.extra_args
615 else:
620 else:
616 return []
621 return []
617
622
618 def _create_parser(self, aliases=None, flags=None):
623 def _create_parser(self, aliases=None, flags=None):
619 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
624 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
620 self._add_arguments(aliases, flags)
625 self._add_arguments(aliases, flags)
621
626
622 def _add_arguments(self, aliases=None, flags=None):
627 def _add_arguments(self, aliases=None, flags=None):
623 raise NotImplementedError("subclasses must implement _add_arguments")
628 raise NotImplementedError("subclasses must implement _add_arguments")
624
629
625 def _parse_args(self, args):
630 def _parse_args(self, args):
626 """self.parser->self.parsed_data"""
631 """self.parser->self.parsed_data"""
627 # decode sys.argv to support unicode command-line options
632 # decode sys.argv to support unicode command-line options
628 enc = DEFAULT_ENCODING
633 enc = DEFAULT_ENCODING
629 uargs = [py3compat.cast_unicode(a, enc) for a in args]
634 uargs = [py3compat.cast_unicode(a, enc) for a in args]
630 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
635 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
631
636
632 def _convert_to_config(self):
637 def _convert_to_config(self):
633 """self.parsed_data->self.config"""
638 """self.parsed_data->self.config"""
634 for k, v in vars(self.parsed_data).iteritems():
639 for k, v in vars(self.parsed_data).iteritems():
635 exec "self.config.%s = v"%k in locals(), globals()
640 exec "self.config.%s = v"%k in locals(), globals()
636
641
637 class KVArgParseConfigLoader(ArgParseConfigLoader):
642 class KVArgParseConfigLoader(ArgParseConfigLoader):
638 """A config loader that loads aliases and flags with argparse,
643 """A config loader that loads aliases and flags with argparse,
639 but will use KVLoader for the rest. This allows better parsing
644 but will use KVLoader for the rest. This allows better parsing
640 of common args, such as `ipython -c 'print 5'`, but still gets
645 of common args, such as `ipython -c 'print 5'`, but still gets
641 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
646 arbitrary config with `ipython --InteractiveShell.use_readline=False`"""
642
647
643 def _add_arguments(self, aliases=None, flags=None):
648 def _add_arguments(self, aliases=None, flags=None):
644 self.alias_flags = {}
649 self.alias_flags = {}
645 # print aliases, flags
650 # print aliases, flags
646 if aliases is None:
651 if aliases is None:
647 aliases = self.aliases
652 aliases = self.aliases
648 if flags is None:
653 if flags is None:
649 flags = self.flags
654 flags = self.flags
650 paa = self.parser.add_argument
655 paa = self.parser.add_argument
651 for key,value in aliases.iteritems():
656 for key,value in aliases.iteritems():
652 if key in flags:
657 if key in flags:
653 # flags
658 # flags
654 nargs = '?'
659 nargs = '?'
655 else:
660 else:
656 nargs = None
661 nargs = None
657 if len(key) is 1:
662 if len(key) is 1:
658 paa('-'+key, '--'+key, type=unicode, dest=value, nargs=nargs)
663 paa('-'+key, '--'+key, type=unicode, dest=value, nargs=nargs)
659 else:
664 else:
660 paa('--'+key, type=unicode, dest=value, nargs=nargs)
665 paa('--'+key, type=unicode, dest=value, nargs=nargs)
661 for key, (value, help) in flags.iteritems():
666 for key, (value, help) in flags.iteritems():
662 if key in self.aliases:
667 if key in self.aliases:
663 #
668 #
664 self.alias_flags[self.aliases[key]] = value
669 self.alias_flags[self.aliases[key]] = value
665 continue
670 continue
666 if len(key) is 1:
671 if len(key) is 1:
667 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
672 paa('-'+key, '--'+key, action='append_const', dest='_flags', const=value)
668 else:
673 else:
669 paa('--'+key, action='append_const', dest='_flags', const=value)
674 paa('--'+key, action='append_const', dest='_flags', const=value)
670
675
671 def _convert_to_config(self):
676 def _convert_to_config(self):
672 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
677 """self.parsed_data->self.config, parse unrecognized extra args via KVLoader."""
673 # remove subconfigs list from namespace before transforming the Namespace
678 # remove subconfigs list from namespace before transforming the Namespace
674 if '_flags' in self.parsed_data:
679 if '_flags' in self.parsed_data:
675 subcs = self.parsed_data._flags
680 subcs = self.parsed_data._flags
676 del self.parsed_data._flags
681 del self.parsed_data._flags
677 else:
682 else:
678 subcs = []
683 subcs = []
679
684
680 for k, v in vars(self.parsed_data).iteritems():
685 for k, v in vars(self.parsed_data).iteritems():
681 if v is None:
686 if v is None:
682 # it was a flag that shares the name of an alias
687 # it was a flag that shares the name of an alias
683 subcs.append(self.alias_flags[k])
688 subcs.append(self.alias_flags[k])
684 else:
689 else:
685 # eval the KV assignment
690 # eval the KV assignment
686 self._exec_config_str(k, v)
691 self._exec_config_str(k, v)
687
692
688 for subc in subcs:
693 for subc in subcs:
689 self._load_flag(subc)
694 self._load_flag(subc)
690
695
691 if self.extra_args:
696 if self.extra_args:
692 sub_parser = KeyValueConfigLoader()
697 sub_parser = KeyValueConfigLoader()
693 sub_parser.load_config(self.extra_args)
698 sub_parser.load_config(self.extra_args)
694 self.config._merge(sub_parser.config)
699 self.config.merge(sub_parser.config)
695 self.extra_args = sub_parser.extra_args
700 self.extra_args = sub_parser.extra_args
696
701
697
702
698 def load_pyconfig_files(config_files, path):
703 def load_pyconfig_files(config_files, path):
699 """Load multiple Python config files, merging each of them in turn.
704 """Load multiple Python config files, merging each of them in turn.
700
705
701 Parameters
706 Parameters
702 ==========
707 ==========
703 config_files : list of str
708 config_files : list of str
704 List of config files names to load and merge into the config.
709 List of config files names to load and merge into the config.
705 path : unicode
710 path : unicode
706 The full path to the location of the config files.
711 The full path to the location of the config files.
707 """
712 """
708 config = Config()
713 config = Config()
709 for cf in config_files:
714 for cf in config_files:
710 loader = PyFileConfigLoader(cf, path=path)
715 loader = PyFileConfigLoader(cf, path=path)
711 try:
716 try:
712 next_config = loader.load_config()
717 next_config = loader.load_config()
713 except ConfigFileNotFound:
718 except ConfigFileNotFound:
714 pass
719 pass
715 except:
720 except:
716 raise
721 raise
717 else:
722 else:
718 config._merge(next_config)
723 config.merge(next_config)
719 return config
724 return config
@@ -1,284 +1,284 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 Tests for IPython.config.loader
3 Tests for IPython.config.loader
4
4
5 Authors:
5 Authors:
6
6
7 * Brian Granger
7 * Brian Granger
8 * Fernando Perez (design help)
8 * Fernando Perez (design help)
9 """
9 """
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2011 The IPython Development Team
12 # Copyright (C) 2008-2011 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 import os
22 import os
23 import sys
23 import sys
24 from tempfile import mkstemp
24 from tempfile import mkstemp
25 from unittest import TestCase
25 from unittest import TestCase
26
26
27 from nose import SkipTest
27 from nose import SkipTest
28
28
29 from IPython.testing.tools import mute_warn
29 from IPython.testing.tools import mute_warn
30
30
31 from IPython.utils.traitlets import Unicode
31 from IPython.utils.traitlets import Unicode
32 from IPython.config.configurable import Configurable
32 from IPython.config.configurable import Configurable
33 from IPython.config.loader import (
33 from IPython.config.loader import (
34 Config,
34 Config,
35 PyFileConfigLoader,
35 PyFileConfigLoader,
36 KeyValueConfigLoader,
36 KeyValueConfigLoader,
37 ArgParseConfigLoader,
37 ArgParseConfigLoader,
38 KVArgParseConfigLoader,
38 KVArgParseConfigLoader,
39 ConfigError
39 ConfigError
40 )
40 )
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Actual tests
43 # Actual tests
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46
46
47 pyfile = """
47 pyfile = """
48 c = get_config()
48 c = get_config()
49 c.a=10
49 c.a=10
50 c.b=20
50 c.b=20
51 c.Foo.Bar.value=10
51 c.Foo.Bar.value=10
52 c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3
52 c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3
53 c.D.C.value='hi there'
53 c.D.C.value='hi there'
54 """
54 """
55
55
56 class TestPyFileCL(TestCase):
56 class TestPyFileCL(TestCase):
57
57
58 def test_basic(self):
58 def test_basic(self):
59 fd, fname = mkstemp('.py')
59 fd, fname = mkstemp('.py')
60 f = os.fdopen(fd, 'w')
60 f = os.fdopen(fd, 'w')
61 f.write(pyfile)
61 f.write(pyfile)
62 f.close()
62 f.close()
63 # Unlink the file
63 # Unlink the file
64 cl = PyFileConfigLoader(fname)
64 cl = PyFileConfigLoader(fname)
65 config = cl.load_config()
65 config = cl.load_config()
66 self.assertEqual(config.a, 10)
66 self.assertEqual(config.a, 10)
67 self.assertEqual(config.b, 20)
67 self.assertEqual(config.b, 20)
68 self.assertEqual(config.Foo.Bar.value, 10)
68 self.assertEqual(config.Foo.Bar.value, 10)
69 self.assertEqual(config.Foo.Bam.value, range(10))
69 self.assertEqual(config.Foo.Bam.value, range(10))
70 self.assertEqual(config.D.C.value, 'hi there')
70 self.assertEqual(config.D.C.value, 'hi there')
71
71
72 class MyLoader1(ArgParseConfigLoader):
72 class MyLoader1(ArgParseConfigLoader):
73 def _add_arguments(self, aliases=None, flags=None):
73 def _add_arguments(self, aliases=None, flags=None):
74 p = self.parser
74 p = self.parser
75 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
75 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
76 p.add_argument('-b', dest='MyClass.bar', type=int)
76 p.add_argument('-b', dest='MyClass.bar', type=int)
77 p.add_argument('-n', dest='n', action='store_true')
77 p.add_argument('-n', dest='n', action='store_true')
78 p.add_argument('Global.bam', type=str)
78 p.add_argument('Global.bam', type=str)
79
79
80 class MyLoader2(ArgParseConfigLoader):
80 class MyLoader2(ArgParseConfigLoader):
81 def _add_arguments(self, aliases=None, flags=None):
81 def _add_arguments(self, aliases=None, flags=None):
82 subparsers = self.parser.add_subparsers(dest='subparser_name')
82 subparsers = self.parser.add_subparsers(dest='subparser_name')
83 subparser1 = subparsers.add_parser('1')
83 subparser1 = subparsers.add_parser('1')
84 subparser1.add_argument('-x',dest='Global.x')
84 subparser1.add_argument('-x',dest='Global.x')
85 subparser2 = subparsers.add_parser('2')
85 subparser2 = subparsers.add_parser('2')
86 subparser2.add_argument('y')
86 subparser2.add_argument('y')
87
87
88 class TestArgParseCL(TestCase):
88 class TestArgParseCL(TestCase):
89
89
90 def test_basic(self):
90 def test_basic(self):
91 cl = MyLoader1()
91 cl = MyLoader1()
92 config = cl.load_config('-f hi -b 10 -n wow'.split())
92 config = cl.load_config('-f hi -b 10 -n wow'.split())
93 self.assertEqual(config.Global.foo, 'hi')
93 self.assertEqual(config.Global.foo, 'hi')
94 self.assertEqual(config.MyClass.bar, 10)
94 self.assertEqual(config.MyClass.bar, 10)
95 self.assertEqual(config.n, True)
95 self.assertEqual(config.n, True)
96 self.assertEqual(config.Global.bam, 'wow')
96 self.assertEqual(config.Global.bam, 'wow')
97 config = cl.load_config(['wow'])
97 config = cl.load_config(['wow'])
98 self.assertEqual(config.keys(), ['Global'])
98 self.assertEqual(config.keys(), ['Global'])
99 self.assertEqual(config.Global.keys(), ['bam'])
99 self.assertEqual(config.Global.keys(), ['bam'])
100 self.assertEqual(config.Global.bam, 'wow')
100 self.assertEqual(config.Global.bam, 'wow')
101
101
102 def test_add_arguments(self):
102 def test_add_arguments(self):
103 cl = MyLoader2()
103 cl = MyLoader2()
104 config = cl.load_config('2 frobble'.split())
104 config = cl.load_config('2 frobble'.split())
105 self.assertEqual(config.subparser_name, '2')
105 self.assertEqual(config.subparser_name, '2')
106 self.assertEqual(config.y, 'frobble')
106 self.assertEqual(config.y, 'frobble')
107 config = cl.load_config('1 -x frobble'.split())
107 config = cl.load_config('1 -x frobble'.split())
108 self.assertEqual(config.subparser_name, '1')
108 self.assertEqual(config.subparser_name, '1')
109 self.assertEqual(config.Global.x, 'frobble')
109 self.assertEqual(config.Global.x, 'frobble')
110
110
111 def test_argv(self):
111 def test_argv(self):
112 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
112 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
113 config = cl.load_config()
113 config = cl.load_config()
114 self.assertEqual(config.Global.foo, 'hi')
114 self.assertEqual(config.Global.foo, 'hi')
115 self.assertEqual(config.MyClass.bar, 10)
115 self.assertEqual(config.MyClass.bar, 10)
116 self.assertEqual(config.n, True)
116 self.assertEqual(config.n, True)
117 self.assertEqual(config.Global.bam, 'wow')
117 self.assertEqual(config.Global.bam, 'wow')
118
118
119
119
120 class TestKeyValueCL(TestCase):
120 class TestKeyValueCL(TestCase):
121 klass = KeyValueConfigLoader
121 klass = KeyValueConfigLoader
122
122
123 def test_basic(self):
123 def test_basic(self):
124 cl = self.klass()
124 cl = self.klass()
125 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
125 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
126 with mute_warn():
126 with mute_warn():
127 config = cl.load_config(argv)
127 config = cl.load_config(argv)
128 self.assertEqual(config.a, 10)
128 self.assertEqual(config.a, 10)
129 self.assertEqual(config.b, 20)
129 self.assertEqual(config.b, 20)
130 self.assertEqual(config.Foo.Bar.value, 10)
130 self.assertEqual(config.Foo.Bar.value, 10)
131 self.assertEqual(config.Foo.Bam.value, range(10))
131 self.assertEqual(config.Foo.Bam.value, range(10))
132 self.assertEqual(config.D.C.value, 'hi there')
132 self.assertEqual(config.D.C.value, 'hi there')
133
133
134 def test_expanduser(self):
134 def test_expanduser(self):
135 cl = self.klass()
135 cl = self.klass()
136 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
136 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
137 with mute_warn():
137 with mute_warn():
138 config = cl.load_config(argv)
138 config = cl.load_config(argv)
139 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
139 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
140 self.assertEqual(config.b, os.path.expanduser('~'))
140 self.assertEqual(config.b, os.path.expanduser('~'))
141 self.assertEqual(config.c, os.path.expanduser('~/'))
141 self.assertEqual(config.c, os.path.expanduser('~/'))
142 self.assertEqual(config.d, '~/')
142 self.assertEqual(config.d, '~/')
143
143
144 def test_extra_args(self):
144 def test_extra_args(self):
145 cl = self.klass()
145 cl = self.klass()
146 with mute_warn():
146 with mute_warn():
147 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
147 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
148 self.assertEqual(cl.extra_args, ['b', 'd'])
148 self.assertEqual(cl.extra_args, ['b', 'd'])
149 self.assertEqual(config.a, 5)
149 self.assertEqual(config.a, 5)
150 self.assertEqual(config.c, 10)
150 self.assertEqual(config.c, 10)
151 with mute_warn():
151 with mute_warn():
152 config = cl.load_config(['--', '--a=5', '--c=10'])
152 config = cl.load_config(['--', '--a=5', '--c=10'])
153 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
153 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
154
154
155 def test_unicode_args(self):
155 def test_unicode_args(self):
156 cl = self.klass()
156 cl = self.klass()
157 argv = [u'--a=épsîlön']
157 argv = [u'--a=épsîlön']
158 with mute_warn():
158 with mute_warn():
159 config = cl.load_config(argv)
159 config = cl.load_config(argv)
160 self.assertEqual(config.a, u'épsîlön')
160 self.assertEqual(config.a, u'épsîlön')
161
161
162 def test_unicode_bytes_args(self):
162 def test_unicode_bytes_args(self):
163 uarg = u'--a=é'
163 uarg = u'--a=é'
164 try:
164 try:
165 barg = uarg.encode(sys.stdin.encoding)
165 barg = uarg.encode(sys.stdin.encoding)
166 except (TypeError, UnicodeEncodeError):
166 except (TypeError, UnicodeEncodeError):
167 raise SkipTest("sys.stdin.encoding can't handle 'é'")
167 raise SkipTest("sys.stdin.encoding can't handle 'é'")
168
168
169 cl = self.klass()
169 cl = self.klass()
170 with mute_warn():
170 with mute_warn():
171 config = cl.load_config([barg])
171 config = cl.load_config([barg])
172 self.assertEqual(config.a, u'é')
172 self.assertEqual(config.a, u'é')
173
173
174 def test_unicode_alias(self):
174 def test_unicode_alias(self):
175 cl = self.klass()
175 cl = self.klass()
176 argv = [u'--a=épsîlön']
176 argv = [u'--a=épsîlön']
177 with mute_warn():
177 with mute_warn():
178 config = cl.load_config(argv, aliases=dict(a='A.a'))
178 config = cl.load_config(argv, aliases=dict(a='A.a'))
179 self.assertEqual(config.A.a, u'épsîlön')
179 self.assertEqual(config.A.a, u'épsîlön')
180
180
181
181
182 class TestArgParseKVCL(TestKeyValueCL):
182 class TestArgParseKVCL(TestKeyValueCL):
183 klass = KVArgParseConfigLoader
183 klass = KVArgParseConfigLoader
184
184
185 def test_expanduser2(self):
185 def test_expanduser2(self):
186 cl = self.klass()
186 cl = self.klass()
187 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
187 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
188 with mute_warn():
188 with mute_warn():
189 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
189 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
190 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
190 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
191 self.assertEqual(config.A.b, '~/1/2/3')
191 self.assertEqual(config.A.b, '~/1/2/3')
192
192
193 def test_eval(self):
193 def test_eval(self):
194 cl = self.klass()
194 cl = self.klass()
195 argv = ['-c', 'a=5']
195 argv = ['-c', 'a=5']
196 with mute_warn():
196 with mute_warn():
197 config = cl.load_config(argv, aliases=dict(c='A.c'))
197 config = cl.load_config(argv, aliases=dict(c='A.c'))
198 self.assertEqual(config.A.c, u"a=5")
198 self.assertEqual(config.A.c, u"a=5")
199
199
200
200
201 class TestConfig(TestCase):
201 class TestConfig(TestCase):
202
202
203 def test_setget(self):
203 def test_setget(self):
204 c = Config()
204 c = Config()
205 c.a = 10
205 c.a = 10
206 self.assertEqual(c.a, 10)
206 self.assertEqual(c.a, 10)
207 self.assertEqual('b' in c, False)
207 self.assertEqual('b' in c, False)
208
208
209 def test_auto_section(self):
209 def test_auto_section(self):
210 c = Config()
210 c = Config()
211 self.assertEqual('A' in c, True)
211 self.assertEqual('A' in c, True)
212 self.assertEqual(c._has_section('A'), False)
212 self.assertEqual(c._has_section('A'), False)
213 A = c.A
213 A = c.A
214 A.foo = 'hi there'
214 A.foo = 'hi there'
215 self.assertEqual(c._has_section('A'), True)
215 self.assertEqual(c._has_section('A'), True)
216 self.assertEqual(c.A.foo, 'hi there')
216 self.assertEqual(c.A.foo, 'hi there')
217 del c.A
217 del c.A
218 self.assertEqual(len(c.A.keys()),0)
218 self.assertEqual(len(c.A.keys()),0)
219
219
220 def test_merge_doesnt_exist(self):
220 def test_merge_doesnt_exist(self):
221 c1 = Config()
221 c1 = Config()
222 c2 = Config()
222 c2 = Config()
223 c2.bar = 10
223 c2.bar = 10
224 c2.Foo.bar = 10
224 c2.Foo.bar = 10
225 c1._merge(c2)
225 c1.merge(c2)
226 self.assertEqual(c1.Foo.bar, 10)
226 self.assertEqual(c1.Foo.bar, 10)
227 self.assertEqual(c1.bar, 10)
227 self.assertEqual(c1.bar, 10)
228 c2.Bar.bar = 10
228 c2.Bar.bar = 10
229 c1._merge(c2)
229 c1.merge(c2)
230 self.assertEqual(c1.Bar.bar, 10)
230 self.assertEqual(c1.Bar.bar, 10)
231
231
232 def test_merge_exists(self):
232 def test_merge_exists(self):
233 c1 = Config()
233 c1 = Config()
234 c2 = Config()
234 c2 = Config()
235 c1.Foo.bar = 10
235 c1.Foo.bar = 10
236 c1.Foo.bam = 30
236 c1.Foo.bam = 30
237 c2.Foo.bar = 20
237 c2.Foo.bar = 20
238 c2.Foo.wow = 40
238 c2.Foo.wow = 40
239 c1._merge(c2)
239 c1.merge(c2)
240 self.assertEqual(c1.Foo.bam, 30)
240 self.assertEqual(c1.Foo.bam, 30)
241 self.assertEqual(c1.Foo.bar, 20)
241 self.assertEqual(c1.Foo.bar, 20)
242 self.assertEqual(c1.Foo.wow, 40)
242 self.assertEqual(c1.Foo.wow, 40)
243 c2.Foo.Bam.bam = 10
243 c2.Foo.Bam.bam = 10
244 c1._merge(c2)
244 c1.merge(c2)
245 self.assertEqual(c1.Foo.Bam.bam, 10)
245 self.assertEqual(c1.Foo.Bam.bam, 10)
246
246
247 def test_deepcopy(self):
247 def test_deepcopy(self):
248 c1 = Config()
248 c1 = Config()
249 c1.Foo.bar = 10
249 c1.Foo.bar = 10
250 c1.Foo.bam = 30
250 c1.Foo.bam = 30
251 c1.a = 'asdf'
251 c1.a = 'asdf'
252 c1.b = range(10)
252 c1.b = range(10)
253 import copy
253 import copy
254 c2 = copy.deepcopy(c1)
254 c2 = copy.deepcopy(c1)
255 self.assertEqual(c1, c2)
255 self.assertEqual(c1, c2)
256 self.assertTrue(c1 is not c2)
256 self.assertTrue(c1 is not c2)
257 self.assertTrue(c1.Foo is not c2.Foo)
257 self.assertTrue(c1.Foo is not c2.Foo)
258
258
259 def test_builtin(self):
259 def test_builtin(self):
260 c1 = Config()
260 c1 = Config()
261 exec 'foo = True' in c1
261 exec 'foo = True' in c1
262 self.assertEqual(c1.foo, True)
262 self.assertEqual(c1.foo, True)
263 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
263 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
264
264
265 def test_fromdict(self):
265 def test_fromdict(self):
266 c1 = Config({'Foo' : {'bar' : 1}})
266 c1 = Config({'Foo' : {'bar' : 1}})
267 self.assertEqual(c1.Foo.__class__, Config)
267 self.assertEqual(c1.Foo.__class__, Config)
268 self.assertEqual(c1.Foo.bar, 1)
268 self.assertEqual(c1.Foo.bar, 1)
269
269
270 def test_fromdict_merge(self):
270 def test_fromdictmerge(self):
271 c1 = Config()
271 c1 = Config()
272 c2 = Config({'Foo' : {'bar' : 1}})
272 c2 = Config({'Foo' : {'bar' : 1}})
273 c1._merge(c2)
273 c1.merge(c2)
274 self.assertEqual(c1.Foo.__class__, Config)
274 self.assertEqual(c1.Foo.__class__, Config)
275 self.assertEqual(c1.Foo.bar, 1)
275 self.assertEqual(c1.Foo.bar, 1)
276
276
277 def test_fromdict_merge2(self):
277 def test_fromdictmerge2(self):
278 c1 = Config({'Foo' : {'baz' : 2}})
278 c1 = Config({'Foo' : {'baz' : 2}})
279 c2 = Config({'Foo' : {'bar' : 1}})
279 c2 = Config({'Foo' : {'bar' : 1}})
280 c1._merge(c2)
280 c1.merge(c2)
281 self.assertEqual(c1.Foo.__class__, Config)
281 self.assertEqual(c1.Foo.__class__, Config)
282 self.assertEqual(c1.Foo.bar, 1)
282 self.assertEqual(c1.Foo.bar, 1)
283 self.assertEqual(c1.Foo.baz, 2)
283 self.assertEqual(c1.Foo.baz, 2)
284 self.assertRaises(AttributeError, getattr, c2.Foo, 'baz')
284 self.assertRaises(AttributeError, getattr, c2.Foo, 'baz')
General Comments 0
You need to be logged in to leave comments. Login now