##// END OF EJS Templates
Merge pull request #4276 from minrk/config-extend...
Thomas Kluyver -
r12934:61716e73 merge
parent child Browse files
Show More
@@ -0,0 +1,15 b''
1 Extending Configurable Containers
2 ---------------------------------
3
4 Some configurable traits are containers (list, dict, set)
5 Config objects now support calling ``extend``, ``update``, ``insert``, etc.
6 on traits in config files, which will ultimately result in calling
7 those methods on the original object.
8
9 The effect being that you can now add to containers without having to copy/paste
10 the initial value::
11
12 c = get_config()
13 c.InlineBackend.rc.update({ 'figure.figsize' : (6, 4) })
14
15
@@ -28,7 +28,7 b' Authors:'
28 import datetime
28 import datetime
29 from copy import deepcopy
29 from copy import deepcopy
30
30
31 from loader import Config
31 from .loader import Config, LazyConfigValue
32 from IPython.utils.traitlets import HasTraits, Instance
32 from IPython.utils.traitlets import HasTraits, Instance
33 from IPython.utils.text import indent, wrap_paragraphs
33 from IPython.utils.text import indent, wrap_paragraphs
34
34
@@ -137,7 +137,7 b' class Configurable(HasTraits):'
137 if c._has_section(sname):
137 if c._has_section(sname):
138 my_config.merge(c[sname])
138 my_config.merge(c[sname])
139 return my_config
139 return my_config
140
140
141 def _load_config(self, cfg, section_names=None, traits=None):
141 def _load_config(self, cfg, section_names=None, traits=None):
142 """load traits from a Config object"""
142 """load traits from a Config object"""
143
143
@@ -149,6 +149,11 b' class Configurable(HasTraits):'
149 my_config = self._find_my_config(cfg)
149 my_config = self._find_my_config(cfg)
150 for name, config_value in my_config.iteritems():
150 for name, config_value in my_config.iteritems():
151 if name in traits:
151 if name in traits:
152 if isinstance(config_value, LazyConfigValue):
153 # ConfigValue is a wrapper for using append / update on containers
154 # without having to copy the
155 initial = getattr(self, name)
156 config_value = config_value.get_value(initial)
152 # We have to do a deepcopy here if we don't deepcopy the entire
157 # We have to do a deepcopy here if we don't deepcopy the entire
153 # config object. If we don't, a mutable config_value will be
158 # config object. If we don't, a mutable config_value will be
154 # shared by all instances, effectively making it a class attribute.
159 # shared by all instances, effectively making it a class attribute.
@@ -25,6 +25,7 b' Authors'
25
25
26 import __builtin__ as builtin_mod
26 import __builtin__ as builtin_mod
27 import argparse
27 import argparse
28 import copy
28 import os
29 import os
29 import re
30 import re
30 import sys
31 import sys
@@ -32,6 +33,7 b' import sys'
32 from IPython.utils.path import filefind, get_ipython_dir
33 from IPython.utils.path import filefind, get_ipython_dir
33 from IPython.utils import py3compat, warn
34 from IPython.utils import py3compat, warn
34 from IPython.utils.encoding import DEFAULT_ENCODING
35 from IPython.utils.encoding import DEFAULT_ENCODING
36 from IPython.utils.traitlets import HasTraits, List, Any, TraitError
35
37
36 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
37 # Exceptions
39 # Exceptions
@@ -74,6 +76,92 b' class ArgumentParser(argparse.ArgumentParser):'
74 # Config class for holding config information
76 # Config class for holding config information
75 #-----------------------------------------------------------------------------
77 #-----------------------------------------------------------------------------
76
78
79 class LazyConfigValue(HasTraits):
80 """Proxy object for exposing methods on configurable containers
81
82 Exposes:
83
84 - append, extend, insert on lists
85 - update on dicts
86 - update, add on sets
87 """
88
89 _value = None
90
91 # list methods
92 _extend = List()
93 _prepend = List()
94
95 def append(self, obj):
96 self._extend.append(obj)
97
98 def extend(self, other):
99 self._extend.extend(other)
100
101 def prepend(self, other):
102 """like list.extend, but for the front"""
103 self._prepend[:0] = other
104
105 _inserts = List()
106 def insert(self, index, other):
107 if not isinstance(index, int):
108 raise TypeError("An integer is required")
109 self._inserts.append((index, other))
110
111 # dict methods
112 # update is used for both dict and set
113 _update = Any()
114 def update(self, other):
115 if self._update is None:
116 if isinstance(other, dict):
117 self._update = {}
118 else:
119 self._update = set()
120 self._update.update(other)
121
122 # set methods
123 def add(self, obj):
124 self.update({obj})
125
126 def get_value(self, initial):
127 """construct the value from the initial one
128
129 after applying any insert / extend / update changes
130 """
131 if self._value is not None:
132 return self._value
133 value = copy.deepcopy(initial)
134 if isinstance(value, list):
135 for idx, obj in self._inserts:
136 value.insert(idx, obj)
137 value[:0] = self._prepend
138 value.extend(self._extend)
139
140 elif isinstance(value, dict):
141 if self._update:
142 value.update(self._update)
143 elif isinstance(value, set):
144 if self._update:
145 value.update(self._update)
146 self._value = value
147 return value
148
149 def to_dict(self):
150 """return JSONable dict form of my data
151
152 Currently update as dict or set, extend, prepend as lists, and inserts as list of tuples.
153 """
154 d = {}
155 if self._update:
156 d['update'] = self._update
157 if self._extend:
158 d['extend'] = self._extend
159 if self._prepend:
160 d['prepend'] = self._prepend
161 elif self._inserts:
162 d['inserts'] = self._inserts
163 return d
164
77
165
78 class Config(dict):
166 class Config(dict):
79 """An attribute based dict that can do smart merges."""
167 """An attribute based dict that can do smart merges."""
@@ -125,6 +213,14 b' class Config(dict):'
125 return False
213 return False
126
214
127 def __contains__(self, key):
215 def __contains__(self, key):
216 # allow nested contains of the form `"Section.key" in config`
217 if '.' in key:
218 first, remainder = key.split('.', 1)
219 if first not in self:
220 return False
221 return remainder in self[first]
222
223 # we always have Sections
128 if self._is_section_key(key):
224 if self._is_section_key(key):
129 return True
225 return True
130 else:
226 else:
@@ -147,7 +243,7 b' class Config(dict):'
147 def __deepcopy__(self, memo):
243 def __deepcopy__(self, memo):
148 import copy
244 import copy
149 return type(self)(copy.deepcopy(self.items()))
245 return type(self)(copy.deepcopy(self.items()))
150
246
151 def __getitem__(self, key):
247 def __getitem__(self, key):
152 # We cannot use directly self._is_section_key, because it triggers
248 # We cannot use directly self._is_section_key, because it triggers
153 # infinite recursion on top of PyPy. Instead, we manually fish the
249 # infinite recursion on top of PyPy. Instead, we manually fish the
@@ -170,7 +266,14 b' class Config(dict):'
170 dict.__setitem__(self, key, c)
266 dict.__setitem__(self, key, c)
171 return c
267 return c
172 else:
268 else:
173 return dict.__getitem__(self, key)
269 try:
270 return dict.__getitem__(self, key)
271 except KeyError:
272 # undefined
273 v = LazyConfigValue()
274 dict.__setitem__(self, key, v)
275 return v
276
174
277
175 def __setitem__(self, key, value):
278 def __setitem__(self, key, value):
176 if self._is_section_key(key):
279 if self._is_section_key(key):
@@ -9,11 +9,5 b' lines = """'
9 from IPython.parallel import *
9 from IPython.parallel import *
10 """
10 """
11
11
12 # You have to make sure that attributes that are containers already
12 app.exec_lines.append(lines)
13 # exist before using them. Simple assigning a new list will override
14 # all previous values.
15 if hasattr(app, 'exec_lines'):
16 app.exec_lines.append(lines)
17 else:
18 app.exec_lines = [lines]
19
13
@@ -10,12 +10,4 b' import cmath'
10 from math import *
10 from math import *
11 """
11 """
12
12
13 # You have to make sure that attributes that are containers already
13 app.exec_lines.append(lines)
14 # exist before using them. Simple assigning a new list will override
15 # all previous values.
16
17 if hasattr(app, 'exec_lines'):
18 app.exec_lines.append(lines)
19 else:
20 app.exec_lines = [lines]
21
@@ -21,10 +21,4 b' lines = """'
21 %rehashx
21 %rehashx
22 """
22 """
23
23
24 # You have to make sure that attributes that are containers already
24 app.exec_lines.append(lines)
25 # exist before using them. Simple assigning a new list will override
26 # all previous values.
27 if hasattr(app, 'exec_lines'):
28 app.exec_lines.append(lines)
29 else:
30 app.exec_lines = [lines]
@@ -13,18 +13,8 b" k, m, n = symbols('k m n', integer=True)"
13 f, g, h = symbols('f g h', cls=Function)
13 f, g, h = symbols('f g h', cls=Function)
14 """
14 """
15
15
16 # You have to make sure that attributes that are containers already
16 app.exec_lines.append(lines)
17 # exist before using them. Simple assigning a new list will override
18 # all previous values.
19
20 if hasattr(app, 'exec_lines'):
21 app.exec_lines.append(lines)
22 else:
23 app.exec_lines = [lines]
24
17
25 # Load the sympy_printing extension to enable nice printing of sympy expr's.
18 # Load the sympy_printing extension to enable nice printing of sympy expr's.
26 if hasattr(app, 'extensions'):
19 app.extensions.append('sympy.interactive.ipythonprinting')
27 app.extensions.append('sympyprinting')
28 else:
29 app.extensions = ['sympyprinting']
30
20
@@ -27,7 +27,7 b' from IPython.config.configurable import ('
27 )
27 )
28
28
29 from IPython.utils.traitlets import (
29 from IPython.utils.traitlets import (
30 Integer, Float, Unicode
30 Integer, Float, Unicode, List, Dict, Set,
31 )
31 )
32
32
33 from IPython.config.loader import Config
33 from IPython.config.loader import Config
@@ -281,3 +281,80 b' class TestParentConfigurable(TestCase):'
281 myc = MyConfigurable(parent=parent)
281 myc = MyConfigurable(parent=parent)
282 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
282 self.assertEqual(myc.b, parent.config.MyParent2.MyParent.MyConfigurable.b)
283
283
284 class Containers(Configurable):
285 lis = List(config=True)
286 def _lis_default(self):
287 return [-1]
288
289 s = Set(config=True)
290 def _s_default(self):
291 return {'a'}
292
293 d = Dict(config=True)
294 def _d_default(self):
295 return {'a' : 'b'}
296
297 class TestConfigContainers(TestCase):
298 def test_extend(self):
299 c = Config()
300 c.Containers.lis.extend(range(5))
301 obj = Containers(config=c)
302 self.assertEqual(obj.lis, range(-1,5))
303
304 def test_insert(self):
305 c = Config()
306 c.Containers.lis.insert(0, 'a')
307 c.Containers.lis.insert(1, 'b')
308 obj = Containers(config=c)
309 self.assertEqual(obj.lis, ['a', 'b', -1])
310
311 def test_prepend(self):
312 c = Config()
313 c.Containers.lis.prepend([1,2])
314 c.Containers.lis.prepend([2,3])
315 obj = Containers(config=c)
316 self.assertEqual(obj.lis, [2,3,1,2,-1])
317
318 def test_prepend_extend(self):
319 c = Config()
320 c.Containers.lis.prepend([1,2])
321 c.Containers.lis.extend([2,3])
322 obj = Containers(config=c)
323 self.assertEqual(obj.lis, [1,2,-1,2,3])
324
325 def test_append_extend(self):
326 c = Config()
327 c.Containers.lis.append([1,2])
328 c.Containers.lis.extend([2,3])
329 obj = Containers(config=c)
330 self.assertEqual(obj.lis, [-1,[1,2],2,3])
331
332 def test_extend_append(self):
333 c = Config()
334 c.Containers.lis.extend([2,3])
335 c.Containers.lis.append([1,2])
336 obj = Containers(config=c)
337 self.assertEqual(obj.lis, [-1,2,3,[1,2]])
338
339 def test_insert_extend(self):
340 c = Config()
341 c.Containers.lis.insert(0, 1)
342 c.Containers.lis.extend([2,3])
343 obj = Containers(config=c)
344 self.assertEqual(obj.lis, [1,-1,2,3])
345
346 def test_set_update(self):
347 c = Config()
348 c.Containers.s.update({0,1,2})
349 c.Containers.s.update({3})
350 obj = Containers(config=c)
351 self.assertEqual(obj.s, {'a', 0, 1, 2, 3})
352
353 def test_dict_update(self):
354 c = Config()
355 c.Containers.d.update({'c' : 'd'})
356 c.Containers.d.update({'e' : 'f'})
357 obj = Containers(config=c)
358 self.assertEqual(obj.d, {'a':'b', 'c':'d', 'e':'f'})
359
360
@@ -9,7 +9,7 b' Authors:'
9 """
9 """
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Copyright (C) 2008-2011 The IPython Development Team
12 # Copyright (C) 2008 The IPython Development Team
13 #
13 #
14 # Distributed under the terms of the BSD License. The full license is in
14 # Distributed under the terms of the BSD License. The full license is in
15 # the file COPYING, distributed as part of this software.
15 # the file COPYING, distributed as part of this software.
@@ -279,4 +279,14 b' class TestConfig(TestCase):'
279 self.assertEqual(c1.Foo.__class__, Config)
279 self.assertEqual(c1.Foo.__class__, Config)
280 self.assertEqual(c1.Foo.bar, 1)
280 self.assertEqual(c1.Foo.bar, 1)
281 self.assertEqual(c1.Foo.baz, 2)
281 self.assertEqual(c1.Foo.baz, 2)
282 self.assertRaises(AttributeError, getattr, c2.Foo, 'baz')
282 self.assertNotIn('baz', c2.Foo)
283
284 def test_contains(self):
285 c1 = Config({'Foo' : {'baz' : 2}})
286 c2 = Config({'Foo' : {'bar' : 1}})
287 self.assertIn('Foo', c1)
288 self.assertIn('Foo.baz', c1)
289 self.assertIn('Foo.bar', c2)
290 self.assertNotIn('Foo.bar', c1)
291
292
@@ -282,10 +282,7 b' class BaseIPythonApplication(Application):'
282 if self.profile_dir is not None:
282 if self.profile_dir is not None:
283 # already ran
283 # already ran
284 return
284 return
285 try:
285 if 'ProfileDir.location' not in self.config:
286 # location explicitly specified:
287 location = self.config.ProfileDir.location
288 except AttributeError:
289 # location not specified, find by profile name
286 # location not specified, find by profile name
290 try:
287 try:
291 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
288 p = ProfileDir.find_profile_dir_by_name(self.ipython_dir, self.profile, self.config)
@@ -305,6 +302,7 b' class BaseIPythonApplication(Application):'
305 else:
302 else:
306 self.log.info("Using existing profile dir: %r"%p.location)
303 self.log.info("Using existing profile dir: %r"%p.location)
307 else:
304 else:
305 location = self.config.ProfileDir.location
308 # location is fully specified
306 # location is fully specified
309 try:
307 try:
310 p = ProfileDir.find_profile_dir(location, self.config)
308 p = ProfileDir.find_profile_dir(location, self.config)
@@ -399,9 +399,9 b' class IPControllerApp(BaseParallelApplication):'
399 q.connect_mon(monitor_url)
399 q.connect_mon(monitor_url)
400 q.daemon=True
400 q.daemon=True
401 children.append(q)
401 children.append(q)
402 try:
402 if 'TaskScheduler.scheme_name' in self.config:
403 scheme = self.config.TaskScheduler.scheme_name
403 scheme = self.config.TaskScheduler.scheme_name
404 except AttributeError:
404 else:
405 scheme = TaskScheduler.scheme_name.get_default_value()
405 scheme = TaskScheduler.scheme_name.get_default_value()
406 # Task Queue (in a Process)
406 # Task Queue (in a Process)
407 if scheme == 'pure':
407 if scheme == 'pure':
@@ -211,14 +211,9 b' class IPEngineApp(BaseParallelApplication):'
211
211
212 # allow hand-override of location for disambiguation
212 # allow hand-override of location for disambiguation
213 # and ssh-server
213 # and ssh-server
214 try:
214 if 'EngineFactory.location' not in config:
215 config.EngineFactory.location
216 except AttributeError:
217 config.EngineFactory.location = d['location']
215 config.EngineFactory.location = d['location']
218
216 if 'EngineFactory.sshserver' not in config:
219 try:
220 config.EngineFactory.sshserver
221 except AttributeError:
222 config.EngineFactory.sshserver = d.get('ssh')
217 config.EngineFactory.sshserver = d.get('ssh')
223
218
224 location = config.EngineFactory.location
219 location = config.EngineFactory.location
@@ -313,21 +308,17 b' class IPEngineApp(BaseParallelApplication):'
313 self.log.fatal("Fatal: url file never arrived: %s", self.url_file)
308 self.log.fatal("Fatal: url file never arrived: %s", self.url_file)
314 self.exit(1)
309 self.exit(1)
315
310
311 exec_lines = []
312 for app in ('IPKernelApp', 'InteractiveShellApp'):
313 if '%s.exec_lines' in config:
314 exec_lines = config.IPKernelApp.exec_lines = config[app].exec_lines
315 break
316
316
317 try:
317 exec_files = []
318 exec_lines = config.IPKernelApp.exec_lines
318 for app in ('IPKernelApp', 'InteractiveShellApp'):
319 except AttributeError:
319 if '%s.exec_files' in config:
320 try:
320 exec_files = config.IPKernelApp.exec_files = config[app].exec_files
321 exec_lines = config.InteractiveShellApp.exec_lines
321 break
322 except AttributeError:
323 exec_lines = config.IPKernelApp.exec_lines = []
324 try:
325 exec_files = config.IPKernelApp.exec_files
326 except AttributeError:
327 try:
328 exec_files = config.InteractiveShellApp.exec_files
329 except AttributeError:
330 exec_files = config.IPKernelApp.exec_files = []
331
322
332 if self.startup_script:
323 if self.startup_script:
333 exec_files.append(self.startup_script)
324 exec_files.append(self.startup_script)
@@ -255,10 +255,9 b' class HubFactory(RegistrationFactory):'
255
255
256 ctx = self.context
256 ctx = self.context
257 loop = self.loop
257 loop = self.loop
258
258 if 'TaskScheduler.scheme_name' in self.config:
259 try:
260 scheme = self.config.TaskScheduler.scheme_name
259 scheme = self.config.TaskScheduler.scheme_name
261 except AttributeError:
260 else:
262 from .scheduler import TaskScheduler
261 from .scheduler import TaskScheduler
263 scheme = TaskScheduler.scheme_name.get_default_value()
262 scheme = TaskScheduler.scheme_name.get_default_value()
264
263
@@ -281,18 +281,10 b' class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):'
281 # are removed from the backend.
281 # are removed from the backend.
282
282
283 # parse the colors arg down to current known labels
283 # parse the colors arg down to current known labels
284 try:
284 cfg = self.config
285 colors = self.config.ZMQInteractiveShell.colors
285 colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
286 except AttributeError:
286 style = cfg.IPythonWidget.syntax_style if 'IPythonWidget.syntax_style' in cfg else None
287 colors = None
287 sheet = cfg.IPythonWidget.style_sheet if 'IPythonWidget.style_sheet' in cfg else None
288 try:
289 style = self.config.IPythonWidget.syntax_style
290 except AttributeError:
291 style = None
292 try:
293 sheet = self.config.IPythonWidget.style_sheet
294 except AttributeError:
295 sheet = None
296
288
297 # find the value for colors:
289 # find the value for colors:
298 if colors:
290 if colors:
General Comments 0
You need to be logged in to leave comments. Login now