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