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