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