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