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