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