##// END OF EJS Templates
Added KeyValueConfigLoader with tests.
Brian Granger -
Show More
@@ -1,389 +1,451 b''
1 1 # -*- coding: utf-8 -*-
2 2 # coding: utf-8
3 3 """A simple configuration system.
4 4
5 5 Authors
6 6 -------
7 7 * Brian Granger
8 8 * Fernando Perez
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2009 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is in
15 15 # the file COPYING, distributed as part of this software.
16 16 #-----------------------------------------------------------------------------
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 import __builtin__
23 23 import os
24 24 import sys
25 25
26 26 from IPython.external import argparse
27 27 from IPython.utils.path import filefind
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # Exceptions
31 31 #-----------------------------------------------------------------------------
32 32
33 33
34 34 class ConfigError(Exception):
35 35 pass
36 36
37 37
38 38 class ConfigLoaderError(ConfigError):
39 39 pass
40 40
41 41 #-----------------------------------------------------------------------------
42 42 # Argparse fix
43 43 #-----------------------------------------------------------------------------
44 44
45 45 # Unfortunately argparse by default prints help messages to stderr instead of
46 46 # stdout. This makes it annoying to capture long help screens at the command
47 47 # line, since one must know how to pipe stderr, which many users don't know how
48 48 # to do. So we override the print_help method with one that defaults to
49 49 # stdout and use our class instead.
50 50
51 51 class ArgumentParser(argparse.ArgumentParser):
52 52 """Simple argparse subclass that prints help to stdout by default."""
53 53
54 54 def print_help(self, file=None):
55 55 if file is None:
56 56 file = sys.stdout
57 57 return super(ArgumentParser, self).print_help(file)
58 58
59 59 print_help.__doc__ = argparse.ArgumentParser.print_help.__doc__
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Config class for holding config information
63 63 #-----------------------------------------------------------------------------
64 64
65 65
66 66 class Config(dict):
67 67 """An attribute based dict that can do smart merges."""
68 68
69 69 def __init__(self, *args, **kwds):
70 70 dict.__init__(self, *args, **kwds)
71 71 # This sets self.__dict__ = self, but it has to be done this way
72 72 # because we are also overriding __setattr__.
73 73 dict.__setattr__(self, '__dict__', self)
74 74
75 75 def _merge(self, other):
76 76 to_update = {}
77 77 for k, v in other.iteritems():
78 78 if not self.has_key(k):
79 79 to_update[k] = v
80 80 else: # I have this key
81 81 if isinstance(v, Config):
82 82 # Recursively merge common sub Configs
83 83 self[k]._merge(v)
84 84 else:
85 85 # Plain updates for non-Configs
86 86 to_update[k] = v
87 87
88 88 self.update(to_update)
89 89
90 90 def _is_section_key(self, key):
91 91 if key[0].upper()==key[0] and not key.startswith('_'):
92 92 return True
93 93 else:
94 94 return False
95 95
96 96 def __contains__(self, key):
97 97 if self._is_section_key(key):
98 98 return True
99 99 else:
100 100 return super(Config, self).__contains__(key)
101 101 # .has_key is deprecated for dictionaries.
102 102 has_key = __contains__
103 103
104 104 def _has_section(self, key):
105 105 if self._is_section_key(key):
106 106 if super(Config, self).__contains__(key):
107 107 return True
108 108 return False
109 109
110 110 def copy(self):
111 111 return type(self)(dict.copy(self))
112 112
113 113 def __copy__(self):
114 114 return self.copy()
115 115
116 116 def __deepcopy__(self, memo):
117 117 import copy
118 118 return type(self)(copy.deepcopy(self.items()))
119 119
120 120 def __getitem__(self, key):
121 121 # We cannot use directly self._is_section_key, because it triggers
122 122 # infinite recursion on top of PyPy. Instead, we manually fish the
123 123 # bound method.
124 124 is_section_key = self.__class__._is_section_key.__get__(self)
125 125
126 126 # Because we use this for an exec namespace, we need to delegate
127 127 # the lookup of names in __builtin__ to itself. This means
128 128 # that you can't have section or attribute names that are
129 129 # builtins.
130 130 try:
131 131 return getattr(__builtin__, key)
132 132 except AttributeError:
133 133 pass
134 134 if is_section_key(key):
135 135 try:
136 136 return dict.__getitem__(self, key)
137 137 except KeyError:
138 138 c = Config()
139 139 dict.__setitem__(self, key, c)
140 140 return c
141 141 else:
142 142 return dict.__getitem__(self, key)
143 143
144 144 def __setitem__(self, key, value):
145 145 # Don't allow names in __builtin__ to be modified.
146 146 if hasattr(__builtin__, key):
147 147 raise ConfigError('Config variable names cannot have the same name '
148 148 'as a Python builtin: %s' % key)
149 149 if self._is_section_key(key):
150 150 if not isinstance(value, Config):
151 151 raise ValueError('values whose keys begin with an uppercase '
152 152 'char must be Config instances: %r, %r' % (key, value))
153 153 else:
154 154 dict.__setitem__(self, key, value)
155 155
156 156 def __getattr__(self, key):
157 157 try:
158 158 return self.__getitem__(key)
159 159 except KeyError, e:
160 160 raise AttributeError(e)
161 161
162 162 def __setattr__(self, key, value):
163 163 try:
164 164 self.__setitem__(key, value)
165 165 except KeyError, e:
166 166 raise AttributeError(e)
167 167
168 168 def __delattr__(self, key):
169 169 try:
170 170 dict.__delitem__(self, key)
171 171 except KeyError, e:
172 172 raise AttributeError(e)
173 173
174 174
175 175 #-----------------------------------------------------------------------------
176 176 # Config loading classes
177 177 #-----------------------------------------------------------------------------
178 178
179 179
180 180 class ConfigLoader(object):
181 181 """A object for loading configurations from just about anywhere.
182 182
183 183 The resulting configuration is packaged as a :class:`Struct`.
184 184
185 185 Notes
186 186 -----
187 187 A :class:`ConfigLoader` does one thing: load a config from a source
188 188 (file, command line arguments) and returns the data as a :class:`Struct`.
189 189 There are lots of things that :class:`ConfigLoader` does not do. It does
190 190 not implement complex logic for finding config files. It does not handle
191 191 default values or merge multiple configs. These things need to be
192 192 handled elsewhere.
193 193 """
194 194
195 195 def __init__(self):
196 196 """A base class for config loaders.
197 197
198 198 Examples
199 199 --------
200 200
201 201 >>> cl = ConfigLoader()
202 202 >>> config = cl.load_config()
203 203 >>> config
204 204 {}
205 205 """
206 206 self.clear()
207 207
208 208 def clear(self):
209 209 self.config = Config()
210 210
211 211 def load_config(self):
212 212 """Load a config from somewhere, return a :class:`Config` instance.
213 213
214 214 Usually, this will cause self.config to be set and then returned.
215 215 However, in most cases, :meth:`ConfigLoader.clear` should be called
216 216 to erase any previous state.
217 217 """
218 218 self.clear()
219 219 return self.config
220 220
221 221
222 222 class FileConfigLoader(ConfigLoader):
223 223 """A base class for file based configurations.
224 224
225 225 As we add more file based config loaders, the common logic should go
226 226 here.
227 227 """
228 228 pass
229 229
230 230
231 231 class PyFileConfigLoader(FileConfigLoader):
232 232 """A config loader for pure python files.
233 233
234 234 This calls execfile on a plain python file and looks for attributes
235 235 that are all caps. These attribute are added to the config Struct.
236 236 """
237 237
238 238 def __init__(self, filename, path=None):
239 239 """Build a config loader for a filename and path.
240 240
241 241 Parameters
242 242 ----------
243 243 filename : str
244 244 The file name of the config file.
245 245 path : str, list, tuple
246 246 The path to search for the config file on, or a sequence of
247 247 paths to try in order.
248 248 """
249 249 super(PyFileConfigLoader, self).__init__()
250 250 self.filename = filename
251 251 self.path = path
252 252 self.full_filename = ''
253 253 self.data = None
254 254
255 255 def load_config(self):
256 256 """Load the config from a file and return it as a Struct."""
257 257 self.clear()
258 258 self._find_file()
259 259 self._read_file_as_dict()
260 260 self._convert_to_config()
261 261 return self.config
262 262
263 263 def _find_file(self):
264 264 """Try to find the file by searching the paths."""
265 265 self.full_filename = filefind(self.filename, self.path)
266 266
267 267 def _read_file_as_dict(self):
268 268 """Load the config file into self.config, with recursive loading."""
269 269 # This closure is made available in the namespace that is used
270 270 # to exec the config file. This allows users to call
271 271 # load_subconfig('myconfig.py') to load config files recursively.
272 272 # It needs to be a closure because it has references to self.path
273 273 # and self.config. The sub-config is loaded with the same path
274 274 # as the parent, but it uses an empty config which is then merged
275 275 # with the parents.
276 276 def load_subconfig(fname):
277 277 loader = PyFileConfigLoader(fname, self.path)
278 278 try:
279 279 sub_config = loader.load_config()
280 280 except IOError:
281 281 # Pass silently if the sub config is not there. This happens
282 282 # when a user us using a profile, but not the default config.
283 283 pass
284 284 else:
285 285 self.config._merge(sub_config)
286 286
287 287 # Again, this needs to be a closure and should be used in config
288 288 # files to get the config being loaded.
289 289 def get_config():
290 290 return self.config
291 291
292 292 namespace = dict(load_subconfig=load_subconfig, get_config=get_config)
293 293 fs_encoding = sys.getfilesystemencoding() or 'ascii'
294 294 conf_filename = self.full_filename.encode(fs_encoding)
295 295 execfile(conf_filename, namespace)
296 296
297 297 def _convert_to_config(self):
298 298 if self.data is None:
299 299 ConfigLoaderError('self.data does not exist')
300 300
301 301
302 302 class CommandLineConfigLoader(ConfigLoader):
303 303 """A config loader for command line arguments.
304 304
305 305 As we add more command line based loaders, the common logic should go
306 306 here.
307 307 """
308 308
309 309
310 class KeyValueConfigLoader(CommandLineConfigLoader):
311 """A config loader that loads key value pairs from the command line.
312
313 This allows command line options to be gives in the following form::
314
315 ipython Global.profile="foo" InteractiveShell.autocall=False
316 """
317
318 def __init__(self, argv=None):
319 """Create a key value pair config loader.
320
321 Parameters
322 ----------
323 argv : list
324 A list that has the form of sys.argv[1:] which has unicode
325 elements of the form u"key=value". If this is None (default),
326 then sys.argv[1:] will be used.
327
328 Returns
329 -------
330 config : Config
331 The resulting Config object.
332
333 Examples
334 --------
335
336 >>> from IPython.config.loader import KeyValueConfigLoader
337 >>> cl = KeyValueConfigLoader()
338 >>> cl.load_config(["foo='bar'","A.name='brian'","B.number=0"])
339 {'A': {'name': 'brian'}, 'B': {'number': 0}, 'foo': 'bar'}
340 """
341 if argv == None:
342 argv = sys.argv[1:]
343 self.argv = argv
344
345 def load_config(self, argv=None):
346 """Parse the configuration and generate the Config object.
347
348 Parameters
349 ----------
350 argv : list, optional
351 A list that has the form of sys.argv[1:] which has unicode
352 elements of the form u"key=value". If this is None (default),
353 then self.argv will be used.
354 """
355 self.clear()
356 if argv is None:
357 argv = self.argv
358 for item in argv:
359 pair = tuple(item.split("="))
360 if len(pair) == 2:
361 exec_str = 'self.config.' + pair[0] + '=' + pair[1]
362 exec exec_str in locals(), globals()
363 return self.config
364
365
310 366 class ArgParseConfigLoader(CommandLineConfigLoader):
367 """A loader that uses the argparse module to load from the command line."""
311 368
312 369 def __init__(self, argv=None, *parser_args, **parser_kw):
313 370 """Create a config loader for use with argparse.
314 371
315 372 Parameters
316 373 ----------
317 374
318 375 argv : optional, list
319 376 If given, used to read command-line arguments from, otherwise
320 377 sys.argv[1:] is used.
321 378
322 379 parser_args : tuple
323 380 A tuple of positional arguments that will be passed to the
324 381 constructor of :class:`argparse.ArgumentParser`.
325 382
326 383 parser_kw : dict
327 384 A tuple of keyword arguments that will be passed to the
328 385 constructor of :class:`argparse.ArgumentParser`.
386
387 Returns
388 -------
389 config : Config
390 The resulting Config object.
329 391 """
330 392 super(CommandLineConfigLoader, self).__init__()
331 393 if argv == None:
332 394 argv = sys.argv[1:]
333 395 self.argv = argv
334 396 self.parser_args = parser_args
335 397 self.version = parser_kw.pop("version", None)
336 398 kwargs = dict(argument_default=argparse.SUPPRESS)
337 399 kwargs.update(parser_kw)
338 400 self.parser_kw = kwargs
339 401
340 def load_config(self, args=None):
341 """Parse command line arguments and return as a Struct.
402 def load_config(self, argv=None):
403 """Parse command line arguments and return as a Config object.
342 404
343 405 Parameters
344 406 ----------
345 407
346 408 args : optional, list
347 409 If given, a list with the structure of sys.argv[1:] to parse
348 410 arguments from. If not given, the instance's self.argv attribute
349 411 (given at construction time) is used."""
350 412 self.clear()
351 if args is None:
352 args = self.argv
413 if argv is None:
414 argv = self.argv
353 415 self._create_parser()
354 self._parse_args(args)
416 self._parse_args(argv)
355 417 self._convert_to_config()
356 418 return self.config
357 419
358 420 def get_extra_args(self):
359 421 if hasattr(self, 'extra_args'):
360 422 return self.extra_args
361 423 else:
362 424 return []
363 425
364 426 def _create_parser(self):
365 427 self.parser = ArgumentParser(*self.parser_args, **self.parser_kw)
366 428 self._add_arguments()
367 429
368 430 def _add_arguments(self):
369 431 raise NotImplementedError("subclasses must implement _add_arguments")
370 432
371 433 def _parse_args(self, args):
372 434 """self.parser->self.parsed_data"""
373 435 # decode sys.argv to support unicode command-line options
374 436 uargs = []
375 437 for a in args:
376 438 if isinstance(a, str):
377 439 # don't decode if we already got unicode
378 440 a = a.decode(sys.stdin.encoding or
379 441 sys.getdefaultencoding())
380 442 uargs.append(a)
381 443 self.parsed_data, self.extra_args = self.parser.parse_known_args(uargs)
382 444
383 445 def _convert_to_config(self):
384 446 """self.parsed_data->self.config"""
385 447 for k, v in vars(self.parsed_data).iteritems():
386 448 exec_str = 'self.config.' + k + '= v'
387 449 exec exec_str in locals(), globals()
388 450
389 451
@@ -1,174 +1,188 b''
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 Tests for IPython.config.loader
5 5
6 6 Authors:
7 7
8 8 * Brian Granger
9 9 * Fernando Perez (design help)
10 10 """
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Copyright (C) 2008-2009 The IPython Development Team
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import os
24 24 from tempfile import mkstemp
25 25 from unittest import TestCase
26 26
27 27 from IPython.config.loader import (
28 28 Config,
29 PyFileConfigLoader,
29 PyFileConfigLoader,
30 KeyValueConfigLoader,
30 31 ArgParseConfigLoader,
31 32 ConfigError
32 33 )
33 34
34 35 #-----------------------------------------------------------------------------
35 36 # Actual tests
36 37 #-----------------------------------------------------------------------------
37 38
38 39
39 40 pyfile = """
40 41 c = get_config()
41 c.a = 10
42 c.b = 20
43 c.Foo.Bar.value = 10
44 c.Foo.Bam.value = range(10)
45 c.D.C.value = 'hi there'
42 c.a=10
43 c.b=20
44 c.Foo.Bar.value=10
45 c.Foo.Bam.value=range(10)
46 c.D.C.value='hi there'
46 47 """
47 48
48 49 class TestPyFileCL(TestCase):
49 50
50 51 def test_basic(self):
51 52 fd, fname = mkstemp('.py')
52 53 f = os.fdopen(fd, 'w')
53 54 f.write(pyfile)
54 55 f.close()
55 56 # Unlink the file
56 57 cl = PyFileConfigLoader(fname)
57 58 config = cl.load_config()
58 59 self.assertEquals(config.a, 10)
59 60 self.assertEquals(config.b, 20)
60 61 self.assertEquals(config.Foo.Bar.value, 10)
61 62 self.assertEquals(config.Foo.Bam.value, range(10))
62 63 self.assertEquals(config.D.C.value, 'hi there')
63 64
64 65 class MyLoader1(ArgParseConfigLoader):
65 66 def _add_arguments(self):
66 67 p = self.parser
67 68 p.add_argument('-f', '--foo', dest='Global.foo', type=str)
68 69 p.add_argument('-b', dest='MyClass.bar', type=int)
69 70 p.add_argument('-n', dest='n', action='store_true')
70 71 p.add_argument('Global.bam', type=str)
71 72
72 73 class MyLoader2(ArgParseConfigLoader):
73 74 def _add_arguments(self):
74 75 subparsers = self.parser.add_subparsers(dest='subparser_name')
75 76 subparser1 = subparsers.add_parser('1')
76 77 subparser1.add_argument('-x',dest='Global.x')
77 78 subparser2 = subparsers.add_parser('2')
78 79 subparser2.add_argument('y')
79 80
80 81 class TestArgParseCL(TestCase):
81 82
82 83 def test_basic(self):
83 84 cl = MyLoader1()
84 85 config = cl.load_config('-f hi -b 10 -n wow'.split())
85 86 self.assertEquals(config.Global.foo, 'hi')
86 87 self.assertEquals(config.MyClass.bar, 10)
87 88 self.assertEquals(config.n, True)
88 89 self.assertEquals(config.Global.bam, 'wow')
89 90 config = cl.load_config(['wow'])
90 91 self.assertEquals(config.keys(), ['Global'])
91 92 self.assertEquals(config.Global.keys(), ['bam'])
92 93 self.assertEquals(config.Global.bam, 'wow')
93 94
94 95 def test_add_arguments(self):
95 96 cl = MyLoader2()
96 97 config = cl.load_config('2 frobble'.split())
97 98 self.assertEquals(config.subparser_name, '2')
98 99 self.assertEquals(config.y, 'frobble')
99 100 config = cl.load_config('1 -x frobble'.split())
100 101 self.assertEquals(config.subparser_name, '1')
101 102 self.assertEquals(config.Global.x, 'frobble')
102 103
103 104 def test_argv(self):
104 105 cl = MyLoader1(argv='-f hi -b 10 -n wow'.split())
105 106 config = cl.load_config()
106 107 self.assertEquals(config.Global.foo, 'hi')
107 108 self.assertEquals(config.MyClass.bar, 10)
108 109 self.assertEquals(config.n, True)
109 110 self.assertEquals(config.Global.bam, 'wow')
110 111
111 112
113 class TestKeyValueCL(TestCase):
114
115 def test_basic(self):
116 cl = KeyValueConfigLoader()
117 argv = [s.strip('c.') for s in pyfile.split('\n')[2:-1]]
118 print argv
119 config = cl.load_config(argv)
120 self.assertEquals(config.a, 10)
121 self.assertEquals(config.b, 20)
122 self.assertEquals(config.Foo.Bar.value, 10)
123 self.assertEquals(config.Foo.Bam.value, range(10))
124 self.assertEquals(config.D.C.value, 'hi there')
125
112 126 class TestConfig(TestCase):
113 127
114 128 def test_setget(self):
115 129 c = Config()
116 130 c.a = 10
117 131 self.assertEquals(c.a, 10)
118 132 self.assertEquals(c.has_key('b'), False)
119 133
120 134 def test_auto_section(self):
121 135 c = Config()
122 136 self.assertEquals(c.has_key('A'), True)
123 137 self.assertEquals(c._has_section('A'), False)
124 138 A = c.A
125 139 A.foo = 'hi there'
126 140 self.assertEquals(c._has_section('A'), True)
127 141 self.assertEquals(c.A.foo, 'hi there')
128 142 del c.A
129 143 self.assertEquals(len(c.A.keys()),0)
130 144
131 145 def test_merge_doesnt_exist(self):
132 146 c1 = Config()
133 147 c2 = Config()
134 148 c2.bar = 10
135 149 c2.Foo.bar = 10
136 150 c1._merge(c2)
137 151 self.assertEquals(c1.Foo.bar, 10)
138 152 self.assertEquals(c1.bar, 10)
139 153 c2.Bar.bar = 10
140 154 c1._merge(c2)
141 155 self.assertEquals(c1.Bar.bar, 10)
142 156
143 157 def test_merge_exists(self):
144 158 c1 = Config()
145 159 c2 = Config()
146 160 c1.Foo.bar = 10
147 161 c1.Foo.bam = 30
148 162 c2.Foo.bar = 20
149 163 c2.Foo.wow = 40
150 164 c1._merge(c2)
151 165 self.assertEquals(c1.Foo.bam, 30)
152 166 self.assertEquals(c1.Foo.bar, 20)
153 167 self.assertEquals(c1.Foo.wow, 40)
154 168 c2.Foo.Bam.bam = 10
155 169 c1._merge(c2)
156 170 self.assertEquals(c1.Foo.Bam.bam, 10)
157 171
158 172 def test_deepcopy(self):
159 173 c1 = Config()
160 174 c1.Foo.bar = 10
161 175 c1.Foo.bam = 30
162 176 c1.a = 'asdf'
163 177 c1.b = range(10)
164 178 import copy
165 179 c2 = copy.deepcopy(c1)
166 180 self.assertEquals(c1, c2)
167 181 self.assert_(c1 is not c2)
168 182 self.assert_(c1.Foo is not c2.Foo)
169 183
170 184 def test_builtin(self):
171 185 c1 = Config()
172 186 exec 'foo = True' in c1
173 187 self.assertEquals(c1.foo, True)
174 188 self.assertRaises(ConfigError, setattr, c1, 'ValueError', 10)
General Comments 0
You need to be logged in to leave comments. Login now