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