##// END OF EJS Templates
allow to load config from json file...
Matthias BUSSONNIER -
Show More
@@ -0,0 +1,3 b''
1 * IPython config objects can be loaded from and serialized to JSON.
2 JSON config file have the same base name as their ``.py`` counterpart,
3 and will be loaded with higher priority if found.
@@ -31,7 +31,7 b' from IPython.external.decorator import decorator'
31 31
32 32 from IPython.config.configurable import SingletonConfigurable
33 33 from IPython.config.loader import (
34 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound,
34 KVArgParseConfigLoader, PyFileConfigLoader, Config, ArgumentError, ConfigFileNotFound, JSONFileConfigLoader
35 35 )
36 36
37 37 from IPython.utils.traitlets import (
@@ -492,34 +492,52 b' class Application(SingletonConfigurable):'
492 492
493 493 # flatten flags&aliases, so cl-args get appropriate priority:
494 494 flags,aliases = self.flatten_flags()
495
496 495 loader = KVArgParseConfigLoader(argv=argv, aliases=aliases,
497 flags=flags)
496 flags=flags, log=self.log)
498 497 config = loader.load_config()
499 498 self.update_config(config)
500 499 # store unparsed args in extra_args
501 500 self.extra_args = loader.extra_args
502 501
502 @classmethod
503 def _load_config_file(cls, basefilename, path=None, log=None):
504 """Load config files (json/py) by filename and path."""
505
506 pyloader = PyFileConfigLoader(basefilename+'.py', path=path, log=log)
507 jsonloader = JSONFileConfigLoader(basefilename+'.json', path=path, log=log)
508 config_found = False
509 config = None
510 for loader in [pyloader, jsonloader]:
511 try:
512 config = loader.load_config()
513 config_found = True
514 except ConfigFileNotFound:
515 pass
516 except Exception:
517 # try to get the full filename, but it will be empty in the
518 # unlikely event that the error raised before filefind finished
519 filename = loader.full_filename or filename
520 # problem while running the file
521 log.error("Exception while loading config file %s",
522 filename, exc_info=True)
523 else:
524 log.debug("Loaded config file: %s", loader.full_filename)
525 if config :
526 yield config
527
528 if not config_found :
529 raise ConfigFileNotFound('Neither .json, not .py file found.')
530 raise StopIteration
531
532
503 533 @catch_config_error
504 534 def load_config_file(self, filename, path=None):
505 """Load a .py based config file by filename and path."""
506 loader = PyFileConfigLoader(filename, path=path)
507 try:
508 config = loader.load_config()
509 except ConfigFileNotFound:
510 # problem finding the file, raise
511 raise
512 except Exception:
513 # try to get the full filename, but it will be empty in the
514 # unlikely event that the error raised before filefind finished
515 filename = loader.full_filename or filename
516 # problem while running the file
517 self.log.error("Exception while loading config file %s",
518 filename, exc_info=True)
519 else:
520 self.log.debug("Loaded config file: %s", loader.full_filename)
535 """Load config files (json/py) by filename and path."""
536 filename, ext = os.path.splitext(filename)
537 for config in self._load_config_file(filename, path=path , log=self.log):
521 538 self.update_config(config)
522 539
540
523 541 def generate_config_file(self):
524 542 """generate default config file from Configurables"""
525 543 lines = ["# Configuration file for %s."%self.name]
@@ -28,9 +28,10 b' import copy'
28 28 import os
29 29 import re
30 30 import sys
31 import json
31 32
32 33 from IPython.utils.path import filefind, get_ipython_dir
33 from IPython.utils import py3compat, warn
34 from IPython.utils import py3compat
34 35 from IPython.utils.encoding import DEFAULT_ENCODING
35 36 from IPython.utils.py3compat import unicode_type, iteritems
36 37 from IPython.utils.traitlets import HasTraits, List, Any, TraitError
@@ -303,9 +304,17 b' class ConfigLoader(object):'
303 304 handled elsewhere.
304 305 """
305 306
306 def __init__(self):
307 def _log_default(self):
308 from IPython.config.application import Application
309 return Application.instance().log
310
311 def __init__(self, log=None):
307 312 """A base class for config loaders.
308 313
314 log : instance of :class:`logging.Logger` to use.
315 By default loger of :meth:`IPython.config.application.Application.instance()`
316 will be used
317
309 318 Examples
310 319 --------
311 320
@@ -315,6 +324,11 b' class ConfigLoader(object):'
315 324 {}
316 325 """
317 326 self.clear()
327 if log is None :
328 self.log = self._log_default()
329 self.log.debug('Using default logger')
330 else :
331 self.log = log
318 332
319 333 def clear(self):
320 334 self.config = Config()
@@ -336,17 +350,8 b' class FileConfigLoader(ConfigLoader):'
336 350 As we add more file based config loaders, the common logic should go
337 351 here.
338 352 """
339 pass
340
341
342 class PyFileConfigLoader(FileConfigLoader):
343 """A config loader for pure python files.
344
345 This calls execfile on a plain python file and looks for attributes
346 that are all caps. These attribute are added to the config Struct.
347 """
348 353
349 def __init__(self, filename, path=None):
354 def __init__(self, filename, path=None, **kw):
350 355 """Build a config loader for a filename and path.
351 356
352 357 Parameters
@@ -357,11 +362,53 b' class PyFileConfigLoader(FileConfigLoader):'
357 362 The path to search for the config file on, or a sequence of
358 363 paths to try in order.
359 364 """
360 super(PyFileConfigLoader, self).__init__()
365 super(FileConfigLoader, self).__init__(**kw)
361 366 self.filename = filename
362 367 self.path = path
363 368 self.full_filename = ''
364 self.data = None
369
370 def _find_file(self):
371 """Try to find the file by searching the paths."""
372 self.full_filename = filefind(self.filename, self.path)
373
374 class JSONFileConfigLoader(FileConfigLoader):
375 """A Json file loader for config"""
376
377 def load_config(self):
378 """Load the config from a file and return it as a Struct."""
379 self.clear()
380 try:
381 self._find_file()
382 except IOError as e:
383 raise ConfigFileNotFound(str(e))
384 dct = self._read_file_as_dict()
385 self.config = self._convert_to_config(dct)
386 return self.config
387
388 def _read_file_as_dict(self):
389 with open(self.full_filename) as f :
390 return json.load(f)
391
392 def _convert_to_config(self, dictionary):
393 if 'version' in dictionary:
394 version = dictionary.pop('version')
395 else :
396 version = 1
397 self.log.warn("Unrecognized JSON config file version, assuming version : {}".format(version))
398
399 if version == 1:
400 return Config(dictionary)
401 else :
402 raise ValueError('Unknown version of JSON config file : version number {version}'.format(version=version))
403
404
405 class PyFileConfigLoader(FileConfigLoader):
406 """A config loader for pure python files.
407
408 This is responsible for locating a Python config file by filename and
409 profile name, then executing it in a namespace where it could have access
410 to subconfigs.
411 """
365 412
366 413 def load_config(self):
367 414 """Load the config from a file and return it as a Struct."""
@@ -371,12 +418,8 b' class PyFileConfigLoader(FileConfigLoader):'
371 418 except IOError as e:
372 419 raise ConfigFileNotFound(str(e))
373 420 self._read_file_as_dict()
374 self._convert_to_config()
375 421 return self.config
376 422
377 def _find_file(self):
378 """Try to find the file by searching the paths."""
379 self.full_filename = filefind(self.filename, self.path)
380 423
381 424 def _read_file_as_dict(self):
382 425 """Load the config file into self.config, with recursive loading."""
@@ -429,10 +472,6 b' class PyFileConfigLoader(FileConfigLoader):'
429 472 conf_filename = self.full_filename.encode(fs_encoding)
430 473 py3compat.execfile(conf_filename, namespace)
431 474
432 def _convert_to_config(self):
433 if self.data is None:
434 ConfigLoaderError('self.data does not exist')
435
436 475
437 476 class CommandLineConfigLoader(ConfigLoader):
438 477 """A config loader for command line arguments.
@@ -497,7 +536,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):'
497 536 ipython --profile="foo" --InteractiveShell.autocall=False
498 537 """
499 538
500 def __init__(self, argv=None, aliases=None, flags=None):
539 def __init__(self, argv=None, aliases=None, flags=None, **kw):
501 540 """Create a key value pair config loader.
502 541
503 542 Parameters
@@ -529,7 +568,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):'
529 568 >>> sorted(d.items())
530 569 [('A', {'name': 'brian'}), ('B', {'number': 0})]
531 570 """
532 self.clear()
571 super(KeyValueConfigLoader, self).__init__(**kw)
533 572 if argv is None:
534 573 argv = sys.argv[1:]
535 574 self.argv = argv
@@ -606,7 +645,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):'
606 645 lhs = aliases[lhs]
607 646 if '.' not in lhs:
608 647 # probably a mistyped alias, but not technically illegal
609 warn.warn("Unrecognized alias: '%s', it will probably have no effect."%lhs)
648 self.log.warn("Unrecognized alias: '%s', it will probably have no effect. %s,-- %s"%(lhs,raw, aliases))
610 649 try:
611 650 self._exec_config_str(lhs, rhs)
612 651 except Exception:
@@ -633,7 +672,7 b' class KeyValueConfigLoader(CommandLineConfigLoader):'
633 672 class ArgParseConfigLoader(CommandLineConfigLoader):
634 673 """A loader that uses the argparse module to load from the command line."""
635 674
636 def __init__(self, argv=None, aliases=None, flags=None, *parser_args, **parser_kw):
675 def __init__(self, argv=None, aliases=None, flags=None, log=None, *parser_args, **parser_kw):
637 676 """Create a config loader for use with argparse.
638 677
639 678 Parameters
@@ -656,7 +695,7 b' class ArgParseConfigLoader(CommandLineConfigLoader):'
656 695 config : Config
657 696 The resulting Config object.
658 697 """
659 super(CommandLineConfigLoader, self).__init__()
698 super(CommandLineConfigLoader, self).__init__(log=log)
660 699 self.clear()
661 700 if argv is None:
662 701 argv = sys.argv[1:]
@@ -772,7 +811,7 b' class KVArgParseConfigLoader(ArgParseConfigLoader):'
772 811 self._load_flag(subc)
773 812
774 813 if self.extra_args:
775 sub_parser = KeyValueConfigLoader()
814 sub_parser = KeyValueConfigLoader(log=self.log)
776 815 sub_parser.load_config(self.extra_args)
777 816 self.config.merge(sub_parser.config)
778 817 self.extra_args = sub_parser.extra_args
@@ -22,17 +22,21 b' Authors:'
22 22 import os
23 23 import pickle
24 24 import sys
25 import json
26
25 27 from tempfile import mkstemp
26 28 from unittest import TestCase
27 29
28 30 from nose import SkipTest
31 import nose.tools as nt
32
29 33
30 from IPython.testing.tools import mute_warn
31 34
32 35 from IPython.config.loader import (
33 36 Config,
34 37 LazyConfigValue,
35 38 PyFileConfigLoader,
39 JSONFileConfigLoader,
36 40 KeyValueConfigLoader,
37 41 ArgParseConfigLoader,
38 42 KVArgParseConfigLoader,
@@ -53,21 +57,77 b" c.Foo.Bam.value=list(range(10)) # list() is just so it's the same on Python 3"
53 57 c.D.C.value='hi there'
54 58 """
55 59
56 class TestPyFileCL(TestCase):
60 json1file = """
61 {
62 "version": 1,
63 "a": 10,
64 "b": 20,
65 "Foo": {
66 "Bam": {
67 "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
68 },
69 "Bar": {
70 "value": 10
71 }
72 },
73 "D": {
74 "C": {
75 "value": "hi there"
76 }
77 }
78 }
79 """
57 80
58 def test_basic(self):
81 # should not load
82 json2file = """
83 {
84 "version": 2
85 }
86 """
87
88 import logging
89 log = logging.getLogger('devnull')
90 log.setLevel(0)
91
92 class TestFileCL(TestCase):
93
94 def _check_conf(self, config):
95 self.assertEqual(config.a, 10)
96 self.assertEqual(config.b, 20)
97 self.assertEqual(config.Foo.Bar.value, 10)
98 self.assertEqual(config.Foo.Bam.value, list(range(10)))
99 self.assertEqual(config.D.C.value, 'hi there')
100
101 def test_python(self):
59 102 fd, fname = mkstemp('.py')
60 103 f = os.fdopen(fd, 'w')
61 104 f.write(pyfile)
62 105 f.close()
63 106 # Unlink the file
64 cl = PyFileConfigLoader(fname)
107 cl = PyFileConfigLoader(fname, log=log)
65 108 config = cl.load_config()
66 self.assertEqual(config.a, 10)
67 self.assertEqual(config.b, 20)
68 self.assertEqual(config.Foo.Bar.value, 10)
69 self.assertEqual(config.Foo.Bam.value, list(range(10)))
70 self.assertEqual(config.D.C.value, 'hi there')
109 self._check_conf(config)
110
111 def test_json(self):
112 fd, fname = mkstemp('.json')
113 f = os.fdopen(fd, 'w')
114 f.write(json1file)
115 f.close()
116 # Unlink the file
117 cl = JSONFileConfigLoader(fname, log=log)
118 config = cl.load_config()
119 self._check_conf(config)
120
121 def test_v2raise(self):
122 fd, fname = mkstemp('.json')
123 f = os.fdopen(fd, 'w')
124 f.write(json2file)
125 f.close()
126 # Unlink the file
127 cl = JSONFileConfigLoader(fname, log=log)
128 with nt.assert_raises(ValueError):
129 cl.load_config()
130
71 131
72 132 class MyLoader1(ArgParseConfigLoader):
73 133 def _add_arguments(self, aliases=None, flags=None):
@@ -121,10 +181,9 b' class TestKeyValueCL(TestCase):'
121 181 klass = KeyValueConfigLoader
122 182
123 183 def test_basic(self):
124 cl = self.klass()
184 cl = self.klass(log=log)
125 185 argv = ['--'+s.strip('c.') for s in pyfile.split('\n')[2:-1]]
126 with mute_warn():
127 config = cl.load_config(argv)
186 config = cl.load_config(argv)
128 187 self.assertEqual(config.a, 10)
129 188 self.assertEqual(config.b, 20)
130 189 self.assertEqual(config.Foo.Bar.value, 10)
@@ -132,31 +191,27 b' class TestKeyValueCL(TestCase):'
132 191 self.assertEqual(config.D.C.value, 'hi there')
133 192
134 193 def test_expanduser(self):
135 cl = self.klass()
194 cl = self.klass(log=log)
136 195 argv = ['--a=~/1/2/3', '--b=~', '--c=~/', '--d="~/"']
137 with mute_warn():
138 config = cl.load_config(argv)
196 config = cl.load_config(argv)
139 197 self.assertEqual(config.a, os.path.expanduser('~/1/2/3'))
140 198 self.assertEqual(config.b, os.path.expanduser('~'))
141 199 self.assertEqual(config.c, os.path.expanduser('~/'))
142 200 self.assertEqual(config.d, '~/')
143 201
144 202 def test_extra_args(self):
145 cl = self.klass()
146 with mute_warn():
147 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
203 cl = self.klass(log=log)
204 config = cl.load_config(['--a=5', 'b', '--c=10', 'd'])
148 205 self.assertEqual(cl.extra_args, ['b', 'd'])
149 206 self.assertEqual(config.a, 5)
150 207 self.assertEqual(config.c, 10)
151 with mute_warn():
152 config = cl.load_config(['--', '--a=5', '--c=10'])
208 config = cl.load_config(['--', '--a=5', '--c=10'])
153 209 self.assertEqual(cl.extra_args, ['--a=5', '--c=10'])
154 210
155 211 def test_unicode_args(self):
156 cl = self.klass()
212 cl = self.klass(log=log)
157 213 argv = [u'--a=épsîlön']
158 with mute_warn():
159 config = cl.load_config(argv)
214 config = cl.load_config(argv)
160 215 self.assertEqual(config.a, u'épsîlön')
161 216
162 217 def test_unicode_bytes_args(self):
@@ -166,16 +221,14 b' class TestKeyValueCL(TestCase):'
166 221 except (TypeError, UnicodeEncodeError):
167 222 raise SkipTest("sys.stdin.encoding can't handle 'é'")
168 223
169 cl = self.klass()
170 with mute_warn():
171 config = cl.load_config([barg])
224 cl = self.klass(log=log)
225 config = cl.load_config([barg])
172 226 self.assertEqual(config.a, u'é')
173 227
174 228 def test_unicode_alias(self):
175 cl = self.klass()
229 cl = self.klass(log=log)
176 230 argv = [u'--a=épsîlön']
177 with mute_warn():
178 config = cl.load_config(argv, aliases=dict(a='A.a'))
231 config = cl.load_config(argv, aliases=dict(a='A.a'))
179 232 self.assertEqual(config.A.a, u'épsîlön')
180 233
181 234
@@ -183,18 +236,16 b' class TestArgParseKVCL(TestKeyValueCL):'
183 236 klass = KVArgParseConfigLoader
184 237
185 238 def test_expanduser2(self):
186 cl = self.klass()
239 cl = self.klass(log=log)
187 240 argv = ['-a', '~/1/2/3', '--b', "'~/1/2/3'"]
188 with mute_warn():
189 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
241 config = cl.load_config(argv, aliases=dict(a='A.a', b='A.b'))
190 242 self.assertEqual(config.A.a, os.path.expanduser('~/1/2/3'))
191 243 self.assertEqual(config.A.b, '~/1/2/3')
192 244
193 245 def test_eval(self):
194 cl = self.klass()
246 cl = self.klass(log=log)
195 247 argv = ['-c', 'a=5']
196 with mute_warn():
197 config = cl.load_config(argv, aliases=dict(c='A.c'))
248 config = cl.load_config(argv, aliases=dict(c='A.c'))
198 249 self.assertEqual(config.A.c, u"a=5")
199 250
200 251
@@ -33,7 +33,7 b' import sys'
33 33 from IPython.config.loader import (
34 34 Config, PyFileConfigLoader, ConfigFileNotFound
35 35 )
36 from IPython.config.application import boolean_flag, catch_config_error
36 from IPython.config.application import boolean_flag, catch_config_error, Application
37 37 from IPython.core import release
38 38 from IPython.core import usage
39 39 from IPython.core.completer import IPCompleter
@@ -364,7 +364,6 b' class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):'
364 364 else:
365 365 self.log.debug("IPython not interactive...")
366 366
367
368 367 def load_default_config(ipython_dir=None):
369 368 """Load the default config file from the default ipython_dir.
370 369
@@ -372,15 +371,14 b' def load_default_config(ipython_dir=None):'
372 371 """
373 372 if ipython_dir is None:
374 373 ipython_dir = get_ipython_dir()
374
375 375 profile_dir = os.path.join(ipython_dir, 'profile_default')
376 cl = PyFileConfigLoader("ipython_config.py", profile_dir)
377 try:
378 config = cl.load_config()
379 except ConfigFileNotFound:
380 # no config found
381 config = Config()
382 return config
383 376
377 config = Config()
378 for cf in Application._load_config_file(filename[:-3], path=profile_dir, log=None):
379 config.update(cf)
380
381 return config
384 382
385 383 launch_new_instance = TerminalIPythonApp.launch_instance
386 384
@@ -15,10 +15,10 b' Each of these abstractions is represented by a Python class.'
15 15 Configuration object: :class:`~IPython.config.loader.Config`
16 16 A configuration object is a simple dictionary-like class that holds
17 17 configuration attributes and sub-configuration objects. These classes
18 support dotted attribute style access (``Foo.bar``) in addition to the
19 regular dictionary style access (``Foo['bar']``). Configuration objects
20 are smart. They know how to merge themselves with other configuration
21 objects and they automatically create sub-configuration objects.
18 support dotted attribute style access (``cfg.Foo.bar``) in addition to the
19 regular dictionary style access (``cfg['Foo']['bar']``).
20 The Config object is a wrapper around a simple dictionary with some convenience methods,
21 such as merging and automatic section creation.
22 22
23 23 Application: :class:`~IPython.config.application.Application`
24 24 An application is a process that does a specific job. The most obvious
@@ -85,12 +85,24 b' Now, we show what our configuration objects and files look like.'
85 85 Configuration objects and files
86 86 ===============================
87 87
88 A configuration file is simply a pure Python file that sets the attributes
89 of a global, pre-created configuration object. This configuration object is a
90 :class:`~IPython.config.loader.Config` instance. While in a configuration
91 file, to get a reference to this object, simply call the :func:`get_config`
92 function. We inject this function into the global namespace that the
93 configuration file is executed in.
88 A configuration object is little more than a wrapper around a dictionary.
89 A configuration *file* is simply a mechanism for producing that object.
90 The main IPython configuration file is a plain Python script,
91 which can perform extensive logic to populate the config object.
92 IPython 2.0 introduces a JSON configuration file,
93 which is just a direct JSON serialization of the config dictionary.
94 The JSON format is easily processed by external software.
95
96 When both Python and JSON configuration file are present, both will be loaded,
97 with JSON configuration having higher priority.
98
99 Python configuration Files
100 ~~~~~~~~~~~~~~~~~~~~~~~~~~
101
102 A Python configuration file is a pure Python file that populates a configuration object.
103 This configuration object is a :class:`~IPython.config.loader.Config` instance.
104 While in a configuration file, to get a reference to this object, simply call the :func:`get_config`
105 function, which is available in the global namespace of the script.
94 106
95 107 Here is an example of a super simple configuration file that does nothing::
96 108
@@ -99,10 +111,11 b' Here is an example of a super simple configuration file that does nothing::'
99 111 Once you get a reference to the configuration object, you simply set
100 112 attributes on it. All you have to know is:
101 113
102 * The name of each attribute.
114 * The name of the class to configure.
115 * The name of the attribute.
103 116 * The type of each attribute.
104 117
105 The answers to these two questions are provided by the various
118 The answers to these questions are provided by the various
106 119 :class:`~IPython.config.configurable.Configurable` subclasses that an
107 120 application uses. Let's look at how this would work for a simple configurable
108 121 subclass::
@@ -118,7 +131,7 b' subclass::'
118 131 # The rest of the class implementation would go here..
119 132
120 133 In this example, we see that :class:`MyClass` has three attributes, two
121 of whom (``name``, ``ranking``) can be configured. All of the attributes
134 of (``name``, ``ranking``) can be configured. All of the attributes
122 135 are given types and default values. If a :class:`MyClass` is instantiated,
123 136 but not configured, these default values will be used. But let's see how
124 137 to configure this class in a configuration file::
@@ -173,9 +186,38 b' attribute of ``c`` is not the actual class, but instead is another'
173 186 instance is dynamically created for that attribute. This allows deeply
174 187 hierarchical information created easily (``c.Foo.Bar.value``) on the fly.
175 188
189 JSON configuration Files
190 ~~~~~~~~~~~~~~~~~~~~~~~~
191
192 A JSON configuration file is simply a file that contain a
193 :class:`~IPython.config.loader.Config` dictionary serialized to JSON.
194 A JSON configuration file has the same base name as a Python configuration file,
195 just with a .json extension.
196
197 Configuration described in previous section could be written as follow in a
198 JSON configuration file:
199
200 .. sourcecode:: json
201
202 {
203 "version": "1.0",
204 "MyClass": {
205 "name": "coolname",
206 "ranking": 10
207 }
208 }
209
210 JSON configuration files can be more easily generated or processed by programs
211 or other languages.
212
213
176 214 Configuration files inheritance
177 215 ===============================
178 216
217 .. note::
218
219 This section only apply to Python configuration files.
220
179 221 Let's say you want to have different configuration files for various purposes.
180 222 Our configuration system makes it easy for one configuration file to inherit
181 223 the information in another configuration file. The :func:`load_subconfig`
General Comments 0
You need to be logged in to leave comments. Login now