##// END OF EJS Templates
add parent to Configurable...
MinRK -
Show More
@@ -1,356 +1,376 b''
1 1 # encoding: utf-8
2 2 """
3 3 A base class for objects that are configurable.
4 4
5 5 Inheritance diagram:
6 6
7 7 .. inheritance-diagram:: IPython.config.configurable
8 8 :parts: 3
9 9
10 10 Authors:
11 11
12 12 * Brian Granger
13 13 * Fernando Perez
14 14 * Min RK
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Copyright (C) 2008-2011 The IPython Development Team
19 19 #
20 20 # Distributed under the terms of the BSD License. The full license is in
21 21 # the file COPYING, distributed as part of this software.
22 22 #-----------------------------------------------------------------------------
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Imports
26 26 #-----------------------------------------------------------------------------
27 27
28 28 import datetime
29 29 from copy import deepcopy
30 30
31 31 from loader import Config
32 32 from IPython.utils.traitlets import HasTraits, Instance
33 33 from IPython.utils.text import indent, wrap_paragraphs
34 34
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Helper classes for Configurables
38 38 #-----------------------------------------------------------------------------
39 39
40 40
41 41 class ConfigurableError(Exception):
42 42 pass
43 43
44 44
45 45 class MultipleInstanceError(ConfigurableError):
46 46 pass
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # Configurable implementation
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class Configurable(HasTraits):
53 53
54 54 config = Instance(Config, (), {})
55 parent = Instance('IPython.config.configurable.Configurable')
55 56 created = None
56 57
57 58 def __init__(self, **kwargs):
58 59 """Create a configurable given a config config.
59 60
60 61 Parameters
61 62 ----------
62 63 config : Config
63 64 If this is empty, default values are used. If config is a
64 65 :class:`Config` instance, it will be used to configure the
65 66 instance.
67 parent : Configurable instance
68 The parent
66 69
67 70 Notes
68 71 -----
69 72 Subclasses of Configurable must call the :meth:`__init__` method of
70 73 :class:`Configurable` *before* doing anything else and using
71 74 :func:`super`::
72 75
73 76 class MyConfigurable(Configurable):
74 77 def __init__(self, config=None):
75 78 super(MyConfigurable, self).__init__(config=config)
76 79 # Then any other code you need to finish initialization.
77 80
78 81 This ensures that instances will be configured properly.
79 82 """
83 parent = kwargs.pop('parent', None)
84 if parent:
85 # config is implied from parent
86 if 'config' not in kwargs:
87 kwargs['config'] = parent.config
88 self.parent = parent
89
80 90 config = kwargs.pop('config', None)
81 91 if config is not None:
82 92 # We used to deepcopy, but for now we are trying to just save
83 93 # by reference. This *could* have side effects as all components
84 94 # will share config. In fact, I did find such a side effect in
85 95 # _config_changed below. If a config attribute value was a mutable type
86 96 # all instances of a component were getting the same copy, effectively
87 97 # making that a class attribute.
88 98 # self.config = deepcopy(config)
89 99 self.config = config
90 100 # This should go second so individual keyword arguments override
91 101 # the values in config.
92 102 super(Configurable, self).__init__(**kwargs)
93 103 self.created = datetime.datetime.now()
94 104
95 105 #-------------------------------------------------------------------------
96 106 # Static trait notifiations
97 107 #-------------------------------------------------------------------------
98 108
99 def _config_changed(self, name, old, new):
100 """Update all the class traits having ``config=True`` as metadata.
109 @classmethod
110 def section_names(cls):
111 """return section names as a list"""
112 return [c.__name__ for c in reversed(cls.__mro__) if
113 issubclass(c, Configurable) and issubclass(cls, c)
114 ]
101 115
102 For any class trait with a ``config`` metadata attribute that is
103 ``True``, we update the trait with the value of the corresponding
104 config entry.
105 """
106 # Get all traits with a config metadata entry that is True
107 traits = self.traits(config=True)
116 def _load_config(self, cfg, section_names=None, traits=None):
117 """load traits from a Config object"""
108 118
109 # We auto-load config section for this class as well as any parent
110 # classes that are Configurable subclasses. This starts with Configurable
111 # and works down the mro loading the config for each section.
112 section_names = [cls.__name__ for cls in \
113 reversed(self.__class__.__mro__) if
114 issubclass(cls, Configurable) and issubclass(self.__class__, cls)]
119 if traits is None:
120 traits = self.traits(config=True)
121 if section_names is None:
122 section_names = self.section_names()
115 123
116 124 for sname in section_names:
117 125 # Don't do a blind getattr as that would cause the config to
118 126 # dynamically create the section with name self.__class__.__name__.
119 if new._has_section(sname):
120 my_config = new[sname]
127 if cfg._has_section(sname):
128 my_config = cfg[sname]
121 129 for k, v in traits.iteritems():
122 # Don't allow traitlets with config=True to start with
123 # uppercase. Otherwise, they are confused with Config
124 # subsections. But, developers shouldn't have uppercase
125 # attributes anyways! (PEP 6)
126 if k[0].upper()==k[0] and not k.startswith('_'):
127 raise ConfigurableError('Configurable traitlets with '
128 'config=True must start with a lowercase so they are '
129 'not confused with Config subsections: %s.%s' % \
130 (self.__class__.__name__, k))
131 130 try:
132 131 # Here we grab the value from the config
133 132 # If k has the naming convention of a config
134 133 # section, it will be auto created.
135 134 config_value = my_config[k]
136 135 except KeyError:
137 136 pass
138 137 else:
139 # print "Setting %s.%s from %s.%s=%r" % \
140 # (self.__class__.__name__,k,sname,k,config_value)
141 138 # We have to do a deepcopy here if we don't deepcopy the entire
142 139 # config object. If we don't, a mutable config_value will be
143 140 # shared by all instances, effectively making it a class attribute.
144 141 setattr(self, k, deepcopy(config_value))
145 142
143 def _config_changed(self, name, old, new):
144 """Update all the class traits having ``config=True`` as metadata.
145
146 For any class trait with a ``config`` metadata attribute that is
147 ``True``, we update the trait with the value of the corresponding
148 config entry.
149 """
150 # Get all traits with a config metadata entry that is True
151 traits = self.traits(config=True)
152
153 # We auto-load config section for this class as well as any parent
154 # classes that are Configurable subclasses. This starts with Configurable
155 # and works down the mro loading the config for each section.
156 section_names = self.section_names()
157 self._load_config(new, traits=traits, section_names=section_names)
158
159 # load parent config as well, if we have one
160 parent_section_names = [] if self.parent is None else self.parent.section_names()
161 for parent in parent_section_names:
162 print parent, new._has_section(parent), new[parent]
163 if not new._has_section(parent):
164 continue
165 self._load_config(new[parent], traits=traits, section_names=section_names)
166
146 167 def update_config(self, config):
147 168 """Fire the traits events when the config is updated."""
148 169 # Save a copy of the current config.
149 170 newconfig = deepcopy(self.config)
150 171 # Merge the new config into the current one.
151 172 newconfig.merge(config)
152 173 # Save the combined config as self.config, which triggers the traits
153 174 # events.
154 175 self.config = newconfig
155 176
156 177 @classmethod
157 178 def class_get_help(cls, inst=None):
158 179 """Get the help string for this class in ReST format.
159 180
160 181 If `inst` is given, it's current trait values will be used in place of
161 182 class defaults.
162 183 """
163 184 assert inst is None or isinstance(inst, cls)
164 cls_traits = cls.class_traits(config=True)
165 185 final_help = []
166 186 final_help.append(u'%s options' % cls.__name__)
167 187 final_help.append(len(final_help[0])*u'-')
168 188 for k, v in sorted(cls.class_traits(config=True).iteritems()):
169 189 help = cls.class_get_trait_help(v, inst)
170 190 final_help.append(help)
171 191 return '\n'.join(final_help)
172 192
173 193 @classmethod
174 194 def class_get_trait_help(cls, trait, inst=None):
175 195 """Get the help string for a single trait.
176 196
177 197 If `inst` is given, it's current trait values will be used in place of
178 198 the class default.
179 199 """
180 200 assert inst is None or isinstance(inst, cls)
181 201 lines = []
182 202 header = "--%s.%s=<%s>" % (cls.__name__, trait.name, trait.__class__.__name__)
183 203 lines.append(header)
184 204 if inst is not None:
185 205 lines.append(indent('Current: %r' % getattr(inst, trait.name), 4))
186 206 else:
187 207 try:
188 208 dvr = repr(trait.get_default_value())
189 209 except Exception:
190 210 dvr = None # ignore defaults we can't construct
191 211 if dvr is not None:
192 212 if len(dvr) > 64:
193 213 dvr = dvr[:61]+'...'
194 214 lines.append(indent('Default: %s' % dvr, 4))
195 215 if 'Enum' in trait.__class__.__name__:
196 216 # include Enum choices
197 217 lines.append(indent('Choices: %r' % (trait.values,)))
198 218
199 219 help = trait.get_metadata('help')
200 220 if help is not None:
201 221 help = '\n'.join(wrap_paragraphs(help, 76))
202 222 lines.append(indent(help, 4))
203 223 return '\n'.join(lines)
204 224
205 225 @classmethod
206 226 def class_print_help(cls, inst=None):
207 227 """Get the help string for a single trait and print it."""
208 228 print cls.class_get_help(inst)
209 229
210 230 @classmethod
211 231 def class_config_section(cls):
212 232 """Get the config class config section"""
213 233 def c(s):
214 234 """return a commented, wrapped block."""
215 235 s = '\n\n'.join(wrap_paragraphs(s, 78))
216 236
217 237 return '# ' + s.replace('\n', '\n# ')
218 238
219 239 # section header
220 240 breaker = '#' + '-'*78
221 241 s = "# %s configuration" % cls.__name__
222 242 lines = [breaker, s, breaker, '']
223 243 # get the description trait
224 244 desc = cls.class_traits().get('description')
225 245 if desc:
226 246 desc = desc.default_value
227 247 else:
228 248 # no description trait, use __doc__
229 249 desc = getattr(cls, '__doc__', '')
230 250 if desc:
231 251 lines.append(c(desc))
232 252 lines.append('')
233 253
234 254 parents = []
235 255 for parent in cls.mro():
236 256 # only include parents that are not base classes
237 257 # and are not the class itself
238 258 # and have some configurable traits to inherit
239 259 if parent is not cls and issubclass(parent, Configurable) and \
240 260 parent.class_traits(config=True):
241 261 parents.append(parent)
242 262
243 263 if parents:
244 264 pstr = ', '.join([ p.__name__ for p in parents ])
245 265 lines.append(c('%s will inherit config from: %s'%(cls.__name__, pstr)))
246 266 lines.append('')
247 267
248 268 for name, trait in cls.class_traits(config=True).iteritems():
249 269 help = trait.get_metadata('help') or ''
250 270 lines.append(c(help))
251 271 lines.append('# c.%s.%s = %r'%(cls.__name__, name, trait.get_default_value()))
252 272 lines.append('')
253 273 return '\n'.join(lines)
254 274
255 275
256 276
257 277 class SingletonConfigurable(Configurable):
258 278 """A configurable that only allows one instance.
259 279
260 280 This class is for classes that should only have one instance of itself
261 281 or *any* subclass. To create and retrieve such a class use the
262 282 :meth:`SingletonConfigurable.instance` method.
263 283 """
264 284
265 285 _instance = None
266 286
267 287 @classmethod
268 288 def _walk_mro(cls):
269 289 """Walk the cls.mro() for parent classes that are also singletons
270 290
271 291 For use in instance()
272 292 """
273 293
274 294 for subclass in cls.mro():
275 295 if issubclass(cls, subclass) and \
276 296 issubclass(subclass, SingletonConfigurable) and \
277 297 subclass != SingletonConfigurable:
278 298 yield subclass
279 299
280 300 @classmethod
281 301 def clear_instance(cls):
282 302 """unset _instance for this class and singleton parents.
283 303 """
284 304 if not cls.initialized():
285 305 return
286 306 for subclass in cls._walk_mro():
287 307 if isinstance(subclass._instance, cls):
288 308 # only clear instances that are instances
289 309 # of the calling class
290 310 subclass._instance = None
291 311
292 312 @classmethod
293 313 def instance(cls, *args, **kwargs):
294 314 """Returns a global instance of this class.
295 315
296 316 This method create a new instance if none have previously been created
297 317 and returns a previously created instance is one already exists.
298 318
299 319 The arguments and keyword arguments passed to this method are passed
300 320 on to the :meth:`__init__` method of the class upon instantiation.
301 321
302 322 Examples
303 323 --------
304 324
305 325 Create a singleton class using instance, and retrieve it::
306 326
307 327 >>> from IPython.config.configurable import SingletonConfigurable
308 328 >>> class Foo(SingletonConfigurable): pass
309 329 >>> foo = Foo.instance()
310 330 >>> foo == Foo.instance()
311 331 True
312 332
313 333 Create a subclass that is retrived using the base class instance::
314 334
315 335 >>> class Bar(SingletonConfigurable): pass
316 336 >>> class Bam(Bar): pass
317 337 >>> bam = Bam.instance()
318 338 >>> bam == Bar.instance()
319 339 True
320 340 """
321 341 # Create and save the instance
322 342 if cls._instance is None:
323 343 inst = cls(*args, **kwargs)
324 344 # Now make sure that the instance will also be returned by
325 345 # parent classes' _instance attribute.
326 346 for subclass in cls._walk_mro():
327 347 subclass._instance = inst
328 348
329 349 if isinstance(cls._instance, cls):
330 350 return cls._instance
331 351 else:
332 352 raise MultipleInstanceError(
333 353 'Multiple incompatible subclass instances of '
334 354 '%s are being created.' % cls.__name__
335 355 )
336 356
337 357 @classmethod
338 358 def initialized(cls):
339 359 """Has an instance been created?"""
340 360 return hasattr(cls, "_instance") and cls._instance is not None
341 361
342 362
343 363 class LoggingConfigurable(Configurable):
344 364 """A parent class for Configurables that log.
345 365
346 366 Subclasses have a log trait, and the default behavior
347 367 is to get the logger from the currently running Application
348 368 via Application.instance().log.
349 369 """
350 370
351 371 log = Instance('logging.Logger')
352 372 def _log_default(self):
353 373 from IPython.config.application import Application
354 374 return Application.instance().log
355 375
356 376
@@ -1,183 +1,239 b''
1 1 # encoding: utf-8
2 2 """
3 3 Tests for IPython.config.configurable
4 4
5 5 Authors:
6 6
7 7 * Brian Granger
8 8 * Fernando Perez (design help)
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2011 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 from unittest import TestCase
23 23
24 24 from IPython.config.configurable import (
25 25 Configurable,
26 26 SingletonConfigurable
27 27 )
28 28
29 29 from IPython.utils.traitlets import (
30 30 Integer, Float, Unicode
31 31 )
32 32
33 33 from IPython.config.loader import Config
34 34 from IPython.utils.py3compat import PY3
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Test cases
38 38 #-----------------------------------------------------------------------------
39 39
40 40
41 41 class MyConfigurable(Configurable):
42 42 a = Integer(1, config=True, help="The integer a.")
43 43 b = Float(1.0, config=True, help="The integer b.")
44 44 c = Unicode('no config')
45 45
46 46
47 47 mc_help=u"""MyConfigurable options
48 48 ----------------------
49 49 --MyConfigurable.a=<Integer>
50 50 Default: 1
51 51 The integer a.
52 52 --MyConfigurable.b=<Float>
53 53 Default: 1.0
54 54 The integer b."""
55 55
56 56 mc_help_inst=u"""MyConfigurable options
57 57 ----------------------
58 58 --MyConfigurable.a=<Integer>
59 59 Current: 5
60 60 The integer a.
61 61 --MyConfigurable.b=<Float>
62 62 Current: 4.0
63 63 The integer b."""
64 64
65 65 # On Python 3, the Integer trait is a synonym for Int
66 66 if PY3:
67 67 mc_help = mc_help.replace(u"<Integer>", u"<Int>")
68 68 mc_help_inst = mc_help_inst.replace(u"<Integer>", u"<Int>")
69 69
70 70 class Foo(Configurable):
71 71 a = Integer(0, config=True, help="The integer a.")
72 72 b = Unicode('nope', config=True)
73 73
74 74
75 75 class Bar(Foo):
76 76 b = Unicode('gotit', config=False, help="The string b.")
77 77 c = Float(config=True, help="The string c.")
78 78
79 79
80 80 class TestConfigurable(TestCase):
81 81
82 82 def test_default(self):
83 83 c1 = Configurable()
84 84 c2 = Configurable(config=c1.config)
85 85 c3 = Configurable(config=c2.config)
86 86 self.assertEqual(c1.config, c2.config)
87 87 self.assertEqual(c2.config, c3.config)
88 88
89 89 def test_custom(self):
90 90 config = Config()
91 91 config.foo = 'foo'
92 92 config.bar = 'bar'
93 93 c1 = Configurable(config=config)
94 94 c2 = Configurable(config=c1.config)
95 95 c3 = Configurable(config=c2.config)
96 96 self.assertEqual(c1.config, config)
97 97 self.assertEqual(c2.config, config)
98 98 self.assertEqual(c3.config, config)
99 99 # Test that copies are not made
100 100 self.assertTrue(c1.config is config)
101 101 self.assertTrue(c2.config is config)
102 102 self.assertTrue(c3.config is config)
103 103 self.assertTrue(c1.config is c2.config)
104 104 self.assertTrue(c2.config is c3.config)
105 105
106 106 def test_inheritance(self):
107 107 config = Config()
108 108 config.MyConfigurable.a = 2
109 109 config.MyConfigurable.b = 2.0
110 110 c1 = MyConfigurable(config=config)
111 111 c2 = MyConfigurable(config=c1.config)
112 112 self.assertEqual(c1.a, config.MyConfigurable.a)
113 113 self.assertEqual(c1.b, config.MyConfigurable.b)
114 114 self.assertEqual(c2.a, config.MyConfigurable.a)
115 115 self.assertEqual(c2.b, config.MyConfigurable.b)
116 116
117 117 def test_parent(self):
118 118 config = Config()
119 119 config.Foo.a = 10
120 120 config.Foo.b = "wow"
121 121 config.Bar.b = 'later'
122 122 config.Bar.c = 100.0
123 123 f = Foo(config=config)
124 124 b = Bar(config=f.config)
125 125 self.assertEqual(f.a, 10)
126 126 self.assertEqual(f.b, 'wow')
127 127 self.assertEqual(b.b, 'gotit')
128 128 self.assertEqual(b.c, 100.0)
129 129
130 130 def test_override1(self):
131 131 config = Config()
132 132 config.MyConfigurable.a = 2
133 133 config.MyConfigurable.b = 2.0
134 134 c = MyConfigurable(a=3, config=config)
135 135 self.assertEqual(c.a, 3)
136 136 self.assertEqual(c.b, config.MyConfigurable.b)
137 137 self.assertEqual(c.c, 'no config')
138 138
139 139 def test_override2(self):
140 140 config = Config()
141 141 config.Foo.a = 1
142 142 config.Bar.b = 'or' # Up above b is config=False, so this won't do it.
143 143 config.Bar.c = 10.0
144 144 c = Bar(config=config)
145 145 self.assertEqual(c.a, config.Foo.a)
146 146 self.assertEqual(c.b, 'gotit')
147 147 self.assertEqual(c.c, config.Bar.c)
148 148 c = Bar(a=2, b='and', c=20.0, config=config)
149 149 self.assertEqual(c.a, 2)
150 150 self.assertEqual(c.b, 'and')
151 151 self.assertEqual(c.c, 20.0)
152 152
153 153 def test_help(self):
154 154 self.assertEqual(MyConfigurable.class_get_help(), mc_help)
155 155
156 156 def test_help_inst(self):
157 157 inst = MyConfigurable(a=5, b=4)
158 158 self.assertEqual(MyConfigurable.class_get_help(inst), mc_help_inst)
159 159
160 160
161 161 class TestSingletonConfigurable(TestCase):
162 162
163 163 def test_instance(self):
164 164 from IPython.config.configurable import SingletonConfigurable
165 165 class Foo(SingletonConfigurable): pass
166 166 self.assertEqual(Foo.initialized(), False)
167 167 foo = Foo.instance()
168 168 self.assertEqual(Foo.initialized(), True)
169 169 self.assertEqual(foo, Foo.instance())
170 170 self.assertEqual(SingletonConfigurable._instance, None)
171 171
172 172 def test_inheritance(self):
173 173 class Bar(SingletonConfigurable): pass
174 174 class Bam(Bar): pass
175 175 self.assertEqual(Bar.initialized(), False)
176 176 self.assertEqual(Bam.initialized(), False)
177 177 bam = Bam.instance()
178 178 bam == Bar.instance()
179 179 self.assertEqual(Bar.initialized(), True)
180 180 self.assertEqual(Bam.initialized(), True)
181 181 self.assertEqual(bam, Bam._instance)
182 182 self.assertEqual(bam, Bar._instance)
183 183 self.assertEqual(SingletonConfigurable._instance, None)
184
185
186 class MyParent(Configurable):
187 pass
188
189 class MyParent2(MyParent):
190 pass
191
192 class TestParentConfigurable(TestCase):
193
194 def test_parent_config(self):
195
196 cfg = Config({
197 'MyParent' : {
198 'MyConfigurable' : {
199 'b' : 2.0,
200 }
201 }
202 })
203 parent = MyParent(config=cfg)
204 myc = MyConfigurable(parent=parent)
205 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
206
207 def test_parent_inheritance(self):
208
209 cfg = Config({
210 'MyParent' : {
211 'MyConfigurable' : {
212 'b' : 2.0,
213 }
214 }
215 })
216 parent = MyParent2(config=cfg)
217 myc = MyConfigurable(parent=parent)
218 self.assertEqual(myc.b, parent.config.MyParent.MyConfigurable.b)
219
220 def test_parent_priority(self):
221
222 cfg = Config({
223 'MyConfigurable' : {
224 'b' : 2.0,
225 },
226 'MyParent' : {
227 'MyConfigurable' : {
228 'b' : 3.0,
229 }
230 },
231 'MyParent2' : {
232 'MyConfigurable' : {
233 'b' : 4.0,
234 }
235 }
236 })
237 parent = MyParent2(config=cfg)
238 myc = MyConfigurable(parent=parent)
239 self.assertEqual(myc.b, parent.config.MyParent2.MyConfigurable.b)
General Comments 0
You need to be logged in to leave comments. Login now