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